updates to entry action

This commit is contained in:
Matt Young 2025-07-08 16:27:12 -05:00
parent 66fe859f06
commit b09f1b13ca
6 changed files with 331 additions and 55 deletions

View File

@ -17,37 +17,51 @@ class CreateEntry
/** /**
* @throws ManageEntryException * @throws ManageEntryException
*/ */
public function __invoke(Student|int $student, Audition|int $audition, string|array|null $entry_for = null) public function __invoke(
{ Student|int $student,
return $this->createEntry($student, $audition, $entry_for); 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 * @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)) { if (is_int($student)) {
$student = Student::find($student); $student = Student::find($student);
} }
if (is_int($audition)) { if (is_int($audition)) {
$audition = Audition::find($audition); $audition = Audition::find($audition);
} }
if (! $entry_for) {
$entry_for = ['seating', 'advancement'];
}
$entry_for = collect($entry_for);
$this->verifySubmission($student, $audition); $this->verifySubmission($student, $audition);
if (! $for_advancement && ! $for_seating) {
$for_seating = true;
$for_advancement = true;
}
$entry = Entry::make([ $entry = Entry::make([
'student_id' => $student->id, 'student_id' => $student->id,
'audition_id' => $audition->id, 'audition_id' => $audition->id,
'draw_number' => $this->checkDraw($audition), 'draw_number' => $this->checkDraw($audition),
'for_seating' => $entry_for->contains('seating'), 'for_seating' => $for_seating,
'for_advancement' => $entry_for->contains('advancement'), 'for_advancement' => $for_advancement,
]); ]);
$entry->save(); $entry->save();
if ($late_fee_waived) {
$entry->addFlag('late_fee_waived');
$entry->refresh();
}
return $entry; return $entry;
} }

View File

