Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Opt into search placed candidates backend #12330

Merged
merged 11 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions api/app/Console/Commands/SuspendPlacedCandidates.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Console\Commands;

use App\Enums\PoolCandidateStatus;
use App\Models\PoolCandidate;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;

class SuspendPlacedCandidates extends Command
petertgiles marked this conversation as resolved.
Show resolved Hide resolved
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:suspend-placed-candidates';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Set suspended at date to now() for placed term and indeterminate candidates';

/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$applicableStatuses = [PoolCandidateStatus::PLACED_TERM->name, PoolCandidateStatus::PLACED_INDETERMINATE->name];

PoolCandidate::whereIn('pool_candidate_status', $applicableStatuses)
->whereNull('suspended_at')
->with('user')
->chunkById(100, function (Collection $candidates) {
foreach ($candidates as $candidate) {
/** @var \App\Models\PoolCandidate $candidate */
$candidate->suspended_at = Carbon::now();
$candidate->save();
}
}
);

return Command::SUCCESS;
}
}
3 changes: 3 additions & 0 deletions api/app/Enums/PoolCandidateStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,15 @@ enum PoolCandidateStatus
case EXPIRED;
case REMOVED;

// searchable candidates, so the available status and others treated as available for search purposes
public static function qualifiedEquivalentGroup(): array
{
return [
PoolCandidateStatus::QUALIFIED_AVAILABLE->name,
PoolCandidateStatus::PLACED_TENTATIVE->name,
PoolCandidateStatus::PLACED_CASUAL->name,
PoolCandidateStatus::PLACED_TERM->name,
PoolCandidateStatus::PLACED_INDETERMINATE->name,
];
}

Expand Down
8 changes: 8 additions & 0 deletions api/app/GraphQL/Mutations/PlaceCandidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\GraphQL\Mutations;

use App\Enums\PlacementType;
use App\Models\PoolCandidate;
use Carbon\Carbon;

Expand All @@ -25,6 +26,13 @@ public function __invoke($_, array $args)
$candidate->computed_final_decision = $finalDecision['decision'];
$candidate->computed_final_decision_weight = $finalDecision['weight'];

// If setting to term or indeterminate automatically suspend the candidate, otherwise null the field
if ($placementType === PlacementType::PLACED_TERM->name || $placementType === PlacementType::PLACED_INDETERMINATE->name) {
petertgiles marked this conversation as resolved.
Show resolved Hide resolved
$candidate->suspended_at = $now;
} else {
$candidate->suspended_at = null;
}

$candidate->save();

return $candidate;
Expand Down
1 change: 1 addition & 0 deletions api/app/GraphQL/Mutations/RevertPlaceCandidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public function __invoke($_, array $args)
$candidate->pool_candidate_status = PoolCandidateStatus::QUALIFIED_AVAILABLE->name;
$candidate->placed_at = null;
$candidate->placed_department_id = null;
$candidate->suspended_at = null;

$candidate->save();

Expand Down
2 changes: 1 addition & 1 deletion api/app/GraphQL/Queries/CountPoolCandidatesByPool.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public function __invoke($_, array $args)
}
});

// available candidates scope (scope CANDIDATE_STATUS_QUALIFIED_AVAILABLE or CANDIDATE_STATUS_PLACED_CASUAL, or PLACED_TENTATIVE)
// available candidates scope (qualifiedEquivalentGroup, not expired, not suspended)
PoolCandidate::scopeAvailable($queryBuilder);

// Only display IT & OTHER publishing group candidates
Expand Down
2 changes: 1 addition & 1 deletion api/app/Models/PoolCandidate.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
* @property ?int $status_weight
* @property string $pool_id
* @property string $user_id
* @property ?\Illuminate\Support\Carbon $suspended_at
* @property ?\Carbon\Carbon $suspended_at
* @property \Illuminate\Support\Carbon $created_at
* @property ?\Illuminate\Support\Carbon $updated_at
* @property array $submitted_steps
Expand Down
54 changes: 54 additions & 0 deletions api/tests/Feature/CountPoolCandidatesByPoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -828,4 +828,58 @@ public function testAdditionalAvailabilityScopes()
],
]);
}

