Add users management to site settings (#126)

This commit is contained in:
Riccardo Graziosi
2022-06-24 14:39:35 +02:00
committed by GitHub
parent bc15140512
commit 37fb99a868
71 changed files with 1093 additions and 1409 deletions

View File

@@ -8,8 +8,8 @@ gem 'rails', '6.0.5'
gem 'pg', '>= 0.18', '< 2.0'
gem 'puma', '~> 4.3'
gem 'sass-rails', '~> 5'
gem 'sassc', '2.1.0' # temporarely, because 2.4.0 takes 5 minutes to install...
gem 'webpacker', '4.3.0'
@@ -28,9 +28,6 @@ gem 'pundit', '2.2.0'
# I18n (forward locales to JS)
gem 'i18n-js'
# Administration panel
gem "administrate", '0.16.0'
# React
gem 'react-rails', '~> 2.6.0'

View File

@@ -58,16 +58,6 @@ GEM
zeitwerk (~> 2.2, >= 2.2.2)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
administrate (0.16.0)
actionpack (>= 5.0)
actionview (>= 5.0)
activerecord (>= 5.0)
datetime_picker_rails (~> 0.0.7)
jquery-rails (>= 4.0)
kaminari (>= 1.0)
momentjs-rails (~> 2.8)
sassc-rails (~> 2.1)
selectize-rails (~> 0.6)
babel-source (5.8.35)
babel-transpiler (0.7.0)
babel-source (>= 4.0, < 6)
@@ -91,8 +81,6 @@ GEM
concurrent-ruby (1.1.10)
connection_pool (2.2.5)
crass (1.0.6)
datetime_picker_rails (0.0.7)
momentjs-rails (>= 2.8.1)
devise (4.7.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@@ -117,10 +105,6 @@ GEM
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
jquery-rails (4.5.0)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
kaminari (1.2.2)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.2)
@@ -148,8 +132,6 @@ GEM
mini_mime (1.1.2)
mini_portile2 (2.8.0)
minitest (5.15.0)
momentjs-rails (2.29.1.1)
railties (>= 3.1)
msgpack (1.5.2)
nio4r (2.5.8)
nokogiri (1.13.6)
@@ -239,15 +221,6 @@ GEM
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
sassc (2.1.0)
ffi (~> 1.9)
sassc-rails (2.1.2)
railties (>= 4.0.0)
sassc (>= 2.0)
sprockets (> 3.0)
sprockets-rails
tilt
selectize-rails (0.12.6)
selenium-webdriver (4.1.0)
childprocess (>= 0.5, < 5.0)
rexml (~> 3.2, >= 3.2.5)
@@ -297,7 +270,6 @@ PLATFORMS
ruby
DEPENDENCIES
administrate (= 0.16.0)
bootsnap (>= 1.4.2)
byebug
capybara (>= 2.15)
@@ -314,7 +286,6 @@ DEPENDENCIES
react-rails (~> 2.6.0)
rspec-rails (~> 3.8.2)
sass-rails (~> 5)
sassc (= 2.1.0)
selenium-webdriver
spring
spring-watcher-listen (~> 2.0.0)

View File

@@ -1,19 +0,0 @@
# All Administrate controllers inherit from this `Admin::ApplicationController`,
# making it the ideal place to put authentication logic or other
# before_actions.
#
# If you want to add pagination or other controller-level concerns,
# you're free to overwrite the RESTful controller actions.
module Admin
class ApplicationController < Administrate::ApplicationController
include ApplicationHelper
before_action :authenticate_admin
# Override this value to specify the number of elements to display at a time
# on index pages. Defaults to 20.
# def records_per_page
# params[:per_page] || 20
# end
end
end

View File

@@ -1,14 +0,0 @@
module Admin
class BoardsController < Admin::ApplicationController
before_action :default_order
def default_order
@order ||= Administrate::Order.new(
params.fetch(resource_name, {}).fetch(:order, 'order'),
params.fetch(resource_name, {}).fetch(:direction, 'asc'),
)
end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
end
end

View File

@@ -1,15 +0,0 @@
module Admin
class CommentsController < Admin::ApplicationController
before_action :default_order
def default_order
@order ||= Administrate::Order.new(
params.fetch(resource_name, {}).fetch(:order, 'updated_at'),
params.fetch(resource_name, {}).fetch(:direction, 'desc'),
)
end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end
end

View File

@@ -1,14 +0,0 @@
module Admin
class PostStatusesController < Admin::ApplicationController
before_action :default_order
def default_order
@order ||= Administrate::Order.new(
params.fetch(resource_name, {}).fetch(:order, 'order'),
params.fetch(resource_name, {}).fetch(:direction, 'asc'),
)
end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
end
end

View File

@@ -1,15 +0,0 @@
module Admin
class PostsController < Admin::ApplicationController
before_action :default_order
def default_order
@order ||= Administrate::Order.new(
params.fetch(resource_name, {}).fetch(:order, 'updated_at'),
params.fetch(resource_name, {}).fetch(:direction, 'desc'),
)
end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end
end

View File

@@ -1,69 +0,0 @@
module Admin
class UsersController < Admin::ApplicationController
before_action :default_order
def default_order
@order ||= Administrate::Order.new(
params.fetch(resource_name, {}).fetch(:order, 'updated_at'),
params.fetch(resource_name, {}).fetch(:direction, 'desc'),
)
end
def authenticate_admin
unless user_signed_in?
flash[:alert] = t('backend.errors.not_logged_in')
redirect_to new_user_session_path
return
end
unless current_user.admin?
flash[:alert] = t('backend.errors.not_enough_privileges')
redirect_to root_path
return
end
end
# overwrite default create
def create
user = User.new(user_params)
user.skip_confirmation! # automatically confirm user email
if user.save
flash[:notice] = translate_with_resource('create.success')
redirect_to admin_user_path(user)
else
render :new, locals: {
page: Administrate::Page::Form.new(dashboard, user),
}
end
end
# overwrite default update
def update
user = User.find(params[:id])
if params[:user][:password].empty?
user.assign_attributes(user_params.except(:password))
else
user.assign_attributes(user_params)
end
user.skip_reconfirmation! # automatically reconfirm user email
if user.save
flash[:notice] = translate_with_resource('update.success')
redirect_to admin_user_path(user)
else
render :new, locals: {
page: Administrate::Page::Form.new(dashboard, user),
}
end
end
private
def user_params
params.require(:user).permit(:full_name, :email, :role, :password)
end
end
end

View File

@@ -9,8 +9,10 @@ class ApplicationController < ActionController::Base
protected
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:full_name, :notifications_enabled])
devise_parameter_sanitizer.permit(:account_update, keys: [:full_name, :notifications_enabled])
additional_permitted_parameters = [:full_name, :notifications_enabled]
devise_parameter_sanitizer.permit(:sign_up, keys: additional_permitted_parameters)
devise_parameter_sanitizer.permit(:account_update, keys: additional_permitted_parameters)
end
def load_boards

View File

@@ -0,0 +1,11 @@
class RegistrationsController < Devise::RegistrationsController
# Override destroy to soft delete
def destroy
resource.status = "deleted"
resource.save
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
set_flash_message :notice, :destroyed
yield resource if block_given?
respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
end
end

View File

@@ -1,7 +1,8 @@
class SiteSettingsController < ApplicationController
include ApplicationHelper
before_action :authenticate_admin
before_action :authenticate_admin, only: [:general, :boards, :post_statuses, :roadmap]
before_action :authenticate_power_user, only: [:users]
def general
end
@@ -14,4 +15,7 @@ class SiteSettingsController < ApplicationController
def roadmap
end
def users
end
end

View File

@@ -0,0 +1,36 @@
class UsersController < ApplicationController
before_action :authenticate_user!, only: [:index, :update]
def index
authorize User
@users = User
.all
.order(role: :desc)
render json: @users
end
def update
@user = User.find(params[:id])
authorize @user
@user.assign_attributes user_update_params
if @user.save
render json: @user
else
render json: {
error: @user.errors.full_messages
}, status: :unprocessable_entity
end
end
private
def user_update_params
params
.require(:user)
.permit(policy(@user).permitted_attributes_for_update)
end
end

View File

@@ -1,71 +0,0 @@
require "administrate/base_dashboard"
class BoardDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
description: Field::Text,
order: Field::Number,
posts: Field::HasMany,
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
name
description
order
posts
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
name
description
order
posts
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
description
order
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how boards are displayed
# across all pages of the admin dashboard.
#
def display_resource(board)
"Board \"#{board.name}\""
end
end

View File

