add test for admin EntryController

This commit is contained in:
Matt Young 2025-07-08 23:07:28 -05:00
parent 4963124d22
commit fa25e76c5b
5 changed files with 207 additions and 69 deletions

View File

@ -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 {
/** @noinspection PhpUnhandledExceptionInspection */
$updater($entry, $validData);
} catch (AuditionAdminException $e) {
return redirect()->route('admin.entries.index')->with('error', $e->getMessage());
}
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');

View File

@ -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);
}

View File

@ -88,12 +88,14 @@
<div class="grid grid-cols-2 border-b">
<span class="font-semibold text-sm">{{ $score->judge->full_name() }}</span>
<span class="text-right mb-2">
@if(! $score->entry->audition->hasFlag('seats_published') && ! $score->entry->audition->hasFlag('advancement_published'))
<x-delete-resource-modal
size="15"
action="{{route('scores.destroy',$score->id)}}"
title="Delete score">
Confirm you would like to delete the {{ $score->entry->audition->name }} score for {{ $score->entry->student->full_name() }} by {{ $score->judge->full_name() }}.
</x-delete-resource-modal>
@endif
</span>
</div>
@foreach($score->subscores as $subscore)
@ -105,16 +107,16 @@
<p class="grid grid-cols-2 border-b">
<span
class="font-semibold text-sm">{{ auditionSetting('auditionAbbreviation') }} Total</span>
<span class="text-right font-semibold">{{ $score->seating_total_score }}</span>
<span class="text-right font-semibold">{{ $score->seating_total }}</span>
</p>
@if( auditionSetting('advanceTo'))
<p class="grid grid-cols-2 border-b">
<span class="font-semibold text-sm">{{ auditionSetting('advanceTo') }} Total</span>
<span class="text-right font-semibold">{{ $score->advancement_total_score }}</span>
<span class="text-right font-semibold">{{ $score->advancement_total }}</span>
</p>
@endif
@if(! $score->valid))
@if(! $score->testValidity())
<div class="bg-red-500 text-white p-2 rounded mt-2">
<p class="text-sm">This score is invalid</p>
</div>

View File

@ -1,11 +1,16 @@
<?php
use App\Models\Audition;
use App\Models\Ensemble;
use App\Models\Entry;
use App\Models\Event;
use App\Models\Student;
use App\Models\User;
use App\Settings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\assertDatabaseMissing;
uses(RefreshDatabase::class);
beforeEach(function () {
@ -57,7 +62,6 @@ describe('EntryController::index', function () {
'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());
});
@ -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]);
});
});

View File

@ -1,5 +1,7 @@
<?php
/** @noinspection PhpUndefinedFieldInspection */
use App\Models\Audition;
use App\Models\Entry;
use App\Models\Room;