diff --git a/app/Http/Controllers/Tabulation/TabulationController.php b/app/Http/Controllers/Tabulation/TabulationController.php index 124432b..89ae6eb 100644 --- a/app/Http/Controllers/Tabulation/TabulationController.php +++ b/app/Http/Controllers/Tabulation/TabulationController.php @@ -6,42 +6,40 @@ use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Entry; use App\Models\ScoreSheet; +use App\Services\DoublerService; use App\Services\TabulationService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Session; use function compact; +use function dd; use function dump; use function redirect; class TabulationController extends Controller { protected $tabulationService; + protected $doublerService; - public function __construct(TabulationService $tabulationService) + public function __construct(TabulationService $tabulationService, DoublerService $doublerService) { $this->tabulationService = $tabulationService; + $this->doublerService = $doublerService; } + public function status() { -// $auditions = Audition::with(['entries' => function($query) { -// $query->withCount('scoreSheets'); -// },'room.judges'])->orderBy('score_order')->get(); $auditions = $this->tabulationService->getAuditionsWithStatus(); return view('tabulation.status',compact('auditions')); } public function auditionSeating(Audition $audition) { -// $entries = $audition->entries()->with(['student','scoreSheets.audition.scoringGuide','audition.room.judges'])->get(); -// $entries = $entries->sortByDesc(function ($entry) { -// return $entry->totalScore(); -// }); -// $entries = $audition->rankedEntries()->load('student.entries.audition','scoreSheets.audition.scoringGuide.subscores'); $entries = $this->tabulationService->auditionEntries($audition->id); - - - return view('tabulation.auditionSeating',compact('audition','entries')); + foreach ($entries as $entry) { + $entry->is_doubler = $this->doublerService->entryIsDoubler($entry); + } + return view('tabulation.auditionSeating',compact('audition','entries')); } } diff --git a/app/Models/Audition.php b/app/Models/Audition.php index bf330be..e4933e9 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -19,6 +19,7 @@ class Audition extends Model protected $guarded = []; protected $rankedEntries = null; protected static $completeAuditions = null; + protected $fully_scored; // Set by TabulationService public static function getCompleteAuditions() { @@ -137,6 +138,7 @@ class Audition extends Model $entry->save(); } return null; + // TODO move all draw functions to a DrawService } /** @@ -154,6 +156,17 @@ class Audition extends Model ); } + /* + * Ensures judges_count property is always available + */ + public function getJudgesCountAttribute() + { + if (!isset($this->attributes['judges_count'])) { + $this->attributes['judges_count'] = $this->judges()->count(); + } + return $this->attributes['judges_count']; + } + public function scoredEntries() { return $this->entries->filter(function($entry) { diff --git a/app/Models/Entry.php b/app/Models/Entry.php index de18d3d..fbaa02f 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -15,6 +15,9 @@ class Entry extends Model use HasFactory; protected $guarded = []; protected $hasCheckedScoreSheets = false; + public $final_scores_array; // Set by TabulationService + public $scoring_complete; // Set by TabulationService + public $is_doubler; // Set by DoublerService public function student(): BelongsTo { @@ -43,80 +46,14 @@ class Entry extends Model } + /* + * Ensures score_sheets_count property is always available + */ public function getScoreSheetsCountAttribute() { if (!isset($this->attributes['score_sheets_count'])) { $this->attributes['score_sheets_count'] = $this->scoreSheets()->count(); } - return $this->attributes['score_sheets_count']; } - - public function verifyScoreSheets() - { - if ($this->hasCheckedScoreSheets) return true; - $judges = $this->audition->room->judges; - foreach ($this->scoreSheets as $sheet) { - if (! $judges->contains($sheet->user_id)) { - $invalidJudge = User::find($sheet->user_id); -// redirect ('/tabulation')->with('warning','Invalid scores for entry ' . $this->id . ' exist from ' . $invalidJudge->full_name()); - // Abort execution, and redirect to /tabulation with a warning message - throw new TabulationException('Invalid scores for entry ' . $this->id . ' exist from ' . $invalidJudge->full_name()); - } - } - return true; - } - - - - public function fullyJudged(){ - return $this->scoreSheets->count() >= $this->audition->room->judges->count(); - } - public function scoreFromJudge($user): ScoreSheet|null - { -// return $this->scoreSheets()->where('user_id','=',$user)->first() ?? null; - return $this->scoreSheets->firstWhere('user_id', $user) ?? null; - - } - - public function totalScore() - { - $this->verifyScoreSheets(); - $totalScore = 0; - foreach ($this->scoreSheets as $sheet) - { - $totalScore += $sheet->totalScore(); - } - return $totalScore; - } - - /** - * @throws TabulationException - */ - public function finalScoresArray() - { - $this->verifyScoreSheets(); - $finalScoresArray = []; - $subscoresTiebreakOrder = $this->audition->scoringGuide->subscores->sortBy('tiebreak_order'); - // initialize the return array - foreach ($subscoresTiebreakOrder as $subscore) { - $finalScoresArray[$subscore->id] = 0; - } - // add the subscores from each score sheet - foreach($this->scoreSheets as $sheet) { - foreach($sheet->subscores as $ss) { - $finalScoresArray[$ss['subscore_id']] += $ss['score']; - } - } - // calculate weighted final score - $totalScore = 0; - $totalWeight = 0; - foreach ($subscoresTiebreakOrder as $subscore) { - $totalScore += ($finalScoresArray[$subscore->id] * $subscore->weight); - $totalWeight += $subscore->weight; - } - $totalScore = ($totalScore / $totalWeight); - array_unshift($finalScoresArray,$totalScore); - return $finalScoresArray; - } } diff --git a/app/Models/ScoreSheet.php b/app/Models/ScoreSheet.php index 8ede43b..5f36770 100644 --- a/app/Models/ScoreSheet.php +++ b/app/Models/ScoreSheet.php @@ -44,17 +44,8 @@ class ScoreSheet extends Model return $this->subscores[$id]['score'] ?? false; } - public function totalScore() { - $totalScore = 0; - $totalWeights = 0; - foreach ( $this->audition->scoringGuide->subscores as $subscore) { - $totalScore += $this->getSubscore($subscore->id) * $subscore->weight; - $totalWeights += $subscore->weight; - } - return $totalScore / $totalWeights; - } - public function isValid() { + // TODO move to either TabulationService or a specific service for scoreValidation $judges = $this->audition->judges(); return $judges->contains('id', $this->judge->id); } diff --git a/app/Models/ScoringGuide.php b/app/Models/ScoringGuide.php index 6455993..2508639 100644 --- a/app/Models/ScoringGuide.php +++ b/app/Models/ScoringGuide.php @@ -35,6 +35,7 @@ class ScoringGuide extends Model */ public function validateScores(Array $prospective_score) { + // TODO move to either TabulationService or a specific service for scoreValidation foreach ($this->subscores as $subscore) { if (! array_key_exists($subscore->id,$prospective_score)) return "A score must be provided for " . $subscore->name; if (is_null($prospective_score[$subscore->id])) return "A score must be provided for " . $subscore->name; diff --git a/app/Models/Student.php b/app/Models/Student.php index afa878f..9d6badc 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -33,10 +33,4 @@ class Student extends Model if ($last_name_first) return $this->last_name . ', ' . $this->first_name; return $this->first_name . ' ' . $this->last_name; } - - public function isDoubler() - { - return $this->entries->count() > 1; - } - } diff --git a/app/Models/User.php b/app/Models/User.php index fa2b74f..ccfa667 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -144,11 +144,13 @@ class User extends Authenticatable implements MustVerifyEmail public function scoresForEntry($entry) { + // TODO Again, why is this here? Needs to go somewhere else. Maybe a Judging service return $this->scoreSheets->where('entry_id','=',$entry)->first()?->subscores; } public function timeForEntryScores($entry) { + // TODO Why is this in the User mode? Move it somewhere else return $this->scoreSheets->where('entry_id','=',$entry)->first()?->created_at; } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index dfd4d07..a3ccf86 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,6 +5,7 @@ namespace App\Providers; use App\Events\AuditionChange; use App\Listeners\RefreshAuditionCache; use App\Services\AuditionCacheService; +use App\Services\DoublerService; use App\Services\TabulationService; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; @@ -16,13 +17,17 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - $this->app->singleton(AuditionCacheService::class, function ($app) { + $this->app->singleton(AuditionCacheService::class, function () { return new AuditionCacheService(); }); $this->app->singleton(TabulationService::class, function($app) { return new TabulationService($app->make(AuditionCacheService::class)); }); + + $this->app->singleton(DoublerService::class, function($app) { + return new DoublerService($app->make(AuditionCacheService::class),$app->make(TabulationService::class)); + }); } /** diff --git a/app/Services/AuditionCacheService.php b/app/Services/AuditionCacheService.php index b8e4bf0..c78058f 100644 --- a/app/Services/AuditionCacheService.php +++ b/app/Services/AuditionCacheService.php @@ -21,6 +21,8 @@ class AuditionCacheService { //TODO have changes to judging assignments refresh the cache return Cache::rememberForever($this->cacheKey, function () { return Audition::with(['scoringGuide.subscores','judges']) + ->withCount('judges') + ->withCount('entries') ->orderBy('score_order') ->get() ->keyBy('id'); diff --git a/app/Services/DoublerService.php b/app/Services/DoublerService.php index c55caaa..9d47876 100644 --- a/app/Services/DoublerService.php +++ b/app/Services/DoublerService.php @@ -2,13 +2,62 @@ namespace App\Services; +use App\Models\Entry; +use App\Models\Student; +use Illuminate\Support\Facades\Cache; + class DoublerService { + protected $doublersCacheKey = 'doublers'; + protected $auditionCacheService; + protected $tabulationService; /** * Create a new class instance. */ - public function __construct() + public function __construct(AuditionCacheService $auditionCacheService, TabulationService $tabulationService) { - // + $this->auditionCacheService = $auditionCacheService; + $this->tabulationService = $tabulationService; + } + + public function getDoublers(): \Illuminate\Database\Eloquent\Collection + { + return Cache::remember($this->doublersCacheKey, 10, function () { + $students = Student::withCount('entries') + ->with('entries') + ->havingRaw('entries_count > ?', [1]) + ->get(); + return $students; + }); + } + + public function getDoublerInfo($id): Array + { + // When getting a doubler we need to know + // 1) What their entrires are + // 2) For each audition they're entered in, what is their rank + // 3) For each audition they'er entered in, how many entries are unscored + // 4) How many are accepted on that instrument + $doubler = $this->getDoublers()->firstWhere('id',$id); + $info = []; + + foreach ($doubler->entries as $entry) { + $info[] = [ + 'auditionID' => $entry->audition_id, + 'auditionName' => $this->auditionCacheService->getAudition($entry->audition_id)->name, + 'rank' => $this->tabulationService->entryRank($entry), + 'unscored' => $this->tabulationService->remainingEntriesForAudition($entry->audition_id) + ]; + $entry->audition = $this->auditionCacheService->getAudition($entry->audition_id); + } + + return $info; + } + + + public function entryIsDoubler(Entry $entry): bool + { + // Return true if $entry->student_id is associated with a student in the collection from $this->getDoublers() + return $this->getDoublers()->contains('id', $entry->student_id); } } diff --git a/app/Services/TabulationService.php b/app/Services/TabulationService.php index 58d5f06..7bd24d4 100644 --- a/app/Services/TabulationService.php +++ b/app/Services/TabulationService.php @@ -10,7 +10,6 @@ use App\Models\User; use Illuminate\Support\Facades\Cache; use App\Services\AuditionCacheService; use Illuminate\Support\Facades\DB; -use function array_unshift; class TabulationService @@ -25,12 +24,8 @@ class TabulationService $this->auditionCacheService = $scoringGuideCacheService; } - public function getScoredEntries() - { - return Cache::remember($this->cacheKey, 10, function () { - $entries = Entry::with(['scoreSheets'])->get(); - return $entries->keyBy('id'); - }); + public function entryRank(Entry $entry) { + return $this->auditionEntries($entry->audition_id)[$entry->id]->rank; } public function auditionEntries(Int $auditionId) @@ -38,21 +33,28 @@ class TabulationService $cache_key = 'audition'.$auditionId.'entries'; $audition = $this->auditionCacheService->getAudition($auditionId); - return Cache::remember($cache_key, 2, function () use ($audition) { - $entries = Entry::where('audition_id',$audition->id)->with(['student.school','scoreSheets'])->get(); + return Cache::remember($cache_key, 5, function () use ($audition) { + $entries = Entry::where('audition_id',$audition->id)->with(['student.school','scoreSheets'])->withCount('scoreSheets')->get(); + foreach ($entries as $entry) { $entry->final_score_array = $this->entryFinalScores($entry); + $entry->scoring_complete = ($entry->score_sheets_count == $audition->judges_count) ? true:false; } + // Sort based on final_score_array for ($n=0; $n <= $audition->judges_count; $n++) { $entries = $entries->sortByDesc(function ($entry) use ($n) { return $entry['final_score_array'][$n]; }); } //TODO verify this actually sorts by subscores correcty - - return $entries; + $n = 1; + foreach ($entries as $entry) { + $entry->rank = $n; + $n++; + } + return $entries->keyBy('id'); }); } @@ -102,28 +104,44 @@ class TabulationService return true; } + public function remainingEntriesForAudition($auditionId) + { + $audition = $this->getAuditionsWithStatus()[$auditionId]; + return $audition->entries_count - $audition->scored_entries_count; + } + public function getAuditionsWithStatus() { - // Create an array with the number of scores for each entry - $scoreCountByEntry = ScoreSheet::select('entry_id', DB::raw('count(*) as count')) - ->groupBy('entry_id') - ->get() - ->pluck('count','entry_id'); + return Cache::remember('auditionsWithStatus',30,function() { + // Create an array with the number of scores for each entry + $scoreCountByEntry = ScoreSheet::select('entry_id', DB::raw('count(*) as count')) + ->groupBy('entry_id') + ->get() + ->pluck('count','entry_id'); - // Retrieve auditions from the cache and load entry IDs - $auditions = $this->auditionCacheService->getAuditions(); - $auditions->load('entries'); + // Retrieve auditions from the cache and load entry IDs + $auditions = $this->auditionCacheService->getAuditions(); + $auditions->load('entries'); - // Eager load the count of related models - $auditions->loadCount(['judges', 'entries']); + // Eager load the count of related models + $auditions->loadCount(['judges', 'entries']); - // Iterate over the auditions and calculate the scored_entries_count - return $auditions->map(function ($audition) use ($scoreCountByEntry) { - $audition->scored_entries_count = $audition->entries->reduce(function ($carry, $entry) use ($audition, $scoreCountByEntry) { - $entry->fully_scored = $audition->judges_count == $scoreCountByEntry[$entry->id]; - return $carry + ($entry->fully_scored ? 1 : 0); - }, 0); - return $audition; + // Iterate over the auditions and calculate the scored_entries_count + return $auditions->map(function ($audition) use ($scoreCountByEntry) { + $audition->scored_entries_count = $audition->entries->reduce(function ($carry, $entry) use ($audition, $scoreCountByEntry) { + $entry->fully_scored = $audition->judges_count == $scoreCountByEntry[$entry->id]; + return $carry + ($entry->fully_scored ? 1 : 0); + }, 0); + return $audition; + }); }); } + + public function allResults() { + $auditions = $this->getAuditionsWithStatus(); + foreach ($auditions as $audition) { + $audition->entries = $this->auditionEntries($audition->id); + } + return $auditions; + } } diff --git a/resources/views/components/doubler-block.blade.php b/resources/views/components/doubler-block.blade.php index 8424cae..e3e4c90 100644 --- a/resources/views/components/doubler-block.blade.php +++ b/resources/views/components/doubler-block.blade.php @@ -1,43 +1,46 @@ -@php use App\Models\Audition; @endphp -@props(['student']) -{{--complete badge--}} -{{--

Complete

--}} -{{--in progress badge--}} -{{--

In progress

--}} - +@inject('doublers','\App\Services\DoublerService') +@inject('tabulation','\App\Services\TabulationService') +@props(['studentID']) +@php + $doublerEntryInfo = $doublers->getDoublerInfo($studentID); +@endphp +{{--Complete Badge--}} +{{--

Complete

--}} + +{{--In Progres Badge--}} +{{--

In Progress

--}} diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index ef8d256..065b1f9 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -1,3 +1,4 @@ + Audition Seating - {{ $audition->name }} @@ -25,8 +26,13 @@ {{ $entry->student->full_name() }}, {{ $entry->student->school->name }} - Doubler Block - {{ $entry->final_score_array[0] }} + + @if($entry->is_doubler) + + @endif + + {{ number_format($entry->final_score_array[0],4) }} + @if($entry->scoring_complete) @endif @endforeach diff --git a/resources/views/test.blade.php b/resources/views/test.blade.php index 1b23bf3..8e6393f 100644 --- a/resources/views/test.blade.php +++ b/resources/views/test.blade.php @@ -12,14 +12,9 @@ Test Page - @php - dump($auditions); - - @endphp +@php(dd($auditions)) @foreach($auditions as $audition) - @php - @endphp {{ $audition->name }} has {{ $audition->entries_count }} entries. {{ $audition->scored_entries_count }} are fully scored.
@endforeach