From ca80260bdafd73625d670a35c28407d848a324a1 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 8 Oct 2025 07:32:59 -0500 Subject: [PATCH] Action to enter prelim score sheet implemented. --- app/Actions/Tabulation/EnterPrelimScore.php | 128 ++++++++++++++ app/Models/Entry.php | 5 + app/Models/PrelimScoreSheet.php | 9 + .../Tabulation/EnterPrelimScoreTest.php | 166 ++++++++++++++++++ 4 files changed, 308 insertions(+) create mode 100644 app/Actions/Tabulation/EnterPrelimScore.php create mode 100644 tests/Feature/app/Actions/Tabulation/EnterPrelimScoreTest.php diff --git a/app/Actions/Tabulation/EnterPrelimScore.php b/app/Actions/Tabulation/EnterPrelimScore.php new file mode 100644 index 0000000..bf9d0c5 --- /dev/null +++ b/app/Actions/Tabulation/EnterPrelimScore.php @@ -0,0 +1,128 @@ +id)->exists()) { + throw new AuditionAdminException('User does not exist'); + } + if (! Entry::where('id', $entry->id)->exists()) { + throw new AuditionAdminException('Entry does not exist'); + } + if ($entry->audition->hasFlag('seats_published')) { + throw new AuditionAdminException('Cannot score an entry in an audition where seats are published'); + } + + // Check if the entries audition has a prelim definition + if (! PrelimDefinition::where('audition_id', $entry->audition->id)->exists()) { + throw new AuditionAdminException('The entries audition does not have a prelim'); + } + $prelimDefinition = PrelimDefinition::where('audition_id', $entry->audition->id)->first(); + + // Check that the specified user is assigned to judge this entry in prelims + $check = DB::table('room_user') + ->where('user_id', $user->id) + ->where('room_id', $prelimDefinition->room_id)->exists(); + if (! $check) { + throw new AuditionAdminException('This judge is not assigned to judge this entry in prelims'); + } + + // Check if a score already exists + if (! $prelimScoreSheet) { + if (PrelimScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { + throw new AuditionAdminException('That judge has already entered a prelim score for that entry'); + } + } else { + if ($prelimScoreSheet->user_id != $user->id) { + throw new AuditionAdminException('Existing score sheet is from a different judge'); + } + if ($prelimScoreSheet->entry_id != $entry->id) { + throw new AuditionAdminException('Existing score sheet is for a different entry'); + } + } + + // Check the validity of submitted subscores, format array for storage, and sum score + $subscoresRequired = $prelimDefinition->scoringGuide->subscores; + $subscoresStorageArray = []; + $totalScore = 0; + $maxPossibleTotal = 0; + if ($scores->count() !== $subscoresRequired->count()) { + throw new AuditionAdminException('Invalid number of scores'); + } + + foreach ($subscoresRequired as $subscore) { + // check that there is an element in the $scores collection with the key = $subscore->id + if (! $scores->keys()->contains($subscore->id)) { + throw new AuditionAdminException('Invalid Score Submission'); + } + + if ($scores[$subscore->id] > $subscore->max_score) { + throw new AuditionAdminException('Supplied subscore exceeds maximum allowed'); + } + + // Add subscore to the storage array + $subscoresStorageArray[$subscore->id] = [ + 'score' => $scores[$subscore->id], + 'subscore_id' => $subscore->id, + 'subscore_name' => $subscore->name, + ]; + + // Multiply subscore by weight then add to total + $totalScore += ($subscore->weight * $scores[$subscore->id]); + $maxPossibleTotal += ($subscore->weight * $subscore->max_score); + } + $finalTotalScore = ($maxPossibleTotal === 0) ? 0 : (($totalScore / $maxPossibleTotal) * 100); + + $entry->removeFlag('no_show'); + if ($prelimScoreSheet instanceof PrelimScoreSheet) { + $prelimScoreSheet->update([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $subscoresStorageArray, + 'total' => $finalTotalScore, + ]); + } else { + $prelimScoreSheet = PrelimScoreSheet::create([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $subscoresStorageArray, + 'total' => $finalTotalScore, + ]); + } + + // Log the prelim score entry + $log_message = 'Entered prelim score for entry id '.$entry->id.'.
'; + $log_message .= 'Judge: '.$user->full_name().'
'; + foreach ($prelimScoreSheet->subscores as $subscore) { + $log_message .= $subscore['subscore_name'].': '.$subscore['score'].'
'; + } + $log_message .= 'Total :'.$prelimScoreSheet->total.'
'; + auditionLog($log_message, [ + 'entries' => [$entry->id], + 'users' => [$user->id], + 'auditions' => [$entry->audition_id], + 'students' => [$entry->student_id], + ]); + + return $prelimScoreSheet; + } +} diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 9cbc7c3..ad57b42 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -136,6 +136,11 @@ class Entry extends Model } + public function prelimScoreSheets(): HasMany + { + return $this->hasMany(PrelimScoreSheet::class); + } + public function bonusScores(): HasMany { return $this->hasMany(BonusScore::class); diff --git a/app/Models/PrelimScoreSheet.php b/app/Models/PrelimScoreSheet.php index d81fd71..2502e25 100644 --- a/app/Models/PrelimScoreSheet.php +++ b/app/Models/PrelimScoreSheet.php @@ -7,6 +7,15 @@ use Illuminate\Database\Eloquent\Relations\HasOne; class PrelimScoreSheet extends Model { + protected $fillable = [ + 'user_id', + 'entry_id', + 'subscores', + 'total', + ]; + + protected $casts = ['subscores' => 'json']; + public function user(): HasOne { return $this->hasOne(User::class); diff --git a/tests/Feature/app/Actions/Tabulation/EnterPrelimScoreTest.php b/tests/Feature/app/Actions/Tabulation/EnterPrelimScoreTest.php new file mode 100644 index 0000000..bc4587d --- /dev/null +++ b/tests/Feature/app/Actions/Tabulation/EnterPrelimScoreTest.php @@ -0,0 +1,166 @@ +prelimScoringGuide = ScoringGuide::factory()->create(['id' => 1000]); + SubscoreDefinition::create([ + 'id' => 1001, + 'scoring_guide_id' => $this->prelimScoringGuide->id, + 'name' => 'Scale', + 'max_score' => 100, + 'weight' => 1, + 'display_order' => 1, + 'tiebreak_order' => 3, + 'for_seating' => '1', + 'for_advance' => '0', + ]); + SubscoreDefinition::create([ + 'id' => 1002, + 'scoring_guide_id' => $this->prelimScoringGuide->id, + 'name' => 'Etude 1', + 'max_score' => 100, + 'weight' => 2, + 'display_order' => 2, + 'tiebreak_order' => 1, + 'for_seating' => '1', + 'for_advance' => '1', + ]); + SubscoreDefinition::create([ + 'id' => 1003, + 'scoring_guide_id' => $this->prelimScoringGuide->id, + 'name' => 'Etude 2', + 'max_score' => 100, + 'weight' => 2, + 'display_order' => 3, + 'tiebreak_order' => 2, + 'for_seating' => '0', + 'for_advance' => '1', + ]); + SubscoreDefinition::where('id', '<', 900)->delete(); + $this->finalsRoom = Room::factory()->create(); + $this->prelimRoom = Room::factory()->create(); + $this->audition = Audition::factory()->create(['room_id' => $this->finalsRoom->id]); + $this->prelimDefinition = PrelimDefinition::create([ + 'room_id' => $this->prelimRoom->id, + 'audition_id' => $this->audition->id, + 'scoring_guide_id' => $this->prelimScoringGuide->id, + 'passing_score' => 60, + ]); + + $this->judge1 = User::factory()->create(); + $this->judge2 = User::factory()->create(); + $this->prelimRoom->judges()->attach($this->judge1->id); + $this->prelimRoom->judges()->attach($this->judge2->id); + $this->entry1 = Entry::factory()->create(['audition_id' => $this->audition->id]); + $this->entry2 = Entry::factory()->create(['audition_id' => $this->audition->id]); + $this->scribe = app(EnterPrelimScore::class); + $this->possibleScoreArray = [ + 1001 => 10, + 1002 => 11, + 1003 => 12, + ]; + $this->anotherPossibleScoreArray = [ + 1001 => 20, + 1002 => 21, + 1003 => 22, + ]; + +}); + +it('can enter a prelim score', function () { + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); + expect($this->entry1->prelimScoreSheets()->count())->toBe(1) + ->and($this->entry1->prelimScoreSheets()->first()->total)->toBe(11.2); + + ($this->scribe)($this->judge1, $this->entry2, $this->anotherPossibleScoreArray); + expect($this->entry2->prelimScoreSheets()->count())->toBe(1) + ->and($this->entry2->prelimScoreSheets()->first()->total)->toBe(21.2); +}); + +it('will not enter a score for a judge that does not exist', function () { + $fakeJudge = User::factory()->make(); + ($this->scribe)($fakeJudge, $this->entry1, $this->possibleScoreArray); +})->throws(AuditionAdminException::class, 'User does not exist'); + +it('will not enter a score for an entry that does not exist', function () { + $fakeEntry = Entry::factory()->make(); + ($this->scribe)($this->judge1, $fakeEntry, $this->possibleScoreArray); +})->throws(AuditionAdminException::class, 'Entry does not exist'); + +it('will not score an entry if the audition seats are published', function () { + $this->audition->addFlag('seats_published'); + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); +})->throws(AuditionAdminException::class, 'Cannot score an entry in an audition where seats are published'); + +it('will not score an entry if the judge is not assigned to judge the entry', function () { + $fakeJudge = User::factory()->create(); + ($this->scribe)($fakeJudge, $this->entry1, $this->possibleScoreArray); +})->throws(AuditionAdminException::class, 'This judge is not assigned to judge this entry'); + +it('can modify an existing score sheet', function () { + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); + $scoreSheet = PrelimScoreSheet::first(); + ($this->scribe)($this->judge1, $this->entry1, $this->anotherPossibleScoreArray, $scoreSheet); + expect($this->entry1->prelimScoreSheets()->count())->toBe(1) + ->and($this->entry1->prelimScoreSheets()->first()->total)->toBe(21.2); +}); + +it('will not change the judge on a score sheet', function () { + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); + $scoreSheet = PrelimScoreSheet::first(); + ($this->scribe)($this->judge2, $this->entry1, $this->anotherPossibleScoreArray, $scoreSheet); +})->throws(AuditionAdminException::class, 'Existing score sheet is from a different judge'); + +it('will not accept a second score sheet for a judge ane entry', function () { + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); + ($this->scribe)($this->judge1, $this->entry1, $this->anotherPossibleScoreArray); +})->throws(AuditionAdminException::class, 'That judge has already entered a prelim score for that entry'); + +it('will not change the entry on a score sheet', function () { + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); + $scoreSheet = PrelimScoreSheet::first(); + ($this->scribe)($this->judge1, $this->entry2, $this->anotherPossibleScoreArray, $scoreSheet); +})->throws(AuditionAdminException::class, 'Existing score sheet is for a different entry'); + +it('will not accept an incorrect number of subscores', function () { + array_pop($this->possibleScoreArray); + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); +})->throws(AuditionAdminException::class, 'Invalid number of scores'); + +it('will not accept an invalid subscores', function () { + array_pop($this->possibleScoreArray); + $this->possibleScoreArray[3001] = 100; + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); +})->throws(AuditionAdminException::class, 'Invalid Score Submission'); + +it('will. not accept a subscore in excess of its maximum', function () { + $this->possibleScoreArray[1001] = 1500; + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); +})->throws(AuditionAdminException::class, 'Supplied subscore exceeds maximum allowed'); + +it('removes a no-show flag from an entry', function () { + $this->entry1->addFlag('no_show'); + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); + expect($this->entry1->hasFlag('no_show'))->toBeFalse(); +}); + +it('logs score entry', function () { + ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray); + $logEntry = AuditLogEntry::orderBy('id', 'desc')->first(); + expect($logEntry->message)->toStartWith('Entered prelim score for entry id '); +});