diff --git a/app/controllers/admin/bulk_assign_role_controller.rb b/app/controllers/admin/bulk_assign_role_controller.rb new file mode 100644 index 000000000..9f1d82ee9 --- /dev/null +++ b/app/controllers/admin/bulk_assign_role_controller.rb @@ -0,0 +1,70 @@ +module Admin + class BulkAssignRoleController < Admin::ApplicationController + layout "admin" + include Admin::UsersHelper + + def assign_role + if permitted_params[:role].blank? + raise ArgumentError, + I18n.t("admin.bulk_assign_role_controller.role_blank") + end + + role = permitted_params[:role] + usernames = permitted_params[:usernames].downcase.split(/\s*,\s*/) + note = permitted_params[:note_for_current_role].presence + note ||= I18n.t("admin.bulk_assign_role_controller.role_assigment", role: role) + + begin + usernames.each do |username| + user = User.find_by(username: username) + user_action_status = user_action_status(user, role) + if user + Moderator::ManageActivityAndRoles.handle_user_roles( + admin: current_user, + user: user, + user_params: { user_status: role, note_for_current_role: note }, + ) + end + + AuditLog.create( + category: "admin.bulk_assign_role.add_role", + user: current_user, + roles: current_user.roles_name, + slug: "bulk_assign_role", + data: { + role: role, + username: username, + user_action_status: user_action_status + }, + ) + end + flash[:success] = I18n.t("admin.bulk_assign_role_controller.success_message") + rescue StandardError => e + flash[:danger] = e.message + end + redirect_to admin_bulk_assign_role_index_path + rescue ArgumentError => e + flash[:danger] = e.message + redirect_to admin_bulk_assign_role_index_path + end + + private + + # We need to override this method from Admin::ApplicationController since + # there is no resource to authorize. + def authorization_resource; end + + def user_action_status(user, role) + if user + return "user_already_has_the_role" if user_status(user) == role + + return "role_applied_successfully" + end + "user_not_found" + end + + def permitted_params + params.permit(:usernames, :role, :note_for_current_role) + end + end +end diff --git a/app/models/admin_menu.rb b/app/models/admin_menu.rb index 2d97b1efe..56d20ceb2 100644 --- a/app/models/admin_menu.rb +++ b/app/models/admin_menu.rb @@ -8,6 +8,7 @@ class AdminMenu item(name: "members", controller: "users"), item(name: "invited members", controller: "invitations"), item(name: "gdpr actions", controller: "gdpr_delete_requests"), + item(name: "bulk assign role", controller: "bulk_assign_role"), ] scope :content_manager, "dashboard-line", [ diff --git a/app/views/admin/bulk_assign_role/index.html.erb b/app/views/admin/bulk_assign_role/index.html.erb new file mode 100644 index 000000000..80988155c --- /dev/null +++ b/app/views/admin/bulk_assign_role/index.html.erb @@ -0,0 +1,25 @@ +
+

<%= t("views.admin.users.actions.bulk_assign_role.text") %>

+
+
+ <%= form_with(url: admin_bulk_assign_role_path, local: true, class: "flex flex-col gap-4") do |f| %> +
+ <%= f.label :role, t("views.admin.users.actions.assign_role.role"), class: "crayons-field__label" %> + <%= f.select("role", role_options(current_user)["Statuses"], { include_blank: t("views.admin.users.actions.assign_role.select") }, class: "crayons-select") %> +
+
+ <%= f.label :usernames, "Usernames", class: "crayons-field__label" %> +

+ <%= t("views.admin.users.actions.assign_role.warning") %> +

