Skip to content

Commit

Permalink
Alec/12433 add judge and atty selectors to judgeteam page (#13191)
Browse files Browse the repository at this point in the history
Resolves #12433

### Description

- `OrganizationsUsers` page has the ability to recognize and properly render a Judge Team.  
- A Judge Team admin can add and remove the Decision Drafting Attorney role from users on the team.  
- Labels identifying the assigned roles of each user (Judge Team Lead, Decision Drafting Attorney & Admin) as well as descriptions of those roles are displayed for user clarity.

### Acceptance Criteria
#### Frontend
- [ ] JudgeTeam page has the ability to assign and remove Decision Drafting Attorney role to team members
- [ ] Labels representing assigned roles for each user are displayed
- [ ] Changes do not modify appearance of other organizations' pages

#### Backend
- [ ] `Organizations::UsersController` accepts `attorney` params and updates the `DecisionDraftingAttorney` role via the `OrganizationsUser` model
- [ ] Roles are properly retrieved via an update to `administered_user_serializer`
- [ ] Functionality is tied to feature flag

#### Additional Changes
- [ ] Ensure Admin role button properly working via updates to the following models: `make_user_admin` in `organizations_user` and `add_user` in `judge_team` 

BEFORE
<img width="1225" alt="Screen Shot 2020-01-16 at 4 47 55 PM" src="https://user-images.githubusercontent.com/18618189/72568058-3b01c900-3885-11ea-8d3d-4ccd2fee2d5d.png">

AFTER
![judge_team](https://user-images.githubusercontent.com/18618189/72569719-08f26600-3889-11ea-92e4-74623c5f910c.gif)

### Testing Plan
1. Login as Judge BVAAABSHIRE and navigate to the team management page http://localhost:3000/organizations/bvaaabshire/users
2. Ensure the "Enable Decision Drafting" button gives a user the role of "Decision Drafting Attorney"
3. Ensure the "Disable Decision Drafting" button removes "Decision Drafting Attorney" role from user
  • Loading branch information
ajspotts authored Jan 28, 2020
1 parent 7f2d6d5 commit 4f9db91
Show file tree
Hide file tree
Showing 9 changed files with 461 additions and 163 deletions.
27 changes: 22 additions & 5 deletions app/controllers/organizations/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def index

render json: {
organization_name: organization.name,
judge_team: FeatureToggle.enabled?(:judge_admin_scm) && organization.type == JudgeTeam.name,
organization_users: json_administered_users(organization_users)
}
end
Expand All @@ -25,11 +26,11 @@ def update
no_cache

if params.key?(:admin)
if params[:admin] == true
OrganizationsUser.make_user_admin(user_to_modify, organization)
else
OrganizationsUser.remove_admin_rights_from_user(user_to_modify, organization)
end
adjust_admin_rights
end

if params.key?(:attorney)
adjust_decision_drafting_ability
end

render json: { users: json_administered_users([user_to_modify]) }, status: :ok
Expand Down Expand Up @@ -59,6 +60,22 @@ def user_to_modify
@user_to_modify ||= User.find(params.require(:id))
end

def adjust_admin_rights
if params[:admin] == true
OrganizationsUser.make_user_admin(user_to_modify, organization)
else
OrganizationsUser.remove_admin_rights_from_user(user_to_modify, organization)
end
end

def adjust_decision_drafting_ability
if params[:attorney] == true
OrganizationsUser.enable_decision_drafting(user_to_modify, organization)
else
OrganizationsUser.disable_decision_drafting(user_to_modify, organization)
end
end

def organization_url
params[:organization_url]
end
Expand Down
23 changes: 22 additions & 1 deletion app/models/organizations_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ class OrganizationsUser < ApplicationRecord
# Use instead: organization.add_user(user)

def self.make_user_admin(user, organization)
organization.add_user(user).tap do |org_user|
organization_user = OrganizationsUser.existing_record(user, organization) || organization.add_user(user)
organization_user.tap do |org_user|
org_user.update!(admin: true)
end
end
Expand All @@ -34,4 +35,24 @@ def self.remove_user_from_organization(user, organization)
def self.existing_record(user, organization)
find_by(organization_id: organization.id, user_id: user.id)
end

def self.enable_decision_drafting(user, organization)
org_user = existing_record(user, organization)
return nil unless org_user&.judge_team_role && FeatureToggle.enabled?(:judge_admin_scm)
if org_user.judge_team_role.is_a?(JudgeTeamLead)
fail Caseflow::Error::ActionForbiddenError, message: COPY::JUDGE_TEAM_ATTORNEY_RIGHTS_ERROR
else
org_user.judge_team_role.update!(type: DecisionDraftingAttorney)
end
end

def self.disable_decision_drafting(user, organization)
org_user = existing_record(user, organization)
return nil unless org_user&.judge_team_role && FeatureToggle.enabled?(:judge_admin_scm)
if org_user.judge_team_role.is_a?(JudgeTeamLead)
fail Caseflow::Error::ActionForbiddenError, message: COPY::JUDGE_TEAM_ATTORNEY_RIGHTS_ERROR
else
org_user.judge_team_role.update!(type: nil)
end
end
end
10 changes: 10 additions & 0 deletions app/models/serializers/work_queue/administered_user_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,14 @@ class WorkQueue::AdministeredUserSerializer < WorkQueue::UserSerializer
attribute :admin do |object, params|
params[:organization].user_is_admin?(object)
end
attribute :judge do |object, params|
if params[:organization].type == JudgeTeam.name
params[:organization].judge&.eql?(object)
end
end
attribute :attorney do |object, params|
if params[:organization].type == JudgeTeam.name
params[:organization].attorneys&.include?(object)
end
end
end
24 changes: 19 additions & 5 deletions client/COPY.json
Original file line number Diff line number Diff line change
Expand Up @@ -455,19 +455,32 @@
"LOOKUP_PARTICIPANT_ID_SELECT_USER_LABEL": "Select user",
"LOOKUP_PARTICIPANT_ID_MODAL_NO_ORGS_MESSAGE": "User does not belong to any POA organizations.",

"USER_MANAGEMENT_PAGE_TITLE": "%s management page",
"USER_MANAGEMENT_PAGE_TITLE": "Team management for %s",
"USER_MANAGEMENT_INITIAL_LOAD_ERROR_TITLE": "Failed to load users",
"USER_MANAGEMENT_INITIAL_LOAD_LOADING_MESSAGE": "Loading users...",
"USER_MANAGEMENT_ADD_USER_ERROR_TITLE": "Failed to add user",
"USER_MANAGEMENT_ADD_USER_LOADING_MESSAGE": "Adding user",
"USER_MANAGEMENT_REMOVE_USER_ERROR_TITLE": "Failed to remove user",
"USER_MANAGEMENT_ADMIN_RIGHTS_CHANGE_ERROR_TITLE": "Failed to modify user admin rights",
"USER_MANAGEMENT_GIVE_USER_ADMIN_RIGHTS_BUTTON_TEXT": "Make user admin",
"USER_MANAGEMENT_REMOVE_USER_ADMIN_RIGHTS_BUTTON_TEXT": "Remove admin rights",
"USER_MANAGEMENT_REMOVE_USER_FROM_ORG_BUTTON_TEXT": "Remove user",
"USER_MANAGEMENT_DECISION_DRAFTING_CHANGE_ERROR_TITLE": "Failed to modify decision drafting status",
"USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_NAME": "Add user",
"USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_LABEL": "Add a user to the team:",
"USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_LABEL": "Add new team members",
"USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_TEXT": "User CSS ID to add",
"USER_MANAGEMENT_EDIT_USER_IN_ORG_LABEL": "Edit current team members",
"USER_MANAGEMENT_ADMIN_RIGHTS_HEADING": "Add team admin / Remove admin rights: ",
"USER_MANAGEMENT_ADMIN_RIGHTS_DESCRIPTION": "Determines which users have the same team admin rights as the Judge Team Lead to request, assign, and view atorneys' cases.",
"USER_MANAGEMENT_REMOVE_USER_HEADING": "Remove from team: ",
"USER_MANAGEMENT_REMOVE_USER_DESCRIPTION": "Remove a user from this judge team.",
"USER_MANAGEMENT_DECISION_DRAFTING_HEADING": "Enable / Disable decision drafting: ",
"USER_MANAGEMENT_DECISION_DRAFTING_DESCRIPTION": "Determine which team members are attorneys who draft decisions. The Judge Team Lead and team admins can view the attorneys' case assignments, assign them new cases, and reassign their cases from the Assign Cases page.",
"USER_MANAGEMENT_JUDGE_LABEL": "Judge Team Lead",
"USER_MANAGEMENT_ATTORNEY_LABEL": "Decision-Drafting Attorney",
"USER_MANAGEMENT_ADMIN_LABEL": "Team Admin",
"USER_MANAGEMENT_DISABLE_DECISION_DRAFTING_BUTTON_TEXT": "Disable decision drafting",
"USER_MANAGEMENT_ENABLE_DECISION_DRAFTING_BUTTON_TEXT": "Enable decision drafting",
"USER_MANAGEMENT_GIVE_USER_ADMIN_RIGHTS_BUTTON_TEXT": "Add team admin",
"USER_MANAGEMENT_REMOVE_USER_ADMIN_RIGHTS_BUTTON_TEXT": "Remove admin rights",
"USER_MANAGEMENT_REMOVE_USER_FROM_ORG_BUTTON_TEXT": "Remove from team",

"ACCESS_DENIED_TITLE": "Additional access needed",
"UNAUTHORIZED_PAGE_ACCESS_MESSAGE": "You aren't authorized to use this part of Caseflow yet.",
Expand Down Expand Up @@ -635,6 +648,7 @@
"VIRTUAL_HEARING_MODAL_CONFIRMATION": "Changes to the Veteran and POA / Representative emails will be used to send notifications <strong>for this hearing only</strong>.",

"JUDGE_TEAM_REMOVE_JUDGE_ERROR": "Cannot remove a judge from their judge team.",
"JUDGE_TEAM_ATTORNEY_RIGHTS_ERROR": "Cannot edit attorney rights on non-attorneys",

"HEARING_UPDATE_SUCCESSFUL_TITLE": "You have successfully updated %s's hearing.",

Expand Down
168 changes: 119 additions & 49 deletions client/app/queue/OrganizationUsers.jsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-len */
import React from 'react';
import PropTypes from 'prop-types';
import { css } from 'glamor';
Expand All @@ -14,28 +15,41 @@ import { LOGO_COLORS } from '../constants/AppConstants';
import COPY from '../../COPY.json';
import LoadingDataDisplay from '../components/LoadingDataDisplay';

const buttonPaddingStyle = css({ margin: '0 1rem' });
const userStyle = css({ margin: '2rem 0 3rem',
borderBottom: '1rem solid gray',
borderWidth: '1px' });
const topUserStyle = css({ margin: '2rem 0 3rem',
borderTop: '1rem solid gray',
borderBottom: '1rem solid gray',
borderWidth: '1px',
paddingTop: '2.5rem' });
const buttonPaddingStyle = css({ marginRight: '1rem',
display: 'inline-block',
height: '6rem' });

export default class OrganizationUsers extends React.PureComponent {
constructor(props) {
super(props);

this.state = {
organizationName: null,
judgeTeam: null,
organizationUsers: [],
remainingUsers: [],
loading: true,
error: null,
addingUser: null,
removingUser: {},
changingAdminRights: {}
changingAdminRights: {},
changingDecisionDrafting: {}
};
}

loadingPromise = () => {
return ApiUtil.get(`/organizations/${this.props.organization}/users`).then((response) => {
this.setState({
organizationName: response.body.organization_name,
judgeTeam: response.body.judge_team,
organizationUsers: response.body.organization_users.data,
remainingUsers: [],
loading: false
Expand Down Expand Up @@ -71,9 +85,10 @@ export default class OrganizationUsers extends React.PureComponent {
addingUser: value
});

ApiUtil.post(`/organizations/${this.props.organization}/users`, { data }).then(() => {
ApiUtil.post(`/organizations/${this.props.organization}/users`, { data }).then((response) => {

this.setState({
organizationUsers: [...this.state.organizationUsers, value],
organizationUsers: [...this.state.organizationUsers, response.body.users.data[0]],
remainingUsers: this.state.remainingUsers.filter((user) => user.id !== value.id),
addingUser: null
});
Expand Down Expand Up @@ -123,36 +138,63 @@ export default class OrganizationUsers extends React.PureComponent {
});
}

modifyAdminRights = (user, adminFlag) => () => {
modifyUser = (user, flagName) => {
this.setState({
changingAdminRights: { ...this.state.changingAdminRights,
[flagName]: { ...this.state[flagName],
[user.id]: true }
});
}

modifyUserSuccess = (response, user, flagName) => {
const updatedUser = response.body.users.data[0];
// Replace the existing version of the user so it has the updated attributes
const updatedUserList = this.state.organizationUsers.map((existingUser) => {
return (existingUser.id === updatedUser.id) ? updatedUser : existingUser;
});

this.setState({
organizationUsers: updatedUserList,
[flagName]: { ...this.state[flagName],
[user.id]: false }
});
}

modifyUserError = (title, body, user, flagName) => {
this.setState({
[flagName]: { ...this.state[flagName],
[user.id]: false },
error: {
title,
body
}
});
}

modifyAdminRights = (user, adminFlag) => () => {
const flagName = 'changingAdminRights';

this.modifyUser(user, flagName);

const payload = { data: { admin: adminFlag } };

ApiUtil.patch(`/organizations/${this.props.organization}/users/${user.id}`, payload).then((response) => {
const updatedUser = response.body.users.data[0];
this.modifyUserSuccess(response, user, flagName);
}, (error) => {
this.modifyUserError(COPY.USER_MANAGEMENT_ADMIN_RIGHTS_CHANGE_ERROR_TITLE, error.message, user, flagName);
});
}

// Replace the existing version of the user so it has the correct admin priveleges.
const updatedUserList = this.state.organizationUsers.map((existingUser) => {
return (existingUser.id === updatedUser.id) ? updatedUser : existingUser;
});
modifyDecisionDrafting = (user, attorneyFlag) => () => {
const flagName = 'changingDecisionDrafting';

this.setState({
organizationUsers: updatedUserList,
changingAdminRights: { ...this.state.changingAdminRights,
[user.id]: false }
});
this.modifyUser(user, flagName);

const payload = { data: { attorney: attorneyFlag } };

ApiUtil.patch(`/organizations/${this.props.organization}/users/${user.id}`, payload).then((response) => {
this.modifyUserSuccess(response, user, flagName);
}, (error) => {
this.setState({
changingAdminRights: { ...this.state.changingAdminRights,
[user.id]: false },
error: {
title: COPY.USER_MANAGEMENT_ADMIN_RIGHTS_CHANGE_ERROR_TITLE,
body: error.message
}
});
this.modifyUserError(COPY.USER_MANAGEMENT_DECISION_DRAFTING_CHANGE_ERROR_TITLE, error.message, user, flagName);
});
}

Expand All @@ -173,35 +215,49 @@ export default class OrganizationUsers extends React.PureComponent {
});
}

decisionDraftingButton = (user, attorney) =>
<span {...buttonPaddingStyle}><Button
name={attorney ? COPY.USER_MANAGEMENT_DISABLE_DECISION_DRAFTING_BUTTON_TEXT : COPY.USER_MANAGEMENT_ENABLE_DECISION_DRAFTING_BUTTON_TEXT}
id={attorney ? `Disable-decision-drafting-${user.id}` : `Enable-decision-drafting-${user.id}`}
classNames={attorney ? ['usa-button-secondary'] : ['usa-button-primary']}
loading={this.state.changingDecisionDrafting[user.id]}
onClick={this.modifyDecisionDrafting(user, !attorney)} /></span>

adminButton = (user, admin) =>
<span {...buttonPaddingStyle}><Button
name={admin ? COPY.USER_MANAGEMENT_REMOVE_USER_ADMIN_RIGHTS_BUTTON_TEXT : COPY.USER_MANAGEMENT_GIVE_USER_ADMIN_RIGHTS_BUTTON_TEXT}
id={admin ? `Remove-admin-rights-${user.id}` : `Add-team-admin-${user.id}`}
classNames={admin ? ['usa-button-secondary'] : ['usa-button-primary']}
loading={this.state.changingAdminRights[user.id]}
onClick={this.modifyAdminRights(user, !admin)} /></span>

removeUserButton = (user) =>
<span {...buttonPaddingStyle}><Button
name={COPY.USER_MANAGEMENT_REMOVE_USER_FROM_ORG_BUTTON_TEXT}
id={`Remove-user-${user.id}`}
classNames={['usa-button-secondary']}
loading={this.state.removingUser[user.id]}
onClick={this.removeUser(user)} /></span>

mainContent = () => {
const listOfUsers = this.state.organizationUsers.map((user) => {
return <li key={user.id}>{this.formatName(user)} &nbsp;
<span {...buttonPaddingStyle}>
{ !user.attributes.admin && <Button
name={COPY.USER_MANAGEMENT_GIVE_USER_ADMIN_RIGHTS_BUTTON_TEXT}
id={`Make-user-admin-${user.id}`}
classNames={['usa-button-primary']}
loading={this.state.changingAdminRights[user.id]}
onClick={this.modifyAdminRights(user, true)} /> }
{ user.attributes.admin && <Button
name={COPY.USER_MANAGEMENT_REMOVE_USER_ADMIN_RIGHTS_BUTTON_TEXT}
id={`Remove-admin-rights-${user.id}`}
classNames={['usa-button-secondary']}
loading={this.state.changingAdminRights[user.id]}
onClick={this.modifyAdminRights(user, false)} /> }
</span>
<Button
name={COPY.USER_MANAGEMENT_REMOVE_USER_FROM_ORG_BUTTON_TEXT}
id={`Remove-user-${user.id}`}
classNames={['usa-button-secondary']}
loading={this.state.removingUser[user.id]}
onClick={this.removeUser(user)} />
</li>;
const judgeTeam = this.state.judgeTeam;
const listOfUsers = this.state.organizationUsers.map((user, i) => {
const { judge, attorney, admin } = user.attributes;
const style = i === 0 ? topUserStyle : userStyle;

return <div {...style}>
<li key={user.id}>{this.formatName(user)}
{ judgeTeam && judge && <strong> ( {COPY.USER_MANAGEMENT_JUDGE_LABEL} )</strong> }
{ judgeTeam && attorney && <strong> ( {COPY.USER_MANAGEMENT_ATTORNEY_LABEL} )</strong> }
{ judgeTeam && admin && <strong> ( {COPY.USER_MANAGEMENT_ADMIN_LABEL} )</strong> } &nbsp;</li>
{ judgeTeam && !judge && this.decisionDraftingButton(user, attorney) }
{ this.adminButton(user, admin) }
{ this.removeUserButton(user) }
</div>;
});

return <React.Fragment>
<ul>{listOfUsers}</ul>
<h1>{COPY.USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_LABEL}</h1>
<h2>{COPY.USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_LABEL}</h2>
<SearchableDropdown
name={COPY.USER_MANAGEMENT_ADD_USER_TO_ORG_DROPDOWN_NAME}
hideLabel
Expand All @@ -215,6 +271,20 @@ export default class OrganizationUsers extends React.PureComponent {
value={null}
onChange={this.addUser}
async={this.asyncLoadUser} />
<br />
<div>
{ judgeTeam &&
<div>
<h2>{COPY.USER_MANAGEMENT_EDIT_USER_IN_ORG_LABEL}</h2>
<ul>
<li><strong>{COPY.USER_MANAGEMENT_DECISION_DRAFTING_HEADING}</strong>{COPY.USER_MANAGEMENT_DECISION_DRAFTING_DESCRIPTION}</li>
<li><strong>{COPY.USER_MANAGEMENT_ADMIN_RIGHTS_HEADING}</strong>{COPY.USER_MANAGEMENT_ADMIN_RIGHTS_DESCRIPTION}</li>
<li><strong>{COPY.USER_MANAGEMENT_REMOVE_USER_HEADING}</strong>{COPY.USER_MANAGEMENT_REMOVE_USER_DESCRIPTION}</li>
</ul>
</div>
}
<ul>{listOfUsers}</ul>
</div>
</React.Fragment>;
}

Expand Down
Loading

0 comments on commit 4f9db91

Please sign in to comment.