auditionadmin-19 Implement Olympic scoring

This commit is contained in:
Matt Young 2024-07-14 20:56:10 -05:00
parent 772115099f
commit 5ac72c2301
10 changed files with 139 additions and 45 deletions

View File

@ -0,0 +1,99 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Services\AuditionService;
use App\Services\EntryService;
use Illuminate\Support\Facades\Cache;
use function auditionSetting;
class AllowForOlympicScoring implements CalculateEntryScore
{
protected CalculateScoreSheetTotal $calculator;
protected AuditionService $auditionService;
protected EntryService $entryService;
public function __construct(CalculateScoreSheetTotal $calculator, AuditionService $auditionService, EntryService $entryService)
{
$this->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');
}
}
}

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Actions\Tabulation\CalculateEntryScore;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Entry; use App\Models\Entry;
@ -109,7 +110,7 @@ class EntryController extends Controller
return redirect('/admin/entries'); return redirect('/admin/entries');
} }
public function edit(Entry $entry) public function edit(Entry $entry, CalculateEntryScore $calculator)
{ {
if ($entry->audition->hasFlag('seats_published')) { if ($entry->audition->hasFlag('seats_published')) {
return to_route('admin.entries.index')->with('error', 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(); $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get();
$auditions = Audition::orderBy('score_order')->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', ['entry' => $entry, 'students' => $students, 'auditions' => $auditions]);
return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores')); return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores'));
} }

View File

@ -2,9 +2,11 @@
namespace App\Models; namespace App\Models;
use App\Actions\Tabulation\CalculateScoreSheetTotal;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOneThrough; use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Support\Facades\App;
class ScoreSheet extends Model class ScoreSheet extends Model
{ {
@ -50,4 +52,11 @@ class ScoreSheet extends Model
return $judges->contains('id', $this->judge->id); return $judges->contains('id', $this->judge->id);
} }
public function totalScore($mode)
{
$calculator = App::make(CalculateScoreSheetTotal::class);
return $calculator($mode, $this->entry, $this->judge);
}
} }

View File

@ -2,7 +2,7 @@
namespace App\Providers; namespace App\Providers;
use App\Actions\Tabulation\AllJudgesCount; use App\Actions\Tabulation\AllowForOlympicScoring;
use App\Actions\Tabulation\CalculateEntryScore; use App\Actions\Tabulation\CalculateEntryScore;
use App\Actions\Tabulation\CalculateScoreSheetTotal; use App\Actions\Tabulation\CalculateScoreSheetTotal;
use App\Models\Audition; use App\Models\Audition;
@ -45,7 +45,7 @@ class AppServiceProvider extends ServiceProvider
public function register(): void public function register(): void
{ {
$this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class); $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(DrawService::class, DrawService::class);
$this->app->singleton(AuditionService::class, AuditionService::class); $this->app->singleton(AuditionService::class, AuditionService::class);
$this->app->singleton(EntryService::class, EntryService::class); $this->app->singleton(EntryService::class, EntryService::class);

View File

@ -1,28 +0,0 @@
<?php
namespace App\Providers;
use App\Actions\Tabulation\AllJudgesCount;
use App\Actions\Tabulation\CalculateEntryScore;
use App\Actions\Tabulation\CalculateScoreSheetTotal;
use Illuminate\Support\ServiceProvider;
class CalculateEntryScoreProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class);
$this->app->singleton(CalculateEntryScore::class, AllJudgesCount::class);
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}

View File

@ -2,7 +2,6 @@
return [ return [
App\Providers\AppServiceProvider::class, App\Providers\AppServiceProvider::class,
App\Providers\CalculateEntryScoreProvider::class,
App\Providers\FortifyServiceProvider::class, App\Providers\FortifyServiceProvider::class,
App\Providers\InvoiceDataServiceProvider::class, App\Providers\InvoiceDataServiceProvider::class,
]; ];

View File

@ -29,7 +29,10 @@
<x-form.toggle-checkbox name="judging_enabled"/><span>Enable score entry by judges</span> <x-form.toggle-checkbox name="judging_enabled"/><span>Enable score entry by judges</span>
</div> </div>
<div class="col-span-6 flex space-x-3"> <div class="col-span-6 flex space-x-3">
<x-form.toggle-checkbox name="olympic_scoring"/><span>Olympic scoring</span> <x-form.toggle-checkbox
checked="{{ auditionSetting('olympic_scoring') }}"
name="olympic_scoring"/>
<span>Olympic scoring</span>
</div> </div>
</x-form.body-grid> </x-form.body-grid>
</x-layout.page-section> </x-layout.page-section>

View File

