From 6f0a4ac9bc3691fa40a1eee02000fec6059ead98 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 10 Jul 2024 23:22:37 -0500 Subject: [PATCH] Seating page lists entries in score order --- app/Actions/Tabulation/AllJudgesCount.php | 11 ++- .../Tabulation/CalculateEntryScore.php | 1 + .../Tabulation/RankAuditionEntries.php | 71 +++++++++++++++++++ .../Tabulation/SeatAuditionController.php | 22 +++--- app/Http/Controllers/TestController.php | 39 ++-------- app/Models/Entry.php | 2 + app/Models/Event.php | 5 ++ app/Providers/AppServiceProvider.php | 9 ++- composer.json | 1 + composer.lock | 67 ++++++++++++++++- resources/views/test.blade.php | 9 +-- .../Actions/RankAuditionEntriesTest.php | 61 ++++++++++++++++ 12 files changed, 246 insertions(+), 52 deletions(-) create mode 100644 app/Actions/Tabulation/RankAuditionEntries.php create mode 100644 tests/Feature/Actions/RankAuditionEntriesTest.php diff --git a/app/Actions/Tabulation/AllJudgesCount.php b/app/Actions/Tabulation/AllJudgesCount.php index 15ba4c8..4cdb648 100644 --- a/app/Actions/Tabulation/AllJudgesCount.php +++ b/app/Actions/Tabulation/AllJudgesCount.php @@ -21,6 +21,7 @@ class AllJudgesCount implements CalculateEntryScore $this->basicValidation($mode, $entry); $this->areAllJudgesIn($entry); $this->areAllJudgesValid($entry); + return $this->getJudgeTotals($mode, $entry); } @@ -30,8 +31,14 @@ class AllJudgesCount implements CalculateEntryScore foreach ($entry->audition->judges as $judge) { $scores[] = $this->calculator->__invoke($mode, $entry, $judge); } - for ($i = 0; $i < count($scores[0]); $i++) { - $sums[] = $scores[0][$i] + $scores[1][$i]; + // 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; diff --git a/app/Actions/Tabulation/CalculateEntryScore.php b/app/Actions/Tabulation/CalculateEntryScore.php index 8a719a1..8cb48a8 100644 --- a/app/Actions/Tabulation/CalculateEntryScore.php +++ b/app/Actions/Tabulation/CalculateEntryScore.php @@ -6,5 +6,6 @@ use App\Models\Entry; interface CalculateEntryScore { + public function calculate(string $mode, Entry $entry): array; } diff --git a/app/Actions/Tabulation/RankAuditionEntries.php b/app/Actions/Tabulation/RankAuditionEntries.php new file mode 100644 index 0000000..aee334c --- /dev/null +++ b/app/Actions/Tabulation/RankAuditionEntries.php @@ -0,0 +1,71 @@ +calculator = $calculator; + } + + public function booo() + { + return 'blah'; + } + + public function rank(string $mode, Audition $audition): Collection + { + $this->basicValidation($mode, $audition); + $entries = match ($mode) { + 'seating' => $audition->entries()->forSeating()->get(), + 'advancement' => $audition->entries()->forAdvancement()->get(), + }; + + foreach ($entries as $entry) { + try { + $entry->score_totals = $this->calculator->calculate($mode, $entry); + } catch (TabulationException $ex) { + $entry->score_totals = [-1]; + $entry->score_message = $ex->getMessage(); + } + } + // Sort entries based on their total score, then by subscores in tiebreak order + $entries = $entries->sort(function ($a, $b) { + for ($i = 0; $i < count($a->score_totals); $i++) { + if ($a->score_totals[$i] > $b->score_totals[$i]) { + return -1; + } elseif ($a->score_totals[$i] < $b->score_totals[$i]) { + return 1; + } + } + + return 0; + }); + $rank = 1; + foreach ($entries as $entry) { + $entry->rank = $rank; + $rank++; + } + + return $entries; + } + + protected function basicValidation($mode, Audition $audition): void + { + if ($mode !== 'seating' && $mode !== 'advancement') { + throw new TabulationException('Mode must be seating or advancement'); + } + if (! $audition->exists()) { + throw new TabulationException('Invalid audition provided'); + } + } +} diff --git a/app/Http/Controllers/Tabulation/SeatAuditionController.php b/app/Http/Controllers/Tabulation/SeatAuditionController.php index 980ec52..91dec57 100644 --- a/app/Http/Controllers/Tabulation/SeatAuditionController.php +++ b/app/Http/Controllers/Tabulation/SeatAuditionController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Tabulation; use App\Actions\Tabulation\CalculateEntryScore; +use App\Actions\Tabulation\RankAuditionEntries; use App\Exceptions\TabulationException; use App\Http\Controllers\Controller; use App\Models\Audition; @@ -12,30 +13,31 @@ use Illuminate\Http\Request; class SeatAuditionController extends Controller { protected CalculateEntryScore $calc; + protected RankAuditionEntries $ranker; - public function __construct(CalculateEntryScore $calc) + public function __construct(CalculateEntryScore $calc, RankAuditionEntries $ranker) { $this->calc = $calc; + $this->ranker = $ranker; } public function __invoke(Request $request, Audition $audition) { $entryData = []; - $entries = Entry::forSeating()->with('student.school')->where('audition_id', $audition->id)->get(); + #$entries = Entry::forSeating()->with('student.school')->where('audition_id', $audition->id)->get(); + $entries = $this->ranker->rank('seating', $audition); + $entries->load('student.school'); foreach ($entries as $entry) { - try { - $totalScore = $this->calc->calculate('seating', $entry); - } catch (TabulationException $ex) { - $totalScore[0] = $ex->getMessage(); - } + $totalScoreColumn = $entry->score_totals[0] >= 0 ? + $entry->score_totals[0] : $entry->score_message; $entryData[] = [ - 'rank' => 'not implemented', + 'rank' => $entry->rank, 'id' => $entry->id, 'studentName' => $entry->student->full_name(), 'schoolName' => $entry->student->school->name, 'drawNumber' => $entry->draw_number, - 'totalScore' => $totalScore[0], - 'fullyScored' => is_numeric($totalScore[0]), + 'totalScore' => $totalScoreColumn, + 'fullyScored' => $entry->score_totals[0] >= 0, ]; } diff --git a/app/Http/Controllers/TestController.php b/app/Http/Controllers/TestController.php index 0d68144..9a39421 100644 --- a/app/Http/Controllers/TestController.php +++ b/app/Http/Controllers/TestController.php @@ -3,45 +3,20 @@ namespace App\Http\Controllers; use App\Actions\Tabulation\CalculateEntryScore; -use App\Actions\Tabulation\CalculateScoreSheetTotal; -use App\Exceptions\TabulationException; -use App\Models\Entry; -use App\Models\User; +use App\Actions\Tabulation\RankAuditionEntries; class TestController extends Controller { - protected CalculateEntryScore $bigCalc; - public function __construct(CalculateEntryScore $bigCalc) + protected RankAuditionEntries $rankomatic; + + public function __construct(RankAuditionEntries $rankomatic) { - $this->bigCalc = $bigCalc; + $this->rankomatic = $rankomatic; } public function flashTest() { - $entries = Entry::forSeating()->with('student')->where('audition_id', 17)->get(); - $rows = []; - foreach ($entries as $entry) { - try { - $totalScore = $this->bigCalc->calculate('seating', $entry)[0]; - } catch (TabulationException $ex){ - $totalScore = '--'; - } - $rows[] = [ - 'name' => $entry->student->full_name(), - 'totalScore' => $totalScore, - ]; - } - $scoreCalc = new CalculateScoreSheetTotal; - $bam = $scoreCalc('seating', Entry::find(916), User::find(65))[0]; -// try { -// $test = $this->bigCalc->calculate('seating', Entry::find(1061))[0]; -// } catch (TabulationException $ex) { -// dd($ex); -// } - - - - - return view('test', compact('rows', 'bam')); + dd($this->rankomatic->booo()); + return view('test'); } } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 3d59f15..0acd0cd 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOneThrough; +use Staudenmeir\BelongsToThrough; use App\Models\ScoreSheet; class Entry extends Model @@ -38,6 +39,7 @@ class Entry extends Model return $this->belongsTo(Audition::class); } + public function school(): HasOneThrough { return $this->hasOneThrough( diff --git a/app/Models/Event.php b/app/Models/Event.php index b8b9726..b391c21 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -21,4 +21,9 @@ class Event extends Model return $this->hasMany(Ensemble::class) ->orderBy('rank'); } + + public function entries() + { + return $this->hasManyThrough(Entry::class, Audition::class); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index bc0e89d..0580e45 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,9 @@ namespace App\Providers; +use App\Actions\Tabulation\AllJudgesCount; +use App\Actions\Tabulation\CalculateEntryScore; +use App\Actions\Tabulation\CalculateScoreSheetTotal; use App\Models\Audition; use App\Models\Entry; use App\Models\Room; @@ -40,9 +43,9 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - // $this->app->singleton(DrawService::class, function () { - // return new DrawService(); - // }); + $this->app->singleton(DrawService::class, function () { + return new DrawService(); + }); // // $this->app->singleton(AuditionService::class, function () { // return new AuditionService(); diff --git a/composer.json b/composer.json index 853e072..c723ebb 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "laravel/pail": "^1.1", "laravel/tinker": "^2.9", "predis/predis": "^2.2", + "staudenmeir/belongs-to-through": "^2.5", "symfony/http-client": "^7.1", "symfony/mailgun-mailer": "^7.1" }, diff --git a/composer.lock b/composer.lock index 1a7527c..a9f9731 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7aab57ef52f0152526434decd76ef1e1", + "content-hash": "cd8959ab9db27e12c6fce8cf87c52d90", "packages": [ { "name": "bacon/bacon-qr-code", @@ -3601,6 +3601,71 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "staudenmeir/belongs-to-through", + "version": "v2.16", + "source": { + "type": "git", + "url": "https://github.com/staudenmeir/belongs-to-through.git", + "reference": "79667db6660fa0065b24415bab29a5f85a0128c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staudenmeir/belongs-to-through/zipball/79667db6660fa0065b24415bab29a5f85a0128c7", + "reference": "79667db6660fa0065b24415bab29a5f85a0128c7", + "shasum": "" + }, + "require": { + "illuminate/database": "^11.0", + "php": "^8.2" + }, + "require-dev": { + "barryvdh/laravel-ide-helper": "^3.0", + "orchestra/testbench": "^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Staudenmeir\\BelongsToThrough\\IdeHelperServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Znck\\Eloquent\\": "src/", + "Staudenmeir\\BelongsToThrough\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rahul Kadyan", + "email": "hi@znck.me" + }, + { + "name": "Jonas Staudenmeir", + "email": "mail@jonas-staudenmeir.de" + } + ], + "description": "Laravel Eloquent BelongsToThrough relationships", + "support": { + "issues": "https://github.com/staudenmeir/belongs-to-through/issues", + "source": "https://github.com/staudenmeir/belongs-to-through/tree/v2.16" + }, + "funding": [ + { + "url": "https://paypal.me/JonasStaudenmeir", + "type": "custom" + } + ], + "time": "2024-03-09T09:53:11+00:00" + }, { "name": "symfony/clock", "version": "v7.0.7", diff --git a/resources/views/test.blade.php b/resources/views/test.blade.php index ff56041..0eb3464 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;use App\Models\Entry;use App\Models\User; @endphp +@php use App\Enums\AuditionFlags;use App\Models\Audition;use App\Models\AuditionFlag;use App\Models\Entry;use App\Models\Event;use App\Models\User; @endphp @php @endphp @inject('scoreservice','App\Services\ScoreService'); @inject('auditionService','App\Services\AuditionService'); @@ -7,8 +7,9 @@ @inject('drawService', 'App\Services\DrawService') Test Page - {{ $bam ?? '' }} - @foreach($rows as $row) - {{ $row['name'] }} - {{ $row['totalScore'] }}
- @endforeach + @foreach(Event::first()->entries->groupBy('student_id') as $student) + Name: {{ $student[0]->student->full_name() }}
+ Entry Count: {{ $student->count() }}
+ @endforeach
diff --git a/tests/Feature/Actions/RankAuditionEntriesTest.php b/tests/Feature/Actions/RankAuditionEntriesTest.php new file mode 100644 index 0000000..5e1a677 --- /dev/null +++ b/tests/Feature/Actions/RankAuditionEntriesTest.php @@ -0,0 +1,61 @@ +rank('wrong', Audition::factory()->create()); +})->throws(TabulationException::class, 'Mode must be seating or advancement'); +it('throws an exception if an invalid audition is provided', function () { + // Arrange + $ranker = new RankAuditionEntries(new AllJudgesCount()); + // Act & Assert + $ranker->rank('seating', Audition::factory()->make()); +})->throws(TabulationException::class, 'Invalid audition provided'); +it('includes all entries of the given mode in the return', function () { + $audition = Audition::factory()->create(); + $entries = Entry::factory()->seatingOnly()->count(10)->create(['audition_id' => $audition->id]); + $otherEntries = Entry::factory()->advanceOnly()->count(10)->create(['audition_id' => $audition->id]); + $ranker = new RankAuditionEntries(new AllJudgesCount()); + // Act + $return = $ranker->rank('seating', $audition); + // Assert + foreach ($entries as $entry) { + expect($return->pluck('id')->toArray())->toContain($entry->id); + } + foreach ($otherEntries as $entry) { + expect($return->pluck('id')->toArray())->not()->toContain($entry->id); + } +}); +it('places entries in the proper order', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + Room::find(1000)->addJudge($judge); + $entries = Entry::factory()->count(5)->create(['audition_id' => 1000]); + $scoreArray1 = [1001 => 90, 1002 => 90, 1003 => 90, 1004 => 90, 1005 => 90]; + $scoreArray2 = [1001 => 60, 1002 => 60, 1003 => 60, 1004 => 60, 1005 => 60]; + $scoreArray3 = [1001 => 80, 1002 => 80, 1003 => 80, 1004 => 80, 1005 => 80]; + $scoreArray4 = [1001 => 100, 1002 => 100, 1003 => 100, 1004 => 100, 1005 => 100]; + $scoreArray5 = [1001 => 70, 1002 => 70, 1003 => 70, 1004 => 70, 1005 => 70]; + enterScore($judge, $entries[0], $scoreArray1); + enterScore($judge, $entries[1], $scoreArray2); + enterScore($judge, $entries[2], $scoreArray3); + enterScore($judge, $entries[3], $scoreArray4); + enterScore($judge, $entries[4], $scoreArray5); + $ranker = new RankAuditionEntries(new AllJudgesCount()); + $expectedOrder = [4, 1, 3, 5, 2]; + // Act + $return = $ranker->rank('seating', Audition::find(1000)); + // Assert + expect($return->pluck('id')->toArray())->toBe($expectedOrder); +});