// asserts placed term/indeterminate candidates can appear in this query, can't check by id though so check that 2 out of 3 appear
public function testPlacedSuspendedNotSuspendedCandidates()
Comment on lines +832 to +833
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supplemented by testScopeAvailable()

{
$itPool = Pool::factory()->create([
...$this->poolData(),
'publishing_group' => PublishingGroup::IT_JOBS->name,
]);
PoolCandidate::truncate();
PoolCandidate::factory()->availableInSearch()->create([
'pool_id' => $itPool,
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'suspended_at' => null,
]);
PoolCandidate::factory()->availableInSearch()->create([
'pool_id' => $itPool,
'pool_candidate_status' => PoolCandidateStatus::PLACED_INDETERMINATE->name,
'suspended_at' => null,
]);
PoolCandidate::factory()->availableInSearch()->create([
'pool_id' => $itPool,
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'suspended_at' => config('constants.far_past_datetime'),
]);

// expect 2, the 2 un-suspended ones
$this->graphQL(
/** @lang GraphQL */
'
query ($where: ApplicantFilterInput) {
countPoolCandidatesByPool(where: $where) {
pool { id }
candidateCount
}
}
',
[
'where' => [
'pools' => [
['id' => $itPool->id],
],
],
]
)->assertSimilarJson([
'data' => [
'countPoolCandidatesByPool' => [
[
'pool' => ['id' => $itPool->id],
'candidateCount' => 2,
petertgiles marked this conversation as resolved.
Show resolved Hide resolved
],
],
],
]);
}
}
33 changes: 33 additions & 0 deletions api/tests/Feature/PoolCandidateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
use Tests\TestCase;
use Tests\UsesProtectedGraphqlEndpoint;

use function PHPUnit\Framework\assertEqualsCanonicalizing;

class PoolCandidateTest extends TestCase
{
use MakesGraphQLRequests;
Expand Down Expand Up @@ -849,4 +851,35 @@ public function testScopeCandidatesInCommunity(): void
])->assertJsonFragment(['total' => 1])
->assertJsonFragment(['id' => $communityCandidate->id]);
}

// test scopeAvailable
public function testScopeAvailable(): void
{
PoolCandidate::truncate();
$candidate = PoolCandidate::factory()->availableInSearch()->create([
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'expiry_date' => null,
'suspended_at' => null,
]);

$suspendedCandidate = PoolCandidate::factory()->availableInSearch()->create([
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'expiry_date' => null,
'suspended_at' => config('constants.far_past_datetime'),
]);

$expiredCandidate = PoolCandidate::factory()->availableInSearch()->create([
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'expiry_date' => config('constants.past_date'),
'suspended_at' => null,
]);

$queryBuilder = PoolCandidate::query();
$candidateIds = PoolCandidate::scopeAvailable($queryBuilder)->get()->pluck('id')->toArray();

// suspended and expired not present
assertEqualsCanonicalizing([
$candidate->id,
], $candidateIds);
}
}
57 changes: 57 additions & 0 deletions api/tests/Feature/PoolCandidateUpdateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ protected function setUp(): void
placedDepartment {
id
}
suspendedAt
}
}
';
Expand All @@ -175,6 +176,7 @@ protected function setUp(): void
placedDepartment {
id
}
suspendedAt
}
}
';
Expand Down Expand Up @@ -502,12 +504,66 @@ public function testPlaceCandidateMutation(): void
assertSame($response['placedDepartment']['id'], $department->id);
}

// placing candidates sets suspended_at automatically or keeps/makes it null appropriately
public function testPlaceCandidateMutationSuspensionLogic(): void
{
$department = Department::factory()->create();
$this->poolCandidate->pool_candidate_status = PoolCandidateStatus::QUALIFIED_AVAILABLE->name;
$this->poolCandidate->placed_at = null;
$this->poolCandidate->save();
$placedDoNotSuspend = [
PlacementType::PLACED_TENTATIVE->name,
PlacementType::PLACED_CASUAL->name,
];
$placedDoSuspend = [
PlacementType::PLACED_TERM->name,
PlacementType::PLACED_INDETERMINATE->name,
];

foreach ($placedDoNotSuspend as $placementType) {
$response = $this->actingAs($this->poolOperatorUser, 'api')
->graphQL(
$this->placeCandidateMutation,
[
'id' => $this->poolCandidate->id,
'placeCandidate' => [
'placementType' => $placementType,
'departmentId' => $department->id,
],
]
)->json('data.placeCandidate');

assertSame($response['status']['value'], $placementType);
assertNotNull($response['placedAt']);
assertNull($response['suspendedAt']);
}

foreach ($placedDoSuspend as $placementType) {
$response = $this->actingAs($this->poolOperatorUser, 'api')
->graphQL(
$this->placeCandidateMutation,
[
'id' => $this->poolCandidate->id,
'placeCandidate' => [
'placementType' => $placementType,
'departmentId' => $department->id,
],
]
)->json('data.placeCandidate');

assertSame($response['status']['value'], $placementType);
assertNotNull($response['placedAt']);
assertNotNull($response['suspendedAt']);
}
}