+ <%= f.text_area :usernames, placeholder: "username1, username2, username3", required: true, size: "40x3", class: "crayons-textfield" %> +
+
+ <%= f.label :note_for_current_role, t("views.admin.users.actions.assign_role.note"), class: "crayons-field__label" %> + <%= f.text_area :note_for_current_role, required: true, class: "crayons-textfield", placeholder: "" %> +
+
+ <%= f.submit t("views.admin.users.actions.assign_role.text"), class: "c-btn c-btn--primary" %> +
+ <% end %> +
diff --git a/config/locales/controllers/admin/en.yml b/config/locales/controllers/admin/en.yml index 439e99b11..dc90e5f95 100644 --- a/config/locales/controllers/admin/en.yml +++ b/config/locales/controllers/admin/en.yml @@ -19,6 +19,10 @@ en: deleted: Broadcast has been deleted! updated: Broadcast has been updated! wrong: Something went wrong with deleting the broadcast. + bulk_assign_role_controller: + role_assigment: Role %{role} assigned to user. + role_blank: Please choose a role to add. + success_message: Roles are being added. The task will finish shortly. consumer_apps_controller: created: '%{app} has been created!' deleted: '%{app} has been deleted!' diff --git a/config/locales/controllers/admin/fr.yml b/config/locales/controllers/admin/fr.yml index f5d4bede0..7f84b26b9 100644 --- a/config/locales/controllers/admin/fr.yml +++ b/config/locales/controllers/admin/fr.yml @@ -19,6 +19,10 @@ fr: deleted: Le Broadcast a été supprimée ! updated: Le Broadcast a été mise à jour ! wrong: Quelque chose s'est mal passé avec la suppression du Broadcast. + bulk_assign_role_controller: + role_assigment: Role %{role} assigned to user. + role_blank: Please choose a role to add. + success_message: Roles are being added. The task will finish shortly. consumer_apps_controller: created: '%{app} a été créé !' deleted: '%{app} a été supprimé !' diff --git a/config/locales/views/admin/en.yml b/config/locales/views/admin/en.yml index 301832142..eb1b6d639 100644 --- a/config/locales/views/admin/en.yml +++ b/config/locales/views/admin/en.yml @@ -113,6 +113,7 @@ en: select: Select role note: "Add a note to this action:" submit: Add + warning: "Comma Delimited, All Lowercase, No @ Symbol Required In Front Of Usernames Warning: Usernames are case-sensitive and must be all lowercase! This task will fail silently when supplied with incorrect usernames." add_org: text: Add organization heading: Add organization @@ -136,6 +137,8 @@ en: aria_credits: Amount of credits to add or remove note: "Add a note to this action:" submit: Adjust balance + bulk_assign_role: + text: Bulk Assign Role copy_email: Copy email address danger: desc1: This action is irreversible. diff --git a/config/locales/views/admin/fr.yml b/config/locales/views/admin/fr.yml index d6cd02357..3da193b26 100644 --- a/config/locales/views/admin/fr.yml +++ b/config/locales/views/admin/fr.yml @@ -113,6 +113,7 @@ fr: select: Select role note: "Add a note to this action:" submit: Add + warning: "Comma Delimited, All Lowercase, No @ Symbol Required In Front Of Usernames Warning: Usernames are case-sensitive and must be all lowercase! This task will fail silently when supplied with incorrect usernames." add_org: text: Add organization heading: Add organization @@ -136,6 +137,8 @@ fr: aria_credits: Amount of credits to add or remove note: "Add a note to this action:" submit: Adjust balance + bulk_assign_role: + text: Bulk Assign Role copy_email: Copy email address danger: desc1: This action is irreversible. diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 5fce1d6b8..38e446e4f 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -67,6 +67,9 @@ namespace :admin do end resources :gdpr_delete_requests, only: %i[index destroy] + + resources :bulk_assign_role, only: %i[index] + post "/bulk_assign_role", to: "bulk_assign_role#assign_role" end scope :content_manager do diff --git a/cypress/e2e/seededFlows/adminFlows/bulk_assign_role/bulkAssignRole.spec.js b/cypress/e2e/seededFlows/adminFlows/bulk_assign_role/bulkAssignRole.spec.js new file mode 100644 index 000000000..8a847b297 --- /dev/null +++ b/cypress/e2e/seededFlows/adminFlows/bulk_assign_role/bulkAssignRole.spec.js @@ -0,0 +1,42 @@ +import { + verifyAndDismissUserUpdatedMessage, + verifyAndDismissDangerMessage, +} from '../users/userAdminUtilitites'; +describe('Bulk Assign Role', () => { + beforeEach(() => { + cy.testSetup(); + cy.fixture('users/adminUser.json').as('user'); + + cy.get('@user').then((user) => { + cy.loginAndVisit(user, '/admin/member_manager/bulk_assign_role'); + }); + }); + + it('should show success message for correct fields ', () => { + cy.get('.crayons-select').select('Trusted'); + + cy.get('.crayons-textfield[name="usernames"]').type('test1, test2, test3'); + + cy.get('.crayons-textfield[name="note_for_current_role"]').type( + 'Sample note', + ); + + cy.findByText('Assign role').click(); + + verifyAndDismissUserUpdatedMessage( + 'Roles are being added. The task will finish shortly.', + ); + }); + + it('should show error message if role is not selected ', () => { + cy.get('.crayons-textfield[name="usernames"]').type('test1, test2, test3'); + + cy.get('.crayons-textfield[name="note_for_current_role"]').type( + 'Sample note', + ); + + cy.findByText('Assign role').click(); + + verifyAndDismissDangerMessage('Please choose a role to add.'); + }); +}); diff --git a/cypress/e2e/seededFlows/adminFlows/users/userAdminUtilitites.js b/cypress/e2e/seededFlows/adminFlows/users/userAdminUtilitites.js index 85a17bb1f..4a49e47ee 100644 --- a/cypress/e2e/seededFlows/adminFlows/users/userAdminUtilitites.js +++ b/cypress/e2e/seededFlows/adminFlows/users/userAdminUtilitites.js @@ -20,3 +20,26 @@ export function verifyAndDismissUserUpdatedMessage( cy.findByTestId('flash-success').should('not.exist'); } + +/** + * E2E helper function for user admin tests that validates the correct flash danger message appears + * + * @param {string} [message="There was some error"] The expected flash message text + */ +export function verifyAndDismissDangerMessage( + message = 'There was some error', +) { + cy.findByTestId('flash-danger') + .as('danger') + .then((element) => { + expect(element.text().trim()).equal(message); + }); + + cy.get('@danger').within(() => { + cy.findByRole('button', { name: 'Dismiss message' }) + .should('have.focus') + .click(); + }); + + cy.findByTestId('flash-danger').should('not.exist'); +} diff --git a/spec/requests/admin/bulk_assign_role_spec.rb b/spec/requests/admin/bulk_assign_role_spec.rb new file mode 100644 index 000000000..2f59e3e0c --- /dev/null +++ b/spec/requests/admin/bulk_assign_role_spec.rb @@ -0,0 +1,118 @@ +require "rails_helper" + +RSpec.describe "Admin::BulkAssignRole" do + let(:admin) { create(:user, :super_admin) } + let(:user1) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:usernames_string) { "#{user1.username}, #{user2.username}, #{user3.username}" } + let(:role) { "Trusted" } + let(:note_for_current_role) { "Assigned Trusted role" } + let(:extra_space_usernames_string) { "#{user1.username}, #{user2.username}, #{user3.username}" } + let(:invalid_role) { "Invalid Role" } + + describe "POST /admin/member_manager/bulk_assign_role" do + before do + sign_in admin + end + + it "adds trusted role successfully" do + params = { usernames: usernames_string, role: role, note_for_current_role: note_for_current_role } + post admin_bulk_assign_role_path, params: params + + expect(response).to redirect_to(admin_bulk_assign_role_index_path) + expect(flash[:success]).to eq(I18n.t("admin.bulk_assign_role_controller.success_message")) + + expect(user1.roles.count).to eq(1) + expect(user1.roles.last.name).to eq(role.downcase) + + expect(user2.roles.count).to eq(1) + expect(user2.roles.last.name).to eq(role.downcase) + + expect(user3.roles.count).to eq(1) + expect(user3.roles.last.name).to eq(role.downcase) + end + + it "adds role with extra whitespace in usernames" do + params = { usernames: extra_space_usernames_string, role: role, note_for_current_role: note_for_current_role } + post admin_bulk_assign_role_path, params: params + + expect(user1.roles.count).to eq(1) + expect(user1.roles.last.name).to eq(role.downcase) + + expect(user2.roles.count).to eq(1) + expect(user2.roles.last.name).to eq(role.downcase) + + expect(user3.roles.count).to eq(1) + expect(user3.roles.last.name).to eq(role.downcase) + end + + it "shows error if role is blank" do + params = { usernames: usernames_string, note_for_current_role: note_for_current_role } + post admin_bulk_assign_role_path, params: params + + expect(response).to redirect_to(admin_bulk_assign_role_index_path) + expect(flash[:danger]).to eq(I18n.t("admin.bulk_assign_role_controller.role_blank")) + + expect(user1.roles.count).to eq(0) + expect(user2.roles.count).to eq(0) + expect(user3.roles.count).to eq(0) + end + + it "adds default note if user input is empty" do + params = { usernames: usernames_string, role: role } + post admin_bulk_assign_role_path, params: params + + expect(user1.notes.last.content).to eq(I18n.t("admin.bulk_assign_role_controller.role_assigment", role: role)) + end + + it "adds role to valid usernames only and creates user_not_found AuditLog" do + invalid_username = "invalidusername" + params = { usernames: "#{user1.username}, #{user2.username}, #{invalid_username}", role: role, + note_for_current_role: note_for_current_role } + post admin_bulk_assign_role_path, params: params + + expect(user1.roles.count).to eq(1) + expect(user1.roles.last.name).to eq(role.downcase) + + expect(user2.roles.count).to eq(1) + expect(user2.roles.last.name).to eq(role.downcase) + + log = AuditLog.last + expect(log.data["role"]).to eq(role) + expect(log.data["username"]).to eq(invalid_username) + expect(log.data["user_action_status"]).to eq("user_not_found") + end + + it "adds successful AuditLog" do + params = { usernames: usernames_string, role: role, note_for_current_role: note_for_current_role } + post admin_bulk_assign_role_path, params: params + + logs = AuditLog.last(3) + expect(logs[0].category).to eq("admin.bulk_assign_role.add_role") + expect(logs[0].user).to eq(admin) + + expect(logs[0].data["username"]).to eq(user1.username) + expect(logs[0].data["user_action_status"]).to eq("role_applied_successfully") + + expect(logs[1].data["username"]).to eq(user2.username) + expect(logs[1].data["user_action_status"]).to eq("role_applied_successfully") + + expect(logs[2].data["username"]).to eq(user3.username) + expect(logs[2].data["user_action_status"]).to eq("role_applied_successfully") + end + + it "adds role already applied AuditLog" do + params = { usernames: user1.username.to_s, role: role, note_for_current_role: note_for_current_role } + post admin_bulk_assign_role_path, params: params + + # Adding the role again to trigger the "user_already_has_the_role" AuditLog. + post admin_bulk_assign_role_path, params: params + + log = AuditLog.last + expect(log.data["role"]).to eq(role) + expect(log.data["username"]).to eq(user1.username) + expect(log.data["user_action_status"]).to eq("user_already_has_the_role") + end + end +end