diff --git a/app/Http/Controllers/Admin/SchoolController.php b/app/Http/Controllers/Admin/SchoolController.php index e0c9485..da73483 100644 --- a/app/Http/Controllers/Admin/SchoolController.php +++ b/app/Http/Controllers/Admin/SchoolController.php @@ -49,6 +49,7 @@ class SchoolController extends Controller if (! Auth::user()->is_admin) { abort(403); } + $school->loadCount('students'); return view('admin.schools.edit', ['school' => $school]); } @@ -71,7 +72,8 @@ class SchoolController extends Controller 'zip' => request('zip'), ]); - return redirect()->route('admin.schools.show', ['school' => $school->id])->with('success', 'School '.$school->name.' updated'); + return redirect()->route('admin.schools.show', ['school' => $school->id])->with('success', + 'School '.$school->name.' updated'); } public function create() @@ -104,6 +106,17 @@ class SchoolController extends Controller return redirect('/admin/schools')->with('success', 'School '.$school->name.' created'); } + public function destroy(School $school) + { + if ($school->students()->count() > 0) { + return to_route('admin.schools.index')->with('error', 'You cannot delete a school with students.'); + } + $name = $school->name; + $school->delete(); + + return to_route('admin.schools.index')->with('success', 'School '.$school->name.' deleted'); + } + public function add_domain(School $school) { if (! Auth::user()->is_admin) { @@ -115,7 +128,8 @@ class SchoolController extends Controller ]); SchoolEmailDomain::updateOrInsert([ 'school_id' => $school->id, - 'domain' => request('domain')]); + 'domain' => request('domain'), + ]); return redirect()->route('admin.schools.show', $school)->with('success', 'Domain Added'); diff --git a/app/Http/Controllers/Admin/StudentController.php b/app/Http/Controllers/Admin/StudentController.php index 6c775c9..24d29a5 100644 --- a/app/Http/Controllers/Admin/StudentController.php +++ b/app/Http/Controllers/Admin/StudentController.php @@ -9,7 +9,9 @@ use App\Models\Student; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; use function abort; +use function to_route; use function view; class StudentController extends Controller @@ -19,7 +21,7 @@ class StudentController extends Controller if (! Auth::user()->is_admin) { abort(403); } - $students = Student::with(['school', 'entries'])->orderBy('last_name')->orderBy('first_name')->paginate(15); + $students = Student::with(['school'])->withCount('entries')->orderBy('last_name')->orderBy('first_name')->paginate(15); return view('admin.students.index', ['students' => $students]); } @@ -66,8 +68,9 @@ class StudentController extends Controller $minGrade = Audition::min('minimum_grade'); $maxGrade = Audition::max('maximum_grade'); $schools = School::orderBy('name')->get(); - - return view('admin.students.edit', ['student' => $student, 'schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]); + $student->loadCount('entries'); + return view('admin.students.edit', + ['student' => $student, 'schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]); } public function update(Request $request, Student $student) @@ -84,7 +87,8 @@ class StudentController extends Controller foreach ($student->entries as $entry) { if ($entry->audition->minimum_grade > request('grade') || $entry->audition->maximum_grade < request('grade')) { - return redirect('/admin/students/'.$student->id.'/edit')->with('error', 'This student is entered in an audition that is not available to their new grade.'); + return redirect('/admin/students/'.$student->id.'/edit')->with('error', + 'This student is entered in an audition that is not available to their new grade.'); } } @@ -98,4 +102,16 @@ class StudentController extends Controller return redirect('/admin/students'); } + + public function destroy(Student $student) + { + Log::debug('Deleting student '.$student->id); + if($student->entries()->count() > 0) { + return to_route('admin.students.index')->with('error', 'You cannot delete a student with entries.'); + } + $name = $student->full_name(); + $student->delete(); + + return to_route('admin.students.index')->with('success', 'Student '.$name.' deleted successfully.'); + } } diff --git a/composer.json b/composer.json index 177ffef..2f4c226 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "codedge/laravel-fpdf": "^1.12", "laravel/fortify": "^1.21", "laravel/framework": "^11.0", + "laravel/pail": "^1.1", "laravel/tinker": "^2.9", "predis/predis": "^2.2", "symfony/http-client": "^7.1", diff --git a/composer.lock b/composer.lock index d9ebfa5..b27dd43 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "91ced10faccf6fc9b3fe05fe288ec1d6", + "content-hash": "a21ed75b45b3f61cbc76446701fbc3ce", "packages": [ { "name": "bacon/bacon-qr-code", @@ -1491,6 +1491,84 @@ }, "time": "2024-05-21T17:57:45+00:00" }, + { + "name": "laravel/pail", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "c22fe771277971eb9cd224955996bcf39c1a710d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/c22fe771277971eb9cd224955996bcf39c1a710d", + "reference": "c22fe771277971eb9cd224955996bcf39c1a710d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-pcntl": "*", + "illuminate/console": "^10.24|^11.0", + "illuminate/contracts": "^10.24|^11.0", + "illuminate/log": "^10.24|^11.0", + "illuminate/process": "^10.24|^11.0", + "illuminate/support": "^10.24|^11.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "laravel/pint": "^1.13", + "orchestra/testbench": "^8.12|^9.0", + "pestphp/pest": "^2.20", + "pestphp/pest-plugin-type-coverage": "^2.3", + "phpstan/phpstan": "^1.10", + "symfony/var-dumper": "^6.3|^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2024-05-08T18:19:39+00:00" + }, { "name": "laravel/prompts", "version": "v0.1.22", diff --git a/resources/views/admin/schools/edit.blade.php b/resources/views/admin/schools/edit.blade.php index 99b93a9..8475878 100644 --- a/resources/views/admin/schools/edit.blade.php +++ b/resources/views/admin/schools/edit.blade.php @@ -1,7 +1,16 @@ - Edit School + + Edit School + @if($school->students_count === 0) + + + Confirm you would like to delete the school {{ $school->name }}. This action cannot be undone. + + + @endif + diff --git a/resources/views/admin/students/edit.blade.php b/resources/views/admin/students/edit.blade.php index 81436b2..d8acda4 100644 --- a/resources/views/admin/students/edit.blade.php +++ b/resources/views/admin/students/edit.blade.php @@ -1,10 +1,19 @@ - Edit Student + + Edit Student + @if($student->entries_count === 0) + + + Please confirm you'd like to delete this student. This action cannot be undone. + + + @endif + - - - + + + Grade @php($n = $minGrade) @@ -13,7 +22,7 @@ @php($n++); @endwhile - + School @foreach ($schools as $school) diff --git a/resources/views/admin/students/index.blade.php b/resources/views/admin/students/index.blade.php index 1d946f6..a56a5f3 100644 --- a/resources/views/admin/students/index.blade.php +++ b/resources/views/admin/students/index.blade.php @@ -23,7 +23,7 @@ {{ $student->full_name(true) }} {{ $student->school->name }} {{ $student->grade }} - {{ $student->entries->count() }} + {{ $student->entries_count }} @endforeach diff --git a/resources/views/components/delete-resource-modal.blade.php b/resources/views/components/delete-resource-modal.blade.php new file mode 100644 index 0000000..b147988 --- /dev/null +++ b/resources/views/components/delete-resource-modal.blade.php @@ -0,0 +1,126 @@ +@props(['size' => 20,'title','method'=>'DELETE','action']) + +
+ + + + + + +
diff --git a/resources/views/components/form/red-trash-button.blade.php b/resources/views/components/form/red-trash-button.blade.php index 8b126d9..8b93ee1 100644 --- a/resources/views/components/form/red-trash-button.blade.php +++ b/resources/views/components/form/red-trash-button.blade.php @@ -10,5 +10,4 @@ - diff --git a/routes/admin.php b/routes/admin.php index 8479387..6be959e 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -90,6 +90,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::post('/', 'store')->name('admin.students.store'); Route::get('/{student}/edit', 'edit')->name('admin.students.edit'); Route::patch('/{student}', 'update')->name('admin.students.update'); + Route::delete('/{student}', 'destroy')->name('admin.students.destroy'); }); // Admin School Routes @@ -103,6 +104,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::patch('/{school}', 'update')->name('admin.schools.update'); Route::post('/', 'store')->name('admin.schools.store'); Route::delete('/domain/{domain}', 'destroy_domain')->name('admin.schools.destroy_domain'); + Route::delete('/{school}', 'destroy')->name('admin.schools.destroy'); }); diff --git a/routes/console.php b/routes/console.php index eff2ed2..a258d86 100644 --- a/routes/console.php +++ b/routes/console.php @@ -6,3 +6,9 @@ use Illuminate\Support\Facades\Artisan; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote')->hourly(); + +Artisan::command('logs:remove', function () { + exec('rm -f '.storage_path('logs/*.log')); + exec('rm -f '.base_path('*.log')); + $this->comment('Logs have been removed!'); +})->describe('Remove log files'); diff --git a/storage/pail/.gitignore b/storage/pail/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/pail/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Feature/Pages/Admin/SchoolsShowTest.php b/tests/Feature/Pages/Admin/SchoolsShowTest.php index d3c42da..0c01581 100644 --- a/tests/Feature/Pages/Admin/SchoolsShowTest.php +++ b/tests/Feature/Pages/Admin/SchoolsShowTest.php @@ -1,7 +1,9 @@ assertOk() ->assertSee($newData['name']); }); +it('includes a form to destroy the school IF it has no students', function () { + // Arrange + $condemnedSchool = School::factory()->create(); + actingAs($this->adminUser); + // Act & Assert + get(route('admin.schools.edit', $condemnedSchool)) + ->assertOk() + ->assertSeeInOrder([ + 'form', + 'method', + 'POST', + 'action=', + route('admin.schools.destroy', $condemnedSchool), + '/form', + ], false) + ->assertSee('', false); +}); +it('does not include the destruction form if the school has students', function () { + // Arrange + $condemnedSchool = School::factory()->create(); + Student::factory()->create(['school_id' => $condemnedSchool->id]); + actingAs($this->adminUser); + // Act & Assert + get(route('admin.schools.edit', $condemnedSchool)) + ->assertOk() + ->assertDontSee('', false); +}); +it('allows an administrator to destroy a school without students', function () { + // Arrange + $condemnedSchool = School::factory()->create(); + // Act & Assert + expect($condemnedSchool->exists())->toBeTrue(); + actingAs($this->adminUser); + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.schools.destroy', $condemnedSchool)) + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.schools.index')); + expect(School::find($condemnedSchool->id))->toBeNull(); +}); +it('does not allow an administrator to destroy a student with entries', function () { + // Arrange + $condemnedSchool = School::factory()->create(); + Student::factory()->create(['school_id' => $condemnedSchool->id]); + // Act & Assert + expect($condemnedSchool->exists())->toBeTrue(); + actingAs($this->adminUser); + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.schools.destroy', $condemnedSchool)) + ->assertSessionHas('error', 'You cannot delete a school with students.') + ->assertRedirect(route('admin.schools.index')) + ->assertSessionHasNoErrors(); + expect(School::find($condemnedSchool->id))->toBeInstanceOf(School::class); +}); +it('does not allow a non administrator to delete a student', function () { + // Arrange + $condemnedSchool = School::factory()->create(); + // Act & Assert + expect($condemnedSchool->exists())->toBeTrue(); + actingAs(User::factory()->create()); + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.schools.destroy', $condemnedSchool)) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'You do not have admin access.') + ->assertRedirect(route('dashboard')); + expect(School::find($condemnedSchool->id))->toBeInstanceOf(School::class); +}); diff --git a/tests/Feature/Pages/Admin/StudentEditTest.php b/tests/Feature/Pages/Admin/StudentEditTest.php index bef15cf..e024ba5 100644 --- a/tests/Feature/Pages/Admin/StudentEditTest.php +++ b/tests/Feature/Pages/Admin/StudentEditTest.php @@ -1,12 +1,14 @@ create(['minimum_grade' => 1, 'maximum_grade' => 18]); // Needed for the grade select actingAs($this->adminUser); - $fieldList = ['first_name', 'last_name', 'grade', 'school_id']; // Act & Assert $response = get(route('admin.students.edit', $this->student)); $response->assertOk(); @@ -157,3 +158,69 @@ it('allows an administrator to edit a student', function () { ->assertSee($newData['grade']) ->assertSee($newSchool->name); }); +it('includes a form to destroy the student IF they have no entries', function () { + // Arrange + $condemnedStudent = Student::factory()->create(); + actingAs($this->adminUser); + // Act & Assert + get(route('admin.students.edit', $condemnedStudent)) + ->assertOk() + ->assertSeeInOrder([ + 'form', + 'method', + 'POST', + 'action=', + route('admin.students.destroy', $condemnedStudent), + '/form', + ], false) + ->assertSee('', false); +}); +it('does not include the destruction form if the student has entries', function () { + // Arrange + $condemnedStudent = Student::factory()->create(); + Entry::factory()->create(['student_id' => $condemnedStudent->id]); + actingAs($this->adminUser); + // Act & Assert + get(route('admin.students.edit', $condemnedStudent)) + ->assertOk() + ->assertDontSee('', false); +}); +it('allows an administrator to destroy a student without entries', function () { + // Arrange + $condemnedStudent = Student::factory()->create(); + // Act & Assert + expect($condemnedStudent->exists())->toBeTrue(); + actingAs($this->adminUser); + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.students.destroy', $condemnedStudent)) + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.students.index')); + expect(Student::find($condemnedStudent->id))->toBeNull(); +}); +it('does not allow an administrator to destroy a student with entries', function () { + // Arrange + $condemnedStudent = Student::factory()->create(); + Entry::factory()->create(['student_id' => $condemnedStudent->id]); + // Act & Assert + expect($condemnedStudent->exists())->toBeTrue(); + actingAs($this->adminUser); + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.students.destroy', $condemnedStudent)) + ->assertSessionHas('error', 'You cannot delete a student with entries.') + ->assertRedirect(route('admin.students.index')) + ->assertSessionHasNoErrors(); + expect(Student::find($condemnedStudent->id))->toBeInstanceOf(Student::class); +}); +it('does not allow a non administrator to delete a student', function () { + // Arrange + $condemnedStudent = Student::factory()->create(); + // Act & Assert + expect($condemnedStudent->exists())->toBeTrue(); + actingAs(User::factory()->create()); + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.students.destroy', $condemnedStudent)) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'You do not have admin access.') + ->assertRedirect(route('dashboard')); + expect(Student::find($condemnedStudent->id))->toBeInstanceOf(Student::class); +});