diff --git a/app/Actions/Entries/CreateEntry.php b/app/Actions/Entries/CreateEntry.php index 175d85a..2f1df8f 100644 --- a/app/Actions/Entries/CreateEntry.php +++ b/app/Actions/Entries/CreateEntry.php @@ -5,12 +5,9 @@ namespace App\Actions\Entries; use App\Exceptions\AuditionAdminException; use App\Exceptions\ManageEntryException; use App\Models\Audition; -use App\Models\AuditLogEntry; use App\Models\Entry; use App\Models\Student; -use function auth; - class CreateEntry { public function __construct() @@ -50,20 +47,6 @@ class CreateEntry 'for_advancement' => $entry_for->contains('advancement'), ]); $entry->save(); - if (auth()->user()) { - $message = 'Entered '.$entry->student->full_name().' from '.$entry->student->school->name.' in '.$entry->audition->name.'.'; - AuditLogEntry::create([ - 'user' => auth()->user()->email, - 'ip_address' => request()->ip(), - 'message' => $message, - 'affected' => [ - 'entries' => [$entry->id], - 'students' => [$entry->student_id], - 'auditions' => [$entry->audition_id], - 'schools' => [$entry->student->school_id], - ], - ]); - } return $entry; } diff --git a/app/Http/Controllers/EntryController.php b/app/Http/Controllers/EntryController.php index ce5c5fc..534fc5b 100644 --- a/app/Http/Controllers/EntryController.php +++ b/app/Http/Controllers/EntryController.php @@ -3,11 +3,9 @@ namespace App\Http\Controllers; use App\Actions\Entries\CreateEntry; -use App\Exceptions\AuditionAdminException; +use App\Http\Requests\EntryStoreRequest; use App\Models\Audition; -use App\Models\AuditLogEntry; use App\Models\Entry; -use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -17,11 +15,19 @@ class EntryController extends Controller { public function index() { + if (! auth()->user()->school_id) { + abort(403); + } - $entries = Auth::user()->entries()->with(['student', 'audition'])->get(); - $entries = $entries->sortBy(function ($entry) { - return $entry->student->last_name.$entry->student->first_name.$entry->audition->score_order; - }); + $entries = Auth::user()->entries() + ->select('entries.*') + ->join('students as s', 's.id', '=', 'entries.student_id') + ->join('auditions as a', 'a.id', '=', 'entries.audition_id') + ->with(['student', 'audition']) + ->orderBy('s.last_name') + ->orderBy('s.first_name') + ->orderBy('a.score_order') + ->get(); $auditions = Audition::open()->get(); $students = Auth::user()->students; $students->load('school'); @@ -29,37 +35,11 @@ class EntryController extends Controller return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]); } - public function store(Request $request, CreateEntry $creator) + public function store(EntryStoreRequest $request, CreateEntry $creator) { - if ($request->user()->cannot('create', Entry::class)) { - abort(403); - } - $validData = $request->validate([ - 'student_id' => ['required', 'exists:students,id'], - 'audition_id' => ['required', 'exists:auditions,id'], - ]); - $audition = Audition::find($validData['audition_id']); - $currentDate = Carbon::now('America/Chicago'); - $currentDate = $currentDate->format('Y-m-d'); - if ($audition->entry_deadline < $currentDate) { - return redirect()->route('entries.index')->with('error', 'The entry deadline for that audition has passed'); - } + $validData = $request->validatedWithEnterFor(); - $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'; - } - - try { - $creator($validData['student_id'], $validData['audition_id'], $enter_for); - } catch (AuditionAdminException $ex) { - return redirect()->route('entries.index')->with('error', $ex->getMessage()); - } + $creator($validData['student_id'], $validData['audition_id'], $validData['enter_for'] ?? []); return redirect()->route('entries.index')->with('success', 'The entry has been added.'); } @@ -69,21 +49,7 @@ class EntryController extends Controller if ($request->user()->cannot('delete', $entry)) { abort(403); } - if (auth()->user()) { - $message = 'Deleted entry '.$entry->id; - $affected = [ - 'entries' => [$entry->id], - 'auditions' => [$entry->audition_id], - 'schools' => [$entry->student->school_id], - 'students' => [$entry->student_id], - ]; - AuditLogEntry::create([ - 'user' => auth()->user()->email, - 'ip_address' => request()->ip(), - 'message' => $message, - 'affected' => $affected, - ]); - } + $entry->delete(); return redirect()->route('entries.index')->with('success', diff --git a/app/Http/Requests/EntryStoreRequest.php b/app/Http/Requests/EntryStoreRequest.php new file mode 100644 index 0000000..886b518 --- /dev/null +++ b/app/Http/Requests/EntryStoreRequest.php @@ -0,0 +1,84 @@ +user()->is_admin) { + return true; + } + if (auth()->user()->school_id) { + return true; + } + + return false; + } + + public function rules() + { + return [ + 'student_id' => ['required', 'exists:students,id'], + 'audition_id' => ['required', 'exists:auditions,id'], + 'for_seating' => ['sometimes', 'boolean'], + 'for_advancement' => ['sometimes', 'boolean'], + ]; + } + + public function withValidator($validator) + { + $validator->after(function ($validator) { + $auditionId = $this->input('audition_id'); + $audition = Audition::find($auditionId); + + if (! $audition) { + $validator->errors()->add('audition_id', 'The selected audition does not exist.'); + + return; + } + + $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.'); + } + }); + } + + /** + * Prepare the data for validation. + */ + protected function prepareForValidation() + { + // Normalize the boolean inputs to 1 or 0 + $this->merge([ + 'for_seating' => $this->boolean('for_seating'), + 'for_advancement' => $this->boolean('for_advancement'), + ]); + } + + /** + * Get the data after validation and add the "enter_for" array. + */ + public function validatedWithEnterFor() + { + $validated = $this->validated(); + + $enter_for = []; + if (! empty($validated['for_seating'])) { + $enter_for[] = 'seating'; + } + if (! empty($validated['for_advancement'])) { + $enter_for[] = 'advancement'; + } + + $validated['enter_for'] = $enter_for; + + return $validated; + } +} diff --git a/app/Observers/EntryObserver.php b/app/Observers/EntryObserver.php index aa020d8..d36ec45 100644 --- a/app/Observers/EntryObserver.php +++ b/app/Observers/EntryObserver.php @@ -18,13 +18,25 @@ class EntryObserver $count = $entry->student->entriesForEvent($entry->audition->event_id)->count(); // If less than two entries, they're not a doubler - if ($count < 2) { - return; + if ($count > 1) { + // Update doublers for the event + $syncer = app(DoublerSync::class); + $syncer($entry->audition->event_id); } - // Update doublers for the event - $syncer = app(DoublerSync::class); - $syncer($entry->audition->event_id); + // Log Entry Creation + $message = 'Created Entry #'.$entry->id; + $message .= '
Audition: '.$entry->audition->name; + $message .= '
Student: '.$entry->student->full_name(); + $message .= '
Grade: '.$entry->student->grade; + $message .= '
School: '.$entry->student->school->name; + + $affected = [ + 'students' => [$entry->student_id], + 'schools' => [$entry->student->school_id], + 'auditions' => [$entry->audition_id], + ]; + auditionLog($message, $affected); } @@ -47,5 +59,18 @@ class EntryObserver Doubler::where('student_id', $entry->student_id)->delete(); $audition = Audition::where('id', $entry->audition_id)->first(); $syncer($audition->event_id); + + $message = 'Deleted Entry #'.$entry->id; + $message .= '
Audition: '.$entry->audition->name; + $message .= '
Student: '.$entry->student->full_name(); + $message .= '
Grade: '.$entry->student->grade; + $message .= '
School: '.$entry->student->school->name; + + $affected = [ + 'students' => [$entry->student_id], + 'schools' => [$entry->student->school_id], + 'auditions' => [$entry->audition_id], + ]; + auditionLog($message, $affected); } } diff --git a/tests/Feature/app/Actions/Entries/CreateEntriesTest.php b/tests/Feature/app/Actions/Entries/CreateEntriesTest.php index 47ce7b9..601c17f 100644 --- a/tests/Feature/app/Actions/Entries/CreateEntriesTest.php +++ b/tests/Feature/app/Actions/Entries/CreateEntriesTest.php @@ -4,7 +4,6 @@ use App\Actions\Entries\CreateEntry; use App\Exceptions\AuditionAdminException; use App\Models\Audition; use App\Models\AuditionFlag; -use App\Models\AuditLogEntry; use App\Models\Entry; use App\Models\Student; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -139,18 +138,3 @@ it('throws and exception if the student is above the maximum grade for the audit $audition = Audition::factory()->create(['minimum_grade' => 9, 'maximum_grade' => 10]); $this->scribe->createEntry($student, $audition); })->throws(AuditionAdminException::class, 'The grade of the student exceeds the maximum for that audition'); - -it('logs the entry creation', function () { - actAsAdmin(); - $student = Student::factory()->create(['grade' => 9]); - $audition = Audition::factory()->create(['minimum_grade' => 9, 'maximum_grade' => 12]); - $this->scribe->createEntry($student, $audition); - $thisEntry = Entry::where('student_id', $student->id)->first(); - $logEntry = AuditLogEntry::orderBy('id', 'desc')->first(); - expect($logEntry->message)->toEqual('Entered '.$thisEntry->student->full_name().' from '.$thisEntry->student->school->name.' in '.$audition->name.'.') - ->and($logEntry->affected['entries'])->toEqual([$thisEntry->id]) - ->and($logEntry->affected['students'])->toEqual([$thisEntry->student_id]) - ->and($logEntry->affected['auditions'])->toEqual([$thisEntry->audition_id]) - ->and($logEntry->affected['schools'])->toEqual([$thisEntry->student->school->id]) - ->and($logEntry->user)->toEqual(auth()->user()->email); -}); diff --git a/tests/Feature/app/Http/Controllers/EntryContrllerTest.php b/tests/Feature/app/Http/Controllers/EntryContrllerTest.php new file mode 100644 index 0000000..cee13f2 --- /dev/null +++ b/tests/Feature/app/Http/Controllers/EntryContrllerTest.php @@ -0,0 +1,126 @@ +create(); + $response = $this->actingAs($user)->get(route('entries.index')); + $response->assertForbidden(); + }); + + it('provides an index of the entries for the users school', function () { + $user = User::factory()->create(); + $school = School::factory()->create(); + $user->school_id = $school->id; + $user->save(); + Student::factory()->count(3) + ->forSchool($school) + ->has(Entry::factory()->count(2)) + ->create(); + expect(Entry::count())->toEqual(6); + $response = $this->actingAs($user)->get(route('entries.index')); + $response->assertOk() + ->assertViewIs('entries.index') + ->assertViewHas('entries'); + $this->assertCount(6, $response->viewData('entries')); + foreach (Entry::all() as $entry) { + $response->assertSee($entry->student->full_name()); + } + }); + + it('provides a form for creating new entries', function () { + $user = User::factory()->create(); + $school = School::factory()->create(); + $user->school_id = $school->id; + $user->save(); + Student::factory()->count(3) + ->forSchool($school) + ->has(Entry::factory()->count(2)) + ->create(); + Audition::each(function (Audition $audition) { + $audition->update(['entry_deadline' => now()->subDays(10)]); + }); + $openAuditions = Audition::factory()->count(12)->create(['entry_deadline' => now()->addDays(10)]); + $response = $this->actingAs($user)->get(route('entries.index')); + + $response->assertOk() + ->assertViewHas('students') + ->assertViewHas('auditions') + ->assertSee(route('entries.store')); + $this->assertCount(12, $response->viewData('auditions')); + $this->assertCount(3, $response->viewData('students')); + foreach ($openAuditions as $audition) { + $response->assertSee($audition->name); + } + }); +}); + +describe('EntryController::store', function () { + it('denies access if the user does not have a school', function () { + $user = User::factory()->create(); + $response = $this->actingAs($user)->post(route('entries.store')); + $response->assertForbidden(); + }); + + it('creates a new entry for the users school', function () { + $user = User::factory()->create(); + $school = School::factory()->create(); + $user->school_id = $school->id; + $user->save(); + $student = Student::factory()->forSchool($school)->create(['grade' => 10]); + $audition = Audition::factory()->create([ + 'minimum_grade' => 9, + 'maximum_grade' => 12, + 'entry_deadline' => now()->addDays(10), + ]); + $response = actingAs($user)->post(route('entries.store'), [ + 'student_id' => $student->id, + 'audition_id' => $audition->id, + 'for_advancement' => 'on', + 'for_seating' => 'on', + ]); + $response->assertRedirect(route('entries.index')) + ->assertSessionHas('success'); + $this->assertCount(1, Entry::all()); + $entry = Entry::first(); + expect($entry->student_id)->toBe($student->id) + ->and($entry->audition_id)->toBe($audition->id) + ->and($entry->for_advancement)->toBeTruthy() + ->and($entry->for_seating)->toBeTruthy(); + + }); +}); +describe('EntryController::destroy', function () { + it('denies access if the user is not a director at the entries school', function () { + $user = User::factory()->create(); + $school = School::factory()->create(); + $user->update(['school_id' => $school->id]); + $user->refresh(); + $entry = Entry::factory()->create(); + $response = $this->actingAs($user)->delete(route('entries.destroy', $entry->id)); + $response->assertForbidden(); + }); + it('deletes an entry', function () { + $user = User::factory()->create(); + $school = School::factory()->create(); + $student = Student::factory()->forSchool($school)->create(); + $entry = Entry::factory()->forStudent($student)->create(); + $user->school_id = $school->id; + $user->save(); + expect(Entry::count())->toEqual(1); + $response = $this->actingAs($user)->delete(route('entries.destroy', $entry->id)); + $response->assertRedirect(route('entries.index')) + ->assertSessionHas('success'); + expect(Entry::count())->toEqual(0); + }); +});