Work on refactoring student controller and test

This commit is contained in:
Matt Young 2025-07-04 17:20:49 -05:00
parent c22f3ddadf
commit 9717ae852e
12 changed files with 178 additions and 49 deletions

View File

@ -2,10 +2,10 @@
namespace App\Http\Controllers;
use App\Http\Requests\StudentStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Student;
use App\Rules\UniqueFullNameAtSchool;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -31,58 +31,24 @@ class StudentController extends Controller
['students' => $students, 'auditions' => $auditions, 'shirtSizes' => $shirtSizes]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request)
public function store(StudentStoreRequest $request)
{
if ($request->user()->cannot('create', Student::class)) {
abort(403);
}
$request->validate([
'first_name' => ['required'],
'last_name' => [
'required',
new UniqueFullNameAtSchool(request('first_name'), request('last_name'), Auth::user()->school_id),
],
'grade' => ['required', 'integer'],
'shirt_size' => [
'nullable',
function ($attribute, $value, $fail) {
if (! array_key_exists($value, Student::$shirtSizes)) {
$fail("The selected $attribute is invalid.");
}
},
],
]);
$student = Student::create([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'school_id' => Auth::user()->school_id,
]);
if (request('shirt_size') !== 'none') {
if ($request['shirt_size'] !== 'none') {
$student->update(['optional_data->shirt_size' => $request['shirt_size']]);
}
$message = 'Created student #'.$student->id.' - '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
]);
return redirect('/students')->with('success', 'Student Created');
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Requests;
use App\Models\Student;
use App\Rules\UniqueFullNameAtSchool;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use function array_key_exists;
use function request;
class StudentStoreRequest extends FormRequest
{
public function rules(): array
{
return [
'first_name' => ['required'],
'last_name' => [
'required',
new UniqueFullNameAtSchool(request('first_name'), request('last_name'), Auth::user()->school_id),
],
'grade' => ['required', 'integer'],
'shirt_size' => [
'nullable',
function ($attribute, $value, $fail) {
if (! array_key_exists($value, Student::$shirtSizes)) {
$fail("The selected $attribute is invalid.");
}
},
],
];
}
public function authorize(): bool
{
return true;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Observers;
use App\Models\AuditLogEntry;
use App\Models\Student;
class StudentObserver
{
/**
* Handle the Student "created" event.
*/
public function created(Student $student): void
{
$message = 'Created student #'.$student->id.' - '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email ?? 'none',
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
]);
}
/**
* Handle the Student "updated" event.
*/
public function updated(Student $student): void
{
//
}
/**
* Handle the Student "deleted" event.
*/
public function deleted(Student $student): void
{
//
}
/**
* Handle the Student "restored" event.
*/
public function restored(Student $student): void
{
//
}
/**
* Handle the Student "force deleted" event.
*/
public function forceDeleted(Student $student): void
{
//
}
}

View File

@ -12,11 +12,13 @@ use App\Models\Entry;
use App\Models\EntryFlag;
use App\Models\SchoolEmailDomain;
use App\Models\ScoreSheet;
use App\Models\Student;
use App\Observers\BonusScoreObserver;
use App\Observers\EntryFlagObserver;
use App\Observers\EntryObserver;
use App\Observers\SchoolEmailDomainObserver;
use App\Observers\ScoreSheetObserver;
use App\Observers\StudentObserver;
use App\Services\AuditionService;
use App\Services\DoublerService;
use App\Services\DrawService;
@ -55,6 +57,7 @@ class AppServiceProvider extends ServiceProvider
Entry::observe(EntryObserver::class);
SchoolEmailDomain::observe(SchoolEmailDomainObserver::class);
ScoreSheet::observe(ScoreSheetObserver::class);
Student::observe(StudentObserver::class);
EntryFlag::observe(EntryFlagObserver::class);
// Model::preventLazyLoading(! app()->isProduction());

View File

@ -26,4 +26,13 @@ class StudentFactory extends Factory
'grade' => rand(7, 12),
];
}
public function forSchool(School $school)
{
return $this->state(function (array $attributes) use ($school) {
return [
'school_id' => $school->id,
];
});
}
}

View File

