From a28380e2fe6604cd8655d859addefbd508ec308f Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 7 Jul 2024 00:24:05 -0500 Subject: [PATCH] Progress on draw. Edit form and destroy method next. --- app/Http/Controllers/Admin/DrawController.php | 44 ++++++- app/Http/Requests/RunDrawRequest.php | 53 ++++++++ app/Providers/AppServiceProvider.php | 6 + app/Services/DrawService.php | 43 +++++++ .../draw/clear-draw-modal-confirm.blade.php | 117 ++++++++++++++++++ .../admin/draw/clear-draw-warning.blade.php | 21 ++++ ...awn-auditions-exist-notification.blade.php | 32 +++++ resources/views/admin/draw/edit.blade.php | 37 ++++++ resources/views/admin/draw/index.blade.php | 5 + .../delete-resource-modal.blade.php | 1 - resources/views/test.blade.php | 3 +- routes/admin.php | 3 +- tests/Feature/Pages/Setup/DrawIndexTest.php | 28 ++++- tests/Feature/Pages/Setup/DrawStoreTest.php | 75 +++++++++++ 14 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 app/Http/Requests/RunDrawRequest.php create mode 100644 app/Services/DrawService.php create mode 100644 resources/views/admin/draw/clear-draw-modal-confirm.blade.php create mode 100644 resources/views/admin/draw/clear-draw-warning.blade.php create mode 100644 resources/views/admin/draw/drawn-auditions-exist-notification.blade.php create mode 100644 resources/views/admin/draw/edit.blade.php create mode 100644 tests/Feature/Pages/Setup/DrawStoreTest.php diff --git a/app/Http/Controllers/Admin/DrawController.php b/app/Http/Controllers/Admin/DrawController.php index e874b90..b89ed75 100644 --- a/app/Http/Controllers/Admin/DrawController.php +++ b/app/Http/Controllers/Admin/DrawController.php @@ -3,14 +3,54 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Requests\RunDrawRequest; +use App\Models\Audition; use App\Models\Event; +use App\Services\DrawService; +use Illuminate\Http\Request; + +use function array_keys; +use function to_route; class DrawController extends Controller { - public function index() + protected $drawService; + + public function __construct(DrawService $drawService) + { + $this->drawService = $drawService; + } + + public function index(Request $request) { $events = Event::with('auditions')->get(); + // $drawnAuditionsExist is true if any audition->hasFlag('drawn') is true + $drawnAuditionsExist = Audition::whereHas('flags', function ($query) { + $query->where('flag_name', 'drawn'); + })->exists(); - return view('admin.draw.index', compact('events')); + return view('admin.draw.index', compact('events', 'drawnAuditionsExist')); + } + + public function store(RunDrawRequest $request) + { + $auditions = Audition::with('flags')->findMany(array_keys($request->input('audition', []))); + + if ($this->drawService->checkCollectionForDrawnAuditions($auditions)) { + return to_route('admin.draw.index')->with('error', + 'Invalid attempt to draw an audition that has already been drawn'); + } + + $this->drawService->runDrawsOnCollection($auditions); + + return to_route('admin.draw.index')->with('status', 'Draw completed successfully'); + } + + public function edit(Request $request) + { + $drawnAuditions = Audition::whereHas('flags', function ($query) { + $query->where('flag_name', 'drawn'); + })->get(); + return view('admin.draw.edit', compact('drawnAuditions')); } } diff --git a/app/Http/Requests/RunDrawRequest.php b/app/Http/Requests/RunDrawRequest.php new file mode 100644 index 0000000..dfb0e22 --- /dev/null +++ b/app/Http/Requests/RunDrawRequest.php @@ -0,0 +1,53 @@ + ['required', 'array'], + ]; + } + + public function messages(): array + { + return [ + 'audition.required' => 'No auditions were selected', + 'audition.array' => 'Invalid request format', + ]; + } + + public function withValidator($validator): void + { + $validator->after(function ($validator) { + foreach ($this->input('audition', []) as $auditionId => $value) { + if (! is_numeric($auditionId) || ! Audition::where('id', $auditionId)->exists()) { + $validator->errors()->add('audition', 'One or more invalid auditions were selected'); + } + } + }); + + } + + protected function failedValidation(Validator $validator) + { + $msg = $validator->errors()->get('audition')[0]; + + return to_route('admin.draw.index')->with('error', $msg); + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8d6a8d7..6d1eea9 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -36,6 +36,7 @@ use App\Observers\SubscoreDefinitionObserver; use App\Observers\UserObserver; use App\Services\AuditionService; use App\Services\DoublerService; +use App\Services\DrawService; use App\Services\EntryService; use App\Services\ScoreService; use App\Services\SeatingService; @@ -43,6 +44,7 @@ use App\Services\TabulationService; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; + class AppServiceProvider extends ServiceProvider { /** @@ -50,6 +52,10 @@ class AppServiceProvider extends ServiceProvider */ public function register(): void { + $this->app->singleton(DrawService::class, function () { + return new DrawService(); + }); + $this->app->singleton(AuditionService::class, function () { return new AuditionService(); }); diff --git a/app/Services/DrawService.php b/app/Services/DrawService.php new file mode 100644 index 0000000..5ee96c6 --- /dev/null +++ b/app/Services/DrawService.php @@ -0,0 +1,43 @@ +where('audition_id', $audition->id)->update(['draw_number' => null]); + + $randomizedEntries = $audition->entries->shuffle(); + foreach ($randomizedEntries as $index => $entry) { + $entry->draw_number = $index + 1; + $entry->save(); + } + $audition->addFlag('drawn'); + } + + public function runDrawsOnCollection($auditions): void + { + $auditions->each(fn ($audition) => $this->runOneDraw($audition)); + } + + public function checkCollectionForDrawnAuditions($auditions): bool + { + + $auditions->loadMissing('flags'); + + return $auditions->contains(fn ($audition) => $audition->hasFlag('drawn')); + } +} diff --git a/resources/views/admin/draw/clear-draw-modal-confirm.blade.php b/resources/views/admin/draw/clear-draw-modal-confirm.blade.php new file mode 100644 index 0000000..56ee072 --- /dev/null +++ b/resources/views/admin/draw/clear-draw-modal-confirm.blade.php @@ -0,0 +1,117 @@ +@php +/** + * @var int $size=20 Size of the icon + */ +@endphp +@props(['size' => 20]) + +
+ + + + + +
diff --git a/resources/views/admin/draw/clear-draw-warning.blade.php b/resources/views/admin/draw/clear-draw-warning.blade.php new file mode 100644 index 0000000..ba9c916 --- /dev/null +++ b/resources/views/admin/draw/clear-draw-warning.blade.php @@ -0,0 +1,21 @@ +
+
+
+ +
+
+

Caution!!!

+
+
    +
  • This will clear any existing draw numbers
  • +
  • Any cards, sign in sheets or other materials you've printed will be invalid
  • +
  • This action cannot be undone
  • +
+
+
+
+
diff --git a/resources/views/admin/draw/drawn-auditions-exist-notification.blade.php b/resources/views/admin/draw/drawn-auditions-exist-notification.blade.php new file mode 100644 index 0000000..7a5d53d --- /dev/null +++ b/resources/views/admin/draw/drawn-auditions-exist-notification.blade.php @@ -0,0 +1,32 @@ +
+
+
+ +
+ +
+
+

Some auditions have already been drawn. + Click here + to clear previous draws.

+
+
+ + +
+
+ +
+
+
+
diff --git a/resources/views/admin/draw/edit.blade.php b/resources/views/admin/draw/edit.blade.php new file mode 100644 index 0000000..28e6eaa --- /dev/null +++ b/resources/views/admin/draw/edit.blade.php @@ -0,0 +1,37 @@ +@php + /** + * @var \App\Models\Audition[] $drawnAuditions A collection of all auditions that have been drawn + */ +@endphp + + + @include('admin.draw.clear-draw-warning') + + + + + Previously Drawn Auditions + + + + + +
+ @foreach($drawnAuditions as $audition) +
+ + {{$audition->name}} +
+ @endforeach +
+
+
+
+ Clear Draw +
+
+ @include('admin.draw.clear-draw-modal-confirm') +
+ +
+
diff --git a/resources/views/admin/draw/index.blade.php b/resources/views/admin/draw/index.blade.php index ac658ee..ad9e992 100644 --- a/resources/views/admin/draw/index.blade.php +++ b/resources/views/admin/draw/index.blade.php @@ -1,10 +1,14 @@ @php /** * @var \App\Models\Event[] $events A collection of all events with auditions + * @var bool $drawnAuditionsExist A boolean value indicating if there are any drawn auditions */ @endphp + @if($drawnAuditionsExist) + @include('admin.draw.drawn-auditions-exist-notification') + @endif @foreach($events as $event) @continue($event->auditions->isEmpty()) @@ -18,6 +22,7 @@
@foreach($event->auditions as $audition) + @continue($audition->hasFlag('drawn'))
{{$audition->name}} diff --git a/resources/views/components/delete-resource-modal.blade.php b/resources/views/components/delete-resource-modal.blade.php index 6c444ea..ad7dc91 100644 --- a/resources/views/components/delete-resource-modal.blade.php +++ b/resources/views/components/delete-resource-modal.blade.php @@ -130,5 +130,4 @@
- diff --git a/resources/views/test.blade.php b/resources/views/test.blade.php index 88a3835..2140a66 100644 --- a/resources/views/test.blade.php +++ b/resources/views/test.blade.php @@ -13,10 +13,11 @@ @inject('auditionService','App\Services\AuditionService'); @inject('entryService','App\Services\EntryService') @inject('seatingService','App\Services\SeatingService') +@inject('drawService', 'App\Services\DrawService') Test Page @php - dump(Audition::open()->get()); + $drawService->hello(); @endphp diff --git a/routes/admin.php b/routes/admin.php index d99f460..918324d 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -76,7 +76,8 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::prefix('draw')->controller(\App\Http\Controllers\Admin\DrawController::class)->group(function () { Route::get('/', 'index')->name('admin.draw.index'); Route::post('/', 'store')->name('admin.draw.store'); - Route::delete('/', 'destroy')->name('admin.draw.destroy'); + Route::get('/clear', 'edit')->name('admin.draw.edit'); // Select auditions for which the user would like to clear the draw + Route::delete('/', 'destroy')->name('admin.draw.destroy'); // Clear the draw for the selected auditions }); // Admin Entries Routes diff --git a/tests/Feature/Pages/Setup/DrawIndexTest.php b/tests/Feature/Pages/Setup/DrawIndexTest.php index 495b594..16193d8 100644 --- a/tests/Feature/Pages/Setup/DrawIndexTest.php +++ b/tests/Feature/Pages/Setup/DrawIndexTest.php @@ -39,7 +39,7 @@ it('has a section for each event that has auditions', function () { } } }); -it('lists auditions in each section', function () { +it('lists auditions that have not been drawn in each section', function () { // Arrange $events = Event::factory()->count(2)->create(); foreach ($events as $event) { @@ -52,12 +52,26 @@ it('lists auditions in each section', function () { foreach ($events as $event) { $response->assertElementExists('#event-section-'.$event->id, function (AssertElement $element) use ($event) { foreach ($event->auditions as $audition) { + if ($audition->hasFlag('drawn')) { + continue; + } $element->contains('#auditiongroup-'.$audition->id); $element->containsText($audition->name); } }); } }); +it('does not list auditions that are already drawn', function () { + // Arrange + $audition = Audition::factory()->create(); + $audition->addFlag('drawn'); + actAsAdmin(); + // Act & Assert + $response = $this->get(route('admin.draw.index')); + $response + ->assertOk() + ->assertDontSee($audition->name); +}); it('each audition has a checkbox with its name', function () { // Arrange $events = Event::factory()->count(2)->create(); @@ -151,3 +165,15 @@ it('submits to the route admin.draw.store and has CSRF protection', function () $form->hasCSRF(); }); }); +it('displays a warning if some auditions are already drawn with a link to undo draws', function () { + // Arrange + $audition = Audition::factory()->create(); + $audition->addFlag('drawn'); + actAsAdmin(); + // Act & Assert + $response = $this->get(route('admin.draw.index')); + $response + ->assertOk() + ->assertSee('Some auditions have already been drawn.', false) + ->assertSee(route('admin.draw.edit'), false); +}); diff --git a/tests/Feature/Pages/Setup/DrawStoreTest.php b/tests/Feature/Pages/Setup/DrawStoreTest.php new file mode 100644 index 0000000..ea291e4 --- /dev/null +++ b/tests/Feature/Pages/Setup/DrawStoreTest.php @@ -0,0 +1,75 @@ +create(); + Entry::factory()->count(10)->create(['audition_id' => $audition->id]); + actAsAdmin(); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->post(route('admin.draw.store'), ['audition' => [$audition->id => 'on']]) + ->assertSessionHasNoErrors() + ->assertSessionHas('status', 'Draw completed successfully') + ->assertRedirect(route('admin.draw.index')); +}); +it('returns an error message if no auditions were selected', function () { + // Arrange + actAsAdmin(); + // Act & Assert + $response = $this->post(route('admin.draw.store')); + $response + ->assertSessionHas('error', 'No auditions were selected') + ->assertRedirect(route('admin.draw.index')); +}); +it('only allows admin to run a draw', function () { + // Act & Assert + $this->post(route('admin.draw.store'))->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.draw.store')) + ->assertSessionHas('error', 'You are not authorized to perform this action') + ->assertRedirect(route('dashboard')); +}); +it('returns with error if a draw is requested for a non-extant audition', function () { + // Arrange + actAsAdmin(); + // Act & Assert + $response = $this->post(route('admin.draw.store', ['audition[999]' => 'on'])); + + $response + ->assertSessionHas('error', 'One or more invalid auditions were selected') + ->assertRedirect(route('admin.draw.index')); +}); +it('sets a drawn flag on the audition once a draw is run', function () { + // Arrange + $audition = Audition::factory()->create(); + actAsAdmin(); + // Act & Assert + $response = $this->post(route('admin.draw.store', ['audition['.$audition->id.']' => 'on'])); + expect($audition->hasFlag('drawn'))->toBeTrue(); +}); +it('refuses to draw an audition that has an existing draw', function () { + // Arrange + $audition = Audition::factory()->create(); + $audition->addFlag('drawn'); + actAsAdmin(); + // Act & Assert + $this->post(route('admin.draw.store', ['audition['.$audition->id.']' => 'on'])) + ->assertSessionHas('error', 'Invalid attempt to draw an audition that has already been drawn') + ->assertRedirect(route('admin.draw.index')); +}); +it('randomizes the order of the entries in the audition', function () { + // Arrange + $audition = Audition::factory()->hasEntries(10)->create(); + $entries = Entry::factory()->count(30)->create(['audition_id' => $audition->id]); + actAsAdmin(); + // Act & Assert + $this->post(route('admin.draw.store'), ['audition['.$audition->id.']' => 'on']); + // assert that entries sorted by draw_number are in a random order + expect($audition->entries->sortBy('draw_number')->pluck('id')) + ->not()->toBe($entries->pluck('id')); +}); +// sets the draw_number column on each entry in the audition based on the randomized entry order