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
|
# DDoS protection
|
||||||
gem 'rack-attack', '6.7.0'
|
gem 'rack-attack', '6.7.0'
|
||||||
|
|
||||||
|
# Slugs
|
||||||
|
gem 'friendly_id', '5.5.1'
|
||||||
|
|
||||||
group :development, :test do
|
group :development, :test do
|
||||||
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ GEM
|
|||||||
factory_bot (~> 5.0.2)
|
factory_bot (~> 5.0.2)
|
||||||
railties (>= 4.2.0)
|
railties (>= 4.2.0)
|
||||||
ffi (1.15.5)
|
ffi (1.15.5)
|
||||||
|
friendly_id (5.5.1)
|
||||||
|
activerecord (>= 4.0.0)
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
httparty (0.21.0)
|
httparty (0.21.0)
|
||||||
@@ -286,6 +288,7 @@ DEPENDENCIES
|
|||||||
cssbundling-rails (= 1.1.2)
|
cssbundling-rails (= 1.1.2)
|
||||||
devise (= 4.7.3)
|
devise (= 4.7.3)
|
||||||
factory_bot_rails (= 5.0.2)
|
factory_bot_rails (= 5.0.2)
|
||||||
|
friendly_id (= 5.5.1)
|
||||||
httparty (= 0.21.0)
|
httparty (= 0.21.0)
|
||||||
i18n-js (= 3.9.2)
|
i18n-js (= 3.9.2)
|
||||||
jbuilder (= 2.11.5)
|
jbuilder (= 2.11.5)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ApplicationController < ActionController::Base
|
|||||||
# Load tenant data
|
# Load tenant data
|
||||||
@tenant = Current.tenant_or_raise!
|
@tenant = Current.tenant_or_raise!
|
||||||
@tenant_setting = TenantSetting.first_or_create
|
@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
|
# Setup locale
|
||||||
I18n.locale = @tenant.locale
|
I18n.locale = @tenant.locale
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class BoardsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@board = Board.find(params[:id])
|
@board = Board.friendly.find(params[:id])
|
||||||
@page_title = @board.name
|
@page_title = @board.name
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,6 +80,6 @@ class BoardsController < ApplicationController
|
|||||||
def board_params
|
def board_params
|
||||||
params
|
params
|
||||||
.require(:board)
|
.require(:board)
|
||||||
.permit(:name, :description)
|
.permit(:name, :description, :slug)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ class OAuthsController < ApplicationController
|
|||||||
# Generates authorize url with required parameters and redirects to provider
|
# Generates authorize url with required parameters and redirects to provider
|
||||||
def start
|
def start
|
||||||
if params[:reason] == 'tenantsignup'
|
if params[:reason] == 'tenantsignup'
|
||||||
@o_auth = OAuth.include_only_defaults.find(params[:id])
|
@o_auth = OAuth.include_only_defaults.friendly.find(params[:id])
|
||||||
else
|
else
|
||||||
@o_auth = OAuth.include_defaults.find(params[:id])
|
@o_auth = OAuth.include_defaults.friendly.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
return if params[:reason] != 'test' and not @o_auth.is_enabled?
|
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)
|
Current.tenant ||= Tenant.find_by(subdomain: tenant_domain)
|
||||||
|
|
||||||
if reason == 'tenantsignup'
|
if reason == 'tenantsignup'
|
||||||
@o_auth = OAuth.include_only_defaults.find(params[:id])
|
@o_auth = OAuth.include_only_defaults.friendly.find(params[:id])
|
||||||
else
|
else
|
||||||
@o_auth = OAuth.include_defaults.find(params[:id])
|
@o_auth = OAuth.include_defaults.friendly.find(params[:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
return if reason != 'test' and not @o_auth.is_enabled?
|
return if reason != 'test' and not @o_auth.is_enabled?
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class PostsController < ApplicationController
|
|||||||
.select(
|
.select(
|
||||||
:id,
|
:id,
|
||||||
:title,
|
:title,
|
||||||
|
:slug,
|
||||||
:description,
|
:description,
|
||||||
:post_status_id,
|
:post_status_id,
|
||||||
'COUNT(DISTINCT likes.id) AS likes_count',
|
'COUNT(DISTINCT likes.id) AS likes_count',
|
||||||
@@ -48,9 +49,11 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
def show
|
def show
|
||||||
@post = Post
|
@post = Post
|
||||||
|
.friendly
|
||||||
.select(
|
.select(
|
||||||
:id,
|
:id,
|
||||||
:title,
|
:title,
|
||||||
|
:slug,
|
||||||
:description,
|
:description,
|
||||||
:board_id,
|
:board_id,
|
||||||
:user_id,
|
:user_id,
|
||||||
|
|||||||
@@ -62,8 +62,12 @@ export const deleteBoard = (
|
|||||||
} else {
|
} else {
|
||||||
dispatch(boardDeleteFailure(json.error));
|
dispatch(boardDeleteFailure(json.error));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(res);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch(boardDeleteFailure(e));
|
dispatch(boardDeleteFailure(e));
|
||||||
|
|
||||||
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -48,6 +48,7 @@ export const updateBoard = (
|
|||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
|
slug: string,
|
||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
|
||||||
dispatch(boardUpdateStart());
|
dispatch(boardUpdateStart());
|
||||||
@@ -60,6 +61,7 @@ export const updateBoard = (
|
|||||||
board: {
|
board: {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
slug,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class NewPost extends React.Component<Props, State> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => (
|
setTimeout(() => (
|
||||||
window.location.href = `/posts/${json.id}`
|
window.location.href = `/posts/${json.slug || json.id}`
|
||||||
), 1000);
|
), 1000);
|
||||||
} else {
|
} else {
|
||||||
this.setState({error: json.error});
|
this.setState({error: json.error});
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const PostList = ({
|
|||||||
<PostListItem
|
<PostListItem
|
||||||
id={post.id}
|
id={post.id}
|
||||||
title={post.title}
|
title={post.title}
|
||||||
|
slug={post.slug}
|
||||||
description={post.description}
|
description={post.description}
|
||||||
postStatus={postStatuses.find(postStatus => postStatus.id === post.postStatusId)}
|
postStatus={postStatuses.find(postStatus => postStatus.id === post.postStatusId)}
|
||||||
likeCount={post.likeCount}
|
likeCount={post.likeCount}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import IPostStatus from '../../interfaces/IPostStatus';
|
|||||||
interface Props {
|
interface Props {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
postStatus: IPostStatus;
|
postStatus: IPostStatus;
|
||||||
likeCount: number;
|
likeCount: number;
|
||||||
@@ -25,6 +26,7 @@ interface Props {
|
|||||||
const PostListItem = ({
|
const PostListItem = ({
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
slug,
|
||||||
description,
|
description,
|
||||||
postStatus,
|
postStatus,
|
||||||
likeCount,
|
likeCount,
|
||||||
@@ -36,7 +38,7 @@ const PostListItem = ({
|
|||||||
isLoggedIn,
|
isLoggedIn,
|
||||||
authenticityToken,
|
authenticityToken,
|
||||||
}: Props) => (
|
}: Props) => (
|
||||||
<div onClick={() => window.location.href = `/posts/${id}`} className="postListItem">
|
<div onClick={() => window.location.href = `/posts/${slug || id}`} className="postListItem">
|
||||||
<LikeButton
|
<LikeButton
|
||||||
postId={id}
|
postId={id}
|
||||||
likeCount={likeCount}
|
likeCount={likeCount}
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ interface Props {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
slug?: string;
|
||||||
index: number;
|
index: number;
|
||||||
settingsAreUpdating: boolean;
|
settingsAreUpdating: boolean;
|
||||||
|
|
||||||
handleUpdate(
|
handleUpdate(
|
||||||
id: number,
|
id: number,
|
||||||
description: string,
|
|
||||||
name: string,
|
name: string,
|
||||||
|
description: string,
|
||||||
|
slug: string,
|
||||||
onSuccess: Function,
|
onSuccess: Function,
|
||||||
): void;
|
): void;
|
||||||
handleDelete(id: number): void;
|
handleDelete(id: number): void;
|
||||||
@@ -46,11 +48,12 @@ class BoardsEditable extends React.Component<Props, State> {
|
|||||||
this.setState({editMode: !this.state.editMode});
|
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(
|
this.props.handleUpdate(
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
slug,
|
||||||
() => this.setState({editMode: false}),
|
() => this.setState({editMode: false}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -60,6 +63,7 @@ class BoardsEditable extends React.Component<Props, State> {
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
slug,
|
||||||
index,
|
index,
|
||||||
settingsAreUpdating,
|
settingsAreUpdating,
|
||||||
handleDelete,
|
handleDelete,
|
||||||
@@ -108,6 +112,7 @@ class BoardsEditable extends React.Component<Props, State> {
|
|||||||
id={id}
|
id={id}
|
||||||
name={name}
|
name={name}
|
||||||
description={description}
|
description={description}
|
||||||
|
slug={slug}
|
||||||
handleUpdate={this.handleUpdate}
|
handleUpdate={this.handleUpdate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useForm, SubmitHandler } from 'react-hook-form';
|
|||||||
import I18n from 'i18n-js';
|
import I18n from 'i18n-js';
|
||||||
|
|
||||||
import Button from '../../common/Button';
|
import Button from '../../common/Button';
|
||||||
|
import { DangerText } from '../../common/CustomTexts';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mode: 'create' | 'update';
|
mode: 'create' | 'update';
|
||||||
@@ -10,6 +11,7 @@ interface Props {
|
|||||||
id?: number;
|
id?: number;
|
||||||
name?: string;
|
name?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
slug?: string;
|
||||||
|
|
||||||
handleCreate?(
|
handleCreate?(
|
||||||
name: string,
|
name: string,
|
||||||
@@ -20,12 +22,14 @@ interface Props {
|
|||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
|
slug?: string,
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IBoardForm {
|
interface IBoardForm {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BoardForm = ({
|
const BoardForm = ({
|
||||||
@@ -33,6 +37,7 @@ const BoardForm = ({
|
|||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
slug,
|
||||||
handleCreate,
|
handleCreate,
|
||||||
handleUpdate,
|
handleUpdate,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
@@ -40,15 +45,19 @@ const BoardForm = ({
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isValid },
|
formState: { isValid, errors },
|
||||||
|
watch,
|
||||||
} = useForm<IBoardForm>({
|
} = useForm<IBoardForm>({
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: name || '',
|
name: name || '',
|
||||||
description: description || '',
|
description: description || '',
|
||||||
|
slug: slug || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formBoardName = watch('name');
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<IBoardForm> = data => {
|
const onSubmit: SubmitHandler<IBoardForm> = data => {
|
||||||
if (mode === 'create') {
|
if (mode === 'create') {
|
||||||
handleCreate(
|
handleCreate(
|
||||||
@@ -57,7 +66,7 @@ const BoardForm = ({
|
|||||||
() => reset({ name: '', description: '' })
|
() => reset({ name: '', description: '' })
|
||||||
);
|
);
|
||||||
} else {
|
} 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')}
|
placeholder={I18n.t('site_settings.boards.form.description')}
|
||||||
className="formControl"
|
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>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ interface Props {
|
|||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
|
slug: string,
|
||||||
onSuccess: Function,
|
onSuccess: Function,
|
||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
): void;
|
): void;
|
||||||
@@ -61,8 +62,8 @@ class BoardsSiteSettingsP extends React.Component<Props> {
|
|||||||
this.props.submitBoard(name, description, onSuccess, this.props.authenticityToken);
|
this.props.submitBoard(name, description, onSuccess, this.props.authenticityToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdate(id: number, name: string, description: string, onSuccess: Function) {
|
handleUpdate(id: number, name: string, description: string, slug: string, onSuccess: Function) {
|
||||||
this.props.updateBoard(id, name, description, onSuccess, this.props.authenticityToken);
|
this.props.updateBoard(id, name, description, slug, onSuccess, this.props.authenticityToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleDragEnd(result) {
|
handleDragEnd(result) {
|
||||||
@@ -105,6 +106,7 @@ class BoardsSiteSettingsP extends React.Component<Props> {
|
|||||||
id={board.id}
|
id={board.id}
|
||||||
name={board.name}
|
name={board.name}
|
||||||
description={board.description}
|
description={board.description}
|
||||||
|
slug={board.slug}
|
||||||
index={i}
|
index={i}
|
||||||
settingsAreUpdating={settingsAreUpdating}
|
settingsAreUpdating={settingsAreUpdating}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ const mapDispatchToProps = (dispatch: any) => ({
|
|||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
) {
|
) {
|
||||||
dispatch(submitBoard(name, description, authenticityToken)).then(res => {
|
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,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
description: string,
|
description: string,
|
||||||
|
slug: string,
|
||||||
onSuccess: Function,
|
onSuccess: Function,
|
||||||
authenticityToken: string,
|
authenticityToken: string,
|
||||||
) {
|
) {
|
||||||
dispatch(updateBoard(id, name, description, authenticityToken)).then(res => {
|
dispatch(updateBoard(id, name, description, slug, authenticityToken)).then(res => {
|
||||||
if (res && res.status === HttpStatus.OK) onSuccess();
|
if (res && res.status === HttpStatus.OK) {
|
||||||
|
onSuccess();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -54,7 +61,12 @@ const mapDispatchToProps = (dispatch: any) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
deleteBoard(id: number, authenticityToken: string) {
|
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;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IBoard;
|
export default IBoard;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
interface IPost {
|
interface IPost {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
boardId: number;
|
boardId: number;
|
||||||
postStatusId?: number;
|
postStatusId?: number;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ interface IBoardJSON {
|
|||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default IBoardJSON;
|
export default IBoardJSON;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
interface IPostJSON {
|
interface IPostJSON {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
|
slug?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
board_id: number;
|
board_id: number;
|
||||||
post_status_id?: number;
|
post_status_id?: number;
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const boardsReducer = (
|
|||||||
id: board.id,
|
id: board.id,
|
||||||
name: board.name,
|
name: board.name,
|
||||||
description: board.description,
|
description: board.description,
|
||||||
|
slug: board.slug,
|
||||||
})),
|
})),
|
||||||
areLoading: false,
|
areLoading: false,
|
||||||
error: '',
|
error: '',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import IPost from '../interfaces/IPost';
|
|||||||
const initialState: IPost = {
|
const initialState: IPost = {
|
||||||
id: 0,
|
id: 0,
|
||||||
title: '',
|
title: '',
|
||||||
|
slug: null,
|
||||||
description: null,
|
description: null,
|
||||||
boardId: 0,
|
boardId: 0,
|
||||||
postStatusId: null,
|
postStatusId: null,
|
||||||
@@ -37,6 +38,7 @@ const postReducer = (
|
|||||||
return {
|
return {
|
||||||
id: action.post.id,
|
id: action.post.id,
|
||||||
title: action.post.title,
|
title: action.post.title,
|
||||||
|
slug: action.post.slug,
|
||||||
description: action.post.description,
|
description: action.post.description,
|
||||||
boardId: action.post.board_id,
|
boardId: action.post.board_id,
|
||||||
postStatusId: action.post.post_status_id,
|
postStatusId: action.post.post_status_id,
|
||||||
|
|||||||
@@ -1,9 +1,19 @@
|
|||||||
class Board < ApplicationRecord
|
class Board < ApplicationRecord
|
||||||
include TenantOwnable
|
include TenantOwnable
|
||||||
include Orderable
|
include Orderable
|
||||||
|
extend FriendlyId
|
||||||
|
|
||||||
has_many :posts, dependent: :destroy
|
has_many :posts, dependent: :destroy
|
||||||
|
|
||||||
|
before_save :sanitize_slug
|
||||||
|
|
||||||
validates :name, presence: true, uniqueness: { scope: :tenant_id }
|
validates :name, presence: true, uniqueness: { scope: :tenant_id }
|
||||||
validates :description, length: { in: 0..1024 }, allow_nil: true
|
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
|
end
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
class OAuth < ApplicationRecord
|
class OAuth < ApplicationRecord
|
||||||
include TenantOwnable
|
|
||||||
include ApplicationHelper
|
include ApplicationHelper
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
|
||||||
|
include TenantOwnable
|
||||||
|
extend FriendlyId
|
||||||
|
|
||||||
has_many :tenant_default_o_auths, dependent: :destroy
|
has_many :tenant_default_o_auths, dependent: :destroy
|
||||||
|
|
||||||
attr_accessor :state
|
attr_accessor :state
|
||||||
@@ -17,6 +19,8 @@ class OAuth < ApplicationRecord
|
|||||||
validates :scope, presence: true
|
validates :scope, presence: true
|
||||||
validates :json_user_email_path, presence: true
|
validates :json_user_email_path, presence: true
|
||||||
|
|
||||||
|
friendly_id :generate_random_slug, use: :slugged
|
||||||
|
|
||||||
def is_default?
|
def is_default?
|
||||||
tenant_id == nil
|
tenant_id == nil
|
||||||
end
|
end
|
||||||
@@ -27,9 +31,9 @@ class OAuth < ApplicationRecord
|
|||||||
# for this reason, we don't preprend tenant subdomain
|
# for this reason, we don't preprend tenant subdomain
|
||||||
# but rather use the "login" subdomain
|
# but rather use the "login" subdomain
|
||||||
if self.is_default?
|
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
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -46,6 +50,14 @@ class OAuth < ApplicationRecord
|
|||||||
is_default? and tenant_default_o_auths.exists?
|
is_default? and tenant_default_o_auths.exists?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def generate_random_slug
|
||||||
|
loop do
|
||||||
|
self.slug = SecureRandom.hex(8)
|
||||||
|
break unless self.class.exists?(slug: slug)
|
||||||
|
end
|
||||||
|
slug
|
||||||
|
end
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
# returns all tenant-specific o_auths plus all default o_auths that are enabled site-wide
|
# returns all tenant-specific o_auths plus all default o_auths that are enabled site-wide
|
||||||
def include_all_defaults
|
def include_all_defaults
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
class Post < ApplicationRecord
|
class Post < ApplicationRecord
|
||||||
include TenantOwnable
|
include TenantOwnable
|
||||||
|
extend FriendlyId
|
||||||
|
|
||||||
belongs_to :board
|
belongs_to :board
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
@@ -15,6 +16,8 @@ class Post < ApplicationRecord
|
|||||||
|
|
||||||
paginates_per Rails.application.posts_per_page
|
paginates_per Rails.application.posts_per_page
|
||||||
|
|
||||||
|
friendly_id :title, use: :slugged
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
def find_with_post_status_in(post_statuses)
|
def find_with_post_status_in(post_statuses)
|
||||||
where(post_status_id: post_statuses.pluck(:id))
|
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.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
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.datetime "updated_at", precision: 6, null: false
|
||||||
t.integer "order", null: false
|
t.integer "order", null: false
|
||||||
t.bigint "tenant_id", 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 ["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"
|
t.index ["tenant_id"], name: "index_boards_on_tenant_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -80,7 +82,9 @@ ActiveRecord::Schema.define(version: 2024_03_21_171022) do
|
|||||||
t.bigint "tenant_id"
|
t.bigint "tenant_id"
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_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 ["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"
|
t.index ["tenant_id"], name: "index_o_auths_on_tenant_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -118,8 +122,10 @@ ActiveRecord::Schema.define(version: 2024_03_21_171022) do
|
|||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
t.bigint "tenant_id", null: false
|
t.bigint "tenant_id", null: false
|
||||||
|
t.string "slug"
|
||||||
t.index ["board_id"], name: "index_posts_on_board_id"
|
t.index ["board_id"], name: "index_posts_on_board_id"
|
||||||
t.index ["post_status_id"], name: "index_posts_on_post_status_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 ["tenant_id"], name: "index_posts_on_tenant_id"
|
||||||
t.index ["user_id"], name: "index_posts_on_user_id"
|
t.index ["user_id"], name: "index_posts_on_user_id"
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user