diff --git a/.gitignore b/.gitignore index 46340a6..5a8b744 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ yarn-error.log /.fleet /.idea /.vscode +/app/Http/Controllers/TestController.php +/resources/views/test.blade.php diff --git a/app/Actions/Tabulation/AllJudgesCount.php b/app/Actions/Tabulation/AllJudgesCount.php new file mode 100644 index 0000000..aa76b98 --- /dev/null +++ b/app/Actions/Tabulation/AllJudgesCount.php @@ -0,0 +1,87 @@ +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); + } + $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'); + } + } +} diff --git a/app/Actions/Tabulation/CalculateEntryScore.php b/app/Actions/Tabulation/CalculateEntryScore.php new file mode 100644 index 0000000..8cb48a8 --- /dev/null +++ b/app/Actions/Tabulation/CalculateEntryScore.php @@ -0,0 +1,11 @@ +auditionService = $auditionService; + $this->entryService = $entryService; + $this->userService = $userService; + } + + 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 = $this->auditionService->getSubscores($entry->audition, $mode); + $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 (! $this->entryService->entryExists($entry)) { + throw new TabulationException('Invalid entry provided'); + } + if (! $this->userService->userExists($judge)) { + throw new TabulationException('Invalid judge provided'); + } + } +} diff --git a/app/Actions/Tabulation/EnterScore.php b/app/Actions/Tabulation/EnterScore.php new file mode 100644 index 0000000..b8c01ad --- /dev/null +++ b/app/Actions/Tabulation/EnterScore.php @@ -0,0 +1,101 @@ +basicChecks($user, $entry, $scores); + $this->checkJudgeAssignment($user, $entry); + $this->checkForExistingScore($user, $entry); + $this->validateScoresSubmitted($entry, $scores); + $entry->removeFlag('no_show'); + $newScoreSheet = ScoreSheet::create([ + 'user_id' => $user->id, + 'entry_id' => $entry->id, + 'subscores' => $this->subscoresForStorage($entry, $scores), + ]); + + return $newScoreSheet; + } + + protected function subscoresForStorage(Entry $entry, Collection $scores) + { + $subscores = []; + foreach ($entry->audition->scoringGuide->subscores as $subscore) { + $subscores[$subscore->id] = [ + 'score' => $scores[$subscore->id], + 'subscore_id' => $subscore->id, + 'subscore_name' => $subscore->name, + ]; + } + + return $subscores; + } + + protected function checkForExistingScore(User $user, Entry $entry) + { + if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { + throw new ScoreEntryException('That judge has already entered scores for that entry'); + } + } + + protected function validateScoresSubmitted(Entry $entry, Collection $scores) + { + $subscoresRequired = $entry->audition->scoringGuide->subscores; + + foreach ($subscoresRequired as $subscore) { + // check that there is an element in the $scores collection with the key = $subscore->id + if (! $scores->keys()->contains($subscore->id)) { + throw new ScoreEntryException('Invalid Score Submission'); + } + if ($scores[$subscore->id] > $subscore->max_score) { + throw new ScoreEntryException('Supplied subscore exceeds maximum allowed'); + } + } + } + + protected function checkJudgeAssignment(User $user, Entry $entry) + { + $check = DB::table('room_user') + ->where('room_id', $entry->audition->room_id) + ->where('user_id', $user->id)->exists(); + if (! $check) { + throw new ScoreEntryException('This judge is not assigned to judge this entry'); + } + } + + protected function basicChecks(User $user, Entry $entry, Collection $scores) + { + if (! $user->exists()) { + throw new ScoreEntryException('User does not exist'); + } + if (! $entry->exists()) { + throw new ScoreEntryException('Entry does not exist'); + } + if ($entry->audition->hasFlag('seats_published')) { + throw new ScoreEntryException('Cannot score an entry in an audition with published seats'); + } + if ($entry->audition->hasFlag('advancement_published')) { + throw new ScoreEntryException('Cannot score an entry in an audition with published advancement'); + } + $requiredScores = $entry->audition->scoringGuide->subscores()->count(); + if ($scores->count() !== $requiredScores) { + throw new ScoreEntryException('Invalid number of scores'); + } + } +} diff --git a/app/Actions/Tabulation/GetAuditionSeats.php b/app/Actions/Tabulation/GetAuditionSeats.php new file mode 100644 index 0000000..0dc1101 --- /dev/null +++ b/app/Actions/Tabulation/GetAuditionSeats.php @@ -0,0 +1,40 @@ +getSeats($audition); + } + + protected function getSeats(Audition $audition) + { + $ensembles = Ensemble::where('event_id', $audition->event_id)->orderBy('rank')->get(); + $seats = Seat::with('student.school')->where('audition_id', $audition->id)->orderBy('seat')->get(); + $return = []; + foreach ($ensembles as $ensemble) { + $ensembleSeats = $seats->filter(fn ($seat) => $seat->ensemble_id === $ensemble->id); + foreach ($ensembleSeats as $seat) { + $return[] = [ + 'ensemble' => $ensemble->name, + 'seat' => $seat->seat, + 'student_name' => $seat->student->full_name(), + 'school_name' => $seat->student->school->name, + ]; + } + } + + return $return; + } +} diff --git a/app/Actions/Tabulation/PublishSeats.php b/app/Actions/Tabulation/PublishSeats.php new file mode 100644 index 0000000..7c37b23 --- /dev/null +++ b/app/Actions/Tabulation/PublishSeats.php @@ -0,0 +1,31 @@ +id + Seat::where('audition_id', $audition->id)->delete(); + foreach ($seats as $seat) { + Seat::create([ + 'ensemble_id' => $seat['ensemble_id'], + 'audition_id' => $seat['audition_id'], + 'seat' => $seat['seat'], + 'entry_id' => $seat['entry_id'], + ]); + } + $audition->addFlag('seats_published'); + Cache::forget('resultsSeatList'); + } +} diff --git a/app/Actions/Tabulation/RankAuditionEntries.php b/app/Actions/Tabulation/RankAuditionEntries.php new file mode 100644 index 0000000..aef64e3 --- /dev/null +++ b/app/Actions/Tabulation/RankAuditionEntries.php @@ -0,0 +1,93 @@ +calculator = $calculator; + } + + public function rank(string $mode, Audition $audition): Collection + { + $cacheKey = 'audition'.$audition->id.$mode; + + return Cache::remember($cacheKey, 30, function () use ($mode, $audition) { + return $this->calculateRank($mode, $audition); + }); + + } + + public function calculateRank(string $mode, Audition $audition): Collection + { + $this->basicValidation($mode, $audition); + $entries = match ($mode) { + 'seating' => $audition->entries()->forSeating()->with('scoreSheets')->get(), + 'advancement' => $audition->entries()->forAdvancement()->with('scoreSheets')->get(), + }; + + foreach ($entries as $entry) { + $entry->setRelation('audition', $audition); + 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; + // We don't really get a rank for seating if we have certain flags + if ($mode === 'seating') { + if ($entry->hasFlag('declined')) { + $entry->rank = 'Declined'; + } elseif ($entry->hasFlag('no_show')) { + $entry->rank = 'No Show'; + } elseif ($entry->hasFlag('failed_prelim')) { + $entry->rank = 'Failed Prelim'; + } + } + + if (is_numeric($entry->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/Actions/Tabulation/UnpublishSeats.php b/app/Actions/Tabulation/UnpublishSeats.php new file mode 100644 index 0000000..e00cbbc --- /dev/null +++ b/app/Actions/Tabulation/UnpublishSeats.php @@ -0,0 +1,21 @@ +removeFlag('seats_published'); + Cache::forget('resultsSeatList'); + Seat::where('audition_id', $audition->id)->delete(); + } +} diff --git a/app/Events/AuditionChange.php b/app/Events/AuditionChange.php deleted file mode 100644 index b8aa81a..0000000 --- a/app/Events/AuditionChange.php +++ /dev/null @@ -1,36 +0,0 @@ -refreshCache = $refreshCache; - } - - /** - * Get the channels the event should broadcast on. - * - * @return array - */ - public function broadcastOn(): array - { - return [ - new PrivateChannel('channel-name'), - ]; - } -} diff --git a/app/Events/EntryChange.php b/app/Events/EntryChange.php deleted file mode 100644 index ae1890c..0000000 --- a/app/Events/EntryChange.php +++ /dev/null @@ -1,36 +0,0 @@ -auditionId = $auditionId; - } - - /** - * Get the channels the event should broadcast on. - * - * @return array - */ - public function broadcastOn(): array - { - return [ - new PrivateChannel('channel-name'), - ]; - } -} diff --git a/app/Events/ScoreSheetChange.php b/app/Events/ScoreSheetChange.php deleted file mode 100644 index 7178c49..0000000 --- a/app/Events/ScoreSheetChange.php +++ /dev/null @@ -1,36 +0,0 @@ -entryId = $entryId; - } - - /** - * Get the channels the event should broadcast on. - * - * @return array - */ - public function broadcastOn(): array - { - return [ - new PrivateChannel('channel-name'), - ]; - } -} diff --git a/app/Events/ScoringGuideChange.php b/app/Events/ScoringGuideChange.php deleted file mode 100644 index b265ad9..0000000 --- a/app/Events/ScoringGuideChange.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ - public function broadcastOn(): array - { - return [ - new PrivateChannel('channel-name'), - ]; - } -} diff --git a/app/Events/SeatingLimitChange.php b/app/Events/SeatingLimitChange.php deleted file mode 100644 index 5217dfe..0000000 --- a/app/Events/SeatingLimitChange.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ - public function broadcastOn(): array - { - return [ - new PrivateChannel('channel-name'), - ]; - } -} diff --git a/app/Exceptions/AuditionServiceException.php b/app/Exceptions/AuditionServiceException.php new file mode 100644 index 0000000..ef02c52 --- /dev/null +++ b/app/Exceptions/AuditionServiceException.php @@ -0,0 +1,10 @@ +get(); + $events = Event::with('auditions.flags')->get(); // $drawnAuditionsExist is true if any audition->hasFlag('drawn') is true $drawnAuditionsExist = Audition::whereHas('flags', function ($query) { $query->where('flag_name', 'drawn'); diff --git a/app/Http/Controllers/Admin/RoomController.php b/app/Http/Controllers/Admin/RoomController.php index 4f1cee1..0b64b1f 100644 --- a/app/Http/Controllers/Admin/RoomController.php +++ b/app/Http/Controllers/Admin/RoomController.php @@ -17,7 +17,7 @@ class RoomController extends Controller if (! Auth::user()->is_admin) { abort(403); } - $rooms = Room::with('auditions.entries')->orderBy('name')->get(); + $rooms = Room::with('auditions.entries','entries')->orderBy('name')->get(); return view('admin.rooms.index', ['rooms' => $rooms]); } diff --git a/app/Http/Controllers/Admin/SchoolController.php b/app/Http/Controllers/Admin/SchoolController.php index da73483..3d335a6 100644 --- a/app/Http/Controllers/Admin/SchoolController.php +++ b/app/Http/Controllers/Admin/SchoolController.php @@ -23,11 +23,9 @@ class SchoolController extends Controller public function index() { - if (! Auth::user()->is_admin) { - abort(403); - } $schools = School::with(['users', 'students', 'entries'])->orderBy('name')->get(); $schoolTotalFees = []; + foreach ($schools as $school) { $schoolTotalFees[$school->id] = $this->invoiceService->getGrandTotal($school->id); } diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index 9396f2e..c2ac9c7 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -21,6 +21,7 @@ class EntryController extends Controller }); $auditions = Audition::open()->get(); $students = Auth::user()->students; + $students->load('school'); return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]); } diff --git a/app/Http/Controllers/JudgingController.php b/app/Http/Controllers/JudgingController.php index 56c09a0..0f0a6ee 100644 --- a/app/Http/Controllers/JudgingController.php +++ b/app/Http/Controllers/JudgingController.php @@ -5,11 +5,11 @@ namespace App\Http\Controllers; use App\Models\Audition; use App\Models\Entry; use App\Models\JudgeAdvancementVote; +use App\Models\ScoreSheet; use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Gate; -use App\Models\ScoreSheet; use function compact; use function redirect; @@ -20,6 +20,7 @@ class JudgingController extends Controller public function index() { $rooms = Auth::user()->judgingAssignments; + $rooms->load('auditions'); return view('judging.index', compact('rooms')); } @@ -150,6 +151,7 @@ class JudgingController extends Controller return redirect(url()->previous())->with('error', 'Error saving advancement vote'); } } + return null; } } diff --git a/app/Http/Controllers/Tabulation/AdvancementController.php b/app/Http/Controllers/Tabulation/AdvancementController.php index 93eac92..b5499fc 100644 --- a/app/Http/Controllers/Tabulation/AdvancementController.php +++ b/app/Http/Controllers/Tabulation/AdvancementController.php @@ -2,36 +2,63 @@ namespace App\Http\Controllers\Tabulation; +use App\Actions\Tabulation\RankAuditionEntries; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Entry; -use App\Services\TabulationService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Cache; class AdvancementController extends Controller { - protected TabulationService $tabulationService; - - public function __construct(TabulationService $tabulationService) + protected RankAuditionEntries $ranker; + public function __construct(RankAuditionEntries $ranker) { - $this->tabulationService = $tabulationService; + $this->ranker = $ranker; } public function status() { - $auditions = $this->tabulationService->getAuditionsWithStatus('advancement'); + $auditions = Audition::forAdvancement() + ->with('flags') + ->withCount([ + 'entries' => function ($query) { + $query->where('for_advancement', 1); + }, + ]) + ->withCount([ + 'unscoredEntries' => function ($query) { + $query->where('for_advancement', 1); + }, + ]) + ->get(); + $auditionData = []; + $auditions->each(function ($audition) use (&$auditionData) { + $scoredPercent = ($audition->entries_count > 0) ? + round((($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count) * 100) + : 100; + $auditionData[] = [ + 'id' => $audition->id, + 'name' => $audition->name, + 'entries_count' => $audition->entries_count, + 'unscored_entries_count' => $audition->unscored_entries_count, + 'scored_entries_count' => $audition->entries_count - $audition->unscored_entries_count, + 'scored_percentage' => $scoredPercent, + 'scoring_complete' => $audition->unscored_entries_count == 0, + 'published' => $audition->hasFlag('advancement_published'), + ]; + }); - return view('tabulation.advancement.status', compact('auditions')); + return view('tabulation.advancement.status', compact('auditionData')); } public function ranking(Request $request, Audition $audition) { - $entries = $this->tabulationService->auditionEntries($audition->id, 'advancement'); + $entries = $this->ranker->rank('advancement', $audition); $entries->load('advancementVotes'); - $scoringComplete = $entries->every(function ($entry) { - return $entry->scoring_complete; + return $entry->score_totals[0] >= 0; }); return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete')); @@ -46,8 +73,9 @@ class AdvancementController extends Controller foreach ($entries as $entry) { $entry->addFlag('will_advance'); } - - return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success', 'Passers have been set successfully'); + Cache::forget('audition'.$audition->id.'advancement'); + return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success', + 'Passers have been set successfully'); } public function clearAuditionPassers(Request $request, Audition $audition) @@ -56,7 +84,8 @@ class AdvancementController extends Controller foreach ($audition->entries as $entry) { $entry->removeFlag('will_advance'); } - - return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success', 'Passers have been cleared successfully'); + Cache::forget('audition'.$audition->id.'advancement'); + return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success', + 'Passers have been cleared successfully'); } } diff --git a/app/Http/Controllers/Tabulation/DoublerDecisionController.php b/app/Http/Controllers/Tabulation/DoublerDecisionController.php index 7d95056..4bc67ad 100644 --- a/app/Http/Controllers/Tabulation/DoublerDecisionController.php +++ b/app/Http/Controllers/Tabulation/DoublerDecisionController.php @@ -4,13 +4,14 @@ namespace App\Http\Controllers\Tabulation; use App\Http\Controllers\Controller; use App\Models\Entry; -use App\Models\EntryFlag; use App\Services\DoublerService; use App\Services\EntryService; +use Illuminate\Support\Facades\Cache; class DoublerDecisionController extends Controller { protected $doublerService; + protected $entryService; public function __construct(DoublerService $doublerService, EntryService $entryService) @@ -21,24 +22,16 @@ class DoublerDecisionController extends Controller public function accept(Entry $entry) { - $doublerInfo = $this->doublerService->getDoublerInfo($entry->student_id); - foreach ($doublerInfo as $info) { - $this->entryService->clearEntryCacheForAudition($info['auditionID']); - if ($info['entryID'] != $entry->id) { - try { - EntryFlag::create([ - 'entry_id' => $info['entryID'], - 'flag_name' => 'declined', - ]); - } catch (\Exception $e) { - session()->flash('error', 'Entry ID'.$info['entryID'].' has already been declined.'); - } - + $doublerInfo = $this->doublerService->simpleDoubleInfo($entry); + foreach ($doublerInfo as $doublerEntry) { + /** @var Entry $doublerEntry */ + if ($doublerEntry->id !== $entry->id) { + $doublerEntry->addFlag('declined'); } } - $this->doublerService->refreshDoublerCache(); $returnMessage = $entry->student->full_name().' accepted seating in '.$entry->audition->name; + $this->clearCache($entry); return redirect()->back()->with('success', $returnMessage); @@ -52,10 +45,17 @@ class DoublerDecisionController extends Controller $entry->addFlag('declined'); - $this->doublerService->refreshDoublerCache(); - $returnMessage = $entry->student->full_name().' declined seating in '.$entry->audition->name; + $this->clearCache($entry); return redirect()->back()->with('success', $returnMessage); } + + protected function clearCache($entry) + { + $cacheKey = 'event'.$entry->audition->event_id.'doublers-seating'; + Cache::forget($cacheKey); + $cacheKey = 'event'.$entry->audition->event_id.'doublers-advancement'; + Cache::forget($cacheKey); + } } diff --git a/app/Http/Controllers/Tabulation/SeatAuditionFormController.php b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php new file mode 100644 index 0000000..f06c392 --- /dev/null +++ b/app/Http/Controllers/Tabulation/SeatAuditionFormController.php @@ -0,0 +1,123 @@ +calc = $calc; + $this->ranker = $ranker; + $this->doublerService = $doublerService; + $this->entryService = $entryService; + $this->auditionService = $auditionService; + } + + public function __invoke(Request $request, Audition $audition) + { + // If a seating proposal was posted, deal wth it + if ($request->method() == 'POST') { + $requestedEnsembleAccepts = $request->input('ensembleAccept'); + } else { + $requestedEnsembleAccepts = false; + } + + $entryData = []; + $entries = $this->ranker->rank('seating', $audition); + $entries->load('student.school'); + $seatable = [ + 'allScored' => true, + 'doublersResolved' => true, + ]; + foreach ($entries as $entry) { + $totalScoreColumn = 'No Score'; + $fullyScored = false; + if ($entry->score_totals) { + $totalScoreColumn = $entry->score_totals[0] >= 0 ? $entry->score_totals[0] : $entry->score_message; + $fullyScored = $entry->score_totals[0] >= 0; + } + $doublerData = $this->doublerService->entryDoublerData($entry); + $entryData[] = [ + 'rank' => $entry->rank, + 'id' => $entry->id, + 'studentName' => $entry->student->full_name(), + 'schoolName' => $entry->student->school->name, + 'drawNumber' => $entry->draw_number, + 'totalScore' => $totalScoreColumn, + 'fullyScored' => $fullyScored, + 'doubleData' => $doublerData, + ]; + // If this entries double decision isn't made, block seating + if ($doublerData && $doublerData[$entry->id]['status'] == 'undecided') { + $seatable['doublersResolved'] = false; + } + // If entry is unscored, block seating + if (! $fullyScored) { + $seatable['allScored'] = false; + } + } + + $rightPanel = $this->pickRightPanel($audition, $seatable); + $seatableEntries = []; + if ($seatable['doublersResolved'] && $seatable['allScored']) { + $seatableEntries = $entries->reject(function ($entry) { + if ($entry->hasFlag('declined')) { + return true; + } + if ($entry->hasFlag('no_show')) { + return true; + } + + return false; + }); + } + + return view('tabulation.auditionSeating', compact('entryData', 'audition', 'rightPanel', 'seatableEntries', 'requestedEnsembleAccepts')); + } + + protected function pickRightPanel(Audition $audition, array $seatable) + { + if ($audition->hasFlag('seats_published')) { + $resultsWindow = new GetAuditionSeats; + $rightPanel['view'] = 'tabulation.auditionSeating-show-published-seats'; + $rightPanel['data'] = $resultsWindow($audition); + return $rightPanel; + } + if ($seatable['allScored'] == false || $seatable['doublersResolved'] == false) { + $rightPanel['view'] = 'tabulation.auditionSeating-unable-to-seat-card'; + $rightPanel['data'] = $seatable; + + return $rightPanel; + } + + $rightPanel['view'] = 'tabulation.auditionSeating-right-complete-not-published'; + $rightPanel['data'] = $this->auditionService->getSeatingLimits($audition); + + return $rightPanel; + } +} diff --git a/app/Http/Controllers/Tabulation/SeatingPublicationController.php b/app/Http/Controllers/Tabulation/SeatingPublicationController.php new file mode 100644 index 0000000..2adfda6 --- /dev/null +++ b/app/Http/Controllers/Tabulation/SeatingPublicationController.php @@ -0,0 +1,33 @@ +id.'seatingProposal'; + $seats = $request->session()->get($sessionKey); + + $publisher($audition, $seats); + + $request->session()->forget($sessionKey); + + return redirect()->route('seating.audition', ['audition' => $audition->id]); + } + + public function unpublishSeats(Request $request, Audition $audition) + { + $publisher = new UnpublishSeats; + $publisher($audition); + + return redirect()->route('seating.audition', ['audition' => $audition->id]); + } +} diff --git a/app/Http/Controllers/Tabulation/SeatingStatusController.php b/app/Http/Controllers/Tabulation/SeatingStatusController.php new file mode 100644 index 0000000..dd8f6f8 --- /dev/null +++ b/app/Http/Controllers/Tabulation/SeatingStatusController.php @@ -0,0 +1,42 @@ +withCount(['entries'=> function ($query) { + $query->where('for_seating', 1); + }]) + ->withCount(['unscoredEntries'=>function ($query) { + $query->where('for_seating', 1); + }]) + ->with('flags') + ->get(); + $auditionData = []; + foreach ($auditions as $audition) { + $auditionData[$audition->id] = [ + 'id' => $audition->id, + 'name' => $audition->name, + 'scoredEntriesCount' => $audition->entries_count - $audition->unscored_entries_count, + 'totalEntriesCount' => $audition->entries_count, + 'scoredPercentage' => $audition->entries_count > 0 ? ($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count * 100 : 100, + 'scoringComplete' => $audition->unscored_entries_count === 0, + 'seatsPublished' => $audition->hasFlag('seats_published'), + 'audition' => $audition, + ]; + } + $auditionData = collect($auditionData); + + return view('tabulation.status', compact('auditionData')); + } +} diff --git a/app/Http/Controllers/Tabulation/TabulationController.php b/app/Http/Controllers/Tabulation/TabulationController.php deleted file mode 100644 index 78b5e7f..0000000 --- a/app/Http/Controllers/Tabulation/TabulationController.php +++ /dev/null @@ -1,124 +0,0 @@ -tabulationService = $tabulationService; - $this->doublerService = $doublerService; - $this->seatingService = $seatingService; - $this->auditionService = $auditionService; - } - - public function status() - { - $auditions = $this->tabulationService->getAuditionsWithStatus('seating'); - - return view('tabulation.status', compact('auditions')); - } - - public function auditionSeating(Request $request, Audition $audition) - { - if ($request->method() == 'POST') { - $requestedEnsembleAccepts = $request->input('ensembleAccept'); - } else { - $requestedEnsembleAccepts = false; - } - - $entries = $this->tabulationService->auditionEntries($audition->id); - $entries = $entries->filter(function ($entry) { - return $entry->for_seating; - }); - - $doublerComplete = true; - foreach ($entries as $entry) { - - if ($this->doublerService->studentIsDoubler($entry->student_id)) { // If this entry is a doubler - if ($this->doublerService->getDoublerInfo($entry->student_id)[$entry->id]['status'] === 'undecided') { // If there is no decision for this entry - $doublerComplete = false; - } - } - } - - $scoringComplete = $entries->every(function ($entry) { - return $entry->scoring_complete; - }); - $ensembleLimits = $this->seatingService->getLimitForAudition($audition->id); - $auditionComplete = $scoringComplete && $doublerComplete; - - $seatableEntries = $this->seatingService->getSeatableEntries($audition->id); - $seatableEntries = $seatableEntries->filter(function ($entry) { - return $entry->for_seating; - }); - - return view('tabulation.auditionSeating', compact('audition', - 'entries', - 'scoringComplete', - 'doublerComplete', - 'auditionComplete', - 'ensembleLimits', - 'seatableEntries', - 'requestedEnsembleAccepts')); - } - - public function publishSeats(Request $request, Audition $audition) - { - // TODO move this to SeatingService - $sessionKey = 'audition'.$audition->id.'seatingProposal'; - $seats = $request->session()->get($sessionKey); - foreach ($seats as $seat) { - Seat::create([ - 'ensemble_id' => $seat['ensemble_id'], - 'audition_id' => $seat['audition_id'], - 'seat' => $seat['seat'], - 'entry_id' => $seat['entry_id'], - ]); - } - $audition->addFlag('seats_published'); - $request->session()->forget($sessionKey); - Cache::forget('resultsSeatList'); - Cache::forget('publishedAuditions'); - Cache::forget('audition'.$audition->id.'seats'); - - // TODO move the previous Cache functions here and in unplublish to the services, need to add an event for publishing an audition as well - return redirect()->route('tabulation.audition.seat', ['audition' => $audition->id]); - } - - public function unpublishSeats(Request $request, Audition $audition) - { - // TODO move this to SeatingService - $audition->removeFlag('seats_published'); - Cache::forget('resultsSeatList'); - Cache::forget('publishedAuditions'); - Cache::forget('audition'.$audition->id.'seats'); - $this->seatingService->forgetSeatsForAudition($audition->id); - Seat::where('audition_id', $audition->id)->delete(); - - return redirect()->route('tabulation.audition.seat', ['audition' => $audition->id]); - } -} diff --git a/app/Http/Controllers/TestController.php b/app/Http/Controllers/TestController.php deleted file mode 100644 index 99fc429..0000000 --- a/app/Http/Controllers/TestController.php +++ /dev/null @@ -1,33 +0,0 @@ -scoringGuideCacheService = $scoringGuideCacheService; - $this->tabulationService = $tabulationService; - $this->invoiceService = $invoiceService; - } - - public function flashTest(Request $request) - { - $lines = $this->invoiceService->getLines(12); - $totalFees = $this->invoiceService->getGrandTotal(12); - return view('test', compact('lines','totalFees')); - } -} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 9d60447..d109f38 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -83,4 +83,3 @@ class UserController extends Controller } -//TODO allow users to modify their profile information. RoomJudgeChange::dispatch(); when they do diff --git a/app/Listeners/RefreshAuditionCache.php b/app/Listeners/RefreshAuditionCache.php deleted file mode 100644 index 0728bad..0000000 --- a/app/Listeners/RefreshAuditionCache.php +++ /dev/null @@ -1,31 +0,0 @@ -auditionService = $cacheService; - } - - /** - * Handle the event. - */ - public function handle(AuditionChange $event): void - { - if ($event->refreshCache) { - $this->auditionService->refreshCache(); - } else { - $this->auditionService->clearCache(); - } - } -} diff --git a/app/Listeners/RefreshEntryCache.php b/app/Listeners/RefreshEntryCache.php deleted file mode 100644 index cac64e6..0000000 --- a/app/Listeners/RefreshEntryCache.php +++ /dev/null @@ -1,34 +0,0 @@ -entryService = $cacheService; - } - - /** - * Handle the event. - */ - public function handle(EntryChange $event): void - { - if ($event->auditionId) { - $this->entryService->clearEntryCacheForAudition($event->auditionId); - } else { - $this->entryService->clearEntryCaches(); - } - } -} diff --git a/app/Listeners/RefreshScoreSheetCache.php b/app/Listeners/RefreshScoreSheetCache.php deleted file mode 100644 index 1604550..0000000 --- a/app/Listeners/RefreshScoreSheetCache.php +++ /dev/null @@ -1,35 +0,0 @@ -scoreService = $scoreService; - } - - /** - * Handle the event. - */ - public function handle(ScoreSheetChange $event): void - { - $this->scoreService->clearScoreSheetCountCache(); - if ($event->entryId) { - $this->scoreService->clearEntryTotalScoresCache($event->entryId); - } - // If we are in local environment, send a success flash message - if (config('app.env') === 'local') { - session()->flash('success','Cleared cache for entry ID ' . $event->entryId); - } - } -} diff --git a/app/Listeners/RefreshScoringGuideCache.php b/app/Listeners/RefreshScoringGuideCache.php deleted file mode 100644 index 3a38cc4..0000000 --- a/app/Listeners/RefreshScoringGuideCache.php +++ /dev/null @@ -1,29 +0,0 @@ -scoreService = $scoreService; - } - - /** - * Handle the event. - */ - public function handle(ScoringGuideChange $event): void - { - $this->scoreService->clearScoringGuideCache(); - $this->scoreService->clearAllCachedTotalScores(); - } -} diff --git a/app/Listeners/RefreshSeatingLimitCache.php b/app/Listeners/RefreshSeatingLimitCache.php deleted file mode 100644 index d309d15..0000000 --- a/app/Listeners/RefreshSeatingLimitCache.php +++ /dev/null @@ -1,28 +0,0 @@ -seatingService = $seatingService; - } - - /** - * Handle the event. - */ - public function handle(SeatingLimitChange $event): void - { - $this->seatingService->refreshLimits(); - } -} diff --git a/app/Models/Audition.php b/app/Models/Audition.php index 21d6c78..aba117d 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -17,6 +17,9 @@ class Audition extends Model { use HasFactory; + /** + * @var int|mixed + */ protected $guarded = []; public function event(): BelongsTo @@ -29,6 +32,12 @@ class Audition extends Model return $this->hasMany(Entry::class); } + public function unscoredEntries(): HasMany + { + return $this->hasMany(Entry::class) + ->whereDoesntHave('scoreSheets'); + } + public function room(): BelongsTo { return $this->belongsTo(Room::class); @@ -114,12 +123,12 @@ class Audition extends Model public function scopeForSeating(Builder $query): void { - $query->where('for_seating', 1); + $query->where('for_seating', 1)->orderBy('score_order'); } public function scopeForAdvancement(Builder $query): void { - $query->where('for_advancement', 1); + $query->where('for_advancement', 1)->orderBy('score_order'); } public function scopeSeatsPublished(Builder $query): Builder diff --git a/app/Models/Entry.php b/app/Models/Entry.php index 3d59f15..bd3ad71 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -10,7 +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 App\Models\ScoreSheet; +use Illuminate\Support\Facades\Cache; class Entry extends Model { diff --git a/app/Models/Event.php b/app/Models/Event.php index b8b9726..07fe9df 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -5,6 +5,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasManyThrough; class Event extends Model { @@ -21,4 +22,9 @@ class Event extends Model return $this->hasMany(Ensemble::class) ->orderBy('rank'); } + + public function entries(): HasManyThrough + { + return $this->hasManyThrough(Entry::class, Audition::class); + } } diff --git a/app/Models/Room.php b/app/Models/Room.php index d92b944..0a8d2e9 100644 --- a/app/Models/Room.php +++ b/app/Models/Room.php @@ -46,13 +46,11 @@ class Room extends Model { $this->judges()->attach($userId); $this->load('judges'); - AuditionChange::dispatch(); } public function removeJudge($userId): void { $this->judges()->detach($userId); $this->load('judges'); - AuditionChange::dispatch(); } } diff --git a/app/Models/User.php b/app/Models/User.php index 350792f..eeb74cb 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -125,7 +125,7 @@ class User extends Authenticatable implements MustVerifyEmail public function isJudge(): bool { - return $this->judgingAssignments->count() > 0; + return $this->judgingAssignments()->count() > 0; } /** diff --git a/app/Observers/AuditionObserver.php b/app/Observers/AuditionObserver.php index 27d139f..aa2bc4c 100644 --- a/app/Observers/AuditionObserver.php +++ b/app/Observers/AuditionObserver.php @@ -13,8 +13,7 @@ class AuditionObserver */ public function created(Audition $audition): void { - AuditionChange::dispatch(); - EntryChange::dispatch($audition->id); + // } /** @@ -22,8 +21,7 @@ class AuditionObserver */ public function updated(Audition $audition): void { - AuditionChange::dispatch(); - EntryChange::dispatch($audition->id); + // } /** @@ -31,8 +29,7 @@ class AuditionObserver */ public function deleted(Audition $audition): void { - AuditionChange::dispatch(); - EntryChange::dispatch($audition->id); + // } /** @@ -40,8 +37,7 @@ class AuditionObserver */ public function restored(Audition $audition): void { - AuditionChange::dispatch(); - EntryChange::dispatch($audition->id); + // } /** @@ -49,7 +45,6 @@ class AuditionObserver */ public function forceDeleted(Audition $audition): void { - AuditionChange::dispatch(); - EntryChange::dispatch($audition->id); + // } } diff --git a/app/Observers/RoomObserver.php b/app/Observers/RoomObserver.php index 984b373..ce2c790 100644 --- a/app/Observers/RoomObserver.php +++ b/app/Observers/RoomObserver.php @@ -20,7 +20,7 @@ class RoomObserver */ public function updated(Room $room): void { - AuditionChange::dispatch(); + // } /** @@ -28,7 +28,7 @@ class RoomObserver */ public function deleted(Room $room): void { - AuditionChange::dispatch(); + // } /** @@ -36,7 +36,7 @@ class RoomObserver */ public function restored(Room $room): void { - AuditionChange::dispatch(); + // } /** @@ -44,6 +44,6 @@ class RoomObserver */ public function forceDeleted(Room $room): void { - AuditionChange::dispatch(); + // } } diff --git a/app/Observers/RoomUserObserver.php b/app/Observers/RoomUserObserver.php index f089319..024f3ba 100644 --- a/app/Observers/RoomUserObserver.php +++ b/app/Observers/RoomUserObserver.php @@ -12,7 +12,7 @@ class RoomUserObserver */ public function created(RoomUser $roomUser): void { - AuditionChange::dispatch(); + // } /** @@ -20,7 +20,7 @@ class RoomUserObserver */ public function updated(RoomUser $roomUser): void { - AuditionChange::dispatch(); + // } /** @@ -28,7 +28,7 @@ class RoomUserObserver */ public function deleted(RoomUser $roomUser): void { - AuditionChange::dispatch(); + // } /** @@ -36,7 +36,7 @@ class RoomUserObserver */ public function restored(RoomUser $roomUser): void { - AuditionChange::dispatch(); + // } /** @@ -44,6 +44,6 @@ class RoomUserObserver */ public function forceDeleted(RoomUser $roomUser): void { - AuditionChange::dispatch(); + // } } diff --git a/app/Observers/ScoreSheetObserver.php b/app/Observers/ScoreSheetObserver.php index 533763e..ceb8711 100644 --- a/app/Observers/ScoreSheetObserver.php +++ b/app/Observers/ScoreSheetObserver.php @@ -12,7 +12,7 @@ class ScoreSheetObserver */ public function created(ScoreSheet $scoreSheet): void { - ScoreSheetChange::dispatch($scoreSheet->entry_id); + // } /** @@ -20,7 +20,7 @@ class ScoreSheetObserver */ public function updated(ScoreSheet $scoreSheet): void { - ScoreSheetChange::dispatch($scoreSheet->entry_id); + // } /** @@ -28,7 +28,7 @@ class ScoreSheetObserver */ public function deleted(ScoreSheet $scoreSheet): void { - ScoreSheetChange::dispatch($scoreSheet->entry_id); + // } /** @@ -36,7 +36,7 @@ class ScoreSheetObserver */ public function restored(ScoreSheet $scoreSheet): void { - ScoreSheetChange::dispatch($scoreSheet->entry_id); + // } /** @@ -44,6 +44,6 @@ class ScoreSheetObserver */ public function forceDeleted(ScoreSheet $scoreSheet): void { - ScoreSheetChange::dispatch($scoreSheet->entry_id); + // } } diff --git a/app/Observers/ScoringGuideObserver.php b/app/Observers/ScoringGuideObserver.php index 00a1dc7..bf35243 100644 --- a/app/Observers/ScoringGuideObserver.php +++ b/app/Observers/ScoringGuideObserver.php @@ -13,7 +13,7 @@ class ScoringGuideObserver */ public function created(ScoringGuide $scoringGuide): void { - ScoringGuideChange::dispatch(); + // } /** @@ -21,8 +21,7 @@ class ScoringGuideObserver */ public function updated(ScoringGuide $scoringGuide): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } /** @@ -30,8 +29,7 @@ class ScoringGuideObserver */ public function deleted(ScoringGuide $scoringGuide): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } /** @@ -39,8 +37,7 @@ class ScoringGuideObserver */ public function restored(ScoringGuide $scoringGuide): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } /** @@ -48,7 +45,6 @@ class ScoringGuideObserver */ public function forceDeleted(ScoringGuide $scoringGuide): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } } diff --git a/app/Observers/SeatingLimitObserver.php b/app/Observers/SeatingLimitObserver.php index 5b36ca7..4f4602f 100644 --- a/app/Observers/SeatingLimitObserver.php +++ b/app/Observers/SeatingLimitObserver.php @@ -12,7 +12,7 @@ class SeatingLimitObserver */ public function created(SeatingLimit $seatingLimit): void { - ScoringGuideChange::dispatch(); + // } /** @@ -20,7 +20,7 @@ class SeatingLimitObserver */ public function updated(SeatingLimit $seatingLimit): void { - ScoringGuideChange::dispatch(); + // } /** @@ -28,7 +28,7 @@ class SeatingLimitObserver */ public function deleted(SeatingLimit $seatingLimit): void { - ScoringGuideChange::dispatch(); + // } /** @@ -36,7 +36,7 @@ class SeatingLimitObserver */ public function restored(SeatingLimit $seatingLimit): void { - ScoringGuideChange::dispatch(); + // } /** @@ -44,6 +44,6 @@ class SeatingLimitObserver */ public function forceDeleted(SeatingLimit $seatingLimit): void { - ScoringGuideChange::dispatch(); + // } } diff --git a/app/Observers/SubscoreDefinitionObserver.php b/app/Observers/SubscoreDefinitionObserver.php index ae54dec..afbeccb 100644 --- a/app/Observers/SubscoreDefinitionObserver.php +++ b/app/Observers/SubscoreDefinitionObserver.php @@ -13,8 +13,7 @@ class SubscoreDefinitionObserver */ public function created(SubscoreDefinition $subscoreDefinition): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } /** @@ -22,8 +21,7 @@ class SubscoreDefinitionObserver */ public function updated(SubscoreDefinition $subscoreDefinition): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } /** @@ -31,8 +29,7 @@ class SubscoreDefinitionObserver */ public function deleted(SubscoreDefinition $subscoreDefinition): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } /** @@ -40,8 +37,7 @@ class SubscoreDefinitionObserver */ public function restored(SubscoreDefinition $subscoreDefinition): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } /** @@ -49,7 +45,6 @@ class SubscoreDefinitionObserver */ public function forceDeleted(SubscoreDefinition $subscoreDefinition): void { - AuditionChange::dispatch(); - ScoringGuideChange::dispatch(); + // } } diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index e1dc7d3..81c623f 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -20,7 +20,7 @@ class UserObserver */ public function updated(User $user): void { - AuditionChange::dispatch(); + // } /** @@ -28,7 +28,7 @@ class UserObserver */ public function deleted(User $user): void { - AuditionChange::dispatch(); + // } /** @@ -44,6 +44,6 @@ class UserObserver */ public function forceDeleted(User $user): void { - AuditionChange::dispatch(); + // } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 3107614..845e62b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,21 +2,15 @@ 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\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; use App\Models\RoomUser; use App\Models\School; +use App\Models\ScoreSheet; use App\Models\ScoringGuide; use App\Models\SeatingLimit; use App\Models\Student; @@ -38,12 +32,10 @@ use App\Services\DoublerService; use App\Services\DrawService; use App\Services\EntryService; use App\Services\ScoreService; -use App\Services\SeatingService; -use App\Services\TabulationService; -use Illuminate\Support\Facades\Event; +use App\Services\StudentService; +use App\Services\UserService; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; -use App\Models\ScoreSheet; - class AppServiceProvider extends ServiceProvider { @@ -52,36 +44,14 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { - $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 ($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(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class); + $this->app->singleton(CalculateEntryScore::class, AllJudgesCount::class); + $this->app->singleton(DrawService::class, DrawService::class); + $this->app->singleton(AuditionService::class, AuditionService::class); + $this->app->singleton(EntryService::class, EntryService::class); + $this->app->singleton(ScoreService::class, ScoreService::class); + $this->app->singleton(UserService::class, UserService::class); + $this->app->singleton(DoublerService::class, DoublerService::class); } /** @@ -102,29 +72,6 @@ class AppServiceProvider extends ServiceProvider User::observe(UserObserver::class); SeatingLimit::observe(SeatingLimitObserver::class); - Event::listen( - AuditionChange::class, - RefreshAuditionCache::class - ); - - Event::listen( - EntryChange::class, - RefreshEntryCache::class - ); - - Event::listen( - ScoringGuideChange::class, - RefreshScoringGuideCache::class - ); - - Event::listen( - ScoreSheetChange::class, - RefreshScoreSheetCache::class - ); - - Event::listen( - SeatingLimitChange::class, - RefreshSeatingLimitCache::class - ); + //Model::preventLazyLoading(! app()->isProduction()); } } diff --git a/app/Providers/CalculateEntryScoreProvider.php b/app/Providers/CalculateEntryScoreProvider.php new file mode 100644 index 0000000..8d0a2d1 --- /dev/null +++ b/app/Providers/CalculateEntryScoreProvider.php @@ -0,0 +1,28 @@ +app->singleton(CalculateScoreSheetTotal::class, CalculateScoreSheetTotal::class); + $this->app->singleton(CalculateEntryScore::class, AllJudgesCount::class); + } + + /** + * Bootstrap services. + */ + public function boot(): void + { + // + } +} diff --git a/app/Services/AuditionService.php b/app/Services/AuditionService.php index 6e2d9a9..09edccf 100644 --- a/app/Services/AuditionService.php +++ b/app/Services/AuditionService.php @@ -2,102 +2,131 @@ namespace App\Services; +use App\Exceptions\AuditionServiceException; use App\Models\Audition; -use App\Models\ScoringGuide; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Support\Facades\App; +use App\Models\Ensemble; +use App\Models\SeatingLimit; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\Session; class AuditionService { - protected $cacheKey = 'auditions'; - /** * Create a new class instance. */ + public static Collection $allAuditionIds; + public function __construct() { - // + $cacheKey = 'allAuditionIds'; + self::$allAuditionIds = Cache::remember($cacheKey, 60, function () { + return Audition::pluck('id'); + }); } /** - * Return or fill cache of auditions including the audition, - * scoringGuide.subscores, judges, judges_count, and entries_count + * @throws AuditionServiceException */ - public function getAuditions($mode = 'seating'): \Illuminate\Database\Eloquent\Collection + public function getSubscores(Audition $audition, $mode = 'seating', $sort = 'tiebreak') { - $auditions = Cache::remember($this->cacheKey, 3600, function () { - if (App::environment('local')) { - Session::flash('success', 'Audition Cache Updated'); + $cacheKey = 'auditionSubscores-'.$audition->id.'-'.$mode.'-'.$sort; + + return Cache::remember($cacheKey, 10, function () use ($audition, $mode, $sort) { + $this->validateAudition($audition); + $this->validateMode($mode); + $this->validateSort($sort); + + $sortColumn = match ($sort) { + 'tiebreak' => 'tiebreak_order', + 'display' => 'display_order', + }; + $modeColumn = match ($mode) { + 'seating' => 'for_seating', + 'advancement' => 'for_advance', + }; + $audition->load('scoringGuide.subscores'); + + return $audition->scoringGuide->subscores->where($modeColumn, true)->sortBy($sortColumn); + }); + } + + public function getJudgesOLD(Audition $audition) + { + $cacheKey = 'auditionJudges-'.$audition->id; + + return Cache::remember($cacheKey, 10, function () use ($audition) { + $this->validateAudition($audition); + + return $audition->judges; + }); + } + + public function getJudges(Audition $audition) + { + $cacheKey = 'auditionJudgeAssignments'; + $assignments = Cache::remember($cacheKey, 60, function () { + $allAuditions = Audition::with('judges')->get(); + $return = []; + foreach ($allAuditions as $audition) { + $return[$audition->id] = $audition->judges; } - return Audition::with(['scoringGuide.subscores', 'judges']) - ->withCount('judges') - ->withCount('entries') - ->withCount(['entries as seating_entries_count' => function (Builder $query) { - $query->where('for_seating', true); - }]) - ->withCount(['entries as advancement_entries_count' => function (Builder $query) { - $query->where('for_advancement', true); - }]) - ->orderBy('score_order') - ->get() - ->keyBy('id'); + return $return; }); - switch ($mode) { - case 'seating': - return $auditions->filter(fn ($audition) => $audition->for_seating); - case 'advancement': - return $auditions->filter(fn ($audition) => $audition->for_advancement); - default: - return $auditions; + return $assignments[$audition->id]; + } + + public function getSeatingLimits(Audition $audition) + { + $cacheKey = 'auditionSeatingLimits'; + $allLimits = Cache::remember($cacheKey, 60, function () { + $lims = []; + $auditions = Audition::all(); + $ensembles = Ensemble::orderBy('rank')->get(); + foreach ($auditions as $audition) { + foreach ($ensembles as $ensemble) { + if ($ensemble->event_id !== $audition->event_id) { + continue; + } + $lims[$audition->id][$ensemble->id] = [ + 'ensemble' => $ensemble, + 'limit' => 0, + ]; + } + } + $limits = SeatingLimit::all(); + + foreach ($limits as $limit) { + $lims[$limit->audition_id][$limit->ensemble_id] = [ + 'ensemble' => $ensembles->find($limit->ensemble_id), + 'limit' =>$limit->maximum_accepted, + ]; + } + return $lims; + }); + + return $allLimits[$audition->id] ?? []; + } + + protected function validateAudition($audition) + { + if (! $audition->exists()) { + throw new AuditionServiceException('Invalid audition provided'); } } - public function getAudition($id): Audition + protected function validateMode($mode) { - return $this->getAuditions()->firstWhere('id', $id); + if ($mode !== 'seating' && $mode !== 'advancement') { + throw new AuditionServiceException('Invalid mode requested. Mode must be seating or advancement'); + } } - public function refreshCache(): void + protected function validateSort($sort) { - Cache::forget($this->cacheKey); - $this->getAuditions(); - } - - public function clearCache(): void - { - Cache::forget($this->cacheKey); - } - - public function getPublishedAuditions() - { - $cacheKey = 'publishedAuditions'; - - return Cache::remember( - $cacheKey, - now()->addHour(), - function () { - return Audition::with('flags')->orderBy('score_order')->get()->filter(fn ($audition) => $audition->hasFlag('seats_published')); - }); - } - - public function getPublishedAdvancementAuditions() - { - $cacheKey = 'publishedAdvancementAuditions'; - return Cache::remember( - $cacheKey, - now()->addHour(), - function () { - return Audition::with('flags')->orderBy('score_order')->get()->filter(fn ($audition) => $audition->hasFlag('advancement_published')); - }); - - } - - public function clearPublishedAuditionsCache(): void - { - Cache::forget('publishedAuditions'); + if ($sort !== 'tiebreak' && $sort !== 'display') { + throw new AuditionServiceException('Invalid sort requested. Sort must be tiebreak or weight'); + } } } diff --git a/app/Services/DoublerService.php b/app/Services/DoublerService.php index a7e3c55..f7a114c 100644 --- a/app/Services/DoublerService.php +++ b/app/Services/DoublerService.php @@ -2,124 +2,134 @@ namespace App\Services; +use App\Exceptions\TabulationException; use App\Models\Entry; +use App\Models\Event; use App\Models\Student; -use Illuminate\Contracts\Database\Eloquent\Builder; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; class DoublerService { - protected $doublersCacheKey = 'doublers'; + protected EntryService $entryService; - protected $auditionService; + protected AuditionService $auditionService; - protected $tabulationService; - - protected $seatingService; - - /** - * Create a new class instance. - */ - public function __construct(AuditionService $auditionService, TabulationService $tabulationService, SeatingService $seatingService) + public function __construct(EntryService $entryService, AuditionService $auditionService) { - $this->auditionService = $auditionService; - $this->tabulationService = $tabulationService; - $this->seatingService = $seatingService; + $this->entryService = $entryService; + $this->auditionService = $auditionService; } /** - * Returns a collection of students that have more than one entry + * returns a collection of doublers for the event in the form of + * [studentId => [student=>student, entries=>[entries]] */ - public function getDoublers(): \Illuminate\Database\Eloquent\Collection + public function doublersForEvent(Event $event, string $mode = 'seating') { - // TODO creating or destroying an entry should refresh the doubler cache - // TODO needs to split by event so that a doubler may enter jazz and concert events for example - $doublers = Cache::remember($this->doublersCacheKey, 60, function () { - return Student::withCount(['entries' => function (Builder $query) { - $query->where('for_seating', true); - }]) - ->with(['entries' => function (Builder $query) { - $query->where('for_seating', true); - }]) - ->havingRaw('entries_count > ?', [1]) - ->get(); + $cacheKey = 'event'.$event->id.'doublers-'.$mode; + + return Cache::remember($cacheKey, 60, function () use ($event, $mode) { + return $this->findDoublersForEvent($event, $mode); }); - - return $doublers; - } - - public function refreshDoublerCache() - { - Cache::forget($this->doublersCacheKey); - $this->getDoublers(); } /** - * Returns an array of information about each entry for a specific doubler. Info for each entry includes - * entryID - * auditionID - * auditionName - * rank => This student's rank in the given audition - * unscored => How many entries remain to be scored in this audition - * limits => acceptance limits for this audition - * status => accepted, declined, or undecided - * - * @param int $studentId The ID of the doubler + * @throws TabulationException */ - public function getDoublerInfo($studentId): array + protected function findDoublersForEvent(Event $event, string $mode = 'seating'): array { - $doubler = $this->getDoublers()->firstWhere('id', $studentId); + // TODO add scoped entry queries to the event model + $this->validateEvent($event); + $entries = $event->entries()->with('audition')->with('student')->get(); + $entries = match ($mode) { + 'seating' => $entries->filter(fn ($entry) => $entry->for_seating === 1), + 'advancement' => $entries->filter(fn ($entry) => $entry->for_advance === 1), + }; - // Split $doubler->entries into two arrays based on the result of hasFlag('declined') - $undecidedEntries = $doubler->entries->filter(function ($entry) { - return ! $entry->hasFlag('declined'); - }); - $acceptedEntry = null; - if ($undecidedEntries->count() == 1) { - $acceptedEntry = $undecidedEntries->first(); + $grouped = $entries->groupBy('student_id'); + // Filter out student groups with only one entry in the event + $grouped = $grouped->filter(fn ($s) => $s->count() > 1); + $doubler_array = []; + foreach ($grouped as $student_id => $entries) { + $doubler_array[$student_id] = [ + 'student_id' => $student_id, + 'entries' => $entries, + ]; } - // TODO can I rewrite this? - // When getting a doubler we need to know - // 1) What their entries are - // 2) For each audition they're entered in, what is their rank - // 3) For each audition they're entered in, how many entries are unscored - // 4) How many are accepted on that instrument - // 5) Status - accepted, declined or undecided + return $doubler_array; + } - $info = []; + public function simpleDoubleInfo(Entry $primaryEntry) + { + if (! isset($this->findDoublersForEvent($primaryEntry->audition->event)[$primaryEntry->student_id])) { + return false; + } - foreach ($doubler->entries as $entry) { + return $this->findDoublersForEvent($primaryEntry->audition->event)[$primaryEntry->student_id]['entries']; + } + + public function entryDoublerData(Entry $primaryEntry) + { + if (! isset($this->doublersForEvent($primaryEntry->audition->event)[$primaryEntry->student_id])) { + return false; + } + $entries = $this->doublersForEvent($primaryEntry->audition->event)[$primaryEntry->student_id]['entries']; + $entryData = collect([]); + /** @var Collection $entries */ + foreach ($entries as $entry) { + $status = 'undecided'; if ($entry->hasFlag('declined')) { $status = 'declined'; - } elseif ($entry === $acceptedEntry) { - $status = 'accepted'; - } else { - $status = 'undecided'; } - $info[$entry->id] = [ - 'entryID' => $entry->id, - 'auditionID' => $entry->audition_id, - 'auditionName' => $this->auditionService->getAudition($entry->audition_id)->name, - 'rank' => $this->tabulationService->entryRank($entry), - 'unscored' => $this->tabulationService->remainingEntriesForAudition($entry->audition_id), - 'limits' => $this->seatingService->getLimitForAudition($entry->audition_id), + if ($entry->hasFlag('no_show')) { + $status = 'no_show'; + } + + $lims = $this->auditionService->getSeatingLimits($entry->audition); + $limits = []; + foreach ($lims as $lim) { + $limits[] = [ + 'ensemble_name' => $lim['ensemble']->name, + 'accepts' => $lim['limit'], + 'ensemble' => $lim['ensemble'], + ]; + } + + $entryData[$entry->id] = [ + 'entry' => $entry, + 'audition' => $entry->audition, + 'auditionName' => $entry->audition->name, 'status' => $status, + 'rank' => $this->entryService->rankOfEntry('seating', $entry), + 'unscored_entries' => $entry->audition->unscored_entries_count, + 'seating_limits' => $limits, ]; - $entry->audition = $this->auditionService->getAudition($entry->audition_id); + } + // find out how many items in the collection $entryData have a status of 'undecided' + $undecided_count = $entryData->filter(fn ($entry) => $entry['status'] === 'undecided')->count(); + // if $undecided_count is 1 set the item where status is 'undecided' to 'accepted' + if ($undecided_count === 1) { + $entryData->transform(function ($entry) { + if ($entry['status'] === 'undecided') { + $entry['status'] = 'accepted'; + } + + return $entry; + }); } - return $info; + return $entryData; } /** - * Checks if a student is a doubler based on the given student ID - * - * @param int $studentId The ID of the student to check - * @return bool Returns true if the student is a doubler, false otherwise + * @throws TabulationException */ - public function studentIsDoubler($studentId): bool + protected function validateEvent(Event $event) { - return $this->getDoublers()->contains('id', $studentId); + if (! $event->exists) { + throw new TabulationException('Invalid event provided'); + } } } diff --git a/app/Services/EntryService.php b/app/Services/EntryService.php index f2071d6..0f719e0 100644 --- a/app/Services/EntryService.php +++ b/app/Services/EntryService.php @@ -2,95 +2,22 @@ namespace App\Services; +use App\Actions\Tabulation\RankAuditionEntries; use App\Models\Entry; -use Illuminate\Support\Collection; +use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Cache; class EntryService { - protected $auditionCache; - /** * Create a new class instance. */ - public function __construct(AuditionService $auditionCache) + public function __construct() { - $this->auditionCache = $auditionCache; + } - /** - * Return a collection of all entries for the provided auditionId along with the - * student.school for each entry. - * - * @return \Illuminate\Database\Eloquent\Collection - */ - public function getEntriesForAudition($auditionId, $mode = 'seating') - { - // TODO this invokes a lot of lazy loading. Perhaps cache the data for all entries then draw from that for each audition - $cacheKey = 'audition'.$auditionId.'entries'; - - $entries = Cache::remember($cacheKey, 3600, function () use ($auditionId) { - return Entry::where('audition_id', $auditionId) - ->with('student.school') - ->get() - ->keyBy('id'); - }); - - switch ($mode) { - case 'seating': - return $entries->filter(function ($entry) { - return $entry->for_seating; - }); - case 'advancement': - return $entries->filter(function ($entry) { - return $entry->for_advancement; - }); - default: - return $entries; - } - } - - /** - * Returns a collection of collections of entries, one collection for each audition. - * The outer collection is keyed by the audition ID. The included entries are - * with their student.school. - */ - public function getAllEntriesByAudition(): Collection - { - $auditions = $this->auditionCache->getAuditions(); - $allEntries = []; - foreach ($auditions as $audition) { - $allEntries[$audition->id] = $this->getEntriesForAudition($audition->id); - } - - return collect($allEntries); - } - - public function getAllEntries() - { - $cacheKey = 'allEntries'; - - return Cache::remember($cacheKey, 5, function () { - return Entry::all(); - }); - } - - public function clearEntryCacheForAudition($auditionId): void - { - $cacheKey = 'audition'.$auditionId.'entries'; - Cache::forget($cacheKey); - Cache::forget('allEntries'); - } - - public function clearEntryCaches(): void - { - $auditions = $this->auditionCache->getAuditions(); - foreach ($auditions as $audition) { - $this->clearEntryCacheForAudition($audition->id); - } - } - - public function entryIsLate(Entry $entry): bool + public function isEntryLate(Entry $entry): bool { if ($entry->hasFlag('wave_late_fee')) { return false; @@ -98,4 +25,25 @@ class EntryService return $entry->created_at > $entry->audition->entry_deadline; } + + public function entryExists(Entry $entry): bool + { + $cacheKey = 'allEntryIds'; + $allEntryIds = Cache::remember($cacheKey, 60, function () { + return Entry::pluck('id'); + }); + + return $allEntryIds->contains($entry->id); + } + + public function rankOfEntry(string $mode, Entry $entry) + { + $ranker = App::make(RankAuditionEntries::class); + $rankings = $ranker->rank($mode, $entry->audition); + $rankedEntry = $rankings->find($entry->id); + if (isset($rankedEntry->score_message)) { + return $rankedEntry->score_message; + } + return $rankings->find($entry->id)->rank ?? 'No Rank'; + } } diff --git a/app/Services/Invoice/InvoiceOneFeePerEntry.php b/app/Services/Invoice/InvoiceOneFeePerEntry.php index b08b27d..1ca5b34 100644 --- a/app/Services/Invoice/InvoiceOneFeePerEntry.php +++ b/app/Services/Invoice/InvoiceOneFeePerEntry.php @@ -39,7 +39,7 @@ class InvoiceOneFeePerEntry implements InvoiceDataService foreach ($school->students as $student) { foreach ($entries[$student->id] ?? [] as $entry) { $entryFee = $entry->audition->entry_fee / 100; - $lateFee = $this->entryService->entryIsLate($entry) ? auditionSetting('late_fee') / 100 : 0; + $lateFee = $this->entryService->isEntryLate($entry) ? auditionSetting('late_fee') / 100 : 0; $invoiceData['lines'][] = [ 'student_name' => $student->full_name(true), diff --git a/app/Services/ScoreService.php b/app/Services/ScoreService.php index e15d9da..afd29c2 100644 --- a/app/Services/ScoreService.php +++ b/app/Services/ScoreService.php @@ -3,216 +3,22 @@ namespace App\Services; use App\Models\Entry; -use App\Models\ScoringGuide; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Cache; -use Illuminate\Support\Facades\DB; -use App\Models\ScoreSheet; -use function array_unshift; +use App\Models\User; class ScoreService { - protected $auditionCache; - - protected $entryCache; - /** * Create a new class instance. */ - public function __construct(AuditionService $auditionCache, EntryService $entryCache) + public function __construct() { - $this->auditionCache = $auditionCache; - $this->entryCache = $entryCache; - } - - /** - * Cache all scoring guides - */ - public function getScoringGuides(): \Illuminate\Database\Eloquent\Collection - { - $cacheKey = 'scoringGuides'; - - return Cache::remember($cacheKey, 3600, fn () => ScoringGuide::with('subscores')->withCount('subscores')->get()); - } - - /** - * Retrieve a single scoring guide from the cache - */ - public function getScoringGuide($id): ScoringGuide - { - return $this->getScoringGuides()->find($id); - } - - /** - * Clear the scoring guide cache - */ - public function clearScoringGuideCache(): void - { - Cache::forget('scoringGuides'); - } - - /** - * Returns an array where each key is an entry id and the value is the number - * of score sheets assigned to that entry. - * - * @return Collection - */ - public function entryScoreSheetCounts() - { - $cacheKey = 'entryScoreSheetCounts'; - - return Cache::remember($cacheKey, 10, function () { - // For each Entry get the number of ScoreSheets associated with it - $scoreSheetCountsByEntry = ScoreSheet::select('entry_id', DB::raw('count(*) as count')) - ->groupBy('entry_id') - ->get() - ->pluck('count', 'entry_id'); - - $entryScoreSheetCounts = []; - $entries = $this->entryCache->getAllEntries(); - foreach ($entries as $entry) { - $entryScoreSheetCounts[$entry->id] = $scoreSheetCountsByEntry[$entry->id] ?? 0; - } - - return $entryScoreSheetCounts; - - }); } - - /** - * Get final scores array for the requested entry. The first element is the total score. The following elements are sums - * of each subscore in tiebreaker order - * - * @return array - */ - public function entryTotalScores(Entry $entry) + public function isEntryFullyScored(Entry $entry): bool { - $cacheKey = 'entry'.$entry->id.'totalScores'; - - return Cache::remember($cacheKey, 3600, function () use ($entry) { - return $this->calculateFinalScoreArray($entry->audition->scoring_guide_id, $entry->scoreSheets); - }); - } - - /** - * Calculate and cache scores for all entries for the provided audition ID - * - * @return void - */ - public function calculateScoresForAudition($auditionId, $mode= 'seating') - { - static $alreadyChecked = []; - // if $auditionId is in the array $alreadyChecked return - if (in_array($auditionId, $alreadyChecked)) { - return; - } - $alreadyChecked[] = $auditionId; - $audition = $this->auditionCache->getAudition($auditionId); - $scoringGuideId = $audition->scoring_guide_id; - $entries = $this->entryCache->getEntriesForAudition($auditionId, $mode); - $entries->load('scoreSheets'); // TODO Cache this somehow, it's expensive and repetitive on the seating page - - foreach ($entries as $entry) { - $cacheKey = 'entry'.$entry->id.'totalScores'; - if (Cache::has($cacheKey)) { - continue; - } - $thisTotalScore = $this->calculateFinalScoreArray($scoringGuideId, $entry->scoreSheets); - Cache::put($cacheKey, $thisTotalScore, 3600); - } - } - - public function clearScoreSheetCountCache() - { - $cacheKey = 'entryScoreSheetCounts'; - Cache::forget($cacheKey); - } - - public function clearEntryTotalScoresCache($entryId) - { - $cacheKey = 'entry'.$entryId.'totalScores'; - Cache::forget($cacheKey); - } - - public function clearAllCachedTotalScores() - { - foreach ($this->entryCache->getAllEntries() as $entry) { - $cacheKey = 'entry'.$entry->id.'totalScores'; - Cache::forget($cacheKey); - } - } - - /** - * Calculate final score using the provided scoring guide and score sheets. Returns an array of scores - * The first element is the total score. The following elements are the sum of each subscore - * in tiebreaker order. - */ - public function calculateFinalScoreArray($scoringGuideId, array|Collection $scoreSheets): array - { - - $sg = $this->getScoringGuide($scoringGuideId); - - // TODO cache the scoring guides with their subscores - $subscores = $sg->subscores->sortBy('tiebreak_order'); - - $ignoredSubscores = []; // This will be subscores not used for seating - - // Init final scores array - $finalScoresArray = []; - foreach ($subscores as $subscore) { - if (! $subscore->for_seating) { // Ignore scores that are not for seating - $ignoredSubscores[] = $subscore->id; - - continue; - } - $finalScoresArray[$subscore->id] = 0; - } - - foreach ($scoreSheets as $sheet) { - foreach ($sheet->subscores as $ss) { - if (in_array($ss['subscore_id'], $ignoredSubscores)) { // Ignore scores that are not for seating - continue; - } - $finalScoresArray[$ss['subscore_id']] += $ss['score']; - } - } - - // calculate weighted final score - $totalScore = 0; - $totalWeight = 0; - foreach ($subscores as $subscore) { - if (in_array($subscore->id, $ignoredSubscores)) { // Ignore scores that are not for seating - continue; - } - $totalScore += ($finalScoresArray[$subscore->id] * $subscore->weight); - $totalWeight += $subscore->weight; - } - $totalScore = ($totalScore / $totalWeight); - array_unshift($finalScoresArray, $totalScore); - - return $finalScoresArray; - } - - /** - * Validate that the judge on the score sheet is actually assigned to judge - * then entry - * - * @return bool - */ - public function validateScoreSheet(ScoreSheet $sheet) - { - // TODO use this when calculating scores - $entry = $this->entryCache->getAllEntries()->find($sheet->entry_id); - $audition = $this->auditionCache->getAudition($entry->audition_id); - $validJudges = $audition->judges; - // send a laravel flash message with an error if the $sheet->user_id is not in the collection $validJudges - if (! $validJudges->contains('id', $sheet->user_id)) { - session()->flash('error', 'Entry ID '.$sheet->entry_id.' has an invalid score entered by '.$sheet->judge->full_name()); - } - - // check if $sheet->user_id is in the collection $validJudges, return false if not, true if it is - return $validJudges->contains('id', $sheet->user_id); + $requiredJudges = $entry->audition->judges()->count(); + $scoreSheets = $entry->scoreSheets()->count(); + return $requiredJudges === $scoreSheets; } } diff --git a/app/Services/SeatingService.php b/app/Services/SeatingService.php deleted file mode 100644 index 1b39a30..0000000 --- a/app/Services/SeatingService.php +++ /dev/null @@ -1,90 +0,0 @@ -tabulationService = $tabulationService; - } - - public function getAcceptanceLimits() - { - return Cache::remember($this->limitsCacheKey, now()->addDay(), function () { - $limits = SeatingLimit::with('ensemble')->get(); - // Sort limits by ensemble->rank - $limits = $limits->sortBy(function ($limit) { - return $limit->ensemble->rank; - }); - - return $limits->groupBy('audition_id'); - }); - } - - public function getLimitForAudition($auditionId) - { - if (! $this->getAcceptanceLimits()->has($auditionId)) { - return new \Illuminate\Database\Eloquent\Collection(); - } - return $this->getAcceptanceLimits()[$auditionId]; - } - - public function refreshLimits(): void - { - Cache::forget($this->limitsCacheKey); - } - - public function getSeatableEntries($auditionId) - { - $entries = $this->tabulationService->auditionEntries($auditionId); - - return $entries->reject(function ($entry) { - return $entry->hasFlag('declined'); - }); - } - - public function getSeatsForAudition($auditionId) - { - $cacheKey = 'audition'.$auditionId.'seats'; - // TODO rework to pull entry info from cache - return Cache::remember($cacheKey, now()->addHour(), function () use ($auditionId) { - return Seat::with('entry.student.school') - ->where('audition_id', $auditionId) - ->orderBy('seat') - ->get() - ->groupBy('ensemble_id'); - }); - } - - public function forgetSeatsForAudition($auditionId) - { - $cacheKey = 'audition'.$auditionId.'seats'; - Cache::forget($cacheKey); - } - - public function getEnsemblesForEvent($eventId) - { - static $eventEnsembles = []; - - if (array_key_exists($eventId, $eventEnsembles)) { - return $eventEnsembles[$eventId]; - } - $event = Event::find($eventId); - $eventEnsembles[$eventId] = $event->ensembles; - - return $eventEnsembles[$eventId]; - } -} diff --git a/app/Services/TabulationService.php b/app/Services/TabulationService.php deleted file mode 100644 index 3957432..0000000 --- a/app/Services/TabulationService.php +++ /dev/null @@ -1,176 +0,0 @@ -auditionService = $auditionService; - $this->scoreService = $scoreService; - $this->entryService = $entryService; - } - - /** - * Returns the rank of the entry in its audition - * - * @return mixed - */ - public function entryRank(Entry $entry) - { - return $this->auditionEntries($entry->audition_id)[$entry->id]->rank; - } - - /** - * Returns a collection of entries including their calculated final_score_array and ranked - * based upon their scores. - * - * @return \Illuminate\Support\Collection|mixed - */ - public function auditionEntries(int $auditionId, $mode = 'seating') - { - static $cache = []; - if (isset($cache[$auditionId])) { - return $cache[$auditionId]; - } - - $audition = $this->auditionService->getAudition($auditionId); - $entries = $this->entryService->getEntriesForAudition($auditionId, $mode); - $this->scoreService->calculateScoresForAudition($auditionId); - // TODO will need to pass a mode to the above function to only use subscores for hte appropriate mode - foreach ($entries as $entry) { - $entry->final_score_array = $this->scoreService->entryTotalScores($entry); - $entry->scoring_complete = ($this->scoreService->entryScoreSheetCounts()[$entry->id] == $audition->judges_count); - } - // Sort the array $entries by the first element in the final_score_array on each entry, then by the second element in that array continuing through each element in the final_score_array for each entry - $entries = $entries->sort(function ($a, $b) { - for ($i = 0; $i < count($a->final_score_array); $i++) { - if ($a->final_score_array[$i] != $b->final_score_array[$i]) { - return $b->final_score_array[$i] > $a->final_score_array[$i] ? 1 : -1; - } - } - - return 0; - }); - //TODO verify this actually sorts by subscores correctly - - // Assign a rank to each entry. In the case of a declined seat by a doubler, indicate as so and do not increment rank - $n = 1; - /** @var Entry $entry */ - foreach ($entries as $entry) { - if (! $entry->hasFlag('declined') or $mode != 'seating') { - $entry->rank = $n; - $n++; - } else { - $entry->rank = $n.' - declined'; - } - } - - $cache[$auditionId] = $entries->keyBy('id'); - - return $entries->keyBy('id'); - } - - public function entryScoreSheetsAreValid(Entry $entry): bool - { - //TODO consider making this move the invalid score to another database for further investigation - $validJudges = $this->auditionService->getAudition($entry->audition_id)->judges; - foreach ($entry->scoreSheets as $sheet) { - if (! $validJudges->contains($sheet->user_id)) { - $invalidJudge = User::find($sheet->user_id); - Session::flash('error', 'Invalid scores for entry '.$entry->id.' exist from '.$invalidJudge->full_name()); - - return false; - } - } - - return true; - } - - /** - * Returns the number of un-scored entries for the audition with the given ID. - * - * @return mixed - */ - public function remainingEntriesForAudition($auditionId, $mode = 'seating') - { - $audition = $this->getAuditionsWithStatus($mode)[$auditionId]; - - switch ($mode) { - case 'seating': - return $audition->seating_entries_count - $audition->scored_entries_count; - case 'advancement': - return $audition->advancement_entries_count - $audition->scored_entries_count; - } - - return $audition->entries_count - $audition->scored_entries_count; - } - - /** - * Get the array of all auditions from the cache. For each one, set a property - * scored_entries_count that indicates the number of entries for that audition that - * have a number of score sheets equal to the number of judges for that audition. - * - * @return mixed - */ - public function getAuditionsWithStatus($mode = 'seating') - { - return Cache::remember('auditionsWithStatus', 30, function () use ($mode) { - - // Retrieve auditions from the cache and load entry IDs - $auditions = $this->auditionService->getAuditions($mode); - // Iterate over the auditions and calculate the scored_entries_count - foreach ($auditions as $audition) { - $scored_entries_count = 0; - $entries_to_check = $this->entryService->getEntriesForAudition($audition->id); - - switch ($mode) { - case 'seating': - $entries_to_check = $entries_to_check->filter(function ($entry) { - return $entry->for_seating; - }); - $auditions = $auditions->filter(function ($audition) { - return $audition->for_seating; - }); - break; - case 'advancement': - $entries_to_check = $entries_to_check->filter(function ($entry) { - return $entry->for_advancement; - }); - $auditions = $auditions->filter(function ($audition) { - return $audition->for_advancement; - }); - break; - } - - foreach ($entries_to_check as $entry) { - if ($this->scoreService->entryScoreSheetCounts()[$entry->id] - $audition->judges_count == 0) { - $scored_entries_count++; - } - } - - $audition->scored_entries_count = $scored_entries_count; - } - - return $auditions; - - }); - } -} diff --git a/app/Services/UserService.php b/app/Services/UserService.php new file mode 100644 index 0000000..545e069 --- /dev/null +++ b/app/Services/UserService.php @@ -0,0 +1,24 @@ +contains($user->id); + } +} diff --git a/app/Settings.php b/app/Settings.php index c31f260..d26e314 100644 --- a/app/Settings.php +++ b/app/Settings.php @@ -25,6 +25,7 @@ class Settings public static function get($key, $default = null) { $settings = Cache::get(self::$cacheKey, []); + return $settings[$key] ?? $default; } diff --git a/app/helpers.php b/app/helpers.php index ef50236..09e1327 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -1,6 +1,11 @@ 'redis', 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + 'prefix' => env('REDIS_CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'), ], 'dynamodb' => [ diff --git a/database/seeders/AuditionWithScoringGuideAndRoom.php b/database/seeders/AuditionWithScoringGuideAndRoom.php new file mode 100644 index 0000000..24b2748 --- /dev/null +++ b/database/seeders/AuditionWithScoringGuideAndRoom.php @@ -0,0 +1,84 @@ +create(['id' => 1000]); + $sg = ScoringGuide::factory()->create(['id' => 1000]); + SubscoreDefinition::create([ + 'id' => 1001, + 'scoring_guide_id' => $sg->id, + 'name' => 'Scale', + 'max_score' => 100, + 'weight' => 1, + 'display_order' => 1, + 'tiebreak_order' => 5, + 'for_seating' => 1, + 'for_advance' => 0, + ]); + SubscoreDefinition::create([ + 'id' => 1002, + 'scoring_guide_id' => $sg->id, + 'name' => 'Etude 1', + 'max_score' => 100, + 'weight' => 2, + 'display_order' => 2, + 'tiebreak_order' => 3, + 'for_seating' => 1, + 'for_advance' => 1, + ]); + SubscoreDefinition::create([ + 'id' => 1003, + 'scoring_guide_id' => $sg->id, + 'name' => 'Etude 2', + 'max_score' => 100, + 'weight' => 2, + 'display_order' => 3, + 'tiebreak_order' => 4, + 'for_seating' => 1, + 'for_advance' => 1, + ]); + SubscoreDefinition::create([ + 'id' => 1004, + 'scoring_guide_id' => $sg->id, + 'name' => 'Sight Reading', + 'max_score' => 100, + 'weight' => 3, + 'display_order' => 4, + 'tiebreak_order' => 2, + 'for_seating' => 1, + 'for_advance' => 1, + ]); + SubscoreDefinition::create([ + 'id' => 1005, + 'scoring_guide_id' => $sg->id, + 'name' => 'Tone', + 'max_score' => 100, + 'weight' => 1, + 'display_order' => 5, + 'tiebreak_order' => 1, + 'for_seating' => 0, + 'for_advance' => 1, + ]); + Audition::factory()->create([ + 'id' => 1000, + 'room_id' => $room->id, + 'scoring_guide_id' => $sg->id, + 'name' => 'Test Audition', + ]); + } +} diff --git a/database/seeders/RoomSeeder.php b/database/seeders/RoomSeeder.php deleted file mode 100644 index bdf439e..0000000 --- a/database/seeders/RoomSeeder.php +++ /dev/null @@ -1,17 +0,0 @@ - Enter Scores Enter No-Shows - Audition Status + Audition Status {{ auditionSetting('advanceTo') }} Status diff --git a/resources/views/tabulation/advancement/results-table.blade.php b/resources/views/tabulation/advancement/results-table.blade.php index 049e6e3..0cba0b4 100644 --- a/resources/views/tabulation/advancement/results-table.blade.php +++ b/resources/views/tabulation/advancement/results-table.blade.php @@ -7,7 +7,6 @@ Draw # Student Name Total Score - All Scores? Votes @if($scoringComplete) Pass? @@ -17,6 +16,13 @@ @foreach($entries as $entry) + @php + if ($entry->score_totals[0] < 0) { + $score = $entry->score_message; + } else { + $score = number_format($entry->score_totals[0] ?? 0,4); + } + @endphp {{ $entry->rank }} {{ $entry->id }} @@ -25,12 +31,8 @@ {{ $entry->student->full_name() }} {{ $entry->student->school->name }} - {{ number_format($entry->final_score_array[0] ?? 0,4) }} - - @if($entry->scoring_complete) - - @endif - + {{ $score }} + @foreach($entry->advancementVotes as $vote)
- @foreach($auditions as $audition) - @php - $percent = 100; - if($audition->advancement_entries_count > 0) { - $percent = round(($audition->scored_entries_count / $audition->advancement_entries_count) * 100); - } - @endphp + @foreach($auditionData as $audition) - +
- {{ $audition->name }} - {{ $audition->scored_entries_count }} / {{ $audition->advancement_entries_count }} Scored + {{ $audition['name'] }} + {{ $audition['scored_entries_count'] }} / {{ $audition['entries_count'] }} Scored
-
+
- @if( $audition->scored_entries_count == $audition->advancement_entries_count) + @if( $audition['scoring_complete']) @endif - @if( $audition->hasFlag('advancement_published')) + @if( $audition['published']) @endif diff --git a/resources/views/tabulation/auditionSeating-doubler-block.blade.php b/resources/views/tabulation/auditionSeating-doubler-block.blade.php index 9783156..9f7635d 100644 --- a/resources/views/tabulation/auditionSeating-doubler-block.blade.php +++ b/resources/views/tabulation/auditionSeating-doubler-block.blade.php @@ -1,14 +1,13 @@ -@php($doublerEntryInfo = $doublerService->getDoublerInfo($entry->student_id)) @php($doublerButtonClasses = 'hidden rounded-md bg-white px-2.5 py-1.5 text-xs text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:block')
    - @foreach($doublerEntryInfo as $info) - @php($isopen = $info['status'] == 'undecided') + @foreach($entry['doubleData'] as $double) + @php($isopen = $double['status'] == 'undecided')
  • - - {{ $info['auditionName'] }} - {{ $info['status'] }} + + {{ $double['auditionName'] }} - {{ $double['status'] }}

    @@ -21,26 +20,23 @@
      -
    • -

      Ranked {{ $info['rank'] }}

      - - - -

      {{ $info['unscored'] }} Unscored

      +
    • +

      Ranked {{ $double['rank'] }}

      +

      {{ $double['unscored_entries'] }} Unscored

    • - @foreach($info['limits'] as $limit) -
    • {{ $limit->ensemble->name }} accepts {{ $limit->maximum_accepted }}
    • + @foreach($double['seating_limits'] as $limit) +
    • {{$limit['ensemble_name']}} accepts {{ $limit['accepts'] }}
    • @endforeach
    - @if ($info['status'] === 'undecided') -
    + @if ($double['status'] == 'undecided') + @csrf
    -
    + @csrf
    diff --git a/resources/views/tabulation/auditionSeating-fill-seats-form.blade.php b/resources/views/tabulation/auditionSeating-fill-seats-form.blade.php index 8f1a0d5..dd5794c 100644 --- a/resources/views/tabulation/auditionSeating-fill-seats-form.blade.php +++ b/resources/views/tabulation/auditionSeating-fill-seats-form.blade.php @@ -1,17 +1,20 @@ + @php + @endphp Seating
    - + @csrf - @foreach($ensembleLimits as $ensembleLimit) + @foreach($rightPanel['data'] as $ensembleLimit) @php - $value = $requestedEnsembleAccepts[$ensembleLimit->ensemble->id] ?? $ensembleLimit->maximum_accepted; + $value = $requestedEnsembleAccepts[$ensembleLimit['ensemble']->id] ?? $ensembleLimit['limit']; +// $value = $ensembleLimit['limit']; @endphp - @endforeach diff --git a/resources/views/tabulation/auditionSeating-results-table.blade.php b/resources/views/tabulation/auditionSeating-results-table.blade.php index 2877e03..1279f96 100644 --- a/resources/views/tabulation/auditionSeating-results-table.blade.php +++ b/resources/views/tabulation/auditionSeating-results-table.blade.php @@ -16,23 +16,35 @@ - @foreach($entries as $entry) + @foreach($entryData as $entry) - {{ $entry->rank }} - {{ $entry->id }} - {{ $entry->draw_number }} + {{ $entry['rank'] }} + {{ $entry['id'] }} + {{ $entry['drawNumber'] }} - {{ $entry->student->full_name() }} - {{ $entry->student->school->name }} + {{ $entry['studentName'] }} + {{ $entry['schoolName'] }} - @if($doublerService->studentIsDoubler($entry->student_id)) + @if($entry['doubleData']) @include('tabulation.auditionSeating-doubler-block') +{{-- DOUBLER
    --}} +{{-- @foreach($entry['doubleData'] as $double)--}} +{{-- ID: {{ $double['entryId'] }} - {{ $double['name'] }} - {{ $double['rank'] }}
    --}} +{{-- Unscored Entries: {{ $double['unscored_in_audition'] }}
    --}} +{{-- @foreach($double['limits'] as $limit)--}} +{{-- {{$limit['ensemble']->name}}: {{ $limit['limit'] }}
    --}} +{{-- @endforeach--}} +{{--
    --}} +{{-- @endforeach--}} @endif +{{-- @if($doublerService->studentIsDoubler($entry->student_id))--}} +{{-- @include('tabulation.auditionSeating-doubler-block')--}} +{{-- @endif--}}
    - {{ number_format($entry->final_score_array[0] ?? 0,4) }} + {{ $entry['totalScore'] }} - @if($entry->scoring_complete) + @if($entry['fullyScored']) @endif diff --git a/resources/views/tabulation/auditionSeating-right-complete-not-published.blade.php b/resources/views/tabulation/auditionSeating-right-complete-not-published.blade.php new file mode 100644 index 0000000..27797b3 --- /dev/null +++ b/resources/views/tabulation/auditionSeating-right-complete-not-published.blade.php @@ -0,0 +1,2 @@ +@include('tabulation.auditionSeating-fill-seats-form') +@include('tabulation.auditionSeating-show-proposed-seats') diff --git a/resources/views/tabulation/auditionSeating-show-proposed-seats.blade.php b/resources/views/tabulation/auditionSeating-show-proposed-seats.blade.php index e312c13..dd00bfa 100644 --- a/resources/views/tabulation/auditionSeating-show-proposed-seats.blade.php +++ b/resources/views/tabulation/auditionSeating-show-proposed-seats.blade.php @@ -2,19 +2,20 @@ $seatingProposal = []; @endphp -@foreach($ensembleLimits as $ensembleLimit) +@foreach($rightPanel['data'] as $ensembleLimit) - {{ $ensembleLimit->ensemble->name }} - DRAFT + {{ $ensembleLimit['ensemble']->name }} - DRAFT @php - $maxAccepted = $requestedEnsembleAccepts[$ensembleLimit->ensemble->id] ?? $ensembleLimit->maximum_accepted; + $maxAccepted = $requestedEnsembleAccepts[$ensembleLimit['ensemble']->id] ?? $ensembleLimit['limit']; +// $maxAccepted = $ensembleLimit['limit']; @endphp @for($n=1; $n <= $maxAccepted; $n++) @php $entry = $seatableEntries->shift(); if (is_null($entry)) continue; $seatingProposal[] = [ - 'ensemble_id' => $ensembleLimit->ensemble->id, + 'ensemble_id' => $ensembleLimit['ensemble']->id, 'audition_id' => $audition->id, 'seat' => $n, 'entry_id' => $entry->id, @@ -28,7 +29,7 @@ @endforeach -
    + @csrf Seat and Publish
    diff --git a/resources/views/tabulation/auditionSeating-show-published-seats.blade.php b/resources/views/tabulation/auditionSeating-show-published-seats.blade.php index 2ce2ac7..ef920a1 100644 --- a/resources/views/tabulation/auditionSeating-show-published-seats.blade.php +++ b/resources/views/tabulation/auditionSeating-show-published-seats.blade.php @@ -1,12 +1,10 @@ -@inject('seatingService','App\Services\SeatingService') - Seats are Published Unpublish @@ -15,17 +13,34 @@ -@foreach($ensembleLimits as $ensembleLimit) + @php - $ensembleSeats = $seatingService->getSeatsForAudition($audition->id)[$ensembleLimit->ensemble->id] ?? array(); + $previousEnsemble = null; @endphp - - {{ $ensembleLimit->ensemble->name }} - @foreach($ensembleSeats as $seat) + @foreach($rightPanel['data'] as $seat) + @if($seat['ensemble'] !== $previousEnsemble) + {{$seat['ensemble']}} + @endif - {{ $seat->seat }} - {{ $seat->student->full_name() }} + {{ $seat['seat'] }} - {{ $seat['student_name'] }} - @endforeach + @php - -@endforeach + $previousEnsemble = $seat['ensemble']; + @endphp + @endforeach + +{{--@foreach($ensembleLimits as $ensembleLimit)--}} +{{-- @php--}} +{{-- $ensembleSeats = $seatingService->getSeatsForAudition($audition->id)[$ensembleLimit->ensemble->id] ?? array();--}} +{{-- @endphp--}} +{{-- --}} +{{-- {{ $ensembleLimit->ensemble->name }}--}} +{{-- @foreach($ensembleSeats as $seat)--}} +{{-- --}} +{{-- {{ $seat->seat }} - {{ $seat->student->full_name() }}--}} +{{-- --}} +{{-- @endforeach--}} + +{{-- --}} +{{--@endforeach--}} diff --git a/resources/views/tabulation/auditionSeating-unable-to-seat-card.blade.php b/resources/views/tabulation/auditionSeating-unable-to-seat-card.blade.php index d243c37..54711ea 100644 --- a/resources/views/tabulation/auditionSeating-unable-to-seat-card.blade.php +++ b/resources/views/tabulation/auditionSeating-unable-to-seat-card.blade.php @@ -1,10 +1,10 @@ Unable to seat this audition - @if(! $scoringComplete) + @if(! $rightPanel['data']['allScored'])

    The audition cannot be seated while it has unscored entries.

    @endif - @if(! $doublerComplete) + @if(! $rightPanel['data']['doublersResolved'])

    The audition cannot be seated while it has unresolved doublers.

    @endif
    diff --git a/resources/views/tabulation/auditionSeating.blade.php b/resources/views/tabulation/auditionSeating.blade.php index c916ff8..065ae86 100644 --- a/resources/views/tabulation/auditionSeating.blade.php +++ b/resources/views/tabulation/auditionSeating.blade.php @@ -10,20 +10,21 @@ @include('tabulation.auditionSeating-results-table')
    - @if($audition->hasFlag('seats_published')) - @include('tabulation.auditionSeating-show-published-seats') - @elseif(! $auditionComplete) - @include('tabulation.auditionSeating-unable-to-seat-card') - @else - @include('tabulation.auditionSeating-fill-seats-form') - @include('tabulation.auditionSeating-show-proposed-seats') - @endif - - + @include($rightPanel['view'])
    +{{--
    --}} +{{-- @if($audition->hasFlag('seats_published'))--}} +{{-- @include('tabulation.auditionSeating-show-published-seats')--}} +{{-- @elseif(! $auditionComplete)--}} +{{-- @include('tabulation.auditionSeating-unable-to-seat-card')--}} +{{-- @else--}} +{{-- @include('tabulation.auditionSeating-fill-seats-form')--}} +{{-- @include('tabulation.auditionSeating-show-proposed-seats')--}} +{{-- @endif--}} + + +{{--
    --}}
    - -{{--TODO deal with unlikely scenario of a doubler is entered for seating on some auditions but not others--}} diff --git a/resources/views/tabulation/status.blade.php b/resources/views/tabulation/status.blade.php index bf594b1..55cde57 100644 --- a/resources/views/tabulation/status.blade.php +++ b/resources/views/tabulation/status.blade.php @@ -13,33 +13,26 @@ - @foreach($auditions as $audition) - @php - $percent = 100; - if($audition->seating_entries_count > 0) { - $percent = round(($audition->scored_entries_count / $audition->seating_entries_count) * 100); - } - @endphp + @foreach($auditionData as $audition) - - +
    - {{ $audition->name }} - {{ $audition->scored_entries_count }} / {{ $audition->seating_entries_count }} Scored + {{ $audition['name'] }} + {{ $audition['scoredEntriesCount'] }} / {{ $audition['totalEntriesCount'] }} Scored
    -
    +
    - @if( $audition->scored_entries_count == $audition->seating_entries_count) + @if( $audition['scoringComplete']) @endif - @if( $audition->hasFlag('seats_published')) + @if( $audition['seatsPublished']) @endif diff --git a/resources/views/test.blade.php b/resources/views/test.blade.php deleted file mode 100644 index 44f7d68..0000000 --- a/resources/views/test.blade.php +++ /dev/null @@ -1,16 +0,0 @@ -@php use App\Enums\AuditionFlags;use App\Models\Audition;use App\Models\AuditionFlag; @endphp -@php @endphp -@inject('scoreservice','App\Services\ScoreService'); -@inject('auditionService','App\Services\AuditionService'); -@inject('entryService','App\Services\EntryService') -@inject('seatingService','App\Services\SeatingService') -@inject('drawService', 'App\Services\DrawService') - - Test Page - @php - $audition = Audition::first(); - $audition->addFlag('drawn'); - @endphp - - - diff --git a/routes/tabulation.php b/routes/tabulation.php index 956ddde..2bde4eb 100644 --- a/routes/tabulation.php +++ b/routes/tabulation.php @@ -1,37 +1,46 @@ group(function () { // Score Management - Route::prefix('scores/')->controller(\App\Http\Controllers\Tabulation\ScoreController::class)->group(function () { + Route::prefix('scores/')->controller(ScoreController::class)->group(function () { Route::get('/choose_entry', 'chooseEntry')->name('scores.chooseEntry'); Route::get('/entry', 'entryScoreSheet')->name('scores.entryScoreSheet'); Route::post('/entry/{entry}', 'saveEntryScoreSheet')->name('scores.saveEntryScoreSheet'); - Route::delete('/{score}', [\App\Http\Controllers\Tabulation\ScoreController::class, 'destroyScore'])->name('scores.destroy'); + Route::delete('/{score}', + [ScoreController::class, 'destroyScore'])->name('scores.destroy'); }); // Entry Flagging - Route::prefix('entry-flags/')->controller(\App\Http\Controllers\Tabulation\EntryFlagController::class)->group(function () { + Route::prefix('entry-flags/')->controller(EntryFlagController::class)->group(function () { Route::get('/choose_no_show', 'noShowSelect')->name('entry-flags.noShowSelect'); Route::get('/propose-no-show', 'noShowConfirm')->name('entry-flags.confirmNoShow'); Route::post('/no-show/{entry}', 'enterNoShow')->name('entry-flags.enterNoShow'); Route::delete('/no-show/{entry}', 'undoNoShow')->name('entry-flags.undoNoShow'); }); - // Generic Tabulation Routes - Route::prefix('tabulation/')->controller(\App\Http\Controllers\Tabulation\TabulationController::class)->group(function () { - Route::get('/status', 'status')->name('tabulation.status'); - Route::match(['get', 'post'], '/auditions/{audition}', 'auditionSeating')->name('tabulation.audition.seat'); - Route::post('/auditions/{audition}/publish-seats', 'publishSeats')->name('tabulation.seat.publish'); - Route::post('/auditions/{audition}/unpublish-seats', 'unpublishSeats')->name('tabulation.seat.unpublish'); + // Seating Routes + Route::prefix('seating/')->group(function () { + Route::get('/', SeatingStatusController::class)->name('seating.status'); + Route::match(['get', 'post'], '/{audition}', SeatAuditionFormController::class)->name('seating.audition'); + Route::post('/{audition}/publish', [SeatingPublicationController::class, 'publishSeats'])->name('seating.audition.publish'); + Route::post('/{audition}/unpublish', [SeatingPublicationController::class, 'unpublishSeats'])->name('seating.audition.unpublish'); }); // Advancement Routes - Route::prefix('advancement/')->controller(\App\Http\Controllers\Tabulation\AdvancementController::class)->group(function () { + Route::prefix('advancement/')->controller(AdvancementController::class)->group(function () { Route::get('/status', 'status')->name('advancement.status'); Route::get('/{audition}', 'ranking')->name('advancement.ranking'); Route::post('/{audition}', 'setAuditionPassers')->name('advancement.setAuditionPassers'); diff --git a/tests/Feature/Actions/CalculateEntryScore/AllJudgesCountTest.php b/tests/Feature/Actions/CalculateEntryScore/AllJudgesCountTest.php new file mode 100644 index 0000000..17c1dbd --- /dev/null +++ b/tests/Feature/Actions/CalculateEntryScore/AllJudgesCountTest.php @@ -0,0 +1,141 @@ +calculate('WRONG', Entry::factory()->create()); +})->throws(TabulationException::class, 'Mode must be seating or advancement'); + +it('throws an exception if entry is not valid', function () { + // Arrange + #$calculator = new AllJudgesCount(); + $calculator = App::make(AllJudgesCount::class); + // Act + $calculator->calculate('seating', Entry::factory()->make()); + // Assert +})->throws(TabulationException::class, 'Invalid entry specified'); +it('throws an exception if entry is missing judge scores', function () { + // Arrange + loadSampleAudition(); + $judge1 = User::factory()->create(); + $judge2 = User::factory()->create(); + Room::find(1000)->addJudge($judge1); + Room::find(1000)->addJudge($judge2); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $scores = [ + 1001 => 50, + 1002 => 60, + 1003 => 70, + 1004 => 80, + 1005 => 90, + ]; + #$calculator = new AllJudgesCount(); + $calculator = App::make(AllJudgesCount::class); + enterScore($judge1, $entry, $scores); + // Act + $calculator->calculate('seating', $entry); + // Assert +})->throws(TabulationException::class, 'Not all score sheets are in'); + +it('throws an exception if a score exists from an invalid judge', function () { + // Arrange + loadSampleAudition(); + $judge1 = User::factory()->create(); + $judge2 = User::factory()->create(); + $judge3 = User::factory()->create(); + Room::find(1000)->addJudge($judge1); + Room::find(1000)->addJudge($judge2); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $scores = [ + 1001 => 50, + 1002 => 60, + 1003 => 70, + 1004 => 80, + 1005 => 90, + ]; + #$calculator = new AllJudgesCount(); + $calculator = App::make(AllJudgesCount::class); + enterScore($judge1, $entry, $scores); + $scoreSheetToSpoof = enterScore($judge2, $entry, $scores); + $scoreSheetToSpoof->update(['user_id' => $judge3->id]); + // Act + $calculator->calculate('seating', $entry); + // Assert +})->throws(TabulationException::class, 'Score exists from a judge not assigned to this audition'); + +it('correctly calculates scores for seating', function () { + // Arrange + loadSampleAudition(); + $judge1 = User::factory()->create(); + $judge2 = User::factory()->create(); + Room::find(1000)->addJudge($judge1); + Room::find(1000)->addJudge($judge2); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $scores = [ + 1001 => 50, + 1002 => 60, + 1003 => 70, + 1004 => 80, + 1005 => 90, + ]; + $scores2 = [ + 1001 => 55, + 1002 => 65, + 1003 => 75, + 1004 => 85, + 1005 => 95, + ]; + #$calculator = new AllJudgesCount(); + $calculator = App::make(AllJudgesCount::class); + enterScore($judge1, $entry, $scores); + enterScore($judge2, $entry, $scores2); + // Act + $finalScores = $calculator->calculate('seating', $entry); + // Assert + $expectedScores = [142.5, 165, 125, 145, 105]; + expect($finalScores)->toBe($expectedScores); +}); + +it('correctly calculates scores for advancement', function () { + // Arrange + loadSampleAudition(); + $judge1 = User::factory()->create(); + $judge2 = User::factory()->create(); + Room::find(1000)->addJudge($judge1); + Room::find(1000)->addJudge($judge2); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $scores = [ + 1001 => 50, + 1002 => 60, + 1003 => 70, + 1004 => 80, + 1005 => 90, + ]; + $scores2 = [ + 1001 => 55, + 1002 => 65, + 1003 => 75, + 1004 => 85, + 1005 => 95, + ]; + $calculator = App::make(AllJudgesCount::class); + enterScore($judge1, $entry, $scores); + enterScore($judge2, $entry, $scores2); + // Act + $finalScores = $calculator->calculate('advancement', $entry); + // Assert + $expectedScores = [152.5, 185, 165, 125, 145]; + expect($finalScores)->toBe($expectedScores); +}); diff --git a/tests/Feature/Actions/CalculateScoreSheetTotalTest.php b/tests/Feature/Actions/CalculateScoreSheetTotalTest.php new file mode 100644 index 0000000..a3ce1c1 --- /dev/null +++ b/tests/Feature/Actions/CalculateScoreSheetTotalTest.php @@ -0,0 +1,77 @@ +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 = app(CalculateScoreSheetTotal::class); + $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 = app(CalculateScoreSheetTotal::class); + $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 + loadSampleAudition(); + $judge = User::factory()->create(); + Room::find(1000)->addJudge($judge); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Artisan::call('cache:clear'); + $calculator = app(CalculateScoreSheetTotal::class); + // Act + $calculator('seating', $entry, $judge); + //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 = app(CalculateScoreSheetTotal::class); + $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 = app(CalculateScoreSheetTotal::class); + $total = $calculator('advancement', $entry, $judge); + expect($total[0])->toBe(73.75); + $expectedArray = [73.75, 90, 80, 60, 70]; + expect($total)->toBe($expectedArray); +}); diff --git a/tests/Feature/Actions/EnterScoreTest.php b/tests/Feature/Actions/EnterScoreTest.php new file mode 100644 index 0000000..b9dd140 --- /dev/null +++ b/tests/Feature/Actions/EnterScoreTest.php @@ -0,0 +1,187 @@ +scoreEntry = new EnterScore(); + $this->scoreEntry = App::make(EnterScore::class); +}); + +test('throws an exception if the user does not exist', function () { + $user = User::factory()->make(); + $entry = Entry::factory()->create(); + enterScore($user, $entry, []); +})->throws(ScoreEntryException::class, 'User does not exist'); +test('throws an exception if the entry does not exist', function () { + $user = User::factory()->create(); + $entry = Entry::factory()->make(); + enterScore($user, $entry, []); +})->throws(ScoreEntryException::class, 'Entry does not exist'); +it('throws an exception if the seats for the entries audition are published', function () { + // Arrange + $user = User::factory()->create(); + $entry = Entry::factory()->create(); + $entry->audition->addFlag('seats_published'); + // Act & Assert + enterScore($user, $entry, []); +})->throws(ScoreEntryException::class, 'Cannot score an entry in an audition with published seats'); +it('throws an exception if the advancement for the entries audition is published', function () { + // Arrange + $user = User::factory()->create(); + $entry = Entry::factory()->create(); + $entry->audition->addFlag('advancement_published'); + // Act & Assert + enterScore($user, $entry, []); +})->throws(ScoreEntryException::class, 'Cannot score an entry in an audition with published advancement'); +it('throws an exception if too many scores are submitted', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Room::find(1000)->addJudge($judge); + // Act & Assert + enterScore($judge, $entry, [1, 2, 5, 3, 2, 1]); +})->throws(ScoreEntryException::class, 'Invalid number of scores'); +it('throws an exception if too few scores are submitted', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Room::find(1000)->addJudge($judge); + // Act & Assert + enterScore($judge, $entry, [1, 2, 5, 3]); +})->throws(ScoreEntryException::class, 'Invalid number of scores'); +it('throws an exception if the user is not assigned to judge the entry', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + // Act & Assert + enterScore($judge, $entry, [1, 2, 5, 3, 7]); +})->throws(ScoreEntryException::class, 'This judge is not assigned to judge this entry'); +it('throws an exception if an invalid subscore is provided', function () { + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1006 => 88, + ]; + enterScore($judge, $entry, $scores); +})->throws(ScoreEntryException::class, 'Invalid Score Submission'); +it('throws an exception if a submitted subscore exceeds the maximum allowed', function () { + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1005 => 101, + ]; + enterScore($judge, $entry, $scores); +})->throws(ScoreEntryException::class, 'Supplied subscore exceeds maximum allowed'); +it('removes an existing no_show flag from the entry if one exists', function () { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $entry->addFlag('no_show'); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1005 => 98, + ]; + // Act + enterScore($judge, $entry, $scores); + // Assert + expect($entry->hasFlag('no_show'))->toBeFalse(); +}); +it('saves the score with a properly formatted subscore object', function () { + // Arrange + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $entry->addFlag('no_show'); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1005 => 98, + ]; + $desiredReturn = [ + 1001 => [ + 'score' => 98, + 'subscore_id' => 1001, + 'subscore_name' => 'Scale', + ], + 1002 => [ + 'score' => 90, + 'subscore_id' => 1002, + 'subscore_name' => 'Etude 1', + ], + 1003 => [ + 'score' => 87, + 'subscore_id' => 1003, + 'subscore_name' => 'Etude 2', + ], + 1004 => [ + 'score' => 78, + 'subscore_id' => 1004, + 'subscore_name' => 'Sight Reading', + ], + 1005 => [ + 'score' => 98, + 'subscore_id' => 1005, + 'subscore_name' => 'Tone', + ], + ]; + // Act + $newScore = enterScore($judge, $entry, $scores); + // Assert + $checkScoreSheet = ScoreSheet::find($newScore->id); + expect($checkScoreSheet->exists())->toBeTrue() + ->and($checkScoreSheet->subscores)->toBe($desiredReturn); +}); +it('throws an exception of the entry already has a score by the judge', function() { + // Arrange + loadSampleAudition(); + $judge = User::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => 1000]); + $entry->addFlag('no_show'); + Room::find(1000)->addJudge($judge); + $scores = [ + 1001 => 98, + 1002 => 90, + 1003 => 87, + 1004 => 78, + 1005 => 98, + ]; + // Act + enterScore($judge, $entry, $scores); + // Assert + enterScore($judge, $entry, $scores); +})->throws(ScoreEntryException::class, 'That judge has already entered scores for that entry'); diff --git a/tests/Feature/Actions/RankAuditionEntriesTest.php b/tests/Feature/Actions/RankAuditionEntriesTest.php new file mode 100644 index 0000000..6ced42e --- /dev/null +++ b/tests/Feature/Actions/RankAuditionEntriesTest.php @@ -0,0 +1,68 @@ +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()); + $ranker = App::make(RankAuditionEntries::class); + // 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()); + $ranker = App::make(RankAuditionEntries::class); + // 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 + Artisan::call('cache:clear'); + 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); + Artisan::call('cache:clear'); + $ranker = App::make(RankAuditionEntries::class); + $expectedOrder = [4, 1, 3, 5, 2]; + // Act + $return = $ranker->rank('seating', Audition::find(1000)); + // Assert + expect($return->pluck('id')->toArray())->toBe($expectedOrder); +}); diff --git a/tests/Feature/Models/EntryTest.php b/tests/Feature/Models/EntryTest.php index c0e21a9..4d30f30 100644 --- a/tests/Feature/Models/EntryTest.php +++ b/tests/Feature/Models/EntryTest.php @@ -169,6 +169,7 @@ it('has a forAdvancement scope that only returns those entries entered for advan Entry::factory()->count(10)->create(['for_seating' => true, 'for_advancement' => true]); Entry::factory()->count(5)->create(['for_seating' => false, 'for_advancement' => true]); // Act & Assert + expect(Entry::forAdvancement()->count())->toBe(16) ->and(Entry::forAdvancement()->get()->first())->toBeInstanceOf(Entry::class) ->and(Entry::forAdvancement()->get()->first()->student->first_name)->toBe('Advance Only'); diff --git a/tests/Feature/Pages/Admin/SchoolsIndexTest.php b/tests/Feature/Pages/Admin/SchoolsIndexTest.php index bc05450..d7d8e9a 100644 --- a/tests/Feature/Pages/Admin/SchoolsIndexTest.php +++ b/tests/Feature/Pages/Admin/SchoolsIndexTest.php @@ -4,9 +4,12 @@ use App\Models\Entry; use App\Models\School; use App\Models\Student; use App\Models\User; +use App\Services\EntryService; +use App\Services\Invoice\InvoiceOneFeePerEntry; use App\Settings; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\App; use function Pest\Laravel\actingAs; use function Pest\Laravel\get; @@ -62,7 +65,8 @@ it('has a new school link', function () { }); it('shows school data', function () { // Arrange - $invoiceDataService = new App\Services\Invoice\InvoiceOneFeePerEntry(new App\Services\EntryService(new App\Services\AuditionService())); + #$invoiceDataService = new App\Services\Invoice\InvoiceOneFeePerEntry(new App\Services\EntryService(new App\Services\AuditionService())); + $invoiceDataService = App::make(InvoiceOneFeePerEntry::class); Settings::set('school_fees', 1000); Settings::set('late_fee', 2500); actingAs($this->adminUser); diff --git a/tests/Feature/Pages/Advancement/statusTest.php b/tests/Feature/Pages/Advancement/statusTest.php new file mode 100644 index 0000000..c83969e --- /dev/null +++ b/tests/Feature/Pages/Advancement/statusTest.php @@ -0,0 +1,41 @@ +assertRedirect(route('home')); + actingAs(User::factory()->create()); + get(route('advancement.status')) + ->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); +}); +it('responds to an admin or tab user', function () { + actAsAdmin(); + get(route('advancement.status')) + ->assertOk(); + actAsTab(); + get(route('advancement.status')) + ->assertOk(); +}); +it('includes advancement auditions', function () { + $audition = Audition::factory()->create(); + actAsAdmin(); + get(route('advancement.status')) + ->assertOk() + ->assertSee($audition->name); +}); +it('does not include auditions not for advancement', function () { + $audition = Audition::factory()->seatingOnly()->create(); + actAsAdmin(); + get(route('advancement.status')) + ->assertOk() + ->assertDontSee($audition->name); +}); diff --git a/tests/Feature/Pages/EntriesIndexTest.php b/tests/Feature/Pages/EntriesIndexTest.php index e8b2d20..fd47344 100644 --- a/tests/Feature/Pages/EntriesIndexTest.php +++ b/tests/Feature/Pages/EntriesIndexTest.php @@ -48,7 +48,7 @@ it('has appropriate students in JS array for select', function () { 'id: ', $student->id, 'name: ', - $student->full_name(true), + e($student->full_name(true)), ], false); // The false parameter makes the assertion case-sensitive and allows for HTML tags }); diff --git a/tests/Feature/Pages/Seating/auditionSeatingTest.php b/tests/Feature/Pages/Seating/auditionSeatingTest.php new file mode 100644 index 0000000..4858a9c --- /dev/null +++ b/tests/Feature/Pages/Seating/auditionSeatingTest.php @@ -0,0 +1,73 @@ +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(); +}); +// TODO make tests with varied information +it('returns the audition object and an array of info on each entry', function () { + // Arrange + $entry = Entry::factory()->create(['audition_id' => $this->audition->id]); + actAsAdmin(); + // Act + $response = get($this->r); + $response + ->assertOk() + ->assertViewHas('audition', $this->audition); + $viewData = $response->viewData('entryData'); + expect($viewData[0]['rank'])->toBe(1); + expect($viewData[0]['id'])->toBe($entry->id); + expect($viewData[0]['studentName'])->toBe($entry->student->full_name()); + expect($viewData[0]['schoolName'])->toBe($entry->student->school->name); + expect($viewData[0]['drawNumber'])->toBe($entry->draw_number); + expect($viewData[0]['totalScore'])->toBe('No Score'); + expect($viewData[0]['fullyScored'])->toBeFalse(); +}); +it('identifies a doubler', function () { + // Arrange + $audition1 = Audition::factory()->create(['event_id' => $this->audition->event_id]); + $audition2 = Audition::factory()->create(['event_id' => $this->audition->event_id]); + $student = Student::factory()->create(); + Entry::factory()->create(['audition_id' => $audition1->id, 'student_id' => $student->id]); + Entry::factory()->create(['audition_id' => $audition2->id, 'student_id' => $student->id]); + Entry::factory()->create(['audition_id' => $this->audition->id, 'student_id' => $student->id]); + actAsAdmin(); + // Act & Assert + $response = get($this->r); + $response->assertOk(); + $viewData = $response->viewData('entryData'); + expect($viewData[0]['doubleData'])->toBeTruthy(); +}); diff --git a/tests/Feature/Pages/Seating/statusTest.php b/tests/Feature/Pages/Seating/statusTest.php new file mode 100644 index 0000000..686da05 --- /dev/null +++ b/tests/Feature/Pages/Seating/statusTest.php @@ -0,0 +1,162 @@ +assertRedirect(route('home')); + actAsNormal(); + get(route('seating.status')) + ->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); +}); +it('responds to an admin', function () { + // Arrange + actAsAdmin(); + // Act & Assert + get(route('seating.status')) + ->assertOk(); +}); +it('responds to a tabulator', function () { + // Arrange + actAsTab(); + // Act & Assert + get(route('seating.status')) + ->assertOk(); +}); +it('sends the view a collection of audition data that includes data needed by the view', function () { + // Arrange + $auditions = Audition::factory()->count(5)->create(); + actAsAdmin(); + // Act + $response = get(route('seating.status')); + // Assert + foreach ($auditions as $audition) { + $response->assertOk() + ->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['id'] === $audition->id; + }); + $response->assertOk() + ->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['name'] === $audition->name; + }); + } +}); +it('has correct count info for an audition with 5 entries none scored', function () { + $audition = Audition::factory()->create(); + Entry::factory()->count(5)->create(['audition_id' => $audition->id]); + + actAsAdmin(); + $response = get(route('seating.status')); + $response->assertOk(); + + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoredEntriesCount'] === 0; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['totalEntriesCount'] === 5; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoredPercentage'] === 0; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoringComplete'] === false; + }); +}); +it('has correct count info for an audition with 8 entries 2 scored', function () { + $judge = User::factory()->create(); + + $audition = Audition::factory()->create(); + $entries = Entry::factory()->count(2)->create(['audition_id' => $audition->id]); + $entries->each(fn ($entry) => ScoreSheet::create([ + 'user_id' => $judge->id, + 'entry_id' => $entry->id, + 'subscores' => 7, + ])); + Entry::factory()->count(6)->create(['audition_id' => $audition->id]); + + actAsAdmin(); + $response = get(route('seating.status')); + $response->assertOk(); + + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoredEntriesCount'] === 2; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['totalEntriesCount'] === 8; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoredPercentage'] == 25; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoringComplete'] === false; + }); +}); + +it('has correct count info for an audition with 1 entries 1 scored', function () { + $judge = User::factory()->create(); + + $audition = Audition::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => $audition->id]); + ScoreSheet::create([ + 'user_id' => $judge->id, + 'entry_id' => $entry->id, + 'subscores' => 3, + ]); + + actAsAdmin(); + $response = get(route('seating.status')); + $response->assertOk(); + + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoredEntriesCount'] === 1; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['totalEntriesCount'] === 1; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoredPercentage'] == 100; + }); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['scoringComplete'] === true; + }); +}); + +it('correctly shows a flag when the audition is flagged as seated', function () { + $audition = Audition::factory()->create(); + + actAsAdmin(); + $response = get(route('seating.status')); + $response->assertOk(); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['seatsPublished'] === false; + }); + $audition->addFlag('seats_published'); + $response = get(route('seating.status')); + $response->assertOk(); + $response->assertViewHas('auditionData', function ($viewAuditionData) use ($audition) { + return $viewAuditionData[$audition->id]['seatsPublished'] === true; + }); +}); +it('shows seating auditions', function() { + $audition = Audition::factory()->create(); + actAsAdmin(); + get(route('seating.status')) + ->assertOk() + ->assertSee($audition->name); +}); +it('does not show advancement only auditions', function() { + $audition = Audition::factory()->advancementOnly()->create(); + actAsAdmin(); + get(route('seating.status')) + ->assertOk() + ->assertDontSee($audition->name); +}); diff --git a/tests/Feature/Pages/Setup/DrawStoreTest.php b/tests/Feature/Pages/Setup/DrawStoreTest.php index ad3e63f..44cfe60 100644 --- a/tests/Feature/Pages/Setup/DrawStoreTest.php +++ b/tests/Feature/Pages/Setup/DrawStoreTest.php @@ -4,6 +4,7 @@ use App\Models\Audition; use App\Models\Entry; use App\Services\DrawService; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\App; uses(RefreshDatabase::class); @@ -77,7 +78,8 @@ it('sets the draw_number column on each entry in the audition based on the rando // Arrange $audition = Audition::factory()->hasEntries(10)->create(); Entry::all()->each(fn ($entry) => expect($entry->draw_number)->toBeNull()); - $drawService = new DrawService(); + #$drawService = new DrawService(); + $drawService = App::make(DrawService::class); $drawService->runOneDraw($audition); // Act & Assert Entry::all()->each(fn ($entry) => expect($entry->draw_number)->not()->toBeNull()); @@ -86,7 +88,8 @@ it('only sets draw numbers in the specified audition', function () { // Arrange $audition = Audition::factory()->hasEntries(10)->create(); $bonusEntry = Entry::factory()->create(); - $drawService = new DrawService(); + #$drawService = new DrawService(); + $drawService = App::make(DrawService::class); $drawService->runOneDraw($audition); // Act & Assert expect($bonusEntry->draw_number)->toBeNull(); diff --git a/tests/Feature/Services/AuditionServiceTest.php b/tests/Feature/Services/AuditionServiceTest.php new file mode 100644 index 0000000..c707ed0 --- /dev/null +++ b/tests/Feature/Services/AuditionServiceTest.php @@ -0,0 +1,65 @@ +expectException(\App\Exceptions\AuditionServiceException::class); + $auditionService->getSubscores(new Audition(), 'invalid_mode'); +}); +it('throws an exception when an invalid sort is requested', function () { + // Arrange + //$auditionService = new \App\Services\AuditionService(); + $auditionService = App::make(AuditionService::class); + $this->expectException(\App\Exceptions\AuditionServiceException::class); + // Act + $auditionService->getSubscores(new Audition(), 'seating', 'invalid_sort'); +}); +it('throws an exception when an invalid audition is provided', function () { + // Arrange + //$auditionService = new \App\Services\AuditionService(); + $auditionService = App::make(AuditionService::class); + $this->expectException(\App\Exceptions\AuditionServiceException::class); + $auditionService->getSubscores(new Audition(), 'seating', 'tiebreak'); + // Act & Assert + +}); +it('gets subscores for an audition', function () { + // Arrange + loadSampleAudition(); + //$auditionService = new \App\Services\AuditionService(); + $auditionService = App::make(AuditionService::class); + // Act + $subscores = $auditionService->getSubscores(Audition::find(1000), 'seating', 'tiebreak'); + // Assert + expect($subscores->toArray())->toBe(Audition::find(1000)->scoringGuide->subscores->where('for_seating', + true)->sortBy('tiebreak_order')->toArray()); +}); +// getJudges() +it('gets judges for an audition', function () { + loadSampleAudition(); + $auditionService = App::make(AuditionService::class); + $judge = User::factory()->create(); + $notJudge = User::factory()->create(); + Room::find(1000)->addJudge($judge); + $testValue = $auditionService->getJudges(Audition::find(1000)); + $test = $testValue->contains(function ($item) use ($judge) { + return $item->id === $judge->id; + }); + $negativeTest = $testValue->contains(function ($item) use ($notJudge) { + return $item->id === $notJudge->id; + }); + expect($test)->toBeTrue(); + expect($negativeTest)->toBeFalse(); +}); diff --git a/tests/Feature/Services/DoublerServiceTest.php b/tests/Feature/Services/DoublerServiceTest.php new file mode 100644 index 0000000..691b5aa --- /dev/null +++ b/tests/Feature/Services/DoublerServiceTest.php @@ -0,0 +1,78 @@ +doublerService = App::make(DoublerService::class); +}); + +it('throws an error if an invalid event is provided', function () { + $event = Event::factory()->make(); + $this->doublerService->doublersForEvent($event); +})->throws(TabulationException::class, 'Invalid event provided'); +it('returns doublers for an event', function () { + $concertEvent = Event::factory()->create(['name' => 'Concert Band', 'id' => 1000]); + $jazzEvent = Event::factory()->create(['name' => 'Jazz Band', 'id' => 1001]); + Audition::factory()->create([ + 'event_id' => 1000, 'name' => 'Alto Sax', 'minimum_grade' => 7, 'maximum_grade' => 12, 'id' => 1000, + ]); + Audition::factory()->create([ + 'event_id' => 1000, 'name' => 'Tenor Sax', 'minimum_grade' => 7, 'maximum_grade' => 12, 'id' => 1001, + ]); + Audition::factory()->create([ + 'event_id' => 1000, 'name' => 'Baritone Sax', 'minimum_grade' => 7, 'maximum_grade' => 12, 'id' => 1002, + ]); + Audition::factory()->create([ + 'event_id' => 1000, 'name' => 'Clarinet', 'minimum_grade' => 7, 'maximum_grade' => 12, 'id' => 1003, + ]); + Audition::factory()->create([ + 'event_id' => 1000, 'name' => 'Bass Clarinet', + 'minimum_grade' => 7, 'maximum_grade' => 12, 'id' => 1004, + ]); + Audition::factory()->create([ + 'event_id' => 1001, 'name' => 'Jazz Alto', 'minimum_grade' => 7, + 'maximum_grade' => 12, 'id' => 1005, + ]); + Audition::factory()->create([ + 'event_id' => 1001, 'name' => 'Jazz Tenor', 'minimum_grade' => 7, + 'maximum_grade' => 12, 'id' => 1006, + ]); + Audition::factory()->create([ + 'event_id' => 1001, 'name' => 'Jazz Baritone', + 'minimum_grade' => 7, 'maximum_grade' => 12, 'id' => 1007, + ]); + $allSaxDude = Student::factory()->create(['grade' => 11, 'id' => 1000]); + Student::factory()->create(['grade' => 9, 'id' => 1001]); + Student::factory()->create(['grade' => 9, 'id' => 1002]); + Entry::create(['student_id' => 1000, 'audition_id' => 1000]); + Entry::create(['student_id' => 1000, 'audition_id' => 1001]); + Entry::create(['student_id' => 1000, 'audition_id' => 1002]); + Entry::create(['student_id' => 1000, 'audition_id' => 1005]); + Entry::create(['student_id' => 1000, 'audition_id' => 1006]); + Entry::create(['student_id' => 1000, 'audition_id' => 1007]); + Entry::create(['student_id' => 1001, 'audition_id' => 1003]); + Entry::create(['student_id' => 1001, 'audition_id' => 1004]); + Entry::create(['student_id' => 1002, 'audition_id' => 1000]); + Entry::create(['student_id' => 1002, 'audition_id' => 1005]); + + $return = $this->doublerService->doublersForEvent($concertEvent); + expect(count($return))->toBe(2) + ->and($return[1000]['student_id'])->toBe($allSaxDude->id) + ->and($return[1000]['entries']->count())->toBe(3) + ->and($return[1001]['entries']->count())->toBe(2); + assertArrayNotHasKey(1002, $return); + $return = $this->doublerService->doublersForEvent($jazzEvent); + expect(count($return))->toBe(1) + ->and($return[1000]['student_id'])->toBe($allSaxDude->id) + ->and($return[1000]['entries']->count())->toBe(3); +}); diff --git a/tests/Feature/Services/EntryServiceTest.php b/tests/Feature/Services/EntryServiceTest.php new file mode 100644 index 0000000..f26245f --- /dev/null +++ b/tests/Feature/Services/EntryServiceTest.php @@ -0,0 +1,24 @@ +entryService = App::make(EntryService::class); +}); + +it('checks if an entry is late', function() { + $openAudition = Audition::factory()->create(['entry_deadline' => Carbon::tomorrow()]); + $closedAudition = Audition::factory()->create(['entry_deadline' => Carbon::yesterday()]); + + $onTime = Entry::factory()->create(['audition_id' => $openAudition->id]); + $late = Entry::factory()->create(['audition_id' => $closedAudition]); + expect($this->entryService->isEntryLate($onTime))->toBeFalse(); + expect($this->entryService->isEntryLate($late))->toBeTrue(); +}); diff --git a/tests/Feature/Services/ScoreServiceTest.php b/tests/Feature/Services/ScoreServiceTest.php new file mode 100644 index 0000000..df9d3e3 --- /dev/null +++ b/tests/Feature/Services/ScoreServiceTest.php @@ -0,0 +1,38 @@ +scoreService = new ScoreService(); + $this->scoreService = App::make(ScoreService::class); +}); + +it('can check if an entry is fully scored', function () { + $room = Room::factory()->create(); + $judges = User::factory()->count(2)->create(); + $judges->each(fn ($judge) => $room->addJudge($judge)); + $audition = Audition::factory()->create(['room_id' => $room->id]); + $entry = Entry::factory()->create(['audition_id' => $audition->id]); + expect($this->scoreService->isEntryFullyScored($entry))->toBeFalse(); + ScoreSheet::create([ + 'user_id' => $judges->first()->id, + 'entry_id' => $entry->id, + 'subscores' => 7, + ]); + expect($this->scoreService->isEntryFullyScored($entry))->toBeFalse(); + ScoreSheet::create([ + 'user_id' => $judges->last()->id, + 'entry_id' => $entry->id, + 'subscores' => 7, + ]); + expect($this->scoreService->isEntryFullyScored($entry))->toBeTrue(); +}); diff --git a/tests/Feature/Services/UserServiceTest.php b/tests/Feature/Services/UserServiceTest.php new file mode 100644 index 0000000..bab8fe8 --- /dev/null +++ b/tests/Feature/Services/UserServiceTest.php @@ -0,0 +1,19 @@ +userService = App::make(UserService::class); +}); + +it('checks if a user exists', function() { + $realUser = User::factory()->create(); + $fakeUser = User::factory()->make(); + expect ($this->userService->userExists($realUser))->toBeTrue(); + expect ($this->userService->userExists($fakeUser))->toBeFalse(); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 6cd9f4c..a07cc90 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -15,6 +15,7 @@ use App\Models\User; use App\Settings; use Illuminate\Foundation\Testing\TestCase; use function Pest\Laravel\actingAs; +use function Pest\Laravel\artisan; uses( Tests\TestCase::class, @@ -59,6 +60,10 @@ function actAsNormal() { actingAs(User::factory()->create()); } +function loadSampleAudition() +{ + artisan('db:seed', ['--class' => 'AuditionWithScoringGuideAndRoom']); +} uses()->beforeEach(function () { Settings::set('auditionName', 'Somewhere Band Directors Association');