@ -20,7 +20,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Entry Related Routes
Route::middleware([
'auth', 'verified', 'can:create,App\Models\Entry',
'auth', 'verified',
])->controller(EntryController::class)->group(function () {
Route::get('/entries', 'index')->name('entries.index');
Route::get('/entries/create', 'create')->name('entries.create');
@ -37,7 +37,7 @@ Route::middleware([
// Student Related Routes
Route::middleware([
'auth', 'verified', 'can:create,App\Models\Student',
'auth', 'verified',
])->controller(StudentController::class)->group(function () {
Route::get('/students', 'index')->name('students.index');
Route::post('students', 'store')->name('students.store');
@ -61,7 +61,7 @@ Route::middleware(['auth', 'verified'])->controller(SchoolController::class)->gr
// Doubler Related Routes
Route::middleware([
'auth', 'verified', 'can:viewAny,App\Models\DoublerRequest',
'auth', 'verified',
])->controller(DoublerRequestController::class)->prefix('doubler_request')->group(function () {
Route::get('/', 'index')->name('doubler_request.index');
Route::post('/', 'makeRequest')->name('doubler_request.make_request');

View File

@ -68,7 +68,7 @@ it('shows students index only for a user with a school', function () {
$user = User::factory()->create();
$this->actingAs($user);
get(route('students.index'))
->assertStatus(403);
->assertRedirect(route('dashboard'));
$school = School::factory()->create();
$user->school_id = $school->id;

View File

@ -146,7 +146,7 @@ it('logs the entry creation', function () {
$audition = Audition::factory()->create(['minimum_grade' => 9, 'maximum_grade' => 12]);
$this->scribe->createEntry($student, $audition);
$thisEntry = Entry::where('student_id', $student->id)->first();
$logEntry = AuditLogEntry::first();
$logEntry = AuditLogEntry::orderBy('id', 'desc')->first();
expect($logEntry->message)->toEqual('Entered '.$thisEntry->student->full_name().' from '.$thisEntry->student->school->name.' in '.$audition->name.'.')
->and($logEntry->affected['entries'])->toEqual([$thisEntry->id])
->and($logEntry->affected['students'])->toEqual([$thisEntry->student_id])

View File

@ -203,7 +203,7 @@ it('logs changes', function () {
$originalEntry = Entry::find($this->entry->id);
$newAudition = Audition::factory()->create(['minimum_grade' => 9, 'maximum_grade' => 10, 'name' => 'Alphorn']);
($this->entryScribe)($this->entry, ['audition' => $newAudition]);
$logEntry = AuditLogEntry::latest()->first();
$logEntry = AuditLogEntry::orderBy('id', 'desc')->first();
expect($logEntry->affected['auditions'])->toEqual([$originalEntry->audition_id, $newAudition->id])
->and($logEntry->affected['entries'])->toEqual([$this->entry->id])
->and($logEntry->affected['students'])->toEqual([$this->entry->student_id])

View File

@ -6,6 +6,7 @@ use App\Actions\Tabulation\EnterNoShow;
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\SubscoreDefinition;
use App\Models\User;
@ -29,7 +30,7 @@ it('can enter a no-show', function () {
it('creates a log entry when entering a no-show', function () {
($this->rollTaker)($this->entry);
$logEntry = \App\Models\AuditLogEntry::latest()->first();
$logEntry = AuditLogEntry::orderBy('id', 'desc')->first();
expect($logEntry->message)->toStartWith('No Show has been entered for');
});
@ -40,7 +41,7 @@ it('can enter a failed prelim', function () {
it('creates a log entry when entering a failed prelim', function () {
($this->rollTaker)($this->entry, 'failprelim');
$logEntry = \App\Models\AuditLogEntry::latest()->first();
$logEntry = AuditLogEntry::orderBy('id', 'desc')->first();
expect($logEntry->message)->toStartWith('Failed prelim has been entered for');
});

View File

@ -5,6 +5,7 @@
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\ScoreSheet;
use App\Models\SubscoreDefinition;
@ -122,6 +123,6 @@ it('removes a no-show flag from an entry', function () {
it('logs score entry', function () {
($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
$logEntry = \App\Models\AuditLogEntry::latest()->first();
$logEntry = AuditLogEntry::orderBy('id', 'desc')->first();
expect($logEntry->message)->toStartWith('Entered Score for entry id ');
});

View File

@ -0,0 +1,52 @@
<?php
use App\Models\Audition;
use App\Models\School;
use App\Models\Student;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
describe('Test the index method of the student controller', function () {
it('redirects to the dashboard if the user does not have a school', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('students.index'));
$response->assertRedirect(route('dashboard'));
actAsAdmin();
$response = $this->get(route('students.index'));
$response->assertRedirect(route('dashboard'));
});
it('redirects to the students index if the user has a school', function () {
$user = User::factory()->create();
$school = School::factory()->create();
$user->school_id = $school->id;
$user->save();
$this->actingAs($user);
$response = $this->get(route('students.index'));
$response->assertOk();
});
it('returns the view students.index', function () {
$user = User::factory()->create();
$school = School::factory()->create();
$audition = Audition::factory()->create();
$student = Student::factory()->forSchool($school)->create();
$user->school_id = $school->id;
$user->save();
$this->actingAs($user);
$response = $this->get(route('students.index'));
$response->assertViewIs('students.index')
->assertViewHas('students', function ($students) use ($student) {
return $students->contains($student);
})
->assertViewHas('auditions', function ($auditions) use ($audition) {
return $auditions->contains($audition);
})
->assertSee(route('students.store'));
});
});
describe('Test the store method of the student controller', function () {
});