Merge pull request #13 from okorpheus/enter-and-store-no-show

Enter and store no show
This commit is contained in:
Matt 2024-07-07 23:32:28 -05:00 committed by GitHub
commit 2779bd5f88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 483 additions and 152 deletions

View File

@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests - paralell" type="PestRunConfigurationType">
<option name="pestRunnerSettings">
<PestRunner directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases" />
</option>
<option name="runnerSettings">
<PhpTestRunnerSettings directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases" />
</option>
<method v="2" />
</configuration>
</component>

View File

@ -181,37 +181,4 @@ class AuditionController extends Controller
return to_route('admin.auditions.index')->with('success', 'Audition deleted successfully'); return to_route('admin.auditions.index')->with('success', 'Audition deleted successfully');
} }
public function prepareDraw()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$allAuditions = Audition::with('entries')->orderBy('score_order')->get();
$nodraw_auditions = $allAuditions->filter(function ($audition) {
return $audition->has_no_draw();
});
$drawn_auditions = $allAuditions->filter(function ($audition) {
return $audition->has_complete_draw();
});
$partial_draw_auditions = $allAuditions->filter(function ($audition) {
return $audition->has_partial_draw();
});
return view('admin.entries.prepare_draw',
compact('nodraw_auditions', 'drawn_auditions', 'partial_draw_auditions'));
}
public function runDraw(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$draw_auditions = Audition::with('entries')->find(array_keys($request->input('auditions')));
foreach ($draw_auditions as $audition) {
$audition->runDraw();
}
return redirect(' / admin / auditions / run_draw');
}
} }

View File