@@ -1,76 +0,0 @@
require "administrate/base_dashboard"
class CommentDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
body: Field::Text,
parent_id: Field::Number,
parent: Field::BelongsTo.with_options(class_name: "Comment"),
children: Field::HasMany.with_options(class_name: "Comment"),
user: Field::BelongsTo,
post: Field::BelongsTo,
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
body
user
post
children
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
body
parent_id
parent
children
user
post
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
body
parent
user
post
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how comments are displayed
# across all pages of the admin dashboard.
#
# def display_resource(comment)
# "Comment ##{comment.id}"
# end
end

View File

@@ -1,79 +0,0 @@
require "administrate/base_dashboard"
class PostDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
title: Field::String,
description: Field::Text,
comments: Field::HasMany,
user: Field::BelongsTo,
board: Field::BelongsTo,
post_status: Field::BelongsTo,
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
title
description
user
board
post_status
comments
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
title
description
user
board
post_status
comments
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
title
description
user
board
post_status
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how posts are displayed
# across all pages of the admin dashboard.
#
# def display_resource(post)
# "Post ##{post.id}"
# end
end

View File

@@ -1,75 +0,0 @@
require "administrate/base_dashboard"
class PostStatusDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
color: ColorField,
order: Field::Number,
show_in_roadmap: Field::Boolean,
posts: Field::HasMany,
created_at: Field::DateTime,
updated_at: Field::DateTime,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
name
color
order
show_in_roadmap
posts
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
name
color
order
show_in_roadmap
posts
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
color
order
show_in_roadmap
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how post statuses are displayed
# across all pages of the admin dashboard.
#
def display_resource(post_status)
"PostStatus \"#{post_status.name}\""
end
end

View File

@@ -1,88 +0,0 @@
require "administrate/base_dashboard"
class UserDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
email: Field::String,
password: Field::Password,
encrypted_password: Field::String,
reset_password_token: Field::String,
reset_password_sent_at: Field::DateTime,
remember_created_at: Field::DateTime,
confirmation_token: Field::String,
confirmed_at: Field::DateTime,
confirmation_sent_at: Field::DateTime,
unconfirmed_email: Field::String,
created_at: Field::DateTime,
updated_at: Field::DateTime,
role: RoleField,
full_name: Field::String,
posts: Field::HasMany,
comments: Field::HasMany,
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
full_name
email
role
posts
comments
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
full_name
email
role
password
posts
comments
created_at
updated_at
confirmed_at
confirmation_sent_at
unconfirmed_email
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
full_name
email
role
password
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how users are displayed
# across all pages of the admin dashboard.
#
def display_resource(user)
"#{user.role.capitalize} \"#{user.full_name}\""
end
end

View File

@@ -1,7 +0,0 @@
require "administrate/field/base"
class ColorField < Administrate::Field::Base
def to_s
data.to_s
end
end

View File

@@ -1,7 +0,0 @@
require "administrate/field/base"
class IdField < Administrate::Field::Base
def to_s
data.to_s
end
end

View File

@@ -1,13 +0,0 @@
require "administrate/field/base"
class RoleField < Administrate::Field::Base
def to_s
data.to_s
end
def select_field_values(form_builder)
form_builder.object.class.public_send(attribute.to_s.pluralize).keys.map do |v|
[v.titleize, v]
end
end
end

View File

@@ -1,12 +1,27 @@
module ApplicationHelper
def authenticate_admin
def check_user_signed_in
unless user_signed_in?
flash[:alert] = t('backend.errors.not_logged_in')
redirect_to new_user_session_path
return false
end
end
def authenticate_admin
return if check_user_signed_in == false
unless current_user.admin?
flash[:alert] = t('backend.errors.not_enough_privileges')
redirect_to root_path
return
end
end
unless current_user.moderator? || current_user.admin?
def authenticate_power_user
return if check_user_signed_in == false
unless current_user.admin? or current_user.moderator?
flash[:alert] = t('backend.errors.not_enough_privileges')
redirect_to root_path
return

View File

@@ -0,0 +1,60 @@
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import IUserJSON from '../../interfaces/json/IUser';
import { State } from '../../reducers/rootReducer';
export const USERS_REQUEST_START = 'USERS_REQUEST_START';
interface UsersRequestStartAction {
type: typeof USERS_REQUEST_START;
}
export const USERS_REQUEST_SUCCESS = 'USERS_REQUEST_SUCCESS';
interface UsersRequestSuccessAction {
type: typeof USERS_REQUEST_SUCCESS;
users: Array<IUserJSON>;
}
export const USERS_REQUEST_FAILURE = 'USERS_REQUEST_FAILURE';
interface UsersRequestFailureAction {
type: typeof USERS_REQUEST_FAILURE;
error: string;
}
export type UsersRequestActionTypes =
UsersRequestStartAction |
UsersRequestSuccessAction |
UsersRequestFailureAction;
const usersRequestStart = (): UsersRequestActionTypes => ({
type: USERS_REQUEST_START,
});
const usersRequestSuccess = (
users: Array<IUserJSON>
): UsersRequestActionTypes => ({
type: USERS_REQUEST_SUCCESS,
users,
});
const usersRequestFailure = (error: string): UsersRequestActionTypes => ({
type: USERS_REQUEST_FAILURE,
error,
});
export const requestUsers = (): ThunkAction<void, State, null, Action<string>> => (
async (dispatch) => {
dispatch(usersRequestStart());
try {
const response = await fetch('/users');
const json = await response.json();
dispatch(usersRequestSuccess(json));
} catch (e) {
dispatch(usersRequestFailure(e));
}
}
)

View File

@@ -0,0 +1,87 @@
import { Action } from "redux";
import { ThunkAction } from "redux-thunk";
import HttpStatus from "../../constants/http_status";
import buildRequestHeaders from "../../helpers/buildRequestHeaders";
import IUserJSON from "../../interfaces/json/IUser";
import { State } from "../../reducers/rootReducer";
export const USER_UPDATE_START = 'USER_UPDATE_START';
interface UserUpdateStartAction {
type: typeof USER_UPDATE_START;
}
export const USER_UPDATE_SUCCESS = 'USER_UPDATE_SUCCESS';
interface UserUpdateSuccessAction {
type: typeof USER_UPDATE_SUCCESS;
user: IUserJSON;
}
export const USER_UPDATE_FAILURE = 'USER_UPDATE_FAILURE';
interface UserUpdateFailureAction {
type: typeof USER_UPDATE_FAILURE;
error: string;
}
export type UserUpdateActionTypes =
UserUpdateStartAction |
UserUpdateSuccessAction |
UserUpdateFailureAction;
const userUpdateStart = (): UserUpdateStartAction => ({
type: USER_UPDATE_START,
});
const userUpdateSuccess = (
userJSON: IUserJSON,
): UserUpdateSuccessAction => ({
type: USER_UPDATE_SUCCESS,
user: userJSON,
});
const userUpdateFailure = (error: string): UserUpdateFailureAction => ({
type: USER_UPDATE_FAILURE,
error,
});
interface UpdateUserParams {
id: number;
role?: string;
status?: string;
authenticityToken: string;
}
export const updateUser = ({
id,
role = null,
status = null,
authenticityToken,
}: UpdateUserParams): ThunkAction<void, State, null, Action<string>> => async (dispatch) => {
dispatch(userUpdateStart());
const user = Object.assign({},
role !== null ? { role } : null,
status !== null ? { status } : null,
);
try {
const res = await fetch(`/users/${id}`, {
method: 'PATCH',
headers: buildRequestHeaders(authenticityToken),
body: JSON.stringify({ user }),
});
const json = await res.json();
if (res.status === HttpStatus.OK) {
dispatch(userUpdateSuccess(json));
} else {
dispatch(userUpdateFailure(json.error));
}
return Promise.resolve(res);
} catch (e) {
dispatch(userUpdateFailure(e));
return Promise.resolve(null);
}
};

View File

