From 0203505f5bfa93213b9db7118bf5eb81b489ceec Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 26 Jun 2024 22:56:31 -0500 Subject: [PATCH 1/3] Judges can enter yes/no/dq votes for advancement entries --- app/Http/Controllers/JudgingController.php | 71 +++++++++++++------ app/Models/Entry.php | 5 ++ app/Models/JudgeAdvancementVote.php | 25 +++++++ app/Models/User.php | 48 ++++++++----- ...8_create_judge_advancement_votes_table.php | 33 +++++++++ .../judging/advancement-vote-form.blade.php | 37 ++++++++++ .../views/judging/entry_score_sheet.blade.php | 9 ++- routes/judging.php | 10 +-- 8 files changed, 195 insertions(+), 43 deletions(-) create mode 100644 app/Models/JudgeAdvancementVote.php create mode 100644 database/migrations/2024_06_27_030228_create_judge_advancement_votes_table.php create mode 100644 resources/views/judging/advancement-vote-form.blade.php diff --git a/app/Http/Controllers/JudgingController.php b/app/Http/Controllers/JudgingController.php index 8857ab2..bd17aaa 100644 --- a/app/Http/Controllers/JudgingController.php +++ b/app/Http/Controllers/JudgingController.php @@ -4,12 +4,13 @@ namespace App\Http\Controllers; use App\Models\Audition; use App\Models\Entry; +use App\Models\JudgeAdvancementVote; use App\Models\ScoreSheet; -use App\Models\SubscoreDefinition; use App\Models\User; -use Illuminate\Support\Facades\Gate; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; + use function compact; use function redirect; use function url; @@ -19,77 +20,107 @@ class JudgingController extends Controller public function index() { $rooms = Auth::user()->judgingAssignments; + return view('judging.index', compact('rooms')); } public function auditionEntryList(Audition $audition) { // TODO verify user is assigned to judge this audition - $entries = Entry::where('audition_id','=',$audition->id)->orderBy('draw_number')->with('audition')->get(); + $entries = Entry::where('audition_id', '=', $audition->id)->orderBy('draw_number')->with('audition')->get(); $subscores = $audition->scoringGuide->subscores()->orderBy('display_order')->get(); - return view('judging.audition_entry_list', compact('audition','entries','subscores')); + + return view('judging.audition_entry_list', compact('audition', 'entries', 'subscores')); } public function entryScoreSheet(Entry $entry) { // TODO verify user is assigned to judge this audition - $oldSheet = ScoreSheet::where('user_id',Auth::id())->where('entry_id',$entry->id)->value('subscores') ?? null; - return view('judging.entry_score_sheet',compact('entry','oldSheet')); + $oldSheet = ScoreSheet::where('user_id', Auth::id())->where('entry_id', $entry->id)->value('subscores') ?? null; + $oldVote = JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->first(); + $oldVote = $oldVote ? $oldVote->vote : 'novote'; + + return view('judging.entry_score_sheet', compact('entry', 'oldSheet', 'oldVote')); } public function saveScoreSheet(Request $request, Entry $entry) { - Gate::authorize('create',[ScoreSheet::class,$entry]); + Gate::authorize('create', [ScoreSheet::class, $entry]); // TODO verify user is assigned to judge this audition $scoringGuide = $entry->audition->scoringGuide()->with('subscores')->first(); $scoreValidation = $scoringGuide->validateScores($request->input('score')); if ($scoreValidation != 'success') { - return redirect(url()->previous())->with('error', $scoreValidation)->with('oldScores',$request->all()); + return redirect(url()->previous())->with('error', $scoreValidation)->with('oldScores', $request->all()); } $scoreSheetArray = []; - foreach($scoringGuide->subscores as $subscore) { + foreach ($scoringGuide->subscores as $subscore) { $scoreSheetArray[$subscore->id] = [ 'score' => $request->input('score')[$subscore->id], 'subscore_id' => $subscore->id, - 'subscore_name' => $subscore->name + 'subscore_name' => $subscore->name, ]; } ScoreSheet::create([ 'user_id' => Auth::user()->id, 'entry_id' => $entry->id, - 'subscores' => $scoreSheetArray + 'subscores' => $scoreSheetArray, ]); - return redirect('/judging/audition/' . $entry->audition_id)->with('success','Entered scores for ' . $entry->audition->name . ' ' . $entry->draw_number); + $this->advancementVote($request, $entry); + + return redirect('/judging/audition/'.$entry->audition_id)->with('success', 'Entered scores for '.$entry->audition->name.' '.$entry->draw_number); } public function updateScoreSheet(Request $request, Entry $entry) { - $scoreSheet = ScoreSheet::where('user_id',Auth::id())->where('entry_id',$entry->id)->first(); - if (!$scoreSheet) return redirect()->back()->with('error','Attempt to edit non existent entry'); - Gate::authorize('update',$scoreSheet); + $scoreSheet = ScoreSheet::where('user_id', Auth::id())->where('entry_id', $entry->id)->first(); + if (! $scoreSheet) { + return redirect()->back()->with('error', 'Attempt to edit non existent entry'); + } + Gate::authorize('update', $scoreSheet); $scoringGuide = $entry->audition->scoringGuide()->with('subscores')->first(); $scoreValidation = $scoringGuide->validateScores($request->input('score')); if ($scoreValidation != 'success') { - return redirect(url()->previous())->with('error', $scoreValidation)->with('oldScores',$request->all()); + return redirect(url()->previous())->with('error', $scoreValidation)->with('oldScores', $request->all()); } $scoreSheetArray = []; - foreach($scoringGuide->subscores as $subscore) { + foreach ($scoringGuide->subscores as $subscore) { $scoreSheetArray[$subscore->id] = [ 'score' => $request->input('score')[$subscore->id], 'subscore_id' => $subscore->id, - 'subscore_name' => $subscore->name + 'subscore_name' => $subscore->name, ]; } $scoreSheet->update([ - 'subscores' => $scoreSheetArray + 'subscores' => $scoreSheetArray, ]); - return redirect('/judging/audition/' . $entry->audition_id)->with('success','Updated scores for ' . $entry->audition->name . ' ' . $entry->draw_number); + + $this->advancementVote($request, $entry); + + return redirect('/judging/audition/'.$entry->audition_id)->with('success', 'Updated scores for '.$entry->audition->name.' '.$entry->draw_number); } + protected function advancementVote(Request $request, Entry $entry) + { + if ($entry->for_advancement) { + $request->validate([ + 'advancement-vote' => ['required', 'in:yes,no,dq'], + ]); + try { + JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->delete(); + JudgeAdvancementVote::create([ + 'user_id' => Auth::user()->id, + 'entry_id' => $entry->id, + 'vote' => $request->input('advancement-vote'), + ]); + } catch (\Exception $e) { + return redirect(url()->previous())->with('error', 'Error saving advancement vote'); + } + } + } } diff --git a/app/Models/Entry.php b/app/Models/Entry.php index d63a7c2..db37e65 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -51,6 +51,11 @@ class Entry extends Model } + public function advancementVotes(): HasMany + { + return $this->hasMany(JudgeAdvancementVote::class); + } + public function flags(): HasMany { return $this->hasMany(EntryFlag::class); diff --git a/app/Models/JudgeAdvancementVote.php b/app/Models/JudgeAdvancementVote.php new file mode 100644 index 0000000..e02f95d --- /dev/null +++ b/app/Models/JudgeAdvancementVote.php @@ -0,0 +1,25 @@ +belongsTo(Entry::class); + } + + public function judge(): BelongsTo + { + return $this->belongsTo(User::class); + } + +} diff --git a/app/Models/User.php b/app/Models/User.php index ccfa667..eb96143 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,7 +2,6 @@ namespace App\Models; - use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -11,7 +10,6 @@ use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use phpDocumentor\Reflection\Types\Boolean; class User extends Authenticatable implements MustVerifyEmail { @@ -30,7 +28,7 @@ class User extends Authenticatable implements MustVerifyEmail 'email', 'password', 'profile_image_url', - 'school_id' + 'school_id', ]; /** @@ -56,16 +54,19 @@ class User extends Authenticatable implements MustVerifyEmail ]; } - public function full_name(Bool $last_name_first = false): String + public function full_name(bool $last_name_first = false): string { - if ($last_name_first) return $this->last_name . ', ' . $this->first_name; - return $this->first_name . ' ' . $this->last_name; + if ($last_name_first) { + return $this->last_name.', '.$this->first_name; + } + + return $this->first_name.' '.$this->last_name; } - public function short_name(): String + public function short_name(): string { // return the first letter of $this->first_name and the full $this->last_name - return $this->first_name[0] . '. ' . $this->last_name; + return $this->first_name[0].'. '.$this->last_name; } public function has_school(): bool @@ -76,7 +77,8 @@ class User extends Authenticatable implements MustVerifyEmail public function emailDomain(): string { $pos = strpos($this->email, '@'); - return substr($this->email, $pos+1); + + return substr($this->email, $pos + 1); } public function school(): BelongsTo @@ -87,7 +89,7 @@ class User extends Authenticatable implements MustVerifyEmail public function students(): HasManyThrough { return $this - ->hasManyThrough(Student::class, School::class, 'id','school_id','school_id','id') + ->hasManyThrough(Student::class, School::class, 'id', 'school_id', 'school_id', 'id') ->orderBy('last_name') ->orderBy('first_name'); } @@ -106,7 +108,7 @@ class User extends Authenticatable implements MustVerifyEmail public function rooms(): BelongsToMany { - return $this->belongsToMany(Room::class,'room_user'); + return $this->belongsToMany(Room::class, 'room_user'); } public function judgingAssignments(): BelongsToMany @@ -114,26 +116,38 @@ class User extends Authenticatable implements MustVerifyEmail return $this->rooms(); } - public function isJudge(): Bool + public function advancementVotes(): HasMany + { + return $this->hasMany(JudgeAdvancementVote::class); + } + + public function isJudge(): bool { return $this->judgingAssignments->count() > 0; } /** * Return an array of schools using the users email domain + * * @return SchoolEmailDomain[] */ public function possibleSchools() { if ($this->school_id) { $return[] = $this->school; + return $return; } - return SchoolEmailDomain::with('school')->where('domain','=',$this->emailDomain())->get(); + + return SchoolEmailDomain::with('school')->where('domain', '=', $this->emailDomain())->get(); } - public function canTab() { - if ($this->is_admin) return true; + public function canTab() + { + if ($this->is_admin) { + return true; + } + return $this->is_tab; } @@ -145,12 +159,12 @@ class User extends Authenticatable implements MustVerifyEmail public function scoresForEntry($entry) { // TODO Again, why is this here? Needs to go somewhere else. Maybe a Judging service - return $this->scoreSheets->where('entry_id','=',$entry)->first()?->subscores; + return $this->scoreSheets->where('entry_id', '=', $entry)->first()?->subscores; } public function timeForEntryScores($entry) { // TODO Why is this in the User mode? Move it somewhere else - return $this->scoreSheets->where('entry_id','=',$entry)->first()?->created_at; + return $this->scoreSheets->where('entry_id', '=', $entry)->first()?->created_at; } } diff --git a/database/migrations/2024_06_27_030228_create_judge_advancement_votes_table.php b/database/migrations/2024_06_27_030228_create_judge_advancement_votes_table.php new file mode 100644 index 0000000..c983e7a --- /dev/null +++ b/database/migrations/2024_06_27_030228_create_judge_advancement_votes_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignIdFor(User::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + $table->foreignIdFor(Entry::class)->constrained()->cascadeOnDelete()->cascadeOnUpdate(); + $table->string('vote'); + $table->unique(['user_id', 'entry_id']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('judge_advancement_votes'); + } +}; diff --git a/resources/views/judging/advancement-vote-form.blade.php b/resources/views/judging/advancement-vote-form.blade.php new file mode 100644 index 0000000..b98569f --- /dev/null +++ b/resources/views/judging/advancement-vote-form.blade.php @@ -0,0 +1,37 @@ +
+ + {{ auditionSetting('advanceTo') }} Advancement +

Only choose DQ if a rule of some kinds was broken

+
+
+ + +
+
+ + +
+
+ + +
+
+ @error('advancement-vote') +

{{ $message }}

+ @enderror +
diff --git a/resources/views/judging/entry_score_sheet.blade.php b/resources/views/judging/entry_score_sheet.blade.php index 36054ba..36806ad 100644 --- a/resources/views/judging/entry_score_sheet.blade.php +++ b/resources/views/judging/entry_score_sheet.blade.php @@ -1,6 +1,9 @@ + {{-- TODO A user should only be able to get this form for an entry they're actually assigned to judge--}} + @php $oldScores = session()->get('oldScores') ?? null; + // TODO get old vote @endphp Entry Dashboard @@ -15,7 +18,7 @@ - + @if($oldSheet) {{-- if there are existing sores, make this a patch request --}} @method('PATCH') @endif @@ -46,6 +49,10 @@ @endforeach + + @if($entry->for_advancement) + @include('judging.advancement-vote-form') + @endif Save Scores diff --git a/routes/judging.php b/routes/judging.php index b8be7dc..978434f 100644 --- a/routes/judging.php +++ b/routes/judging.php @@ -5,9 +5,9 @@ use App\Http\Middleware\CheckIfCanJudge; use Illuminate\Support\Facades\Route; Route::middleware(['auth', 'verified', CheckIfCanJudge::class])->prefix('judging')->controller(JudgingController::class)->group(function () { - Route::get('/', 'index'); - Route::get('/audition/{audition}', 'auditionEntryList'); - Route::get('/entry/{entry}', 'entryScoreSheet'); - Route::post('/entry/{entry}', 'saveScoreSheet'); - Route::patch('/entry/{entry}', 'updateScoreSheet'); + Route::get('/', 'index')->name('judging.index'); + Route::get('/audition/{audition}', 'auditionEntryList')->name('judging.auditionEntryList'); + Route::get('/entry/{entry}', 'entryScoreSheet')->name('judging.entryScoreSheet'); + Route::post('/entry/{entry}', 'saveScoreSheet')->name('judging.saveScoreSheet'); + Route::patch('/entry/{entry}', 'updateScoreSheet')->name('judging.updateScoreSheet'); }); -- 2.39.5 From 90aac42fb479f2163bc2d224894d78c8eba72de8 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 26 Jun 2024 23:36:08 -0500 Subject: [PATCH 2/3] Show past votes on entry list for judges --- app/Http/Controllers/JudgingController.php | 6 +++-- .../components/icons/thumbs-down.blade.php | 4 ++++ .../components/icons/thumbs-up.blade.php | 4 ++++ .../judging/audition_entry_list.blade.php | 23 +++++++++++++++++++ .../views/judging/entry_score_sheet.blade.php | 2 +- 5 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 resources/views/components/icons/thumbs-down.blade.php create mode 100644 resources/views/components/icons/thumbs-up.blade.php diff --git a/app/Http/Controllers/JudgingController.php b/app/Http/Controllers/JudgingController.php index bd17aaa..2cf405c 100644 --- a/app/Http/Controllers/JudgingController.php +++ b/app/Http/Controllers/JudgingController.php @@ -30,7 +30,9 @@ class JudgingController extends Controller $entries = Entry::where('audition_id', '=', $audition->id)->orderBy('draw_number')->with('audition')->get(); $subscores = $audition->scoringGuide->subscores()->orderBy('display_order')->get(); - return view('judging.audition_entry_list', compact('audition', 'entries', 'subscores')); + $votes = JudgeAdvancementVote::where('user_id', Auth::id())->get(); + + return view('judging.audition_entry_list', compact('audition', 'entries', 'subscores', 'votes')); } public function entryScoreSheet(Entry $entry) @@ -107,7 +109,7 @@ class JudgingController extends Controller protected function advancementVote(Request $request, Entry $entry) { - if ($entry->for_advancement) { + if ($entry->for_advancement and auditionSetting('advanceTo')) { $request->validate([ 'advancement-vote' => ['required', 'in:yes,no,dq'], ]); diff --git a/resources/views/components/icons/thumbs-down.blade.php b/resources/views/components/icons/thumbs-down.blade.php new file mode 100644 index 0000000..1990c19 --- /dev/null +++ b/resources/views/components/icons/thumbs-down.blade.php @@ -0,0 +1,4 @@ +@props(['color' => 'currentColor']) + diff --git a/resources/views/components/icons/thumbs-up.blade.php b/resources/views/components/icons/thumbs-up.blade.php new file mode 100644 index 0000000..1f5c405 --- /dev/null +++ b/resources/views/components/icons/thumbs-up.blade.php @@ -0,0 +1,4 @@ +@props(['color' => 'currentColor']) + diff --git a/resources/views/judging/audition_entry_list.blade.php b/resources/views/judging/audition_entry_list.blade.php index 878ff4c..8bd90ce 100644 --- a/resources/views/judging/audition_entry_list.blade.php +++ b/resources/views/judging/audition_entry_list.blade.php @@ -9,6 +9,9 @@ @foreach($subscores as $subscore) {{ $subscore->name }} @endforeach + @if(auditionSetting('advanceTo') and $audition->for_advancement) + {{ auditionSetting('advanceTo') }} + @endif Timestamp @@ -27,6 +30,26 @@ @endphp @endforeach + @if(auditionSetting('advanceTo') and $audition->for_advancement) + + @if($votes->contains('entry_id', $entry->id)) + @php + $vote = $votes->where('entry_id',$entry->id)->first(); + @endphp + @switch($vote->vote) + @case('yes') + + @break + @case('no') + + @break + @case('dq') + + @break + @endswitch + @endif + + @endif {{ Auth::user()->timeForEntryScores($entry->id)?->setTimezone('America/Chicago')->format('m/d/y H:i') }} diff --git a/resources/views/judging/entry_score_sheet.blade.php b/resources/views/judging/entry_score_sheet.blade.php index 36806ad..3d3102e 100644 --- a/resources/views/judging/entry_score_sheet.blade.php +++ b/resources/views/judging/entry_score_sheet.blade.php @@ -50,7 +50,7 @@ @endforeach - @if($entry->for_advancement) + @if($entry->for_advancement AND auditionSetting('advanceTo')) @include('judging.advancement-vote-form') @endif -- 2.39.5 From 25aa76c63c40ce36b7ea4ed15aa49ff48bd4b2a2 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 27 Jun 2024 00:48:23 -0500 Subject: [PATCH 3/3] Show votes on Tabulation Advancement page --- .../Tabulation/AdvancementController.php | 2 ++ app/Models/JudgeAdvancementVote.php | 2 +- resources/views/components/tooltip.blade.php | 15 ++++++++++++ .../advancement/results-table.blade.php | 24 +++++++++++++++++++ 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 resources/views/components/tooltip.blade.php diff --git a/app/Http/Controllers/Tabulation/AdvancementController.php b/app/Http/Controllers/Tabulation/AdvancementController.php index 9581faa..93eac92 100644 --- a/app/Http/Controllers/Tabulation/AdvancementController.php +++ b/app/Http/Controllers/Tabulation/AdvancementController.php @@ -27,6 +27,8 @@ class AdvancementController extends Controller public function ranking(Request $request, Audition $audition) { $entries = $this->tabulationService->auditionEntries($audition->id, 'advancement'); + $entries->load('advancementVotes'); + $scoringComplete = $entries->every(function ($entry) { return $entry->scoring_complete; diff --git a/app/Models/JudgeAdvancementVote.php b/app/Models/JudgeAdvancementVote.php index e02f95d..0ee328c 100644 --- a/app/Models/JudgeAdvancementVote.php +++ b/app/Models/JudgeAdvancementVote.php @@ -19,7 +19,7 @@ class JudgeAdvancementVote extends Model public function judge(): BelongsTo { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class, 'user_id'); } } diff --git a/resources/views/components/tooltip.blade.php b/resources/views/components/tooltip.blade.php new file mode 100644 index 0000000..bdbdc4f --- /dev/null +++ b/resources/views/components/tooltip.blade.php @@ -0,0 +1,15 @@ +@php + $classes = "pointer-events-none absolute bg-white transform -translate-y-12 z-50 p-3 max-w-sm rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"; +@endphp +
merge(['class'=>$classes]) }} x-cloak + x-transition:enter="transform ease-out duration-300 transition " + x-transition:enter-start="-translate-y-12 opacity-0 sm:translate-y-0 sm:translate-x-2" + x-transition:enter-end="translate-y-0 opacity-100 sm:translate-x-0" + x-transition:leave="transition ease-in duration-100" + x-transition:leave-start="opacity-100" + x-transition:leave-end="opacity-0"> + {{ $slot }} +
+ + + diff --git a/resources/views/tabulation/advancement/results-table.blade.php b/resources/views/tabulation/advancement/results-table.blade.php index 5fcd732..049e6e3 100644 --- a/resources/views/tabulation/advancement/results-table.blade.php +++ b/resources/views/tabulation/advancement/results-table.blade.php @@ -8,6 +8,7 @@ Student Name Total Score All Scores? + Votes @if($scoringComplete) Pass? @endif @@ -30,6 +31,29 @@ @endif + + @foreach($entry->advancementVotes as $vote) +
+ + + {{ $vote->judge->full_name() }} + + @switch($vote->vote) + @case('yes') + + @break + @case('no') + + @break + @case('dq') + + @break + @endswitch +
+ @endforeach +
@if( $audition->hasFlag('advancement_published') ) @if($entry->hasFlag('will_advance')) -- 2.39.5