From b09f1b13cae82caabb71629b3b86c238ca9e7886 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 8 Jul 2025 16:27:12 -0500 Subject: [PATCH] updates to entry action --- app/Actions/Entries/CreateEntry.php | 38 ++- .../Controllers/Admin/EntryController.php | 39 +-- app/Http/Controllers/EntryController.php | 8 +- app/Http/Requests/EntryStoreRequest.php | 39 ++- .../app/Actions/Entries/CreateEntriesTest.php | 2 +- .../Controllers/Admin/EntryControllerTest.php | 260 ++++++++++++++++++ 6 files changed, 331 insertions(+), 55 deletions(-) create mode 100644 tests/Feature/app/Http/Controllers/Admin/EntryControllerTest.php diff --git a/app/Actions/Entries/CreateEntry.php b/app/Actions/Entries/CreateEntry.php index 2f1df8f..b1e907d 100644 --- a/app/Actions/Entries/CreateEntry.php +++ b/app/Actions/Entries/CreateEntry.php @@ -17,37 +17,51 @@ class CreateEntry /** * @throws ManageEntryException */ - public function __invoke(Student|int $student, Audition|int $audition, string|array|null $entry_for = null) - { - return $this->createEntry($student, $audition, $entry_for); + public function __invoke( + Student|int $student, + Audition|int $audition, + $for_seating = false, + $for_advancement = false, + $late_fee_waived = false + ) { + return $this->createEntry($student, $audition, $for_seating, $for_advancement, $late_fee_waived); } /** * @throws ManageEntryException */ - public function createEntry(Student|int $student, Audition|int $audition, string|array|null $entry_for = null): Entry - { + public function createEntry( + Student|int $student, + Audition|int $audition, + $for_seating = false, + $for_advancement = false, + $late_fee_waived = false + ): Entry { if (is_int($student)) { $student = Student::find($student); } if (is_int($audition)) { $audition = Audition::find($audition); } - - if (! $entry_for) { - $entry_for = ['seating', 'advancement']; - } - $entry_for = collect($entry_for); $this->verifySubmission($student, $audition); + if (! $for_advancement && ! $for_seating) { + $for_seating = true; + $for_advancement = true; + } $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'), + 'for_seating' => $for_seating, + 'for_advancement' => $for_advancement, ]); $entry->save(); + if ($late_fee_waived) { + $entry->addFlag('late_fee_waived'); + $entry->refresh(); + } + return $entry; } diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index b25e840..9898526 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -7,6 +7,7 @@ use App\Actions\Entries\UpdateEntry; use App\Exceptions\AuditionAdminException; use App\Exceptions\ManageEntryException; use App\Http\Controllers\Controller; +use App\Http\Requests\EntryStoreRequest; use App\Models\Audition; use App\Models\AuditLogEntry; use App\Models\Entry; @@ -15,7 +16,6 @@ use App\Models\Seat; use App\Models\Student; use App\Services\ScoreService; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; use function auditionSetting; use function compact; @@ -25,9 +25,6 @@ class EntryController extends Controller { public function index() { - if (! Auth::user()->is_admin) { - abort(403); - } $perPage = 25; $filters = session('adminEntryFilters') ?? null; $minGrade = Audition::min('minimum_grade'); @@ -38,31 +35,31 @@ class EntryController extends Controller $entries = Entry::with(['student.school', 'audition']); $entries->orderBy('id', 'DESC'); if ($filters) { - if ($filters['id']) { + if ($filters['id'] ?? false) { $entries->where('id', $filters['id']); } - if ($filters['audition']) { + if ($filters['audition'] ?? false) { $entries->where('audition_id', $filters['audition']); } - if ($filters['school']) { + if ($filters['school'] ?? false) { $entries->whereHas('student', function ($query) use ($filters) { $query->where('school_id', '=', $filters['school']); }); } - if ($filters['grade']) { + if ($filters['grade'] ?? false) { $entries->whereHas('student', function ($query) use ($filters) { $query->where('grade', $filters['grade']); }); } - if ($filters['first_name']) { + if ($filters['first_name'] ?? false) { $entries->whereHas('student', function ($query) use ($filters) { $query->where('first_name', 'like', '%'.$filters['first_name'].'%'); }); } - if ($filters['last_name']) { + if ($filters['last_name'] ?? false) { $entries->whereHas('student', function ($query) use ($filters) { $query->where('last_name', 'like', '%'.$filters['last_name'].'%'); }); @@ -110,27 +107,9 @@ class EntryController extends Controller return view('admin.entries.create', ['students' => $students, 'auditions' => $auditions]); } - public function store(Request $request, CreateEntry $creator) + public function store(EntryStoreRequest $request, CreateEntry $creator) { - if (! Auth::user()->is_admin) { - abort(403); - } - $validData = request()->validate([ - 'student_id' => ['required', 'exists:students,id'], - 'audition_id' => ['required', 'exists:auditions,id'], - ]); - - $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; - $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; - $validData['late_fee_waived'] = $request->get('late_fee_waived') ? 1 : 0; - $enter_for = []; - if ($validData['for_seating']) { - $enter_for[] = 'seating'; - } - if ($validData['for_advancement']) { - $enter_for[] = 'advancement'; - } - + $validData = $request->validatedWithEnterFor(); try { $entry = $creator($validData['student_id'], $validData['audition_id'], $enter_for); } catch (ManageEntryException $ex) { diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index 534fc5b..7f252a2 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -38,8 +38,12 @@ class EntryController extends Controller public function store(EntryStoreRequest $request, CreateEntry $creator) { $validData = $request->validatedWithEnterFor(); - - $creator($validData['student_id'], $validData['audition_id'], $validData['enter_for'] ?? []); + $creator( + $validData['student_id'], + $validData['audition_id'], + for_seating: $validData['for_seating'], + for_advancement: $validData['for_advancement'], + ); return redirect()->route('entries.index')->with('success', 'The entry has been added.'); } diff --git a/app/Http/Requests/EntryStoreRequest.php b/app/Http/Requests/EntryStoreRequest.php index 886b518..40a96f8 100644 --- a/app/Http/Requests/EntryStoreRequest.php +++ b/app/Http/Requests/EntryStoreRequest.php @@ -3,12 +3,13 @@ namespace App\Http\Requests; use App\Models\Audition; +use Auth; use Carbon\Carbon; use Illuminate\Foundation\Http\FormRequest; class EntryStoreRequest extends FormRequest { - public function authorize() + public function authorize(): bool { if (auth()->user()->is_admin) { return true; @@ -20,17 +21,25 @@ class EntryStoreRequest extends FormRequest return false; } - public function rules() + public function rules(): array { - return [ + $rules = [ 'student_id' => ['required', 'exists:students,id'], 'audition_id' => ['required', 'exists:auditions,id'], 'for_seating' => ['sometimes', 'boolean'], 'for_advancement' => ['sometimes', 'boolean'], ]; + + // Add late_fee_waived validation only for admin users + if (auth()->user()->is_admin) { + $rules['late_fee_waived'] = ['sometimes', 'boolean']; + } + + return $rules; + } - public function withValidator($validator) + public function withValidator($validator): void { $validator->after(function ($validator) { $auditionId = $this->input('audition_id'); @@ -42,10 +51,12 @@ class EntryStoreRequest extends FormRequest return; } - $currentDate = Carbon::now('America/Chicago')->format('Y-m-d'); + if (! Auth::user()->is_admin) { //Admins don't care about deadlines + $currentDate = Carbon::now('America/Chicago')->format('Y-m-d'); - if ($audition->entry_deadline < $currentDate) { - $validator->errors()->add('entry_deadline', 'The entry deadline for that audition has passed.'); + if ($audition->entry_deadline < $currentDate) { + $validator->errors()->add('entry_deadline', 'The entry deadline for that audition has passed.'); + } } }); } @@ -53,13 +64,21 @@ class EntryStoreRequest extends FormRequest /** * Prepare the data for validation. */ - protected function prepareForValidation() + protected function prepareForValidation(): void { // Normalize the boolean inputs to 1 or 0 - $this->merge([ + $data = [ 'for_seating' => $this->boolean('for_seating'), 'for_advancement' => $this->boolean('for_advancement'), - ]); + ]; + + // Only include late_fee_waived in the data if the user is admin + if (auth()->user()->is_admin) { + $data['late_fee_waived'] = $this->boolean('late_fee_waived'); + } + + $this->merge($data); + } /** diff --git a/tests/Feature/app/Actions/Entries/CreateEntriesTest.php b/tests/Feature/app/Actions/Entries/CreateEntriesTest.php index 601c17f..d8a7748 100644 --- a/tests/Feature/app/Actions/Entries/CreateEntriesTest.php +++ b/tests/Feature/app/Actions/Entries/CreateEntriesTest.php @@ -64,7 +64,7 @@ it('allows setting only seating', function () { test('allows setting only advancement', function () { $student = Student::factory()->create(['grade' => 9]); $audition = Audition::factory()->create(['minimum_grade' => 9, 'maximum_grade' => 12]); - $this->scribe->createEntry($student, $audition, 'advancement'); + $this->scribe->createEntry($student, $audition, for_advancement: true); $thisEntry = Entry::where('student_id', $student->id)->first(); expect($thisEntry->for_seating)->toBeFalsy() diff --git a/tests/Feature/app/Http/Controllers/Admin/EntryControllerTest.php b/tests/Feature/app/Http/Controllers/Admin/EntryControllerTest.php new file mode 100644 index 0000000..327735d --- /dev/null +++ b/tests/Feature/app/Http/Controllers/Admin/EntryControllerTest.php @@ -0,0 +1,260 @@ +event = Event::factory()->create(); + $this->auditions = Audition::factory()->count(2)->create(['event_id' => $this->event->id]); + $this->students = Student::factory()->count(2)->create(); + $this->entry1 = Entry::factory()->create([ + 'audition_id' => $this->auditions[0]->id, 'student_id' => $this->students[0]->id, 'for_seating' => 1, + 'for_advancement' => 0, + ]); + $this->entry2 = Entry::factory()->create([ + 'audition_id' => $this->auditions[1]->id, 'student_id' => $this->students[1]->id, 'for_seating' => 1, + 'for_advancement' => 1, + ]); + $this->entry3 = Entry::factory()->create([ + 'audition_id' => $this->auditions[0]->id, 'student_id' => $this->students[1]->id, 'for_seating' => 0, + 'for_advancement' => 1, + ]); + $this->entry4 = Entry::factory()->create([ + 'audition_id' => $this->auditions[1]->id, 'student_id' => $this->students[0]->id, 'for_seating' => 1, + 'for_advancement' => 1, + ]); + $this->entry1->student->update(['grade' => 9]); + $this->entry2->student->update(['grade' => 10]); +}); + +describe('EntryController::index', function () { + it('denies access to non-admins', function () { + $this->get(route('admin.entries.index'))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.entries.index'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.entries.index'))->assertRedirect(route('dashboard')); + }); + it('provides a list of entries', function () { + actAsAdmin(); + $response = $this->get(route('admin.entries.index'))->assertOk() + ->assertViewIs('admin.entries.index'); + foreach (Entry::all() as $entry) { + $response->assertSee($entry->student->full_name()); + $response->assertSee($entry->audition->name); + } + }); + describe('test filters', function () { + it('can filter by ID', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'id' => $this->entry4->id, + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $response->assertSee($this->entry4->student->full_name()) + ->assertDontSee($this->entry2->student->full_name()); + }); + it('can filter by first_name', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'first_name' => $this->entry4->student->first_name, + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $response->assertSee($this->entry4->student->full_name()) + ->assertDontSee($this->entry2->student->full_name()); + }); + it('can filter by last_name', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'last_name' => $this->entry4->student->last_name, + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $response->assertSee($this->entry4->student->full_name()) + ->assertDontSee($this->entry2->student->full_name()); + }); + it('can filter by audition', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'audition' => $this->entry1->audition_id, + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $returnedEntries = $response->viewData('entries'); + expect($returnedEntries->contains('id', $this->entry1->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry2->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry3->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry4->id))->toBeFalse(); + }); + it('can filter by school', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'school' => $this->entry1->student->school_id, + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $returnedEntries = $response->viewData('entries'); + expect($returnedEntries->contains('id', $this->entry1->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry2->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry3->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry4->id))->toBeTrue(); + }); + it('can filter by grade', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'grade' => 9, + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $returnedEntries = $response->viewData('entries'); + expect($returnedEntries->contains('id', $this->entry1->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry2->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry3->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry4->id))->toBeTrue(); + }); + it('can show auditions entered in seating', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'entry_type' => 'seats', + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $returnedEntries = $response->viewData('entries'); + expect($returnedEntries->contains('id', $this->entry1->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry2->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry3->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry4->id))->toBeTrue(); + }); + it('can show auditions entered in advancement', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'entry_type' => 'advancement', + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $returnedEntries = $response->viewData('entries'); + expect($returnedEntries->contains('id', $this->entry1->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry2->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry3->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry4->id))->toBeTrue(); + }); + it('can show auditions entered only in seating', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'entry_type' => 'seatsOnly', + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $returnedEntries = $response->viewData('entries'); + expect($returnedEntries->contains('id', $this->entry1->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry2->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry3->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry4->id))->toBeFalse(); + }); + it('can show auditions entered only in advancement', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'entry_type' => 'advancementOnly', + ], + ])->get(route('admin.entries.index'))->assertOk(); + saveContentLocally($response->getContent()); + $returnedEntries = $response->viewData('entries'); + expect($returnedEntries->contains('id', $this->entry1->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry2->id))->toBeFalse() + ->and($returnedEntries->contains('id', $this->entry3->id))->toBeTrue() + ->and($returnedEntries->contains('id', $this->entry4->id))->toBeFalse(); + }); + it('can limit the number of results per page', function () { + actAsAdmin(); + $response = $this->withSession([ + 'adminEntryFilters' => [ + 'entries_per_page' => 1, + ], + ])->get(route('admin.entries.index'))->assertOk(); + $returnedEntries = $response->viewData('entries'); + expect($returnedEntries->count())->toEqual(1); + }); + }); +}); + +describe('EntryController::create', function () { + it('denies access to non-admins', function () { + $this->get(route('admin.entries.create'))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.entries.create'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.entries.create'))->assertRedirect(route('dashboard')); + }); + it('provides a form to make an entry', function () { + actAsAdmin(); + $response = $this->get(route('admin.entries.create')); + $response->assertOk(); + }); + it('provides auditions to the form', function () { + actAsAdmin(); + $response = $this->get(route('admin.entries.create')); + $response->assertOk(); + $response->assertViewHas('auditions'); + $returnedAuditions = $response->viewData('auditions'); + foreach (Audition::all() as $audition) { + expect($returnedAuditions->contains('id', $audition->id))->toBeTrue(); + } + }); + it('does not provide published auditions to the form', function () { + actAsAdmin(); + $unavailableAudition[0] = Audition::factory()->create(); + $unavailableAudition[0]->addFlag('seats_published'); + $unavailableAudition[1] = Audition::factory()->create(); + $unavailableAudition[1]->addFlag('advancement_published'); + + $response = $this->get(route('admin.entries.create')); + $response->assertOk(); + $response->assertViewHas('auditions'); + $returnedAuditions = $response->viewData('auditions'); + foreach ($unavailableAudition as $audition) { + expect($returnedAuditions->contains('id', $audition->id))->toBeFalse(); + } + }); +}); + +describe('EntryController::store', function () { + beforeEach(function () { + $this->testAudition = Audition::factory()->create(); + $this->testStudent = Student::factory()->create(); + $this->testSubmitData = [ + 'student_id' => $this->testStudent->id, + 'audition_id' => $this->testAudition->id, + 'for_seating' => 'on', + 'for_advancement' => 'on', + 'late_fee_waived' => 'on', + ]; + }); + it('denies access to non-admins', function () { + $this->post(route('admin.entries.store'), $this->testSubmitData)->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.entries.store'), $this->testSubmitData)->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.entries.store'), $this->testSubmitData)->assertRedirect(route('dashboard')); + }); + it('creates an entry', function () { + actAsAdmin(); + $this->post(route('admin.entries.store'), $this->testSubmitData); + }); +});