From 75f60d0cdbef6d522f469ff3e35bc3628f9602a6 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 7 May 2025 15:06:13 -0500 Subject: [PATCH 1/6] Historical seat migration and relationships --- app/Models/HistoricalSeat.php | 17 ++++++++++ app/Models/Student.php | 5 +++ ...7_195828_create_historical_seats_table.php | 31 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 app/Models/HistoricalSeat.php create mode 100644 database/migrations/2025_05_07_195828_create_historical_seats_table.php diff --git a/app/Models/HistoricalSeat.php b/app/Models/HistoricalSeat.php new file mode 100644 index 0000000..cff2db9 --- /dev/null +++ b/app/Models/HistoricalSeat.php @@ -0,0 +1,17 @@ +belongsTo(Student::class); + } +} diff --git a/app/Models/Student.php b/app/Models/Student.php index 925fc29..2554aa2 100644 --- a/app/Models/Student.php +++ b/app/Models/Student.php @@ -45,6 +45,11 @@ class Student extends Model return $this->belongsTo(School::class); } + public function historicalSeats(): HasMany + { + return $this->hasMany(HistoricalSeat::class); + } + public function users(): HasManyThrough { return $this->hasManyThrough( diff --git a/database/migrations/2025_05_07_195828_create_historical_seats_table.php b/database/migrations/2025_05_07_195828_create_historical_seats_table.php new file mode 100644 index 0000000..8d52912 --- /dev/null +++ b/database/migrations/2025_05_07_195828_create_historical_seats_table.php @@ -0,0 +1,31 @@ +id(); + $table->timestamps(); + $table->foreignIdFor(Student::class)->constrained()->onDelete('cascade'); + $table->integer('year'); + $table->string('seat_description'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('historical_seats'); + } +}; From 095881761c84421132dee40615453b80deba15bb Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 29 May 2025 11:10:15 -0500 Subject: [PATCH 2/6] Add action to save seats to historical table. --- .../RecordHistoricalSeats.php | 34 +++++++++++++++++++ app/Models/HistoricalSeat.php | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 app/Actions/YearEndProcedures/RecordHistoricalSeats.php diff --git a/app/Actions/YearEndProcedures/RecordHistoricalSeats.php b/app/Actions/YearEndProcedures/RecordHistoricalSeats.php new file mode 100644 index 0000000..fee9460 --- /dev/null +++ b/app/Actions/YearEndProcedures/RecordHistoricalSeats.php @@ -0,0 +1,34 @@ +saveSeats(); + } + + public function saveSeats(): void + { + $seats = Seat::all(); + foreach ($seats as $seat) { + $student_id = $seat->student->id; + $year = Carbon::now()->year; + $seat_description = $seat->ensemble->name.' - '.$seat->audition->name.' - '.$seat->seat; + HistoricalSeat::create([ + 'student_id' => $student_id, + 'year' => $year, + 'seat_description' => $seat_description, + ]); + } + } +} diff --git a/app/Models/HistoricalSeat.php b/app/Models/HistoricalSeat.php index cff2db9..7403cff 100644 --- a/app/Models/HistoricalSeat.php +++ b/app/Models/HistoricalSeat.php @@ -10,6 +10,8 @@ class HistoricalSeat extends Model { use HasFactory; + protected $guarded = []; + public function student(): BelongsTo { return $this->belongsTo(Student::class); From 92c8de0cf28805d3814ab24e9520c4f718549c55 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 29 May 2025 13:42:49 -0500 Subject: [PATCH 3/6] Add action to save seats to historical table. --- .../RecordHistoricalSeats.php | 33 +++++++---- .../Actions/RecordHistoricalSeatsTest.php | 56 +++++++++++++++++++ 2 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 tests/Feature/Actions/RecordHistoricalSeatsTest.php diff --git a/app/Actions/YearEndProcedures/RecordHistoricalSeats.php b/app/Actions/YearEndProcedures/RecordHistoricalSeats.php index fee9460..08701f1 100644 --- a/app/Actions/YearEndProcedures/RecordHistoricalSeats.php +++ b/app/Actions/YearEndProcedures/RecordHistoricalSeats.php @@ -2,6 +2,7 @@ namespace App\Actions\YearEndProcedures; +use App\Exceptions\AuditionAdminException; use App\Models\HistoricalSeat; use App\Models\Seat; use Carbon\Carbon; @@ -17,18 +18,28 @@ class RecordHistoricalSeats $this->saveSeats(); } - public function saveSeats(): void + /** + * @throws AuditionAdminException + */ + public function saveSeats() { - $seats = Seat::all(); - foreach ($seats as $seat) { - $student_id = $seat->student->id; - $year = Carbon::now()->year; - $seat_description = $seat->ensemble->name.' - '.$seat->audition->name.' - '.$seat->seat; - HistoricalSeat::create([ - 'student_id' => $student_id, - 'year' => $year, - 'seat_description' => $seat_description, - ]); + if (! auth()->user() or ! auth()->user()->is_admin) { + throw new AuditionAdminException('Only administrators may perform this action'); } + $seats = Seat::all(); + if ($seats->count() > 0) { + foreach ($seats as $seat) { + $student_id = $seat->student->id; + $year = Carbon::now()->year; + $seat_description = $seat->ensemble->name.' - '.$seat->audition->name.' - '.$seat->seat; + HistoricalSeat::create([ + 'student_id' => $student_id, + 'year' => $year, + 'seat_description' => $seat_description, + ]); + } + } + + return true; } } diff --git a/tests/Feature/Actions/RecordHistoricalSeatsTest.php b/tests/Feature/Actions/RecordHistoricalSeatsTest.php new file mode 100644 index 0000000..f1ab21c --- /dev/null +++ b/tests/Feature/Actions/RecordHistoricalSeatsTest.php @@ -0,0 +1,56 @@ + $action())->toThrow( + AuditionAdminException::class, + 'Only administrators may perform this action' + ); + + actAsNormal(); + expect(fn () => $action())->toThrow( + AuditionAdminException::class, + 'Only administrators may perform this action' + ); + + actAsAdmin(); + expect($action->saveSeats())->toBeTrue(); + +}); + +it('saves a seated student to the historical table', function () { + actAsAdmin(); + $entry = Entry::factory()->create(); + Entry::factory(5)->create(); + $action = new RecordHistoricalSeats(); + $ensemble = Ensemble::create([ + 'event_id' => $entry->audition->event_id, + 'name' => 'Test Ensemble', + 'code' => 'te', + 'rank' => 1, + ]); + $seat = Seat::create([ + 'ensemble_id' => $ensemble->id, + 'audition_id' => $entry->audition_id, + 'seat' => '1', + 'entry_id' => $entry->id, + ]); + $action->saveSeats(); + $historical_seats = HistoricalSeat::all(); + $test_seat = $historical_seats->first(); + expect($test_seat->student_id)->toBe($entry->student_id) + ->and($historical_seats)->toHaveCount(1) + ->and($test_seat->seat_description)->toBe($ensemble->name.' - '.$entry->audition->name.' - '.$seat->seat) + ->and(Student::count())->toBe(6); +}); From 3a9f5ab123cb9d30949bd37c2cae0ef9063f8552 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 29 May 2025 15:19:56 -0500 Subject: [PATCH 4/6] Create year end cleanup action --- .../YearEndProcedures/YearEndCleanup.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 app/Actions/YearEndProcedures/YearEndCleanup.php diff --git a/app/Actions/YearEndProcedures/YearEndCleanup.php b/app/Actions/YearEndProcedures/YearEndCleanup.php new file mode 100644 index 0000000..ccf4861 --- /dev/null +++ b/app/Actions/YearEndProcedures/YearEndCleanup.php @@ -0,0 +1,76 @@ +cleanup(); + } + + /** + * @param $options array array of reset options - possible values are deleteRooms + * removeAuditionsFromRoom unassignJudges + + * + * @throws AuditionAdminException + */ + public function cleanup($options = []): true + { + if (! auth()->user() or ! auth()->user()->is_admin) { + throw new AuditionAdminException('Only administrators may perform this action'); + } + + // Delete all records in the audit_log_entries table + AuditLogEntry::truncate(); + AuditionFlag::truncate(); + BonusScore::truncate(); + CalculatedScore::truncate(); + DoublerRequest::truncate(); + EntryFlag::truncate(); + ScoreSheet::truncate(); + Seat::truncate(); + JudgeAdvancementVote::truncate(); + DB::table('entries')->delete(); + NominationEnsembleEntry::truncate(); + + if (in_array('deleteRooms', $options)) { + DB::table('auditions')->update(['room_id' => null]); + DB::table('auditions')->update(['order_in_room' => null]); + DB::table('room_judges')->truncate(); + DB::table('rooms')->truncate(); + } + + if (in_array('removeAuditionsFromRoom', $options)) { + DB::table('auditions')->update(['room_id' => null]); + DB::table('auditions')->update(['order_in_room' => null]); + } + + if (in_array('unassignJudges', $options)) { + DB::table('room_judges')->truncate(); + } + + return true; + + } +} From b5cb2a78701214fc5daaa43a2c0240d73fc13428 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 29 May 2025 18:20:37 -0500 Subject: [PATCH 5/6] Add admin option to reset site. --- .../YearEndProcedures/YearEndCleanup.php | 23 +++++++++------- .../Admin/YearEndResetController.php | 26 +++++++++++++++++++ .../views/admin/year_end_reset.blade.php | 23 ++++++++++++++++ .../layout/navbar/menus/admin.blade.php | 1 + routes/admin.php | 5 ++++ 5 files changed, 68 insertions(+), 10 deletions(-) create mode 100644 app/Http/Controllers/Admin/YearEndResetController.php create mode 100644 resources/views/admin/year_end_reset.blade.php diff --git a/app/Actions/YearEndProcedures/YearEndCleanup.php b/app/Actions/YearEndProcedures/YearEndCleanup.php index ccf4861..8605c03 100644 --- a/app/Actions/YearEndProcedures/YearEndCleanup.php +++ b/app/Actions/YearEndProcedures/YearEndCleanup.php @@ -13,6 +13,7 @@ use App\Models\JudgeAdvancementVote; use App\Models\NominationEnsembleEntry; use App\Models\ScoreSheet; use App\Models\Seat; +use App\Models\Student; use Illuminate\Support\Facades\DB; use function auth; @@ -23,20 +24,20 @@ class YearEndCleanup { } - public function __invoke(): void + public function __invoke(array $options = []): void { - $this->cleanup(); + $this->cleanup($options); } /** * @param $options array array of reset options - possible values are deleteRooms - * removeAuditionsFromRoom unassignJudges - + * removeAuditionsFromRoom unassignJudges * * @throws AuditionAdminException */ - public function cleanup($options = []): true + public function cleanup(array $options = []): true { + if (! auth()->user() or ! auth()->user()->is_admin) { throw new AuditionAdminException('Only administrators may perform this action'); } @@ -54,20 +55,22 @@ class YearEndCleanup DB::table('entries')->delete(); NominationEnsembleEntry::truncate(); + Student::query()->increment('grade'); + if (in_array('deleteRooms', $options)) { DB::table('auditions')->update(['room_id' => null]); - DB::table('auditions')->update(['order_in_room' => null]); - DB::table('room_judges')->truncate(); - DB::table('rooms')->truncate(); + DB::table('auditions')->update(['order_in_room' => '0']); + DB::table('room_user')->truncate(); + DB::table('rooms')->delete(); } if (in_array('removeAuditionsFromRoom', $options)) { DB::table('auditions')->update(['room_id' => null]); - DB::table('auditions')->update(['order_in_room' => null]); + DB::table('auditions')->update(['order_in_room' => '0']); } if (in_array('unassignJudges', $options)) { - DB::table('room_judges')->truncate(); + DB::table('room_user')->truncate(); } return true; diff --git a/app/Http/Controllers/Admin/YearEndResetController.php b/app/Http/Controllers/Admin/YearEndResetController.php new file mode 100644 index 0000000..52d81d2 --- /dev/null +++ b/app/Http/Controllers/Admin/YearEndResetController.php @@ -0,0 +1,26 @@ +options; + $cleanUpProcedure($options); + auditionLog('Executed year end reset.', []); + + return redirect()->route('dashboard')->with('success', 'Year end reset completed'); + } +} diff --git a/resources/views/admin/year_end_reset.blade.php b/resources/views/admin/year_end_reset.blade.php new file mode 100644 index 0000000..47cf818 --- /dev/null +++ b/resources/views/admin/year_end_reset.blade.php @@ -0,0 +1,23 @@ + + Year End Reset + + Reset Options + + + + + + + Complete Year End Reset + + + Confirm Year End Reset + Confirm you would like to perform a year end reset. This will delete all seats, scores, entries, and log entries, + as well as any optional data you chose. It will also increment the grade of all students in the database. + This action will result in data loss and cannot be undone. + Confirm Reset + + + + + diff --git a/resources/views/components/layout/navbar/menus/admin.blade.php b/resources/views/components/layout/navbar/menus/admin.blade.php index cfae63a..00abdd5 100644 --- a/resources/views/components/layout/navbar/menus/admin.blade.php +++ b/resources/views/components/layout/navbar/menus/admin.blade.php @@ -32,6 +32,7 @@ Export Results Export Entries Print Stand Name Tags + Year End Reset diff --git a/routes/admin.php b/routes/admin.php index 13a4c17..bb55c10 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -21,6 +21,7 @@ use App\Http\Controllers\Admin\SchoolController; use App\Http\Controllers\Admin\ScoringGuideController; use App\Http\Controllers\Admin\StudentController; use App\Http\Controllers\Admin\UserController; +use App\Http\Controllers\Admin\YearEndResetController; use App\Http\Middleware\CheckIfAdmin; use Illuminate\Support\Facades\Route; @@ -33,6 +34,10 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::get('/recap', [RecapController::class, 'selectAudition'])->name('admin.recap.selectAudition'); Route::get('/recap/{audition}', [RecapController::class, 'showRecap'])->name('admin.recap.recap'); + // Year end prodecures + Route::get('/year_end_procedures', [YearEndResetController::class, 'index'])->name('admin.year_end_procedures'); + Route::post('/year_end_procedures', [YearEndResetController::class, 'execute'])->name('admin.year_end_procedures'); + Route::post('/auditions/roomUpdate', [ AuditionController::class, 'roomUpdate', ]); // Endpoint for JS assigning auditions to rooms From 15e46e493a3aa6f8be8b6aca5ca6a80cd80fbe45 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 29 May 2025 22:11:08 -0500 Subject: [PATCH 6/6] Add admin option to reset site. --- .../YearEndProcedures/YearEndCleanup.php | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/app/Actions/YearEndProcedures/YearEndCleanup.php b/app/Actions/YearEndProcedures/YearEndCleanup.php index 8605c03..933340b 100644 --- a/app/Actions/YearEndProcedures/YearEndCleanup.php +++ b/app/Actions/YearEndProcedures/YearEndCleanup.php @@ -24,7 +24,7 @@ class YearEndCleanup { } - public function __invoke(array $options = []): void + public function __invoke(?array $options = []): void { $this->cleanup($options); } @@ -35,13 +35,16 @@ class YearEndCleanup * * @throws AuditionAdminException */ - public function cleanup(array $options = []): true + public function cleanup(?array $options = []): true { if (! auth()->user() or ! auth()->user()->is_admin) { throw new AuditionAdminException('Only administrators may perform this action'); } + $historian = new RecordHistoricalSeats; + $historian(); + // Delete all records in the audit_log_entries table AuditLogEntry::truncate(); AuditionFlag::truncate(); @@ -57,20 +60,22 @@ class YearEndCleanup Student::query()->increment('grade'); - if (in_array('deleteRooms', $options)) { - DB::table('auditions')->update(['room_id' => null]); - DB::table('auditions')->update(['order_in_room' => '0']); - DB::table('room_user')->truncate(); - DB::table('rooms')->delete(); - } + if (is_array($options)) { + if (in_array('deleteRooms', $options)) { + DB::table('auditions')->update(['room_id' => null]); + DB::table('auditions')->update(['order_in_room' => '0']); + DB::table('room_user')->truncate(); + DB::table('rooms')->delete(); + } - if (in_array('removeAuditionsFromRoom', $options)) { - DB::table('auditions')->update(['room_id' => null]); - DB::table('auditions')->update(['order_in_room' => '0']); - } + if (in_array('removeAuditionsFromRoom', $options)) { + DB::table('auditions')->update(['room_id' => null]); + DB::table('auditions')->update(['order_in_room' => '0']); + } - if (in_array('unassignJudges', $options)) { - DB::table('room_user')->truncate(); + if (in_array('unassignJudges', $options)) { + DB::table('room_user')->truncate(); + } } return true;