@ -6,10 +6,8 @@ use App\Http\Controllers\Controller;
use App\Models\Audition; use App\Models\Audition;
use App\Models\School; use App\Models\School;
use App\Models\Student; use App\Models\Student;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use function abort; use function abort;
use function to_route; use function to_route;
use function view; use function view;
@ -50,7 +48,7 @@ class StudentController extends Controller
'school_id' => ['required', 'exists:schools,id'], 'school_id' => ['required', 'exists:schools,id'],
]); ]);
$student = Student::create([ Student::create([
'first_name' => request('first_name'), 'first_name' => request('first_name'),
'last_name' => request('last_name'), 'last_name' => request('last_name'),
'grade' => request('grade'), 'grade' => request('grade'),
@ -69,11 +67,12 @@ class StudentController extends Controller
$maxGrade = Audition::max('maximum_grade'); $maxGrade = Audition::max('maximum_grade');
$schools = School::orderBy('name')->get(); $schools = School::orderBy('name')->get();
$student->loadCount('entries'); $student->loadCount('entries');
return view('admin.students.edit', return view('admin.students.edit',
['student' => $student, 'schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]); ['student' => $student, 'schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]);
} }
public function update(Request $request, Student $student) public function update(Student $student)
{ {
if (! Auth::user()->is_admin) { if (! Auth::user()->is_admin) {
abort(403); abort(403);
@ -105,8 +104,7 @@ class StudentController extends Controller
public function destroy(Student $student) public function destroy(Student $student)
{ {
Log::debug('Deleting student '.$student->id); if ($student->entries()->count() > 0) {
if($student->entries()->count() > 0) {
return to_route('admin.students.index')->with('error', 'You cannot delete a student with entries.'); return to_route('admin.students.index')->with('error', 'You cannot delete a student with entries.');
} }
$name = $student->full_name(); $name = $student->full_name();

View File

@ -0,0 +1,103 @@
<?php
namespace App\Http\Controllers\Tabulation;
use App\Http\Controllers\Controller;
use App\Models\Entry;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use function to_route;
class EntryFlagController extends Controller
{
public function noShowSelect()
{
$method = 'GET';
$formRoute = 'entry-flags.confirmNoShow';
return view('tabulation.choose_entry', compact('method', 'formRoute'));
}
public function noShowConfirm(Request $request)
{
$validData = $request->validate([
'entry_id' => 'required|exists:entries,id',
]);
$entry = Entry::with('flags')->withCount('scoreSheets')->find($validData['entry_id']);
// If any results are published, get gone
if ($entry->audition->hasFlag('seats_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advance_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show for an entry in an audition where advancement is published');
}
if ($entry->hasFlag('no_show')) {
$formId = 'no-show-cancellation-form';
$buttonName = 'Remove No Show';
$submitRouteName = 'entry-flags.undoNoShow';
$cardHeading = 'Undo No-Show';
$method = 'DELETE';
} else {
$formId = 'no-show-confirmation-form';
$buttonName = 'Confirm No Show';
$submitRouteName = 'entry-flags.enterNoShow';
$cardHeading = 'Confirm No-Show';
$method = 'POST';
}
$scores = [];
if ($entry->score_sheets_count > 0) {
$scores = $entry->scoreSheets;
$scores->load('judge');
}
return view('tabulation.no_show_confirm',
compact('entry',
'formId',
'buttonName',
'submitRouteName',
'cardHeading',
'method',
'scores'));
}
public function enterNoShow(Entry $entry)
{
if ($entry->audition->hasFlag('seats_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advance_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show for an entry in an audition where advancement is published');
}
DB::table('score_sheets')->where('entry_id', $entry->id)->delete();
$entry->addFlag('no_show');
$msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').';
return to_route('entry-flags.noShowSelect')->with('success', $msg);
}
public function undoNoShow(Entry $entry)
{
if ($entry->audition->hasFlag('seats_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot undo a no-show for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advance_published')) {
return to_route('entry-flags.noShowSelect')->with('error',
'Cannot undo a no-show for an entry in an audition where advancement is published');
}
$entry->removeFlag('no_show');
return to_route('entry-flags.noShowSelect')->with('success',
'No Show status has been removed for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').');
}
}

View File

@ -7,36 +7,44 @@ use App\Models\Entry;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
class ScoreController extends Controller class ScoreController extends Controller
{ {
public function chooseEntry(Request $request) public function chooseEntry(Request $request)
{ {
return view('tabulation.choose_entry'); $method = 'GET';
$formRoute = 'scores.entryScoreSheet';
return view('tabulation.choose_entry', compact('method', 'formRoute'));
} }
public function destroyScore(ScoreSheet $score) { public function destroyScore(ScoreSheet $score)
{
$score->delete(); $score->delete();
return redirect()->back()->with('success','Score Deleted');
return redirect()->back()->with('success', 'Score Deleted');
} }
public function entryScoreSheet(Request $request) public function entryScoreSheet(Request $request)
{ {
$existing_sheets = []; $existing_sheets = [];
$entry = Entry::with(['student','audition.room.judges'])->find($request->input('entry_id')); $entry = Entry::with(['student', 'audition.room.judges'])->find($request->input('entry_id'));
$judges = $entry->audition->room->judges; $judges = $entry->audition->room->judges;
foreach ($judges as $judge) { foreach ($judges as $judge) {
$scoreSheet = ScoreSheet::where('entry_id',$entry->id)->where('user_id',$judge->id)->first(); $scoreSheet = ScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
if ($scoreSheet) { if ($scoreSheet) {
Session::flash('caution','Scores exist for this entry. Now editing existing scores'); Session::flash('caution', 'Scores exist for this entry. Now editing existing scores');
$existing_sheets[$judge->id] = $scoreSheet; $existing_sheets[$judge->id] = $scoreSheet;
} }
} }
$scoring_guide = $entry->audition->scoringGuide; $scoring_guide = $entry->audition->scoringGuide;
$subscores = $entry->audition->scoringGuide->subscores->sortBy('display_order'); $subscores = $entry->audition->scoringGuide->subscores->sortBy('display_order');
if (!$entry) { if (! $entry) {
return redirect()->route('tabulation.chooseEntry')->with('error','Entry not found'); return redirect()->route('tabulation.chooseEntry')->with('error', 'Entry not found');
} }
return view('tabulation.entry_score_sheet', compact('entry','judges','scoring_guide','subscores','existing_sheets'));
return view('tabulation.entry_score_sheet',
compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets'));
} }
public function saveEntryScoreSheet(Request $request, Entry $entry) public function saveEntryScoreSheet(Request $request, Entry $entry)
@ -52,16 +60,17 @@ class ScoreController extends Controller
$scoreValidation = $scoringGuide->validateScores($request->input('judge'.$judge->id)); $scoreValidation = $scoringGuide->validateScores($request->input('judge'.$judge->id));
if ($scoreValidation != 'success') { if ($scoreValidation != 'success') {
return redirect(url()->previous())->with('error', $judge->full_name() . ': ' . $scoreValidation)->with('oldScores',$request->all()); return redirect(url()->previous())->with('error',
$judge->full_name().': '.$scoreValidation)->with('oldScores', $request->all());
} }
$scoreSubmission = $request->input('judge'.$judge->id); $scoreSubmission = $request->input('judge'.$judge->id);
$scoresToSave = []; $scoresToSave = [];
foreach ($subscores as $subscore) { foreach ($subscores as $subscore) {
$scoresToSave[$subscore->id] = [ $scoresToSave[$subscore->id] = [
'subscore_id'=>$subscore->id, 'subscore_id' => $subscore->id,
'subscore_name' => $subscore->name, 'subscore_name' => $subscore->name,
'score' => intval($scoreSubmission[$subscore->id]) 'score' => intval($scoreSubmission[$subscore->id]),
]; ];
} }
$preparedScoreSheets[$judge->id]['scores'] = $scoresToSave; $preparedScoreSheets[$judge->id]['scores'] = $scoresToSave;
@ -72,7 +81,7 @@ class ScoreController extends Controller
['subscores' => $sheet['scores']] ['subscores' => $sheet['scores']]
); );
} }
return redirect()->route('scores.chooseEntry')->with('success',count($preparedScoreSheets) . " Scores saved");
}
return redirect()->route('scores.chooseEntry')->with('success', count($preparedScoreSheets).' Scores saved');
}
} }

View File

@ -25,7 +25,7 @@ class CheckIfCanTab
return $next($request); return $next($request);
} }
return redirect('/')->with('error', 'You do not have access to score tabulation.'); return redirect('dashboard')->with('error', 'You are not authorized to perform this action');
} }
} }

