auditionadmin-19 Implement Olympic scoring
This commit is contained in:
parent
772115099f
commit
5ac72c2301
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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,
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue