From 0eda3ab32e5b2cd6e9661d8afc61b22760f13104 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 9 Jul 2024 22:23:23 -0500 Subject: [PATCH] Implement EnterScore action --- app/Actions/EnterScore.php | 101 ++++++++++ app/Exceptions/ScoreEntryException.php | 10 + app/Services/ScoreService.php | 24 +-- app/helpers.php | 17 +- .../AuditionWithScoringGuideAndRoom.php | 84 ++++++++ database/seeders/RoomSeeder.php | 17 -- database/seeders/SchoolSeeder.php | 17 -- database/seeders/ScoreSheetSeeder.php | 17 -- database/seeders/ScoringGuideSeeder.php | 17 -- database/seeders/StudentSeeder.php | 17 -- database/seeders/SubscoreDefinitionSeeder.php | 17 -- .../layout/navbar/menus/tabulation.blade.php | 2 +- resources/views/test.blade.php | 15 +- tests/Feature/Actions/EnterScoreTest.php | 185 ++++++++++++++++++ .../Seating/statusTest.php} | 0 tests/Feature/Services/ScoreServiceTest.php | 44 +++++ tests/Pest.php | 5 + 17 files changed, 467 insertions(+), 122 deletions(-) create mode 100644 app/Actions/EnterScore.php create mode 100644 app/Exceptions/ScoreEntryException.php create mode 100644 database/seeders/AuditionWithScoringGuideAndRoom.php delete mode 100644 database/seeders/RoomSeeder.php delete mode 100644 database/seeders/SchoolSeeder.php delete mode 100644 database/seeders/ScoreSheetSeeder.php delete mode 100644 database/seeders/ScoringGuideSeeder.php delete mode 100644 database/seeders/StudentSeeder.php delete mode 100644 database/seeders/SubscoreDefinitionSeeder.php create mode 100644 tests/Feature/Actions/EnterScoreTest.php rename tests/Feature/{Seating/indexTest.php => Pages/Seating/statusTest.php} (100%) create mode 100644 tests/Feature/Services/ScoreServiceTest.php diff --git a/app/Actions/EnterScore.php b/app/Actions/EnterScore.php new file mode 100644 index 0000000..25c357b --- /dev/null +++ b/app/Actions/EnterScore.php @@ -0,0 +1,101 @@ +basicChecks($user, $entry, $scores); + $this->checkJudgeAssignment($user, $entry); + $this->checkForExistingScore($user, $entry); + $this->validateScoresSubmitted($entry, $scores); + $entry->removeFlag('no_show'); + $newScoreSheet = ScoreSheet::create([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $this->subscoresForStorage($entry, $scores), + ]); + + return $newScoreSheet; + } + + protected function subscoresForStorage(Entry $entry, Collection $scores) + { + $subscores = []; + foreach ($entry->audition->scoringGuide->subscores as $subscore) { + $subscores[$subscore->id] = [ + 'score' => $scores[$subscore->id], + 'subscore_id' => $subscore->id, + 'subscore_name' => $subscore->name, + ]; + } + + return $subscores; + } + + protected function checkForExistingScore(User $user, Entry $entry) + { + if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { + throw new ScoreEntryException('That judge has already entered scores for that entry'); + } + } + + protected function validateScoresSubmitted(Entry $entry, Collection $scores) + { + $subscoresRequired = $entry->audition->scoringGuide->subscores; + + 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 ScoreEntryException('Invalid Score Submission'); + } + if ($scores[$subscore->id] > $subscore->max_score) { + throw new ScoreEntryException('Supplied subscore exceeds maximum allowed'); + } + } + } + + protected function checkJudgeAssignment(User $user, Entry $entry) + { + $check = DB::table('room_user') + ->where('room_id', $entry->audition->room_id) + ->where('user_id', $user->id)->exists(); + if (! $check) { + throw new ScoreEntryException('This judge is not assigned to judge this entry'); + } + } + + protected function basicChecks(User $user, Entry $entry, Collection $scores) + { + if (! $user->exists()) { + throw new ScoreEntryException('User does not exist'); + } + if (! $entry->exists()) { + throw new ScoreEntryException('Entry does not exist'); + } + if ($entry->audition->hasFlag('seats_published')) { + throw new ScoreEntryException('Cannot score an entry in an audition with published seats'); + } + if ($entry->audition->hasFlag('advancement_published')) { + throw new ScoreEntryException('Cannot score an entry in an audition with published advancement'); + } + $requiredScores = $entry->audition->scoringGuide->subscores()->count(); + if ($scores->count() !== $requiredScores) { + throw new ScoreEntryException('Invalid number of scores'); + } + } +} diff --git a/app/Exceptions/ScoreEntryException.php b/app/Exceptions/ScoreEntryException.php new file mode 100644 index 0000000..9086c34 --- /dev/null +++ b/app/Exceptions/ScoreEntryException.php @@ -0,0 +1,10 @@ +auditionCache = $auditionCache; - $this->entryCache = $entryCache; + + } + public function isEntryFullyScored(Entry $entry): bool + { + $requiredJudges = $entry->audition->judges()->count(); + $scoreSheets = $entry->scoreSheets()->count(); + + return $requiredJudges === $scoreSheets; } + } diff --git a/app/helpers.php b/app/helpers.php index ef50236..22f435f 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,5 +1,9 @@ create(['id' => 1000]); + $sg = ScoringGuide::factory()->create(['id' => 1000]); + SubscoreDefinition::create([ + 'id' => 1001, + 'scoring_guide_id' => $sg->id, + 'name' => 'Scale', + 'max_score' => 100, + 'weight' => 1, + 'display_order' => 1, + 'tiebreak_order' => 5, + 'for_seating' => 1, + 'for_advance' => 0, + ]); + SubscoreDefinition::create([ + 'id' => 1002, + 'scoring_guide_id' => $sg->id, + 'name' => 'Etude 1', + 'max_score' => 100, + 'weight' => 2, + 'display_order' => 2, + 'tiebreak_order' => 3, + 'for_seating' => 1, + 'for_advance' => 1, + ]); + SubscoreDefinition::create([ + 'id' => 1003, + 'scoring_guide_id' => $sg->id, + 'name' => 'Etude 2', + 'max_score' => 100, + 'weight' => 2, + 'display_order' => 3, + 'tiebreak_order' => 4, + 'for_seating' => 1, + 'for_advance' => 1, + ]); + SubscoreDefinition::create([ + 'id' => 1004, + 'scoring_guide_id' => $sg->id, + 'name' => 'Sight Reading', + 'max_score' => 100, + 'weight' => 3, + 'display_order' => 4, + 'tiebreak_order' => 2, + 'for_seating' => 1, + 'for_advance' => 1, + ]); + SubscoreDefinition::create([ + 'id' => 1005, + 'scoring_guide_id' => $sg->id, + 'name' => 'Tone', + 'max_score' => 100, + 'weight' => 1, + 'display_order' => 5, + 'tiebreak_order' => 1, + 'for_seating' => 0, + 'for_advance' => 1, + ]); + Audition::factory()->create([ + 'id' => 1000, + 'room_id' => $room->id, + 'scoring_guide_id' => $sg->id, + 'name' => 'Test Audition', + ]); + } +} diff --git a/database/seeders/RoomSeeder.php b/database/seeders/RoomSeeder.php deleted file mode 100644 index bdf439e..0000000 --- a/database/seeders/RoomSeeder.php +++ /dev/null @@ -1,17 +0,0 @@ - Enter Scores Enter No-Shows - Audition Status + Audition Status {{ auditionSetting('advanceTo') }} Status diff --git a/resources/views/test.blade.php b/resources/views/test.blade.php index 44f7d68..bd114cc 100644 --- a/resources/views/test.blade.php +++ b/resources/views/test.blade.php @@ -1,4 +1,4 @@ -@php use App\Enums\AuditionFlags;use App\Models\Audition;use App\Models\AuditionFlag; @endphp +@php use App\Enums\AuditionFlags;use App\Models\Audition;use App\Models\AuditionFlag;use App\Models\Entry;use App\Models\User; @endphp @php @endphp @inject('scoreservice','App\Services\ScoreService'); @inject('auditionService','App\Services\AuditionService'); @@ -8,8 +8,17 @@ Test Page @php - $audition = Audition::first(); - $audition->addFlag('drawn'); + $entry = Entry::find(1127); + $judge = User::find(65); + $scoreArray = [ + 1 => 50, + 2 => 60, + 3 => 70, + 4 => 80, + 5 => 90, + ]; + enterScore($judge, $entry, $scoreArray); + dump($entry->audition->name); @endphp diff --git a/tests/Feature/Actions/EnterScoreTest.php b/tests/Feature/Actions/EnterScoreTest.php new file mode 100644 index 0000000..43cf3af --- /dev/null +++ b/tests/Feature/Actions/EnterScoreTest.php @@ -0,0 +1,185 @@ +scoreEntry = new EnterScore(); +}); + +test('throws an exception if the user does not exist', function () { + $user = User::factory()->make(); + $entry = Entry::factory()->create(); + enterScore($user, $entry, []); +})->throws(ScoreEntryException::class, 'User does not exist'); +test('throws an exception if the entry does not exist', function () { + $user = User::factory()->create(); + $entry = Entry::factory()->make(); + enterScore($user, $entry, []); +})->throws(ScoreEntryException::class, 'Entry does not exist'); +it('throws an exception if the seats for the entries audition are published', function () { + // Arrange + $user = User::factory()->create(); + $entry = Entry::factory()->create(); + $entry->audition->addFlag('seats_published'); + // Act & Assert + enterScore($user, $entry, []); +})->throws(ScoreEntryException::class, 'Cannot score an entry in an audition with published seats'); +it('throws an exception if the advancement for the entries audition is published', function () { + // Arrange + $user = User::factory()->create(); + $entry = Entry::factory()->create(); + $entry->audition->addFlag('advancement_published'); + // Act & Assert + enterScore($user, $entry, []); +})->throws(ScoreEntryException::class, 'Cannot score an entry in an audition with published advancement'); +it('throws an exception if too many scores are submitted', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Room::find(1000)->addJudge($judge); + // Act & Assert + enterScore($judge, $entry, [1, 2, 5, 3, 2, 1]); +})->throws(ScoreEntryException::class, 'Invalid number of scores'); +it('throws an exception if too few scores are submitted', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Room::find(1000)->addJudge($judge); + // Act & Assert + enterScore($judge, $entry, [1, 2, 5, 3]); +})->throws(ScoreEntryException::class, 'Invalid number of scores'); +it('throws an exception if the user is not assigned to judge the entry', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + // Act & Assert + enterScore($judge, $entry, [1, 2, 5, 3, 7]); +})->throws(ScoreEntryException::class, 'This judge is not assigned to judge this entry'); +it('throws an exception if an invalid subscore is provided', function () { + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1006 => 88, + ]; + enterScore($judge, $entry, $scores); +})->throws(ScoreEntryException::class, 'Invalid Score Submission'); +it('throws an exception if a submitted subscore exceeds the maximum allowed', function () { + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1005 => 101, + ]; + enterScore($judge, $entry, $scores); +})->throws(ScoreEntryException::class, 'Supplied subscore exceeds maximum allowed'); +it('removes an existing no_show flag from the entry if one exists', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $entry->addFlag('no_show'); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1005 => 98, + ]; + // Act + enterScore($judge, $entry, $scores); + // Assert + expect($entry->hasFlag('no_show'))->toBeFalse(); +}); +it('saves the score with a properly formatted subscore object', function () { + // Arrange + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $entry->addFlag('no_show'); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1005 => 98, + ]; + $desiredReturn = [ + 1001 => [ + 'score' => 98, + 'subscore_id' => 1001, + 'subscore_name' => 'Scale', + ], + 1002 => [ + 'score' => 90, + 'subscore_id' => 1002, + 'subscore_name' => 'Etude 1', + ], + 1003 => [ + 'score' => 87, + 'subscore_id' => 1003, + 'subscore_name' => 'Etude 2', + ], + 1004 => [ + 'score' => 78, + 'subscore_id' => 1004, + 'subscore_name' => 'Sight Reading', + ], + 1005 => [ + 'score' => 98, + 'subscore_id' => 1005, + 'subscore_name' => 'Tone', + ], + ]; + // Act + $newScore = enterScore($judge, $entry, $scores); + // Assert + $checkScoreSheet = ScoreSheet::find($newScore->id); + expect($checkScoreSheet->exists())->toBeTrue() + ->and($checkScoreSheet->subscores)->toBe($desiredReturn); +}); +it('throws an exception of the entry already has a score by the judge', function() { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $entry->addFlag('no_show'); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1005 => 98, + ]; + // Act + enterScore($judge, $entry, $scores); + // Assert + enterScore($judge, $entry, $scores); +})->throws(ScoreEntryException::class, 'That judge has already entered scores for that entry'); diff --git a/tests/Feature/Seating/indexTest.php b/tests/Feature/Pages/Seating/statusTest.php similarity index 100% rename from tests/Feature/Seating/indexTest.php rename to tests/Feature/Pages/Seating/statusTest.php diff --git a/tests/Feature/Services/ScoreServiceTest.php b/tests/Feature/Services/ScoreServiceTest.php new file mode 100644 index 0000000..97728f6 --- /dev/null +++ b/tests/Feature/Services/ScoreServiceTest.php @@ -0,0 +1,44 @@ +scoreService = new ScoreService(); +}); +it('can record a score', function () { + // Arrange + // run the seeder AuditionWithScoringGuideAndRoom + artisan('db:seed', ['--class' => 'AuditionWithScoringGuideAndRoom']); + // Act & Assert + expect(Audition::find(1000)->name)->toBe('Test Audition'); +}); + +it('can check if an entry is fully scored', function () { + $room = Room::factory()->create(); + $judges = User::factory()->count(2)->create(); + $judges->each(fn ($judge) => $room->addJudge($judge)); + $audition = Audition::factory()->create(['room_id' => $room->id]); + $entry = Entry::factory()->create(['audition_id' => $audition->id]); + expect($this->scoreService->isEntryFullyScored($entry))->toBeFalse(); + ScoreSheet::create([ + 'user_id' => $judges->first()->id, + 'entry_id' => $entry->id, + 'subscores' => 7, + ]); + expect($this->scoreService->isEntryFullyScored($entry))->toBeFalse(); + ScoreSheet::create([ + 'user_id' => $judges->last()->id, + 'entry_id' => $entry->id, + 'subscores' => 7, + ]); + expect($this->scoreService->isEntryFullyScored($entry))->toBeTrue(); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 6cd9f4c..a07cc90 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -15,6 +15,7 @@ use App\Models\User; use App\Settings; use Illuminate\Foundation\Testing\TestCase; use function Pest\Laravel\actingAs; +use function Pest\Laravel\artisan; uses( Tests\TestCase::class, @@ -59,6 +60,10 @@ function actAsNormal() { actingAs(User::factory()->create()); } +function loadSampleAudition() +{ + artisan('db:seed', ['--class' => 'AuditionWithScoringGuideAndRoom']); +} uses()->beforeEach(function () { Settings::set('auditionName', 'Somewhere Band Directors Association');