Merge pull request #111 from okorpheus/EndYearProcedures

End year procedures implementation
This commit is contained in:
Matt 2025-05-29 22:13:24 -05:00 committed by GitHub
commit 6c51d134de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 295 additions and 0 deletions

View File

@ -0,0 +1,45 @@
<?php
namespace App\Actions\YearEndProcedures;
use App\Exceptions\AuditionAdminException;
use App\Models\HistoricalSeat;
use App\Models\Seat;
use Carbon\Carbon;
class RecordHistoricalSeats
{
public function __construct()
{
}
public function __invoke(): void
{
$this->saveSeats();
}
/**
* @throws AuditionAdminException
*/
public function saveSeats()
{
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;
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Actions\YearEndProcedures;
use App\Exceptions\AuditionAdminException;
use App\Models\AuditionFlag;
use App\Models\AuditLogEntry;
use App\Models\BonusScore;
use App\Models\CalculatedScore;
use App\Models\DoublerRequest;
use App\Models\EntryFlag;
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;
class YearEndCleanup
{
public function __construct()
{
}
public function __invoke(?array $options = []): void
{
$this->cleanup($options);
}
/**
* @param $options array array of reset options - possible values are deleteRooms
* removeAuditionsFromRoom unassignJudges
*
* @throws AuditionAdminException
*/
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();
BonusScore::truncate();
CalculatedScore::truncate();
DoublerRequest::truncate();
EntryFlag::truncate();
ScoreSheet::truncate();
Seat::truncate();
JudgeAdvancementVote::truncate();
DB::table('entries')->delete();
NominationEnsembleEntry::truncate();
Student::query()->increment('grade');
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('unassignJudges', $options)) {
DB::table('room_user')->truncate();
}
}
return true;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\YearEndProcedures\YearEndCleanup;
use App\Http\Controllers\Controller;
use function auditionLog;
class YearEndResetController extends Controller
{
public function index()
{
return view('admin.year_end_reset');
}
public function execute()
{
$cleanUpProcedure = new YearEndCleanup;
$options = request()->options;
$cleanUpProcedure($options);
auditionLog('Executed year end reset.', []);
return redirect()->route('dashboard')->with('success', 'Year end reset completed');
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class HistoricalSeat extends Model
{
use HasFactory;
protected $guarded = [];
public function student(): BelongsTo
{
return $this->belongsTo(Student::class);
}
}

View File

@ -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(

View File

@ -0,0 +1,31 @@
<?php
use App\Models\Student;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('historical_seats', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,23 @@
<x-layout.app>
<x-layout.page-header>Year End Reset</x-layout.page-header>
<x-card.card class="mt-5 max-w-xl m-auto">
<x-card.heading>Reset Options</x-card.heading>
<x-form.form action="{{ route('admin.year_end_procedures') }}">
<x-form.checkbox name="options[]" label="Delete Rooms" value="deleteRooms" />
<x-form.checkbox name="options[]" label="Remove Auditions From Rooms" value="removeAuditionsFromRoom" />
<x-form.checkbox name="options[]" label="Unassign Judges" value="unassignJudges" />
<x-form.footer class="mb-3" x-data="{ 'showModal': false }" @keydown.escape="showModal = false">
<x-form.button type="button" @click="showModal = true">
Complete Year End Reset
</x-form.button>
<x-modal-body>
<x-slot:title>Confirm Year End Reset</x-slot:title>
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.
<x-form.button class="mt-3">Confirm Reset</x-form.button>
</x-modal-body>
</x-form.footer>
</x-form.form>
</x-card.card>
</x-layout.app>

View File

@ -32,6 +32,7 @@
<a href="{{route('admin.export_results')}}" class="block p-2 hover:text-indigo-600">Export Results</a>
<a href="{{route('admin.export_entries')}}" class="block p-2 hover:text-indigo-600">Export Entries</a>
<a href="{{route('admin.print_stand_name_tags')}}" class="block p-2 hover:text-indigo-600">Print Stand Name Tags</a>
<a href="{{route('admin.year_end_procedures')}}" class="block p-2 hover:text-indigo-600">Year End Reset</a>
</div>
</div>
</div>

View File

@ -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

View File

@ -0,0 +1,56 @@
<?php
use App\Actions\YearEndProcedures\RecordHistoricalSeats;
use App\Exceptions\AuditionAdminException;
use App\Models\Ensemble;
use App\Models\Entry;
use App\Models\HistoricalSeat;
use App\Models\Seat;
use App\Models\Student;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('only executes for an admin user', function () {
$action = new RecordHistoricalSeats();
expect(fn () => $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);
});