Refactor and test EntryController

This commit is contained in:
Matt Young 2025-07-05 19:46:45 -05:00
parent f3dd8bea0d
commit dcd1d74fdc
6 changed files with 257 additions and 89 deletions

View File

@ -5,12 +5,9 @@ namespace App\Actions\Entries;
use App\Exceptions\AuditionAdminException; use App\Exceptions\AuditionAdminException;
use App\Exceptions\ManageEntryException; use App\Exceptions\ManageEntryException;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
use App\Models\Student; use App\Models\Student;
use function auth;
class CreateEntry class CreateEntry
{ {
public function __construct() public function __construct()
@ -50,20 +47,6 @@ class CreateEntry
'for_advancement' => $entry_for->contains('advancement'), 'for_advancement' => $entry_for->contains('advancement'),
]); ]);
$entry->save(); $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; return $entry;
} }

View File

@ -3,11 +3,9 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Entries\CreateEntry; use App\Actions\Entries\CreateEntry;
use App\Exceptions\AuditionAdminException; use App\Http\Requests\EntryStoreRequest;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -17,11 +15,19 @@ class EntryController extends Controller
{ {
public function index() public function index()
{ {
if (! auth()->user()->school_id) {
abort(403);
}
$entries = Auth::user()->entries()->with(['student', 'audition'])->get(); $entries = Auth::user()->entries()
$entries = $entries->sortBy(function ($entry) { ->select('entries.*')
return $entry->student->last_name.$entry->student->first_name.$entry->audition->score_order; ->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(); $auditions = Audition::open()->get();
$students = Auth::user()->students; $students = Auth::user()->students;
$students->load('school'); $students->load('school');
@ -29,37 +35,11 @@ class EntryController extends Controller
return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]); 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)) { $validData = $request->validatedWithEnterFor();
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['for_seating'] = $request->get('for_seating') ? 1 : 0; $creator($validData['student_id'], $validData['audition_id'], $validData['enter_for'] ?? []);
$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());
}
return redirect()->route('entries.index')->with('success', 'The entry has been added.'); 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)) { if ($request->user()->cannot('delete', $entry)) {
abort(403); 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(); $entry->delete();
return redirect()->route('entries.index')->with('success', return redirect()->route('entries.index')->with('success',

View File

@ -0,0 +1,84 @@
<?php
namespace App\Http\Requests;
use App\Models\Audition;
use Carbon\Carbon;
use Illuminate\Foundation\Http\FormRequest;
class EntryStoreRequest extends FormRequest
{
public function authorize()
{
if (auth()->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;
}
}

View File

@ -18,13 +18,25 @@ class EntryObserver
$count = $entry->student->entriesForEvent($entry->audition->event_id)->count(); $count = $entry->student->entriesForEvent($entry->audition->event_id)->count();
// If less than two entries, they're not a doubler // If less than two entries, they're not a doubler
if ($count < 2) { if ($count > 1) {
return; // Update doublers for the event
$syncer = app(DoublerSync::class);
$syncer($entry->audition->event_id);
} }
// Update doublers for the event // Log Entry Creation
$syncer = app(DoublerSync::class); $message = 'Created Entry #'.$entry->id;
$syncer($entry->audition->event_id); $message .= '<br>Audition: '.$entry->audition->name;
$message .= '<br>Student: '.$entry->student->full_name();
$message .= '<br>Grade: '.$entry->student->grade;
$message .= '<br>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(); Doubler::where('student_id', $entry->student_id)->delete();
$audition = Audition::where('id', $entry->audition_id)->first(); $audition = Audition::where('id', $entry->audition_id)->first();
$syncer($audition->event_id); $syncer($audition->event_id);
$message = 'Deleted Entry #'.$entry->id;
$message .= '<br>Audition: '.$entry->audition->name;
$message .= '<br>Student: '.$entry->student->full_name();
$message .= '<br>Grade: '.$entry->student->grade;
$message .= '<br>School: '.$entry->student->school->name;
$affected = [
'students' => [$entry->student_id],
'schools' => [$entry->student->school_id],
'auditions' => [$entry->audition_id],
];
auditionLog($message, $affected);
} }
} }

View File

@ -4,7 +4,6 @@ use App\Actions\Entries\CreateEntry;
use App\Exceptions\AuditionAdminException; use App\Exceptions\AuditionAdminException;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditionFlag; use App\Models\AuditionFlag;
use App\Models\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
use App\Models\Student; use App\Models\Student;
use Illuminate\Foundation\Testing\RefreshDatabase; 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]); $audition = Audition::factory()->create(['minimum_grade' => 9, 'maximum_grade' => 10]);
$this->scribe->createEntry($student, $audition); $this->scribe->createEntry($student, $audition);
})->throws(AuditionAdminException::class, 'The grade of the student exceeds the maximum for that 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);
});

View File

@ -0,0 +1,126 @@
<?php
use App\Models\Audition;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs;
uses(RefreshDatabase::class);
describe('EntryController::index', function () {
it('denies access if the user does not have a school', function () {
$user = User::factory()->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);
});
});