Skip to content

Commit

Permalink
[Feature] new Community Manager role (#8094)
Browse files Browse the repository at this point in the history
* Add community manager to rolepermission seeder

* Give community managers ability to assign any team role
this will work better long term, AND be easier to implement

* create assignUserToTeam mutation

* Update mutation to affect only single user and single team

* Use User query with reduced scope, and don't for it to render table
Instead of Users query, get UserPublicProfiles, a safer query available to more user roles, and sufficient since we only need names. Also, don't wait for the potentially long-running user query to render the table, since its only required for adding new users to team.

* Abuse useMemo to avoid infinite re-rendering

* Show loading message on disabled Select until users are loaded

* Test community-manager role permissions

* Add PHPUnit tests for new mutation

* Check in Role enum

* Allow Community Manager to access some admin pages

* Seed comunity manager

* Reveal relevant links to Community Mngrs, on Admin

* Seed PlatformAdmin with all roles

* ensure authorizedToView scope doesn't block viewing of basic info

* Allow Community Managers to view all pools

* Use new mutation when editing roles on Team Members page

* Add french translation

* Remove unused variables

* Reverse change to user scopeAuthorizedToView

* Fix form input id

* Combine mutation arguments

* Add authorizedToView scope so Community Manager can view Pool pages (when not allowed to view candidates)

* Allow Community Managers to publish pools

* ACTUALLY allow Community Managers to publish pools

* Run PHP linter

---------

Co-authored-by: tristan-orourke <tristan.orourke@gmail.com>
  • Loading branch information
tristan-orourke and tristan-orourke authored Oct 20, 2023
1 parent 524e8cd commit 90bfd94
Show file tree
Hide file tree
Showing 27 changed files with 713 additions and 118 deletions.
13 changes: 13 additions & 0 deletions api/app/Enums/Role.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Enums;

enum Role: string
{
case BASE_USER = 'base_user';
case APPLICANT = 'applicant';
case POOL_OPERATOR = 'pool_operator';
case REQUEST_RESPONDER = 'request_responder';
case COMMUNITY_MANAGER = 'community_manager';
case PLATFORM_ADMIN = 'platform_admin';
}
30 changes: 30 additions & 0 deletions api/app/GraphQL/Mutations/UpdateUserTeamRoles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\GraphQL\Mutations;

use App\Models\Team;
use App\Models\User;

final class UpdateUserTeamRoles
{
/**
* Duplicates a pool
*
* @param array{} $args
*/
public function __invoke($_, array $args)
{
$team = Team::find($args['teamId']); // Even if we don't use the whole team object, ensure it exists.
$user = User::find($args['userId']);

// Assemble a roleAssignments object which makes sense to User->setRoleAssignmentsInputAttribute
// Do this by inserting the team id into each attach/detach/sync key of role assignments
$roleAssignments = $args['roleAssignments'];
foreach ($roleAssignments as $key => $value) {
$roleAssignments[$key]['team'] = $team->id;
}
$user->setRoleAssignmentsInputAttribute($roleAssignments);

return $team->fresh();
}
}
30 changes: 30 additions & 0 deletions api/app/GraphQL/Validators/TeamRolesInputValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace App\GraphQL\Validators;

use Illuminate\Validation\Rule;
use Nuwave\Lighthouse\Validation\Validator;

final class TeamRolesInputValidator extends Validator
{
public function __construct()
{
}

/**
* Return the validation rules.
*
* @return array<string, array<mixed>>
*/
public function rules(): array
{
return [
'roles.*' => [
'distinct',
Rule::exists('roles', 'id')->where(function ($query) {
return $query->where('is_team_based', true);
}),
],
];
}
}
11 changes: 10 additions & 1 deletion api/app/Policies/TeamPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ public function delete(User $user)

/**
* Determine whether the user can view a specific teams, team members.
* Likely to be updated later to allow the team admin and teammates to view their own team
*
* @param \App\Models\Team/null $team
* @return \Illuminate\Auth\Access\Response|bool
Expand All @@ -75,4 +74,14 @@ public function viewTeamMembers(User $user, Team $team)
{
return $user->isAbleTo('view-any-teamMembers') || $user->isAbleTo('view-team-teamMembers', $team);
}

/**
* Determine whether the user can assign any user to this team (giving them any team-based role)
*
* @return \Illuminate\Auth\Access\Response|bool
*/
public function assignTeamMembers(User $user, Team $team)
{
return $user->isAbleTo('assign-any-role') || $user->isAbleTo('assign-any-teamRole') || $user->isAbleTo('assign-team-role', $team);
}
}
37 changes: 36 additions & 1 deletion api/config/rolepermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
'role' => 'role',
'directiveForm' => 'directiveForm',
'applicantProfile' => 'applicantProfile',
'teamRole' => 'teamRole',
],

