From 5ac72c2301c9c80789a2ab378872874a1b8613e2 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 14 Jul 2024 20:56:10 -0500 Subject: [PATCH] auditionadmin-19 Implement Olympic scoring --- .../Tabulation/AllowForOlympicScoring.php | 99 +++++++++++++++++++ .../Controllers/Admin/EntryController.php | 7 +- app/Models/ScoreSheet.php | 9 ++ app/Providers/AppServiceProvider.php | 4 +- app/Providers/CalculateEntryScoreProvider.php | 28 ------ bootstrap/providers.php | 1 - .../views/admin/audition-settings.blade.php | 5 +- resources/views/admin/entries/edit.blade.php | 15 ++- .../AllJudgesCountTest.php | 14 +-- .../Actions/RankAuditionEntriesTest.php | 2 +- 10 files changed, 139 insertions(+), 45 deletions(-) create mode 100644 app/Actions/Tabulation/AllowForOlympicScoring.php delete mode 100644 app/Providers/CalculateEntryScoreProvider.php diff --git a/app/Actions/Tabulation/AllowForOlympicScoring.php b/app/Actions/Tabulation/AllowForOlympicScoring.php new file mode 100644 index 0000000..a5346cf --- /dev/null +++ b/app/Actions/Tabulation/AllowForOlympicScoring.php @@ -0,0 +1,99 @@ +calculator = $calculator; + $this->auditionService = $auditionService; + $this->entryService = $entryService; + } + + public function calculate(string $mode, Entry $entry): array + { + + $cacheKey = 'entryScore-'.$entry->id.'-'.$mode; + return Cache::remember($cacheKey, 10, function () use ($mode, $entry) { + $this->basicValidation($mode, $entry); + $this->areAllJudgesIn($entry); + $this->areAllJudgesValid($entry); + + return $this->getJudgeTotals($mode, $entry); + }); + + } + + protected function getJudgeTotals($mode, Entry $entry) + { + + $scores = []; + foreach ($this->auditionService->getJudges($entry->audition) as $judge) { + $scores[] = $this->calculator->__invoke($mode, $entry, $judge); + } + // sort the scores array by the total score + usort($scores, function ($a, $b) { + return $a[0] <=> $b[0]; + }); + + // we can only really do olympic scoring if there are at least 3 scores + if (count($scores) >= 3 && auditionSetting('olympic_scoring')) { + // remove the highest and lowest scores + array_pop($scores); + array_shift($scores); + } + $sums = []; + // Sum each subscore from the judges + foreach ($scores as $score) { + $index = 0; + foreach ($score as $value) { + $sums[$index] = $sums[$index] ?? 0; + $sums[$index] += $value; + $index++; + } + } + return $sums; + } + + protected function basicValidation($mode, $entry): void + { + if ($mode !== 'seating' && $mode !== 'advancement') { + throw new TabulationException('Mode must be seating or advancement'); + } + + if (! $this->entryService->entryExists($entry)) { + throw new TabulationException('Invalid entry specified'); + } + } + + protected function areAllJudgesIn(Entry $entry): void + { + $assignedJudgeCount = $this->auditionService->getJudges($entry->audition)->count(); + if ($entry->scoreSheets->count() !== $assignedJudgeCount) { + throw new TabulationException('Not all score sheets are in'); + } + } + + protected function areAllJudgesValid(Entry $entry): void + { + $validJudgeIds = $this->auditionService->getJudges($entry->audition)->sort()->pluck('id')->toArray(); + $existingJudgeIds = $entry->scoreSheets->sort()->pluck('user_id')->toArray(); + if ($validJudgeIds !== $existingJudgeIds) { + throw new TabulationException('Score exists from a judge not assigned to this audition'); + } + } +} diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index 79e016a..a04da74 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Admin; +use App\Actions\Tabulation\CalculateEntryScore; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Entry; @@ -109,7 +110,7 @@ class EntryController extends Controller return redirect('/admin/entries'); } - public function edit(Entry $entry) + public function edit(Entry $entry, CalculateEntryScore $calculator) { if ($entry->audition->hasFlag('seats_published')) { return to_route('admin.entries.index')->with('error', @@ -123,8 +124,8 @@ class EntryController extends Controller $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get(); $auditions = Audition::orderBy('score_order')->get(); - $scores = $entry->scoreSheets()->get(); - + $scores = $entry->scoreSheets()->with('audition', 'judge')->get(); + $scores->each(fn ($score) => $score->entry = $entry); // return view('admin.entries.edit', ['entry' => $entry, 'students' => $students, 'auditions' => $auditions]); return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores')); } diff --git a/app/Models/ScoreSheet.php b/app/Models/ScoreSheet.php index cf93064..1d41ccc 100644 --- a/app/Models/ScoreSheet.php +++ b/app/Models/ScoreSheet.php @@ -2,9 +2,11 @@ namespace App\Models; +use App\Actions\Tabulation\CalculateScoreSheetTotal; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasOneThrough; +use Illuminate\Support\Facades\App; class ScoreSheet extends Model { @@ -50,4 +52,11 @@ class ScoreSheet extends Model return $judges->contains('id', $this->judge->id); } + + public function totalScore($mode) + { + $calculator = App::make(CalculateScoreSheetTotal::class); + + return $calculator($mode, $this->entry, $this->judge); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 845e62b..6b048c4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,7 +2,7 @@ namespace App\Providers; -use App\Actions\Tabulation\AllJudgesCount; +use App\Actions\Tabulation\AllowForOlympicScoring; use App\Actions\Tabulation\CalculateEntryScore; use App\Actions\Tabulation\CalculateScoreSheetTotal; use App\Models\Audition; @@ -45,7 +45,7 @@ class AppServiceProvider extends ServiceProvider public function register(): void { $this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class); - $this->app->singleton(CalculateEntryScore::class, AllJudgesCount::class); + $this->app->singleton(CalculateEntryScore::class, AllowForOlympicScoring::class); $this->app->singleton(DrawService::class, DrawService::class); $this->app->singleton(AuditionService::class, AuditionService::class); $this->app->singleton(EntryService::class, EntryService::class); diff --git a/app/Providers/CalculateEntryScoreProvider.php b/app/Providers/CalculateEntryScoreProvider.php deleted file mode 100644 index 8d0a2d1..0000000 --- a/app/Providers/CalculateEntryScoreProvider.php +++ /dev/null @@ -1,28 +0,0 @@ -app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class); - $this->app->singleton(CalculateEntryScore::class, AllJudgesCount::class); - } - - /** - * Bootstrap services. - */ - public function boot(): void - { - // - } -} diff --git a/bootstrap/providers.php b/bootstrap/providers.php index fd4c1d5..9edefb0 100644 --- a/bootstrap/providers.php +++ b/bootstrap/providers.php @@ -2,7 +2,6 @@ return [ App\Providers\AppServiceProvider::class, - App\Providers\CalculateEntryScoreProvider::class, App\Providers\FortifyServiceProvider::class, App\Providers\InvoiceDataServiceProvider::class, ]; diff --git a/resources/views/admin/audition-settings.blade.php b/resources/views/admin/audition-settings.blade.php index 17cbcf9..87a1d34 100644 --- a/resources/views/admin/audition-settings.blade.php +++ b/resources/views/admin/audition-settings.blade.php @@ -29,7 +29,10 @@ Enable score entry by judges
- Olympic scoring + + Olympic scoring
diff --git a/resources/views/admin/entries/edit.blade.php b/resources/views/admin/entries/edit.blade.php index 9abef9b..08d01c1 100644 --- a/resources/views/admin/entries/edit.blade.php +++ b/resources/views/admin/entries/edit.blade.php @@ -1,6 +1,6 @@ @php use App\Models\Seat; @endphp - + Edit Entry #{{ $entry->id }} @if($entry->scoreSheets()->count() === 0) @@ -66,7 +66,7 @@ - + Scores
@@ -80,6 +80,17 @@ {{$subscore['score']}}

@endforeach +

+ {{ auditionSetting('auditionAbbreviation') }} Total + {{ $score->totalScore('seating')[0] }} +

+ + @if( auditionSetting('advanceTo')) +

+ {{ auditionSetting('advanceTo') }} Total + {{ $score->totalScore('advancement')[0] }} +

+ @endif @if(! $score->isValid())
@csrf diff --git a/tests/Feature/Actions/CalculateEntryScore/AllJudgesCountTest.php b/tests/Feature/Actions/CalculateEntryScore/AllJudgesCountTest.php index 17c1dbd..2225abf 100644 --- a/tests/Feature/Actions/CalculateEntryScore/AllJudgesCountTest.php +++ b/tests/Feature/Actions/CalculateEntryScore/AllJudgesCountTest.php @@ -2,7 +2,7 @@ /** @noinspection PhpUnhandledExceptionInspection */ -use App\Actions\Tabulation\AllJudgesCount; +use App\Actions\Tabulation\AllowForOlympicScoring; use App\Exceptions\TabulationException; use App\Models\Entry; use App\Models\Room; @@ -14,14 +14,14 @@ uses(RefreshDatabase::class); it('throws an exception if mode is not seating or advancement', function () { #$calculator = new AllJudgesCount(); - $calculator = App::make(AllJudgesCount::class); + $calculator = App::make(AllowForOlympicScoring::class); $calculator->calculate('WRONG', Entry::factory()->create()); })->throws(TabulationException::class, 'Mode must be seating or advancement'); it('throws an exception if entry is not valid', function () { // Arrange #$calculator = new AllJudgesCount(); - $calculator = App::make(AllJudgesCount::class); + $calculator = App::make(AllowForOlympicScoring::class); // Act $calculator->calculate('seating', Entry::factory()->make()); // Assert @@ -42,7 +42,7 @@ it('throws an exception if entry is missing judge scores', function () { 1005 => 90, ]; #$calculator = new AllJudgesCount(); - $calculator = App::make(AllJudgesCount::class); + $calculator = App::make(AllowForOlympicScoring::class); enterScore($judge1, $entry, $scores); // Act $calculator->calculate('seating', $entry); @@ -66,7 +66,7 @@ it('throws an exception if a score exists from an invalid judge', function () { 1005 => 90, ]; #$calculator = new AllJudgesCount(); - $calculator = App::make(AllJudgesCount::class); + $calculator = App::make(AllowForOlympicScoring::class); enterScore($judge1, $entry, $scores); $scoreSheetToSpoof = enterScore($judge2, $entry, $scores); $scoreSheetToSpoof->update(['user_id' => $judge3->id]); @@ -98,7 +98,7 @@ it('correctly calculates scores for seating', function () { 1005 => 95, ]; #$calculator = new AllJudgesCount(); - $calculator = App::make(AllJudgesCount::class); + $calculator = App::make(AllowForOlympicScoring::class); enterScore($judge1, $entry, $scores); enterScore($judge2, $entry, $scores2); // Act @@ -130,7 +130,7 @@ it('correctly calculates scores for advancement', function () { 1004 => 85, 1005 => 95, ]; - $calculator = App::make(AllJudgesCount::class); + $calculator = App::make(AllowForOlympicScoring::class); enterScore($judge1, $entry, $scores); enterScore($judge2, $entry, $scores2); // Act diff --git a/tests/Feature/Actions/RankAuditionEntriesTest.php b/tests/Feature/Actions/RankAuditionEntriesTest.php index 6ced42e..8fdf1f6 100644 --- a/tests/Feature/Actions/RankAuditionEntriesTest.php +++ b/tests/Feature/Actions/RankAuditionEntriesTest.php @@ -1,6 +1,6 @@