View File

@ -16,14 +16,6 @@ class Audition extends Model
protected $guarded = []; protected $guarded = [];
protected $rankedEntries = null;
protected static $completeAuditions = null;
protected $fully_scored; // Set by TabulationService
protected $scored_entries_count; //Set by TabulationService
public function event(): BelongsTo public function event(): BelongsTo
{ {
return $this->belongsTo(Event::class); return $this->belongsTo(Event::class);
@ -49,84 +41,6 @@ class Audition extends Model
return '$'.number_format($this->entry_fee / 100, 2); return '$'.number_format($this->entry_fee / 100, 2);
} }
public function has_no_draw(): bool
{
// return true if all of my entries have a null draw_number
//return $this->entries->every(fn($entry) => is_null($entry->draw_number));
// return $this->entries()->whereNotNull('draw_number')->doesntExist();
if ($this->entries->count() == 0) {
return false;
}
if ($this->relationLoaded('entries')) {
return $this->entries->every(function ($entry) {
return is_null($entry->draw_number);
});
} else {
return $this->entries()->whereNotNull('draw_number')->doesntExist();
}
}
public function has_complete_draw(): bool
{
// return true if all of my entries have a draw_number
// return $this->entries->every(fn($entry) => !is_null($entry->draw_number));
// return $this->entries()->whereNull('draw_number')->doesntExist();
if ($this->entries->count() == 0) {
return false;
}
if ($this->relationLoaded('entries')) {
return $this->entries->every(function ($entry) {
return ! is_null($entry->draw_number);
});
} else {
return $this->entries()->whereNull('draw_number')->doesntExist();
}
}
public function has_partial_draw(): bool
{
if ($this->entries->count() == 0) {
return false;
}
// return true I have at least one entry with a null draw number AND at least one entry with a non-null draw number
//return $this->entries->contains(fn($entry) => is_null($entry->draw_number)) && $this->entries->contains(fn($entry) => !is_null($entry->draw_number));
//$hasNull = $this->entries()->whereNull('draw_number')->exists();
//$hasNotNull = $this->entries()->whereNotNull('draw_number')->exists();
//return $hasNull && $hasNotNull;
if ($this->relationLoaded('entries')) {
$hasNull = $this->entries->contains(function ($entry) {
return is_null($entry->draw_number);
});
$hasNotNull = $this->entries->contains(function ($entry) {
return ! is_null($entry->draw_number);
});
return $hasNull && $hasNotNull;
} else {
$hasNull = $this->entries()->whereNull('draw_number')->exists();
$hasNotNull = $this->entries()->whereNotNull('draw_number')->exists();
return $hasNull && $hasNotNull;
}
}
public function runDraw(): null
{
$entries = $this->entries->shuffle();
foreach ($entries as $index => $entry) {
$entry->update(['draw_number' => $index + 1]);
$entry->save();
}
return null;
// TODO move all draw functions to a DrawService
}
/** /**
* @return BelongsToMany|User[] * @return BelongsToMany|User[]
*/ */