/*
Expand Down Expand Up @@ -381,6 +382,10 @@
'en' => 'Update metadata associated with any Role',
'fr' => 'Mettre à jour des métadonnées associées à tout rôle',
],
'assign-any-teamRole' => [
'en' => 'Assign any user to any team, with any role.',
'fr' => 'Affecter n\'importe quel utilisateur à n\'importe quelle équipe, avec n\'importe quel rôle.',
],

'create-any-directiveForm' => [
'en' => 'Create any directive form',
Expand Down Expand Up @@ -474,6 +479,18 @@
'is_team_based' => false,
],

'community_manager' => [
'display_name' => [
'en' => 'Community Manager',
'fr' => 'Gestionnaire de communauté',
],
'description' => [
'en' => 'Publishes pools, creates teams, and adds Pool Operators to teams.',
'fr' => 'Publie des pools, crée des équipes et ajoute des opérateurs des bassins aux équipes.',
],
'is_team_based' => false,
],

'platform_admin' => [
'display_name' => [
'en' => 'Platform Administrator',
Expand Down Expand Up @@ -625,6 +642,24 @@
],
],

'community_manager' => [
'userBasicInfo' => [
'any' => ['view'],
],
'pool' => [
'any' => ['view', 'publish'],
],
'teamMembers' => [
'any' => ['view'],
],
'team' => [
'any' => ['view', 'create', 'update', 'delete'],
],
'teamRole' => [
'any' => ['assign'],
],
],

'platform_admin' => [
'classification' => [
'any' => ['create', 'update', 'delete'],
Expand Down Expand Up @@ -660,7 +695,7 @@
'any' => ['view'],
],
'team' => [
'any' => ['create', 'update', 'delete'],
'any' => ['view', 'create', 'update', 'delete'],
],
'role' => [
'any' => ['view', 'assign'],
Expand Down
14 changes: 13 additions & 1 deletion api/database/factories/UserFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ public function asRequestResponder()
/**
* Attach the pool operator role to a user after creation.
*
* @param string $team Name of the team to attach the role to
* @param string|array $team Name of the team or teams to attach the role to
* @return $this
*/
public function asPoolOperator(string|array $team)
Expand All @@ -250,6 +250,18 @@ public function asPoolOperator(string|array $team)
});
}

/**
* Attach the Community Manager role to a user after creation.
*
* @return $this
*/
public function asCommunityManager()
{
return $this->afterCreating(function (User $user) {
$user->addRole('community_manager');
});
}