public function testRevertPlaceCandidateMutation(): void
{
$department = Department::factory()->create();
$this->poolCandidate->pool_candidate_status = PoolCandidateStatus::NEW_APPLICATION->name;
$this->poolCandidate->placed_department_id = $department->id;
$this->poolCandidate->placed_at = config('constants.far_past_date');
$this->poolCandidate->suspended_at = config('constants.far_past_date');
$this->poolCandidate->save();

// cannot execute mutation due to candidate not being placed
Expand Down Expand Up @@ -535,6 +591,7 @@ public function testRevertPlaceCandidateMutation(): void
assertSame($response['status']['value'], PoolCandidateStatus::QUALIFIED_AVAILABLE->name);
assertNull($response['placedAt']);
assertNull($response['placedDepartment']);
assertNull($response['suspendedAt']);
}

public function testQualifyCandidateMutation(): void
Expand Down
15 changes: 12 additions & 3 deletions api/tests/Feature/UserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,7 @@ public function testCountApplicantsQuery(): void
'user_id' => $user['id'],
]);

// group one
PoolCandidate::factory()->count(8)->create([
'pool_id' => $pool1['id'],
'expiry_date' => config('constants.far_future_date'),
Expand All @@ -1739,6 +1740,7 @@ public function testCountApplicantsQuery(): void
'looking_for_bilingual' => false,
]),
]);
// group two
PoolCandidate::factory()->count(5)->create([
'pool_id' => $pool1['id'],
'expiry_date' => config('constants.far_future_date'),
Expand All @@ -1750,6 +1752,7 @@ public function testCountApplicantsQuery(): void
]),
]);
// Should appear in searches, but in pool 2.
// group three
PoolCandidate::factory()->create([
'pool_id' => $pool2['id'],
'expiry_date' => config('constants.far_future_date'),
Expand All @@ -1761,6 +1764,7 @@ public function testCountApplicantsQuery(): void
]),
]);
// Expired in pool - should not appear in searches
// group four
PoolCandidate::factory()->create([
'pool_id' => $pool1['id'],
'expiry_date' => '2000-01-01',
Expand All @@ -1771,18 +1775,21 @@ public function testCountApplicantsQuery(): void
'looking_for_bilingual' => false,
]),
]);
// Already placed - should not appear in searches
// Already placed - should appear in searches
// group five
PoolCandidate::factory()->create([
'pool_id' => $pool1['id'],
'expiry_date' => config('constants.far_future_date'),
'pool_candidate_status' => PoolCandidateStatus::PLACED_TERM->name,
'suspended_at' => null,
'user_id' => User::factory([
'looking_for_english' => true,
'looking_for_french' => false,
'looking_for_bilingual' => false,
]),
]);
// User status inactive - should not appear in searches
// group six
PoolCandidate::factory()->create([
'pool_id' => $pool1['id'],
'expiry_date' => config('constants.far_future_date'),
Expand Down Expand Up @@ -1812,7 +1819,8 @@ public function testCountApplicantsQuery(): void
);
$response->assertJson([
'data' => [
'countApplicants' => 14, // including base admin user
// contains groups one, two, and five
'countApplicants' => 15, // including base admin user
petertgiles marked this conversation as resolved.
Show resolved Hide resolved
],
]);

Expand All @@ -1834,7 +1842,8 @@ public function testCountApplicantsQuery(): void
]
)->assertJson([
'data' => [
'countApplicants' => 9, //including base admin user
// counts groups one and five, filtered out two due to added language
'countApplicants' => 10, // including base admin user
],
]);
}
Expand Down
Loading