From 575ce9854be5ab4fcb8f119cc3bf595fd142f280 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sat, 6 Jul 2024 09:34:23 -0500 Subject: [PATCH 1/3] Draw Index complete --- app/Http/Controllers/Admin/DrawController.php | 16 ++ app/Models/Event.php | 2 +- resources/views/admin/draw/index.blade.php | 36 +++++ .../views/components/form/checkbox.blade.php | 15 +- .../layout/navbar/menus/setup.blade.php | 2 +- routes/admin.php | 11 +- tests/Feature/Models/EventTest.php | 2 +- tests/Feature/Pages/Setup/DrawIndexTest.php | 153 ++++++++++++++++++ 8 files changed, 229 insertions(+), 8 deletions(-) create mode 100644 app/Http/Controllers/Admin/DrawController.php create mode 100644 resources/views/admin/draw/index.blade.php create mode 100644 tests/Feature/Pages/Setup/DrawIndexTest.php diff --git a/app/Http/Controllers/Admin/DrawController.php b/app/Http/Controllers/Admin/DrawController.php new file mode 100644 index 0000000..e874b90 --- /dev/null +++ b/app/Http/Controllers/Admin/DrawController.php @@ -0,0 +1,16 @@ +get(); + + return view('admin.draw.index', compact('events')); + } +} diff --git a/app/Models/Event.php b/app/Models/Event.php index 2df0d6a..b8b9726 100644 --- a/app/Models/Event.php +++ b/app/Models/Event.php @@ -13,7 +13,7 @@ class Event extends Model public function auditions(): HasMany { - return $this->hasMany(Audition::class); + return $this->hasMany(Audition::class)->orderBy('score_order'); } public function ensembles(): HasMany diff --git a/resources/views/admin/draw/index.blade.php b/resources/views/admin/draw/index.blade.php new file mode 100644 index 0000000..ac658ee --- /dev/null +++ b/resources/views/admin/draw/index.blade.php @@ -0,0 +1,36 @@ +@php + /** + * @var \App\Models\Event[] $events A collection of all events with auditions + */ +@endphp + + + + @foreach($events as $event) + @continue($event->auditions->isEmpty()) + + + {{ $event->name }} + + + + + +
+ @foreach($event->auditions as $audition) +
+ + {{$audition->name}} +
+ @endforeach +
+
+
+
+ Run Draw +
+
+
+ @endforeach +
+
diff --git a/resources/views/components/form/checkbox.blade.php b/resources/views/components/form/checkbox.blade.php index ea0af3a..fbba523 100644 --- a/resources/views/components/form/checkbox.blade.php +++ b/resources/views/components/form/checkbox.blade.php @@ -1,7 +1,16 @@ -@props(['name','label' => false,'description' => '', 'checked' => false]) +@props(['name', +'label' => false, +'description' => '', + 'checked' => false, + 'id' => false]) +@php + if(! $id): + $id = $name; + endif; +@endphp
-
@if($label) - +

{{ $description }}