@ -7,6 +7,7 @@ use App\Actions\Entries\UpdateEntry;
use App\Exceptions\AuditionAdminException; use App\Exceptions\AuditionAdminException;
use App\Exceptions\ManageEntryException; use App\Exceptions\ManageEntryException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\EntryStoreRequest;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
@ -15,7 +16,6 @@ use App\Models\Seat;
use App\Models\Student; use App\Models\Student;
use App\Services\ScoreService; use App\Services\ScoreService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function auditionSetting; use function auditionSetting;
use function compact; use function compact;
@ -25,9 +25,6 @@ class EntryController extends Controller
{ {
public function index() public function index()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$perPage = 25; $perPage = 25;
$filters = session('adminEntryFilters') ?? null; $filters = session('adminEntryFilters') ?? null;
$minGrade = Audition::min('minimum_grade'); $minGrade = Audition::min('minimum_grade');
@ -38,31 +35,31 @@ class EntryController extends Controller
$entries = Entry::with(['student.school', 'audition']); $entries = Entry::with(['student.school', 'audition']);
$entries->orderBy('id', 'DESC'); $entries->orderBy('id', 'DESC');
if ($filters) { if ($filters) {
if ($filters['id']) { if ($filters['id'] ?? false) {
$entries->where('id', $filters['id']); $entries->where('id', $filters['id']);
} }
if ($filters['audition']) { if ($filters['audition'] ?? false) {
$entries->where('audition_id', $filters['audition']); $entries->where('audition_id', $filters['audition']);
} }
if ($filters['school']) { if ($filters['school'] ?? false) {
$entries->whereHas('student', function ($query) use ($filters) { $entries->whereHas('student', function ($query) use ($filters) {
$query->where('school_id', '=', $filters['school']); $query->where('school_id', '=', $filters['school']);
}); });
} }
if ($filters['grade']) { if ($filters['grade'] ?? false) {
$entries->whereHas('student', function ($query) use ($filters) { $entries->whereHas('student', function ($query) use ($filters) {
$query->where('grade', $filters['grade']); $query->where('grade', $filters['grade']);
}); });
} }
if ($filters['first_name']) { if ($filters['first_name'] ?? false) {
$entries->whereHas('student', function ($query) use ($filters) { $entries->whereHas('student', function ($query) use ($filters) {
$query->where('first_name', 'like', '%'.$filters['first_name'].'%'); $query->where('first_name', 'like', '%'.$filters['first_name'].'%');
}); });
} }
if ($filters['last_name']) { if ($filters['last_name'] ?? false) {
$entries->whereHas('student', function ($query) use ($filters) { $entries->whereHas('student', function ($query) use ($filters) {
$query->where('last_name', 'like', '%'.$filters['last_name'].'%'); $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]); 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) { $validData = $request->validatedWithEnterFor();
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';
}
try { try {
$entry = $creator($validData['student_id'], $validData['audition_id'], $enter_for); $entry = $creator($validData['student_id'], $validData['audition_id'], $enter_for);
} catch (ManageEntryException $ex) { } catch (ManageEntryException $ex) {

View File

@ -38,8 +38,12 @@ class EntryController extends Controller
public function store(EntryStoreRequest $request, CreateEntry $creator) public function store(EntryStoreRequest $request, CreateEntry $creator)
{ {
$validData = $request->validatedWithEnterFor(); $validData = $request->validatedWithEnterFor();
$creator(
$creator($validData['student_id'], $validData['audition_id'], $validData['enter_for'] ?? []); $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.'); return redirect()->route('entries.index')->with('success', 'The entry has been added.');
} }

View File

@ -3,12 +3,13 @@
namespace App\Http\Requests; namespace App\Http\Requests;
use App\Models\Audition; use App\Models\Audition;
use Auth;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
class EntryStoreRequest extends FormRequest class EntryStoreRequest extends FormRequest
{ {
public function authorize() public function authorize(): bool
{ {
if (auth()->user()->is_admin) { if (auth()->user()->is_admin) {
return true; return true;
@ -20,17 +21,25 @@ class EntryStoreRequest extends FormRequest
return false; return false;
} }
public function rules() public function rules(): array
{ {
return [ $rules = [
'student_id' => ['required', 'exists:students,id'], 'student_id' => ['required', 'exists:students,id'],
'audition_id' => ['required', 'exists:auditions,id'], 'audition_id' => ['required', 'exists:auditions,id'],
'for_seating' => ['sometimes', 'boolean'], 'for_seating' => ['sometimes', 'boolean'],
'for_advancement' => ['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) { $validator->after(function ($validator) {
$auditionId = $this->input('audition_id'); $auditionId = $this->input('audition_id');
@ -42,10 +51,12 @@ class EntryStoreRequest extends FormRequest
return; 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) { if ($audition->entry_deadline < $currentDate) {
$validator->errors()->add('entry_deadline', 'The entry deadline for that audition has passed.'); $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. * Prepare the data for validation.
*/ */
protected function prepareForValidation() protected function prepareForValidation(): void
{ {
// Normalize the boolean inputs to 1 or 0 // Normalize the boolean inputs to 1 or 0
$this->merge([ $data = [
'for_seating' => $this->boolean('for_seating'), 'for_seating' => $this->boolean('for_seating'),
'for_advancement' => $this->boolean('for_advancement'), '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);
} }
/** /**

View File

@ -64,7 +64,7 @@ it('allows setting only seating', function () {
test('allows setting only advancement', function () { test('allows setting only advancement', function () {
$student = Student::factory()->create(['grade' => 9]); $student = Student::factory()->create(['grade' => 9]);
$audition = Audition::factory()->create(['minimum_grade' => 9, 'maximum_grade' => 12]); $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(); $thisEntry = Entry::where('student_id', $student->id)->first();
expect($thisEntry->for_seating)->toBeFalsy() expect($thisEntry->for_seating)->toBeFalsy()

View File

@ -0,0 +1,260 @@
<?php
use App\Models\Audition;
use App\Models\Entry;
use App\Models\Event;
use App\Models\Student;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->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);
});
});