From 09fb156a4e9d512dc5dd4200b3d45cbc280f7607 Mon Sep 17 00:00:00 2001 From: Riccardo Graziosi <31478034+riggraz@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:23:31 +0200 Subject: [PATCH] Add slugs for Posts, Boards and OAuths (#321) --- Gemfile | 3 + Gemfile.lock | 3 + app/controllers/application_controller.rb | 2 +- app/controllers/boards_controller.rb | 4 +- app/controllers/o_auths_controller.rb | 8 +- app/controllers/posts_controller.rb | 3 + app/javascript/actions/Board/deleteBoard.ts | 4 + app/javascript/actions/Board/updateBoard.ts | 2 + app/javascript/components/Board/NewPost.tsx | 4 +- app/javascript/components/Board/PostList.tsx | 1 + .../components/Board/PostListItem.tsx | 6 +- .../SiteSettings/Boards/BoardEditable.tsx | 9 +- .../SiteSettings/Boards/BoardForm.tsx | 32 +++++- .../Boards/BoardsSiteSettingsP.tsx | 6 +- .../containers/BoardsSiteSettings.tsx | 20 +++- app/javascript/interfaces/IBoard.ts | 1 + app/javascript/interfaces/IPost.ts | 1 + app/javascript/interfaces/json/IBoard.ts | 1 + app/javascript/interfaces/json/IPost.ts | 1 + app/javascript/reducers/boardsReducer.ts | 1 + app/javascript/reducers/postReducer.ts | 2 + app/models/board.rb | 10 ++ app/models/o_auth.rb | 18 ++- app/models/post.rb | 3 + config/initializers/friendly_id.rb | 107 ++++++++++++++++++ .../20240404141611_add_slug_to_posts.rb | 7 ++ ...3252_change_slug_post_unique_constraint.rb | 6 + .../20240404153446_add_slug_to_o_auth.rb | 7 ++ .../20240404161306_add_slug_to_boards.rb | 7 ++ db/schema.rb | 8 +- 30 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 config/initializers/friendly_id.rb create mode 100644 db/migrate/20240404141611_add_slug_to_posts.rb create mode 100644 db/migrate/20240404153252_change_slug_post_unique_constraint.rb create mode 100644 db/migrate/20240404153446_add_slug_to_o_auth.rb create mode 100644 db/migrate/20240404161306_add_slug_to_boards.rb diff --git a/Gemfile b/Gemfile index 14a1406f..7d84fe43 100644 --- a/Gemfile +++ b/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] diff --git a/Gemfile.lock b/Gemfile.lock index 41529c5b..e82109f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 9d8d5a88..6cfc7fca 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/boards_controller.rb b/app/controllers/boards_controller.rb index 79d477dd..bc192d93 100644 --- a/app/controllers/boards_controller.rb +++ b/app/controllers/boards_controller.rb @@ -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 diff --git a/app/controllers/o_auths_controller.rb b/app/controllers/o_auths_controller.rb index 3c1c3a0a..f7d3e7fa 100644 --- a/app/controllers/o_auths_controller.rb +++ b/app/controllers/o_auths_controller.rb @@ -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? diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 95c5be32..698839a4 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -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, diff --git a/app/javascript/actions/Board/deleteBoard.ts b/app/javascript/actions/Board/deleteBoard.ts index 47d793e1..1eb7a98b 100644 --- a/app/javascript/actions/Board/deleteBoard.ts +++ b/app/javascript/actions/Board/deleteBoard.ts @@ -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); } } ); \ No newline at end of file diff --git a/app/javascript/actions/Board/updateBoard.ts b/app/javascript/actions/Board/updateBoard.ts index 2cd1f914..4b53ced4 100644 --- a/app/javascript/actions/Board/updateBoard.ts +++ b/app/javascript/actions/Board/updateBoard.ts @@ -48,6 +48,7 @@ export const updateBoard = ( id: number, name: string, description: string, + slug: string, authenticityToken: string, ): ThunkAction> => async (dispatch) => { dispatch(boardUpdateStart()); @@ -60,6 +61,7 @@ export const updateBoard = ( board: { name, description, + slug, }, }), }); diff --git a/app/javascript/components/Board/NewPost.tsx b/app/javascript/components/Board/NewPost.tsx index 5f8434de..328526af 100644 --- a/app/javascript/components/Board/NewPost.tsx +++ b/app/javascript/components/Board/NewPost.tsx @@ -107,7 +107,7 @@ class NewPost extends React.Component { }); const json = await res.json(); this.setState({isLoading: false}); - + if (res.status === HttpStatus.Created) { this.setState({ success: I18n.t('board.new_post.submit_success'), @@ -117,7 +117,7 @@ class NewPost extends React.Component { }); setTimeout(() => ( - window.location.href = `/posts/${json.id}` + window.location.href = `/posts/${json.slug || json.id}` ), 1000); } else { this.setState({error: json.error}); diff --git a/app/javascript/components/Board/PostList.tsx b/app/javascript/components/Board/PostList.tsx index 4484b793..7c39d9d0 100644 --- a/app/javascript/components/Board/PostList.tsx +++ b/app/javascript/components/Board/PostList.tsx @@ -55,6 +55,7 @@ const PostList = ({ postStatus.id === post.postStatusId)} likeCount={post.likeCount} diff --git a/app/javascript/components/Board/PostListItem.tsx b/app/javascript/components/Board/PostListItem.tsx index 15dd4332..a5aa2c23 100644 --- a/app/javascript/components/Board/PostListItem.tsx +++ b/app/javascript/components/Board/PostListItem.tsx @@ -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) => ( -
window.location.href = `/posts/${id}`} className="postListItem"> +
window.location.href = `/posts/${slug || id}`} className="postListItem"> - +
{title} diff --git a/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx b/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx index 4bdc1d0a..b2856306 100644 --- a/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx +++ b/app/javascript/components/SiteSettings/Boards/BoardEditable.tsx @@ -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 { 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 { id, name, description, + slug, index, settingsAreUpdating, handleDelete, @@ -108,6 +112,7 @@ class BoardsEditable extends React.Component { id={id} name={name} description={description} + slug={slug} handleUpdate={this.handleUpdate} /> diff --git a/app/javascript/components/SiteSettings/Boards/BoardForm.tsx b/app/javascript/components/SiteSettings/Boards/BoardForm.tsx index 72c8bbd7..bbb1cad9 100644 --- a/app/javascript/components/SiteSettings/Boards/BoardForm.tsx +++ b/app/javascript/components/SiteSettings/Boards/BoardForm.tsx @@ -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({ mode: 'onChange', defaultValues: { name: name || '', description: description || '', + slug: slug || undefined, }, }); + const formBoardName = watch('name'); + const onSubmit: SubmitHandler = 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' && ( + <> +
+
+
boards/
+
+ +
+ + {errors.slug?.type === 'pattern' && I18n.t('common.validations.url')} + + + )} ); } diff --git a/app/javascript/components/SiteSettings/Boards/BoardsSiteSettingsP.tsx b/app/javascript/components/SiteSettings/Boards/BoardsSiteSettingsP.tsx index 77705701..50d64f97 100644 --- a/app/javascript/components/SiteSettings/Boards/BoardsSiteSettingsP.tsx +++ b/app/javascript/components/SiteSettings/Boards/BoardsSiteSettingsP.tsx @@ -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 { 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 { id={board.id} name={board.name} description={board.description} + slug={board.slug} index={i} settingsAreUpdating={settingsAreUpdating} diff --git a/app/javascript/containers/BoardsSiteSettings.tsx b/app/javascript/containers/BoardsSiteSettings.tsx index 4ed1f76e..91117b14 100644 --- a/app/javascript/containers/BoardsSiteSettings.tsx +++ b/app/javascript/containers/BoardsSiteSettings.tsx @@ -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(); + } + }); }, }); diff --git a/app/javascript/interfaces/IBoard.ts b/app/javascript/interfaces/IBoard.ts index c9fbc4c1..fa11c54e 100644 --- a/app/javascript/interfaces/IBoard.ts +++ b/app/javascript/interfaces/IBoard.ts @@ -2,6 +2,7 @@ interface IBoard { id: number; name: string; description?: string; + slug?: string; } export default IBoard; \ No newline at end of file diff --git a/app/javascript/interfaces/IPost.ts b/app/javascript/interfaces/IPost.ts index 29354369..c8b31b31 100644 --- a/app/javascript/interfaces/IPost.ts +++ b/app/javascript/interfaces/IPost.ts @@ -1,6 +1,7 @@ interface IPost { id: number; title: string; + slug?: string; description?: string; boardId: number; postStatusId?: number; diff --git a/app/javascript/interfaces/json/IBoard.ts b/app/javascript/interfaces/json/IBoard.ts index 813ff5c5..38019965 100644 --- a/app/javascript/interfaces/json/IBoard.ts +++ b/app/javascript/interfaces/json/IBoard.ts @@ -2,6 +2,7 @@ interface IBoardJSON { id: number; name: string; description?: string; + slug?: string; } export default IBoardJSON; \ No newline at end of file diff --git a/app/javascript/interfaces/json/IPost.ts b/app/javascript/interfaces/json/IPost.ts index ee7ce337..cc39d528 100644 --- a/app/javascript/interfaces/json/IPost.ts +++ b/app/javascript/interfaces/json/IPost.ts @@ -1,6 +1,7 @@ interface IPostJSON { id: number; title: string; + slug?: string; description?: string; board_id: number; post_status_id?: number; diff --git a/app/javascript/reducers/boardsReducer.ts b/app/javascript/reducers/boardsReducer.ts index 0eb6755d..8d9ea2f7 100644 --- a/app/javascript/reducers/boardsReducer.ts +++ b/app/javascript/reducers/boardsReducer.ts @@ -63,6 +63,7 @@ const boardsReducer = ( id: board.id, name: board.name, description: board.description, + slug: board.slug, })), areLoading: false, error: '', diff --git a/app/javascript/reducers/postReducer.ts b/app/javascript/reducers/postReducer.ts index 73724464..96fc6aec 100644 --- a/app/javascript/reducers/postReducer.ts +++ b/app/javascript/reducers/postReducer.ts @@ -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, diff --git a/app/models/board.rb b/app/models/board.rb index b1ac1b3f..f12bb303 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -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 diff --git a/app/models/o_auth.rb b/app/models/o_auth.rb index cfb969e1..3cdf5e0c 100644 --- a/app/models/o_auth.rb +++ b/app/models/o_auth.rb @@ -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 diff --git a/app/models/post.rb b/app/models/post.rb index 1af64bbd..8fa1e190 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -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)) diff --git a/config/initializers/friendly_id.rb b/config/initializers/friendly_id.rb new file mode 100644 index 00000000..0c72b97a --- /dev/null +++ b/config/initializers/friendly_id.rb @@ -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? || _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 diff --git a/db/migrate/20240404141611_add_slug_to_posts.rb b/db/migrate/20240404141611_add_slug_to_posts.rb new file mode 100644 index 00000000..273b52b7 --- /dev/null +++ b/db/migrate/20240404141611_add_slug_to_posts.rb @@ -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 diff --git a/db/migrate/20240404153252_change_slug_post_unique_constraint.rb b/db/migrate/20240404153252_change_slug_post_unique_constraint.rb new file mode 100644 index 00000000..16d51e92 --- /dev/null +++ b/db/migrate/20240404153252_change_slug_post_unique_constraint.rb @@ -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 diff --git a/db/migrate/20240404153446_add_slug_to_o_auth.rb b/db/migrate/20240404153446_add_slug_to_o_auth.rb new file mode 100644 index 00000000..ff1b0335 --- /dev/null +++ b/db/migrate/20240404153446_add_slug_to_o_auth.rb @@ -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 diff --git a/db/migrate/20240404161306_add_slug_to_boards.rb b/db/migrate/20240404161306_add_slug_to_boards.rb new file mode 100644 index 00000000..2d4ec6da --- /dev/null +++ b/db/migrate/20240404161306_add_slug_to_boards.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 56a2c401..4a5f8f71 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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