Rewrite tabulation #14

Merged
okorpheus merged 43 commits from rewrite-tabulation into master 2024-07-14 05:36:29 +00:00
6 changed files with 200 additions and 48 deletions
Showing only changes of commit 49ebfda9a8 - Show all commits

View File

@ -0,0 +1,57 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Models\ScoreSheet;
use App\Models\User;
class CalculateScoreSheetTotal
{
public function __construct()
{
}
public function __invoke(string $mode, Entry $entry, User $judge): array
{
$this->basicValidations($mode, $entry, $judge);
$scoreSheet = ScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
if (! $scoreSheet) {
throw new TabulationException('No score sheet by that judge for that entry');
}
$subscores = match ($mode) {
'seating' => $entry->audition->scoringGuide->subscores->where('for_seating', true)->sortBy('tiebreak_order'),
'advancement' => $entry->audition->scoringGuide->subscores->where('for_advance', true)->sortBy('tiebreak_order'),
};
$scoreTotal = 0;
$weightsTotal = 0;
$scoreArray = [];
foreach ($subscores as $subscore) {
$weight = $subscore['weight'];
$score = $scoreSheet->subscores[$subscore->id]['score'];
$scoreArray[] = $score;
$scoreTotal += ($score * $weight);
$weightsTotal += $weight;
}
$finalScore = $scoreTotal / $weightsTotal;
// put $final score at the beginning of the $ScoreArray
array_unshift($scoreArray, $finalScore);
return $scoreArray;
}
protected function basicValidations($mode, $entry, $judge): void
{
if ($mode !== 'seating' and $mode !== 'advancement') {
throw new TabulationException('Invalid mode requested. Mode must be seating or advancement');
}
if (! $entry->exists()) {
throw new TabulationException('Invalid entry provided');
}
if (! $judge->exists()) {
throw new TabulationException('Invalid judge provided');
}
}
}

View File