@endif @error($name) diff --git a/resources/views/components/layout/navbar/menus/setup.blade.php b/resources/views/components/layout/navbar/menus/setup.blade.php index ddcf040..42c4d80 100644 --- a/resources/views/components/layout/navbar/menus/setup.blade.php +++ b/resources/views/components/layout/navbar/menus/setup.blade.php @@ -28,7 +28,7 @@ Scoring Rooms Judges - Run Draw + Run Draw
diff --git a/routes/admin.php b/routes/admin.php index aae16f1..d99f460 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -68,8 +68,15 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::patch('/{audition}', 'update')->name('admin.auditions.update'); Route::post('/reorder', 'reorder')->name('admin.auditions.reorder'); Route::delete('/{audition}', 'destroy')->name('admin.auditions.destroy'); - Route::get('/run_draw', 'prepareDraw')->name('admin.auditions.prepareDraw'); - Route::post('/run_draw', 'runDraw')->name('admin.auditions.runDraw'); + #Route::get('/run_draw', 'prepareDraw')->name('admin.auditions.prepareDraw'); + #Route::post('/run_draw', 'runDraw')->name('admin.auditions.runDraw'); + }); + + // Admin Audition Draw Routes + 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'); }); // Admin Entries Routes diff --git a/tests/Feature/Models/EventTest.php b/tests/Feature/Models/EventTest.php index 18fa8ca..a97f644 100644 --- a/tests/Feature/Models/EventTest.php +++ b/tests/Feature/Models/EventTest.php @@ -9,7 +9,7 @@ uses(RefreshDatabase::class); it('has auditions', function () { $event = Event::factory()->create(); - $ddAudition = Audition::factory()->create(['event_id' => $event->id, 'name' => 'Digereedoo']); + Audition::factory()->create(['event_id' => $event->id, 'name' => 'Digereedoo','score_order' => 0]); Audition::factory()->count(7)->create(['event_id' => $event->id]); expect($event->auditions->count())->toBe(8) diff --git a/tests/Feature/Pages/Setup/DrawIndexTest.php b/tests/Feature/Pages/Setup/DrawIndexTest.php new file mode 100644 index 0000000..495b594 --- /dev/null +++ b/tests/Feature/Pages/Setup/DrawIndexTest.php @@ -0,0 +1,153 @@ +get(route('admin.draw.index'))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.draw.index')) + ->assertSessionHas('error', 'You are not authorized to perform this action') + ->assertRedirect(route('dashboard')); + +}); +it('returns the view admin.draw.index', function () { + actAsAdmin(); + $response = $this->get(route('admin.draw.index')); + $response->assertViewIs('admin.draw.index'); +}); +it('has a section for each event that has auditions', function () { + // Arrange + $events = Event::factory()->count(3)->create(); + Audition::factory()->create(['event_id' => $events[0]->id]); + Audition::factory()->create(['event_id' => $events[1]->id]); + actAsAdmin(); + // Act + $response = $this->get(route('admin.draw.index')); + // Assert + foreach ($events as $event) { + if ($event->auditions->count() > 0) { + $response->assertElementExists('#event-section-'.$event->id); + } else { + $response->assertDontSee('id="event-section-'.$event->id, false); + } + } +}); +it('lists auditions in each section', function () { + // Arrange + $events = Event::factory()->count(2)->create(); + foreach ($events as $event) { + Audition::factory()->count(5)->create(['event_id' => $event->id]); + } + actAsAdmin(); + // Act + $response = $this->get(route('admin.draw.index')); + // Assert + foreach ($events as $event) { + $response->assertElementExists('#event-section-'.$event->id, function (AssertElement $element) use ($event) { + foreach ($event->auditions as $audition) { + $element->contains('#auditiongroup-'.$audition->id); + $element->containsText($audition->name); + } + }); + } +}); +it('each audition has a checkbox with its name', function () { + // Arrange + $events = Event::factory()->count(2)->create(); + foreach ($events as $event) { + Audition::factory()->count(5)->create(['event_id' => $event->id]); + } + actAsAdmin(); + // Act + $response = $this->get(route('admin.draw.index')); + $response->assertOk(); + // Assert + foreach ($events as $event) { + $response->assertElementExists('#event-section-'.$event->id, function (AssertElement $element) use ($event) { + foreach ($event->auditions as $audition) { + $element->contains('#auditiongroup-'.$audition->id, function (AssertElement $element) use ($audition) { + $element->containsText($audition->name); + }); + $element->contains('#auditionCheckbox-'.$audition->id); + } + }); + } +}); +it('lists auditions in score order', function () { + // Arrange + $event = Event::factory()->create(); + $third = Audition::factory()->create(['event_id' => $event->id, 'score_order' => 3]); + $first = Audition::factory()->create(['event_id' => $event->id, 'score_order' => 1]); + $fourth = Audition::factory()->create(['event_id' => $event->id, 'score_order' => 4]); + $second = Audition::factory()->create(['event_id' => $event->id, 'score_order' => 2]); + actAsAdmin(); + // Act & Assert + $response = $this->get(route('admin.draw.index')); + $response->assertOk(); + $response->assertSeeInOrder([ + e($first->name), + e($second->name), + e($third->name), + e($fourth->name), + ], false); +}); +it('has a form wrapping all event sections', function () { + // Arrange + $events = Event::factory()->count(2)->create(); + foreach ($events as $event) { + Audition::factory()->count(5)->create(['event_id' => $event->id]); + } + actAsAdmin(); + // Act + $response = $this->get(route('admin.draw.index')); + // Assert + $response + ->assertOk() + ->assertElementExists('#draw-form', function (AssertElement $element) use ($events) { + foreach ($events as $event) { + $element->contains('#event-section-'.$event->id); + } + }); + +}); +it('has a submit button in each event section', function () { + // Arrange + $events = Event::factory()->count(3)->create(); + Audition::factory()->create(['event_id' => $events[0]->id]); + Audition::factory()->create(['event_id' => $events[1]->id]); + actAsAdmin(); + // Act + $response = $this->get(route('admin.draw.index')); + // Assert + foreach ($events as $event) { + if ($event->auditions->count() > 0) { + $response->assertElementExists('#event-section-'.$event->id, function (AssertElement $element) { + $element->contains('button[type="submit"]'); + }); + } + } +}); +it('submits to the route admin.draw.store and has CSRF protection', function () { + // Arrange + $events = Event::factory()->count(3)->create(); + foreach ($events as $event) { + Audition::factory()->create(['event_id' => $event->id]); + } + actAsAdmin(); + // Act + $response = $this->get(route('admin.draw.index')); + // Assert + $response + ->assertOk() + ->assertFormExists('#draw-form', function (AssertForm $form) { + $form->hasAction(route('admin.draw.store')); + $form->hasCSRF(); + }); +}); -- 2.39.5 From a28380e2fe6604cd8655d859addefbd508ec308f Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 7 Jul 2024 00:24:05 -0500 Subject: [PATCH 2/3] 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 -- 2.39.5 From 817eb574bdde29b1792fe36f82cc8550d9fb9141 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Sun, 7 Jul 2024 15:57:00 -0500 Subject: [PATCH 3/3] Draw functionality complete --- app/Http/Controllers/Admin/DrawController.php | 12 +++ app/Http/Requests/ClearDrawRequest.php | 53 +++++++++++++ app/Services/DrawService.php | 11 +++ tests/Feature/Pages/Setup/DrawEditTest.php | 77 +++++++++++++++++++ tests/Feature/Pages/Setup/DrawStoreTest.php | 22 +++++- 5 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 app/Http/Requests/ClearDrawRequest.php create mode 100644 tests/Feature/Pages/Setup/DrawEditTest.php diff --git a/app/Http/Controllers/Admin/DrawController.php b/app/Http/Controllers/Admin/DrawController.php index b89ed75..6578e94 100644 --- a/app/Http/Controllers/Admin/DrawController.php +++ b/app/Http/Controllers/Admin/DrawController.php @@ -3,12 +3,14 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Requests\ClearDrawRequest; use App\Http\Requests\RunDrawRequest; use App\Models\Audition; use App\Models\Event; use App\Services\DrawService; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Log; use function array_keys; use function to_route; @@ -51,6 +53,16 @@ class DrawController extends Controller $drawnAuditions = Audition::whereHas('flags', function ($query) { $query->where('flag_name', 'drawn'); })->get(); + return view('admin.draw.edit', compact('drawnAuditions')); } + + public function destroy(ClearDrawRequest $request) + { + $auditions = Audition::with('flags')->findMany(array_keys($request->input('audition', []))); + $this->drawService->clearDrawsOnCollection($auditions); + + return to_route('admin.draw.index')->with('status', 'Draw completed successfully'); + + } } diff --git a/app/Http/Requests/ClearDrawRequest.php b/app/Http/Requests/ClearDrawRequest.php new file mode 100644 index 0000000..d4b12ee --- /dev/null +++ b/app/Http/Requests/ClearDrawRequest.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/Services/DrawService.php b/app/Services/DrawService.php index 5ee96c6..53d162c 100644 --- a/app/Services/DrawService.php +++ b/app/Services/DrawService.php @@ -40,4 +40,15 @@ class DrawService return $auditions->contains(fn ($audition) => $audition->hasFlag('drawn')); } + + public function clearDrawForAudition(Audition $audition): void + { + $audition->removeFlag('drawn'); + DB::table('entries')->where('audition_id', $audition->id)->update(['draw_number' => null]); + } + + public function clearDrawsOnCollection($auditions): void + { + $auditions->each(fn ($audition) => $this->clearDrawForAudition($audition)); + } } diff --git a/tests/Feature/Pages/Setup/DrawEditTest.php b/tests/Feature/Pages/Setup/DrawEditTest.php new file mode 100644 index 0000000..1bd3dae --- /dev/null +++ b/tests/Feature/Pages/Setup/DrawEditTest.php @@ -0,0 +1,77 @@ +get(route('admin.draw.edit')) + ->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.draw.edit')) + ->assertSessionHas('error', 'You are not authorized to perform this action') + ->assertRedirect(route('dashboard')); + actAsAdmin(); + $this->get(route('admin.draw.edit')) + ->assertOk() + ->assertViewIs('admin.draw.edit'); + +}); +it('lists auditions that have been drawn', function () { + $audition = Audition::factory()->create(); + $drawnAudition = Audition::factory()->create(); + $drawnAudition->addFlag('drawn'); + actAsAdmin(); + $response = $this->get(route('admin.draw.edit')); + $response->assertOk() + ->assertSee($drawnAudition->name) + ->assertDontSee($audition->name); +}); +it('has a warning message', function () { + actAsAdmin(); + $response = $this->get(route('admin.draw.edit')); + $response->assertOk() + ->assertSee('Caution'); +}); + +it('submits to the admin.draw.destroy route', function () { + actAsAdmin(); + $this->get(route('admin.draw.edit')) + ->assertOk() + ->assertSee(route('admin.draw.destroy')); +}); +it('does not allow a non-admin user to clear a draw', function () { + $this->delete(route('admin.draw.destroy')) + ->assertRedirect(route('home')); + actAsNormal(); + $this->delete(route('admin.draw.destroy')) + ->assertSessionHas('error', 'You are not authorized to perform this action') + ->assertRedirect(route('dashboard')); +}); +it('allows an administrator to clear a draw', function () { + $audition = Audition::factory()->create(); + $audition->addFlag('drawn'); + $entries = Entry::factory()->count(3)->create(['audition_id' => $audition->id]); + $n = 1; + $entries->each(function ($entry) use (&$n) { + $entry->draw_number = $n; + $entry->save(); + $n++; + }); + actAsAdmin(); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->delete(route('admin.draw.destroy'), ['audition' => [$audition->id => 'on']]) + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.draw.index')); + + $entries->each(function ($entry) { + $entry->refresh(); + expect($entry->draw_number)->toBeNull(); + }); + + $checkAudition = Audition::find($audition->id); + expect($checkAudition->hasFlag('drawn'))->toBeFalse(); +}); diff --git a/tests/Feature/Pages/Setup/DrawStoreTest.php b/tests/Feature/Pages/Setup/DrawStoreTest.php index ea291e4..ad3e63f 100644 --- a/tests/Feature/Pages/Setup/DrawStoreTest.php +++ b/tests/Feature/Pages/Setup/DrawStoreTest.php @@ -2,6 +2,7 @@ use App\Models\Audition; use App\Models\Entry; +use App\Services\DrawService; use Illuminate\Foundation\Testing\RefreshDatabase; uses(RefreshDatabase::class); @@ -48,7 +49,7 @@ it('sets a drawn flag on the audition once a draw is run', function () { $audition = Audition::factory()->create(); actAsAdmin(); // Act & Assert - $response = $this->post(route('admin.draw.store', ['audition['.$audition->id.']' => 'on'])); + $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 () { @@ -72,4 +73,21 @@ it('randomizes the order of the entries in the audition', function () { 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 +it('sets the draw_number column on each entry in the audition based on the randomized entry order', function () { + // Arrange + $audition = Audition::factory()->hasEntries(10)->create(); + Entry::all()->each(fn ($entry) => expect($entry->draw_number)->toBeNull()); + $drawService = new DrawService(); + $drawService->runOneDraw($audition); + // Act & Assert + Entry::all()->each(fn ($entry) => expect($entry->draw_number)->not()->toBeNull()); +}); +it('only sets draw numbers in the specified audition', function () { + // Arrange + $audition = Audition::factory()->hasEntries(10)->create(); + $bonusEntry = Entry::factory()->create(); + $drawService = new DrawService(); + $drawService->runOneDraw($audition); + // Act & Assert + expect($bonusEntry->draw_number)->toBeNull(); +}); -- 2.39.5