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])
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Really Clear the draw??
+
+
+
+ Click confirm below if you're sure. After doing so, be sure to destroy any materials you may have printed for those
+ auditions to avoid any chance of confusion on audition day.
+
+
+
+
+
+
+
+
+
+
+
+
+
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'))
-
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