diff --git a/app/Actions/Tabulation/CheckPrelimResult.php b/app/Actions/Tabulation/CheckPrelimResult.php
new file mode 100644
index 0000000..3317196
--- /dev/null
+++ b/app/Actions/Tabulation/CheckPrelimResult.php
@@ -0,0 +1,61 @@
+removeFlag('passed_prelim');
+ $entry->removeFlag('failed_prelim');
+ }
+
+ if (! $entry->exists) {
+ throw new AuditionAdminException('Entry does not exist');
+ }
+
+ if (! $entry->audition->prelimDefinition) {
+ throw new AuditionAdminException('Entry does not have a prelim');
+ }
+
+ if ($entry->hasFlag('failed_prelim') || $entry->hasFlag('passed_prelim')) {
+ return 'noChange';
+ }
+
+ if (! $entry->audition->prelimDefinition->room || $entry->audition->prelimDefinition->room->judges()->count() == 0) {
+ return 'noJudgesAssigned';
+ }
+
+ $scoresRequired = $entry->audition->prelimDefinition->room->judges()->count();
+ $scoresAssigned = $entry->prelimScoreSheets()->count();
+ if ($scoresAssigned < $scoresRequired) {
+ return 'missing'.$scoresRequired - $scoresAssigned.'scores';
+ }
+
+ $totalScore = 0;
+ foreach ($entry->prelimScoreSheets as $scoreSheet) {
+ $totalScore += $scoreSheet->total;
+ }
+ $averageScore = $totalScore / $scoresAssigned;
+ if ($averageScore >= $entry->audition->prelimDefinition->passing_score) {
+ $entry->addFlag('passed_prelim');
+
+ return 'markedPassed';
+ } else {
+ $entry->addFlag('failed_prelim');
+
+ return 'markedFailed';
+ }
+ }
+}
diff --git a/app/Actions/Tabulation/EnterPrelimScore.php b/app/Actions/Tabulation/EnterPrelimScore.php
new file mode 100644
index 0000000..76536e3
--- /dev/null
+++ b/app/Actions/Tabulation/EnterPrelimScore.php
@@ -0,0 +1,137 @@
+id)->exists()) {
+ throw new AuditionAdminException('User does not exist');
+ }
+ if (! Entry::where('id', $entry->id)->exists()) {
+ throw new AuditionAdminException('Entry does not exist');
+ }
+ if ($entry->audition->hasFlag('seats_published')) {
+ throw new AuditionAdminException('Cannot score an entry in an audition where seats are published');
+ }
+
+ // Check if the entries audition has a prelim definition
+ if (! PrelimDefinition::where('audition_id', $entry->audition->id)->exists()) {
+ throw new AuditionAdminException('The entries audition does not have a prelim');
+ }
+ $prelimDefinition = PrelimDefinition::where('audition_id', $entry->audition->id)->first();
+
+ // Don't allow changes to prelims scores if the entry has a finals score
+ if ($entry->scoreSheets()->count() > 0) {
+ throw new AuditionAdminException('Cannot change prelims scores for an entry that has finals scores');
+ }
+
+ // Check that the specified user is assigned to judge this entry in prelims
+ $check = DB::table('room_user')
+ ->where('user_id', $user->id)
+ ->where('room_id', $prelimDefinition->room_id)->exists();
+ if (! $check) {
+ throw new AuditionAdminException('This judge is not assigned to judge this entry in prelims');
+ }
+
+ // Check if a score already exists
+ if (! $prelimScoreSheet) {
+ if (PrelimScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
+ throw new AuditionAdminException('That judge has already entered a prelim score for that entry');
+ }
+ } else {
+ if ($prelimScoreSheet->user_id != $user->id) {
+ throw new AuditionAdminException('Existing score sheet is from a different judge');
+ }
+ if ($prelimScoreSheet->entry_id != $entry->id) {
+ throw new AuditionAdminException('Existing score sheet is for a different entry');
+ }
+ }
+
+ // Check the validity of submitted subscores, format array for storage, and sum score
+ $subscoresRequired = $prelimDefinition->scoringGuide->subscores;
+ $subscoresStorageArray = [];
+ $totalScore = 0;
+ $maxPossibleTotal = 0;
+ if ($scores->count() !== $subscoresRequired->count()) {
+ throw new AuditionAdminException('Invalid number of scores');
+ }
+
+ 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 AuditionAdminException('Invalid Score Submission');
+ }
+
+ if ($scores[$subscore->id] > $subscore->max_score) {
+ throw new AuditionAdminException('Supplied subscore exceeds maximum allowed');
+ }
+
+ // Add subscore to the storage array
+ $subscoresStorageArray[$subscore->id] = [
+ 'score' => $scores[$subscore->id],
+ 'subscore_id' => $subscore->id,
+ 'subscore_name' => $subscore->name,
+ ];
+
+ // Multiply subscore by weight then add to total
+ $totalScore += ($subscore->weight * $scores[$subscore->id]);
+ $maxPossibleTotal += ($subscore->weight * $subscore->max_score);
+ }
+ $finalTotalScore = ($maxPossibleTotal === 0) ? 0 : (($totalScore / $maxPossibleTotal) * 100);
+
+ $entry->removeFlag('no_show');
+ if ($prelimScoreSheet instanceof PrelimScoreSheet) {
+ $prelimScoreSheet->update([
+ 'user_id' => $user->id,
+ 'entry_id' => $entry->id,
+ 'subscores' => $subscoresStorageArray,
+ 'total' => $finalTotalScore,
+ ]);
+ } else {
+ $prelimScoreSheet = PrelimScoreSheet::create([
+ 'user_id' => $user->id,
+ 'entry_id' => $entry->id,
+ 'subscores' => $subscoresStorageArray,
+ 'total' => $finalTotalScore,
+ ]);
+ }
+
+ // Log the prelim score entry
+ $log_message = 'Entered prelim score for entry id '.$entry->id.'.
';
+ $log_message .= 'Judge: '.$user->full_name().'
';
+ foreach ($prelimScoreSheet->subscores as $subscore) {
+ $log_message .= $subscore['subscore_name'].': '.$subscore['score'].'
';
+ }
+ $log_message .= 'Total :'.$prelimScoreSheet->total.'
';
+ auditionLog($log_message, [
+ 'entries' => [$entry->id],
+ 'users' => [$user->id],
+ 'auditions' => [$entry->audition_id],
+ 'students' => [$entry->student_id],
+ ]);
+
+ // Check if we can make a status decision
+ $checker = app(CheckPrelimResult::class);
+ $checker($entry, true);
+
+ return $prelimScoreSheet;
+ }
+}
diff --git a/app/Console/Commands/fictionalize.php b/app/Console/Commands/fictionalize.php
index 4ad0984..cc1e0c4 100644
--- a/app/Console/Commands/fictionalize.php
+++ b/app/Console/Commands/fictionalize.php
@@ -8,48 +8,80 @@ use App\Models\User;
use Faker\Factory;
use Illuminate\Console\Command;
-/**
- * @codeCoverageIgnore
- */
class fictionalize extends Command
{
- /**
- * The name and signature of the console command.
- *
- * @var string
- */
- protected $signature = 'audition:fictionalize';
+ protected $signature = 'audition:fictionalize
+ {--students : Fictionalize student names}
+ {--schools : Fictionalize school names}
+ {--users : Fictionalize user data}
+ {--all : Fictionalize all data types}';
- /**
- * The console command description.
- *
- * @var string
- */
- protected $description = 'Command description';
+ protected $description = 'Replace real names with fictional data for specified entity types';
- /**
- * Execute the console command.
- */
public function handle()
{
$faker = Factory::create();
- foreach (Student::all() as $student) {
- $student->first_name = $faker->firstName();
- $student->last_name = $faker->lastName();
- $student->save();
+
+ // If no options are specified or --all is used, process everything
+ $processAll = $this->option('all') ||
+ (! $this->option('students') && ! $this->option('schools') && ! $this->option('users'));
+
+ if ($processAll || $this->option('students')) {
+ $this->info('Fictionalizing students...');
+ $bar = $this->output->createProgressBar(Student::count());
+
+ Student::chunk(100, function ($students) use ($faker, $bar) {
+ foreach ($students as $student) {
+ $student->update([
+ 'first_name' => $faker->firstName(),
+ 'last_name' => $faker->lastName(),
+ ]);
+ $bar->advance();
+ }
+ });
+
+ $bar->finish();
+ $this->newLine();
}
- foreach (School::all() as $school) {
- $school->name = $faker->city().' High School';
- $school->save();
+ if ($processAll || $this->option('schools')) {
+ $this->info('Fictionalizing schools...');
+ $bar = $this->output->createProgressBar(School::count());
+
+ School::chunk(100, function ($schools) use ($faker, $bar) {
+ foreach ($schools as $school) {
+ $school->update([
+ 'name' => $faker->city().' High School',
+ ]);
+ $bar->advance();
+ }
+ });
+
+ $bar->finish();
+ $this->newLine();
}
- foreach (User::where('email', '!=', 'matt@mattyoung.us')->get() as $user) {
- $user->email = $faker->email();
- $user->first_name = $faker->firstName();
- $user->last_name = $faker->lastName();
- $user->cell_phone = $faker->phoneNumber();
- $user->save();
+ if ($processAll || $this->option('users')) {
+ $this->info('Fictionalizing users...');
+ $bar = $this->output->createProgressBar(User::where('email', '!=', 'matt@mattyoung.us')->count());
+
+ User::where('email', '!=', 'matt@mattyoung.us')
+ ->chunk(100, function ($users) use ($faker, $bar) {
+ foreach ($users as $user) {
+ $user->update([
+ 'email' => $faker->unique()->email(),
+ 'first_name' => $faker->firstName(),
+ 'last_name' => $faker->lastName(),
+ 'cell_phone' => $faker->phoneNumber(),
+ ]);
+ $bar->advance();
+ }
+ });
+
+ $bar->finish();
+ $this->newLine();
}
+
+ $this->info('Fictionalization complete!');
}
}
diff --git a/app/Enums/EntryFlags.php b/app/Enums/EntryFlags.php
index bb87b0d..5129f35 100644
--- a/app/Enums/EntryFlags.php
+++ b/app/Enums/EntryFlags.php
@@ -8,5 +8,6 @@ enum EntryFlags: string
case DECLINED = 'declined';
case NO_SHOW = 'no_show';
case FAILED_PRELIM = 'failed_prelim';
+ case PASSED_PRELIM = 'passed_prelim';
case LATE_FEE_WAIVED = 'late_fee_waived';
}
diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php
index c7b8cdb..dbd2fd3 100644
--- a/app/Http/Controllers/Admin/EntryController.php
+++ b/app/Http/Controllers/Admin/EntryController.php
@@ -7,6 +7,7 @@ use App\Actions\Entries\UpdateEntry;
use App\Http\Controllers\Controller;
use App\Http\Requests\EntryStoreRequest;
use App\Models\Audition;
+use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\School;
use App\Models\Seat;
@@ -139,7 +140,9 @@ class EntryController extends Controller
// TODO: When updating Laravel, can we use the chaperone method I heard about ot load the entry back into the score
$scores = $entry->scoreSheets()->with('audition', 'judge', 'entry')->get();
- return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores'));
+ $logEntries = AuditLogEntry::whereJsonContains('affected->entries', $entry->id)->orderBy('created_at', 'desc')->get();
+
+ return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores', 'logEntries'));
}
public function update(Request $request, Entry $entry, UpdateEntry $updater)
diff --git a/app/Http/Controllers/Admin/PrelimDefinitionController.php b/app/Http/Controllers/Admin/PrelimDefinitionController.php
new file mode 100644
index 0000000..a71b272
--- /dev/null
+++ b/app/Http/Controllers/Admin/PrelimDefinitionController.php
@@ -0,0 +1,70 @@
+get();
+ $rooms = Room::all();
+ $guides = ScoringGuide::all();
+ $method = 'POST';
+ $action = route('admin.prelim_definitions.store');
+ $prelim = false;
+
+ return view('admin.prelim_definitions.createOrUpdate', compact('auditions', 'rooms', 'guides', 'method', 'action', 'prelim'));
+ }
+
+ public function store(PrelimDefinitionStoreOrUpdateRequest $request)
+ {
+ $validated = $request->validated();
+ PrelimDefinition::create($validated);
+
+ return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition created');
+ }
+
+ public function edit(PrelimDefinition $prelimDefinition)
+ {
+ $auditions = Audition::doesntHave('prelimDefinition')->get();
+ $rooms = Room::all();
+ $guides = ScoringGuide::all();
+ $method = 'PATCH';
+ $action = route('admin.prelim_definitions.update', $prelimDefinition);
+ $prelim = $prelimDefinition;
+
+ return view('admin.prelim_definitions.createOrUpdate', compact('auditions', 'rooms', 'guides', 'method', 'action', 'prelim'));
+
+ }
+
+ public function update(PrelimDefinition $prelimDefinition, PrelimDefinitionStoreOrUpdateRequest $request)
+ {
+ $validated = $request->validated();
+ $prelimDefinition->update($validated);
+
+ return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition updated');
+ }
+
+ public function destroy(PrelimDefinition $prelimDefinition)
+ {
+ $prelimDefinition->delete();
+
+ return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition deleted');
+ }
+}
diff --git a/app/Http/Controllers/Admin/RoomController.php b/app/Http/Controllers/Admin/RoomController.php
index df5f31a..0754cf9 100644
--- a/app/Http/Controllers/Admin/RoomController.php
+++ b/app/Http/Controllers/Admin/RoomController.php
@@ -19,6 +19,8 @@ class RoomController extends Controller
{
$rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get();
+
+ // Check if room id 0 exists, if not, create it and assign all unassigned auditions to it
if (! $rooms->contains('id', 0)) {
$unassignedRoom = Room::create([
'id' => 0,
diff --git a/app/Http/Controllers/Admin/SchoolController.php b/app/Http/Controllers/Admin/SchoolController.php
index d3fbca9..c8b411f 100644
--- a/app/Http/Controllers/Admin/SchoolController.php
+++ b/app/Http/Controllers/Admin/SchoolController.php
@@ -6,6 +6,7 @@ use App\Actions\Schools\CreateSchool;
use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller;
use App\Http\Requests\SchoolStoreRequest;
+use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\SchoolEmailDomain;
use App\Models\User;
@@ -37,8 +38,9 @@ class SchoolController extends Controller
public function show(School $school)
{
+ $logEntries = AuditLogEntry::whereJsonContains('affected->schools', $school->id)->orderBy('created_at', 'desc')->get();
- return view('admin.schools.show', ['school' => $school]);
+ return view('admin.schools.show', compact('school', 'logEntries'));
}
public function edit(School $school)
diff --git a/app/Http/Controllers/Admin/ScoringGuideController.php b/app/Http/Controllers/Admin/ScoringGuideController.php
index 657e9ae..a357c79 100644
--- a/app/Http/Controllers/Admin/ScoringGuideController.php
+++ b/app/Http/Controllers/Admin/ScoringGuideController.php
@@ -9,6 +9,7 @@ use App\Models\SubscoreDefinition;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
+use function auditionSetting;
use function request;
use function response;
@@ -81,6 +82,10 @@ class ScoringGuideController extends Controller
// Put the new subscore at the end of the list for both display and tiebreak order
$display_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('display_order') + 1;
$tiebreak_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('tiebreak_order') + 1;
+ if (! auditionSetting('advanceTo')) {
+ $validateData['for_advance'] = 0;
+ $validateData['for_seating'] = 1;
+ }
SubscoreDefinition::create([
'scoring_guide_id' => $guide->id,
diff --git a/app/Http/Controllers/Admin/StudentController.php b/app/Http/Controllers/Admin/StudentController.php
index 5fd2514..def4519 100644
--- a/app/Http/Controllers/Admin/StudentController.php
+++ b/app/Http/Controllers/Admin/StudentController.php
@@ -81,8 +81,11 @@ class StudentController extends Controller
$event_entries = $student->entries()->with('audition.flags')->get()->groupBy('audition.event_id');
$events = Event::all();
+ $logEntries = AuditLogEntry::whereJsonContains('affected->students', $student->id)->orderBy('created_at',
+ 'desc')->get();
+
return view('admin.students.edit',
- compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries'));
+ compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries', 'logEntries'));
}
public function update(StudentStoreRequest $request, Student $student)
diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php
index 2c79a2d..6d7ce3b 100644
--- a/app/Http/Controllers/Admin/UserController.php
+++ b/app/Http/Controllers/Admin/UserController.php
@@ -31,8 +31,10 @@ class UserController extends Controller
{
$schools = School::orderBy('name')->get();
+ $logEntries = AuditLogEntry::whereJsonContains('affected->users', $user->id)->orderBy('created_at', 'desc')->get();
+ $userActions = AuditLogEntry::where('user', $user->email)->orderBy('created_at', 'desc')->get();
- return view('admin.users.edit', ['user' => $user, 'schools' => $schools]);
+ return view('admin.users.edit', compact('user', 'schools', 'logEntries', 'userActions'));
}
public function create()
diff --git a/app/Http/Controllers/Judging/JudgingController.php b/app/Http/Controllers/Judging/JudgingController.php
index 2b2b506..4ca72eb 100644
--- a/app/Http/Controllers/Judging/JudgingController.php
+++ b/app/Http/Controllers/Judging/JudgingController.php
@@ -27,7 +27,7 @@ class JudgingController extends Controller
public function index()
{
- $rooms = Auth::user()->judgingAssignments()->with('auditions')->get();
+ $rooms = Auth::user()->judgingAssignments()->with('auditions')->with('prelimAuditions')->get();
$bonusScoresToJudge = Auth::user()->bonusJudgingAssignments()->with('auditions')->get();
//$rooms->load('auditions');
@@ -41,6 +41,11 @@ class JudgingController extends Controller
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge that audition');
}
$entries = Entry::where('audition_id', '=', $audition->id)->orderBy('draw_number')->with('audition')->get();
+
+ // If there is a prelim audition, only show entries that have passed the prelim
+ if ($audition->prelimDefinition) {
+ $entries = $entries->reject(fn ($entry) => ! $entry->hasFlag('passed_prelim'));
+ }
$subscores = $audition->scoringGuide->subscores()->orderBy('display_order')->get();
$votes = JudgeAdvancementVote::where('user_id', Auth::id())->get();
diff --git a/app/Http/Controllers/Judging/PrelimJudgingController.php b/app/Http/Controllers/Judging/PrelimJudgingController.php
new file mode 100644
index 0000000..21fd1e0
--- /dev/null
+++ b/app/Http/Controllers/Judging/PrelimJudgingController.php
@@ -0,0 +1,103 @@
+user()->cannot('judge', $prelimDefinition)) {
+ return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
+ }
+ $entries = $prelimDefinition->audition->entries;
+ $subscores = $prelimDefinition->scoringGuide->subscores()->orderBy('display_order')->get();
+ $published = $prelimDefinition->audition->hasFlag('seats_published');
+
+ $prelimScoresheets = PrelimScoreSheet::where('user_id', Auth::id())->get()->keyBy('entry_id');
+
+ return view('judging.prelim_entry_list',
+ compact('prelimDefinition', 'entries', 'subscores', 'published', 'prelimScoresheets'));
+ }
+
+ public function prelimScoreEntryForm(Entry $entry)
+ {
+ if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
+ return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
+ }
+ if ($entry->audition->hasFlag('seats_published')) {
+ return redirect()->route('dashboard')->with('error',
+ 'Scores for entries in published auditions cannot be modified.');
+ }
+ if ($entry->hasFlag('no_show')) {
+ return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('error',
+ 'The requested entry is marked as a no-show. Scores cannot be entered.');
+ }
+
+ $oldSheet = PrelimScoreSheet::where('user_id', Auth::id())->where('entry_id',
+ $entry->id)->value('subscores') ?? null;
+
+ return view('judging.prelim_entry_form', compact('entry', 'oldSheet'));
+ }
+
+ /**
+ * @throws AuditionAdminException
+ */
+ public function savePrelimScoreSheet(Entry $entry, Request $request, EnterPrelimScore $scribe)
+ {
+ if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
+ return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
+ }
+
+ // Validate form data
+ $subscores = $entry->audition->prelimDefinition->scoringGuide->subscores;
+ $validationChecks = [];
+ foreach ($subscores as $subscore) {
+ $validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
+ }
+ $validatedData = $request->validate($validationChecks);
+
+ // Enter the score
+ $scribe(auth()->user(), $entry, $validatedData['score']);
+
+ return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('success',
+ 'Entered prelim scores for '.$entry->audition->name.' '.$entry->draw_number);
+ }
+
+ public function updatePrelimScoreSheet(Entry $entry, Request $request, EnterPrelimScore $scribe)
+ {
+ if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
+ return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
+ }
+
+ // Validate form data
+ $subscores = $entry->audition->prelimDefinition->scoringGuide->subscores;
+ $validationChecks = [];
+ foreach ($subscores as $subscore) {
+ $validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
+ }
+ $validatedData = $request->validate($validationChecks);
+
+ // Get the existing score
+ $scoreSheet = PrelimScoreSheet::where('user_id', auth()->user()->id)->where('entry_id', $entry->id)->first();
+
+ if (! $scoreSheet) {
+ return redirect()->back()->with('error', 'No score sheet exists.');
+ }
+
+ // Update the score
+ $scribe(auth()->user(), $entry, $validatedData['score'], $scoreSheet);
+
+ return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('success',
+ 'Updated prelim scores for '.$entry->audition->name.' '.$entry->draw_number);
+
+ }
+}
diff --git a/app/Http/Controllers/MonitorController.php b/app/Http/Controllers/MonitorController.php
index 68d93f6..93414eb 100644
--- a/app/Http/Controllers/MonitorController.php
+++ b/app/Http/Controllers/MonitorController.php
@@ -2,8 +2,11 @@
namespace App\Http\Controllers;
+use App\Models\Audition;
use App\Models\Entry;
+use function compact;
+
class MonitorController extends Controller
{
public function index()
@@ -11,86 +14,44 @@ class MonitorController extends Controller
if (! auth()->user()->hasFlag('monitor')) {
abort(403);
}
- $method = 'POST';
- $formRoute = 'monitor.enterFlag';
- $title = 'Flag Entry';
- return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
+ $auditions = Audition::orderBy('score_order')->with('flags')->get();
+ $audition = null;
+ return view('monitor.index', compact('audition', 'auditions'));
}
- public function flagForm()
+ public function auditionStatus(Audition $audition)
{
if (! auth()->user()->hasFlag('monitor')) {
abort(403);
}
- $validData = request()->validate([
- 'entry_id' => ['required', 'integer', 'exists:entries,id'],
- ]);
-
- $entry = Entry::find($validData['entry_id']);
-
- // If the entries audition is published, bounce out
- if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
- return redirect()->route('monitor.index')->with('error', 'Cannot set flags while results are published');
+ if ($audition->hasFlag('seats_published') || $audition->hasFlag('advancement_published')) {
+ return redirect()->route('monitor.index')->with('error', 'Results for that audition are published');
}
- // If entry has scores, bounce on out
- if ($entry->scoreSheets()->count() > 0) {
- return redirect()->route('monitor.index')->with('error', 'That entry has existing scores');
- }
+ $auditions = Audition::orderBy('score_order')->with('flags')->get();
+ $entries = $audition->entries()->with('flags')->with('student.school')->withCount([
+ 'prelimScoreSheets', 'scoreSheets',
+ ])->orderBy('draw_number')->get();
- return view('monitor_entry_flag_form', compact('entry'));
+ return view('monitor.index', compact('audition', 'auditions', 'entries'));
}
- public function storeFlag(Entry $entry)
+ public function toggleNoShow(Entry $entry)
{
- if (! auth()->user()->hasFlag('monitor')) {
- abort(403);
- }
-
- // If the entries audition is published, bounce out
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
- return redirect()->route('monitor.index')->with('error', 'Cannot set flags while results are published');
+ return redirect()->route('monitor.index')->with('error', 'Results for that audition are published');
}
- // If entry has scores, bounce on out
- if ($entry->scoreSheets()->count() > 0) {
- return redirect()->route('monitor.index')->with('error', 'That entry has existing scores');
- }
-
- $action = request()->input('action');
- $result = match ($action) {
- 'failed-prelim' => $this->setFlag($entry, 'failed_prelim'),
- 'no-show' => $this->setFlag($entry, 'no_show'),
- 'clear' => $this->setFlag($entry, 'clear'),
- default => redirect()->route('monitor.index')->with('error', 'Invalid action requested'),
- };
-
- return redirect()->route('monitor.index')->with('success', 'Flag set for entry #'.$entry->id);
- }
-
- private function setFlag(Entry $entry, string $flag)
- {
- if ($flag === 'no_show') {
- $entry->removeFlag('failed_prelim');
- $entry->addFlag('no_show');
-
- return true;
- }
- if ($flag === 'failed_prelim') {
- $entry->addFlag('failed_prelim');
- $entry->addFlag('no_show');
-
- return true;
- }
- if ($flag === 'clear') {
- $entry->removeFlag('failed_prelim');
+ if ($entry->hasFlag('no_show')) {
$entry->removeFlag('no_show');
- return true;
+ return redirect()->back()->with('success', 'No Show Flag Cleared');
}
+ $entry->addFlag('no_show');
+ return redirect()->back()->with('success', 'No Show Entered');
}
}
diff --git a/app/Http/Controllers/Tabulation/ScoreController.php b/app/Http/Controllers/Tabulation/ScoreController.php
index d506b47..b25509a 100644
--- a/app/Http/Controllers/Tabulation/ScoreController.php
+++ b/app/Http/Controllers/Tabulation/ScoreController.php
@@ -2,11 +2,13 @@
namespace App\Http\Controllers\Tabulation;
+use App\Actions\Tabulation\EnterPrelimScore;
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Entry;
use App\Models\EntryTotalScore;
+use App\Models\PrelimScoreSheet;
use App\Models\ScoreSheet;
use App\Models\User;
use Illuminate\Http\Request;
@@ -67,8 +69,25 @@ class ScoreController extends Controller
'This entry is marked as a no-show. Entering a score will remove the no-show flag');
}
+ if ($entry->audition->prelimDefinition) {
+ $existing_prelim_sheets = [];
+ $prelim_subscores = $entry->audition->prelimDefinition->scoringGuide->subscores->sortBy('display_order');
+ $prelim_judges = $entry->audition->prelimDefinition->room->judges;
+ foreach ($prelim_judges as $judge) {
+ $prelim_scoreSheet = PrelimScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
+ if ($prelim_scoreSheet) {
+ Session::flash('caution', 'Prelim scores exist for this entry. Now editing existing scores');
+ $existing_prelim_sheets[$judge->id] = $prelim_scoreSheet;
+ }
+ }
+ } else {
+ $prelim_subscores = null;
+ $prelim_judges = null;
+ $existing_prelim_sheets = null;
+ }
+
return view('tabulation.entry_score_sheet',
- compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets'));
+ compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets', 'prelim_subscores', 'prelim_judges', 'existing_prelim_sheets'));
}
public function saveEntryScoreSheet(Request $request, Entry $entry, EnterScore $scoreRecorder)
@@ -85,7 +104,7 @@ class ScoreController extends Controller
* The array should have a key for each subscore and the value of the score submitted
*/
foreach ($request->all() as $key => $value) {
- // We're not interested in submission values that don't ahve judge in the name
+ // We're not interested in submission values that don't have judge in the name
if (! str_contains($key, 'judge')) {
continue;
}
@@ -114,6 +133,50 @@ class ScoreController extends Controller
return redirect()->route('scores.chooseEntry')->with('success', 'Scores saved');
}
+ public function savePrelimEntryScoreSheet(Request $request, Entry $entry, EnterPrelimScore $scoreRecorder)
+ {
+ $publishedCheck = $this->checkIfPublished($entry);
+ if ($publishedCheck) {
+ return $publishedCheck;
+ }
+
+ /**
+ * Here we process the submission from the scoring form.
+ * We're expecting submitted data to include an array for each judge.
+ * Each array should be called judge+ the judges ID number
+ * The array should have a key for each subscore and the value of the score submitted
+ */
+ foreach ($request->all() as $key => $value) {
+ // We're not interested in submission values that don't have judge in the name
+ if (! str_contains($key, 'judge')) {
+ continue;
+ }
+ // Extract the judge ID from the field name and load the user
+ $judge_id = str_replace('judge', '', $key);
+ $judge = User::find($judge_id);
+
+ // Check for existing scores, if so, tell EnterScores action that we're updating it, otherwise a new score
+ $existingScore = PrelimScoreSheet::where('entry_id', $entry->id)
+ ->where('user_id', $judge->id)->first();
+ if ($existingScore === null) {
+ $existingScore = false;
+ }
+
+ try {
+ $scoreRecorder($judge, $entry, $value, $existingScore);
+ } catch (AuditionAdminException $e) {
+ return redirect()->route('scores.entryScoreSheet', ['entry_id' => $entry->id])
+ ->with('error', $e->getMessage());
+ }
+ }
+
+ // Since we're entering a score, this apparently isn't a no show.
+ $entry->removeFlag('no_show');
+
+ return redirect()->route('scores.chooseEntry')->with('success', 'Prelim Scores Saved');
+
+ }
+
protected function checkIfPublished($entry)
{
// We're not going to enter scores if seats are published
diff --git a/app/Http/Controllers/Tabulation/Seating/ShowAuditionSeatingPage.php b/app/Http/Controllers/Tabulation/Seating/ShowAuditionSeatingPage.php
index 79d6b52..7f92acf 100644
--- a/app/Http/Controllers/Tabulation/Seating/ShowAuditionSeatingPage.php
+++ b/app/Http/Controllers/Tabulation/Seating/ShowAuditionSeatingPage.php
@@ -69,9 +69,14 @@ class ShowAuditionSeatingPage extends Controller
$query->where('flag_name', 'failed_prelim');
})
->with('student.school')
+ ->with('PrelimScoreSheets')
->orderBy('draw_number')
->get();
+ $failed_prelim_entries = $failed_prelim_entries->sortByDesc(function ($entry) {
+ return $entry->prelimTotalScore();
+ });
+
// Get Doublers
$doublerData = Doubler::where('event_id', $audition->event_id)
->whereIn('student_id', $scored_entries->pluck('student_id'))
diff --git a/app/Http/Requests/PrelimDefinitionStoreOrUpdateRequest.php b/app/Http/Requests/PrelimDefinitionStoreOrUpdateRequest.php
new file mode 100644
index 0000000..d2cef01
--- /dev/null
+++ b/app/Http/Requests/PrelimDefinitionStoreOrUpdateRequest.php
@@ -0,0 +1,28 @@
+ [
+ 'required',
+ 'exists:auditions,id',
+ Rule::unique('prelim_definitions', 'audition_id')->ignore($this->prelimDefinition),
+ ],
+ 'room_id' => ['nullable', 'exists:rooms,id'],
+ 'scoring_guide_id' => ['nullable', 'exists:scoring_guides,id'],
+ 'passing_score' => ['required', 'integer', 'min:0', 'max:100'],
+ ];
+ }
+
+ public function authorize(): bool
+ {
+ return auth()->user()->is_admin;
+ }
+}
diff --git a/app/Models/Audition.php b/app/Models/Audition.php
index 6c86d5f..9ae5c8a 100644
--- a/app/Models/Audition.php
+++ b/app/Models/Audition.php
@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Collection;
use function in_array;
@@ -165,6 +166,11 @@ class Audition extends Model
return $this->hasMany(Seat::class);
}
+ public function prelimDefinition(): HasOne
+ {
+ return $this->hasOne(PrelimDefinition::class);
+ }
+
/**
* @codeCoverageIgnoreStart
*/
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 9cbc7c3..a64e1cf 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -136,6 +136,35 @@ class Entry extends Model
}
+ public function prelimScoreSheets(): HasMany
+ {
+ return $this->hasMany(PrelimScoreSheet::class);
+ }
+
+ public function prelimTotalScore()
+ {
+ return once(function () {
+ $total = 0;
+ foreach ($this->prelimScoreSheets as $sheet) {
+ $total += $sheet->total;
+ }
+
+ return $total / $this->prelimScoreSheets->count();
+ });
+ }
+
+ public function prelimResult()
+ {
+ if ($this->hasFlag('passed_prelim')) {
+ return 'passed';
+ }
+ if ($this->hasFlag('failed_prelim')) {
+ return 'failed';
+ }
+
+ return null;
+ }
+
public function bonusScores(): HasMany
{
return $this->hasMany(BonusScore::class);
@@ -172,6 +201,7 @@ class Entry extends Model
'declined' => EntryFlags::DECLINED,
'no_show' => EntryFlags::NO_SHOW,
'failed_prelim' => EntryFlags::FAILED_PRELIM,
+ 'passed_prelim' => EntryFlags::PASSED_PRELIM,
'late_fee_waived' => EntryFlags::LATE_FEE_WAIVED,
};
$this->flags()->create(['flag_name' => $enum]);
diff --git a/app/Models/PrelimDefinition.php b/app/Models/PrelimDefinition.php
new file mode 100644
index 0000000..dfa7f47
--- /dev/null
+++ b/app/Models/PrelimDefinition.php
@@ -0,0 +1,32 @@
+belongsTo(Audition::class);
+ }
+
+ public function room(): BelongsTo
+ {
+ return $this->belongsTo(Room::class);
+ }
+
+ public function scoringGuide(): BelongsTo
+ {
+ return $this->belongsTo(ScoringGuide::class);
+ }
+}
diff --git a/app/Models/PrelimScoreSheet.php b/app/Models/PrelimScoreSheet.php
new file mode 100644
index 0000000..67ff953
--- /dev/null
+++ b/app/Models/PrelimScoreSheet.php
@@ -0,0 +1,34 @@
+ 'json'];
+
+ public function user(): HasOne
+ {
+ return $this->hasOne(User::class);
+ }
+
+ public function entry(): HasOne
+ {
+ return $this->hasOne(Entry::class);
+ }
+
+ public function getSubscore($id)
+ {
+ return $this->subscores[$id]['score'] ?? false;
+ // this function is used at resources/views/tabulation/entry_score_sheet.blade.php
+ }
+}
diff --git a/app/Models/Room.php b/app/Models/Room.php
index ce615ad..aa4de9a 100644
--- a/app/Models/Room.php
+++ b/app/Models/Room.php
@@ -19,6 +19,11 @@ class Room extends Model
return $this->hasMany(Audition::class)->orderBy('order_in_room')->orderBy('score_order');
}
+ public function prelimAuditions(): HasMany
+ {
+ return $this->hasMany(PrelimDefinition::class);
+ }
+
public function entries(): HasManyThrough
{
return $this->hasManyThrough(
diff --git a/app/Observers/PrelimDefinitionObserver.php b/app/Observers/PrelimDefinitionObserver.php
new file mode 100644
index 0000000..e66a915
--- /dev/null
+++ b/app/Observers/PrelimDefinitionObserver.php
@@ -0,0 +1,51 @@
+audition->name.'.';
+ $affected = ['auditions' => [$prelimDefinition->audition_id]];
+ auditionLog($message, $affected);
+ }
+
+ /**
+ * Handle the PrelimDefinition "updated" event.
+ */
+ public function updated(PrelimDefinition $prelimDefinition): void
+ {
+ $message = 'Updated Prelim for '.$prelimDefinition->audition->name.'.';
+ if ($prelimDefinition->getOriginal('room_id') !== $prelimDefinition->room_id) {
+ $oldRoom = Room::find($prelimDefinition->getOriginal('room_id'));
+ $message .= '
Room: '.$oldRoom->name.' -> '.$prelimDefinition->room->name;
+ }
+
+ if ($prelimDefinition->getOriginal('scoring_guide_id') !== $prelimDefinition->scoring_guide_id) {
+ $oldScoringGuide = ScoringGuide::find($prelimDefinition->getOriginal('scoring_guide_id'));
+ $message .= '
Scoring Guide: '.$oldScoringGuide->name.' -> '.$prelimDefinition->scoringGuide->name;
+ }
+
+ if ($prelimDefinition->getOriginal('passing_score') !== $prelimDefinition->passing_score) {
+ $message .= '
Passing Score: '.$prelimDefinition->getOriginal('passing_score').' -> '.$prelimDefinition->passing_score;
+ }
+ auditionLog($message, ['auditions' => [$prelimDefinition->audition_id]]);
+ }
+
+ /**
+ * Handle the PrelimDefinition "deleted" event.
+ */
+ public function deleted(PrelimDefinition $prelimDefinition): void
+ {
+ $message = 'Deleted Prelim for '.$prelimDefinition->audition->name.'.';
+ auditionLog($message, ['auditions' => [$prelimDefinition->audition_id]]);
+ }
+}
diff --git a/app/Policies/PrelimDefinitionPolicy.php b/app/Policies/PrelimDefinitionPolicy.php
new file mode 100644
index 0000000..e8d63e1
--- /dev/null
+++ b/app/Policies/PrelimDefinitionPolicy.php
@@ -0,0 +1,50 @@
+is_admin;
+ }
+
+ public function view(User $user, PrelimDefinition $prelimDefinition): bool
+ {
+ return $user->is_admin;
+ }
+
+ public function create(User $user): bool
+ {
+ return $user->is_admin;
+ }
+
+ public function update(User $user, PrelimDefinition $prelimDefinition): bool
+ {
+ return $user->is_admin;
+ }
+
+ public function delete(User $user, PrelimDefinition $prelimDefinition): bool
+ {
+ return $user->is_admin;
+ }
+
+ public function judge(User $user, PrelimDefinition $prelimDefinition): bool
+ {
+ return $user->judgingAssignments->contains($prelimDefinition->room_id);
+ }
+
+ public function restore(User $user, PrelimDefinition $prelimDefinition): bool
+ {
+ }
+
+ public function forceDelete(User $user, PrelimDefinition $prelimDefinition): bool
+ {
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 398ee75..c7288e0 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -12,6 +12,7 @@ use App\Models\BonusScore;
use App\Models\Entry;
use App\Models\EntryFlag;
use App\Models\Event;
+use App\Models\PrelimDefinition;
use App\Models\School;
use App\Models\SchoolEmailDomain;
use App\Models\ScoreSheet;
@@ -23,6 +24,7 @@ use App\Observers\BonusScoreObserver;
use App\Observers\EntryFlagObserver;
use App\Observers\EntryObserver;
use App\Observers\EventObserver;
+use App\Observers\PrelimDefinitionObserver;
use App\Observers\SchoolEmailDomainObserver;
use App\Observers\SchoolObserver;
use App\Observers\ScoreSheetObserver;
@@ -69,6 +71,7 @@ class AppServiceProvider extends ServiceProvider
EntryFlag::observe(EntryFlagObserver::class);
Event::observe(EventObserver::class);
School::observe(SchoolObserver::class);
+ PrelimDefinition::observe(PrelimDefinitionObserver::class);
SchoolEmailDomain::observe(SchoolEmailDomainObserver::class);
ScoreSheet::observe(ScoreSheetObserver::class);
ScoringGuide::observe(ScoringGuideObserver::class);
diff --git a/database/migrations/2025_09_11_141701_create_prelim_definitions_table.php b/database/migrations/2025_09_11_141701_create_prelim_definitions_table.php
new file mode 100644
index 0000000..fbb61a6
--- /dev/null
+++ b/database/migrations/2025_09_11_141701_create_prelim_definitions_table.php
@@ -0,0 +1,29 @@
+id();
+ $table->foreignIdFor(Audition::class)->unique()->constrained()->cascadeOnDelete()->cascadeOnUpdate();
+ $table->foreignIdFor(Room::class)->nullable()->constrained()->nullOnDelete()->cascadeOnUpdate();
+ $table->integer('order_in_room')->nullable();
+ $table->foreignIdFor(ScoringGuide::class)->nullable()->constrained()->nullOnDelete()->cascadeOnUpdate();
+ $table->float('passing_score');
+ $table->timestamps();
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::dropIfExists('prelim_definitions');
+ }
+};
diff --git a/database/migrations/2025_09_23_020626_create_prelim_score_sheets_table.php b/database/migrations/2025_09_23_020626_create_prelim_score_sheets_table.php
new file mode 100644
index 0000000..a1cc58a
--- /dev/null
+++ b/database/migrations/2025_09_23_020626_create_prelim_score_sheets_table.php
@@ -0,0 +1,34 @@
+id();
+ $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate();
+ $table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate();
+ $table->json('subscores');
+ $table->decimal('total', 9, 6);
+ $table->timestamps();
+ $table->unique(['user_id', 'entry_id']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('prelim_score_sheets');
+ }
+};
diff --git a/resources/views/admin/entries/edit.blade.php b/resources/views/admin/entries/edit.blade.php
index a2b6912..9fcce7e 100644
--- a/resources/views/admin/entries/edit.blade.php
+++ b/resources/views/admin/entries/edit.blade.php
@@ -127,4 +127,29 @@
+
+
+
+
+
+
+ @endforeach
+
+
+
+
+
+ @endforeach
+
+
+
+
+
+ @endforeach
+
+
+
+
+
+ @endforeach
+
+
+
+
+
+ @endforeach
+
+
+
+
+
+ @endforeach
+
No Show
+ @endif + @if($entry->scoreSheets()->count() > 0) +Has Finals Scores
+ @endif + @if(! $published && ! $entry->hasFlag('no_show') && $entry->scoreSheets()->count() < 1) + + @endif +{{ $entry->student->full_name() }}
@@ -11,7 +21,8 @@0.000
@@ -67,35 +78,145 @@ + + @if($entry->audition->prelimDefinition) +{{ $entry->student->full_name() }}
+{{ $entry->student->school->name }}
++ 0.000 +
+