diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index 9898526..c7b8cdb 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -4,17 +4,13 @@ namespace App\Http\Controllers\Admin; use App\Actions\Entries\CreateEntry; 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; use App\Models\School; use App\Models\Seat; use App\Models\Student; -use App\Services\ScoreService; use Illuminate\Http\Request; use function auditionSetting; @@ -68,7 +64,6 @@ class EntryController extends Controller if (isset($filters['entry_type']) && $filters['entry_type']) { // TODO define actions for each possible type filter from index.blade.php of the admin entry match ($filters['entry_type']) { - 'all' => null, 'seats' => $entries->where('for_seating', true), 'advancement' => $entries->where('for_advancement', true), 'seatsOnly' => $entries->where('for_seating', true)->where('for_advancement', false), @@ -110,11 +105,16 @@ class EntryController extends Controller public function store(EntryStoreRequest $request, CreateEntry $creator) { $validData = $request->validatedWithEnterFor(); - try { - $entry = $creator($validData['student_id'], $validData['audition_id'], $enter_for); - } catch (ManageEntryException $ex) { - return redirect()->route('admin.entries.index')->with('error', $ex->getMessage()); - } + + /** @noinspection PhpUnhandledExceptionInspection */ + $entry = $creator( + student: $validData['student_id'], + audition: $validData['audition_id'], + for_seating: $validData['for_seating'], + for_advancement: $validData['for_advancement'], + late_fee_waived: $validData['late_fee_waived'], + ); + if ($validData['late_fee_waived']) { $entry->addFlag('late_fee_waived'); } @@ -122,7 +122,7 @@ class EntryController extends Controller return redirect(route('admin.entries.index'))->with('success', 'The entry has been added.'); } - public function edit(Entry $entry, ScoreService $scoreService) + public function edit(Entry $entry) { if ($entry->audition->hasFlag('seats_published')) { return to_route('admin.entries.index')->with('error', @@ -136,31 +136,33 @@ class EntryController extends Controller $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get(); $auditions = Audition::orderBy('score_order')->get(); - $scores = $entry->scoreSheets()->with('audition', 'judge')->get(); - foreach ($scores as $score) { - $score->entry = $entry; - $score->valid = $scoreService->isScoreSheetValid($score); - $score->seating_total_score = $score->seating_total ?? 0; - $score->advancement_total_score = $score->advancement_total ?? 0; - } + // TODO: When updating Laravel, can we use the chaperone method I heard about ot load the entry back into the score + $scores = $entry->scoreSheets()->with('audition', 'judge', 'entry')->get(); return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores')); } public function update(Request $request, Entry $entry, UpdateEntry $updater) { - if ($entry->audition->hasFlag('seats_published')) { + // If the entry's current audition is published, we can't change it + if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) { return to_route('admin.entries.index')->with('error', - 'Entries in auditions with seats published cannot be modified'); + 'Entries in published auditions cannot be modified'); } - if ($entry->audition->hasFlag('advancement_published')) { - return to_route('admin.entries.index')->with('error', - 'Entries in auditions with advancement results published cannot be modified'); - } $validData = request()->validate([ 'audition_id' => ['required', 'exists:auditions,id'], + 'late_fee_waived' => ['sometimes'], + 'for_seating' => ['sometimes'], + 'for_advancement' => ['sometimes'], ]); + $proposedAudition = Audition::find($validData['audition_id']); + + // If the entry's new audition is published, we can't change it + if ($proposedAudition->hasFlag('seats_published') || $proposedAudition->hasFlag('advancement_published')) { + return to_route('admin.entries.index')->with('error', + 'Entries cannot be moved to published auditions'); + } $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; @@ -170,11 +172,10 @@ class EntryController extends Controller if (! auditionSetting('advanceTo')) { $validData['for_seating'] = 1; } - try { - $updater($entry, $validData); - } catch (AuditionAdminException $e) { - return redirect()->route('admin.entries.index')->with('error', $e->getMessage()); - } + + /** @noinspection PhpUnhandledExceptionInspection */ + $updater($entry, $validData); + if ($validData['late_fee_waived']) { $entry->addFlag('late_fee_waived'); } else { @@ -184,17 +185,13 @@ class EntryController extends Controller return to_route('admin.entries.index')->with('success', 'Entry updated successfully'); } - public function destroy(Request $request, Entry $entry) + public function destroy(Entry $entry) { - if ($entry->audition->hasFlag('seats_published')) { + if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) { return to_route('admin.entries.index')->with('error', - 'Entries in auditions with seats published cannot be deleted'); + 'Entries in published auditions cannot be deleted'); } - if ($entry->audition->hasFlag('advancement_published')) { - return to_route('admin.entries.index')->with('error', - 'Entries in auditions with advancement results published cannot be deleted'); - } if (Seat::where('entry_id', $entry->id)->exists()) { return redirect()->route('admin.entries.index')->with('error', 'Cannot delete an entry that is seated'); } @@ -203,21 +200,7 @@ class EntryController extends Controller return redirect()->route('admin.entries.index')->with('error', 'Cannot delete an entry that has been scored'); } - 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('admin.entries.index')->with('success', 'Entry Deleted'); diff --git a/app/Models/ScoreSheet.php b/app/Models/ScoreSheet.php index 5d14979..304a0ba 100644 --- a/app/Models/ScoreSheet.php +++ b/app/Models/ScoreSheet.php @@ -49,7 +49,7 @@ class ScoreSheet extends Model // this function is used at resources/views/tabulation/entry_score_sheet.blade.php } - public function testValidity() + public function testValidity(): bool { return $this->audition->judges->contains('id', $this->user_id); } diff --git a/resources/views/admin/entries/edit.blade.php b/resources/views/admin/entries/edit.blade.php index f4fdd6c..a2b6912 100644 --- a/resources/views/admin/entries/edit.blade.php +++ b/resources/views/admin/entries/edit.blade.php @@ -88,12 +88,14 @@
{{ $score->judge->full_name() }} - + @if(! $score->entry->audition->hasFlag('seats_published') && ! $score->entry->audition->hasFlag('advancement_published')) + Confirm you would like to delete the {{ $score->entry->audition->name }} score for {{ $score->entry->student->full_name() }} by {{ $score->judge->full_name() }}. + @endif
@foreach($score->subscores as $subscore) @@ -105,16 +107,16 @@

{{ auditionSetting('auditionAbbreviation') }} Total - {{ $score->seating_total_score }} + {{ $score->seating_total }}

@if( auditionSetting('advanceTo'))

{{ auditionSetting('advanceTo') }} Total - {{ $score->advancement_total_score }} + {{ $score->advancement_total }}

@endif - @if(! $score->valid)) + @if(! $score->testValidity())

