Merge pull request #4 from okorpheus/ipmlement-judge-pass-fail-advancement

Implement judge pass fail advancement
This commit is contained in:
Matt 2024-06-27 00:54:59 -05:00 committed by GitHub
commit 0f88ae9573
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 269 additions and 43 deletions

View File

@ -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,6 +20,7 @@ class JudgingController extends Controller
public function index()
{
$rooms = Auth::user()->judgingAssignments;
return view('judging.index', compact('rooms'));
}
@ -27,14 +29,20 @@ class JudgingController extends Controller
// TODO verify user is assigned to judge this audition
$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)
{
// 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'));
$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)
@ -51,16 +59,18 @@ class JudgingController extends Controller
$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,
]);
$this->advancementVote($request, $entry);
return redirect('/judging/audition/'.$entry->audition_id)->with('success', 'Entered scores for '.$entry->audition->name.' '.$entry->draw_number);
}
@ -68,7 +78,9 @@ class JudgingController extends Controller
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');
if (! $scoreSheet) {
return redirect()->back()->with('error', 'Attempt to edit non existent entry');
}
Gate::authorize('update', $scoreSheet);
$scoringGuide = $entry->audition->scoringGuide()->with('subscores')->first();
@ -81,15 +93,36 @@ class JudgingController extends Controller
$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,
]);
$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 and auditionSetting('advanceTo')) {
$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');
}
}
}
}

View File

@ -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;

View File

@ -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);

View File

@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class JudgeAdvancementVote extends Model
{
use HasFactory;
protected $guarded = [];
public function entry(): BelongsTo
{
return $this->belongsTo(Entry::class);
}
public function judge(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}
}

View File