View File

@ -2,10 +2,12 @@
namespace Database\Factories; namespace Database\Factories;
use App\Models\Ensemble;
use App\Models\Event;
use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Database\Eloquent\Factories\Factory;
/** /**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Ensemble> * @extends Factory<Ensemble>
*/ */
class EnsembleFactory extends Factory class EnsembleFactory extends Factory
{ {
@ -17,8 +19,8 @@ class EnsembleFactory extends Factory
public function definition(): array public function definition(): array
{ {
return [ return [
'name' => $this->faker->name, 'name' => $this->faker->words(4, true),
'event_id' => \App\Models\Event::factory(), 'event_id' => Event::factory(),
'code' => $this->faker->randomLetter().$this->faker->randomLetter, 'code' => $this->faker->randomLetter().$this->faker->randomLetter,
'rank' => $this->faker->numberBetween(1, 10), 'rank' => $this->faker->numberBetween(1, 10),
]; ];

View File

@ -4,7 +4,6 @@ namespace Database\Seeders;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class ScoreAllAuditions extends Seeder class ScoreAllAuditions extends Seeder
@ -16,23 +15,23 @@ class ScoreAllAuditions extends Seeder
{ {
$judges = User::all(); $judges = User::all();
foreach ($judges as $judge) { foreach ($judges as $judge) {
foreach( $judge->rooms as $room) { foreach ($judge->rooms as $room) {
foreach ($room->auditions as $audition){ foreach ($room->auditions as $audition) {
$scoringGuide = $audition->scoringGuide; $scoringGuide = $audition->scoringGuide;
$subscores = $scoringGuide->subscores; $subscores = $scoringGuide->subscores;
foreach ($audition->entries as $entry){ foreach ($audition->entries as $entry) {
$scoreArray = []; $scoreArray = [];
foreach ($subscores as $subscore) { foreach ($subscores as $subscore) {
$scoreArray[$subscore->id] = [ $scoreArray[$subscore->id] = [
'score' => mt_rand(0,100), 'score' => mt_rand(0, 100),
'subscore_id' => $subscore->id, 'subscore_id' => $subscore->id,
'subscore_name' => $subscore->name 'subscore_name' => $subscore->name,
]; ];
} }
ScoreSheet::create([ ScoreSheet::create([
'user_id' => $judge->id, 'user_id' => $judge->id,
'entry_id' => $entry->id, 'entry_id' => $entry->id,
'subscores' => $scoreArray 'subscores' => $scoreArray,
]); ]);
} }
} }

View File

@ -2,7 +2,7 @@
<div x-data="{ toggle: {{ $checked ? 'true':'false'}} }" class="flex items-center"> <div x-data="{ toggle: {{ $checked ? 'true':'false'}} }" class="flex items-center">
<label {{ $attributes->merge(['class'=>'relative inline-flex items-center cursor-pointer']) }}>{{ $slot }} <label {{ $attributes->merge(['class'=>'relative inline-flex items-center cursor-pointer']) }}>{{ $slot }}
<input type="checkbox" x-model="toggle" name="{{ $name }}" class="sr-only" {{ $checked ? 'checked':''}} value="1" > <input type="checkbox" x-model="toggle" name="{{ $name }}" class="sr-only" {{ $checked ? 'checked':''}} value="1" >
<div :class="toggle ? 'bg-blue-600' : 'bg-gray-200'" class="w-11 h-6 rounded-full transition-colors duration-300"></div> <div :class="toggle ? 'bg-indigo-600' : 'bg-gray-200'" class="w-11 h-6 rounded-full transition-colors duration-300"></div>
<div :class="toggle ? 'translate-x-6' : 'translate-x-1'" class="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-300"></div> <div :class="toggle ? 'translate-x-6' : 'translate-x-1'" class="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-300"></div>
</label> </label>
@error($name) @error($name)

View File

@ -1,9 +1,16 @@
@php
/**
* @var string $method Method for the select form
* @var string $formRoute Route for the form action. Should be a route name
*/
@endphp
<x-layout.app> <x-layout.app>
<x-slot:page_title>Choose Entry</x-slot:page_title> <x-slot:page_title>Choose Entry</x-slot:page_title>
<x-card.card class="mx-auto max-w-sm"> <x-card.card class="mx-auto max-w-sm">
<x-card.heading>Choose Entry</x-card.heading> <x-card.heading>Choose Entry</x-card.heading>
<div class=""> <div class="">
<x-form.form method="GET" action="{{ route('scores.entryScoreSheet') }}" class="mb-4 mt-3"> <x-form.form method="{{ $method }}" action="{{ route($formRoute) }}" class="mb-4 mt-3" id="entry-select-form">
<x-form.field name="entry_id" label_text="Entry ID"></x-form.field> <x-form.field name="entry_id" label_text="Entry ID"></x-form.field>
<x-form.footer > <x-form.footer >
<x-form.button>Select</x-form.button> <x-form.button>Select</x-form.button>

View File

@ -0,0 +1,44 @@
<x-layout.app x-data="{ showButton: {{ $scores ? 'false':'true' }} }">
@if($scores)
<x-card.card class="mx-auto max-w-2xl mb-3">
<x-card.heading class="text-red-600">
WARNING: Entry has existing scores
</x-card.heading>
<div class="grid md:grid-cols-3 px-4 space-x-3 pb-6">
@foreach($scores as $score)
<div>
<div class="my-3 border-b">{{ $score->judge->full_name() }}</div>
<ul>
@foreach($score->subscores as $subscore)
<div class="grid grid-cols-2 gap-x-8 border-b">
<span>{{ $subscore['subscore_name'] }}</span>
<span>{{ $subscore['score'] }}</span>
</div>
@endforeach
</ul>
</div>
@endforeach
</div>
<div class="pb-5 pl-5 flex gap-1">
<x-form.checkbox name="confirm" x-model="showButton" /> I understand that marking this entry as a no-show will delete existing scores. The scores cannot be recovered.
</div>
</x-card.card>
@endif
<x-card.card class="mx-auto max-w-md" x-show="showButton" x-cloak>
<x-card.heading>{{ $cardHeading }}</x-card.heading>
<x-card.list.body>
<x-card.list.row>
{{ $entry->student->full_name() }}
<x-card.list.row-text-subtext>{{$entry->school->name}}</x-card.list.row-text-subtext>
</x-card.list.row>
<x-card.list.row>
{{ $entry->audition->name }} #{{ $entry->draw_number ?? ' no draw number' }}
</x-card.list.row>
</x-card.list.body>
<x-form.footer class="mb-4">
<x-form.form method="{{ $method }}" action="{{route($submitRouteName, $entry)}}" id="{{ $formId }}">
<x-form.button type="submit" >{{ $buttonName }}</x-form.button>
</x-form.form>
</x-form.footer>
</x-card.card>
</x-layout.app>

View File

@ -0,0 +1,6 @@
<div class="flex items-center">
<label class="'relative inline-flex items-center cursor-pointer'">{{ $slot }}
<input type="checkbox" x-model="toggle" name="{{ $name }}" class="sr-only" {{ $checked ? 'checked':''}} value="1" >
<div :class="toggle ? 'bg-indigo-600' : 'bg-gray-200'" class="w-11 h-6 rounded-full transition-colors duration-300"></div>
<div :class="toggle ? 'translate-x-6' : 'translate-x-1'" class="absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform duration-300"></div>
</label>

View File

@ -14,6 +14,14 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function ()
Route::delete('/{score}', [\App\Http\Controllers\Tabulation\ScoreController::class, 'destroyScore'])->name('scores.destroy'); Route::delete('/{score}', [\App\Http\Controllers\Tabulation\ScoreController::class, 'destroyScore'])->name('scores.destroy');
}); });
// Entry Flagging
Route::prefix('entry-flags/')->controller(\App\Http\Controllers\Tabulation\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 // Generic Tabulation Routes
Route::prefix('tabulation/')->controller(\App\Http\Controllers\Tabulation\TabulationController::class)->group(function () { Route::prefix('tabulation/')->controller(\App\Http\Controllers\Tabulation\TabulationController::class)->group(function () {
Route::get('/status', 'status')->name('tabulation.status'); Route::get('/status', 'status')->name('tabulation.status');

View File

@ -0,0 +1,105 @@
<?php
use App\Models\Audition;
use App\Models\Entry;
use App\Models\Room;
use App\Models\ScoringGuide;
use App\Models\SubscoreDefinition;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Artisan;
use Sinnbeck\DomAssertions\Asserts\AssertForm;
use function Pest\Laravel\get;
uses(RefreshDatabase::class);
it('only allows an admin or tab user to confirm a no-show', function () {
$entry = Entry::factory()->create();
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry->id]))
->assertRedirect(route('home'));
actAsNormal();
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry->id]))
->assertRedirect(route('dashboard'))
->assertSessionHas('error', 'You are not authorized to perform this action');
actAsTab();
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry->id]))
->assertOk();
actAsAdmin();
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry->id]))
->assertOk();
});
it('will not work with an entry if it is in a published audition', function () {
$entry1 = Entry::factory()->create();
$entry2 = Entry::factory()->create();
$entry1->audition->addFlag('seats_published');
$entry2->audition->addFlag('advance_published');
actAsTab();
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry1->id]))
->assertRedirect(route('entry-flags.noShowSelect'))
->assertSessionHas('error', 'Cannot enter a no-show for an entry in an audition where seats are published');
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry2->id]))
->assertRedirect(route('entry-flags.noShowSelect'))
->assertSessionHas('error', 'Cannot enter a no-show for an entry in an audition where advancement is published');
});
it('has information for the requested entry', function () {
// Arrange
$entry = Entry::factory()->create();
// Act & Assert
actAsTab();
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry->id]))
->assertOk()
->assertSee($entry->student->full_name())
->assertSee($entry->audition->name)
->assertSee($entry->student->school->name)
->assertSee($entry->draw_number);
});
it('posts to entry-flags.enterNoShow and has a submit button', function () {
// Arrange
$entry = Entry::factory()->create();
// Act & Assert
actAsTab();
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry->id]))
->assertOk()
->assertFormExists('#no-show-confirmation-form', function (AssertForm $form) use ($entry) {
$form->hasMethod('POST')
->hasAction(route('entry-flags.enterNoShow', ['entry' => $entry->id]))
->contains('button', ['type' => 'submit'])
->hasCSRF();
});
});
it('posts to entry-flags.undoNoShow and has a remove button if the entry is already a noshow', function () {
// Arrange
$entry = Entry::factory()->create();
$entry->addFlag('no_show');
// Act & Assert
actAsTab();
get(route('entry-flags.confirmNoShow', ['entry_id' => $entry->id]))
->assertOk()
->assertFormExists('#no-show-cancellation-form', function (AssertForm $form) use ($entry) {
$form->hasMethod('POST')
->hasAction(route('entry-flags.undoNoShow', ['entry' => $entry->id]))
->contains('button', ['type' => 'submit'])
->hasCSRF();
});
});
it('shows a box with scores if the entry is scored', function () {
// Arrange
$judges = User::factory()->count(3)->create();
$room = Room::factory()->create();
$judges->each(fn($judge) => $room->addJudge($judge->id));
$scoringGuide = ScoringGuide::factory()->create();
SubscoreDefinition::factory()->count(5)->create(['scoring_guide_id' => $scoringGuide->id]);
$audition = Audition::factory()->create(['room_id' => $room->id, 'scoring_guide_id' => $scoringGuide->id]);
$entry = Entry::factory()->create(['audition_id' => $audition->id]);
// Run the ScoreAllAuditions seeder
Artisan::call('db:seed', ['--class' => 'ScoreAllAuditions']);
// Act & Assert
actAsTab();
$response = get(route('entry-flags.confirmNoShow', ['entry_id' => $entry->id]));
$response->assertOk()
->assertSee('WARNING')
->assertSee('existing scores');
$judges->each(fn($judge) => $response->assertSee($judge->full_name()));
});

