mirror of
https://github.com/astuto/astuto.git
synced 2025-12-15 03:07:52 +01:00
Add slugs for Posts, Boards and OAuths (#321)
This commit is contained in:
committed by
GitHub
parent
e887bca9cf
commit
09fb156a4e
3
Gemfile
3
Gemfile
@@ -44,6 +44,9 @@ gem 'kaminari', '1.2.2'
|
||||
# DDoS protection
|
||||
gem 'rack-attack', '6.7.0'
|
||||
|
||||
# Slugs
|
||||
gem 'friendly_id', '5.5.1'
|
||||
|
||||
group :development, :test do
|
||||
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
||||
|
||||
|
||||
@@ -103,6 +103,8 @@ GEM
|
||||
factory_bot (~> 5.0.2)
|
||||
railties (>= 4.2.0)
|
||||
ffi (1.15.5)
|
||||
friendly_id (5.5.1)
|
||||
activerecord (>= 4.0.0)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
httparty (0.21.0)
|
||||
@@ -286,6 +288,7 @@ DEPENDENCIES
|
||||
cssbundling-rails (= 1.1.2)
|
||||
devise (= 4.7.3)
|
||||
factory_bot_rails (= 5.0.2)
|
||||
friendly_id (= 5.5.1)
|
||||
httparty (= 0.21.0)
|
||||
i18n-js (= 3.9.2)
|
||||
jbuilder (= 2.11.5)
|
||||
|
||||
@@ -35,7 +35,7 @@ class ApplicationController < ActionController::Base
|
||||
# Load tenant data
|
||||
@tenant = Current.tenant_or_raise!
|
||||
@tenant_setting = TenantSetting.first_or_create
|
||||
@boards = Board.select(:id, :name).order(order: :asc)
|
||||
@boards = Board.select(:id, :name, :slug).order(order: :asc)
|
||||
|
||||
# Setup locale
|
||||
I18n.locale = @tenant.locale
|
||||
|
||||
@@ -10,7 +10,7 @@ class BoardsController < ApplicationController
|
||||
end
|
||||
|
||||
def show
|
||||
@board = Board.find(params[:id])
|
||||
@board = Board.friendly.find(params[:id])
|
||||
@page_title = @board.name
|
||||
end
|
||||
|
||||
@@ -80,6 +80,6 @@ class BoardsController < ApplicationController
|
||||
def board_params
|
||||
params
|
||||
.require(:board)
|
||||
.permit(:name, :description)
|
||||
.permit(:name, :description, :slug)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,9 +12,9 @@ class OAuthsController < ApplicationController
|
||||
# Generates authorize url with required parameters and redirects to provider
|
||||
def start
|
||||
if params[:reason] == 'tenantsignup'
|
||||
@o_auth = OAuth.include_only_defaults.find(params[:id])
|
||||
@o_auth = OAuth.include_only_defaults.friendly.find(params[:id])
|
||||
else
|
||||
@o_auth = OAuth.include_defaults.find(params[:id])
|
||||
@o_auth = OAuth.include_defaults.friendly.find(params[:id])
|
||||
end
|
||||
|
||||
return if params[:reason] != 'test' and not @o_auth.is_enabled?
|
||||
@@ -42,9 +42,9 @@ class OAuthsController < ApplicationController
|
||||
Current.tenant ||= Tenant.find_by(subdomain: tenant_domain)
|
||||
|
||||
if reason == 'tenantsignup'
|
||||
@o_auth = OAuth.include_only_defaults.find(params[:id])
|
||||
@o_auth = OAuth.include_only_defaults.friendly.find(params[:id])
|
||||
else
|
||||
@o_auth = OAuth.include_defaults.find(params[:id])
|
||||
@o_auth = OAuth.include_defaults.friendly.find(params[:id])
|
||||
end
|
||||
|
||||
return if reason != 'test' and not @o_auth.is_enabled?
|
||||
|
||||
@@ -9,6 +9,7 @@ class PostsController < ApplicationController
|
||||
.select(
|
||||
:id,
|
||||
:title,
|
||||
:slug,
|
||||
:description,
|
||||
:post_status_id,
|
||||
'COUNT(DISTINCT likes.id) AS likes_count',
|
||||
@@ -48,9 +49,11 @@ class PostsController < ApplicationController
|
||||
|
||||
def show
|
||||
@post = Post
|
||||
.friendly
|
||||
.select(
|
||||
:id,
|
||||
:title,
|
||||
:slug,
|
||||
:description,
|
||||
:board_id,
|
||||
:user_id,
|
||||
|
||||
@@ -62,8 +62,12 @@ export const deleteBoard = (
|
||||
} else {
|
||||
dispatch(boardDeleteFailure(json.error));
|
||||
}
|
||||
|
||||
return Promise.resolve(res);
|
||||
} catch (e) {
|
||||
dispatch(boardDeleteFailure(e));
|
||||
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -48,6 +48,7 @@ export const updateBoard = (
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
slug: string,
|
||||
authenticityToken: string,
|
||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||
dispatch(boardUpdateStart());
|
||||
@@ -60,6 +61,7 @@ export const updateBoard = (
|
||||
board: {
|
||||
name,
|
||||
description,
|
||||
slug,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -117,7 +117,7 @@ class NewPost extends React.Component<Props, State> {
|
||||
});
|
||||
|
||||
setTimeout(() => (
|
||||
window.location.href = `/posts/${json.id}`
|
||||
window.location.href = `/posts/${json.slug || json.id}`
|
||||
), 1000);
|
||||
} else {
|
||||
this.setState({error: json.error});
|
||||
|
||||
@@ -55,6 +55,7 @@ const PostList = ({
|
||||
<PostListItem
|
||||
id={post.id}
|
||||
title={post.title}
|
||||
slug={post.slug}
|
||||
description={post.description}
|
||||
postStatus={postStatuses.find(postStatus => postStatus.id === post.postStatusId)}
|
||||
likeCount={post.likeCount}
|
||||
|
||||
@@ -10,6 +10,7 @@ import IPostStatus from '../../interfaces/IPostStatus';
|
||||
interface Props {
|
||||
id: number;
|
||||
title: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
postStatus: IPostStatus;
|
||||
likeCount: number;
|
||||
@@ -25,6 +26,7 @@ interface Props {
|
||||
const PostListItem = ({
|
||||
id,
|
||||
title,
|
||||
slug,
|
||||
description,
|
||||
postStatus,
|
||||
likeCount,
|
||||
@@ -36,7 +38,7 @@ const PostListItem = ({
|
||||
isLoggedIn,
|
||||
authenticityToken,
|
||||
}: Props) => (
|
||||
<div onClick={() => window.location.href = `/posts/${id}`} className="postListItem">
|
||||
<div onClick={() => window.location.href = `/posts/${slug || id}`} className="postListItem">
|
||||
<LikeButton
|
||||
postId={id}
|
||||
likeCount={likeCount}
|
||||
|
||||
@@ -14,13 +14,15 @@ interface Props {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
index: number;
|
||||
settingsAreUpdating: boolean;
|
||||
|
||||
handleUpdate(
|
||||
id: number,
|
||||
description: string,
|
||||
name: string,
|
||||
description: string,
|
||||
slug: string,
|
||||
onSuccess: Function,
|
||||
): void;
|
||||
handleDelete(id: number): void;
|
||||
@@ -46,11 +48,12 @@ class BoardsEditable extends React.Component<Props, State> {
|
||||
this.setState({editMode: !this.state.editMode});
|
||||
}
|
||||
|
||||
handleUpdate(id: number, name: string, description: string) {
|
||||
handleUpdate(id: number, name: string, description: string, slug: string) {
|
||||
this.props.handleUpdate(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
slug,
|
||||
() => this.setState({editMode: false}),
|
||||
);
|
||||
}
|
||||
@@ -60,6 +63,7 @@ class BoardsEditable extends React.Component<Props, State> {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
slug,
|
||||
index,
|
||||
settingsAreUpdating,
|
||||
handleDelete,
|
||||
@@ -108,6 +112,7 @@ class BoardsEditable extends React.Component<Props, State> {
|
||||
id={id}
|
||||
name={name}
|
||||
description={description}
|
||||
slug={slug}
|
||||
handleUpdate={this.handleUpdate}
|
||||
/>
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import I18n from 'i18n-js';
|
||||
|
||||
import Button from '../../common/Button';
|
||||
import { DangerText } from '../../common/CustomTexts';
|
||||
|
||||
interface Props {
|
||||
mode: 'create' | 'update';
|
||||
@@ -10,6 +11,7 @@ interface Props {
|
||||
id?: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
|
||||
handleCreate?(
|
||||
name: string,
|
||||
@@ -20,12 +22,14 @@ interface Props {
|
||||
id: number,
|
||||
name: string,
|
||||
description?: string,
|
||||
slug?: string,
|
||||
): void;
|
||||
}
|
||||
|
||||
interface IBoardForm {
|
||||
name: string;
|
||||
description: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
const BoardForm = ({
|
||||
@@ -33,6 +37,7 @@ const BoardForm = ({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
slug,
|
||||
handleCreate,
|
||||
handleUpdate,
|
||||
}: Props) => {
|
||||
@@ -40,15 +45,19 @@ const BoardForm = ({
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isValid },
|
||||
formState: { isValid, errors },
|
||||
watch,
|
||||
} = useForm<IBoardForm>({
|
||||
mode: 'onChange',
|
||||
defaultValues: {
|
||||
name: name || '',
|
||||
description: description || '',
|
||||
slug: slug || undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const formBoardName = watch('name');
|
||||
|
||||
const onSubmit: SubmitHandler<IBoardForm> = data => {
|
||||
if (mode === 'create') {
|
||||
handleCreate(
|
||||
@@ -57,7 +66,7 @@ const BoardForm = ({
|
||||
() => reset({ name: '', description: '' })
|
||||
);
|
||||
} else {
|
||||
handleUpdate(id, data.name, data.description);
|
||||
handleUpdate(id, data.name, data.description, data.slug);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +100,25 @@ const BoardForm = ({
|
||||
placeholder={I18n.t('site_settings.boards.form.description')}
|
||||
className="formControl"
|
||||
/>
|
||||
|
||||
{mode === 'update' && (
|
||||
<>
|
||||
<div className="input-group">
|
||||
<div className="input-group-prepend">
|
||||
<div className="input-group-text">boards/</div>
|
||||
</div>
|
||||
<input
|
||||
{...register('slug', { pattern: /^[a-zA-Z0-9-]+$/ })}
|
||||
type="text"
|
||||
placeholder={formBoardName.trim().replace(/\s/g, '-').toLowerCase()}
|
||||
className="formControl"
|
||||
/>
|
||||
</div>
|
||||
<DangerText>
|
||||
{errors.slug?.type === 'pattern' && I18n.t('common.validations.url')}
|
||||
</DangerText>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ interface Props {
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
slug: string,
|
||||
onSuccess: Function,
|
||||
authenticityToken: string,
|
||||
): void;
|
||||
@@ -61,8 +62,8 @@ class BoardsSiteSettingsP extends React.Component<Props> {
|
||||
this.props.submitBoard(name, description, onSuccess, this.props.authenticityToken);
|
||||
}
|
||||
|
||||
handleUpdate(id: number, name: string, description: string, onSuccess: Function) {
|
||||
this.props.updateBoard(id, name, description, onSuccess, this.props.authenticityToken);
|
||||
handleUpdate(id: number, name: string, description: string, slug: string, onSuccess: Function) {
|
||||
this.props.updateBoard(id, name, description, slug, onSuccess, this.props.authenticityToken);
|
||||
}
|
||||
|
||||
handleDragEnd(result) {
|
||||
@@ -105,6 +106,7 @@ class BoardsSiteSettingsP extends React.Component<Props> {
|
||||
id={board.id}
|
||||
name={board.name}
|
||||
description={board.description}
|
||||
slug={board.slug}
|
||||
index={i}
|
||||
settingsAreUpdating={settingsAreUpdating}
|
||||
|
||||
|
||||
@@ -28,7 +28,10 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
authenticityToken: string,
|
||||
) {
|
||||
dispatch(submitBoard(name, description, authenticityToken)).then(res => {
|
||||
if (res && res.status === HttpStatus.Created) onSuccess();
|
||||
if (res && res.status === HttpStatus.Created) {
|
||||
onSuccess();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -36,11 +39,15 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
id: number,
|
||||
name: string,
|
||||
description: string,
|
||||
slug: string,
|
||||
onSuccess: Function,
|
||||
authenticityToken: string,
|
||||
) {
|
||||
dispatch(updateBoard(id, name, description, authenticityToken)).then(res => {
|
||||
if (res && res.status === HttpStatus.OK) onSuccess();
|
||||
dispatch(updateBoard(id, name, description, slug, authenticityToken)).then(res => {
|
||||
if (res && res.status === HttpStatus.OK) {
|
||||
onSuccess();
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -54,7 +61,12 @@ const mapDispatchToProps = (dispatch: any) => ({
|
||||
},
|
||||
|
||||
deleteBoard(id: number, authenticityToken: string) {
|
||||
dispatch(deleteBoard(id, authenticityToken));
|
||||
dispatch(deleteBoard(id, authenticityToken)).then(res => {
|
||||
console.log(res);
|
||||
if (res && res.status === HttpStatus.Accepted) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ interface IBoard {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export default IBoard;
|
||||
@@ -1,6 +1,7 @@
|
||||
interface IPost {
|
||||
id: number;
|
||||
title: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
boardId: number;
|
||||
postStatusId?: number;
|
||||
|
||||
@@ -2,6 +2,7 @@ interface IBoardJSON {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
slug?: string;
|
||||
}
|
||||
|
||||
export default IBoardJSON;
|
||||
@@ -1,6 +1,7 @@
|
||||
interface IPostJSON {
|
||||
id: number;
|
||||
title: string;
|
||||
slug?: string;
|
||||
description?: string;
|
||||
board_id: number;
|
||||
post_status_id?: number;
|
||||
|
||||
@@ -63,6 +63,7 @@ const boardsReducer = (
|
||||
id: board.id,
|
||||
name: board.name,
|
||||
description: board.description,
|
||||
slug: board.slug,
|
||||
})),
|
||||
areLoading: false,
|
||||
error: '',
|
||||
|
||||
@@ -13,6 +13,7 @@ import IPost from '../interfaces/IPost';
|
||||
const initialState: IPost = {
|
||||
id: 0,
|
||||
title: '',
|
||||
slug: null,
|
||||
description: null,
|
||||
boardId: 0,
|
||||
postStatusId: null,
|
||||
@@ -37,6 +38,7 @@ const postReducer = (
|
||||
return {
|
||||
id: action.post.id,
|
||||
title: action.post.title,
|
||||
slug: action.post.slug,
|
||||
description: action.post.description,
|
||||
boardId: action.post.board_id,
|
||||
postStatusId: action.post.post_status_id,
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
class Board < ApplicationRecord
|
||||
include TenantOwnable
|
||||
include Orderable
|
||||
extend FriendlyId
|
||||
|
||||
has_many :posts, dependent: :destroy
|
||||
|
||||
before_save :sanitize_slug
|
||||
|
||||
validates :name, presence: true, uniqueness: { scope: :tenant_id }
|
||||
validates :description, length: { in: 0..1024 }, allow_nil: true
|
||||
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
def sanitize_slug
|
||||
self.slug = self.slug.parameterize
|
||||
self.slug = nil if self.slug.blank? # friendly_id will generate a slug if it's nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
class OAuth < ApplicationRecord
|
||||
include TenantOwnable
|
||||
include ApplicationHelper
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
include TenantOwnable
|
||||
extend FriendlyId
|
||||
|
||||
has_many :tenant_default_o_auths, dependent: :destroy
|
||||
|
||||
attr_accessor :state
|
||||
@@ -17,6 +19,8 @@ class OAuth < ApplicationRecord
|
||||
validates :scope, presence: true
|
||||
validates :json_user_email_path, presence: true
|
||||
|
||||
friendly_id :generate_random_slug, use: :slugged
|
||||
|
||||
def is_default?
|
||||
tenant_id == nil
|
||||
end
|
||||
@@ -27,9 +31,9 @@ class OAuth < ApplicationRecord
|
||||
# for this reason, we don't preprend tenant subdomain
|
||||
# but rather use the "login" subdomain
|
||||
if self.is_default?
|
||||
get_url_for(method(:o_auth_callback_url), resource: id, disallow_custom_domain: true, options: { subdomain: "login", host: Rails.application.base_url })
|
||||
get_url_for(method(:o_auth_callback_url), resource: self, disallow_custom_domain: true, options: { subdomain: "login", host: Rails.application.base_url })
|
||||
else
|
||||
get_url_for(method(:o_auth_callback_url), resource: id, disallow_custom_domain: true)
|
||||
get_url_for(method(:o_auth_callback_url), resource: self, disallow_custom_domain: true)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,6 +50,14 @@ class OAuth < ApplicationRecord
|
||||
is_default? and tenant_default_o_auths.exists?
|
||||
end
|
||||
|
||||
def generate_random_slug
|
||||
loop do
|
||||
self.slug = SecureRandom.hex(8)
|
||||
break unless self.class.exists?(slug: slug)
|
||||
end
|
||||
slug
|
||||
end
|
||||
|
||||
class << self
|
||||
# returns all tenant-specific o_auths plus all default o_auths that are enabled site-wide
|
||||
def include_all_defaults
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
class Post < ApplicationRecord
|
||||
include TenantOwnable
|
||||
extend FriendlyId
|
||||
|
||||
belongs_to :board
|
||||
belongs_to :user
|
||||
@@ -15,6 +16,8 @@ class Post < ApplicationRecord
|
||||
|
||||
paginates_per Rails.application.posts_per_page
|
||||
|
||||
friendly_id :title, use: :slugged
|
||||
|
||||
class << self
|
||||
def find_with_post_status_in(post_statuses)
|
||||
where(post_status_id: post_statuses.pluck(:id))
|
||||
|
||||
107
config/initializers/friendly_id.rb
Normal file
107
config/initializers/friendly_id.rb
Normal file
@@ -0,0 +1,107 @@
|
||||
# FriendlyId Global Configuration
|
||||
#
|
||||
# Use this to set up shared configuration options for your entire application.
|
||||
# Any of the configuration options shown here can also be applied to single
|
||||
# models by passing arguments to the `friendly_id` class method or defining
|
||||
# methods in your model.
|
||||
#
|
||||
# To learn more, check out the guide:
|
||||
#
|
||||
# http://norman.github.io/friendly_id/file.Guide.html
|
||||
|
||||
FriendlyId.defaults do |config|
|
||||
# ## Reserved Words
|
||||
#
|
||||
# Some words could conflict with Rails's routes when used as slugs, or are
|
||||
# undesirable to allow as slugs. Edit this list as needed for your app.
|
||||
config.use :reserved
|
||||
|
||||
config.reserved_words = %w[new edit index session login logout users admin
|
||||
stylesheets assets javascripts images]
|
||||
|
||||
# This adds an option to treat reserved words as conflicts rather than exceptions.
|
||||
# When there is no good candidate, a UUID will be appended, matching the existing
|
||||
# conflict behavior.
|
||||
|
||||
config.treat_reserved_as_conflict = true
|
||||
|
||||
# ## Friendly Finders
|
||||
#
|
||||
# Uncomment this to use friendly finders in all models. By default, if
|
||||
# you wish to find a record by its friendly id, you must do:
|
||||
#
|
||||
# MyModel.friendly.find('foo')
|
||||
#
|
||||
# If you uncomment this, you can do:
|
||||
#
|
||||
# MyModel.find('foo')
|
||||
#
|
||||
# This is significantly more convenient but may not be appropriate for
|
||||
# all applications, so you must explicitly opt-in to this behavior. You can
|
||||
# always also configure it on a per-model basis if you prefer.
|
||||
#
|
||||
# Something else to consider is that using the :finders addon boosts
|
||||
# performance because it will avoid Rails-internal code that makes runtime
|
||||
# calls to `Module.extend`.
|
||||
#
|
||||
# config.use :finders
|
||||
#
|
||||
# ## Slugs
|
||||
#
|
||||
# Most applications will use the :slugged module everywhere. If you wish
|
||||
# to do so, uncomment the following line.
|
||||
#
|
||||
# config.use :slugged
|
||||
#
|
||||
# By default, FriendlyId's :slugged addon expects the slug column to be named
|
||||
# 'slug', but you can change it if you wish.
|
||||
#
|
||||
# config.slug_column = 'slug'
|
||||
#
|
||||
# By default, slug has no size limit, but you can change it if you wish.
|
||||
#
|
||||
# config.slug_limit = 128
|
||||
#
|
||||
# When FriendlyId can not generate a unique ID from your base method, it appends
|
||||
# a UUID, separated by a single dash. You can configure the character used as the
|
||||
# separator. If you're upgrading from FriendlyId 4, you may wish to replace this
|
||||
# with two dashes.
|
||||
#
|
||||
# config.sequence_separator = '-'
|
||||
#
|
||||
# Note that you must use the :slugged addon **prior** to the line which
|
||||
# configures the sequence separator, or else FriendlyId will raise an undefined
|
||||
# method error.
|
||||
#
|
||||
# ## Tips and Tricks
|
||||
#
|
||||
# ### Controlling when slugs are generated
|
||||
#
|
||||
# As of FriendlyId 5.0, new slugs are generated only when the slug field is
|
||||
# nil, but if you're using a column as your base method can change this
|
||||
# behavior by overriding the `should_generate_new_friendly_id?` method that
|
||||
# FriendlyId adds to your model. The change below makes FriendlyId 5.0 behave
|
||||
# more like 4.0.
|
||||
# Note: Use(include) Slugged module in the config if using the anonymous module.
|
||||
# If you have `friendly_id :name, use: slugged` in the model, Slugged module
|
||||
# is included after the anonymous module defined in the initializer, so it
|
||||
# overrides the `should_generate_new_friendly_id?` method from the anonymous module.
|
||||
#
|
||||
# config.use :slugged
|
||||
# config.use Module.new {
|
||||
# def should_generate_new_friendly_id?
|
||||
# slug.blank? || <your_column_name_here>_changed?
|
||||
# end
|
||||
# }
|
||||
#
|
||||
# FriendlyId uses Rails's `parameterize` method to generate slugs, but for
|
||||
# languages that don't use the Roman alphabet, that's not usually sufficient.
|
||||
# Here we use the Babosa library to transliterate Russian Cyrillic slugs to
|
||||
# ASCII. If you use this, don't forget to add "babosa" to your Gemfile.
|
||||
#
|
||||
# config.use Module.new {
|
||||
# def normalize_friendly_id(text)
|
||||
# text.to_slug.normalize! :transliterations => [:russian, :latin]
|
||||
# end
|
||||
# }
|
||||
end
|
||||
7
db/migrate/20240404141611_add_slug_to_posts.rb
Normal file
7
db/migrate/20240404141611_add_slug_to_posts.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class AddSlugToPosts < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :posts, :slug, :string
|
||||
|
||||
add_index :posts, :slug, unique: true
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
class ChangeSlugPostUniqueConstraint < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
remove_index :posts, :slug
|
||||
add_index :posts, [:slug, :tenant_id], unique: true
|
||||
end
|
||||
end
|
||||
7
db/migrate/20240404153446_add_slug_to_o_auth.rb
Normal file
7
db/migrate/20240404153446_add_slug_to_o_auth.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class AddSlugToOAuth < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :o_auths, :slug, :string
|
||||
|
||||
add_index :o_auths, [:slug, :tenant_id], unique: true
|
||||
end
|
||||
end
|
||||
7
db/migrate/20240404161306_add_slug_to_boards.rb
Normal file
7
db/migrate/20240404161306_add_slug_to_boards.rb
Normal file
@@ -0,0 +1,7 @@
|
||||
class AddSlugToBoards < ActiveRecord::Migration[6.1]
|
||||
def change
|
||||
add_column :boards, :slug, :string
|
||||
|
||||
add_index :boards, [:slug, :tenant_id], unique: true
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2024_03_21_171022) do
|
||||
ActiveRecord::Schema.define(version: 2024_04_04_161306) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
@@ -22,7 +22,9 @@ ActiveRecord::Schema.define(version: 2024_03_21_171022) do
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.integer "order", null: false
|
||||
t.bigint "tenant_id", null: false
|
||||
t.string "slug"
|
||||
t.index ["name", "tenant_id"], name: "index_boards_on_name_and_tenant_id", unique: true
|
||||
t.index ["slug", "tenant_id"], name: "index_boards_on_slug_and_tenant_id", unique: true
|
||||
t.index ["tenant_id"], name: "index_boards_on_tenant_id"
|
||||
end
|
||||
|
||||
@@ -80,7 +82,9 @@ ActiveRecord::Schema.define(version: 2024_03_21_171022) do
|
||||
t.bigint "tenant_id"
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.string "slug"
|
||||
t.index ["name", "tenant_id"], name: "index_o_auths_on_name_and_tenant_id", unique: true
|
||||
t.index ["slug", "tenant_id"], name: "index_o_auths_on_slug_and_tenant_id", unique: true
|
||||
t.index ["tenant_id"], name: "index_o_auths_on_tenant_id"
|
||||
end
|
||||
|
||||
@@ -118,8 +122,10 @@ ActiveRecord::Schema.define(version: 2024_03_21_171022) do
|
||||
t.datetime "created_at", precision: 6, null: false
|
||||
t.datetime "updated_at", precision: 6, null: false
|
||||
t.bigint "tenant_id", null: false
|
||||
t.string "slug"
|
||||
t.index ["board_id"], name: "index_posts_on_board_id"
|
||||
t.index ["post_status_id"], name: "index_posts_on_post_status_id"
|
||||
t.index ["slug", "tenant_id"], name: "index_posts_on_slug_and_tenant_id", unique: true
|
||||
t.index ["tenant_id"], name: "index_posts_on_tenant_id"
|
||||
t.index ["user_id"], name: "index_posts_on_user_id"
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user