@ -1,6 +1,6 @@
@php use App\Models\Seat; @endphp @php use App\Models\Seat; @endphp
<x-layout.app> <x-layout.app>
<x-card.card class="mx-auto max-w-2xl"> <x-card.card class="mx-auto max-w-3xl">
<x-card.heading> <x-card.heading>
Edit Entry #{{ $entry->id }} Edit Entry #{{ $entry->id }}
@if($entry->scoreSheets()->count() === 0) @if($entry->scoreSheets()->count() === 0)
@ -66,7 +66,7 @@
</x-card.card> </x-card.card>
<x-card.card class="mx-auto max-w-2xl mt-6"> <x-card.card class="mx-auto max-w-3xl mt-6">
<x-card.heading>Scores</x-card.heading> <x-card.heading>Scores</x-card.heading>
<x-card.list.body> <x-card.list.body>
<div class="grid sm:grid-cols-3 space-3 m-3"> <div class="grid sm:grid-cols-3 space-3 m-3">
@ -80,6 +80,17 @@
<span class="text-right">{{$subscore['score']}}</span> <span class="text-right">{{$subscore['score']}}</span>
</p> </p>
@endforeach @endforeach
<p class="grid grid-cols-2 border-b">
<span class="font-semibold">{{ auditionSetting('auditionAbbreviation') }} Total</span>
<span class="text-right font-semibold">{{ $score->totalScore('seating')[0] }}</span>
</p>
@if( auditionSetting('advanceTo'))
<p class="grid grid-cols-2 border-b">
<span class="font-semibold">{{ auditionSetting('advanceTo') }} Total</span>
<span class="text-right font-semibold">{{ $score->totalScore('advancement')[0] }}</span>
</p>
@endif
@if(! $score->isValid()) @if(! $score->isValid())
<form method="POST" action="{{ route('scores.destroy',['score'=>$score->id]) }}"> <form method="POST" action="{{ route('scores.destroy',['score'=>$score->id]) }}">
@csrf @csrf

View File

@ -2,7 +2,7 @@
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
use App\Actions\Tabulation\AllJudgesCount; use App\Actions\Tabulation\AllowForOlympicScoring;
use App\Exceptions\TabulationException; use App\Exceptions\TabulationException;
use App\Models\Entry; use App\Models\Entry;
use App\Models\Room; use App\Models\Room;
@ -14,14 +14,14 @@ uses(RefreshDatabase::class);
it('throws an exception if mode is not seating or advancement', function () { it('throws an exception if mode is not seating or advancement', function () {
#$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class); $calculator = App::make(AllowForOlympicScoring::class);
$calculator->calculate('WRONG', Entry::factory()->create()); $calculator->calculate('WRONG', Entry::factory()->create());
})->throws(TabulationException::class, 'Mode must be seating or advancement'); })->throws(TabulationException::class, 'Mode must be seating or advancement');
it('throws an exception if entry is not valid', function () { it('throws an exception if entry is not valid', function () {
// Arrange // Arrange
#$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class); $calculator = App::make(AllowForOlympicScoring::class);
// Act // Act
$calculator->calculate('seating', Entry::factory()->make()); $calculator->calculate('seating', Entry::factory()->make());
// Assert // Assert
@ -42,7 +42,7 @@ it('throws an exception if entry is missing judge scores', function () {
1005 => 90, 1005 => 90,
]; ];
#$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class); $calculator = App::make(AllowForOlympicScoring::class);
enterScore($judge1, $entry, $scores); enterScore($judge1, $entry, $scores);
// Act // Act
$calculator->calculate('seating', $entry); $calculator->calculate('seating', $entry);
@ -66,7 +66,7 @@ it('throws an exception if a score exists from an invalid judge', function () {
1005 => 90, 1005 => 90,
]; ];
#$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class); $calculator = App::make(AllowForOlympicScoring::class);
enterScore($judge1, $entry, $scores); enterScore($judge1, $entry, $scores);
$scoreSheetToSpoof = enterScore($judge2, $entry, $scores); $scoreSheetToSpoof = enterScore($judge2, $entry, $scores);
$scoreSheetToSpoof->update(['user_id' => $judge3->id]); $scoreSheetToSpoof->update(['user_id' => $judge3->id]);
@ -98,7 +98,7 @@ it('correctly calculates scores for seating', function () {
1005 => 95, 1005 => 95,
]; ];
#$calculator = new AllJudgesCount(); #$calculator = new AllJudgesCount();
$calculator = App::make(AllJudgesCount::class); $calculator = App::make(AllowForOlympicScoring::class);
enterScore($judge1, $entry, $scores); enterScore($judge1, $entry, $scores);
enterScore($judge2, $entry, $scores2); enterScore($judge2, $entry, $scores2);
// Act // Act
@ -130,7 +130,7 @@ it('correctly calculates scores for advancement', function () {
1004 => 85, 1004 => 85,
1005 => 95, 1005 => 95,
]; ];
$calculator = App::make(AllJudgesCount::class); $calculator = App::make(AllowForOlympicScoring::class);
enterScore($judge1, $entry, $scores); enterScore($judge1, $entry, $scores);
enterScore($judge2, $entry, $scores2); enterScore($judge2, $entry, $scores2);
// Act // Act

View File

@ -1,6 +1,6 @@
<?php <?php
use App\Actions\Tabulation\AllJudgesCount; use App\Actions\Tabulation\AllowForOlympicScoring;
use App\Actions\Tabulation\RankAuditionEntries; use App\Actions\Tabulation\RankAuditionEntries;
use App\Exceptions\TabulationException; use App\Exceptions\TabulationException;
use App\Models\Audition; use App\Models\Audition;