Create tests for app/actions/tabulation/RankAuditionEntries

This commit is contained in:
Matt Young 2025-07-02 13:33:19 -05:00
parent 157e2f496a
commit 1a3d88bfa8
3 changed files with 371 additions and 2 deletions

View File

@ -27,7 +27,7 @@ class RankAuditionEntries
public function __invoke(Audition $audition, string $rank_type): Collection|Entry public function __invoke(Audition $audition, string $rank_type): Collection|Entry
{ {
if ($rank_type !== 'seating' && $rank_type !== 'advancement') { 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; $cache_duration = 15;
@ -87,7 +87,7 @@ class RankAuditionEntries
private function get_advancement_ranks(Audition $audition): Collection|Entry private function get_advancement_ranks(Audition $audition): Collection|Entry
{ {
return $audition->entries() $sortedEntries = $audition->entries()
->whereHas('totalScore') ->whereHas('totalScore')
->with('totalScore') ->with('totalScore')
->with('student.school') ->with('student.school')
@ -106,5 +106,12 @@ class RankAuditionEntries
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC') ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC')
->select('entries.*') ->select('entries.*')
->get(); ->get();
$n = 1;
foreach ($sortedEntries as $entry) {
$entry->advancementRank = $n;
$n++;
}
return $sortedEntries;
} }
} }

View File

@ -10,6 +10,8 @@ class EntryTotalScore extends Model
{ {
use HasFactory; use HasFactory;
protected $guarded = [];
protected $casts = [ protected $casts = [
'seating_subscore_totals' => 'json', 'seating_subscore_totals' => 'json',
'advancement_subscore_totals' => 'json', 'advancement_subscore_totals' => 'json',

View File

@ -0,0 +1,360 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
use App\Actions\Tabulation\RankAuditionEntries;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition;
use App\Models\BonusScoreDefinition;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->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);
});