@@ -92,7 +92,7 @@ class BoardsSiteSettingsP extends React.Component<Props> {
return (
<>
<Box>
<h2>{I18n.t('site_settings.boards.title')}</h2>
<h2>{ I18n.t('site_settings.boards.title') }</h2>
{
boards.items.length > 0 ?

View File

@@ -0,0 +1,157 @@
import * as React from "react";
import Gravatar from 'react-gravatar';
import I18n from 'i18n-js';
import IUser, { UserRoles, USER_ROLE_ADMIN, USER_ROLE_USER, USER_STATUS_ACTIVE, USER_STATUS_BLOCKED, USER_STATUS_DELETED } from "../../../interfaces/IUser";
import Separator from "../../common/Separator";
import UserForm from "./UserForm";
import { MutedText } from "../../common/CustomTexts";
interface Props {
user: IUser;
updateUserRole(
id: number,
role: UserRoles,
closeEditMode: Function,
): void;
updateUserStatus(
id: number,
status: typeof USER_STATUS_ACTIVE | typeof USER_STATUS_BLOCKED,
): void;
currentUserRole: UserRoles;
currentUserEmail: string;
}
interface State {
editMode: boolean;
}
class UserEditable extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { editMode: false };
this.toggleEditMode = this.toggleEditMode.bind(this);
this._handleUpdateUserRole = this._handleUpdateUserRole.bind(this);
this._handleUpdateUserStatus = this._handleUpdateUserStatus.bind(this);
}
toggleEditMode() {
this.setState({ editMode: !this.state.editMode });
}
_handleUpdateUserRole(newRole: UserRoles) {
this.props.updateUserRole(
this.props.user.id,
newRole,
this.toggleEditMode,
);
}
_handleUpdateUserStatus() {
const { user } = this.props;
const currentStatus = user.status;
let newStatus: typeof USER_STATUS_ACTIVE | typeof USER_STATUS_BLOCKED;
if (currentStatus === 'deleted') return;
if (currentStatus === 'active') newStatus = 'blocked';
else newStatus = 'active';
const confirmationMessage =
newStatus === 'blocked' ?
I18n.t('site_settings.users.block_confirmation', { name: user.fullName })
:
I18n.t('site_settings.users.unblock_confirmation', { name: user.fullName });
const confirmationResponse = confirm(confirmationMessage);
if (confirmationResponse) {
this.props.updateUserStatus(user.id, newStatus);
}
}
render() {
const { user, currentUserRole, currentUserEmail } = this.props;
const { editMode } = this.state;
const editEnabled =
user.status === USER_STATUS_ACTIVE &&
currentUserRole === USER_ROLE_ADMIN &&
currentUserEmail !== user.email;
const blockEnabled =
user.status !== USER_STATUS_DELETED &&
(currentUserRole === USER_ROLE_ADMIN || user.role === USER_ROLE_USER) &&
currentUserEmail !== user.email;
return (
<li className="userEditable">
{
editMode === false ?
<>
<div className="userInfo">
<Gravatar email={user.email} size={42} className="gravatar userGravatar" />
<div className="userFullNameRoleStatus">
<span className="userFullName">{ user.fullName }</span>
<div className="userRoleStatus">
<span>
<MutedText>{ I18n.t(`site_settings.users.role_${user.role}`) }</MutedText>
</span>
{
user.status !== USER_STATUS_ACTIVE ?
<>
<Separator />
<span className={`userStatus userStatus${user.status}`}>
{ I18n.t(`site_settings.users.status_${user.status}`) }
</span>
</>
:
null
}
</div>
</div>
</div>
<div className="userEditableActions">
<a
onClick={() => editEnabled && this.toggleEditMode()}
className={editEnabled ? '' : 'actionDisabled'}
>
{ I18n.t('common.buttons.edit') }
</a>
<Separator />
<a
onClick={() => blockEnabled && this._handleUpdateUserStatus()}
className={blockEnabled ? '' : 'actionDisabled'}
>
{
user.status !== USER_STATUS_BLOCKED ?
I18n.t('site_settings.users.block')
:
I18n.t('site_settings.users.unblock')
}
</a>
</div>
</>
:
<>
<UserForm user={user} updateUserRole={this._handleUpdateUserRole} />
<a onClick={this.toggleEditMode} className="userEditCancelButton">
{ I18n.t('common.buttons.cancel') }
</a>
</>
}
</li>
);
}
}
export default UserEditable;

View File

@@ -0,0 +1,79 @@
import * as React from 'react';
import Gravatar from 'react-gravatar';
import I18n from 'i18n-js';
import Button from '../../common/Button';
import IUser, { UserRoles, USER_ROLE_ADMIN, USER_ROLE_MODERATOR, USER_ROLE_USER } from '../../../interfaces/IUser';
interface Props {
user: IUser;
updateUserRole(newRole: UserRoles): void;
}
interface State {
role: UserRoles;
}
class UserForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { role: this.props.user.role };
this._handleUpdateUserRole = this._handleUpdateUserRole.bind(this);
}
_handleUpdateUserRole(selectedRole: UserRoles) {
const { user, updateUserRole } = this.props;
let confirmation = true;
if (selectedRole === 'admin') {
confirmation = confirm(I18n.t('site_settings.users.role_to_admin_confirmation', { name: user.fullName }));
}
if (confirmation) updateUserRole(selectedRole);
}
render() {
const { user } = this.props;
const selectedRole = this.state.role;
return (
<div className="userForm">
<Gravatar email={user.email} size={42} className="gravatar userGravatar" />
<div className="userFullNameRoleForm">
<span className="userFullName">{ user.fullName }</span>
<select
value={selectedRole || 'Loading...'}
onChange={
(e: React.FormEvent) => {
this.setState({role: (e.target as HTMLSelectElement).value as UserRoles});
}}
id="selectPickerUserRole"
className="selectPicker"
>
<optgroup label="Roles">
<option value={USER_ROLE_USER}>
{ I18n.t(`site_settings.users.role_${USER_ROLE_USER}`) }
</option>
<option value={USER_ROLE_MODERATOR}>
{ I18n.t(`site_settings.users.role_${USER_ROLE_MODERATOR}`) }
</option>
<option value={USER_ROLE_ADMIN}>
{ I18n.t(`site_settings.users.role_${USER_ROLE_ADMIN}`) }
</option>
</optgroup>
</select>
</div>
<Button onClick={() => this._handleUpdateUserRole(selectedRole)} className="updateUserButton">
{ I18n.t('common.buttons.update') }
</Button>
</div>
);
}
}
export default UserForm;

View File

@@ -0,0 +1,103 @@
import * as React from 'react';
import I18n from 'i18n-js';
import UserEditable from './UserEditable';
import Box from '../../common/Box';
import SiteSettingsInfoBox from '../../common/SiteSettingsInfoBox';
import { UsersState } from '../../../reducers/usersReducer';
import { UserRoles, USER_STATUS_ACTIVE, USER_STATUS_BLOCKED } from '../../../interfaces/IUser';
import HttpStatus from '../../../constants/http_status';
interface Props {
users: UsersState;
settingsAreUpdating: boolean;
settingsError: string;
requestUsers(): void;
updateUserRole(
id: number,
role: UserRoles,
authenticityToken: string,
): Promise<any>;
updateUserStatus(
id: number,
status: typeof USER_STATUS_ACTIVE | typeof USER_STATUS_BLOCKED,
authenticityToken: string,
): void;
currentUserEmail: string;
currentUserRole: UserRoles;
authenticityToken: string;
}
class UsersSiteSettingsP extends React.Component<Props> {
constructor(props: Props) {
super(props);
this._handleUpdateUserRole = this._handleUpdateUserRole.bind(this);
this._handleUpdateUserStatus = this._handleUpdateUserStatus.bind(this);
}
componentDidMount() {
this.props.requestUsers();
}
_handleUpdateUserRole(id: number, role: UserRoles, closeEditMode: Function) {
this.props.updateUserRole(
id,
role,
this.props.authenticityToken,
).then(res => {
if (res?.status !== HttpStatus.OK) return;
closeEditMode();
});
}
_handleUpdateUserStatus(id: number, status: typeof USER_STATUS_ACTIVE | typeof USER_STATUS_BLOCKED) {
this.props.updateUserStatus(
id,
status,
this.props.authenticityToken,
);
}
render() {
const {
users,
settingsAreUpdating,
settingsError,
currentUserRole,
currentUserEmail,
} = this.props;
return (
<>
<Box>
<h2>{ I18n.t('site_settings.users.title') }</h2>
<ul className="usersList">
{
users.items.map((user, i) => (
<UserEditable
user={user}
updateUserRole={this._handleUpdateUserRole}
updateUserStatus={this._handleUpdateUserStatus}
currentUserEmail={currentUserEmail}
currentUserRole={currentUserRole}
key={i}
/>
))
}
</ul>
</Box>
<SiteSettingsInfoBox areUpdating={settingsAreUpdating || users.areLoading} error={settingsError} />
</>
);
}
}
export default UsersSiteSettingsP;

View File

@@ -0,0 +1,39 @@
import * as React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import UsersSiteSettings from '../../../containers/UsersSiteSettings';
import createStoreHelper from '../../../helpers/createStore';
import { UserRoles } from '../../../interfaces/IUser';
import { State } from '../../../reducers/rootReducer';
interface Props {
currentUserEmail: string;
currentUserRole: UserRoles;
authenticityToken: string;
}
class UsersSiteSettingsRoot extends React.Component<Props> {
store: Store<State, any>;
constructor(props: Props) {
super(props);
this.store = createStoreHelper();
}
render() {
return (
<Provider store={this.store}>
<UsersSiteSettings
currentUserEmail={this.props.currentUserEmail}
currentUserRole={this.props.currentUserRole}
authenticityToken={this.props.authenticityToken}
/>
</Provider>
);
}
}
export default UsersSiteSettingsRoot;