@ -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,13 +54,16 @@ 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;
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;
@ -76,6 +77,7 @@ class User extends Authenticatable implements MustVerifyEmail
public function emailDomain(): string
{
$pos = strpos($this->email, '@');
return substr($this->email, $pos + 1);
}
@ -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();
}
public function canTab() {
if ($this->is_admin) return true;
public function canTab()
{
if ($this->is_admin) {
return true;
}
return $this->is_tab;
}

View File

@ -0,0 +1,33 @@
<?php
use App\Models\Entry;
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('judge_advancement_votes', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,4 @@
@props(['color' => 'currentColor'])
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="{{ $color }}" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M8.97 14.316H5.004c-.322 0-.64-.08-.925-.232a2.022 2.022 0 0 1-.717-.645 2.108 2.108 0 0 1-.242-1.883l2.36-7.201C5.769 3.54 5.96 3 7.365 3c2.072 0 4.276.678 6.156 1.256.473.145.925.284 1.35.404h.114v9.862a25.485 25.485 0 0 0-4.238 5.514c-.197.376-.516.67-.901.83a1.74 1.74 0 0 1-1.21.048 1.79 1.79 0 0 1-.96-.757 1.867 1.867 0 0 1-.269-1.211l1.562-4.63ZM19.822 14H17V6a2 2 0 1 1 4 0v6.823c0 .65-.527 1.177-1.177 1.177Z" clip-rule="evenodd"/>
</svg>

View File

@ -0,0 +1,4 @@
@props(['color' => 'currentColor'])
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="{{ $color }}" viewBox="0 0 24 24">
<path fill-rule="evenodd" d="M15.03 9.684h3.965c.322 0 .64.08.925.232.286.153.532.374.717.645a2.109 2.109 0 0 1 .242 1.883l-2.36 7.201c-.288.814-.48 1.355-1.884 1.355-2.072 0-4.276-.677-6.157-1.256-.472-.145-.924-.284-1.348-.404h-.115V9.478a25.485 25.485 0 0 0 4.238-5.514 1.8 1.8 0 0 1 .901-.83 1.74 1.74 0 0 1 1.21-.048c.396.13.736.397.96.757.225.36.32.788.269 1.211l-1.562 4.63ZM4.177 10H7v8a2 2 0 1 1-4 0v-6.823C3 10.527 3.527 10 4.176 10Z" clip-rule="evenodd"/>
</svg>

View File

@ -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
<div {{ $attributes->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 }}
</div>

View File

@ -0,0 +1,37 @@
<fieldset>
<input type="hidden" name="require-vote" value="true">
<legend class="text-sm font-semibold leading-6 text-gray-900">{{ auditionSetting('advanceTo') }} Advancement </legend>
<p class="mt-1 text-sm leading-6 text-gray-600">Only choose DQ if a rule of some kinds was broken</p>
<div class="mt-6 space-y-6 sm:flex sm:items-center sm:space-x-10 sm:space-y-0">
<div class="flex items-center">
<input id="advance-yes"
name="advancement-vote"
type="radio"
value="yes"
@if($oldVote === 'yes') checked @endif
class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600">
<label for="advance-yes" class="ml-3 block text-sm font-medium leading-6 text-gray-900">Yes</label>
</div>
<div class="flex items-center">
<input id="advance-no"
name="advancement-vote"
type="radio"
value="no"
@if($oldVote === 'no') checked @endif
class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600">
<label for="advance-no" class="ml-3 block text-sm font-medium leading-6 text-gray-900">No</label>
</div>
<div class="flex items-center">
<input id="advance-dq"
name="advancement-vote"
type="radio"
value="dq"
@if($oldVote === 'dq') checked @endif
class="h-4 w-4 border-gray-300 text-indigo-600 focus:ring-indigo-600">
<label for="advance-dq" class="ml-3 block text-sm font-medium leading-6 text-gray-900">DQ</label>
</div>
</div>
@error('advancement-vote')
<p class="text-xs text-red-500 font-semibold mt-1 ml-3">{{ $message }}</p>
@enderror
</fieldset>

View File

@ -9,6 +9,9 @@
@foreach($subscores as $subscore)
<x-table.th :sortable="false">{{ $subscore->name }}</x-table.th>
@endforeach
@if(auditionSetting('advanceTo') and $audition->for_advancement)
<x-table.th>{{ auditionSetting('advanceTo') }}</x-table.th>
@endif
<x-table.th :sortable="true">Timestamp</x-table.th>
</tr>
</thead>
@ -27,6 +30,26 @@
@endphp
</x-table.td>
@endforeach
@if(auditionSetting('advanceTo') and $audition->for_advancement)
<x-table.td>
@if($votes->contains('entry_id', $entry->id))
@php
$vote = $votes->where('entry_id',$entry->id)->first();
@endphp
@switch($vote->vote)
@case('yes')
<x-icons.thumbs-up color="green"/>
@break
@case('no')
<x-icons.thumbs-down color="red"/>
@break
@case('dq')
<x-icons.circle-slash-no color="red"/>
@break
@endswitch
@endif
</x-table.td>
@endif
<x-table.td>
{{ Auth::user()->timeForEntryScores($entry->id)?->setTimezone('America/Chicago')->format('m/d/y H:i') }}
</x-table.td>

View File

@ -1,6 +1,9 @@
<x-layout.app>
{{-- 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
<x-slot:page_title>Entry Dashboard</x-slot:page_title>
@ -15,7 +18,7 @@
</ul>
</x-slot:subheading>
</x-card.heading>
<x-form.form metohd="POST" action="/judging/entry/{{$entry->id}}">
<x-form.form metohd="POST" action="{{ route('judging.saveScoreSheet',['entry' => $entry->id]) }}">
@if($oldSheet) {{-- if there are existing sores, make this a patch request --}}
@method('PATCH')
@endif
@ -46,6 +49,10 @@
</li>
@endforeach
@if($entry->for_advancement AND auditionSetting('advanceTo'))
@include('judging.advancement-vote-form')
@endif
</x-card.list.body>
<x-form.footer><x-form.button class="mb-5">Save Scores</x-form.button></x-form.footer>

View File

@ -8,6 +8,7 @@
<x-table.th>Student Name</x-table.th>
<x-table.th>Total Score</x-table.th>
<x-table.th>All Scores?</x-table.th>
<x-table.th>Votes</x-table.th>
@if($scoringComplete)
<x-table.th>Pass?</x-table.th>
@endif
@ -30,6 +31,29 @@
<x-icons.checkmark color="green"/>
@endif
</x-table.td>
<x-table.td class="flex space-x-1">
@foreach($entry->advancementVotes as $vote)
<div x-data="{ showJudgeName: false, timeout: null }"
x-on:mouseover="clearTimeout(timeout); timeout = setTimeout(() => showJudgeName = true, 300)"
x-on:mouseleave="clearTimeout(timeout); showJudgeName = false">
<x-tooltip x-show="showJudgeName">
{{ $vote->judge->full_name() }}
</x-tooltip>
@switch($vote->vote)
@case('yes')
<x-icons.thumbs-up color="green"/>
@break
@case('no')
<x-icons.thumbs-down color="red"/>
@break
@case('dq')
<x-icons.circle-slash-no color="red"/>
@break
@endswitch
</div>
@endforeach
</x-table.td>
@if( $audition->hasFlag('advancement_published') )
<x-table.td>
@if($entry->hasFlag('will_advance'))

View File

@ -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');
});