View File

@ -0,0 +1,62 @@
<?php
use App\Models\Entry;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\post;
uses(RefreshDatabase::class);
it('only allows an admin or tab user to enter a no-show', function () {
$entry = Entry::factory()->create();
post(route('entry-flags.enterNoShow', $entry))
->assertRedirect(route('home'));
actAsAdmin();
/** @noinspection PhpUnhandledExceptionInspection */
post(route('entry-flags.enterNoShow', $entry))
->assertSessionHasNoErrors()
->assertSessionHas('success',
'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').')
->assertRedirect(route('entry-flags.noShowSelect'));
actAsTab();
/** @noinspection PhpUnhandledExceptionInspection */
post(route('entry-flags.enterNoShow', $entry))
->assertSessionHasNoErrors()
->assertSessionHas('success',
'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').')
->assertRedirect(route('entry-flags.noShowSelect'));
});
it('will not record an entry in a published audition as a no show', function () {
$entry1 = Entry::factory()->create();
$entry2 = Entry::factory()->create();
$entry1->audition->addFlag('seats_published');
$entry2->audition->addFlag('advance_published');
actAsAdmin();
post(route('entry-flags.enterNoShow', $entry1))
->assertRedirect(route('entry-flags.noShowSelect'))
->assertSessionHas('error', 'Cannot enter a no-show for an entry in an audition where seats are published');
post(route('entry-flags.enterNoShow', $entry2))
->assertRedirect(route('entry-flags.noShowSelect'))
->assertSessionHas('error',
'Cannot enter a no-show for an entry in an audition where advancement is published');
});
it('deletes all scores for an entry when a no-show is entered', function () {
$entry = Entry::factory()->create();
$judges = User::factory()->count(3)->create();
$entry->scoreSheets()->create(['user_id' => $judges[0]->id, 'subscores' => 10]);
$entry->scoreSheets()->create(['user_id' => $judges[1]->id, 'subscores' => 10]);
$entry->scoreSheets()->create(['user_id' => $judges[2]->id, 'subscores' => 10]);
expect($entry->scoreSheets()->count())->toBe(3);
actAsAdmin();
post(route('entry-flags.enterNoShow', $entry));
expect($entry->scoreSheets()->count())->toBe(0);
});
it('adds a no_show flag to the entry', function () {
// Arrange
$entry = Entry::factory()->create();
// Act & Assert
actAsAdmin();
post(route('entry-flags.enterNoShow', $entry));
expect(Entry::find($entry->id)->hasFlag('no_show'))->toBeTrue();
});

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Sinnbeck\DomAssertions\Asserts\AssertElement;
use Sinnbeck\DomAssertions\Asserts\AssertForm;
use function Pest\Laravel\get;
uses(RefreshDatabase::class);
it('responds to only admin and tab users', function () {
get(route('entry-flags.noShowSelect'))
->assertRedirect(route('home'));
actAsAdmin();
get(route('entry-flags.noShowSelect'))
->assertOk();
actAsTab();
get(route('entry-flags.noShowSelect'))
->assertOk();
actAsNormal();
get(route('entry-flags.noShowSelect'))
->assertRedirect(route('dashboard'))
->assertSessionHas('error', 'You are not authorized to perform this action');
});
it('has an input for entry_id', function () {
actAsAdmin();
get(route('entry-flags.noShowSelect'))
->assertOk()
->assertElementExists('#entry_id', function (AssertElement $element) {
$element->is('input');
});
});
it('submits to entry-flags.confirmNoShow', function () {
actAsAdmin();
get(route('entry-flags.noShowSelect'))
->assertOk()
->assertFormExists('#entry-select-form', function (AssertForm $form) {
$form->hasMethod('GET')
->hasAction(route('entry-flags.confirmNoShow'));
});
});