This score is invalid

diff --git a/tests/Feature/app/Http/Controllers/Admin/EntryControllerTest.php b/tests/Feature/app/Http/Controllers/Admin/EntryControllerTest.php index 327735d..f5af9ec 100644 --- a/tests/Feature/app/Http/Controllers/Admin/EntryControllerTest.php +++ b/tests/Feature/app/Http/Controllers/Admin/EntryControllerTest.php @@ -1,11 +1,16 @@ $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()); }); @@ -68,7 +72,6 @@ describe('EntryController::index', function () { '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()); }); @@ -79,7 +82,6 @@ describe('EntryController::index', function () { '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()); }); @@ -90,7 +92,6 @@ describe('EntryController::index', function () { '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() @@ -104,7 +105,6 @@ describe('EntryController::index', function () { '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() @@ -118,7 +118,6 @@ describe('EntryController::index', function () { '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() @@ -132,7 +131,6 @@ describe('EntryController::index', function () { '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() @@ -146,7 +144,6 @@ describe('EntryController::index', function () { '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() @@ -160,7 +157,6 @@ describe('EntryController::index', function () { '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() @@ -174,7 +170,6 @@ describe('EntryController::index', function () { '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() @@ -254,7 +249,163 @@ describe('EntryController::store', function () { $this->post(route('admin.entries.store'), $this->testSubmitData)->assertRedirect(route('dashboard')); }); it('creates an entry', function () { + $startingEntryCount = Entry::count(); actAsAdmin(); $this->post(route('admin.entries.store'), $this->testSubmitData); + expect(Entry::count())->toEqual($startingEntryCount + 1); + }); +}); + +describe('EntryController::edit', function () { + it('denies access to non-admins', function () { + $this->get(route('admin.entries.edit', $this->entry1))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.entries.edit', $this->entry1))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.entries.edit', $this->entry1))->assertRedirect(route('dashboard')); + }); + it('will not edit a published audition', function () { + actAsAdmin(); + $this->entry1->audition->addFlag('seats_published'); + $this->entry1->refresh(); + $this->get(route('admin.entries.edit', $this->entry1))->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('error', 'Entries in auditions with seats published cannot be modified'); + $this->entry2->audition->addFlag('advancement_published'); + $this->entry2->refresh(); + $this->get(route('admin.entries.edit', $this->entry2))->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('error', 'Entries in auditions with advancement results published cannot be modified'); + }); + it('presents a form to edit an entry', function () { + actAsAdmin(); + $this->get(route('admin.entries.edit', $this->entry1))->assertOk() + ->assertViewIs('admin.entries.edit'); + }); +}); + +describe('EntryController::update', function () { + it('denies access to non-admins', function () { + $this->patch(route('admin.entries.update', $this->entry1))->assertRedirect(route('home')); + actAsNormal(); + $this->patch(route('admin.entries.update', $this->entry1))->assertRedirect(route('dashboard')); + actAsTab(); + $this->patch(route('admin.entries.update', $this->entry1))->assertRedirect(route('dashboard')); + }); + it('will not update an entry whose current audition has published seats', function () { + actAsAdmin(); + $this->auditions[0]->addFlag('seats_published'); + $response = $this->patch(route('admin.entries.update', $this->entry1), [ + 'audition_id' => $this->auditions[1]->id, + ]); + $response->assertRedirect(route('admin.entries.index'))->assertSessionHas('error', + 'Entries in published auditions cannot be modified'); + }); + it('will not update an entry whose current audition has published advancement', function () { + actAsAdmin(); + $this->auditions[0]->addFlag('advancement_published'); + $response = $this->patch(route('admin.entries.update', $this->entry1), [ + 'audition_id' => $this->auditions[1]->id, + ]); + $response->assertRedirect(route('admin.entries.index'))->assertSessionHas('error', + 'Entries in published auditions cannot be modified'); + }); + it('will not update an entry whose proposed audition has published seats', function () { + actAsAdmin(); + $this->auditions[1]->addFlag('seats_published'); + $response = $this->patch(route('admin.entries.update', $this->entry1), [ + 'audition_id' => $this->auditions[1]->id, + ]); + $response->assertRedirect(route('admin.entries.index'))->assertSessionHas('error', + 'Entries cannot be moved to published auditions'); + }); + it('will not update an entry whose proposed audition has published advancement', function () { + actAsAdmin(); + $this->auditions[1]->addFlag('advancement_published'); + $response = $this->patch(route('admin.entries.update', $this->entry1), [ + 'audition_id' => $this->auditions[1]->id, + ]); + $response->assertRedirect(route('admin.entries.index'))->assertSessionHas('error', + 'Entries cannot be moved to published auditions'); + }); + it('chan change entry type', function () { + actAsAdmin(); + $response = $this->patch(route('admin.entries.update', $this->entry1), [ + 'audition_id' => $this->auditions[0]->id, + 'for_advancement' => 'on', + ]); + $response->assertRedirect(route('admin.entries.index')); + $this->entry1->refresh(); + expect($this->entry1->for_seating)->toBeFalsy() + ->and($this->entry1->for_advancement)->toBeTruthy(); + }); + it('can waive late fees', function () { + actAsAdmin(); + $response = $this->patch(route('admin.entries.update', $this->entry1), [ + 'audition_id' => $this->auditions[0]->id, + 'for_advancement' => 'on', + 'late_fee_waived' => 'on', + ]); + $response->assertRedirect(route('admin.entries.index')); + $this->entry1->refresh(); + expect($this->entry1->hasFlag('late_fee_waived'))->toBeTruthy(); + }); + it('if we dont have advancement, for_seating must be true', function () { + actAsAdmin(); + Settings::set('advanceTo', ''); + $response = $this->patch(route('admin.entries.update', $this->entry1), [ + 'audition_id' => $this->auditions[0]->id, + ]); + $response->assertRedirect(route('admin.entries.index')); + $this->entry1->refresh(); + expect($this->entry1->for_seating)->toBeTruthy() + ->and($this->entry1->for_advancement)->toBeFalsy(); + }); +}); + +describe('EntryController::destroy', function () { + it('denies access to non-admins', function () { + $this->delete(route('admin.entries.destroy', $this->entry1))->assertRedirect(route('home')); + actAsNormal(); + $this->delete(route('admin.entries.destroy', $this->entry1))->assertRedirect(route('dashboard')); + actAsTab(); + $this->delete(route('admin.entries.destroy', $this->entry1))->assertRedirect(route('dashboard')); + }); + it('will not delete an entry with a published audition', function () { + actAsAdmin(); + $this->auditions[0]->addFlag('seats_published'); + $this->auditions[1]->addFlag('advancement_published'); + $this->delete(route('admin.entries.destroy', $this->entry1))->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('error', 'Entries in published auditions cannot be deleted'); + $this->delete(route('admin.entries.destroy', $this->entry2))->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('error', 'Entries in published auditions cannot be deleted'); + }); + it('will not delete an entry that is seated', function () { + actAsAdmin(); + $ensemble = Ensemble::factory()->create(); + DB::table('seats')->insert([ + 'ensemble_id' => $ensemble->id, + 'audition_id' => $this->entry1->audition_id, + 'seat' => 1, + 'entry_id' => $this->entry1->id, + ]); + $this->delete(route('admin.entries.destroy', $this->entry1))->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('error', 'Cannot delete an entry that is seated'); + }); + it('will not delete an entry that is scored', function () { + actAsAdmin(); + DB::table('score_sheets')->insert([ + 'user_id' => User::factory()->create()->id, + 'entry_id' => $this->entry1->id, + 'subscores' => json_encode([3, 5, 6]), + 'seating_total' => 44, + 'advancement_total' => 55, + ]); + $this->delete(route('admin.entries.destroy', $this->entry1))->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('error', 'Cannot delete an entry that has been scored'); + }); + it('can delete an entry', function () { + actAsAdmin(); + $this->delete(route('admin.entries.destroy', $this->entry1))->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('success', 'Entry Deleted'); + assertDatabaseMissing('entries', ['id' => $this->entry1->id]); }); }); diff --git a/tests/Feature/app/Models/ScoreSheetTest.php b/tests/Feature/app/Models/ScoreSheetTest.php index 829853e..afbbd87 100644 --- a/tests/Feature/app/Models/ScoreSheetTest.php +++ b/tests/Feature/app/Models/ScoreSheetTest.php @@ -1,5 +1,7 @@