View File

@@ -0,0 +1,49 @@
import { connect } from "react-redux";
import UsersSiteSettingsP from "../components/SiteSettings/Users/UsersSiteSettingsP";
import { requestUsers } from "../actions/User/requestUsers";
import { updateUser } from "../actions/User/updateUser";
import { UserRoles, USER_STATUS_ACTIVE, USER_STATUS_BLOCKED } from "../interfaces/IUser";
import { State } from "../reducers/rootReducer";
const mapStateToProps = (state: State) => ({
users: state.users,
settingsAreUpdating: state.siteSettings.users.areUpdating,
settingsError: state.siteSettings.users.error,
});
const mapDispatchToProps = (dispatch: any) => ({
requestUsers() {
dispatch(requestUsers());
},
updateUserRole(
id: number,
role: UserRoles,
authenticityToken: string,
): Promise<any> {
return dispatch(updateUser({
id,
role,
authenticityToken,
}));
},
updateUserStatus(
id: number,
status: typeof USER_STATUS_ACTIVE | typeof USER_STATUS_BLOCKED,
authenticityToken: string,
) {
dispatch(updateUser({
id,
status,
authenticityToken,
}));
},
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(UsersSiteSettingsP);

View File

@@ -0,0 +1,29 @@
// Roles
export const USER_ROLE_USER = 'user';
export const USER_ROLE_MODERATOR = 'moderator';
export const USER_ROLE_ADMIN = 'admin';
export type UserRoles =
typeof USER_ROLE_USER |
typeof USER_ROLE_MODERATOR |
typeof USER_ROLE_ADMIN;
// Statuses
export const USER_STATUS_ACTIVE = 'active';
export const USER_STATUS_BLOCKED = 'blocked';
export const USER_STATUS_DELETED = 'deleted';
export type UserStatuses =
typeof USER_STATUS_ACTIVE |
typeof USER_STATUS_BLOCKED |
typeof USER_STATUS_DELETED;
interface IUser {
id: number;
email: string;
fullName: string;
role: UserRoles;
status: UserStatuses;
}
export default IUser;

View File

@@ -0,0 +1,9 @@
interface IUserJSON {
id: number;
email: string;
full_name: string;
role: string;
status: string;
}
export default IUserJSON;

View File

@@ -0,0 +1,58 @@
import {
UsersRequestActionTypes,
USERS_REQUEST_START,
USERS_REQUEST_SUCCESS,
USERS_REQUEST_FAILURE,
} from '../../actions/User/requestUsers';
import {
UserUpdateActionTypes,
USER_UPDATE_START,
USER_UPDATE_SUCCESS,
USER_UPDATE_FAILURE,
} from '../../actions/User/updateUser';
export interface SiteSettingsUsersState {
areUpdating: boolean;
error: string;
}
const initialState: SiteSettingsUsersState = {
areUpdating: false,
error: '',
};
const siteSettingsUsersReducer = (
state = initialState,
action: UsersRequestActionTypes | UserUpdateActionTypes,
) => {
switch (action.type) {
case USERS_REQUEST_START:
case USER_UPDATE_START:
return {
...state,
areUpdating: true,
};
case USERS_REQUEST_SUCCESS:
case USER_UPDATE_SUCCESS:
return {
...state,
areUpdating: false,
error: '',
};
case USERS_REQUEST_FAILURE:
case USER_UPDATE_FAILURE:
return {
...state,
areUpdating: false,
error: action.error,
};
default:
return state;
}
}
export default siteSettingsUsersReducer;

View File

@@ -38,7 +38,7 @@ const initialState: BoardsState = {
items: [],
areLoading: false,
error: '',
}
};
const boardsReducer = (
state = initialState,

View File

@@ -3,6 +3,7 @@ import { combineReducers } from 'redux';
import postsReducer from './postsReducer';
import boardsReducer from './boardsReducer';
import postStatusesReducer from './postStatusesReducer';
import usersReducer from './usersReducer';
import currentPostReducer from './currentPostReducer';
import siteSettingsReducer from './siteSettingsReducer';
@@ -10,6 +11,7 @@ const rootReducer = combineReducers({
posts: postsReducer,
boards: boardsReducer,
postStatuses: postStatusesReducer,
users: usersReducer,
currentPost: currentPostReducer,
siteSettings: siteSettingsReducer,
});

View File

@@ -61,20 +61,37 @@ import {
POSTSTATUS_UPDATE_FAILURE,
} from '../actions/PostStatus/updatePostStatus';
import {
UsersRequestActionTypes,
USERS_REQUEST_START,
USERS_REQUEST_SUCCESS,
USERS_REQUEST_FAILURE,
} from '../actions/User/requestUsers';
import {
UserUpdateActionTypes,
USER_UPDATE_START,
USER_UPDATE_SUCCESS,
USER_UPDATE_FAILURE,
} from '../actions/User/updateUser';
import siteSettingsBoardsReducer, { SiteSettingsBoardsState } from './SiteSettings/boardsReducer';
import siteSettingsPostStatusesReducer, { SiteSettingsPostStatusesState } from './SiteSettings/postStatusesReducer';
import siteSettingsRoadmapReducer, { SiteSettingsRoadmapState } from './SiteSettings/roadmapReducer';
import siteSettingsUsersReducer, { SiteSettingsUsersState } from './SiteSettings/usersReducer';
interface SiteSettingsState {
boards: SiteSettingsBoardsState;
postStatuses: SiteSettingsPostStatusesState;
roadmap: SiteSettingsRoadmapState;
users: SiteSettingsUsersState;
}
const initialState: SiteSettingsState = {
boards: siteSettingsBoardsReducer(undefined, {} as any),
postStatuses: siteSettingsPostStatusesReducer(undefined, {} as any),
roadmap: siteSettingsRoadmapReducer(undefined, {} as any),
users: siteSettingsUsersReducer(undefined, {} as any),
};
const siteSettingsReducer = (
@@ -88,7 +105,9 @@ const siteSettingsReducer = (
PostStatusOrderUpdateActionTypes |
PostStatusDeleteActionTypes |
PostStatusSubmitActionTypes |
PostStatusUpdateActionTypes
PostStatusUpdateActionTypes |
UsersRequestActionTypes |
UserUpdateActionTypes
): SiteSettingsState => {
switch (action.type) {
case BOARDS_REQUEST_START:
@@ -134,6 +153,17 @@ const siteSettingsReducer = (
roadmap: siteSettingsRoadmapReducer(state.roadmap, action),
};
case USERS_REQUEST_START:
case USERS_REQUEST_SUCCESS:
case USERS_REQUEST_FAILURE:
case USER_UPDATE_START:
case USER_UPDATE_SUCCESS:
case USER_UPDATE_FAILURE:
return {
...state,
users: siteSettingsUsersReducer(state.users, action),
};
default:
return state;
}

View File

@@ -0,0 +1,75 @@
import {
UsersRequestActionTypes,
USERS_REQUEST_START,
USERS_REQUEST_SUCCESS,
USERS_REQUEST_FAILURE,
} from '../actions/User/requestUsers';
import {
UserUpdateActionTypes,
USER_UPDATE_SUCCESS,
} from '../actions/User/updateUser';
import IUser from "../interfaces/IUser";
export interface UsersState {
items: Array<IUser>;
areLoading: boolean;
error: string;
}
const initialState: UsersState = {
items: [],
areLoading: false,
error: '',
};
const usersReducer = (
state = initialState,
action: UsersRequestActionTypes | UserUpdateActionTypes,
) => {
switch (action.type) {
case USERS_REQUEST_START:
return {
...state,
areLoading: true,
};
case USERS_REQUEST_SUCCESS:
return {
...state,
areLoading: false,
error: '',
items: action.users.map(userJson => ({
id: userJson.id,
email: userJson.email,
fullName: userJson.full_name,
role: userJson.role,
status: userJson.status,
})),
};
case USERS_REQUEST_FAILURE:
return {
...state,
areLoading: false,
error: action.error,
};
case USER_UPDATE_SUCCESS:
return {
...state,
items: state.items.map(user => {
return (user.id === action.user.id) ?
{...user, role: action.user.role, status: action.user.status}
:
user;
}),
};
default:
return state;
}
}
export default usersReducer;

View File

@@ -172,4 +172,9 @@
background-color: $primary-color !important;
border-color: $primary-color !important;
}
}
.selectPicker {
@extend
.custom-select;
}

View File

@@ -119,12 +119,6 @@
@extend
.d-flex,
.justify-content-between;
.selectPicker {
@extend
.custom-select,
.mx-2;
}
}
.postDescription {

View File

@@ -0,0 +1,68 @@
ul.usersList {
@extend
.pl-1;
list-style: none;
li.userEditable {
@extend
.d-flex,
.justify-content-between,
.my-2,
.p-3;
.userGravatar {
@extend .mr-3, .align-self-center;
}
.userFullName {
font-size: 18px;
}
.userInfo {
@extend .d-flex;
.userFullNameRoleStatus {
@extend
.d-flex,
.flex-column;
}
}
.userEditableActions {
@extend .align-self-center;
}
.userForm {
@extend .d-flex;
.userFullNameRoleForm {
@extend .d-flex, .flex-column;
}
}
.updateUserButton { @extend .align-self-center; }
.userEditCancelButton { @extend .align-self-center; }
a {
cursor: pointer;
&:hover { text-decoration: underline; }
}
a.actionDisabled {
@extend .mutedText;
text-decoration: none;
cursor: not-allowed;
}
.updateUserButton {
margin-left: 16px;
}
.userStatusblocked { color: orange; }
.userStatusdeleted { color: red; }
}
}

View File

@@ -20,6 +20,7 @@
@import 'components/SiteSettings/Boards';
@import 'components/SiteSettings/PostStatuses';
@import 'components/SiteSettings/Roadmap';
@import 'components/SiteSettings/Users';
/* Icons */
@import 'icons/drag_icon';

View File

@@ -8,7 +8,10 @@ class User < ApplicationRecord
has_many :comments, dependent: :destroy
enum role: [:user, :moderator, :admin]
enum status: [:active, :blocked, :deleted]
after_initialize :set_default_role, if: :new_record?
after_initialize :set_default_status, if: :new_record?
after_initialize :skip_confirmation, if: :new_record?
validates :full_name, presence: true, length: { in: 2..32 }
@@ -17,6 +20,18 @@ class User < ApplicationRecord
self.role ||= :user
end
def set_default_status
self.status ||= :active
end
def active_for_authentication?
super && active?
end
def inactive_message
active? ? super : :blocked_or_deleted
end
def skip_confirmation
return if Rails.application.email_confirmation?
skip_confirmation!
@@ -36,4 +51,20 @@ class User < ApplicationRecord
def admin?
role == 'admin'
end
def moderator?
role == 'moderator'
end
def user?
role == 'user'
end
def active?
status == 'active'
end
def blocked?
status == 'blocked'
end
end

View File

@@ -0,0 +1,25 @@
class UserPolicy < ApplicationPolicy
def permitted_attributes_for_update
if user.admin?
[:role, :status]
elsif user.moderator?
[:status]
else
[]
end
end
def index?
user.power_user?
end
def update?
if user.admin?
record.id != user.id
elsif user.moderator?
record.user?
else
false
end
end
end

View File

@@ -1,20 +0,0 @@
<%#
# Flash Partial
This partial renders flash messages on every page.
## Relevant Helpers:
- `flash`:
Returns a hash,
where the keys are the type of flash (alert, error, notice, etc)
and the values are the message to be displayed.
%>
<% if flash.any? %>
<div class="flashes">
<% flash.each do |key, value| -%>
<div class="flash flash-<%= key %>"><%= value.html_safe %></div>
<% end -%>
</div>
<% end %>

View File

@@ -1,45 +0,0 @@
<%#
# Form Partial
This partial is rendered on a resource's `new` and `edit` pages,
and renders all form fields for a resource's editable attributes.
## Local variables:
- `page`:
An instance of [Administrate::Page::Form][1].
Contains helper methods to display a form,
and knows which attributes should be displayed in the resource's form.
[1]: http://www.rubydoc.info/gems/administrate/Administrate/Page/Form
%>
<%= form_for([namespace, page.resource], html: { class: "form" }) do |f| %>
<% if page.resource.errors.any? %>
<div id="error_explanation">
<h2 style="text-align: center;">
<%= t(
"administrate.form.errors",
pluralized_errors: pluralize(page.resource.errors.count, t("administrate.form.error")),
resource_name: display_resource_name(page.resource_name)
) %>
</h2>
<ul>
<% page.resource.errors.full_messages.each do |message| %>
<li class="flash-error"><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<% page.attributes.each do |attribute| -%>
<div class="field-unit field-unit--<%= attribute.html_class %>">
<%= render_field attribute, f: f %>
</div>
<% end -%>
<div class="form-actions">
<%= f.submit %>
</div>
<% end %>

View File

@@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="icon-cancel" viewBox="0 0 48 48">
<path fill-rule="evenodd" d="M24 19.757l-8.485-8.485c-.784-.783-2.047-.782-2.827 0l-1.417 1.416c-.777.777-.78 2.046.002 2.827L19.757 24l-8.485 8.485c-.783.784-.782 2.047 0 2.827l1.416 1.417c.777.777 2.046.78 2.827-.002L24 28.243l8.485 8.485c.784.783 2.047.782 2.827 0l1.417-1.416c.777-.777.78-2.046-.002-2.827L28.243 24l8.485-8.485c.783-.784.782-2.047 0-2.827l-1.416-1.417c-.777-.777-2.046-.78-2.827.002L24 19.757zM24 47c12.703 0 23-10.297 23-23S36.703 1 24 1 1 11.297 1 24s10.297 23 23 23z" />
</symbol>
<symbol id="icon-eyeglass" viewBox="0 0 48 48">
<path d="M27.885 32.515c-2.864 1.966-6.333 3.116-10.07 3.116C7.976 35.63 0 27.656 0 17.817 0 7.976 7.976 0 17.816 0S35.63 7.976 35.63 17.816c0 3.736-1.15 7.205-3.115 10.07l14.53 14.53c1.278 1.277 1.275 3.352 0 4.628-1.28 1.278-3.353 1.278-4.63 0l-14.53-14.53zm-10.07-3.736c6.056 0 10.964-4.91 10.964-10.964 0-6.055-4.91-10.964-10.964-10.964-6.055 0-10.964 4.91-10.964 10.964 0 6.055 4.91 10.963 10.964 10.963z" />
</symbol>
<symbol id="icon-up-caret" viewBox="0 0 48 48">
<path d="M2.988 33.02c-1.66 0-1.943-.81-.618-1.824l20-15.28c.878-.672 2.31-.67 3.188 0l20.075 15.288c1.316 1.003 1.048 1.816-.62 1.816H2.987z" />
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,21 +0,0 @@
<%#
# Javascript Partial
This partial imports the necessary javascript on each page.
By default, it includes the application JS,
but each page can define additional JS sources
by providing a `content_for(:javascript)` block.
%>
<% Administrate::Engine.javascripts.each do |js_path| %>
<%= javascript_include_tag js_path %>
<% end %>
<%= yield :javascript %>
<% if Rails.env.test? %>
<%= javascript_tag do %>
$.fx.off = true;
$.ajaxSetup({ async: false });
<% end %>
<% end %>

View File

@@ -1,26 +0,0 @@
<%#
# Navigation
This partial is used to display the navigation in Administrate.
By default, the navigation contains navigation links
for all resources in the admin dashboard,
as defined by the routes in the `admin/` namespace
%>
<nav class="navigation" role="navigation">
<%= link_to(
"⇦ Back to site",
root_path,
class: "navigation__link button",
style: "color: #f6f7f7; background-color: #293f54; font-size: 15pt;",
"data-turbolinks": "false"
) %>
<% Administrate::Namespace.new(namespace).resources_with_index_route.each do |resource| %>
<%= link_to(
display_resource_name(resource),
resource_index_route(resource),
class: "navigation__link navigation__link--#{nav_link_state(resource)}"
) if valid_action?(:index, resource) && show_action?(:index, model_from_resource(resource)) %>
<% end %>
</nav>

View File

@@ -1,14 +0,0 @@
<%#
# Stylesheet Partial
This partial imports the necessary stylesheets on each page.
By default, it includes the application CSS,
but each page can define additional CSS sources
by providing a `content_for(:stylesheet)` block.
%>
<% Administrate::Engine.stylesheets.each do |css_path| %>
<%= stylesheet_link_tag css_path %>
<% end %>
<%= yield :stylesheet %>

View File

@@ -1,6 +0,0 @@
<div class="field-unit__label">
<%= f.label field.attribute %>
</div>
<div class="field-unit__field">
<%= f.color_field field.attribute, style: 'height: 40px;' %>
</div>

View File

@@ -1,3 +0,0 @@
<div
style="background-color: <%= field.to_s %>; width: 32px; height: 32px; border-radius: 32px;">
</div>

View File

@@ -1,4 +0,0 @@
<div
style="background-color: <%= field.to_s %>; width: 32px; height: 32px; border-radius: 32px;">
</div>
(<%= field.to_s %>)

View File

@@ -1,14 +0,0 @@
<div class="field-unit__label">
<%= f.label field.attribute %>
</div>
<div class="field-unit__field">
<%= f.select field.attribute, field.select_field_values(f) %>
</div>
<div
title="Click to learn more"
style="margin-left: 8px; cursor: pointer;"
onclick="alert('If you add a user with role \'admin\', it will have same powers as you!')"
>
⚠️
</div>

View File

@@ -1 +0,0 @@
<%= field.to_s.titleize %>

View File

@@ -1 +0,0 @@
<%= field.to_s.titleize %>

View File

@@ -35,13 +35,15 @@
</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<% if current_user.power_user? %>
<%= link_to t('header.menu.site_settings'), site_settings_boards_path, class: 'dropdown-item' %>
<%= link_to t('header.menu.admin_panel'), admin_root_path, class: 'dropdown-item', 'data-turbolinks': 'false' %>
<%=
link_to t('header.menu.site_settings'),
current_user.admin? ? site_settings_boards_path : site_settings_users_path,
class: 'dropdown-item'
%>
<div class="dropdown-divider"></div>
<% end %>
<%= link_to t('header.menu.profile_settings'), edit_user_registration_path, class: 'dropdown-item' %>
<div class="dropdown-divider"></div>
<%= link_to t('header.menu.sign_out'), destroy_user_session_path, method: :delete, class: 'dropdown-item' %>

View File

@@ -1,41 +0,0 @@
<%#
# Application Layout
This view template is used as the layout
for every page that Administrate generates.
By default, it renders:
- Navigation
- Content for a search bar
(if provided by a `content_for` block in a nested page)
- Flashes
- Links to stylesheets and JavaScripts
%>
<!DOCTYPE html>
<html lang="<%= I18n.locale %>">
<head>
<meta charset="utf-8">
<meta name="ROBOTS" content="NOODP">
<meta name="viewport" content="initial-scale=1">
<title>
<%= content_for(:title) %> - Admin Panel - <%= Rails.application.name %>
</title>
<%= render "stylesheet" %>
<%= csrf_meta_tags %>
</head>
<body>
<%= render "icons" %>
<div class="app-container">
<%= render "navigation" -%>
<main class="main-content" role="main">
<%= render "flashes" -%>
<%= yield %>
</main>
</div>
<%= render "javascript" %>
</body>
</html>

View File

@@ -3,9 +3,13 @@
<span class="boxTitleText"><%= t('site_settings.menu.title') %></span>
<div class="verticalNavigation" role="tablist" aria-orientation="vertical">
<%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %>
<%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %>
<%= render 'menu_link', label: t('site_settings.menu.roadmap'), path: site_settings_roadmap_path %>
<% if current_user.admin? %>
<%= render 'menu_link', label: t('site_settings.menu.boards'), path: site_settings_boards_path %>
<%= render 'menu_link', label: t('site_settings.menu.post_statuses'), path: site_settings_post_statuses_path %>
<%= render 'menu_link', label: t('site_settings.menu.roadmap'), path: site_settings_roadmap_path %>
<% end %>
<%= render 'menu_link', label: t('site_settings.menu.users'), path: site_settings_users_path %>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
<div class="twoColumnsContainer">
<%= render 'menu' %>
<div>
<%=
react_component(
'SiteSettings/Users',
{
currentUserEmail: current_user.email,
currentUserRole: current_user.role,
authenticityToken: form_authenticity_token
}
)
%>
</div>
</div>

View File

@@ -16,6 +16,7 @@ en:
timeout: "Your session expired. Please sign in again to continue."
unauthenticated: "You need to sign in or sign up before continuing."
unconfirmed: "You have to confirm your email address before continuing."
blocked_or_deleted: "You cannot access your account because it has been blocked or deleted."
mailer:
confirmation_instructions:
subject: "Confirmation instructions"

View File

@@ -20,6 +20,7 @@ it:
timeout: "La tua sessione è scaduta, accedi nuovamente per continuare."
unauthenticated: "Devi accedere o registrarti per continuare."
unconfirmed: "Devi confermare il tuo indirizzo email per continuare."
blocked_or_deleted: "Non puoi accedere al tuo account perché è stato bloccato o eliminato."
mailer:
confirmation_instructions:
subject: "Istruzioni per la conferma"

View File

@@ -52,7 +52,6 @@ en:
header:
menu:
site_settings: 'Site settings'
admin_panel: 'Admin panel (deprecated)'
profile_settings: 'Profile settings'
sign_out: 'Sign out'
log_in: 'Log in / Sign up'
@@ -107,6 +106,7 @@ en:
boards: 'Boards'
post_statuses: 'Statuses'
roadmap: 'Roadmap'
users: 'Users'
info_box:
up_to_date: 'All changes saved'
error: 'An error occurred: %{message}'
@@ -128,6 +128,19 @@ en:
title2: 'Not in roadmap'
empty: 'The roadmap is empty.'
help: 'You can add statuses to the roadmap by dragging them from the section below. If you instead want to create a new status or change their order, go to Site settings -> Statuses.'
users:
title: 'Users'
block: 'Block'
unblock: 'Unblock'
block_confirmation: "%{name} won't be able to log in until it is unblocked. Are you sure?"
unblock_confirmation: "%{name} will be able to log in and submit feedback. Are you sure?"
role_to_admin_confirmation: "%{name} will have the same privileges as you, so they could even demote or block you. Proceed only if you really trust %{name}. Are you sure?"
role_user: 'User'
role_moderator: 'Moderator'
role_admin: 'Administrator'
status_active: 'Active'
status_blocked: 'Blocked'
status_deleted: 'Deleted'
user_mailer:
opening_greeting: 'Hello!'
closing_greeting: 'Have a great day!'

View File

@@ -52,7 +52,6 @@ it:
header:
menu:
site_settings: 'Impostazioni sito'
admin_panel: 'Admin panel (deprecato)'
profile_settings: 'Impostazioni profilo'
sign_out: 'Esci'
log_in: 'Accedi / Registrati'
@@ -107,6 +106,7 @@ it:
boards: 'Bacheche'
post_statuses: 'Stati'
roadmap: 'Roadmap'
users: 'Utenti'
info_box:
up_to_date: 'Tutte le modifiche sono state salvate'
error: 'Si è verificato un errore: %{message}'
@@ -128,6 +128,19 @@ it:
title2: 'Non mostrati in roadmap'
empty: 'La roadmap è vuota.'
help: "Puoi aggiungere stati alla roadmap trascinandoli dalla sezione sottostante. Se invece vuoi creare un nuovo stato o cambiarne l'ordine, vai in Impostazioni sito -> Stati."
users:
title: 'Utenti'
block: 'Blocca'
unblock: 'Sblocca'
block_confirmation: "%{name} non potrà effettuare l'accesso al sito finché non sarà sbloccato. Sei sicuro?"
unblock_confirmation: "%{name} potrà effettuare l'accesso al sito e pubblicare contenuti. Sei sicuro?"
role_to_admin_confirmation: "%{name} avrà i tuoi stessi privilegi, quindi potrebbe anche retrocederti a utente o bloccarti. Procedi solo se ti fidi di %{name}. Sei sicuro?"
role_user: 'Utente'
role_moderator: 'Moderatore'
role_admin: 'Amministratore'
status_active: 'Attivo'
status_blocked: 'Bloccato'
status_deleted: 'Eliminato'
user_mailer:
opening_greeting: 'Ciao!'
closing_greeting: 'Buona giornata!'

View File

@@ -1,18 +1,9 @@
Rails.application.routes.draw do
root to: 'static_pages#roadmap'
get '/roadmap', to: 'static_pages#roadmap'
namespace :admin do
root to: 'boards#index'
resources :boards
resources :comments
resources :posts
resources :post_statuses
resources :users
end
devise_for :users
devise_for :users, :controllers => { :registrations => 'registrations' }
resources :users, only: [:index, :update]
resources :posts, only: [:index, :create, :show, :update, :destroy] do
resource :follows, only: [:create, :destroy]
@@ -36,5 +27,6 @@ Rails.application.routes.draw do
get 'boards'
get 'post_statuses'
get 'roadmap'
get 'users'
end
end

View File

@@ -0,0 +1,5 @@
class AddStatusToUsers < ActiveRecord::Migration[6.0]
def change
add_column :users, :status, :integer
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2022_05_21_161950) do
ActiveRecord::Schema.define(version: 2022_06_22_092039) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -106,6 +106,7 @@ ActiveRecord::Schema.define(version: 2022_05_21_161950) do
t.integer "role"
t.string "full_name"
t.boolean "notifications_enabled", default: true, null: false
t.integer "status"
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true

View File

@@ -22,4 +22,20 @@ FactoryBot.define do
password { 'password' }
role { 'admin' }
end
factory :blocked, class: User do
sequence(:email) { |n| "admin#{n}@example.com" }
full_name { 'First Last' }
password { 'password' }
status { 'blocked' }
end
factory :deleted, class: User do
sequence(:email) { |n| "admin#{n}@example.com" }
full_name { 'First Last' }
password { 'password' }
status { 'deleted' }
end
end

View File

@@ -9,7 +9,7 @@ RSpec.describe User, type: :model do
expect(user).to be_valid
end
it 'creates a user with role "user" by default' do
it 'has role "user" by default' do
expect(User.new.role).to eq('user')
end
@@ -23,6 +23,24 @@ RSpec.describe User, type: :model do
expect(admin).to be_valid
end
it 'has status "active" by default' do
expect(User.new.status).to eq('active')
end
it 'can have the following statuses: "active", "blocked" and "deleted"' do
active = user
blocked = FactoryBot.build(:blocked)
deleted = FactoryBot.build(:deleted)
expect(user.status).to eq('active')
expect(blocked.status).to eq('blocked')
expect(deleted.status).to eq('deleted')
expect(user).to be_valid
expect(blocked).to be_valid
expect(deleted).to be_valid
end
it 'has a non-nil and non-empty full name' do
nil_name_user = FactoryBot.build(:user, full_name: nil)
empty_name_user = FactoryBot.build(:user, full_name: '')
@@ -54,4 +72,11 @@ RSpec.describe User, type: :model do
expect(moderator).to be_a_power_user
expect(admin).to be_a_power_user
end
it 'knows if it is active or blocked' do
expect(user).to be_active
blocked = FactoryBot.build(:blocked)
expect(blocked).to be_blocked
end
end

View File

@@ -1,148 +0,0 @@
require 'rails_helper'
RSpec.describe 'requests to boards in the admin panel', :admin_panel, type: :request do
let(:user) { FactoryBot.create(:user) }
let(:moderator) { FactoryBot.create(:moderator) }
let(:admin) { FactoryBot.create(:admin) }
let(:board) { FactoryBot.create(:board) }
context 'when user is not logged in' do
it 'redirects index action' do
get admin_boards_path
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects show action' do
get admin_board_path(board)
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects new action' do
get new_admin_board_path
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects edit action' do
get edit_admin_board_path(board)
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects create action' do
post admin_boards_path, params: { board: { name: board.name + 'a' } }
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects update action' do
patch admin_board_path(board), params: { board: { name: board.name + 'a' } }
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects destroy action' do
delete admin_board_path(board)
expect(response).to redirect_to(new_user_session_path)
end
end
context 'when user has role "user"' do
before(:each) do
user.confirm
sign_in user
end
it 'redirects index action' do
get admin_boards_path
expect(response).to redirect_to(root_path)
end
it 'redirects show action' do
get admin_board_path(board)
expect(response).to redirect_to(root_path)
end
it 'redirects new action' do
get new_admin_board_path
expect(response).to redirect_to(root_path)
end
it 'redirects edit action' do
get edit_admin_board_path(board)
expect(response).to redirect_to(root_path)
end
it 'redirects create action' do
post admin_boards_path, params: { board: { name: board.name + 'a' } }
expect(response).to redirect_to(root_path)
end
it 'redirects update action' do
patch admin_board_path(board), params: { board: { name: board.name + 'a' } }
expect(response).to redirect_to(root_path)
end
it 'redirects destroy action' do
delete admin_board_path(board)
expect(response).to redirect_to(root_path)
end
end
context 'when user has role "moderator"' do
before(:each) do
moderator.confirm
sign_in moderator
end
it 'fulfills index action' do
get admin_boards_path
expect(response).to have_http_status(:success)
end
it 'fulfills show action' do
get admin_board_path(board)
expect(response).to have_http_status(:success)
end
it 'fulfills new action' do
get new_admin_board_path
expect(response).to have_http_status(:success)
end
it 'fulfills edit action' do
get edit_admin_board_path(board)
expect(response).to have_http_status(:success)
end
it 'fulfills create action' do
post admin_boards_path, params: { board: { name: board.name + 'a' } }
expect(response).to redirect_to(admin_board_path(board.id + 1))
end
it 'fulfills update action' do
patch admin_board_path(board), params: { board: { name: board.name + 'a' } }
expect(response).to redirect_to(admin_board_path(board))
end
it 'fulfills destroy action' do
delete admin_board_path(board)
expect(response).to redirect_to(admin_root_path)
end
end
context 'when user has role "admin"' do
before(:each) do
admin.confirm
sign_in admin
end
it 'fulfills index action' do
get admin_boards_path
expect(response).to have_http_status(:success)
end
it 'fulfills show action' do
get admin_board_path(board)
expect(response).to have_http_status(:success)
end
it 'fulfills new action' do
get new_admin_board_path
expect(response).to have_http_status(:success)
end
it 'fulfills edit action' do
get edit_admin_board_path(board)
expect(response).to have_http_status(:success)
end
it 'fulfills create action' do
post admin_boards_path, params: { board: { name: board.name + 'a' } }
expect(response).to redirect_to(admin_board_path(board.id + 1))
end
it 'fulfills update action' do
patch admin_board_path(board), params: { board: { name: board.name + 'a' } }
expect(response).to redirect_to(admin_board_path(board))
end
it 'fulfills destroy action' do
delete admin_board_path(board)
expect(response).to redirect_to(admin_root_path)
end
end
end

View File

@@ -1,148 +0,0 @@
require 'rails_helper'
RSpec.describe 'requests to post statuses in the admin panel', :admin_panel, type: :request do
let(:user) { FactoryBot.create(:user) }
let(:moderator) { FactoryBot.create(:moderator) }
let(:admin) { FactoryBot.create(:admin) }
let(:post_status) { FactoryBot.create(:post_status) }
context 'when user is not logged in' do
it 'redirects index action' do
get admin_post_statuses_path
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects show action' do
get admin_post_status_path(post_status)
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects new action' do
get new_admin_post_status_path
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects edit action' do
get edit_admin_post_status_path(post_status)
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects create action' do
post admin_post_statuses_path, params: { post_status: { name: post_status.name + 'a' } }
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects update action' do
patch admin_post_status_path(post_status), params: { post_status: { name: post_status.name + 'a' } }
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects destroy action' do
delete admin_post_status_path(post_status)
expect(response).to redirect_to(new_user_session_path)
end
end
context 'when user has role "user"' do
before(:each) do
user.confirm
sign_in user
end
it 'redirects index action' do
get admin_post_statuses_path
expect(response).to redirect_to(root_path)
end
it 'redirects show action' do
get admin_post_status_path(post_status)
expect(response).to redirect_to(root_path)
end
it 'redirects new action' do
get new_admin_post_status_path
expect(response).to redirect_to(root_path)
end
it 'redirects edit action' do
get edit_admin_post_status_path(post_status)
expect(response).to redirect_to(root_path)
end
it 'redirects create action' do
post admin_post_statuses_path, params: { post_status: { name: post_status.name + 'a' } }
expect(response).to redirect_to(root_path)
end
it 'redirects update action' do
patch admin_post_status_path(post_status), params: { post_status: { name: post_status.name + 'a' } }
expect(response).to redirect_to(root_path)
end
it 'redirects destroy action' do
delete admin_post_status_path(post_status)
expect(response).to redirect_to(root_path)
end
end
context 'when user has role "moderator"' do
before(:each) do
moderator.confirm
sign_in moderator
end
it 'fulfills index action' do
get admin_post_statuses_path
expect(response).to have_http_status(:success)
end
it 'fulfills show action' do
get admin_post_status_path(post_status)
expect(response).to have_http_status(:success)
end
it 'fulfills new action' do
get new_admin_post_status_path
expect(response).to have_http_status(:success)
end
it 'fulfills edit action' do
get edit_admin_post_status_path(post_status)
expect(response).to have_http_status(:success)
end
it 'fulfills create action' do
post admin_post_statuses_path, params: { post_status: { name: post_status.name + 'a', color: post_status.color } }
expect(response).to redirect_to(admin_post_status_path(post_status.id + 1))
end
it 'fulfills update action' do
patch admin_post_status_path(post_status), params: { post_status: { name: post_status.name + 'a' } }
expect(response).to redirect_to(admin_post_status_path(post_status))
end
it 'fulfills destroy action' do
delete admin_post_status_path(post_status)
expect(response).to redirect_to(admin_post_statuses_path)
end
end
context 'when user has role "admin"' do
before(:each) do
admin.confirm
sign_in admin
end
it 'fulfills index action' do
get admin_post_statuses_path
expect(response).to have_http_status(:success)
end
it 'fulfills show action' do
get admin_post_status_path(post_status)
expect(response).to have_http_status(:success)
end
it 'fulfills new action' do
get new_admin_post_status_path
expect(response).to have_http_status(:success)
end
it 'fulfills edit action' do
get edit_admin_post_status_path(post_status)
expect(response).to have_http_status(:success)
end
it 'fulfills create action' do
post admin_post_statuses_path, params: { post_status: { name: post_status.name + 'a', color: post_status.color } }
expect(response).to redirect_to(admin_post_status_path(post_status.id + 1))
end
it 'fulfills update action' do
patch admin_post_status_path(post_status), params: { post_status: { name: post_status.name + 'a' } }
expect(response).to redirect_to(admin_post_status_path(post_status))
end
it 'fulfills destroy action' do
delete admin_post_status_path(post_status)
expect(response).to redirect_to(admin_post_statuses_path)
end
end
end

View File

@@ -1,146 +0,0 @@
require 'rails_helper'
RSpec.describe 'requests to users in the admin panel', :admin_panel, type: :request do
let(:user) { FactoryBot.create(:user) }
let(:moderator) { FactoryBot.create(:moderator) }
let(:admin) { FactoryBot.create(:admin) }
context 'when user is not logged in' do
it 'redirects index action' do
get admin_users_path
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects show action' do
get admin_user_path(user)
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects new action' do
get new_admin_user_path
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects edit action' do
get edit_admin_user_path(user)
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects create action' do
post admin_users_path, params: { user: { full_name: user.full_name, email: user.email + 'a', password: user.password } }
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects update action' do
patch admin_user_path(user), params: { user: { full_name: user.full_name } }
expect(response).to redirect_to(new_user_session_path)
end
it 'redirects destroy action' do
delete admin_user_path(user)
expect(response).to redirect_to(new_user_session_path)
end
end
context 'when user has role "user"' do
before(:each) do
user.confirm
sign_in user
end
it 'redirects index action' do
get admin_users_path
expect(response).to redirect_to(root_path)
end
it 'redirects show action' do
get admin_user_path(user)
expect(response).to redirect_to(root_path)
end
it 'redirects new action' do
get new_admin_user_path
expect(response).to redirect_to(root_path)
end
it 'redirects edit action' do
get edit_admin_user_path(user)
expect(response).to redirect_to(root_path)
end
it 'redirects create action' do
post admin_users_path, params: { user: { full_name: user.full_name, email: user.email + 'a', password: user.password } }
expect(response).to redirect_to(root_path)
end
it 'redirects update action' do
patch admin_user_path(user), params: { user: { full_name: user.full_name } }
expect(response).to redirect_to(root_path)
end
it 'redirects destroy action' do
delete admin_user_path(user)
expect(response).to redirect_to(root_path)
end
end
context 'when user has role "moderator"' do
before(:each) do
moderator.confirm
sign_in moderator
end
it 'redirects index action' do
get admin_users_path
expect(response).to redirect_to(root_path)
end
it 'redirects show action' do
get admin_user_path(user)
expect(response).to redirect_to(root_path)
end
it 'redirects new action' do
get new_admin_user_path
expect(response).to redirect_to(root_path)
end
it 'redirects edit action' do
get edit_admin_user_path(user)
expect(response).to redirect_to(root_path)
end
it 'redirects create action' do
post admin_users_path, params: { user: { full_name: user.full_name, email: user.email + 'a', password: user.password } }
expect(response).to redirect_to(root_path)
end
it 'redirects update action' do
patch admin_user_path(user), params: { user: { full_name: user.full_name } }
expect(response).to redirect_to(root_path)
end
it 'redirects destroy action' do
delete admin_user_path(user)
expect(response).to redirect_to(root_path)
end
end
context 'when user has role "admin"' do
before(:each) do
admin.confirm
sign_in admin
end
it 'fulfills index action' do
get admin_users_path
expect(response).to have_http_status(:success)
end
it 'fulfills show action' do
get admin_user_path(user)
expect(response).to have_http_status(:success)
end
it 'fulfills new action' do
get new_admin_user_path
expect(response).to have_http_status(:success)
end
it 'fulfills edit action' do
get edit_admin_user_path(user)
expect(response).to have_http_status(:success)
end
it 'fulfills create action' do
post admin_users_path, params: { user: { full_name: user.full_name, email: user.email + 'a', password: user.password } }
expect(response).to redirect_to(admin_user_path(user.id + 1))
end
it 'fulfills update action' do
patch admin_user_path(user), params: { user: { full_name: user.full_name + 'a', password: '' } }
expect(response).to redirect_to(admin_user_path(user))
end
it 'fulfills destroy action' do
delete admin_user_path(user)
expect(response).to redirect_to(admin_users_path)
end
end
end

View File

@@ -1,128 +0,0 @@
require 'rails_helper'
RSpec.describe 'admin panel routing', :aggregate_failures, type: :routing do
it 'routes root to boards index action' do
expect(get: '/admin').to route_to(
controller: 'admin/boards', action: 'index')
end
it 'routes boards' do
expect(get: '/admin/boards').to route_to(
controller: 'admin/boards', action: 'index'
)
expect(get: '/admin/boards/1').to route_to(
controller: 'admin/boards', action: 'show', id: '1'
)
expect(get: '/admin/boards/new').to route_to(
controller: 'admin/boards', action: 'new'
)
expect(get: '/admin/boards/1/edit').to route_to(
controller: 'admin/boards', action: 'edit', id: '1'
)
expect(post: '/admin/boards').to route_to(
controller: 'admin/boards', action: 'create'
)
expect(patch: '/admin/boards/1').to route_to(
controller: 'admin/boards', action: 'update', id: '1'
)
expect(delete: '/admin/boards/1').to route_to(
controller: 'admin/boards', action: 'destroy', id: '1'
)
end
it 'routes comments' do
expect(get: '/admin/comments').to route_to(
controller: 'admin/comments', action: 'index'
)
expect(get: '/admin/comments/1').to route_to(
controller: 'admin/comments', action: 'show', id: '1'
)
expect(get: '/admin/comments/new').to route_to(
controller: 'admin/comments', action: 'new'
)
expect(get: '/admin/comments/1/edit').to route_to(
controller: 'admin/comments', action: 'edit', id: '1'
)
expect(post: '/admin/comments').to route_to(
controller: 'admin/comments', action: 'create'
)
expect(patch: '/admin/comments/1').to route_to(
controller: 'admin/comments', action: 'update', id: '1'
)
expect(delete: '/admin/comments/1').to route_to(
controller: 'admin/comments', action: 'destroy', id: '1'
)
end
it 'routes posts' do
expect(get: '/admin/posts').to route_to(
controller: 'admin/posts', action: 'index'
)
expect(get: '/admin/posts/1').to route_to(
controller: 'admin/posts', action: 'show', id: '1'
)
expect(get: '/admin/posts/new').to route_to(
controller: 'admin/posts', action: 'new'
)
expect(get: '/admin/posts/1/edit').to route_to(
controller: 'admin/posts', action: 'edit', id: '1'
)
expect(post: '/admin/posts').to route_to(
controller: 'admin/posts', action: 'create'
)
expect(patch: '/admin/posts/1').to route_to(
controller: 'admin/posts', action: 'update', id: '1'
)
expect(delete: '/admin/posts/1').to route_to(
controller: 'admin/posts', action: 'destroy', id: '1'
)
end
it 'routes post statuses' do
expect(get: '/admin/post_statuses').to route_to(
controller: 'admin/post_statuses', action: 'index'
)
expect(get: '/admin/post_statuses/1').to route_to(
controller: 'admin/post_statuses', action: 'show', id: '1'
)
expect(get: '/admin/post_statuses/new').to route_to(
controller: 'admin/post_statuses', action: 'new'
)
expect(get: '/admin/post_statuses/1/edit').to route_to(
controller: 'admin/post_statuses', action: 'edit', id: '1'
)
expect(post: '/admin/post_statuses').to route_to(
controller: 'admin/post_statuses', action: 'create'
)
expect(patch: '/admin/post_statuses/1').to route_to(
controller: 'admin/post_statuses', action: 'update', id: '1'
)
expect(delete: '/admin/post_statuses/1').to route_to(
controller: 'admin/post_statuses', action: 'destroy', id: '1'
)
end
it 'routes users' do
expect(get: '/admin/users').to route_to(
controller: 'admin/users', action: 'index'
)
expect(get: '/admin/users/1').to route_to(
controller: 'admin/users', action: 'show', id: '1'
)
expect(get: '/admin/users/new').to route_to(
controller: 'admin/users', action: 'new'
)
expect(get: '/admin/users/1/edit').to route_to(
controller: 'admin/users', action: 'edit', id: '1'
)
expect(post: '/admin/users').to route_to(
controller: 'admin/users', action: 'create'
)
expect(patch: '/admin/users/1').to route_to(
controller: 'admin/users', action: 'update', id: '1'
)
expect(delete: '/admin/users/1').to route_to(
controller: 'admin/users', action: 'destroy', id: '1'
)
end
end