View File

@ -0,0 +1,55 @@
<?php
use App\Models\Entry;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\delete;
uses(RefreshDatabase::class);
it('only allows an admin or tab user to undo a no-show', function () {
$entry = Entry::factory()->create();
$entry->addFlag('no_show');
delete(route('entry-flags.undoNoShow', $entry))
->assertRedirect(route('home'));
actAsAdmin();
/** @noinspection PhpUnhandledExceptionInspection */
delete(route('entry-flags.undoNoShow', $entry))
->assertSessionHasNoErrors()
->assertSessionHas('success',
'No Show status has been removed for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').')
->assertRedirect(route('entry-flags.noShowSelect'));
actAsTab();
/** @noinspection PhpUnhandledExceptionInspection */
delete(route('entry-flags.undoNoShow', $entry))
->assertSessionHasNoErrors()
->assertSessionHas('success',
'No Show status has been removed for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').')
->assertRedirect(route('entry-flags.noShowSelect'));
});
it('will not undo a no-show flag for an entry in a published audition', function () {
$entry1 = Entry::factory()->create();
$entry2 = Entry::factory()->create();
$entry1->addFlag('no_show');
$entry2->addFlag('no_show');
$entry1->audition->addFlag('seats_published');
$entry2->audition->addFlag('advance_published');
actAsAdmin();
delete(route('entry-flags.undoNoShow', $entry1))
->assertRedirect(route('entry-flags.noShowSelect'))
->assertSessionHas('error', 'Cannot undo a no-show for an entry in an audition where seats are published');
delete(route('entry-flags.undoNoShow', $entry2))
->assertRedirect(route('entry-flags.noShowSelect'))
->assertSessionHas('error',
'Cannot undo a no-show for an entry in an audition where advancement is published');
});
it('removes a no_show flag to the entry', function () {
// Arrange
$entry = Entry::factory()->create();
$entry->addFlag('no_show');
// Act & Assert
actAsAdmin();
delete(route('entry-flags.undoNoShow', $entry));
expect(Entry::find($entry->id)->hasFlag('no_show'))->toBeFalse();
});