diff --git a/app/Console/Commands/Roster/CheckForNewS1ExamPasses.php b/app/Console/Commands/Roster/CheckForNewS1ExamPasses.php new file mode 100644 index 0000000000..fa0b5c4100 --- /dev/null +++ b/app/Console/Commands/Roster/CheckForNewS1ExamPasses.php @@ -0,0 +1,56 @@ +getRecentPassedExamsOfType('OBS'); + + foreach ($recentSuccessfulS1Exams as $exam) { + $ctsMember = Member::where('id', $exam->student_id)->first(); + $coreAccount = Account::find($ctsMember->cid); + + if (! $coreAccount) { + $this->error("Could not find account for student ID {$exam->student_id}."); + + continue; + } + + if (! $coreAccount->hasState(State::findByCode('DIVISION'))) { + $this->error("Account {$coreAccount->id} does not have the DIVISION state."); + + continue; + } + + Roster::upsert(['account_id' => $coreAccount->id], uniqueBy: ['account_id']); + + $this->info("Added account {$coreAccount->id} to the roster."); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 6f8bc06be5..8ad925874c 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -62,6 +62,10 @@ protected function schedule(Schedule $schedule) ->hourlyAt(25) ->graceTimeInMinutes(5); + $schedule->command('roster:check-new-s1-exams') + ->hourlyAt(30) + ->graceTimeInMinutes(5); + // === By Day === // $schedule->command('telescope:prune') diff --git a/app/Models/Cts/PracticalResult.php b/app/Models/Cts/PracticalResult.php new file mode 100644 index 0000000000..1b1e90d418 --- /dev/null +++ b/app/Models/Cts/PracticalResult.php @@ -0,0 +1,23 @@ + 'datetime', + ]; +} diff --git a/app/Repositories/Cts/ExamResultRepository.php b/app/Repositories/Cts/ExamResultRepository.php new file mode 100644 index 0000000000..905d8ab7bc --- /dev/null +++ b/app/Repositories/Cts/ExamResultRepository.php @@ -0,0 +1,17 @@ +where('exam', $type) + ->where('date', '>=', now()->subDays($daysConsideredRecent)) + ->get(); + } +} diff --git a/database/factories/Cts/PracticalResultFactory.php b/database/factories/Cts/PracticalResultFactory.php new file mode 100644 index 0000000000..3430ce8f71 --- /dev/null +++ b/database/factories/Cts/PracticalResultFactory.php @@ -0,0 +1,28 @@ + + */ +class PracticalResultFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'examid' => $this->faker->randomNumber(3), + 'student_id' => factory(Member::class)->create()->id, + 'exam' => $this->faker->randomElement(['OBS', 'TWR', 'APP', 'CTR']), + 'result' => $this->faker->randomElement(['P', 'F']), + 'date' => $this->faker->dateTime(), + ]; + } +} diff --git a/tests/Database/MockCtsDatabase.php b/tests/Database/MockCtsDatabase.php index e331822c68..1d148a55a2 100644 --- a/tests/Database/MockCtsDatabase.php +++ b/tests/Database/MockCtsDatabase.php @@ -217,6 +217,23 @@ public static function create() KEY `student_id` (`student_id`) );" ); + + DB::connection('cts')->statement( + "CREATE TABLE `practical_results` ( + `id` smallint unsigned NOT NULL AUTO_INCREMENT, + `examid` smallint unsigned NOT NULL DEFAULT '0', + `student_id` int unsigned NOT NULL DEFAULT '0', + `exam` enum('P1','P2','P3','P4','P5','P6','P7','P8','P9','OBS','TWR','APP','CTR','S3','C1','C3') NOT NULL DEFAULT 'TWR', + `notes` longtext, + `result` char(1) NOT NULL DEFAULT '', + `date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', + `cert_upgrade` tinyint unsigned DEFAULT '0', + `upgrade_by` int unsigned DEFAULT '0', + `upgrade_date` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `student_id` (`student_id`) + );" + ); } public static function destroy() @@ -260,5 +277,9 @@ public static function destroy() DB::connection('cts')->statement( 'DROP TABLE IF EXISTS `theory_results`;' ); + + DB::connection('cts')->statement( + 'DROP TABLE IF EXISTS `practical_results`;' + ); } } diff --git a/tests/Unit/CTS/ExamResultsRepositoryTest.php b/tests/Unit/CTS/ExamResultsRepositoryTest.php new file mode 100644 index 0000000000..9df1887b86 --- /dev/null +++ b/tests/Unit/CTS/ExamResultsRepositoryTest.php @@ -0,0 +1,88 @@ +create(['id' => 1111111]); + $member = factory(Member::class)->create([ + 'cid' => $account->id, + ]); + + $examResult = PracticalResult::factory()->create([ + 'result' => PracticalResult::PASSED, + 'student_id' => $member->id, + 'exam' => 'OBS', + 'date' => now()->subDays(1), + ]); + + // ensure failed result isn't returned + $notSuccessfulPracticalResult = PracticalResult::factory()->create([ + 'result' => PracticalResult::FAILED, + 'student_id' => $member->id, + 'exam' => 'OBS', + ]); + + $repository = new ExamResultRepository(); + $result = $repository->getRecentPassedExamsOfType('OBS'); + + $this->assertNotNull($result->where('id', $examResult->id)->first()); + $this->assertNull($result->where('id', $notSuccessfulPracticalResult->id)->first()); + } + + public function test_doesnt_return_non_recent_exam_passes() + { + $account = Account::factory()->create(['id' => 1111111]); + $member = factory(Member::class)->create([ + 'cid' => $account->id, + ]); + + $examResult = PracticalResult::factory()->create([ + 'result' => PracticalResult::PASSED, + 'student_id' => $member->id, + 'exam' => 'OBS', + 'date' => now()->subDays(4), + ]); + + $repository = new ExamResultRepository(); + $result = $repository->getRecentPassedExamsOfType('OBS'); + + $this->assertNull($result->where('id', $examResult->id)->first()); + } + + public function test_only_returns_recent_successful_exams_of_specified_type() + { + $account = Account::factory()->create(['id' => 1111111]); + $member = factory(Member::class)->create([ + 'cid' => $account->id, + ]); + + $examResult = PracticalResult::factory()->create([ + 'result' => PracticalResult::PASSED, + 'student_id' => $member->id, + 'exam' => 'TWR', + 'date' => now()->subDays(1), + ]); + + $notSuccessfulPracticalResult = PracticalResult::factory()->create([ + 'result' => PracticalResult::FAILED, + 'student_id' => $member->id, + 'exam' => 'OBS', + 'date' => now()->subDays(1), + ]); + + $repository = new ExamResultRepository(); + $result = $repository->getRecentPassedExamsOfType('OBS'); + + $this->assertNull($result->where('id', $examResult->id)->first()); + $this->assertNull($result->where('id', $notSuccessfulPracticalResult->id)->first()); + } +} diff --git a/tests/Unit/Command/AddNewS1ToRosterCommandTest.php b/tests/Unit/Command/AddNewS1ToRosterCommandTest.php new file mode 100644 index 0000000000..b87c7627ce --- /dev/null +++ b/tests/Unit/Command/AddNewS1ToRosterCommandTest.php @@ -0,0 +1,61 @@ +create(['id' => 1111111]); + $account->addState(State::findByCode('DIVISION')); + + $ctsMember = factory(Member::class)->create(['cid' => $account->id]); + PracticalResult::factory()->create(['student_id' => $ctsMember->id, 'result' => PracticalResult::PASSED, 'exam' => 'OBS', 'date' => now()->subDays(1)]); + + Artisan::call('roster:check-new-s1-exams'); + + $this->assertDatabaseHas('roster', [ + 'account_id' => $account->id, + ]); + } + + public function test_maintains_roster_when_already_on_roster() + { + $account = Account::factory()->create(['id' => 1111111]); + $account->addState(State::findByCode('DIVISION')); + + $ctsMember = factory(Member::class)->create(['cid' => $account->id]); + PracticalResult::factory()->create(['student_id' => $ctsMember->id, 'result' => PracticalResult::PASSED, 'exam' => 'OBS', 'date' => now()->subDays(1)]); + + Roster::create(['account_id' => $account->id]); + + Artisan::call('roster:check-new-s1-exams'); + + $this->assertDatabaseHas('roster', [ + 'account_id' => $account->id, + ]); + } + + public function test_does_not_add_when_not_on_roster_and_missing_division_state() + { + $account = Account::factory()->create(['id' => 1111111]); + $account->addState(State::findByCode('VISITING')); + + $ctsMember = factory(Member::class)->create(['cid' => $account->id]); + PracticalResult::factory()->create(['student_id' => $ctsMember->id, 'result' => PracticalResult::PASSED, 'exam' => 'OBS', 'date' => now()->subDays(1)]); + + Artisan::call('roster:check-new-s1-exams'); + + $this->assertDatabaseMissing('roster', [ + 'account_id' => $account->id, + ]); + } +} diff --git a/tests/Unit/Roster/AccountCanControlTest.php b/tests/Unit/Roster/AccountCanControlTest.php index 50adecd339..bc1a7612b6 100644 --- a/tests/Unit/Roster/AccountCanControlTest.php +++ b/tests/Unit/Roster/AccountCanControlTest.php @@ -9,10 +9,17 @@ use App\Models\Mship\Qualification; use App\Models\Mship\State; use App\Models\Roster; +use Illuminate\Support\Facades\Event; use Tests\TestCase; class AccountCanControlTest extends TestCase { + public function setUp(): void + { + parent::setUp(); + Event::fake(); + } + public function test_detects_can_control_with_rating_when_home_member() { $qualification = Qualification::code('S2')->first();