From 37fb99a868ef3de2edd4fae74b6f9560a3aacf45 Mon Sep 17 00:00:00 2001 From: Riccardo Graziosi <31478034+riggraz@users.noreply.github.com> Date: Fri, 24 Jun 2022 14:39:35 +0200 Subject: [PATCH] Add users management to site settings (#126) --- Gemfile | 5 +- Gemfile.lock | 29 ---- .../admin/application_controller.rb | 19 --- app/controllers/admin/boards_controller.rb | 14 -- app/controllers/admin/comments_controller.rb | 15 -- .../admin/post_statuses_controller.rb | 14 -- app/controllers/admin/posts_controller.rb | 15 -- app/controllers/admin/users_controller.rb | 69 -------- app/controllers/application_controller.rb | 6 +- app/controllers/registrations_controller.rb | 11 ++ app/controllers/site_settings_controller.rb | 6 +- app/controllers/users_controller.rb | 36 ++++ app/dashboards/board_dashboard.rb | 71 -------- app/dashboards/comment_dashboard.rb | 76 --------- app/dashboards/post_dashboard.rb | 79 --------- app/dashboards/post_status_dashboard.rb | 75 --------- app/dashboards/user_dashboard.rb | 88 ---------- app/fields/color_field.rb | 7 - app/fields/id_field.rb | 7 - app/fields/role_field.rb | 13 -- app/helpers/application_helper.rb | 19 ++- app/javascript/actions/User/requestUsers.ts | 60 +++++++ app/javascript/actions/User/updateUser.ts | 87 ++++++++++ .../Boards/BoardsSiteSettingsP.tsx | 2 +- .../SiteSettings/Users/UserEditable.tsx | 157 ++++++++++++++++++ .../SiteSettings/Users/UserForm.tsx | 79 +++++++++ .../SiteSettings/Users/UsersSiteSettingsP.tsx | 103 ++++++++++++ .../components/SiteSettings/Users/index.tsx | 39 +++++ .../containers/UsersSiteSettings.tsx | 49 ++++++ app/javascript/interfaces/IUser.ts | 29 ++++ app/javascript/interfaces/json/IUser.ts | 9 + .../reducers/SiteSettings/usersReducer.ts | 58 +++++++ app/javascript/reducers/boardsReducer.ts | 2 +- app/javascript/reducers/rootReducer.ts | 2 + .../reducers/siteSettingsReducer.ts | 32 +++- app/javascript/reducers/usersReducer.ts | 75 +++++++++ app/javascript/stylesheets/common/_index.scss | 5 + .../stylesheets/components/Post.scss | 6 - .../components/SiteSettings/Users/index.scss | 68 ++++++++ app/javascript/stylesheets/main.scss | 1 + app/models/user.rb | 31 ++++ app/policies/user_policy.rb | 25 +++ app/views/admin/application/_flashes.html.erb | 20 --- app/views/admin/application/_form.html.erb | 45 ----- app/views/admin/application/_icons.html.erb | 13 -- .../admin/application/_javascript.html.erb | 21 --- .../admin/application/_navigation.html.erb | 26 --- .../admin/application/_stylesheet.html.erb | 14 -- app/views/fields/color_field/_form.html.erb | 6 - app/views/fields/color_field/_index.html.erb | 3 - app/views/fields/color_field/_show.html.erb | 4 - app/views/fields/role_field/_form.html.erb | 14 -- app/views/fields/role_field/_index.html.erb | 1 - app/views/fields/role_field/_show.html.erb | 1 - app/views/layouts/_header.html.erb | 8 +- app/views/layouts/admin/application.html.erb | 41 ----- app/views/site_settings/_menu.html.erb | 10 +- app/views/site_settings/users.html.erb | 15 ++ config/locales/devise/devise.en.yml | 1 + config/locales/devise/devise.it.yml | 1 + config/locales/en.yml | 15 +- config/locales/it.yml | 15 +- config/routes.rb | 14 +- .../20220622092039_add_status_to_users.rb | 5 + db/schema.rb | 3 +- spec/factories/users.rb | 16 ++ spec/models/user_spec.rb | 27 ++- spec/requests/admin_panel_boards_spec.rb | 148 ----------------- .../admin_panel_post_statuses_spec.rb | 148 ----------------- spec/requests/admin_panel_users_spec.rb | 146 ---------------- spec/routing/admin_panel_routing_spec.rb | 128 -------------- 71 files changed, 1093 insertions(+), 1409 deletions(-) delete mode 100644 app/controllers/admin/application_controller.rb delete mode 100644 app/controllers/admin/boards_controller.rb delete mode 100644 app/controllers/admin/comments_controller.rb delete mode 100644 app/controllers/admin/post_statuses_controller.rb delete mode 100644 app/controllers/admin/posts_controller.rb delete mode 100644 app/controllers/admin/users_controller.rb create mode 100644 app/controllers/registrations_controller.rb create mode 100644 app/controllers/users_controller.rb delete mode 100644 app/dashboards/board_dashboard.rb delete mode 100644 app/dashboards/comment_dashboard.rb delete mode 100644 app/dashboards/post_dashboard.rb delete mode 100644 app/dashboards/post_status_dashboard.rb delete mode 100644 app/dashboards/user_dashboard.rb delete mode 100644 app/fields/color_field.rb delete mode 100644 app/fields/id_field.rb delete mode 100644 app/fields/role_field.rb create mode 100644 app/javascript/actions/User/requestUsers.ts create mode 100644 app/javascript/actions/User/updateUser.ts create mode 100644 app/javascript/components/SiteSettings/Users/UserEditable.tsx create mode 100644 app/javascript/components/SiteSettings/Users/UserForm.tsx create mode 100644 app/javascript/components/SiteSettings/Users/UsersSiteSettingsP.tsx create mode 100644 app/javascript/components/SiteSettings/Users/index.tsx create mode 100644 app/javascript/containers/UsersSiteSettings.tsx create mode 100644 app/javascript/interfaces/IUser.ts create mode 100644 app/javascript/interfaces/json/IUser.ts create mode 100644 app/javascript/reducers/SiteSettings/usersReducer.ts create mode 100644 app/javascript/reducers/usersReducer.ts create mode 100644 app/javascript/stylesheets/components/SiteSettings/Users/index.scss create mode 100644 app/policies/user_policy.rb delete mode 100644 app/views/admin/application/_flashes.html.erb delete mode 100644 app/views/admin/application/_form.html.erb delete mode 100644 app/views/admin/application/_icons.html.erb delete mode 100644 app/views/admin/application/_javascript.html.erb delete mode 100644 app/views/admin/application/_navigation.html.erb delete mode 100644 app/views/admin/application/_stylesheet.html.erb delete mode 100644 app/views/fields/color_field/_form.html.erb delete mode 100644 app/views/fields/color_field/_index.html.erb delete mode 100644 app/views/fields/color_field/_show.html.erb delete mode 100644 app/views/fields/role_field/_form.html.erb delete mode 100644 app/views/fields/role_field/_index.html.erb delete mode 100644 app/views/fields/role_field/_show.html.erb delete mode 100644 app/views/layouts/admin/application.html.erb create mode 100644 app/views/site_settings/users.html.erb create mode 100644 db/migrate/20220622092039_add_status_to_users.rb delete mode 100644 spec/requests/admin_panel_boards_spec.rb delete mode 100644 spec/requests/admin_panel_post_statuses_spec.rb delete mode 100644 spec/requests/admin_panel_users_spec.rb delete mode 100644 spec/routing/admin_panel_routing_spec.rb diff --git a/Gemfile b/Gemfile index d564eac9..718137ec 100644 --- a/Gemfile +++ b/Gemfile @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock index 3927b61d..0ba312b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb deleted file mode 100644 index 34b9e79c..00000000 --- a/app/controllers/admin/application_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/admin/boards_controller.rb b/app/controllers/admin/boards_controller.rb deleted file mode 100644 index 6516d3e2..00000000 --- a/app/controllers/admin/boards_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/admin/comments_controller.rb b/app/controllers/admin/comments_controller.rb deleted file mode 100644 index 879a37b0..00000000 --- a/app/controllers/admin/comments_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/admin/post_statuses_controller.rb b/app/controllers/admin/post_statuses_controller.rb deleted file mode 100644 index 785ed107..00000000 --- a/app/controllers/admin/post_statuses_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/admin/posts_controller.rb b/app/controllers/admin/posts_controller.rb deleted file mode 100644 index 655059d5..00000000 --- a/app/controllers/admin/posts_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb deleted file mode 100644 index d6cdd8b8..00000000 --- a/app/controllers/admin/users_controller.rb +++ /dev/null @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d71a8ec0..3d32aed3 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb new file mode 100644 index 00000000..d6833751 --- /dev/null +++ b/app/controllers/registrations_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/site_settings_controller.rb b/app/controllers/site_settings_controller.rb index 1b0b2240..0e14b772 100644 --- a/app/controllers/site_settings_controller.rb +++ b/app/controllers/site_settings_controller.rb @@ -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 \ No newline at end of file diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 00000000..bad4a5b3 --- /dev/null +++ b/app/controllers/users_controller.rb @@ -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 diff --git a/app/dashboards/board_dashboard.rb b/app/dashboards/board_dashboard.rb deleted file mode 100644 index f241360e..00000000 --- a/app/dashboards/board_dashboard.rb +++ /dev/null @@ -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 diff --git a/app/dashboards/comment_dashboard.rb b/app/dashboards/comment_dashboard.rb deleted file mode 100644 index 82a6ff4c..00000000 --- a/app/dashboards/comment_dashboard.rb +++ /dev/null @@ -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 diff --git a/app/dashboards/post_dashboard.rb b/app/dashboards/post_dashboard.rb deleted file mode 100644 index 869d6d31..00000000 --- a/app/dashboards/post_dashboard.rb +++ /dev/null @@ -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 diff --git a/app/dashboards/post_status_dashboard.rb b/app/dashboards/post_status_dashboard.rb deleted file mode 100644 index 478cd90b..00000000 --- a/app/dashboards/post_status_dashboard.rb +++ /dev/null @@ -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 diff --git a/app/dashboards/user_dashboard.rb b/app/dashboards/user_dashboard.rb deleted file mode 100644 index e0e142c9..00000000 --- a/app/dashboards/user_dashboard.rb +++ /dev/null @@ -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 diff --git a/app/fields/color_field.rb b/app/fields/color_field.rb deleted file mode 100644 index 1ba760c5..00000000 --- a/app/fields/color_field.rb +++ /dev/null @@ -1,7 +0,0 @@ -require "administrate/field/base" - -class ColorField < Administrate::Field::Base - def to_s - data.to_s - end -end diff --git a/app/fields/id_field.rb b/app/fields/id_field.rb deleted file mode 100644 index c155d4ff..00000000 --- a/app/fields/id_field.rb +++ /dev/null @@ -1,7 +0,0 @@ -require "administrate/field/base" - -class IdField < Administrate::Field::Base - def to_s - data.to_s - end -end diff --git a/app/fields/role_field.rb b/app/fields/role_field.rb deleted file mode 100644 index 0c5e804c..00000000 --- a/app/fields/role_field.rb +++ /dev/null @@ -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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index af9cfc89..ce75b1b4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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 diff --git a/app/javascript/actions/User/requestUsers.ts b/app/javascript/actions/User/requestUsers.ts new file mode 100644 index 00000000..3fda1cec --- /dev/null +++ b/app/javascript/actions/User/requestUsers.ts @@ -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; +} + +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 +): UsersRequestActionTypes => ({ + type: USERS_REQUEST_SUCCESS, + users, +}); + +const usersRequestFailure = (error: string): UsersRequestActionTypes => ({ + type: USERS_REQUEST_FAILURE, + error, +}); + +export const requestUsers = (): ThunkAction> => ( + async (dispatch) => { + dispatch(usersRequestStart()); + + try { + const response = await fetch('/users'); + const json = await response.json(); + + dispatch(usersRequestSuccess(json)); + } catch (e) { + dispatch(usersRequestFailure(e)); + } + } +) \ No newline at end of file diff --git a/app/javascript/actions/User/updateUser.ts b/app/javascript/actions/User/updateUser.ts new file mode 100644 index 00000000..257c8e9e --- /dev/null +++ b/app/javascript/actions/User/updateUser.ts @@ -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> => 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); + } +}; \ No newline at end of file diff --git a/app/javascript/components/SiteSettings/Boards/BoardsSiteSettingsP.tsx b/app/javascript/components/SiteSettings/Boards/BoardsSiteSettingsP.tsx index ab9c10ef..708d3142 100644 --- a/app/javascript/components/SiteSettings/Boards/BoardsSiteSettingsP.tsx +++ b/app/javascript/components/SiteSettings/Boards/BoardsSiteSettingsP.tsx @@ -92,7 +92,7 @@ class BoardsSiteSettingsP extends React.Component { return ( <> -

{I18n.t('site_settings.boards.title')}

+

{ I18n.t('site_settings.boards.title') }

{ boards.items.length > 0 ? diff --git a/app/javascript/components/SiteSettings/Users/UserEditable.tsx b/app/javascript/components/SiteSettings/Users/UserEditable.tsx new file mode 100644 index 00000000..7a0f3a69 --- /dev/null +++ b/app/javascript/components/SiteSettings/Users/UserEditable.tsx @@ -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 { + 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 ( +
  • + { + editMode === false ? + <> +
    + + +
    + { user.fullName } + +
    + + { I18n.t(`site_settings.users.role_${user.role}`) } + + + { + user.status !== USER_STATUS_ACTIVE ? + <> + + + { I18n.t(`site_settings.users.status_${user.status}`) } + + + : + null + } +
    +
    +
    + + + + : + <> + + + { I18n.t('common.buttons.cancel') } + + + } +
  • + ); + } +} + +export default UserEditable; \ No newline at end of file diff --git a/app/javascript/components/SiteSettings/Users/UserForm.tsx b/app/javascript/components/SiteSettings/Users/UserForm.tsx new file mode 100644 index 00000000..306f64cd --- /dev/null +++ b/app/javascript/components/SiteSettings/Users/UserForm.tsx @@ -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 { + 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 ( +
    + + +
    + { user.fullName } + + +
    + + +
    + ); + } +} + +export default UserForm; \ No newline at end of file diff --git a/app/javascript/components/SiteSettings/Users/UsersSiteSettingsP.tsx b/app/javascript/components/SiteSettings/Users/UsersSiteSettingsP.tsx new file mode 100644 index 00000000..cd55d315 --- /dev/null +++ b/app/javascript/components/SiteSettings/Users/UsersSiteSettingsP.tsx @@ -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; + 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 { + 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 ( + <> + +

    { I18n.t('site_settings.users.title') }

    + +
      + { + users.items.map((user, i) => ( + + )) + } +
    +
    + + + + ); + } +} + +export default UsersSiteSettingsP; \ No newline at end of file diff --git a/app/javascript/components/SiteSettings/Users/index.tsx b/app/javascript/components/SiteSettings/Users/index.tsx new file mode 100644 index 00000000..1ccfa8b8 --- /dev/null +++ b/app/javascript/components/SiteSettings/Users/index.tsx @@ -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 { + store: Store; + + constructor(props: Props) { + super(props); + + this.store = createStoreHelper(); + } + + render() { + return ( + + + + ); + } +} + +export default UsersSiteSettingsRoot; \ No newline at end of file diff --git a/app/javascript/containers/UsersSiteSettings.tsx b/app/javascript/containers/UsersSiteSettings.tsx new file mode 100644 index 00000000..bde63960 --- /dev/null +++ b/app/javascript/containers/UsersSiteSettings.tsx @@ -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 { + 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); \ No newline at end of file diff --git a/app/javascript/interfaces/IUser.ts b/app/javascript/interfaces/IUser.ts new file mode 100644 index 00000000..25f32afb --- /dev/null +++ b/app/javascript/interfaces/IUser.ts @@ -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; \ No newline at end of file diff --git a/app/javascript/interfaces/json/IUser.ts b/app/javascript/interfaces/json/IUser.ts new file mode 100644 index 00000000..6a028af8 --- /dev/null +++ b/app/javascript/interfaces/json/IUser.ts @@ -0,0 +1,9 @@ +interface IUserJSON { + id: number; + email: string; + full_name: string; + role: string; + status: string; +} + +export default IUserJSON; \ No newline at end of file diff --git a/app/javascript/reducers/SiteSettings/usersReducer.ts b/app/javascript/reducers/SiteSettings/usersReducer.ts new file mode 100644 index 00000000..e48ece8e --- /dev/null +++ b/app/javascript/reducers/SiteSettings/usersReducer.ts @@ -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; \ No newline at end of file diff --git a/app/javascript/reducers/boardsReducer.ts b/app/javascript/reducers/boardsReducer.ts index 6377c8ac..0eb6755d 100644 --- a/app/javascript/reducers/boardsReducer.ts +++ b/app/javascript/reducers/boardsReducer.ts @@ -38,7 +38,7 @@ const initialState: BoardsState = { items: [], areLoading: false, error: '', -} +}; const boardsReducer = ( state = initialState, diff --git a/app/javascript/reducers/rootReducer.ts b/app/javascript/reducers/rootReducer.ts index 7cf54c4f..2e3c5d5c 100644 --- a/app/javascript/reducers/rootReducer.ts +++ b/app/javascript/reducers/rootReducer.ts @@ -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, }); diff --git a/app/javascript/reducers/siteSettingsReducer.ts b/app/javascript/reducers/siteSettingsReducer.ts index 6d1fdca9..76c12e24 100644 --- a/app/javascript/reducers/siteSettingsReducer.ts +++ b/app/javascript/reducers/siteSettingsReducer.ts @@ -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; } diff --git a/app/javascript/reducers/usersReducer.ts b/app/javascript/reducers/usersReducer.ts new file mode 100644 index 00000000..a3664e61 --- /dev/null +++ b/app/javascript/reducers/usersReducer.ts @@ -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; + 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; \ No newline at end of file diff --git a/app/javascript/stylesheets/common/_index.scss b/app/javascript/stylesheets/common/_index.scss index e03825d1..e9f153e9 100644 --- a/app/javascript/stylesheets/common/_index.scss +++ b/app/javascript/stylesheets/common/_index.scss @@ -172,4 +172,9 @@ background-color: $primary-color !important; border-color: $primary-color !important; } +} + +.selectPicker { + @extend + .custom-select; } \ No newline at end of file diff --git a/app/javascript/stylesheets/components/Post.scss b/app/javascript/stylesheets/components/Post.scss index c176f948..a499bd0d 100644 --- a/app/javascript/stylesheets/components/Post.scss +++ b/app/javascript/stylesheets/components/Post.scss @@ -119,12 +119,6 @@ @extend .d-flex, .justify-content-between; - - .selectPicker { - @extend - .custom-select, - .mx-2; - } } .postDescription { diff --git a/app/javascript/stylesheets/components/SiteSettings/Users/index.scss b/app/javascript/stylesheets/components/SiteSettings/Users/index.scss new file mode 100644 index 00000000..2b32a60f --- /dev/null +++ b/app/javascript/stylesheets/components/SiteSettings/Users/index.scss @@ -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; } + } +} \ No newline at end of file diff --git a/app/javascript/stylesheets/main.scss b/app/javascript/stylesheets/main.scss index 2787cbed..ac29d6f7 100644 --- a/app/javascript/stylesheets/main.scss +++ b/app/javascript/stylesheets/main.scss @@ -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'; \ No newline at end of file diff --git a/app/models/user.rb b/app/models/user.rb index 3ae70b5f..eb7a2fd1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000..bdb87441 --- /dev/null +++ b/app/policies/user_policy.rb @@ -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 \ No newline at end of file diff --git a/app/views/admin/application/_flashes.html.erb b/app/views/admin/application/_flashes.html.erb deleted file mode 100644 index 61de2711..00000000 --- a/app/views/admin/application/_flashes.html.erb +++ /dev/null @@ -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? %> -
    - <% flash.each do |key, value| -%> -
    <%= value.html_safe %>
    - <% end -%> -
    -<% end %> diff --git a/app/views/admin/application/_form.html.erb b/app/views/admin/application/_form.html.erb deleted file mode 100644 index 58ffef12..00000000 --- a/app/views/admin/application/_form.html.erb +++ /dev/null @@ -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? %> -
    -

    - <%= t( - "administrate.form.errors", - pluralized_errors: pluralize(page.resource.errors.count, t("administrate.form.error")), - resource_name: display_resource_name(page.resource_name) - ) %> -

    - -
      - <% page.resource.errors.full_messages.each do |message| %> -
    • <%= message %>
    • - <% end %> -
    -
    - <% end %> - - <% page.attributes.each do |attribute| -%> -
    - <%= render_field attribute, f: f %> -
    - <% end -%> - -
    - <%= f.submit %> -
    -<% end %> diff --git a/app/views/admin/application/_icons.html.erb b/app/views/admin/application/_icons.html.erb deleted file mode 100644 index 1add01ca..00000000 --- a/app/views/admin/application/_icons.html.erb +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/views/admin/application/_javascript.html.erb b/app/views/admin/application/_javascript.html.erb deleted file mode 100644 index 5197fe65..00000000 --- a/app/views/admin/application/_javascript.html.erb +++ /dev/null @@ -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 %> diff --git a/app/views/admin/application/_navigation.html.erb b/app/views/admin/application/_navigation.html.erb deleted file mode 100644 index bd7e3240..00000000 --- a/app/views/admin/application/_navigation.html.erb +++ /dev/null @@ -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 -%> - - diff --git a/app/views/admin/application/_stylesheet.html.erb b/app/views/admin/application/_stylesheet.html.erb deleted file mode 100644 index 7b7bb7e5..00000000 --- a/app/views/admin/application/_stylesheet.html.erb +++ /dev/null @@ -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 %> diff --git a/app/views/fields/color_field/_form.html.erb b/app/views/fields/color_field/_form.html.erb deleted file mode 100644 index a8cfadbc..00000000 --- a/app/views/fields/color_field/_form.html.erb +++ /dev/null @@ -1,6 +0,0 @@ -
    - <%= f.label field.attribute %> -
    -
    - <%= f.color_field field.attribute, style: 'height: 40px;' %> -
    diff --git a/app/views/fields/color_field/_index.html.erb b/app/views/fields/color_field/_index.html.erb deleted file mode 100644 index 986637f7..00000000 --- a/app/views/fields/color_field/_index.html.erb +++ /dev/null @@ -1,3 +0,0 @@ -
    -
    diff --git a/app/views/fields/color_field/_show.html.erb b/app/views/fields/color_field/_show.html.erb deleted file mode 100644 index 50a791a3..00000000 --- a/app/views/fields/color_field/_show.html.erb +++ /dev/null @@ -1,4 +0,0 @@ -
    -
    -(<%= field.to_s %>) \ No newline at end of file diff --git a/app/views/fields/role_field/_form.html.erb b/app/views/fields/role_field/_form.html.erb deleted file mode 100644 index fe93d33c..00000000 --- a/app/views/fields/role_field/_form.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -
    - <%= f.label field.attribute %> -
    -
    - <%= f.select field.attribute, field.select_field_values(f) %> -
    - -
    - ⚠️ -
    \ No newline at end of file diff --git a/app/views/fields/role_field/_index.html.erb b/app/views/fields/role_field/_index.html.erb deleted file mode 100644 index 1430540f..00000000 --- a/app/views/fields/role_field/_index.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= field.to_s.titleize %> diff --git a/app/views/fields/role_field/_show.html.erb b/app/views/fields/role_field/_show.html.erb deleted file mode 100644 index 1430540f..00000000 --- a/app/views/fields/role_field/_show.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= field.to_s.titleize %> diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb index 40d91f43..4507daab 100644 --- a/app/views/layouts/_header.html.erb +++ b/app/views/layouts/_header.html.erb @@ -35,13 +35,15 @@ \ No newline at end of file diff --git a/app/views/site_settings/users.html.erb b/app/views/site_settings/users.html.erb new file mode 100644 index 00000000..212a3a09 --- /dev/null +++ b/app/views/site_settings/users.html.erb @@ -0,0 +1,15 @@ +
    + <%= render 'menu' %> +
    + <%= + react_component( + 'SiteSettings/Users', + { + currentUserEmail: current_user.email, + currentUserRole: current_user.role, + authenticityToken: form_authenticity_token + } + ) + %> +
    +
    \ No newline at end of file diff --git a/config/locales/devise/devise.en.yml b/config/locales/devise/devise.en.yml index 55617bdf..2ad113f5 100644 --- a/config/locales/devise/devise.en.yml +++ b/config/locales/devise/devise.en.yml @@ -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" diff --git a/config/locales/devise/devise.it.yml b/config/locales/devise/devise.it.yml index af41ab0a..8b2f3c95 100644 --- a/config/locales/devise/devise.it.yml +++ b/config/locales/devise/devise.it.yml @@ -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" diff --git a/config/locales/en.yml b/config/locales/en.yml index 085921ac..3185dcb2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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!' diff --git a/config/locales/it.yml b/config/locales/it.yml index f52e7d5f..ad04acbe 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -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!' diff --git a/config/routes.rb b/config/routes.rb index 3da149e8..ad0d6580 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/db/migrate/20220622092039_add_status_to_users.rb b/db/migrate/20220622092039_add_status_to_users.rb new file mode 100644 index 00000000..dd4aec2f --- /dev/null +++ b/db/migrate/20220622092039_add_status_to_users.rb @@ -0,0 +1,5 @@ +class AddStatusToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :status, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 1c1ab87d..ec5ec0b8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 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 diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 4bbbccb5..f2bed8cf 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -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 diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index eff35f97..208a0b78 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/requests/admin_panel_boards_spec.rb b/spec/requests/admin_panel_boards_spec.rb deleted file mode 100644 index 49d7a129..00000000 --- a/spec/requests/admin_panel_boards_spec.rb +++ /dev/null @@ -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 \ No newline at end of file diff --git a/spec/requests/admin_panel_post_statuses_spec.rb b/spec/requests/admin_panel_post_statuses_spec.rb deleted file mode 100644 index fe8ec93c..00000000 --- a/spec/requests/admin_panel_post_statuses_spec.rb +++ /dev/null @@ -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 \ No newline at end of file diff --git a/spec/requests/admin_panel_users_spec.rb b/spec/requests/admin_panel_users_spec.rb deleted file mode 100644 index 99f7e0dc..00000000 --- a/spec/requests/admin_panel_users_spec.rb +++ /dev/null @@ -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 \ No newline at end of file diff --git a/spec/routing/admin_panel_routing_spec.rb b/spec/routing/admin_panel_routing_spec.rb deleted file mode 100644 index 913cc31b..00000000 --- a/spec/routing/admin_panel_routing_spec.rb +++ /dev/null @@ -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 \ No newline at end of file