From 0cb5981b7c5cc554194bfed187201050d4bdd405 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 17 Jul 2024 11:26:29 -0500 Subject: [PATCH 1/5] CreateEntry action functions properly #29 Creating an entry should check on the status of the draw and respond appropriately --- app/Actions/CreateEntry.php | 82 ++++++++++++++ app/Exceptions/CreateEntryException.php | 9 ++ tests/Feature/Actions/CreateEntryTest.php | 127 ++++++++++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 app/Actions/CreateEntry.php create mode 100644 app/Exceptions/CreateEntryException.php create mode 100644 tests/Feature/Actions/CreateEntryTest.php diff --git a/app/Actions/CreateEntry.php b/app/Actions/CreateEntry.php new file mode 100644 index 0000000..a3bf35d --- /dev/null +++ b/app/Actions/CreateEntry.php @@ -0,0 +1,82 @@ +createEntry($student, $audition, $entry_for); + } + + public function createEntry(Student $student, Audition $audition, string|array|null $entry_for = null) + { + if (! $entry_for) { + $entry_for = ['seating', 'advancement']; + } + $entry_for = collect($entry_for); + $this->verifySubmission($student, $audition); + $entry = Entry::make([ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + 'draw_number' => $this->checkDraw($audition), + 'for_seating' => $entry_for->contains('seating'), + 'for_advancement' => $entry_for->contains('advancement'), + ]); + $entry->save(); + + return $entry; + } + + private function checkDraw(Audition $audition) + { + if (! $audition->hasFlag('drawn')) { + return null; + } + // get the maximum value of draw_number from $audition->entries() + $draw_number = $audition->entries()->max('draw_number'); + + return $draw_number + 1; + } + + private function verifySubmission(Student $student, Audition $audition): void + { + // Make sure it's a valid student + if (! $student->exists()) { + throw new CreateEntryException('Invalid student provided'); + } + // Make sure the audition is valid + if (! $audition->exists()) { + throw new CreateEntryException('Invalid audition provided'); + } + // A student can't enter the same audition twice + if (Entry::where('student_id', $student->id)->where('audition_id', $audition->id)->exists()) { + throw new CreateEntryException('That student is already entered in that audition'); + } + // Can't enter a published audition + if ($audition->hasFlag('seats_published')) { + throw new CreateEntryException('Cannot add an entry to an audition where seats are published'); + } + if ($audition->hasFlag('advancement_published')) { + throw new CreateEntryException('Cannot add an entry to an audition where advancement is published'); + } + // Verify the grade of the student is in range for the audition + if ($student->grade > $audition->maximum_grade) { + throw new CreateEntryException('The grade of the student exceeds the maximum for that audition'); + } + if ($student->grade < $audition->minimum_grade) { + throw new CreateEntryException('The grade of the student does not meet the minimum for that audition'); + } + } +} diff --git a/app/Exceptions/CreateEntryException.php b/app/Exceptions/CreateEntryException.php new file mode 100644 index 0000000..8dd356f --- /dev/null +++ b/app/Exceptions/CreateEntryException.php @@ -0,0 +1,9 @@ +createEntry = App::make(CreateEntry::class); +}); + +it('throws an exception if the student does not exist', function () { + $audition = Audition::factory()->create(); + $student = Student::factory()->make(); + $this->createEntry->__invoke($student, $audition); +})->throws(CreateEntryException::class, 'Invalid student provided'); +it('throws an exception if the audition does not exist', function () { + $audition = Audition::factory()->make(); + $student = Student::factory()->create(); + $this->createEntry->__invoke($student, $audition); +})->throws(CreateEntryException::class, 'Invalid audition provided'); +it('throws an exception if the student is already entered in the audition', function () { + // Arrange + $audition = Audition::factory()->create(); + $student = Student::factory()->create(); + $entry = Entry::create([ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + ]); + // Act & Assert + $this->createEntry->createEntry($student, $audition); +})->throws(CreateEntryException::class, 'That student is already entered in that audition'); +it('throws an exception if seats are published for the audition', function () { + // Arrange + $audition = Audition::factory()->create(); + $student = Student::factory()->create(); + $audition->addFlag('seats_published'); + // Act & Assert + $this->createEntry->createEntry($student, $audition); +})->throws(CreateEntryException::class, 'Cannot add an entry to an audition where seats are published'); +it('throws an exception if advancement is published for the audition', function () { + // Arrange + $audition = Audition::factory()->create(); + $student = Student::factory()->create(); + $audition->addFlag('advancement_published'); + // Act & Assert + $this->createEntry->createEntry($student, $audition); +})->throws(CreateEntryException::class, 'Cannot add an entry to an audition where advancement is published'); +it('throws an exception if the grade of the student exceeds the maximum grade for the audition', function () { + // Arrange + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 11]); + // Act & Assert + $this->createEntry->createEntry($student, $audition); +})->throws(CreateEntryException::class, 'The grade of the student exceeds the maximum for that audition'); +it('throws an exception if the grade of the student does not meet the minimum grade for the audition', function () { + // Arrange + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 7]); + // Act & Assert + $this->createEntry->createEntry($student, $audition); +})->throws(CreateEntryException::class, 'The grade of the student does not meet the minimum for that audition'); +it('returns an entry object', function () { + // Arrange + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 8]); + // Act & Assert + $entry = $this->createEntry->createEntry($student, $audition); + expect($entry instanceof Entry)->toBeTrue(); +}); +it('creates an entry with a null draw number if the audition is not drawn', function () { + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 8]); + // Act & Assert + $entry = $this->createEntry->createEntry($student, $audition); + expect($entry->draw_number)->toBeNull(); +}); +it('assigns the next highest draw_number available if the audition is drawn', function () { + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 8]); + $audition->addFlag('drawn'); + foreach (range(1, 5) as $number) { + Entry::factory()->create([ + 'audition_id' => $audition->id, + 'draw_number' => $number, + ]); + } + $entry = $this->createEntry->createEntry($student, $audition); + expect($entry->draw_number)->toBe(6); +}); +it('makes the entry for both seating and advancement if nothing is specified', function () { + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 8]); + // Act & Assert + $entry = $this->createEntry->createEntry($student, $audition); + expect($entry->for_seating)->toBeTrue() + ->and($entry->for_advancement)->toBeTrue(); +}); +it('makes the entry for only seating if only seating is specified', function () { + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 8]); + // Act & Assert + $entry = $this->createEntry->createEntry($student, $audition, 'seating'); + expect($entry->for_seating)->toBeTrue() + ->and($entry->for_advancement)->toBeFalse(); +}); +it('makes the entry for only advancement if only advancement is specified', function () { + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 8]); + // Act & Assert + $entry = $this->createEntry->createEntry($student, $audition, 'advancement'); + expect($entry->for_seating)->toBeFalse() + ->and($entry->for_advancement)->toBeTrue(); +}); +it('makes the entry for both seating and advancement if both are specified', function () { + $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $student = Student::factory()->create(['grade' => 8]); + // Act & Assert + $entry = $this->createEntry->createEntry($student, $audition, ['seating', 'advancement']); + expect($entry->for_seating)->toBeTrue() + ->and($entry->for_advancement)->toBeTrue(); +}); From 19152f4f634622f2f0af19ca3ff41ff6ab5ca07f Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 17 Jul 2024 11:45:29 -0500 Subject: [PATCH 2/5] User createEntry action in user entry creation #29 Creating an entry should check on the status of the draw and respond appropriately --- app/Actions/CreateEntry.php | 20 ++++++++++++++++---- app/Http/Controllers/EntryController.php | 24 ++++++++++++++++-------- tests/Feature/Pages/EntriesIndexTest.php | 5 +++-- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/app/Actions/CreateEntry.php b/app/Actions/CreateEntry.php index a3bf35d..1cfacc6 100644 --- a/app/Actions/CreateEntry.php +++ b/app/Actions/CreateEntry.php @@ -1,7 +1,5 @@ createEntry($student, $audition, $entry_for); } - public function createEntry(Student $student, Audition $audition, string|array|null $entry_for = null) + /** + * @throws CreateEntryException + */ + public function createEntry(Student|int $student, Audition|int $audition, string|array|null $entry_for = null) { + if (is_int($student)) { + $student = Student::find($student); + } + if (is_int($audition)) { + $audition = Audition::find($audition); + } + if (! $entry_for) { $entry_for = ['seating', 'advancement']; } @@ -50,6 +61,7 @@ class CreateEntry return $draw_number + 1; } + /** @noinspection PhpUnhandledExceptionInspection */ private function verifySubmission(Student $student, Audition $audition): void { // Make sure it's a valid student diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index c102318..3180d46 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -2,6 +2,8 @@ namespace App\Http\Controllers; +use App\Actions\CreateEntry; +use App\Exceptions\CreateEntryException; use App\Models\Audition; use App\Models\Entry; use Illuminate\Http\Request; @@ -25,7 +27,7 @@ class EntryController extends Controller return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]); } - public function store(Request $request) + public function store(Request $request, CreateEntry $creator) { if ($request->user()->cannot('create', Entry::class)) { abort(403); @@ -37,15 +39,21 @@ class EntryController extends Controller $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; + $enter_for = []; + if ($validData['for_seating']) { + $enter_for[] = 'seating'; + } + if ($validData['for_advancement']) { + $enter_for[] = 'advancement'; + } - $entry = Entry::create([ - 'student_id' => $validData['student_id'], - 'audition_id' => $validData['audition_id'], - 'for_seating' => $validData['for_seating'], - 'for_advancement' => $validData['for_advancement'], - ]); + try { + $creator($validData['student_id'], $validData['audition_id'], $enter_for); + } catch (CreateEntryException $ex) { + return redirect()->route('entries.index')->with('error', $ex->getMessage()); + } - return redirect('/entries'); + return redirect()->route('entries.index')->with('success', 'The entry has been added.'); } public function destroy(Request $request, Entry $entry) diff --git a/tests/Feature/Pages/EntriesIndexTest.php b/tests/Feature/Pages/EntriesIndexTest.php index fd47344..6ecc175 100644 --- a/tests/Feature/Pages/EntriesIndexTest.php +++ b/tests/Feature/Pages/EntriesIndexTest.php @@ -133,8 +133,8 @@ it('shows appropriate flags for entry types when advancement is enabled', functi it('accepts a valid entry', function () { // Arrange - $student = Student::factory()->create(['school_id' => $this->school->id]); - $audition = Audition::factory()->create(); + $student = Student::factory()->create(['school_id' => $this->school->id, 'grade' => 8]); + $audition = Audition::factory()->create(['maximum_grade' => 9, 'minimum_grade' => 7]); // Act & Assert actingAs($this->user); $response = post(route('entries.store'), [ @@ -144,6 +144,7 @@ it('accepts a valid entry', function () { /** @noinspection PhpUnhandledExceptionInspection */ $response->assertSessionHasNoErrors(); $response->assertRedirect(route('entries.index')); + $response->assertSessionHas('success', 'The entry has been added.'); $this->assertDatabaseHas('entries', [ 'student_id' => $student->id, 'audition_id' => $audition->id, From d292331a1e494bf40edc36a008c17d60370bdfa0 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 17 Jul 2024 14:04:30 -0500 Subject: [PATCH 3/5] Admin createEntry action in user entry creation #29 Creating an entry should check on the status of the draw and respond appropriately Also addresses #37 --- .../Controllers/Admin/EntryController.php | 25 +++++++---- app/Http/Controllers/EntryController.php | 4 ++ .../Feature/Pages/Admin/EntiesCreateTest.php | 42 ++++++++++++++++++- tests/Feature/Pages/EntriesIndexTest.php | 19 +++++++++ 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index a04da74..ab55ea0 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -2,7 +2,9 @@ namespace App\Http\Controllers\Admin; +use App\Actions\CreateEntry; use App\Actions\Tabulation\CalculateEntryScore; +use App\Exceptions\CreateEntryException; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Entry; @@ -87,7 +89,7 @@ class EntryController extends Controller return view('admin.entries.create', ['students' => $students, 'auditions' => $auditions]); } - public function store(Request $request) + public function store(Request $request, CreateEntry $creator) { if (! Auth::user()->is_admin) { abort(403); @@ -99,15 +101,21 @@ class EntryController extends Controller $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; + $enter_for = []; + if ($validData['for_seating']) { + $enter_for[] = 'seating'; + } + if ($validData['for_advancement']) { + $enter_for[] = 'advancement'; + } - Entry::create([ - 'student_id' => $validData['student_id'], - 'audition_id' => $validData['audition_id'], - 'for_seating' => $validData['for_seating'], - 'for_advancement' => $validData['for_advancement'], - ]); + try { + $creator($validData['student_id'], $validData['audition_id'], $enter_for); + } catch (CreateEntryException $ex) { + return redirect()->route('admin.entries.index')->with('error', $ex->getMessage()); + } - return redirect('/admin/entries'); + return redirect(route('admin.entries.index'))->with('success', 'The entry has been added.'); } public function edit(Entry $entry, CalculateEntryScore $calculator) @@ -126,6 +134,7 @@ class EntryController extends Controller $auditions = Audition::orderBy('score_order')->get(); $scores = $entry->scoreSheets()->with('audition', 'judge')->get(); $scores->each(fn ($score) => $score->entry = $entry); + // return view('admin.entries.edit', ['entry' => $entry, 'students' => $students, 'auditions' => $auditions]); return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores')); } diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index 3180d46..f2e7ca4 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -36,6 +36,10 @@ class EntryController extends Controller 'student_id' => ['required', 'exists:students,id'], 'audition_id' => ['required', 'exists:auditions,id'], ]); + $audition = Audition::find($validData['audition_id']); + if ($audition->entry_deadline < now()) { + return redirect()->route('entries.index')->with('error', 'The entry deadline for that audition has passed'); + } $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; diff --git a/tests/Feature/Pages/Admin/EntiesCreateTest.php b/tests/Feature/Pages/Admin/EntiesCreateTest.php index dc71571..3142c0f 100644 --- a/tests/Feature/Pages/Admin/EntiesCreateTest.php +++ b/tests/Feature/Pages/Admin/EntiesCreateTest.php @@ -4,6 +4,7 @@ use App\Models\Audition; use App\Models\Student; use Illuminate\Foundation\Testing\RefreshDatabase; +use function Pest\Laravel\assertDatabaseHas; use function Pest\Laravel\get; use function PHPUnit\Framework\assertEquals; @@ -19,7 +20,7 @@ it('does not respond to a guest', function () { get(route('admin.entries.create')) ->assertRedirect(route('home')); }); -it('passes a collection of all students with thier schools to the view', function () { +it('passes a collection of all students with their schools to the view', function () { // Arrange Student::factory()->count(8)->create(); $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get(); @@ -47,3 +48,42 @@ it('passes a collection of available auditions to the view', function () { $response->assertOk(); assertEquals(array_values($auditions), array_values($viewAuditions)); }); +it('can create an entry', function () { + $audition = Audition::factory()->create(['maximum_grade' => 12, 'minimum_grade' => 7]); + $student = Student::factory()->create(['grade' => 9]); + actAsAdmin(); + $response = $this->post(route('admin.entries.store'), [ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + 'for_seating' => 'on', + ]); + $response->assertRedirect(route('admin.entries.index')) + ->assertSessionDoesntHaveErrors() + ->assertSessionHas('success', 'The entry has been added.'); + assertDatabaseHas('entries', [ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + 'for_seating' => 1, + 'for_advancement' => 0, + ]); +}); +it('can create a late entry', function () { + $audition = Audition::factory()->closed()->create(['maximum_grade' => 12, 'minimum_grade' => 7]); + $student = Student::factory()->create(['grade' => 9]); + actAsAdmin(); + $response = $this->post(route('admin.entries.store'), [ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + 'for_seating' => 'on', + ]); + $response->assertRedirect(route('admin.entries.index')) + ->assertSessionDoesntHaveErrors() + ->assertSessionMissing('error') + ->assertSessionHas('success', 'The entry has been added.'); + assertDatabaseHas('entries', [ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + 'for_seating' => 1, + 'for_advancement' => 0, + ]); +}); diff --git a/tests/Feature/Pages/EntriesIndexTest.php b/tests/Feature/Pages/EntriesIndexTest.php index 6ecc175..2df03aa 100644 --- a/tests/Feature/Pages/EntriesIndexTest.php +++ b/tests/Feature/Pages/EntriesIndexTest.php @@ -9,6 +9,7 @@ use App\Settings; use Illuminate\Foundation\Testing\RefreshDatabase; use function Pest\Laravel\actingAs; +use function Pest\Laravel\assertDatabaseMissing; use function Pest\Laravel\delete; use function Pest\Laravel\get; use function Pest\Laravel\post; @@ -180,3 +181,21 @@ it('shows entry type checkboxes only when advancement is enabled', function () { get(route('entries.index')) ->assertDontSee('Enter for'); }); +it('denies an entry that is late', function () { + // Arrnge + $student = Student::factory()->create(['school_id' => $this->school->id, 'grade' => 8]); + $audition = Audition::factory()->closed()->create(['maximum_grade' => 9, 'minimum_grade' => 7]); + $user = User::factory()->create(['school_id' => $student->school_id]); + actingAs($user); + // Act & Assert + $response = post(route('entries.store'), [ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + ]); + assertDatabaseMissing('entries', [ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + ]); + $response->assertRedirect(route('entries.index')) + ->assertSessionHas('error', 'The entry deadline for that audition has passed'); +}); From 12c37b6250477bf149196a3ee73ad4502f43107b Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 17 Jul 2024 15:52:34 -0500 Subject: [PATCH 4/5] Created UpdateEntry action #29 Creating an entry should check on the status of the draw and respond appropriately Also changed name of CreateEntryException to MangeEntryException Also addresses #37 --- app/Actions/{ => Entries}/CreateEntry.php | 28 +-- app/Actions/Entries/UpdateEntry.php | 142 ++++++++++++ ...Exception.php => ManageEntryException.php} | 2 +- .../Controllers/Admin/EntryController.php | 6 +- app/Http/Controllers/EntryController.php | 6 +- tests/Feature/Actions/CreateEntryTest.php | 18 +- tests/Feature/Actions/UpdateEntryTest.php | 219 ++++++++++++++++++ 7 files changed, 391 insertions(+), 30 deletions(-) rename app/Actions/{ => Entries}/CreateEntry.php (76%) create mode 100644 app/Actions/Entries/UpdateEntry.php rename app/Exceptions/{CreateEntryException.php => ManageEntryException.php} (54%) create mode 100644 tests/Feature/Actions/UpdateEntryTest.php diff --git a/app/Actions/CreateEntry.php b/app/Actions/Entries/CreateEntry.php similarity index 76% rename from app/Actions/CreateEntry.php rename to app/Actions/Entries/CreateEntry.php index 1cfacc6..daa6825 100644 --- a/app/Actions/CreateEntry.php +++ b/app/Actions/Entries/CreateEntry.php @@ -1,8 +1,8 @@ exists()) { - throw new CreateEntryException('Invalid student provided'); + if (! $student || ! $student->exists()) { + throw new ManageEntryException('Invalid student provided'); } // Make sure the audition is valid - if (! $audition->exists()) { - throw new CreateEntryException('Invalid audition provided'); + if (! $audition || ! $audition->exists()) { + throw new ManageEntryException('Invalid audition provided'); } // A student can't enter the same audition twice if (Entry::where('student_id', $student->id)->where('audition_id', $audition->id)->exists()) { - throw new CreateEntryException('That student is already entered in that audition'); + throw new ManageEntryException('That student is already entered in that audition'); } // Can't enter a published audition if ($audition->hasFlag('seats_published')) { - throw new CreateEntryException('Cannot add an entry to an audition where seats are published'); + throw new ManageEntryException('Cannot add an entry to an audition where seats are published'); } if ($audition->hasFlag('advancement_published')) { - throw new CreateEntryException('Cannot add an entry to an audition where advancement is published'); + throw new ManageEntryException('Cannot add an entry to an audition where advancement is published'); } // Verify the grade of the student is in range for the audition if ($student->grade > $audition->maximum_grade) { - throw new CreateEntryException('The grade of the student exceeds the maximum for that audition'); + throw new ManageEntryException('The grade of the student exceeds the maximum for that audition'); } if ($student->grade < $audition->minimum_grade) { - throw new CreateEntryException('The grade of the student does not meet the minimum for that audition'); + throw new ManageEntryException('The grade of the student does not meet the minimum for that audition'); } } } diff --git a/app/Actions/Entries/UpdateEntry.php b/app/Actions/Entries/UpdateEntry.php new file mode 100644 index 0000000..4d44650 --- /dev/null +++ b/app/Actions/Entries/UpdateEntry.php @@ -0,0 +1,142 @@ +updateEntry($entry, $updateData); + } + + /** + * @throws ManageEntryException + */ + public function updateEntry(Entry|int $entry, array $updateData): void + { + if (is_int($entry)) { + $entry = Entry::find($entry); + } + if (! $entry || ! $entry->exists) { + throw new ManageEntryException('Invalid entry provided'); + } + $this->entry = $entry; + if (array_key_exists('for_seating', $updateData)) { + $this->updateForSeating($updateData['for_seating']); + } + if (array_key_exists('for_advancement', $updateData)) { + $this->updateForAdvancement($updateData['for_advancement']); + } + if (array_key_exists('audition', $updateData)) { + $this->updateAudition($updateData['audition']); + } + + $this->entry->save(); + } + + /** + * @throws ManageEntryException + */ + private function updateAudition(Audition|int $audition): void + { + + if (is_int($audition)) { + $audition = Audition::find($audition); + } + if (! $audition || ! $audition->exists) { + throw new ManageEntryException('Invalid audition provided'); + } + + if ($this->entry->audition->hasFlag('seats_published')) { + throw new ManageEntryException('Cannot change the audition for an entry where seating for that entry\'s current audition is published'); + } + if ($this->entry->audition->hasFlag('advancement_published')) { + throw new ManageEntryException('Cannot change the audition for an entry where advancement for that entry\'s current audition is published'); + } + if ($audition->hasFlag('seats_published')) { + throw new ManageEntryException('Cannot change the entry to an audition with published seating'); + } + if ($audition->hasFlag('advancement_published')) { + throw new ManageEntryException('Cannot change the entry to an audition with published advancement'); + } + if ($this->entry->student->grade > $audition->maximum_grade) { + throw new ManageEntryException('The grade of the student exceeds the maximum for that audition'); + } + if ($this->entry->student->grade < $audition->minimum_grade) { + throw new ManageEntryException('The grade of the student does not meet the minimum for that audition'); + } + if ($this->entry->scoreSheets()->count() > 0) { + throw new ManageEntryException('Cannot change the audition for an entry with scores'); + } + if (Entry::where('student_id', $this->entry->student_id)->where('audition_id', $audition->id)->exists()) { + throw new ManageEntryException('That student is already entered in that audition'); + } + // OK we're allowed to change the audition + $this->entry->audition_id = $audition->id; + // Deal with our draw number + if ($audition->hasFlag('drawn')) { + $draw_number = $audition->entries()->max('draw_number'); + $this->entry->draw_number = $draw_number + 1; + } else { + $this->entry->draw_number = null; + } + } + + /** + * @throws ManageEntryException + */ + private function updateForSeating($forSeating): void + { + if ($this->entry->for_seating == $forSeating) { + return; + } + if ($forSeating) { + if ($this->entry->audition->hasFlag('seats_published')) { + throw new ManageEntryException('Cannot add seating to an entry in an audition where seats are published'); + } + $this->entry->for_seating = 1; + } else { + if ($this->entry->audition->hasFlag('seats_published')) { + throw new ManageEntryException('Cannot remove seating from an entry in an audition where seats are published'); + } + $this->entry->for_seating = 0; + } + } + + /** + * @throws ManageEntryException + */ + private function updateForAdvancement($forAdvancement): void + { + if ($this->entry->for_advancement == $forAdvancement) { + return; + } + if ($forAdvancement) { + if ($this->entry->audition->hasFlag('advancement_published')) { + throw new ManageEntryException('Cannot add advancement to an entry in an audition where advancement is published'); + } + $this->entry->for_advancement = 1; + } else { + if ($this->entry->audition->hasFlag('advancement_published')) { + throw new ManageEntryException('Cannot remove advancement from an entry in an audition where advancement is published'); + } + $this->entry->for_advancement = 0; + } + + } +} diff --git a/app/Exceptions/CreateEntryException.php b/app/Exceptions/ManageEntryException.php similarity index 54% rename from app/Exceptions/CreateEntryException.php rename to app/Exceptions/ManageEntryException.php index 8dd356f..6d5c3cf 100644 --- a/app/Exceptions/CreateEntryException.php +++ b/app/Exceptions/ManageEntryException.php @@ -4,6 +4,6 @@ namespace App\Exceptions; use Exception; -class CreateEntryException extends Exception +class ManageEntryException extends Exception { } diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index ab55ea0..2edead0 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -2,9 +2,9 @@ namespace App\Http\Controllers\Admin; -use App\Actions\CreateEntry; +use App\Actions\Entries\CreateEntry; use App\Actions\Tabulation\CalculateEntryScore; -use App\Exceptions\CreateEntryException; +use App\Exceptions\ManageEntryException; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Entry; @@ -111,7 +111,7 @@ class EntryController extends Controller try { $creator($validData['student_id'], $validData['audition_id'], $enter_for); - } catch (CreateEntryException $ex) { + } catch (ManageEntryException $ex) { return redirect()->route('admin.entries.index')->with('error', $ex->getMessage()); } diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index f2e7ca4..cdb992e 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -2,8 +2,8 @@ namespace App\Http\Controllers; -use App\Actions\CreateEntry; -use App\Exceptions\CreateEntryException; +use App\Actions\Entries\CreateEntry; +use App\Exceptions\ManageEntryException; use App\Models\Audition; use App\Models\Entry; use Illuminate\Http\Request; @@ -53,7 +53,7 @@ class EntryController extends Controller try { $creator($validData['student_id'], $validData['audition_id'], $enter_for); - } catch (CreateEntryException $ex) { + } catch (ManageEntryException $ex) { return redirect()->route('entries.index')->with('error', $ex->getMessage()); } diff --git a/tests/Feature/Actions/CreateEntryTest.php b/tests/Feature/Actions/CreateEntryTest.php index ab7fce4..cf1601a 100644 --- a/tests/Feature/Actions/CreateEntryTest.php +++ b/tests/Feature/Actions/CreateEntryTest.php @@ -1,7 +1,7 @@ create(); $student = Student::factory()->make(); $this->createEntry->__invoke($student, $audition); -})->throws(CreateEntryException::class, 'Invalid student provided'); +})->throws(ManageEntryException::class, 'Invalid student provided'); it('throws an exception if the audition does not exist', function () { $audition = Audition::factory()->make(); $student = Student::factory()->create(); $this->createEntry->__invoke($student, $audition); -})->throws(CreateEntryException::class, 'Invalid audition provided'); +})->throws(ManageEntryException::class, 'Invalid audition provided'); it('throws an exception if the student is already entered in the audition', function () { // Arrange $audition = Audition::factory()->create(); @@ -34,7 +34,7 @@ it('throws an exception if the student is already entered in the audition', func ]); // Act & Assert $this->createEntry->createEntry($student, $audition); -})->throws(CreateEntryException::class, 'That student is already entered in that audition'); +})->throws(ManageEntryException::class, 'That student is already entered in that audition'); it('throws an exception if seats are published for the audition', function () { // Arrange $audition = Audition::factory()->create(); @@ -42,7 +42,7 @@ it('throws an exception if seats are published for the audition', function () { $audition->addFlag('seats_published'); // Act & Assert $this->createEntry->createEntry($student, $audition); -})->throws(CreateEntryException::class, 'Cannot add an entry to an audition where seats are published'); +})->throws(ManageEntryException::class, 'Cannot add an entry to an audition where seats are published'); it('throws an exception if advancement is published for the audition', function () { // Arrange $audition = Audition::factory()->create(); @@ -50,21 +50,21 @@ it('throws an exception if advancement is published for the audition', function $audition->addFlag('advancement_published'); // Act & Assert $this->createEntry->createEntry($student, $audition); -})->throws(CreateEntryException::class, 'Cannot add an entry to an audition where advancement is published'); +})->throws(ManageEntryException::class, 'Cannot add an entry to an audition where advancement is published'); it('throws an exception if the grade of the student exceeds the maximum grade for the audition', function () { // Arrange $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); $student = Student::factory()->create(['grade' => 11]); // Act & Assert $this->createEntry->createEntry($student, $audition); -})->throws(CreateEntryException::class, 'The grade of the student exceeds the maximum for that audition'); +})->throws(ManageEntryException::class, 'The grade of the student exceeds the maximum for that audition'); it('throws an exception if the grade of the student does not meet the minimum grade for the audition', function () { // Arrange $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); $student = Student::factory()->create(['grade' => 7]); // Act & Assert $this->createEntry->createEntry($student, $audition); -})->throws(CreateEntryException::class, 'The grade of the student does not meet the minimum for that audition'); +})->throws(ManageEntryException::class, 'The grade of the student does not meet the minimum for that audition'); it('returns an entry object', function () { // Arrange $audition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); diff --git a/tests/Feature/Actions/UpdateEntryTest.php b/tests/Feature/Actions/UpdateEntryTest.php new file mode 100644 index 0000000..c9350e2 --- /dev/null +++ b/tests/Feature/Actions/UpdateEntryTest.php @@ -0,0 +1,219 @@ +updater = App::make(UpdateEntry::class); +}); + +it('throws an error if an invalid entry is provided', function () { + $this->updater->updateEntry(2, []); +})->throws(ManageEntryException::class, 'Invalid entry provided'); + +it('throws an error if we try to remove for_seating while seating is published', function () { + // Arrange + $entry = Entry::factory()->create(); + $entry->audition->addFlag('seats_published'); + $data = ['for_seating' => 0]; + // Act & Assert + $this->updater->updateEntry($entry, $data); +})->throws('Cannot remove seating from an entry in an audition where seats are published'); +it('throws an error if we try to add for_seating while seating is published', function () { + // Arrange + $entry = Entry::factory()->advanceOnly()->create(); + $entry->audition->addFlag('seats_published'); + $data = ['for_seating' => 1]; + // Act & Assert + $this->updater->updateEntry($entry, $data); +})->throws('Cannot add seating to an entry in an audition where seats are published'); +it('allows us to remove for_seating if seating is not published', function () { + // Arrange + $entry = Entry::factory()->create(); + $data = ['for_seating' => 0]; + // Act + $this->updater->updateEntry($entry, $data); + // Assert + $this->assertDatabaseHas('entries', ['id' => $entry->id, 'for_seating' => 0]); +}); +it('allows us to add for_seating if seating is not published', function () { + // Arrange + $entry = Entry::factory()->advanceOnly()->create(); + $data = ['for_seating' => 1]; + // Act + $this->updater->updateEntry($entry, $data); + // Assert + $this->assertDatabaseHas('entries', ['id' => $entry->id, 'for_seating' => 1]); +}); + +it('throws an error if we try to remove for_advancement while seating is published', function () { + // Arrange + $entry = Entry::factory()->create(); + $entry->audition->addFlag('advancement_published'); + $data = ['for_advancement' => 0]; + // Act & Assert + $this->updater->updateEntry($entry, $data); +})->throws('Cannot remove advancement from an entry in an audition where advancement is published'); +it('throws an error if we try to add for_advancement while advancement is published', function () { + // Arrange + $entry = Entry::factory()->seatingOnly()->create(); + $entry->audition->addFlag('advancement_published'); + $data = ['for_advancement' => 1]; + // Act & Assert + $this->updater->updateEntry($entry, $data); +})->throws('Cannot add advancement to an entry in an audition where advancement is published'); +it('allows us to remove for_advancement if advancement is not published', function () { + // Arrange + $entry = Entry::factory()->create(); + $data = ['for_advancement' => 0]; + // Act + $this->updater->updateEntry($entry, $data); + // Assert + $this->assertDatabaseHas('entries', ['id' => $entry->id, 'for_advancement' => 0]); +}); +it('allows us to add for_advancement if advancement is not published', function () { + // Arrange + $entry = Entry::factory()->seatingOnly()->create(); + $data = ['for_advancement' => 1]; + // Act + $this->updater->updateEntry($entry, $data); + // Assert + $this->assertDatabaseHas('entries', ['id' => $entry->id, 'for_advancement' => 1]); +}); + +it('throws an exception if an attempt to change to an invalid audition is made', function () { + $entry = Entry::factory()->create(); + $data = ['audition' => 2]; + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, 'Invalid audition provided'); + +it('cannot change auditions if our current audition advancement is published', function () { + $entry = Entry::factory()->create(); + $entry->audition->addFlag('advancement_published'); + $otherAudition = Audition::factory()->create(); + $data = ['audition' => $otherAudition]; + // Act + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, + 'Cannot change the audition for an entry where advancement for that entry\'s current audition is published'); +it('cannot change auditions if our current audition seating is published', function () { + $entry = Entry::factory()->create(); + $entry->audition->addFlag('seats_published'); + $otherAudition = Audition::factory()->create(); + $data = ['audition' => $otherAudition]; + // Act + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, + 'Cannot change the audition for an entry where seating for that entry\'s current audition is published'); + +it('cannot change auditions if our proposed audition advancement is published', function () { + $entry = Entry::factory()->create(); + $otherAudition = Audition::factory()->create(); + $otherAudition->addFlag('advancement_published'); + $data = ['audition' => $otherAudition]; + // Act + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, 'Cannot change the entry to an audition with published advancement'); +it('cannot change auditions if our proposed audition seating is published', function () { + $entry = Entry::factory()->create(); + $otherAudition = Audition::factory()->create(); + $otherAudition->addFlag('seats_published'); + $data = ['audition' => $otherAudition]; + // Act + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, 'Cannot change the entry to an audition with published seating'); + +it('will not let us switch to an audition that is too old for the student', function () { + $entry = Entry::factory()->create(); + $otherAudition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $entry->student->update(['grade' => 7]); + $data = ['audition' => $otherAudition]; + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, 'The grade of the student does not meet the minimum for that audition'); +it('will not let us switch to an audition that is too young for the student', function () { + $entry = Entry::factory()->create(); + $otherAudition = Audition::factory()->create(['minimum_grade' => 8, 'maximum_grade' => 9]); + $entry->student->update(['grade' => 11]); + $data = ['audition' => $otherAudition]; + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, 'The grade of the student exceeds the maximum for that audition'); +it('will not let us change auditions for an entry with scores', function () { + $entry = Entry::factory()->create(); + $judge = User::factory()->create(); + ScoreSheet::create([ + 'entry_id' => $entry->id, + 'user_id' => $judge->id, + 'subscores' => 100, + ]); + $otherAudition = Audition::factory()->create([ + 'minimum_grade' => $entry->student->grade, 'maximum_grade' => $entry->student->grade, + ]); + $data = ['audition' => $otherAudition]; + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, 'Cannot change the audition for an entry with scores'); +it('will not let us change to an audition that the student is already entered in', function () { + $entry = Entry::factory()->create(); + + $otherAudition = Audition::factory()->create([ + 'minimum_grade' => $entry->student->grade, 'maximum_grade' => $entry->student->grade, + ]); + Entry::create([ + 'student_id' => $entry->student->id, + 'audition_id' => $otherAudition->id, + ]); + $data = ['audition' => $otherAudition]; + $this->updater->updateEntry($entry, $data); +})->throws(ManageEntryException::class, 'That student is already entered in that audition'); +it('allows us to switch auditions', function () { + // Arrange + $entry = Entry::factory()->create(); + $originalAudition = $entry->audition; + $otherAudition = Audition::factory()->create([ + 'minimum_grade' => $entry->student->grade, 'maximum_grade' => $entry->student->grade, + ]); + $data = ['audition' => $otherAudition]; + // Act & Assert + $this->updater->updateEntry($entry, $data); + $this->assertDatabaseHas('entries', ['id' => $entry->id, 'audition_id' => $otherAudition->id]); + $this->assertDatabaseMissing('entries', + ['student_id' => $entry->student->id, 'audition_id' => $originalAudition->id]); +}); +it('sets the draw number null if the new audition is not drawn', function () { + // Arrange + $entry = Entry::factory()->create(['draw_number' => 3]); + $newAudition = Audition::factory()->create([ + 'minimum_grade' => $entry->student->grade, 'maximum_grade' => $entry->student->grade, + ]); + $data = ['audition' => $newAudition]; + // Act + $this->updater->updateEntry($entry, $data); + // Assert + $this->assertDatabaseHas('entries', ['id' => $entry->id, 'draw_number' => null]); +}); +it('sets the draw number last if the new audition is not drawn', function () { + // Arrange + $entry = Entry::factory()->create(['draw_number' => 3]); + $newAudition = Audition::factory()->create([ + 'minimum_grade' => $entry->student->grade, 'maximum_grade' => $entry->student->grade, + ]); + $newAudition->addFlag('drawn'); + foreach (range(1, 6) as $dn) { + Entry::factory()->create(['audition_id' => $newAudition->id, 'draw_number' => $dn]); + } + $data = ['audition' => $newAudition]; + // Act + $this->updater->updateEntry($entry, $data); + + // Assert + $this->assertDatabaseHas('entries', ['id' => $entry->id, 'draw_number' => 7]); +}); From 49ff97e2b01d23b598f4839c020c47fa8ce676e0 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 17 Jul 2024 16:19:30 -0500 Subject: [PATCH 5/5] Use UpdateEntry action on admin edit entry screen #29 Creating an entry should check on the status of the draw and respond appropriately Also changed name of CreateEntryException to MangeEntryException Closes #29 Closes #37 --- app/Actions/Entries/UpdateEntry.php | 3 +++ .../Controllers/Admin/EntryController.php | 19 +++++++++++++------ tests/Feature/Pages/Admin/EntriesEditTest.php | 7 ++++--- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/app/Actions/Entries/UpdateEntry.php b/app/Actions/Entries/UpdateEntry.php index 4d44650..eeb8d90 100644 --- a/app/Actions/Entries/UpdateEntry.php +++ b/app/Actions/Entries/UpdateEntry.php @@ -42,6 +42,9 @@ class UpdateEntry if (array_key_exists('for_advancement', $updateData)) { $this->updateForAdvancement($updateData['for_advancement']); } + if (array_key_exists('audition_id', $updateData)) { + $this->updateAudition($updateData['audition_id']); + } if (array_key_exists('audition', $updateData)) { $this->updateAudition($updateData['audition']); } diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index 2edead0..362b9df 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Admin; use App\Actions\Entries\CreateEntry; +use App\Actions\Entries\UpdateEntry; use App\Actions\Tabulation\CalculateEntryScore; use App\Exceptions\ManageEntryException; use App\Http\Controllers\Controller; @@ -139,7 +140,7 @@ class EntryController extends Controller return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores')); } - public function update(Request $request, Entry $entry) + public function update(Request $request, Entry $entry, UpdateEntry $updater) { if ($entry->audition->hasFlag('seats_published')) { return to_route('admin.entries.index')->with('error', @@ -157,15 +158,21 @@ class EntryController extends Controller $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; + // If the audition is not set to advance to the next round, then the entry must be for seating if (! auditionSetting('advanceTo')) { $validData['for_seating'] = 1; } + try { + $updater($entry, $validData); + } catch (ManageEntryException $e) { + return redirect()->route('admin.entries.index')->with('error', $e->getMessage()); + } - $entry->update([ - 'audition_id' => $validData['audition_id'], - 'for_seating' => $validData['for_seating'], - 'for_advancement' => $validData['for_advancement'], - ]); + // $entry->update([ + // 'audition_id' => $validData['audition_id'], + // 'for_seating' => $validData['for_seating'], + // 'for_advancement' => $validData['for_advancement'], + // ]); return to_route('admin.entries.index')->with('success', 'Entry updated successfully'); } diff --git a/tests/Feature/Pages/Admin/EntriesEditTest.php b/tests/Feature/Pages/Admin/EntriesEditTest.php index e9b3fcb..105e245 100644 --- a/tests/Feature/Pages/Admin/EntriesEditTest.php +++ b/tests/Feature/Pages/Admin/EntriesEditTest.php @@ -137,12 +137,13 @@ it('does not let a normal user update an entry', function () { }); it('allows an admin to update an entry', function () { // Arrange - $newAudition = Audition::factory()->create(); + $newAudition = Audition::factory()->create(['minimum_grade' => 1, 'maximum_grade' => 20]); actAsAdmin(); // Act & Assert /** @noinspection PhpUnhandledExceptionInspection */ patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) ->assertSessionHasNoErrors() + ->assertSessionMissing('error') ->assertSessionHas('success', 'Entry updated successfully') ->assertRedirect(route('admin.entries.index')); $this->entry->refresh(); @@ -221,7 +222,7 @@ it('displays scores', function () { $response->assertSee($subscore->name); } }); -it('has a link to delete scores', function() { +it('has a link to delete scores', function () { // Arrange $sg = ScoringGuide::factory()->create(); SubscoreDefinition::factory()->count(5)->create(['scoring_guide_id' => $sg->id]); @@ -236,7 +237,7 @@ it('has a link to delete scores', function() { $scoreSheet = ScoreSheet::where('entry_id', $entry->id)->first(); actAsAdmin(); $response = get(route('admin.entries.edit', $entry)) - ->assertSee(route('scores.destroy', ['score'=>$scoreSheet])); + ->assertSee(route('scores.destroy', ['score' => $scoreSheet])); }); // Delete tests