@ -2,21 +2,12 @@
namespace App\Providers;
use App\Events\AuditionChange;
use App\Events\EntryChange;
use App\Events\ScoreSheetChange;
use App\Events\ScoringGuideChange;
use App\Events\SeatingLimitChange;
use App\Listeners\RefreshAuditionCache;
use App\Listeners\RefreshEntryCache;
use App\Listeners\RefreshScoreSheetCache;
use App\Listeners\RefreshScoringGuideCache;
use App\Listeners\RefreshSeatingLimitCache;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\Room;
use App\Models\RoomUser;
use App\Models\School;
use App\Models\ScoreSheet;
use App\Models\ScoringGuide;
use App\Models\SeatingLimit;
use App\Models\Student;
@ -40,10 +31,7 @@ use App\Services\EntryService;
use App\Services\ScoreService;
use App\Services\SeatingService;
use App\Services\TabulationService;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use App\Models\ScoreSheet;
class AppServiceProvider extends ServiceProvider
{
@ -52,36 +40,36 @@ 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();
// });
//
// $this->app->singleton(SeatingService::class, function ($app) {
// return new SeatingService($app->make(TabulationService::class));
// });
//
$this->app->singleton(EntryService::class, function () {
return new EntryService();
});
$this->app->singleton(AuditionService::class, function () {
return new AuditionService();
});
$this->app->singleton(SeatingService::class, function ($app) {
return new SeatingService($app->make(TabulationService::class));
});
$this->app->singleton(EntryService::class, function ($app) {
return new EntryService($app->make(AuditionService::class));
});
$this->app->singleton(ScoreService::class, function ($app) {
return new ScoreService($app->make(AuditionService::class), $app->make(EntryService::class));
});
$this->app->singleton(TabulationService::class, function ($app) {
return new TabulationService(
$app->make(AuditionService::class),
$app->make(ScoreService::class),
$app->make(EntryService::class));
});
$this->app->singleton(DoublerService::class, function ($app) {
return new DoublerService($app->make(AuditionService::class), $app->make(TabulationService::class), $app->make(SeatingService::class));
$this->app->singleton(ScoreService::class, function () {
return new ScoreService();
});
//
// $this->app->singleton(TabulationService::class, function ($app) {
// return new TabulationService(
// $app->make(AuditionService::class),
// $app->make(ScoreService::class),
// $app->make(EntryService::class));
// });
//
// $this->app->singleton(DoublerService::class, function ($app) {
// return new DoublerService($app->make(AuditionService::class), $app->make(TabulationService::class), $app->make(SeatingService::class));
// });
}
/**
@ -102,6 +90,5 @@ class AppServiceProvider extends ServiceProvider
User::observe(UserObserver::class);
SeatingLimit::observe(SeatingLimitObserver::class);
}
}

View File

@ -3,6 +3,7 @@
namespace App\Services;
use App\Models\Entry;
use App\Models\User;
class ScoreService
{
@ -21,5 +22,10 @@ class ScoreService
return $requiredJudges === $scoreSheets;
}
public function scoreSheetTotal(string $mode, User $judge, Entry $entry): float
{
}
}

View File

@ -0,0 +1,72 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
use App\Actions\CalculateScoreSheetTotal;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Models\Room;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->calculator = new CalculateScoreSheetTotal();
});
it('throws an exception if an invalid mode is called for', function () {
$calculator = new CalculateScoreSheetTotal();
$calculator('anything', Entry::factory()->create(), User::factory()->create());
})->throws(TabulationException::class, 'Invalid mode requested. Mode must be seating or advancement');
it('throws an exception if an invalid judge is provided', function () {
$calculator = new CalculateScoreSheetTotal();
$calculator('seating', Entry::factory()->create(), User::factory()->make());
})->throws(TabulationException::class, 'Invalid judge provided');
it('throws an exception if an invalid entry is provided', function () {
$calculator = new CalculateScoreSheetTotal();
$calculator('advancement', Entry::factory()->make(), User::factory()->create());
})->throws(TabulationException::class, 'Invalid entry provided');
it('throws an exception if the specified judge has not scored the entry', function () {
// Arrange
$calculator = new CalculateScoreSheetTotal();
// Act
$calculator('seating', Entry::factory()->create(), User::factory()->create());
//Assert
})->throws(TabulationException::class, 'No score sheet by that judge for that entry');
it('correctly calculates final score for seating', function () {
loadSampleAudition();
$judge = User::factory()->create();
Room::find(1000)->addJudge($judge);
$entry = Entry::factory()->create(['audition_id' => 1000]);
$scores = [
1001 => 50,
1002 => 60,
1003 => 70,
1004 => 80,
1005 => 90,
];
enterScore($judge, $entry, $scores);
$calculator = new CalculateScoreSheetTotal();
$total = $calculator('seating', $entry, $judge);
expect($total[0])->toBe(68.75);
$expectedArray = [68.75, 80, 60, 70, 50];
expect($total)->toBe($expectedArray);
});
it('correctly calculates final score for advancement', function () {
loadSampleAudition();
$judge = User::factory()->create();
Room::find(1000)->addJudge($judge);
$entry = Entry::factory()->create(['audition_id' => 1000]);
$scores = [
1001 => 50,
1002 => 60,
1003 => 70,
1004 => 80,
1005 => 90,
];
enterScore($judge, $entry, $scores);
$calculator = new CalculateScoreSheetTotal();
$total = $calculator('advancement', $entry, $judge);
expect($total[0])->toBe(73.75);
$expectedArray = [73.75, 90, 80, 60, 70];
expect($total)->toBe($expectedArray);
});

View File

@ -0,0 +1,37 @@
<?php
use App\Models\Audition;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\get;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->audition = Audition::factory()->create();
$this->r = route('seating.audition', $this->audition);
});
it('denies access to a guest', function () {
get($this->r)
->assertRedirect(route('home'));
});
it('denies access to a normal user', function () {
actAsNormal();
get($this->r)
->assertRedirect(route('dashboard'))
->assertSessionHas('error', 'You are not authorized to perform this action');
});
it('grants access to admin', function () {
// Arrange
actAsAdmin();
// Act & Assert
get($this->r)->assertOk();
});
it('grants access to tabulators', function () {
// Arrange
actAsTab();
// Act & Assert
get($this->r)->assertOk();
});

View File

@ -14,13 +14,6 @@ uses(RefreshDatabase::class);
beforeEach(function () {
$this->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();