Admin Area: Bulk assign user roles (#19733)

* Bulk Assign Roles UI

* Renaming roles to role

* Added role to multiple users without using note.

* Role note added

* Role blank key fix

* Removed roles

* typo fix

* AuditLog Added

* Optimising code

* Good standing role issue

* Good standing role issue fix

* Minor PR changes based on review

* Added cypress tests

* Added spec tests for BulkAssignRole

* Minor fixes
This commit is contained in:
Rajat Talesra 2023-07-18 18:41:54 +05:30 committed by GitHub
parent 20b643ac8f
commit f2cc2ae774
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 296 additions and 0 deletions

View file

@ -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

View file

@ -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", [

View file

@ -0,0 +1,25 @@
<header class="flex justify-between items-center mb-4">
<h1 class="crayons-title"><%= t("views.admin.users.actions.bulk_assign_role.text") %></h1>
</header>
<div class="crayons-card p-6">
<%= form_with(url: admin_bulk_assign_role_path, local: true, class: "flex flex-col gap-4") do |f| %>
<div class="crayons-field">
<%= 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") %>
</div>
<div class="crayons-field">
<%= f.label :usernames, "Usernames", class: "crayons-field__label" %>
<p class="crayons-field__description">
<%= t("views.admin.users.actions.assign_role.warning") %>
</p>
<%= f.text_area :usernames, placeholder: "username1, username2, username3", required: true, size: "40x3", class: "crayons-textfield" %>
</div>
<div class="crayons-field">
<%= 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: "" %>
</div>
<div>
<%= f.submit t("views.admin.users.actions.assign_role.text"), class: "c-btn c-btn--primary" %>
</div>
<% end %>
</div>

View file

@ -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!'

View file

@ -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é !'

View file

@ -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.

View file

@ -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.

View file

@ -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

View file

@ -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.');
});
});

View file

@ -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');
}

View file

@ -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