From 1a3d88bfa81e44860567dee510a505547389f457 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 2 Jul 2025 13:33:19 -0500 Subject: [PATCH] Create tests for app/actions/tabulation/RankAuditionEntries --- .../Tabulation/RankAuditionEntries.php | 11 +- app/Models/EntryTotalScore.php | 2 + .../Tabulation/RankAuditionEntriesTest.php | 360 ++++++++++++++++++ 3 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/app/Actions/Tabulation/RankAuditionEntriesTest.php diff --git a/app/Actions/Tabulation/RankAuditionEntries.php b/app/Actions/Tabulation/RankAuditionEntries.php index f6f19a9..379fad5 100644 --- a/app/Actions/Tabulation/RankAuditionEntries.php +++ b/app/Actions/Tabulation/RankAuditionEntries.php @@ -27,7 +27,7 @@ class RankAuditionEntries public function __invoke(Audition $audition, string $rank_type): Collection|Entry { if ($rank_type !== 'seating' && $rank_type !== 'advancement') { - throw new AuditionAdminException('Invalid rank type: '.$rank_type.' (must be seating or advancement)'); + throw new AuditionAdminException('Invalid rank type (must be seating or advancement)'); } $cache_duration = 15; @@ -87,7 +87,7 @@ class RankAuditionEntries private function get_advancement_ranks(Audition $audition): Collection|Entry { - return $audition->entries() + $sortedEntries = $audition->entries() ->whereHas('totalScore') ->with('totalScore') ->with('student.school') @@ -106,5 +106,12 @@ class RankAuditionEntries ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC') ->select('entries.*') ->get(); + $n = 1; + foreach ($sortedEntries as $entry) { + $entry->advancementRank = $n; + $n++; + } + + return $sortedEntries; } } diff --git a/app/Models/EntryTotalScore.php b/app/Models/EntryTotalScore.php index 9ec59ea..40f4e04 100644 --- a/app/Models/EntryTotalScore.php +++ b/app/Models/EntryTotalScore.php @@ -10,6 +10,8 @@ class EntryTotalScore extends Model { use HasFactory; + protected $guarded = []; + protected $casts = [ 'seating_subscore_totals' => 'json', 'advancement_subscore_totals' => 'json', diff --git a/tests/Feature/app/Actions/Tabulation/RankAuditionEntriesTest.php b/tests/Feature/app/Actions/Tabulation/RankAuditionEntriesTest.php new file mode 100644 index 0000000..55a1b39 --- /dev/null +++ b/tests/Feature/app/Actions/Tabulation/RankAuditionEntriesTest.php @@ -0,0 +1,360 @@ +audition = Audition::factory()->create(['minimum_grade' => 1, 'maximum_grade' => 14]); + $this->entries = Entry::factory()->count(10)->create(['audition_id' => $this->audition->id]); + $this->ranker = app(RankAuditionEntries::class); +}); + +afterEach(function () { + cache()->flush(); +}); + +it('throws an exception if an invalid rank type is specified', function () { + ($this->ranker)($this->audition, 'bababoey'); +})->throws(AuditionAdminException::class, 'Invalid rank type (must be seating or advancement)'); + +// Test Rank for Seating +it('ranks entries for seating if there are no ties', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 25, + 'advancement_total' => 75, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 75, + 'advancement_total' => 25, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + $sortedEntries = ($this->ranker)($this->audition, 'seating'); + expect($sortedEntries[0]->id)->toEqual($this->entries[2]->id) + ->and($sortedEntries[1]->id)->toEqual($this->entries[0]->id) + ->and($sortedEntries[2]->id)->toEqual($this->entries[1]->id); +}); + +it('makes use of bonus scores when set', function () { + $bonusScoreDefinition = BonusScoreDefinition::create([ + 'name' => 'bonus', + 'max_score' => 100, + 'weight' => 1, + 'for_seating' => 1, + 'for_attendance' => 0, + ]); + $bonusScoreDefinition->auditions()->attach($this->audition); + DB::table('bonus_scores')->insert([ + 'entry_id' => $this->entries[2]->id, + 'user_id' => 1, + 'originally_scored_entry' => $this->entries[2]->id, + 'score' => 100, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 25, + 'advancement_total' => 75, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 2 will be first place + $score2 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 2, + 'advancement_total' => 25, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + 'bonus_total' => 100, + ]); + $sortedEntries = ($this->ranker)($this->audition, 'seating'); + expect($sortedEntries[0]->id)->toEqual($this->entries[2]->id) + ->and($sortedEntries[1]->id)->toEqual($this->entries[0]->id) + ->and($sortedEntries[2]->id)->toEqual($this->entries[1]->id); +}); + +it('assigns a seatingRank property to each entry that is scored', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 25, + 'advancement_total' => 75, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 75, + 'advancement_total' => 25, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + $sortedEntries = ($this->ranker)($this->audition, 'seating'); + expect($sortedEntries[0]->seatingRank)->toEqual(1) + ->and($sortedEntries[1]->seatingRank)->toEqual(2) + ->and($sortedEntries[2]->seatingRank)->toEqual(3) + ->and($sortedEntries->count())->toEqual(3); +}); + +it('skips a declined entry when assigning seatingRank properties', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 25, + 'advancement_total' => 75, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 75, + 'advancement_total' => 25, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + $this->entries[0]->addFlag('declined'); + $sortedEntries = ($this->ranker)($this->audition, 'seating'); + expect($sortedEntries[0]->seatingRank)->toEqual(1) + ->and($sortedEntries[1]->seatingRank)->toEqual('declined') + ->and($sortedEntries[2]->seatingRank)->toEqual(2); +}); + +it('uses the second subscore as a second tiebreaker for seating', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 50, + 'advancement_total' => 75, + 'seating_subscore_totals' => [5, 4], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 50, + 'advancement_total' => 25, + 'seating_subscore_totals' => [5, 6], + 'advancement_subscore_totals' => [5, 5], + ]); + $sortedEntries = ($this->ranker)($this->audition, 'seating'); + expect($sortedEntries[0]->id)->toEqual($this->entries[2]->id) + ->and($sortedEntries[1]->id)->toEqual($this->entries[0]->id) + ->and($sortedEntries[2]->id)->toEqual($this->entries[1]->id); +}); + +it('uses the first subscore as a tiebreaker for seating', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 50, + 'advancement_total' => 75, + 'seating_subscore_totals' => [4, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 50, + 'advancement_total' => 25, + 'seating_subscore_totals' => [6, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + $sortedEntries = ($this->ranker)($this->audition, 'seating'); + expect($sortedEntries[0]->id)->toEqual($this->entries[2]->id) + ->and($sortedEntries[1]->id)->toEqual($this->entries[0]->id) + ->and($sortedEntries[2]->id)->toEqual($this->entries[1]->id); +}); + +// Test Rank for Advancement +it('ranks entries for advancement if there are no ties', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 75, + 'advancement_total' => 25, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 25, + 'advancement_total' => 75, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + $sortedEntries = ($this->ranker)($this->audition, 'advancement'); + expect($sortedEntries[0]->id)->toEqual($this->entries[2]->id) + ->and($sortedEntries[1]->id)->toEqual($this->entries[0]->id) + ->and($sortedEntries[2]->id)->toEqual($this->entries[1]->id); +}); + +it('assigns a advancementRank property to each entry that is scored', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 75, + 'advancement_total' => 25, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 25, + 'advancement_total' => 75, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + $sortedEntries = ($this->ranker)($this->audition, 'advancement'); + expect($sortedEntries[0]->advancementRank)->toEqual(1) + ->and($sortedEntries[1]->advancementRank)->toEqual(2) + ->and($sortedEntries[2]->advancementRank)->toEqual(3) + ->and($sortedEntries->count())->toEqual(3); +}); + +it('uses the second subscore as a second tiebreaker for advancement', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 75, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 4], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 25, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 6], + ]); + $sortedEntries = ($this->ranker)($this->audition, 'advancement'); + expect($sortedEntries[0]->id)->toEqual($this->entries[2]->id) + ->and($sortedEntries[1]->id)->toEqual($this->entries[0]->id) + ->and($sortedEntries[2]->id)->toEqual($this->entries[1]->id); +}); + +it('uses the first subscore as a tiebreaker for advancement', function () { + // entry 0 will be second place + $score0 = EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [5, 5], + ]); + // entry 1 will be third place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [4, 5], + ]); + // entry 2 will be first place + $score1 = EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 50, + 'advancement_total' => 50, + 'seating_subscore_totals' => [5, 5], + 'advancement_subscore_totals' => [6, 5], + ]); + $sortedEntries = ($this->ranker)($this->audition, 'advancement'); + expect($sortedEntries[0]->id)->toEqual($this->entries[2]->id) + ->and($sortedEntries[1]->id)->toEqual($this->entries[0]->id) + ->and($sortedEntries[2]->id)->toEqual($this->entries[1]->id); +});