/**
* Attach the admin role to a user after creation.
*
Expand Down
13 changes: 13 additions & 0 deletions api/database/seeders/UserSeederLocal.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function run()
User::factory()
->asApplicant()
->asRequestResponder()
->asCommunityManager()
->asAdmin()
->asPoolOperator(['digital-community-management', 'test-team'])
->withExperiences()
Expand All @@ -47,6 +48,18 @@ public function run()
'sub' => 'platform@test.com',
]);

User::factory()
->asApplicant()
->asCommunityManager()
->withExperiences()
->asGovEmployee()
->create([
'first_name' => 'Community',
'last_name' => 'Manager',
'email' => 'community@test.com',
'sub' => 'community@test.com',
]);

User::factory()
->asApplicant()
->asRequestResponder()
Expand Down
33 changes: 31 additions & 2 deletions api/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ type PoolCandidateStatusChangedNotification implements Notification {
poolName: LocalizedString @rename(attribute: "pool_name")
}

type UserPublicProfile {
type UserPublicProfile @model(class: "\\App\\Models\\User") {
id: ID!
email: Email
firstName: String @rename(attribute: "first_name")
Expand All @@ -174,7 +174,7 @@ type Pool {
operationalRequirements: [OperationalRequirement]
@rename(attribute: "operational_requirements")
poolCandidates: [PoolCandidate]
@hasMany(relation: "publishedPoolCandidates")
@hasMany(relation: "publishedPoolCandidates", scopes: ["authorizedToView"])
@can(ability: "view", resolved: true)
keyTasks: LocalizedString @rename(attribute: "key_tasks")
yourImpact: LocalizedString @rename(attribute: "your_impact")
Expand Down Expand Up @@ -651,6 +651,13 @@ type Query {
@deprecated(
reason: "applicants is deprecated. Use usersPaginated instead. Remove in #7652."
)
userPublicProfiles: [UserPublicProfile]!
@all
@guard
@can(ability: "viewBasicInfo", model: "User")
@deprecated(
reason: "We should avoid non-paginated queries when we know a query will return thousands of values. In this case, we must write an alternative first!"
)
# countApplicants returns the number of candidates matching its filters, and requires no special permissions.
countApplicants(where: ApplicantFilterInput): Int!
@count(model: "User", scopes: ["inITPublishingGroup"])
Expand Down Expand Up @@ -1380,6 +1387,25 @@ input RoleAssignmentHasMany {
detach: RolesInput
sync: RolesInput
}

# This input only accepts Team-Based roles. It is assumed that a team id will be provided at a higher level of input.
input RolesForTeamInput
@validator(class: "App\\GraphQL\\Validators\\TeamRolesInputValidator") {
roles: [ID!]
}

input RolesForTeamHasMany {
attach: RolesForTeamInput
detach: RolesForTeamInput
sync: RolesForTeamInput
}

input UpdateUserTeamRolesInput {
teamId: ID!
userId: UUID!
roleAssignments: RolesForTeamHasMany!
}

input ScreeningResponseBelongsTo {
connect: ID!
}
Expand Down Expand Up @@ -1655,6 +1681,9 @@ type Mutation {
@delete
@guard
@can(ability: "delete", find: "id")
updateUserTeamRoles(
teamRoleAssignments: UpdateUserTeamRolesInput! @spread
): Team @guard @can(ability: "assignTeamMembers", find: "teamId")

# Notifications
markNotificationAsRead(id: UUID!): Notification @guard # can only affect notifications belonging to the logged-in user
Expand Down
18 changes: 18 additions & 0 deletions api/storage/app/lighthouse-schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Query {
): [User]! @deprecated(reason: "users is deprecated. Use usersPaginated instead. Remove in #7660.")
applicant(id: UUID!): User @deprecated(reason: "applicant is deprecated. Use user instead. Remove in #7651.")
applicants(includeIds: [ID]!): [User]! @deprecated(reason: "applicants is deprecated. Use usersPaginated instead. Remove in #7652.")
userPublicProfiles: [UserPublicProfile]! @deprecated(reason: "We should avoid non-paginated queries when we know a query will return thousands of values. In this case, we must write an alternative first!")
countApplicants(where: ApplicantFilterInput): Int!
pool(id: UUID!): Pool
publishedPools(closingAfter: DateTime, publishingGroup: PublishingGroup): [Pool!]!
Expand Down Expand Up @@ -132,6 +133,7 @@ type Mutation {
createTeam(team: CreateTeamInput!): Team
updateTeam(id: UUID!, team: UpdateTeamInput!): Team
deleteTeam(id: UUID!): Team
updateUserTeamRoles(teamRoleAssignments: UpdateUserTeamRolesInput!): Team
markNotificationAsRead(id: UUID!): Notification
createUserSkill(userId: UUID!, skillId: UUID!, userSkill: CreateUserSkillInput): UserSkill
updateUserSkill(id: UUID!, userSkill: UpdateUserSkillInput): UserSkill
Expand Down Expand Up @@ -1158,6 +1160,22 @@ input RoleAssignmentHasMany {
sync: RolesInput
}

input RolesForTeamInput {
roles: [ID!]
}

input RolesForTeamHasMany {
attach: RolesForTeamInput
detach: RolesForTeamInput
sync: RolesForTeamInput
}

input UpdateUserTeamRolesInput {
teamId: ID!
userId: UUID!
roleAssignments: RolesForTeamHasMany!
}

input ScreeningResponseBelongsTo {
connect: ID!
}
Expand Down
Loading

0 comments on commit 90bfd94

Please sign in to comment.