From 0de7edf6e99226bf4f5c4aaa81f1e205835b758e Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 1 Jul 2024 03:17:46 -0500 Subject: [PATCH 01/61] Work on Audition model test and Student index page --- app/Http/Middleware/CheckIfHasSchool.php | 20 ++++++ database/seeders/SampleSettingsSeeder.php | 77 +++++++++++++++++++++++ tests/Feature/Pages/DashbaordTest.php | 5 ++ tests/Feature/Pages/StudentsIndexTest.php | 5 ++ 4 files changed, 107 insertions(+) create mode 100644 app/Http/Middleware/CheckIfHasSchool.php create mode 100644 database/seeders/SampleSettingsSeeder.php create mode 100644 tests/Feature/Pages/DashbaordTest.php create mode 100644 tests/Feature/Pages/StudentsIndexTest.php diff --git a/app/Http/Middleware/CheckIfHasSchool.php b/app/Http/Middleware/CheckIfHasSchool.php new file mode 100644 index 0000000..cc8b2b1 --- /dev/null +++ b/app/Http/Middleware/CheckIfHasSchool.php @@ -0,0 +1,20 @@ + 'auditionName', + 'setting_value' => 'Somewhere Band Directors Association', + ]); + SiteSetting::create([ + 'setting_key' => 'auditionAbbreviation', + 'setting_value' => 'SBDA', + ]); + SiteSetting::create([ + 'setting_key' => 'registrationCode', + 'setting_value' => 'secret', + ]); + SiteSetting::create([ + 'setting_key' => 'advanceTo', + 'setting_value' => 'OMEA', + ]); + SiteSetting::create([ + 'setting_key' => 'judging_enabled', + 'setting_value' => '1', + ]); + SiteSetting::create([ + 'setting_key' => 'organizerName', + 'setting_value' => 'John Doe', + ]); + SiteSetting::create([ + 'setting_key' => 'organizerEmail', + 'setting_value' => 'jdoe@sbda.null', + ]); + SiteSetting::create([ + 'setting_key' => 'olympic_scoring', + 'setting_value' => '0', + ]); + SiteSetting::create([ + 'setting_key' => 'fee_structure', + 'setting_value' => 'oneFeePerEntry', + ]); + SiteSetting::create([ + 'setting_key' => 'late_fee', + 'setting_value' => '1000', + ]); + SiteSetting::create([ + 'setting_key' => 'school_fee', + 'setting_value' => 'SBDA', + ]); + SiteSetting::create([ + 'setting_key' => 'auditionAbbreviation', + 'setting_value' => '2500', + ]); + SiteSetting::create([ + 'setting_key' => 'payment_address', + 'setting_value' => '143 Sousa Lane', + ]); + SiteSetting::create([ + 'setting_key' => 'auditionAbbreviation', + 'setting_value' => 'SBDA', + ]); + SiteSetting::create([ + 'setting_key' => 'auditionAbbreviation', + 'setting_value' => 'SBDA', + ]); + } +} diff --git a/tests/Feature/Pages/DashbaordTest.php b/tests/Feature/Pages/DashbaordTest.php new file mode 100644 index 0000000..0b4b43e --- /dev/null +++ b/tests/Feature/Pages/DashbaordTest.php @@ -0,0 +1,5 @@ + Date: Mon, 1 Jul 2024 08:14:30 -0500 Subject: [PATCH 02/61] Finish Audition model test and student index. Finish AuditionFlag model test --- app/Http/Controllers/StudentController.php | 44 ++++-- app/Http/Middleware/CheckIfAdmin.php | 2 +- app/Http/Middleware/CheckIfHasSchool.php | 10 +- app/Models/Audition.php | 5 +- app/Policies/StudentPolicy.php | 17 ++- app/Rules/UniqueFullNameAtSchool.php | 6 +- database/factories/AuditionFactory.php | 2 +- database/factories/RoomFactory.php | 4 +- database/factories/ScoringGuideFactory.php | 2 +- database/seeders/SampleSettingsSeeder.php | 18 +-- .../layout/navbar/menus/admin.blade.php | 2 +- .../components/layout/navbar/navbar.blade.php | 11 +- resources/views/dashboard/dashboard.blade.php | 2 +- routes/user.php | 32 ++--- tests/Feature/Models/AuditionFlagTest.php | 21 +++ tests/Feature/Models/AuditionTest.php | 105 ++++++++++++++ tests/Feature/Pages/DashbaordTest.php | 53 ++++++- tests/Feature/Pages/StudentsIndexTest.php | 131 +++++++++++++++++- tests/Feature/PagesResponseTest.php | 15 ++ 19 files changed, 418 insertions(+), 64 deletions(-) create mode 100644 tests/Feature/Models/AuditionFlagTest.php diff --git a/app/Http/Controllers/StudentController.php b/app/Http/Controllers/StudentController.php index be1b021..f4d1186 100644 --- a/app/Http/Controllers/StudentController.php +++ b/app/Http/Controllers/StudentController.php @@ -5,10 +5,10 @@ namespace App\Http\Controllers; use App\Models\Audition; use App\Models\School; use App\Models\Student; -use App\Models\User; use App\Rules\UniqueFullNameAtSchool; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; + use function abort; use function redirect; @@ -20,9 +20,13 @@ class StudentController extends Controller */ public function index() { + if (! Auth::user()->school_id) { + return redirect()->route('dashboard'); + } $students = Auth::user()->students()->with('entries')->get(); $auditions = Audition::all(); - return view('students.index',['students' => $students, 'auditions' => $auditions]); + + return view('students.index', ['students' => $students, 'auditions' => $auditions]); } /** @@ -38,21 +42,26 @@ class StudentController extends Controller */ public function store(Request $request) { - if ($request->user()->cannot('create', Student::class)) abort(403); + 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'], + 'last_name' => [ + 'required', + new UniqueFullNameAtSchool(request('first_name'), request('last_name'), Auth::user()->school_id), + ], + 'grade' => ['required', 'integer'], ]); $student = Student::create([ 'first_name' => request('first_name'), 'last_name' => request('last_name'), 'grade' => request('grade'), - 'school_id' => Auth::user()->school_id + 'school_id' => Auth::user()->school_id, ]); - $request->session()->put('auditionMessages',['success','I did it again ma']); + $request->session()->put('auditionMessages', ['success', 'I did it again ma']); return redirect('/students'); } @@ -70,7 +79,10 @@ class StudentController extends Controller */ public function edit(Request $request, Student $student) { - if ($request->user()->cannot('update', $student)) abort(403); + if ($request->user()->cannot('update', $student)) { + abort(403); + } + return view('students.edit', ['student' => $student]); } @@ -79,22 +91,23 @@ class StudentController extends Controller */ public function update(Request $request, Student $student) { - if ($request->user()->cannot('update', $student)) abort(403); + if ($request->user()->cannot('update', $student)) { + abort(403); + } request()->validate([ 'first_name' => ['required'], - 'last_name' => ['required'], - 'grade' => ['required', 'integer'], + 'last_name' => ['required'], + 'grade' => ['required', 'integer'], ]); $student->update([ 'first_name' => request('first_name'), 'last_name' => request('last_name'), - 'grade' => request('grade') + 'grade' => request('grade'), ]); // TODO if a students grade is changed, we need to be sure they are still eligible for the auditions in which they are entered. - return redirect('/students'); } @@ -103,8 +116,11 @@ class StudentController extends Controller */ public function destroy(Request $request, Student $student) { - if ($request->user()->cannot('delete', $student)) abort(403); + if ($request->user()->cannot('delete', $student)) { + abort(403); + } $student->delete(); + return redirect('/students'); } } diff --git a/app/Http/Middleware/CheckIfAdmin.php b/app/Http/Middleware/CheckIfAdmin.php index 36baeb2..69b2a37 100644 --- a/app/Http/Middleware/CheckIfAdmin.php +++ b/app/Http/Middleware/CheckIfAdmin.php @@ -20,7 +20,7 @@ class CheckIfAdmin return $next($request); } - return redirect('/')->with('error', 'You do not have admin access.'); + return redirect(route('home'))->with('error', 'You do not have admin access.'); } } diff --git a/app/Http/Middleware/CheckIfHasSchool.php b/app/Http/Middleware/CheckIfHasSchool.php index cc8b2b1..c77dc33 100644 --- a/app/Http/Middleware/CheckIfHasSchool.php +++ b/app/Http/Middleware/CheckIfHasSchool.php @@ -4,7 +4,10 @@ namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; use Symfony\Component\HttpFoundation\Response; +use function redirect; +use function route; class CheckIfHasSchool { @@ -15,6 +18,11 @@ class CheckIfHasSchool */ public function handle(Request $request, Closure $next): Response { - return $next($request); + if (Auth::check() && Auth::user()->school_id) { + return $next($request); + } + + return redirect(route('dashboard'))->with('error', 'You do not have a school to view students for.'); + } } diff --git a/app/Models/Audition.php b/app/Models/Audition.php index 66be60c..175c053 100644 --- a/app/Models/Audition.php +++ b/app/Models/Audition.php @@ -10,8 +10,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -use function now; - class Audition extends Model { use HasFactory; @@ -26,7 +24,6 @@ class Audition extends Model protected $scored_entries_count; //Set by TabulationService - public function event(): BelongsTo { return $this->belongsTo(Event::class); @@ -175,12 +172,14 @@ class Audition extends Model } $this->flags()->create(['flag_name' => $flag]); + $this->load('flags'); } public function removeFlag($flag): void { // remove related auditionFlag where flag_name = $flag $this->flags()->where('flag_name', $flag)->delete(); + $this->load('flags'); } public function scopeOpen(Builder $query): void diff --git a/app/Policies/StudentPolicy.php b/app/Policies/StudentPolicy.php index ae8e619..5ac1cf4 100644 --- a/app/Policies/StudentPolicy.php +++ b/app/Policies/StudentPolicy.php @@ -5,7 +5,7 @@ namespace App\Policies; use App\Models\Entry; use App\Models\Student; use App\Models\User; -use Illuminate\Auth\Access\Response; + use function is_null; class StudentPolicy @@ -31,7 +31,10 @@ class StudentPolicy */ public function create(User $user): bool { - if($user->is_admin) return true; + if ($user->is_admin) { + return true; + } + return ! is_null($user->school_id); } @@ -41,7 +44,10 @@ class StudentPolicy public function update(User $user, Student $student): bool { - if($user->is_admin) return true; + if ($user->is_admin) { + return true; + } + return $user->school_id == $student->school_id; } @@ -50,7 +56,10 @@ class StudentPolicy */ public function delete(User $user, Student $student): bool { - if (Entry::where('student_id','=',$student->id)->exists()) return false; // Don't allow deletion of a student with entries + if (Entry::where('student_id', '=', $student->id)->exists()) { + return false; + } // Don't allow deletion of a student with entries + return $user->school_id == $student->school_id; } diff --git a/app/Rules/UniqueFullNameAtSchool.php b/app/Rules/UniqueFullNameAtSchool.php index f67a8aa..29e78ad 100644 --- a/app/Rules/UniqueFullNameAtSchool.php +++ b/app/Rules/UniqueFullNameAtSchool.php @@ -19,7 +19,7 @@ class UniqueFullNameAtSchool implements ValidationRule $this->school_id = $schoolID; } - public function passes($attributies, $value) + public function studentExists() { return Student::where('first_name', $this->first_name) ->where('last_name', $this->last_name) @@ -38,6 +38,8 @@ class UniqueFullNameAtSchool implements ValidationRule */ public function validate(string $attribute, mixed $value, Closure $fail): void { - // + if($this->studentExists()) { + $fail($this->message()); + } } } diff --git a/database/factories/AuditionFactory.php b/database/factories/AuditionFactory.php index 4cefeff..494a378 100644 --- a/database/factories/AuditionFactory.php +++ b/database/factories/AuditionFactory.php @@ -41,7 +41,7 @@ class AuditionFactory extends Factory return [ 'event_id' => $event->id, - 'name' => $this->faker->randomElement($instruments).$this->faker->randomNumber(1), + 'name' => $this->faker->randomElement($instruments).$this->faker->randomNumber(3), 'score_order' => 1, 'entry_deadline' => Carbon::tomorrow(), 'entry_fee' => 1000, diff --git a/database/factories/RoomFactory.php b/database/factories/RoomFactory.php index de64aad..46acabf 100644 --- a/database/factories/RoomFactory.php +++ b/database/factories/RoomFactory.php @@ -17,8 +17,8 @@ class RoomFactory extends Factory public function definition(): array { return [ - 'name' => 'Room ' . fake()->numberBetween(7,500), - 'description' => fake()->sentence() + 'name' => 'Room '.fake()->numberBetween(7, 500), + 'description' => fake()->sentence(), ]; } } diff --git a/database/factories/ScoringGuideFactory.php b/database/factories/ScoringGuideFactory.php index 11339b3..9efa3d0 100644 --- a/database/factories/ScoringGuideFactory.php +++ b/database/factories/ScoringGuideFactory.php @@ -17,7 +17,7 @@ class ScoringGuideFactory extends Factory public function definition(): array { return [ - // + 'name' => $this->faker->sentence(3), ]; } } diff --git a/database/seeders/SampleSettingsSeeder.php b/database/seeders/SampleSettingsSeeder.php index 254eced..26090a6 100644 --- a/database/seeders/SampleSettingsSeeder.php +++ b/database/seeders/SampleSettingsSeeder.php @@ -6,7 +6,7 @@ use App\Models\SiteSetting; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Seeder; -class SampleSettings extends Seeder +class SampleSettingsSeeder extends Seeder { /** * Run the database seeds. @@ -55,10 +55,6 @@ class SampleSettings extends Seeder ]); SiteSetting::create([ 'setting_key' => 'school_fee', - 'setting_value' => 'SBDA', - ]); - SiteSetting::create([ - 'setting_key' => 'auditionAbbreviation', 'setting_value' => '2500', ]); SiteSetting::create([ @@ -66,12 +62,16 @@ class SampleSettings extends Seeder 'setting_value' => '143 Sousa Lane', ]); SiteSetting::create([ - 'setting_key' => 'auditionAbbreviation', - 'setting_value' => 'SBDA', + 'setting_key' => 'payment_city', + 'setting_value' => 'Maud', ]); SiteSetting::create([ - 'setting_key' => 'auditionAbbreviation', - 'setting_value' => 'SBDA', + 'setting_key' => 'payment_state', + 'setting_value' => 'OK', + ]); + SiteSetting::create([ + 'setting_key' => 'payment_zip', + 'setting_value' => '77777', ]); } } diff --git a/resources/views/components/layout/navbar/menus/admin.blade.php b/resources/views/components/layout/navbar/menus/admin.blade.php index 21bc4b5..c0430d2 100644 --- a/resources/views/components/layout/navbar/menus/admin.blade.php +++ b/resources/views/components/layout/navbar/menus/admin.blade.php @@ -2,7 +2,7 @@ {{-- {{ $domain->domain }} + + + @endforeach + +
+ @csrf + Add Domain + +
+ + diff --git a/tests/Feature/Pages/Admin/SchoolsCreateTest.php b/tests/Feature/Pages/Admin/SchoolsCreateTest.php index 23a26aa..5420d16 100644 --- a/tests/Feature/Pages/Admin/SchoolsCreateTest.php +++ b/tests/Feature/Pages/Admin/SchoolsCreateTest.php @@ -17,11 +17,11 @@ beforeEach(function () { it('only shows for an admin user', function () { // Act & Assert $checkRoute = 'admin.schools.create'; - get(route($checkRoute, $this->school))->assertRedirect(route('home')); + get(route($checkRoute))->assertRedirect(route('home')); actingAs($this->adminUser); - get(route($checkRoute, $this->school))->assertOk(); + get(route($checkRoute))->assertOk(); actingAs($this->nonAdminUser); - get(route($checkRoute, $this->school))->assertRedirect(route('dashboard')); + get(route($checkRoute))->assertRedirect(route('dashboard')); }); it('submits a post request', function () { // Arrange diff --git a/tests/Feature/Pages/Admin/SchoolsEditTest.php b/tests/Feature/Pages/Admin/SchoolsEditTest.php new file mode 100644 index 0000000..149921c --- /dev/null +++ b/tests/Feature/Pages/Admin/SchoolsEditTest.php @@ -0,0 +1,192 @@ +adminUser = User::factory()->admin()->create(); + $this->nonAdminUser = User::factory()->create(); + $this->tabUser = User::factory()->tab()->create(); + $this->school = School::factory()->create(); +}); +it('only shows for an admin user', function () { + // Act & Assert + $checkRoute = 'admin.schools.edit'; + get(route($checkRoute, $this->school))->assertRedirect(route('home')); + actingAs($this->adminUser); + get(route($checkRoute, $this->school))->assertOk(); + actingAs($this->nonAdminUser); + get(route($checkRoute, $this->school))->assertRedirect(route('dashboard')); +}); +it('submits a patch request', function () { + // Arrange + actingAs($this->adminUser); + // Act & Assert + $response = get(route('admin.schools.edit', $this->school)); + $response->assertOk(); + $response->assertSeeInOrder([ + 'form', + 'method', + 'POST', + 'action=', + route('admin.schools.update', $this->school), + '/form', + ]); + $response->assertSee('', false); +}); +it('has all needed fields', function () { + // Arrange + actingAs($this->adminUser); + $fieldNames = [ + 'name', + 'address', + 'city', + 'state', + 'zip', + ]; + // Act & Assert + $response = get(route('admin.schools.edit', $this->school)); + $response->assertOk(); + foreach ($fieldNames as $fieldName) { + $response->assertSeeInOrder([ + 'input', + 'name=', + $fieldName, + '/', + ]); + } +}); +it('rejects a submission by a non administrator', function () { + // Arrange + actingAs($this->nonAdminUser); + // Act & Assert + $response = patch(route('admin.schools.update', $this->school), [ + 'name' => 'Hacker High', + 'address' => 'Lost Highway', + ]); + $response->assertRedirect(route('dashboard')); +}); +it('allows an administrator to update a school', function () { + // Arrange + actingAs($this->adminUser); + $newData = [ + 'name' => fake()->city(), + 'address' => fake()->streetAddress(), + 'city' => fake()->city(), + 'state' => 'OK', + 'zip' => fake()->postcode(), + ]; + // Act + $response = patch(route('admin.schools.update', $this->school), $newData); + /** @noinspection PhpUnhandledExceptionInspection */ + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.schools.show', $this->school)); + + $this->school->refresh(); + + expect($this->school->name)->toBe($newData['name']) + ->and($this->school->address)->toBe($newData['address']) + ->and($this->school->city)->toBe($newData['city']) + ->and($this->school->state)->toBe($newData['state']) + ->and($this->school->zip)->toEqual($newData['zip']); + + get(route('admin.schools.index')) + ->assertOk() + ->assertSee($newData['name']); +}); +it('has a domain add form', function () { + // Arrange + actingAs($this->adminUser); + // Act & Assert + get(route('admin.schools.edit', $this->school)) + ->assertOk() + ->assertSeeInOrder([ + 'form', + 'method', + 'POST', + 'action=', + route('admin.schools.add_domain', $this->school), + '/form', + ]); +}); +it('as a field to submit a new domain', function () { + // Arrange + actingAs($this->adminUser); + // Act & Assert + get(route('admin.schools.edit', $this->school)) + ->assertOk() + ->assertSeeInOrder([ + 'input', + 'name=', + 'domain', + '/', + ]); +}); +it('allows an administrator to add a domain and shows it on the school edit page', function () { + // Arrange + actingAs($this->adminUser); + $newDomain = 'example.com'; + // Act + $response = post(route('admin.schools.add_domain', $this->school), ['domain' => $newDomain]); + /** @noinspection PhpUnhandledExceptionInspection */ + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.schools.edit', $this->school)); + + $this->school->refresh(); + + $this->assertDatabaseHas('school_email_domains', ['school_id' => $this->school->id, 'domain' => $newDomain]); + + get(route('admin.schools.edit', $this->school)) + ->assertOk() + ->assertSee($newDomain); +}); +it('rejects a domain submission by a non administrator', function () { + // Arrange + actingAs($this->nonAdminUser); + // Act & Assert + $response = post(route('admin.schools.add_domain', $this->school), ['domain' => 'example.com']); + $response->assertRedirect(route('dashboard')); +}); +it('shows a delete button for each domain', function () { + // Arrange + actingAs($this->adminUser); + $domain = $this->school->emailDomains()->create(['domain' => 'example.com']); + // Act & Assert + get(route('admin.schools.edit', $this->school)) + ->assertOk() + ->assertSee(route('admin.schools.destroy_domain', $domain)); +}); +it('allows an admin to delete a domain', function () { + // Arrange + session()->setPreviousUrl(route('admin.schools.edit', $this->school)); + actingAs($this->adminUser); + $domain = $this->school->emailDomains()->create(['domain' => 'example.com']); + // Act + $response = delete(route('admin.schools.destroy_domain', $domain)); + /** @noinspection PhpUnhandledExceptionInspection */ + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.schools.edit', $this->school)); + + $this->assertDatabaseMissing('school_email_domains', ['id' => $domain->id]); + +}); +it('does not allow a non admin user to delete a domain', function () { + // Arrange + actingAs($this->nonAdminUser); + $domain = SchoolEmailDomainFactory::new()->create(); + // Act & Assert + $response = delete(route('admin.schools.destroy_domain', $domain)); + $response->assertRedirect(route('dashboard')); +}); -- 2.39.5 From 5a5207357ee9ec359246514fd08990bb62d1962f Mon Sep 17 00:00:00 2001 From: Matt Young Date: Tue, 2 Jul 2024 17:53:45 -0500 Subject: [PATCH 37/61] AdminSchoolsShow Page Tests --- .../Controllers/Admin/SchoolController.php | 2 +- resources/views/admin/schools/index.blade.php | 2 +- .../admin/schools/show-domain-form.blade.php | 4 +- tests/Feature/Pages/Admin/SchoolsEditTest.php | 4 +- .../Feature/Pages/Admin/SchoolsIndexTest.php | 4 +- tests/Feature/Pages/Admin/SchoolsShowTest.php | 237 ++++++++++++++++++ 6 files changed, 245 insertions(+), 8 deletions(-) create mode 100644 tests/Feature/Pages/Admin/SchoolsShowTest.php diff --git a/app/Http/Controllers/Admin/SchoolController.php b/app/Http/Controllers/Admin/SchoolController.php index c081cfb..f8ac8c9 100644 --- a/app/Http/Controllers/Admin/SchoolController.php +++ b/app/Http/Controllers/Admin/SchoolController.php @@ -118,7 +118,7 @@ class SchoolController extends Controller 'school_id' => $school->id, 'domain' => request('domain')], []); - return redirect('/admin/schools/'.$school->id.'/edit')->with('success', 'Domain Added'); + return redirect()->route('admin.schools.show', $school)->with('success', 'Domain Added'); } diff --git a/resources/views/admin/schools/index.blade.php b/resources/views/admin/schools/index.blade.php index e025c45..61f351a 100644 --- a/resources/views/admin/schools/index.blade.php +++ b/resources/views/admin/schools/index.blade.php @@ -22,7 +22,7 @@ @foreach($schools as $school) - {{ $school->name }} + {{ $school->name }} ${{ number_format($schoolTotalFees[$school->id],2) }} diff --git a/resources/views/admin/schools/show-domain-form.blade.php b/resources/views/admin/schools/show-domain-form.blade.php index f62c928..4100842 100644 --- a/resources/views/admin/schools/show-domain-form.blade.php +++ b/resources/views/admin/schools/show-domain-form.blade.php @@ -3,7 +3,7 @@ @foreach($school->emailDomains as $domain) -
+ @csrf @method('DELETE')
+ + + + + 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); +}); -- 2.39.5 From ec28cffaf3cc0adfb84b9808db5fa0007de188ea Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 3 Jul 2024 22:19:20 -0500 Subject: [PATCH 44/61] AdminEntriesIndex Page Test --- app/Http/Controllers/FilterController.php | 5 +- resources/views/admin/entries/index.blade.php | 8 +- routes/admin.php | 6 +- routes/web.php | 4 +- .../Pages/Admin/EntriesIndexFiltersTest.php | 218 ++++++++++++++++++ .../Feature/Pages/Admin/EntriesIndexTest.php | 77 +++++++ tests/Pest.php | 35 ++- 7 files changed, 339 insertions(+), 14 deletions(-) create mode 100644 tests/Feature/Pages/Admin/EntriesIndexFiltersTest.php create mode 100644 tests/Feature/Pages/Admin/EntriesIndexTest.php diff --git a/app/Http/Controllers/FilterController.php b/app/Http/Controllers/FilterController.php index 00e2012..2cc06bd 100644 --- a/app/Http/Controllers/FilterController.php +++ b/app/Http/Controllers/FilterController.php @@ -16,14 +16,13 @@ class FilterController extends Controller $filters['first_name'] = request('first_name_filter') ? request('first_name_filter') : null; $filters['last_name'] = request('last_name_filter') ? request('last_name_filter') : null; -// session(['admin_entry_filter', $filters]); session(['adminEntryFilters' => $filters]); - return redirect('/admin/entries'); + return redirect('/admin/entries')->with('success', 'Filters Applied'); } public function clearAdminEntryFilter(Request $request) { session()->forget('adminEntryFilters'); - return redirect('/admin/entries'); + return redirect('/admin/entries')->with('success', 'Filters Cleared'); } } diff --git a/resources/views/admin/entries/index.blade.php b/resources/views/admin/entries/index.blade.php index 71b8e3d..173899f 100644 --- a/resources/views/admin/entries/index.blade.php +++ b/resources/views/admin/entries/index.blade.php @@ -4,7 +4,7 @@ {{-- Filter Control--}} Set Filters - + @@ -40,7 +40,7 @@ - Clear Filters + Clear Filters Apply Filters @@ -52,7 +52,7 @@ Entries Double click row to edit - New Entry + New Entry @@ -71,7 +71,7 @@ @foreach($entries as $entry) - + {{ $entry->id }} {{ $entry->audition->name }} {{ $entry->student->full_name() }} diff --git a/routes/admin.php b/routes/admin.php index 6be959e..53563c8 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -75,10 +75,10 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> // Admin Entries Routes Route::prefix('entries')->controller(\App\Http\Controllers\Admin\EntryController::class)->group(function () { Route::get('/', 'index')->name('admin.entries.index'); - Route::get('/create', 'create'); - Route::post('/', 'store'); + Route::get('/create', 'create')->name('admin.entries.create'); + Route::post('/', 'store')->name('admin.entries.store'); Route::get('/{entry}/edit', 'edit')->name('admin.entries.edit'); - Route::patch('/{entry}', 'update'); + Route::patch('/{entry}', 'update')->name('admin.entries.update'); Route::delete('/{entry}', 'destroy')->name('admin.entries.destroy'); }); diff --git a/routes/web.php b/routes/web.php index 246ab3e..c63f5b9 100644 --- a/routes/web.php +++ b/routes/web.php @@ -29,8 +29,8 @@ Route::get('/results', [App\Http\Controllers\ResultsPage::class, '__invoke'])->n // Filter Related Routes Route::prefix('filters')->middleware(['auth', 'verified'])->controller(FilterController::class)->group(function () { - Route::post('/admin_entry_filter', 'adminEntryFilter'); - Route::get('/admin_entry_filter/clear', 'clearAdminEntryFilter'); + Route::post('/admin_entry_filter', 'adminEntryFilter')->name('admin_entry_filter.set'); + Route::get('/admin_entry_filter/clear', 'clearAdminEntryFilter')->name('admin_entry_filter.clear'); }); //Route::get('/my_school', [SchoolController::class, 'my_school'])->middleware('auth','verified'); diff --git a/tests/Feature/Pages/Admin/EntriesIndexFiltersTest.php b/tests/Feature/Pages/Admin/EntriesIndexFiltersTest.php new file mode 100644 index 0000000..0c982d0 --- /dev/null +++ b/tests/Feature/Pages/Admin/EntriesIndexFiltersTest.php @@ -0,0 +1,218 @@ +assertSee(route('admin_entry_filter.set')); +}); +it('can filter by entry ID', function () { + // Arrange + $entry = Entry::factory()->create(); + $entry2 = Entry::factory()->create(); + actAsAdmin(); + // Act & Assert + $response = post(route('admin_entry_filter.set', ['id_filter' => $entry->id])); + $response->assertSessionHas('adminEntryFilters'); + $adminEntryFilters = session('adminEntryFilters'); + assertIsArray($adminEntryFilters); + assertArrayHasKey('id', $adminEntryFilters); + assertEquals($adminEntryFilters['id'], $entry->id); + session([ + 'adminEntryFilters' => [ + 'id' => $entry->id, + 'audition' => null, + 'school' => null, + 'grade' => null, + 'first_name' => null, + 'last_name' => null, + ], + ]); + get(route('admin.entries.index')) + ->assertSee($entry->student->full_name()) + ->assertDontSee($entry2->student->full_name()); +}); +it('can filter by audition', function () { + // Arrange + $audition = Audition::factory()->create(); + $entry = Entry::factory()->create(['audition_id' => $audition->id]); + $entry2 = Entry::factory()->create(['audition_id' => $audition->id]); + $entry3 = Entry::factory()->create(); + actAsAdmin(); + // Act & Assert + $response = post(route('admin_entry_filter.set', ['audition_filter' => $entry->audition->id])); + $response->assertSessionHas('adminEntryFilters'); + $adminEntryFilters = session('adminEntryFilters'); + assertIsArray($adminEntryFilters); + assertArrayHasKey('audition', $adminEntryFilters); + assertEquals($adminEntryFilters['audition'], $audition->id); + session([ + 'adminEntryFilters' => [ + 'id' => null, + 'audition' => $entry->audition->id, + 'school' => null, + 'grade' => null, + 'first_name' => null, + 'last_name' => null, + ], + ]); + get(route('admin.entries.index')) + ->assertSee($entry->student->full_name()) + ->assertSee($entry2->student->full_name()) + ->assertDontSee($entry3->student->full_name()); +}); +it('can filter by school', function () { + // Arrange + $school = School::factory()->create(['id' => 1984]); + $student = Student::factory()->create(['id' => 375, 'school_id' => $school->id]); + $entry = Entry::factory()->create(['student_id' => $student->id]); + $entry2 = Entry::factory()->create(['student_id' => $student->id]); + $entry3 = Entry::factory()->create(); + actAsAdmin(); + // Act & Assert + $response = post(route('admin_entry_filter.set', ['school_filter' => $entry->school->id])); + $response->assertSessionHas('adminEntryFilters'); + $adminEntryFilters = session('adminEntryFilters'); + assertIsArray($adminEntryFilters); + assertArrayHasKey('school', $adminEntryFilters); + assertEquals($adminEntryFilters['school'], $school->id); + session([ + 'adminEntryFilters' => [ + 'id' => null, + 'audition' => null, + 'school' => $entry->school->id, + 'grade' => null, + 'first_name' => null, + 'last_name' => null, + ], + ]); + get(route('admin.entries.index')) + ->assertSee($entry->student->full_name()) + ->assertSee($entry2->student->full_name()) + ->assertDontSee($entry3->student->full_name()); +}); +it('can filter by grade', function () { + // Arrange + $student = Student::factory()->create(['id' => 375, 'grade' => 7]); + $student2 = Student::factory()->create(['id' => 721, 'grade' => 11]); + $entry = Entry::factory()->create(['student_id' => $student->id]); + $entry2 = Entry::factory()->create(['student_id' => $student->id]); + $entry3 = Entry::factory()->create(['student_id' => $student2->id]); + actAsAdmin(); + // Act & Assert + $response = post(route('admin_entry_filter.set', ['grade_filter' => 7])); + $response->assertSessionHas('adminEntryFilters'); + $adminEntryFilters = session('adminEntryFilters'); + assertIsArray($adminEntryFilters); + assertArrayHasKey('grade', $adminEntryFilters); + assertEquals($adminEntryFilters['grade'], $student->grade); + session([ + 'adminEntryFilters' => [ + 'id' => null, + 'audition' => null, + 'school' => null, + 'grade' => '7', + 'first_name' => null, + 'last_name' => null, + ], + ]); + get(route('admin.entries.index')) + ->assertSee($entry->student->full_name()) + ->assertSee($entry2->student->full_name()) + ->assertDontSee($entry3->student->full_name()); +}); +it('can filter by first name', function () { + // Arrange + $student = Student::factory()->create(['id' => 375, 'first_name' => fake()->firstName]); + $student2 = Student::factory()->create(['id' => 721, 'first_name' => $student->first_name]); + $student3 = Student::factory()->create(); + $entry = Entry::factory()->create(['student_id' => $student->id]); + $entry2 = Entry::factory()->create(['student_id' => $student2->id]); + $entry3 = Entry::factory()->create(['student_id' => $student3->id]); + actAsAdmin(); + // Act & Assert + $response = post(route('admin_entry_filter.set', ['first_name_filter' => $student->first_name])); + $response->assertSessionHas('adminEntryFilters'); + $adminEntryFilters = session('adminEntryFilters'); + assertIsArray($adminEntryFilters); + assertArrayHasKey('first_name', $adminEntryFilters); + assertEquals($adminEntryFilters['first_name'], $student->first_name); + session([ + 'adminEntryFilters' => [ + 'id' => null, + 'audition' => null, + 'school' => null, + 'grade' => null, + 'first_name' => $student2->first_name, + 'last_name' => null, + ], + ]); + get(route('admin.entries.index')) + ->assertSee($entry->student->full_name()) + ->assertSee($entry2->student->full_name()) + ->assertDontSee($entry3->student->full_name()); +}); +it('can filter by last name', function () { + // Arrange + $student = Student::factory()->create(['id' => 375, 'last_name' => fake()->lastName]); + $student2 = Student::factory()->create(['id' => 721, 'last_name' => $student->last_name]); + $student3 = Student::factory()->create(); + $entry = Entry::factory()->create(['student_id' => $student->id]); + $entry2 = Entry::factory()->create(['student_id' => $student2->id]); + $entry3 = Entry::factory()->create(['student_id' => $student3->id]); + actAsAdmin(); + // Act & Assert + $response = post(route('admin_entry_filter.set', ['last_name_filter' => $student->last_name])); + $response->assertSessionHas('adminEntryFilters'); + $adminEntryFilters = session('adminEntryFilters'); + assertIsArray($adminEntryFilters); + assertArrayHasKey('last_name', $adminEntryFilters); + assertEquals($adminEntryFilters['last_name'], $student->last_name); + session([ + 'adminEntryFilters' => [ + 'id' => null, + 'audition' => null, + 'school' => null, + 'grade' => null, + 'first_name' => null, + 'last_name' => $student2->last_name, + ], + ]); + get(route('admin.entries.index')) + ->assertSee($entry->student->full_name()) + ->assertSee($entry2->student->full_name()) + ->assertDontSee($entry3->student->full_name()); +}); +it('allows filters to be cleared', function () { + // Arrange + actAsAdmin(); + session([ + 'adminEntryFilters' => [ + 'id' => 815, + 'audition' => 4, + 'school' => 9, + 'grade' => 8, + 'first_name' => fake()->firstName, + 'last_name' => fake()->lastName, + ], + ]); + get(route('admin_entry_filter.clear')); + expect(session()->has('admin_entry_filter'))->toBeFalse(); + // Act & Assert + +}); diff --git a/tests/Feature/Pages/Admin/EntriesIndexTest.php b/tests/Feature/Pages/Admin/EntriesIndexTest.php new file mode 100644 index 0000000..9cc57b0 --- /dev/null +++ b/tests/Feature/Pages/Admin/EntriesIndexTest.php @@ -0,0 +1,77 @@ +assertOk(); +}); +it('normal users cannot see entries index page', function () { + // Arrange + actAsNormal(); + get(route('admin.entries.index')) + ->assertRedirect(route('dashboard')); + // Act & Assert +}); +it('does not allow guests to see the entries index page', function () { + // Arrange + get(route('admin.entries.index')) + ->assertRedirect(route('home')); +}); +it('shows entries on the index page', function () { + $entries = Entry::factory()->count(2)->create(); + actAsAdmin(); + $response = get(route('admin.entries.index')) + ->assertOk(); + foreach ($entries as $entry) { + $response + ->assertSee($entry->id) + ->assertSee($entry->audition->name) + ->assertSee($entry->student->full_name()) + ->assertSee($entry->student->grade) + ->assertSee($entry->school->name); + } +}); +it('has a link to add a new entry', function () { + actAsAdmin(); + get(route('admin.entries.index')) + ->assertSee('New Entry') + ->assertSee(route('admin.entries.create')); +}); +it('has a link to edit each entry', function () { + $entries = Entry::factory()->count(2)->create(); + actAsAdmin(); + $response = get(route('admin.entries.index')) + ->assertOk(); + foreach ($entries as $entry) { + $response + ->assertSee(route('admin.entries.edit', $entry)); + } +}); +it('has pagination after 10 entries', function () { + Entry::factory()->count(10)->create(); + actAsAdmin(); + get(route('admin.entries.index')) + ->assertOk() + ->assertDontSee('Previous') + ->assertDontSee('Next'); + Entry::factory()->count(1)->create(); + get(route('admin.entries.index')) + ->assertOk() + ->assertSee('Next'); + get(route('admin.entries.index', ['page' => 2])) + ->assertOk() + ->assertSee('Previous') + ->assertDontSee('Next'); + Entry::factory()->count(20)->create(); + get(route('admin.entries.index', ['page' => 2])) + ->assertOk() + ->assertSee('Previous') + ->assertSee('Next'); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 50ab1e4..f711b53 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -11,6 +11,11 @@ | */ +use App\Models\User; +use App\Settings; +use Illuminate\Foundation\Testing\TestCase; +use function Pest\Laravel\actingAs; + uses( Tests\TestCase::class, // Illuminate\Foundation\Testing\RefreshDatabase::class, @@ -42,7 +47,33 @@ expect()->extend('toBeOne', function () { | */ -function something() +function actAsAdmin() { - // .. + actingAs(User::factory()->admin()->create()); } +function actAsTab() +{ + actingAs(User::factory()->tab()->create()); +} +function actAsNormal() +{ + actingAs(User::factory()->create()); +} + +uses(TestCase::class)->beforeEach(function () { + Settings::set('auditionName', 'Somewhere Band Directors Association'); + Settings::set('auditionAbbreviation', 'SBDA'); + Settings::set('registrationCode', 'secret'); + Settings::set('advanceTo', 'OMEA'); + Settings::set('judging_enabled', 1); + Settings::set('organizerName', 'John Doe'); + Settings::set('organizerEmail', 'john@sbda.null'); + Settings::set('fee_structure', 'onePerEntry'); + Settings::set('late_fee', 1000); + Settings::set('school_fee', 2500); + Settings::set('payment_address', '1600 Pennsylvania Ave'); + Settings::set('payment_city', 'Washington'); + Settings::set('payment_state', 'DC'); + Settings::set('payment_zip', '20500'); +}); + -- 2.39.5 From c3739950393f6f18568901f5d2a3414aa912cc5e Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 01:56:44 -0500 Subject: [PATCH 45/61] progress on tests --- .../Controllers/Admin/EntryController.php | 38 ++++-- resources/views/admin/entries/edit.blade.php | 12 +- tests/Feature/Pages/Admin/EntriesEditTest.php | 115 ++++++++++++++++++ tests/Pest.php | 4 +- 4 files changed, 150 insertions(+), 19 deletions(-) create mode 100644 tests/Feature/Pages/Admin/EntriesEditTest.php diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index 529788c..d88d49d 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -12,6 +12,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use function compact; +use function to_route; class EntryController extends Controller { @@ -62,12 +63,14 @@ class EntryController extends Controller $entries = $entries->paginate(10); - return view('admin.entries.index', ['entries' => $entries, + return view('admin.entries.index', [ + 'entries' => $entries, 'auditions' => $auditions, 'schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade, - 'filters' => $filters]); + 'filters' => $filters, + ]); } public function create() @@ -106,9 +109,16 @@ class EntryController extends Controller public function edit(Entry $entry) { - if (! Auth::user()->is_admin) { - abort(403); + if ($entry->audition->hasFlag('seats_published')) { + return to_route('admin.entries.index')->with('error', + 'Entries in auditions with seats published 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'); + } + $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get(); $auditions = Audition::orderBy('score_order')->get(); $scores = $entry->scoreSheets()->get(); @@ -119,8 +129,14 @@ class EntryController extends Controller public function update(Request $request, Entry $entry) { - if (! Auth::user()->is_admin) { - abort(403); + if ($entry->audition->hasFlag('seats_published')) { + return to_route('admin.entries.index')->with('error', + 'Entries in auditions with seats published 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([ 'student_id' => ['required', 'exists:students,id'], @@ -142,8 +158,14 @@ class EntryController extends Controller public function destroy(Request $request, Entry $entry) { - if (! Auth::user()->is_admin) { - abort(403); + if ($entry->audition->hasFlag('seats_published')) { + return to_route('admin.entries.index')->with('error', + 'Entries in auditions with seats published 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'); diff --git a/resources/views/admin/entries/edit.blade.php b/resources/views/admin/entries/edit.blade.php index 225b26a..9e66245 100644 --- a/resources/views/admin/entries/edit.blade.php +++ b/resources/views/admin/entries/edit.blade.php @@ -5,15 +5,9 @@ Edit Entry #{{ $entry->id }} - @if(! Seat::where('entry_id', $entry->id)->exists()) -
- @csrf - @method('DELETE') - - - @else - Seated: {{ $entry->seat->ensemble->name }} #{{ $entry->seat->seat }} - @endif + + Confirm you would like to delete entry #{{$entry->id}} by {{$entry->student->full_name()}} on {{$entry->audition->name}}. +
diff --git a/tests/Feature/Pages/Admin/EntriesEditTest.php b/tests/Feature/Pages/Admin/EntriesEditTest.php new file mode 100644 index 0000000..f813945 --- /dev/null +++ b/tests/Feature/Pages/Admin/EntriesEditTest.php @@ -0,0 +1,115 @@ +entry = Entry::factory()->create(); +}); + +it('does not respond to an ordinary user', function () { + actAsNormal(); + get(route('admin.entries.edit', $this->entry)) + ->assertRedirect(route('dashboard')); +}); +it('does not respond to a guest', function () { + // Act & Assert + get(route('admin.entries.edit', $this->entry)) + ->assertRedirect(route('home')); +}); +it('does not respond if the audition is published', function () { + // Arrange + actAsAdmin(); + $this->entry->audition->addFlag('seats_published'); + get(route('admin.entries.edit', $this->entry)) + ->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('error', 'Entries in auditions with seats published cannot be modified'); +}); +it('does not respond if advancement for the audition is published', function () { + // Arrange + actAsAdmin(); + $this->entry->audition->addFlag('advancement_published'); + get(route('admin.entries.edit', $this->entry)) + ->assertRedirect(route('admin.entries.index')) + ->assertSessionHas('error', 'Entries in auditions with advancement results published cannot be modified'); +}); +it('has a delete link', function () { + // Arrange + actAsAdmin(); + // Act & Assert + get(route('admin.entries.edit', $this->entry)) + ->assertSee('', false); +}); +it('has a dropdown for all auditions', function () { + // Arrange + $auditions = Audition::factory()->count(5)->create(); + actAsAdmin(); + $response = get(route('admin.entries.edit', $this->entry)); + foreach ($auditions as $audition) { + $response->assertSeeInOrder([ + 'option', + 'value=', + $audition->id, + 'option', + ], false); + $response->assertSeeInOrder([ + 'option', + 'value=', + $this->entry->audition->id, + 'selected', + 'option', + ], false); + } +}); +it('shows checkboxes for entry types only if advancement is enabled', function () { + actAsAdmin(); + get(route('admin.entries.edit', $this->entry)) + ->assertSee('Enter for '.auditionSetting('auditionAbbreviation')) + ->assertSee('Enter for '.auditionSetting('advanceTo')); + Settings::set('advanceTo', ''); + get(route('admin.entries.edit', $this->entry)) + ->assertDontSee('Enter for '.auditionSetting('auditionAbbreviation')) + ->assertDontSee('Enter for '.auditionSetting('advanceTo')); +}); +it('properly checks boxes based on entries settings', function () { + actAsAdmin(); + get(route('admin.entries.edit', $this->entry)) + ->assertSeeInOrder([ + 'input', + 'name=', + 'for_seating', + 'checked', + auditionSetting('auditionAbbreviation'), + ]) + ->assertSeeInOrder([ + 'input', + 'name=', + 'for_advancement', + 'checked', + auditionSetting('advanceTo'), + ]); + $entry2 = Entry::factory()->advanceOnly()->create(); + get(route('admin.entries.edit', $entry2)) + ->assertSeeInOrder([ + 'input', + 'name=', + 'for_seating', + 'checked', + auditionSetting('auditionAbbreviation'), + ]) + ->assertSeeInOrder([ + 'input', + 'name=', + 'for_advancement', + 'checked', + auditionSetting('advanceTo'), + ]); + +}); +// Submission tests diff --git a/tests/Pest.php b/tests/Pest.php index f711b53..6cd9f4c 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -60,7 +60,7 @@ function actAsNormal() actingAs(User::factory()->create()); } -uses(TestCase::class)->beforeEach(function () { +uses()->beforeEach(function () { Settings::set('auditionName', 'Somewhere Band Directors Association'); Settings::set('auditionAbbreviation', 'SBDA'); Settings::set('registrationCode', 'secret'); @@ -75,5 +75,5 @@ uses(TestCase::class)->beforeEach(function () { Settings::set('payment_city', 'Washington'); Settings::set('payment_state', 'DC'); Settings::set('payment_zip', '20500'); -}); +})->in('Feature'); -- 2.39.5 From 3fc4d136752f9f3b2e0b4e95b3233280cfcd19c6 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 02:08:43 -0500 Subject: [PATCH 46/61] Implement sinnbeck/laravel-dom-assertions --- composer.json | 1 + composer.lock | 72 ++++++++++++++++++- tests/Feature/Pages/Admin/EntriesEditTest.php | 61 ++++++++-------- 3 files changed, 102 insertions(+), 32 deletions(-) diff --git a/composer.json b/composer.json index 2f4c226..853e072 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "nunomaduro/collision": "^8.0", "pestphp/pest": "^2.34", "pestphp/pest-plugin-laravel": "^2.4", + "sinnbeck/laravel-dom-assertions": "^1.5", "spatie/laravel-ignition": "^2.4" }, "autoload": { diff --git a/composer.lock b/composer.lock index b27dd43..1a7527c 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": "a21ed75b45b3f61cbc76446701fbc3ce", + "content-hash": "7aab57ef52f0152526434decd76ef1e1", "packages": [ { "name": "bacon/bacon-qr-code", @@ -9379,6 +9379,76 @@ ], "time": "2023-02-07T11:34:05+00:00" }, + { + "name": "sinnbeck/laravel-dom-assertions", + "version": "v1.5.3", + "source": { + "type": "git", + "url": "https://github.com/sinnbeck/laravel-dom-assertions.git", + "reference": "a2ce7540023fac4e6e010cbe5396b7aad9d22765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sinnbeck/laravel-dom-assertions/zipball/a2ce7540023fac4e6e010cbe5396b7aad9d22765", + "reference": "a2ce7540023fac4e6e010cbe5396b7aad9d22765", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "illuminate/testing": "^9.0|^10.0|^11.0", + "php": "^8.0", + "symfony/css-selector": "^6.0|^7.0" + }, + "require-dev": { + "laravel/pint": "^1.2", + "nunomaduro/larastan": "^2.2", + "orchestra/testbench": "^7.0|^8.0|^9.0", + "pestphp/pest": "^1.0|^2.34", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "vimeo/psalm": "^4.29|^5.22" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Sinnbeck\\DomAssertions\\DomAssertionsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Sinnbeck\\DomAssertions\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "René Sinnbeck", + "email": "rene.sinnbeck@gmail.com", + "homepage": "https://sinnbeck.dev", + "role": "Developer" + } + ], + "homepage": "https://github.com/sinnbeck/laravel-dom-assertions", + "keywords": [ + "assertions", + "blade", + "dom", + "laravel", + "view" + ], + "support": { + "issues": "https://github.com/sinnbeck/laravel-dom-assertions/issues", + "source": "https://github.com/sinnbeck/laravel-dom-assertions/tree/v1.5.3" + }, + "time": "2024-06-17T12:30:14+00:00" + }, { "name": "spatie/backtrace", "version": "1.6.1", diff --git a/tests/Feature/Pages/Admin/EntriesEditTest.php b/tests/Feature/Pages/Admin/EntriesEditTest.php index f813945..83b4f9a 100644 --- a/tests/Feature/Pages/Admin/EntriesEditTest.php +++ b/tests/Feature/Pages/Admin/EntriesEditTest.php @@ -4,6 +4,7 @@ use App\Models\Audition; use App\Models\Entry; use App\Settings; use Illuminate\Foundation\Testing\RefreshDatabase; +use Sinnbeck\DomAssertions\Asserts\AssertElement; use function Pest\Laravel\get; @@ -70,8 +71,8 @@ it('has a dropdown for all auditions', function () { it('shows checkboxes for entry types only if advancement is enabled', function () { actAsAdmin(); get(route('admin.entries.edit', $this->entry)) - ->assertSee('Enter for '.auditionSetting('auditionAbbreviation')) - ->assertSee('Enter for '.auditionSetting('advanceTo')); + ->assertElementExists('#for_seating') + ->assertElementExists('#for_advancement'); Settings::set('advanceTo', ''); get(route('admin.entries.edit', $this->entry)) ->assertDontSee('Enter for '.auditionSetting('auditionAbbreviation')) @@ -80,36 +81,34 @@ it('shows checkboxes for entry types only if advancement is enabled', function ( it('properly checks boxes based on entries settings', function () { actAsAdmin(); get(route('admin.entries.edit', $this->entry)) - ->assertSeeInOrder([ - 'input', - 'name=', - 'for_seating', - 'checked', - auditionSetting('auditionAbbreviation'), - ]) - ->assertSeeInOrder([ - 'input', - 'name=', - 'for_advancement', - 'checked', - auditionSetting('advanceTo'), - ]); - $entry2 = Entry::factory()->advanceOnly()->create(); + ->assertElementExists('#for_seating', function (AssertElement $element) { + $element->is('input') + ->has('checked'); + }) + ->assertElementExists('#for_advancement', function (AssertElement $element) { + $element->is('input') + ->has('checked'); + }); + $entry2 = Entry::factory()->seatingOnly()->create(); get(route('admin.entries.edit', $entry2)) - ->assertSeeInOrder([ - 'input', - 'name=', - 'for_seating', - 'checked', - auditionSetting('auditionAbbreviation'), - ]) - ->assertSeeInOrder([ - 'input', - 'name=', - 'for_advancement', - 'checked', - auditionSetting('advanceTo'), - ]); + ->assertElementExists('#for_seating', function (AssertElement $element) { + $element->is('input') + ->has('checked'); + }) + ->assertElementExists('#for_advancement', function (AssertElement $element) { + $element->is('input') + ->doesntHave('checked'); + }); + $entry3 = Entry::factory()->advanceOnly()->create(); + get(route('admin.entries.edit', $entry3)) + ->assertElementExists('#for_seating', function (AssertElement $element) { + $element->is('input') + ->doesntHave('checked'); + }) + ->assertElementExists('#for_advancement', function (AssertElement $element) { + $element->is('input') + ->has('checked'); + }); }); // Submission tests -- 2.39.5 From 89dd6a9a0ec3245e408aa88ddd67a059d66c48cd Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 11:10:56 -0500 Subject: [PATCH 47/61] AdminEntriesEdit Page Test --- .../Controllers/Admin/EntryController.php | 9 +- app/Http/Middleware/CheckIfAdmin.php | 2 +- database/factories/AuditionFactory.php | 2 +- .../factories/SchoolEmailDomainFactory.php | 2 +- resources/views/admin/entries/edit.blade.php | 2 +- tests/Feature/Pages/Admin/EntriesEditTest.php | 136 +++++++++++++++--- tests/Feature/Pages/Admin/SchoolsShowTest.php | 2 +- tests/Feature/Pages/Admin/StudentEditTest.php | 2 +- 8 files changed, 132 insertions(+), 25 deletions(-) diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index d88d49d..e135475 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -11,6 +11,7 @@ use App\Models\Student; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use function auditionSetting; use function compact; use function to_route; @@ -139,21 +140,23 @@ class EntryController extends Controller 'Entries in auditions with advancement results published cannot be modified'); } $validData = request()->validate([ - 'student_id' => ['required', 'exists:students,id'], 'audition_id' => ['required', 'exists:auditions,id'], ]); $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; + if (! auditionSetting('advanceTo')) { + $validData['for_seating'] = 1; + } + $entry->update([ - 'student_id' => $validData['student_id'], 'audition_id' => $validData['audition_id'], 'for_seating' => $validData['for_seating'], 'for_advancement' => $validData['for_advancement'], ]); - return redirect('/admin/entries'); + return to_route('admin.entries.index')->with('success', 'Entry updated successfully'); } public function destroy(Request $request, Entry $entry) diff --git a/app/Http/Middleware/CheckIfAdmin.php b/app/Http/Middleware/CheckIfAdmin.php index 50d63ab..71c0ceb 100644 --- a/app/Http/Middleware/CheckIfAdmin.php +++ b/app/Http/Middleware/CheckIfAdmin.php @@ -20,7 +20,7 @@ class CheckIfAdmin return $next($request); } - return redirect(route('dashboard'))->with('error', 'You do not have admin access.'); + return redirect(route('dashboard'))->with('error', 'You are not authorized to perform this action'); } } diff --git a/database/factories/AuditionFactory.php b/database/factories/AuditionFactory.php index 484728f..124fd37 100644 --- a/database/factories/AuditionFactory.php +++ b/database/factories/AuditionFactory.php @@ -42,7 +42,7 @@ class AuditionFactory extends Factory return [ 'event_id' => $event->id, #'name' => $this->faker->randomElement($instruments).$this->faker->numberBetween(1, 1000), - 'name' => 'New Instrument ' . $this->faker->unique()->sentence(5), + 'name' => 'New Instrument ' . $this->faker->unique()->words(4,true), 'score_order' => $this->faker->numberBetween(2, 50), 'entry_deadline' => Carbon::tomorrow(), 'entry_fee' => 1000, diff --git a/database/factories/SchoolEmailDomainFactory.php b/database/factories/SchoolEmailDomainFactory.php index e6f4646..5773348 100644 --- a/database/factories/SchoolEmailDomainFactory.php +++ b/database/factories/SchoolEmailDomainFactory.php @@ -20,7 +20,7 @@ class SchoolEmailDomainFactory extends Factory $school = School::factory()->create(); return [ 'school_id' => $school->id, - 'domain' => $this->faker->domainName, + 'domain' => $this->faker->unique()->domainName, ]; } } diff --git a/resources/views/admin/entries/edit.blade.php b/resources/views/admin/entries/edit.blade.php index 9e66245..9f37e22 100644 --- a/resources/views/admin/entries/edit.blade.php +++ b/resources/views/admin/entries/edit.blade.php @@ -11,7 +11,7 @@ - + @if(! Seat::where('entry_id', $entry->id)->exists()) diff --git a/tests/Feature/Pages/Admin/EntriesEditTest.php b/tests/Feature/Pages/Admin/EntriesEditTest.php index 83b4f9a..5214742 100644 --- a/tests/Feature/Pages/Admin/EntriesEditTest.php +++ b/tests/Feature/Pages/Admin/EntriesEditTest.php @@ -2,11 +2,19 @@ use App\Models\Audition; use App\Models\Entry; +use App\Models\Room; +use App\Models\ScoringGuide; +use App\Models\SubscoreDefinition; +use App\Models\User; use App\Settings; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Artisan; use Sinnbeck\DomAssertions\Asserts\AssertElement; +use Sinnbeck\DomAssertions\Asserts\AssertForm; +use Sinnbeck\DomAssertions\Asserts\AssertSelect; use function Pest\Laravel\get; +use function Pest\Laravel\patch; uses(RefreshDatabase::class); beforeEach(function () { @@ -47,26 +55,30 @@ it('has a delete link', function () { get(route('admin.entries.edit', $this->entry)) ->assertSee('', false); }); -it('has a dropdown for all auditions', function () { +it('has a dropdown for all auditions appropriate to the students grade', function () { // Arrange - $auditions = Audition::factory()->count(5)->create(); + $auditions = Audition::factory()->count(5)->create(['minimum_grade' => 1, 'maximum_grade' => 20]); + $oldAudition = Audition::factory()->create(['minimum_grade' => $this->entry->grade + 1]); + $youngAudition = Audition::factory()->create(['maximum_grade' => $this->entry->grade - 1]); actAsAdmin(); - $response = get(route('admin.entries.edit', $this->entry)); + $auditionsArray = []; foreach ($auditions as $audition) { - $response->assertSeeInOrder([ - 'option', - 'value=', - $audition->id, - 'option', - ], false); - $response->assertSeeInOrder([ - 'option', - 'value=', - $this->entry->audition->id, - 'selected', - 'option', - ], false); + $auditionsArray[] = ['value' => $audition->id, 'text' => $audition->name]; + break; } + $response = get(route('admin.entries.edit', $this->entry)); + $response->assertOk(); + // Act & Assert + foreach ($auditions as $audition) { + $response->assertOk() + ->assertFormExists('#entryEditForm', function (AssertForm $form) use ($audition) { + $form->findSelect('', function (AssertSelect $select) use ($audition) { + $select->containsOption(['value' => $audition->id, 'text' => $audition->name]); + }); + }); + } + $response->assertDontSee('value='.$oldAudition->id) + ->assertDontSee('value='.$youngAudition->id); }); it('shows checkboxes for entry types only if advancement is enabled', function () { actAsAdmin(); @@ -112,3 +124,95 @@ it('properly checks boxes based on entries settings', function () { }); // Submission tests +it('does not let a normal user update an entry', function () { + // Arrange + $newAudition = Audition::factory()->create(); + actAsNormal(); + // Act & Assert + patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'You are not authorized to perform this action') + ->assertRedirect(route('dashboard')); +}); +it('allows an admin to update an entry', function () { + // Arrange + $newAudition = Audition::factory()->create(); + actAsAdmin(); + // Act & Assert + patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Entry updated successfully') + ->assertRedirect(route('admin.entries.index')); + $this->entry->refresh(); + expect($this->entry->audition_id)->toBe($newAudition->id) + ->and($this->entry->for_seating)->toBe(0) + ->and($this->entry->for_advancement)->toBe(0); +}); +it('does not allow an administrator to update an entry in a published audition', function () { + // Arrange + $newAudition = Audition::factory()->create(); + actAsAdmin(); + $this->entry->audition->addFlag('seats_published'); + // Act & Assert + patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'Entries in auditions with seats published cannot be modified') + ->assertRedirect(route('admin.entries.index')); + $checkEntry = $this->entry->fresh(); + expect($checkEntry->id === $this->entry->id)->toBeTrue() + ->and($checkEntry->audition_id === $this->entry->audition_id)->toBeTrue() + ->and($checkEntry->student_id === $this->entry->student_id)->toBeTrue() + ->and($checkEntry->for_seating === $this->entry->for_seating)->toBeTrue() + ->and($checkEntry->for_advancement === $this->entry->for_advancement)->toBeTrue(); +}); +it('does not allow an administrator to update an entry in an audition with published advancement results', function () { + // Arrange + $newAudition = Audition::factory()->create(); + actAsAdmin(); + $this->entry->audition->addFlag('advancement_published'); + // Act & Assert + patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'Entries in auditions with advancement results published cannot be modified') + ->assertRedirect(route('admin.entries.index')); + $checkEntry = $this->entry->fresh(); + expect($checkEntry->id === $this->entry->id)->toBeTrue() + ->and($checkEntry->audition_id === $this->entry->audition_id)->toBeTrue() + ->and($checkEntry->student_id === $this->entry->student_id)->toBeTrue() + ->and($checkEntry->for_seating === $this->entry->for_seating)->toBeTrue() + ->and($checkEntry->for_advancement === $this->entry->for_advancement)->toBeTrue(); +}); +it('always sets for_seating to true if advancement is not enabled', function () { + //arrange + Settings::set('advanceTo', ''); + $newAudition = Audition::factory()->create(); + actAsAdmin(); + // Act & Assert + patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Entry updated successfully') + ->assertRedirect(route('admin.entries.index')); + $this->entry->refresh(); + expect($this->entry->audition_id)->toBe($newAudition->id) + ->and($this->entry->for_seating)->toBe(1); + +}); +it('displays scores', function () { + // Arrange + $sg = ScoringGuide::factory()->create(); + SubscoreDefinition::factory()->count(5)->create(['scoring_guide_id' => $sg->id]); + $room = Room::factory()->create(); + $judge = User::factory()->create(); + $room->addJudge($judge); + $audition = Audition::factory()->create(['room_id' => $room->id, 'scoring_guide_id' => $sg->id]); + $entry = Entry::factory()->create(['audition_id' => $audition->id]); + // Run the ScoreAllAuditions seeder + Artisan::call('db:seed', ['--class' => 'ScoreAllAuditions']); + // Act & Assert + actAsAdmin(); + $response = get(route('admin.entries.edit', $entry)) + ->assertSee($judge->full_name()); + foreach ($sg->subscores as $subscore) { + $response->assertSee($subscore->name); + } +}); diff --git a/tests/Feature/Pages/Admin/SchoolsShowTest.php b/tests/Feature/Pages/Admin/SchoolsShowTest.php index 0c01581..126b563 100644 --- a/tests/Feature/Pages/Admin/SchoolsShowTest.php +++ b/tests/Feature/Pages/Admin/SchoolsShowTest.php @@ -299,7 +299,7 @@ it('does not allow a non administrator to delete a student', function () { /** @noinspection PhpUnhandledExceptionInspection */ delete(route('admin.schools.destroy', $condemnedSchool)) ->assertSessionHasNoErrors() - ->assertSessionHas('error', 'You do not have admin access.') + ->assertSessionHas('error', 'You are not authorized to perform this action') ->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 e024ba5..4b9c5e1 100644 --- a/tests/Feature/Pages/Admin/StudentEditTest.php +++ b/tests/Feature/Pages/Admin/StudentEditTest.php @@ -220,7 +220,7 @@ it('does not allow a non administrator to delete a student', function () { /** @noinspection PhpUnhandledExceptionInspection */ delete(route('admin.students.destroy', $condemnedStudent)) ->assertSessionHasNoErrors() - ->assertSessionHas('error', 'You do not have admin access.') + ->assertSessionHas('error', 'You are not authorized to perform this action') ->assertRedirect(route('dashboard')); expect(Student::find($condemnedStudent->id))->toBeInstanceOf(Student::class); }); -- 2.39.5 From 8d9bbf31d53b3b81caea0253a922dc25fcf035ab Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 11:37:30 -0500 Subject: [PATCH 48/61] Fix IDE flags --- tests/Feature/Pages/Admin/EntriesEditTest.php | 61 +++++++++++++++++-- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/tests/Feature/Pages/Admin/EntriesEditTest.php b/tests/Feature/Pages/Admin/EntriesEditTest.php index 5214742..6bbb19e 100644 --- a/tests/Feature/Pages/Admin/EntriesEditTest.php +++ b/tests/Feature/Pages/Admin/EntriesEditTest.php @@ -13,6 +13,7 @@ use Sinnbeck\DomAssertions\Asserts\AssertElement; use Sinnbeck\DomAssertions\Asserts\AssertForm; use Sinnbeck\DomAssertions\Asserts\AssertSelect; +use function Pest\Laravel\delete; use function Pest\Laravel\get; use function Pest\Laravel\patch; @@ -55,17 +56,15 @@ it('has a delete link', function () { get(route('admin.entries.edit', $this->entry)) ->assertSee('', false); }); + it('has a dropdown for all auditions appropriate to the students grade', function () { // Arrange $auditions = Audition::factory()->count(5)->create(['minimum_grade' => 1, 'maximum_grade' => 20]); + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ $oldAudition = Audition::factory()->create(['minimum_grade' => $this->entry->grade + 1]); + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ $youngAudition = Audition::factory()->create(['maximum_grade' => $this->entry->grade - 1]); actAsAdmin(); - $auditionsArray = []; - foreach ($auditions as $audition) { - $auditionsArray[] = ['value' => $audition->id, 'text' => $audition->name]; - break; - } $response = get(route('admin.entries.edit', $this->entry)); $response->assertOk(); // Act & Assert @@ -129,6 +128,7 @@ it('does not let a normal user update an entry', function () { $newAudition = Audition::factory()->create(); actAsNormal(); // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) ->assertSessionHasNoErrors() ->assertSessionHas('error', 'You are not authorized to perform this action') @@ -139,6 +139,7 @@ it('allows an admin to update an entry', function () { $newAudition = Audition::factory()->create(); actAsAdmin(); // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) ->assertSessionHasNoErrors() ->assertSessionHas('success', 'Entry updated successfully') @@ -154,6 +155,7 @@ it('does not allow an administrator to update an entry in a published audition', actAsAdmin(); $this->entry->audition->addFlag('seats_published'); // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) ->assertSessionHasNoErrors() ->assertSessionHas('error', 'Entries in auditions with seats published cannot be modified') @@ -171,6 +173,7 @@ it('does not allow an administrator to update an entry in an audition with publi actAsAdmin(); $this->entry->audition->addFlag('advancement_published'); // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) ->assertSessionHasNoErrors() ->assertSessionHas('error', 'Entries in auditions with advancement results published cannot be modified') @@ -188,6 +191,7 @@ it('always sets for_seating to true if advancement is not enabled', function () $newAudition = Audition::factory()->create(); actAsAdmin(); // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ patch(route('admin.entries.update', $this->entry), ['audition_id' => $newAudition->id]) ->assertSessionHasNoErrors() ->assertSessionHas('success', 'Entry updated successfully') @@ -216,3 +220,50 @@ it('displays scores', function () { $response->assertSee($subscore->name); } }); + +// Delete tests +it('does not allow a normal user to delete an entry', function () { + // Arrange + actAsNormal(); + // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.entries.destroy', $this->entry), ['_method' => 'DELETE']) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'You are not authorized to perform this action') + ->assertRedirect(route('dashboard')); +}); +it('allows an admin to delete an entry', function () { + // Arrange + actAsAdmin(); + // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.entries.destroy', $this->entry), ['_method' => 'DELETE']) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Entry Deleted') + ->assertRedirect(route('admin.entries.index')); + expect(Entry::find($this->entry->id))->toBeNull(); +}); +it('does not allow an admin to delete an entry if that entries audition seats are published', function () { + // Arrange + actAsAdmin(); + // Act & Assert + $this->entry->audition->addFlag('seats_published'); + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.entries.destroy', $this->entry), ['_method' => 'DELETE']) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'Entries in auditions with seats published cannot be deleted') + ->assertRedirect(route('admin.entries.index')); + expect(Entry::find($this->entry->id))->not->toBeNull(); +}); +it('does not allow an admin to delete an entry if that entries advancement is published', function () { + // Arrange + actAsAdmin(); + // Act & Assert + $this->entry->audition->addFlag('advancement_published'); + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.entries.destroy', $this->entry), ['_method' => 'DELETE']) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'Entries in auditions with advancement results published cannot be deleted') + ->assertRedirect(route('admin.entries.index')); + expect(Entry::find($this->entry->id))->not->toBeNull(); +}); -- 2.39.5 From e3f102d6bd7af75cdfff6688f5eafc392bc1e6b2 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 14:27:07 -0500 Subject: [PATCH 49/61] admin entry pages tests done --- .../Controllers/Admin/EntryController.php | 9 ++-- database/factories/AuditionFactory.php | 3 ++ database/factories/EntryFactory.php | 1 + .../views/admin/entries/create.blade.php | 4 +- .../Feature/Pages/Admin/EntiesCreateTest.php | 49 +++++++++++++++++++ 5 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 tests/Feature/Pages/Admin/EntiesCreateTest.php diff --git a/app/Http/Controllers/Admin/EntryController.php b/app/Http/Controllers/Admin/EntryController.php index e135475..79e016a 100644 --- a/app/Http/Controllers/Admin/EntryController.php +++ b/app/Http/Controllers/Admin/EntryController.php @@ -76,11 +76,12 @@ class EntryController extends Controller public function create() { - if (! Auth::user()->is_admin) { - abort(403); - } $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get(); - $auditions = Audition::orderBy('score_order')->get(); + $auditionsRaw = Audition::with('flags')->orderBy('score_order')->get(); + + $auditions = $auditionsRaw->reject(function ($audition) { + return $audition->hasFlag('seats_published') || $audition->hasFlag('advancement_published'); + }); return view('admin.entries.create', ['students' => $students, 'auditions' => $auditions]); } diff --git a/database/factories/AuditionFactory.php b/database/factories/AuditionFactory.php index 124fd37..85bf5b4 100644 --- a/database/factories/AuditionFactory.php +++ b/database/factories/AuditionFactory.php @@ -50,6 +50,9 @@ class AuditionFactory extends Factory 'maximum_grade' => $this->faker->numberBetween(8, 12), 'for_seating' => 1, 'for_advancement' => 1, + 'room_id' => null, + 'order_in_room' => 0, + 'scoring_guide_id' => null, ]; } diff --git a/database/factories/EntryFactory.php b/database/factories/EntryFactory.php index 5ee40fa..80cec5c 100644 --- a/database/factories/EntryFactory.php +++ b/database/factories/EntryFactory.php @@ -26,6 +26,7 @@ class EntryFactory extends Factory 'draw_number' => null, 'for_seating' => 1, 'for_advancement' => 1, + ]; } diff --git a/resources/views/admin/entries/create.blade.php b/resources/views/admin/entries/create.blade.php index 998fa00..d1c4376 100644 --- a/resources/views/admin/entries/create.blade.php +++ b/resources/views/admin/entries/create.blade.php @@ -1,7 +1,7 @@ Create Entry - + @@ -35,7 +35,7 @@ @endif - + Create Entry diff --git a/tests/Feature/Pages/Admin/EntiesCreateTest.php b/tests/Feature/Pages/Admin/EntiesCreateTest.php new file mode 100644 index 0000000..dc71571 --- /dev/null +++ b/tests/Feature/Pages/Admin/EntiesCreateTest.php @@ -0,0 +1,49 @@ +assertRedirect(route('dashboard')); +}); +it('does not respond to a guest', function () { + // Act & Assert + get(route('admin.entries.create')) + ->assertRedirect(route('home')); +}); +it('passes a collection of all students with thier schools to the view', function () { + // Arrange + Student::factory()->count(8)->create(); + $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get(); + actAsAdmin(); + // Act & Assert + get(route('admin.entries.create')) + ->assertViewHas('students', $students); +}); +it('passes a collection of available auditions to the view', function () { + // Arrange + for ($i = 3; $i < 9; $i++) { + Audition::factory()->create(['score_order' => $i]); + } + Audition::factory()->count(5)->create(); + $auditions = Audition::with('flags')->orderBy('score_order')->get(); + $auditions = $auditions->toArray(); + $seatedAudition = Audition::factory()->create(['score_order' => 1]); + $seatedAudition->addFlag('seats_published'); + $advancedAudition = Audition::factory()->create(['score_order' => 2]); + $advancedAudition->addFlag('advancement_published'); + actAsAdmin(); + // Act & Assert + $response = get(route('admin.entries.create')); + $viewAuditions = $response->viewData('auditions')->toArray(); + $response->assertOk(); + assertEquals(array_values($auditions), array_values($viewAuditions)); +}); -- 2.39.5 From 735d79210c603d3190011e6edb9624a63415d3ff Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 15:48:32 -0500 Subject: [PATCH 50/61] Test Audition Settings Page --- .../Controllers/Admin/AuditionSettings.php | 3 +- .../views/admin/audition-settings.blade.php | 11 +- tests/Feature/Pages/Setup/SettingsTest.php | 139 ++++++++++++++++++ 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/Pages/Setup/SettingsTest.php diff --git a/app/Http/Controllers/Admin/AuditionSettings.php b/app/Http/Controllers/Admin/AuditionSettings.php index 6083f20..f879ea8 100644 --- a/app/Http/Controllers/Admin/AuditionSettings.php +++ b/app/Http/Controllers/Admin/AuditionSettings.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Settings; use Illuminate\Http\Request; +use function to_route; class AuditionSettings extends Controller { @@ -35,6 +36,6 @@ class AuditionSettings extends Controller Settings::set($key, $value); } - return view('admin.audition-settings')->with('success', 'Settings Saved'); + return to_route('audition-settings')->with('success', 'Settings Saved'); } } diff --git a/resources/views/admin/audition-settings.blade.php b/resources/views/admin/audition-settings.blade.php index c32ad8a..17cbcf9 100644 --- a/resources/views/admin/audition-settings.blade.php +++ b/resources/views/admin/audition-settings.blade.php @@ -1,7 +1,7 @@ Audition Settings - + Group Information @@ -60,6 +60,15 @@ + + Payment Address + + + + + + +
diff --git a/tests/Feature/Pages/Setup/SettingsTest.php b/tests/Feature/Pages/Setup/SettingsTest.php new file mode 100644 index 0000000..e53e89c --- /dev/null +++ b/tests/Feature/Pages/Setup/SettingsTest.php @@ -0,0 +1,139 @@ +assertOk() + ->assertViewIs('admin.audition-settings'); +}); +it('does not allow normal users to access the settings page', function () { + // Arrange + actAsNormal(); + // Act & Assert + get(route('audition-settings')) + ->assertSessionHas('error', 'You are not authorized to perform this action') + ->assertRedirect(route('dashboard')); +}); +it('does not allow guests to access the settings page', function () { + // Act & Assert + get(route('audition-settings')) + ->assertRedirect(route('home')); +}); +it('has a field with forms for each audition setting', function () { + // Arrange + actAsAdmin(); + // Act + $response = get(route('audition-settings')); + // Assert + $response->assertOk(); + $response->assertFormExists('#settingsForm', function (AssertForm $form) { + $form->hasMethod('POST') + ->hasAction(route('audition-settings-save')) + ->hasCSRF() + ->containsInput([ + 'name' => 'auditionName', + 'value' => auditionSetting('auditionName'), + ]) + ->containsInput([ + 'name' => 'auditionAbbreviation', + 'value' => auditionSetting('auditionAbbreviation'), + ]) + ->containsInput([ + 'name' => 'registrationCode', + 'value' => auditionSetting('registrationCode'), + ]) + ->containsInput([ + 'name' => 'advanceTo', + 'value' => auditionSetting('advanceTo'), + ]) + ->containsInput([ + 'name' => 'organizerName', + 'value' => auditionSetting('organizerName'), + ]) + ->containsInput([ + 'name' => 'organizerEmail', + 'value' => auditionSetting('organizerEmail'), + ]) + ->containsInput([ + 'name' => 'late_fee', + 'value' => number_format(auditionSetting('late_fee')/100, 2), + ]) + ->containsInput([ + 'name' => 'school_fee', + 'value' => number_format(auditionSetting('school_fee')/100, 2), + ]) + ->containsInput([ + 'name' => 'payment_address', + 'value' => auditionSetting('payment_address'), + ]) + ->containsInput([ + 'name' => 'payment_city', + 'value' => auditionSetting('payment_city'), + ]) + ->containsInput([ + 'name' => 'payment_state', + 'value' => auditionSetting('payment_state'), + ]) + ->containsInput([ + 'name' => 'payment_zip', + 'value' => auditionSetting('payment_zip'), + 'type' => 'number', + ]) + ->containsInput([ + 'name' => 'olympic_scoring', + 'type' => 'checkbox', + // TODO how can I test if it is checked when necessary + ]) + ->containsInput([ + 'name' => 'judging_enabled', + 'type' => 'checkbox', + // TODO how can I test if it is checked when necessary + ]) + ->findSelect('#fee_structure', function(AssertSelect $select) { + $select->containsOption([ + 'value' => 'oneFeePerEntry', + 'text'=> 'One fee per entry', + ]) + ->containsOption([ + 'value' => 'oneFeePerStudent', + 'text' => 'One fee per student - one late fee per student if any of their entries are late', + ]); + }); + }); +}); +it('can update audition settings', function () { + // Arrange + actAsAdmin(); + $newData = [ + 'auditionName' => 'New Audition Name', + 'auditionAbbreviation' => 'NAN', + 'registrationCode' => 'NEWCODE', + 'advanceTo' => 'next', + 'organizerName' => 'New Organizer Name', + 'organizerEmail' => 'newemail@new.com', + 'late_fee' => 150.00, + 'school_fee' => 200.00, + 'payment_address' => '123 New St', + 'payment_city' => 'New City', + 'payment_state' => 'NS', + 'payment_zip' => 12345, + 'fee_structure' => 'oneFeePerEntry' + ]; + // Act + $response = post(route('audition-settings-save'), $newData); + // Act & Assert + $response->assertRedirect(route('audition-settings')) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Settings Saved'); +}); -- 2.39.5 From 8bf3219912266e77c50f1ea52ace4a32a427eb4d Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 16:27:41 -0500 Subject: [PATCH 51/61] Events Page Test --- .../Controllers/Admin/EventController.php | 5 +- tests/Feature/Pages/Setup/EventsTest.php | 153 ++++++++++++++++++ 2 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/Pages/Setup/EventsTest.php diff --git a/app/Http/Controllers/Admin/EventController.php b/app/Http/Controllers/Admin/EventController.php index 42a28f2..972bfe7 100644 --- a/app/Http/Controllers/Admin/EventController.php +++ b/app/Http/Controllers/Admin/EventController.php @@ -37,8 +37,9 @@ class EventController extends Controller public function destroy(Request $request, Event $event) { - if (! Auth::user()->is_admin) { - abort(403); + if ($event->auditions()->count() > 0) { + return redirect()->route('admin.events.index')->with('error', + 'Cannot delete an event with auditions'); } $event->delete(); diff --git a/tests/Feature/Pages/Setup/EventsTest.php b/tests/Feature/Pages/Setup/EventsTest.php new file mode 100644 index 0000000..09f452a --- /dev/null +++ b/tests/Feature/Pages/Setup/EventsTest.php @@ -0,0 +1,153 @@ +assertOk(); +}); +it('does not allow normal users to manage events', function () { + // Arrange + actAsNormal(); + // Act & Assert + get(route('admin.events.index')) + ->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); +}); +it('does not allow a guest to manage events', function () { + // Act & Assert + get(route('admin.events.index')) + ->assertRedirect(route('home')); +}); +it('shows a line for each event', function () { + // Arrange + $events = Event::factory()->count(3)->create(); + // Act & Assert + actAsAdmin(); + get(route('admin.events.index')) + ->assertOk() + ->assertSee($events[0]->name) + ->assertSee($events[1]->name) + ->assertSee($events[2]->name); +}); +it('shows a count of auditions in each event', function () { + // Arrange + $noAuditionEvent = Event::factory()->create(); + $fiveAuditionEvent = Event::factory()->hasAuditions(5)->create(); + actAsAdmin(); + // Act & Assert + get(route('admin.events.index')) + ->assertOk() + ->assertSee($noAuditionEvent->name.', 0 Audition') + ->assertSee($fiveAuditionEvent->name.', 5 Audition'); +}); +it('has a delete link only for events with no auditions', function () { + $noAuditionEvent = Event::factory()->create(); + $fiveAuditionEvent = Event::factory()->hasAuditions(5)->create(); + actAsAdmin(); + get(route('admin.events.index')) + ->assertOk() + ->assertSee(route('admin.events.destroy', $noAuditionEvent)) + ->assertDontSee(route('admin.events.destroy', $fiveAuditionEvent)); +}); +it('can delete an event', function () { + // Arrange + $noAuditionEvent = Event::factory()->create(); + $fiveAuditionEvent = Event::factory()->hasAuditions(5)->create(); + actAsAdmin(); + // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.events.destroy', $noAuditionEvent)) + ->assertRedirect(route('admin.events.index')) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Event deleted successfully'); + get(route('admin.events.index')) + ->assertOk() + ->assertDontSee($noAuditionEvent->name) + ->assertSee($fiveAuditionEvent->name); +}); +it('does not allow a normal user to delete an event', function () { + // Arrange + $noAuditionEvent = Event::factory()->create(); + actAsNormal(); + // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.events.destroy', $noAuditionEvent)) + ->assertRedirect(route('dashboard')) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'You are not authorized to perform this action'); + actAsAdmin(); + get(route('admin.events.index')) + ->assertOk() + ->assertSee($noAuditionEvent->name); +}); +it('does not allow a guest to delete an event', function () { + // Arrange + $noAuditionEvent = Event::factory()->create(); + // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.events.destroy', $noAuditionEvent)) + ->assertRedirect(route('home')) + ->assertSessionHasNoErrors(); + actAsAdmin(); + get(route('admin.events.index')) + ->assertOk() + ->assertSee($noAuditionEvent->name); +}); +it('cannot delete an event with auditions', function () { + // Arrange + $event = Event::factory()->hasAuditions(5)->create(); + actAsAdmin(); + // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ + delete(route('admin.events.destroy', $event)) + ->assertRedirect(route('admin.events.index')) + ->assertSessionHas('error', 'Cannot delete an event with auditions'); + get(route('admin.events.index')) + ->assertOk() + ->assertSee($event->name); +}); +it('has a form to add an event', function () { + // Arrange + actAsAdmin(); + // Act & Assert + get(route('admin.events.index')) + ->assertOk() + ->assertSee('Add New Event') + ->assertSee(route('admin.events.store')); +}); +it('allows an admin to add an event', function () { + $newEvent = Event::factory()->make(); + actAsAdmin(); + /** @noinspection PhpUnhandledExceptionInspection */ + post(route('admin.events.store'), ['name' => $newEvent->name]) + ->assertRedirect(route('admin.events.index')) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Event created successfully'); + get(route('admin.events.index')) + ->assertOk() + ->assertSee($newEvent->name); +}); +it('does not allow a guest or normal user to add an event', function () { + // Arrange + $newEvent = Event::factory()->make(); + // Act & Assert + /** @noinspection PhpUnhandledExceptionInspection */ + post(route('admin.events.store'), ['name' => $newEvent->name]) + ->assertRedirect(route('home')) + ->assertSessionHasNoErrors(); + actAsNormal(); + /** @noinspection PhpUnhandledExceptionInspection */ + post(route('admin.events.store'), ['name' => $newEvent->name]) + ->assertRedirect(route('dashboard')) + ->assertSessionHasNoErrors() + ->assertSessionHas('error', 'You are not authorized to perform this action'); +}); -- 2.39.5 From af9bc432a3df16ccbe8290d4cf5ef3204542f5dc Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 22:04:25 -0500 Subject: [PATCH 52/61] AuditionCreate Tests --- .../Controllers/Admin/AuditionController.php | 10 +-- database/factories/AuditionFactory.php | 13 +++ .../views/admin/auditions/index.blade.php | 8 +- .../Pages/Setup/AuditionsCreateTest.php | 90 +++++++++++++++++++ .../Pages/Setup/AuditionsIndexTest.php | 49 ++++++++++ 5 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 tests/Feature/Pages/Setup/AuditionsCreateTest.php create mode 100644 tests/Feature/Pages/Setup/AuditionsIndexTest.php diff --git a/app/Http/Controllers/Admin/AuditionController.php b/app/Http/Controllers/Admin/AuditionController.php index 86d9e12..0dd0625 100644 --- a/app/Http/Controllers/Admin/AuditionController.php +++ b/app/Http/Controllers/Admin/AuditionController.php @@ -14,16 +14,14 @@ use function compact; use function redirect; use function request; use function response; +use function to_route; use function view; class AuditionController extends Controller { public function index() { - if (! Auth::user()->is_admin) { - abort(403); - } - $auditions = Audition::with(['event', 'entries'])->orderBy('score_order')->orderBy('created_at', 'desc')->get(); + $auditions = Audition::with(['event'])->withCount('entries')->orderBy('score_order')->orderBy('created_at', 'desc')->get(); return view('admin.auditions.index', ['auditions' => $auditions]); } @@ -68,7 +66,7 @@ class AuditionController extends Controller 'for_advancement' => $validData['for_advancement'], ]); - return redirect('/admin/auditions'); + return to_route('admin.auditions.index')->with('success', 'Audition created successfully'); } public function edit(Audition $audition) @@ -112,7 +110,7 @@ class AuditionController extends Controller 'for_advancement' => $validData['for_advancement'], ]); - return redirect('/admin/auditions'); + return to_route('admin.auditions.index')->with('success', 'Audition updated successfully'); } public function reorder(Request $request) diff --git a/database/factories/AuditionFactory.php b/database/factories/AuditionFactory.php index 85bf5b4..a7c909e 100644 --- a/database/factories/AuditionFactory.php +++ b/database/factories/AuditionFactory.php @@ -62,4 +62,17 @@ class AuditionFactory extends Factory fn (array $attributes) => ['entry_deadline' => $entryDeadline ?? Carbon::yesterday()] ); } + public function seatingOnly(): self + { + return $this->state( + fn (array $attributes) => ['for_advancement' => 0] + ); + } + + public function advancementOnly(): self + { + return $this->state( + fn (array $attributes) => ['for_seating' => 0] + ); + } } diff --git a/resources/views/admin/auditions/index.blade.php b/resources/views/admin/auditions/index.blade.php index 527d197..d2f8a18 100644 --- a/resources/views/admin/auditions/index.blade.php +++ b/resources/views/admin/auditions/index.blade.php @@ -1,11 +1,11 @@ Audition Administration - + Auditions Drag to reorder. Double click to edit. - New Audition + New Audition @@ -27,7 +27,7 @@
@foreach($auditions as $audition) - {{ $audition->event->name }} {{ $audition->name }} @@ -50,7 +50,7 @@ @endif @endif - {{ $audition->entries->count() }} + {{ $audition->entries_count }} @endforeach diff --git a/tests/Feature/Pages/Setup/AuditionsCreateTest.php b/tests/Feature/Pages/Setup/AuditionsCreateTest.php new file mode 100644 index 0000000..b913fc9 --- /dev/null +++ b/tests/Feature/Pages/Setup/AuditionsCreateTest.php @@ -0,0 +1,90 @@ +assertRedirect(route('home')); + actAsNormal(); + get((route('admin.auditions.create'))) + ->assertRedirect('/dashboard') + ->assertSessionHas('error', 'You are not authorized to perform this action'); + actasAdmin(); + get((route('admin.auditions.create'))) + ->assertOk(); +}); +it('shows necessary fields', function () { + // Arrange + actAsAdmin(); + // Act & Assert + get(route('admin.auditions.create')) + ->assertOk() + ->assertSee(route('admin.auditions.store')) + ->assertSee('name="event_id"', false) + ->assertSee('name="name"', false) + ->assertSee('name="entry_deadline"', false) + ->assertSee('name="entry_fee"', false) + ->assertSee('name="minimum_grade"', false) + ->assertSee('name="maximum_grade"', false) + ->assertSee('name="for_seating"', false) + ->assertSee('name="for_advancement"', false); +}); +it('allows an administrator to create auditions', function () { + // Arrange + $newEvent = Event::factory()->create(); + $changes = [ + 'event_id' => $newEvent->id, + 'name' => 'New Name', + 'entry_deadline' => '1978-01-01', + 'entry_fee' => 10000, + 'minimum_grade' => 3, + 'maximum_grade' => 8, + 'for_advancement' => 'on', + ]; + actAsAdmin(); + // Act + $response = post(route('admin.auditions.store'), $changes); + // Assert + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.auditions.index')) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Audition created successfully'); + $checkAudition = Audition::latest()->first(); + expect($checkAudition->event_id)->toBe($newEvent->id) + ->and($checkAudition->name)->toBe($changes['name']) + ->and($checkAudition->entry_deadline)->toBe($changes['entry_deadline']) + ->and($checkAudition->entry_fee)->toBe($changes['entry_fee'] * 100) + ->and($checkAudition->minimum_grade)->toBe($changes['minimum_grade']) + ->and($checkAudition->maximum_grade)->toBe($changes['maximum_grade']) + ->and($checkAudition->for_seating)->toBe(0) + ->and($checkAudition->for_advancement)->toBe(1); +}); +it('does not allow a normal user or guest to create an audition', function () { + // Arrange + $precount = Audition::count(); + $newEvent = Event::factory()->create(); + $changes = [ + 'event_id' => $newEvent->id, + 'name' => 'New Name', + 'entry_deadline' => '1978-01-01', + 'entry_fee' => 10000, + 'minimum_grade' => 3, + 'maximum_grade' => 8, + 'for_advancement' => 'on', + ]; + // Act & Assert + post(route('admin.auditions.store'), $changes) + ->assertRedirect(route('home')); + actAsNormal(); + post(route('admin.auditions.store'), $changes) + ->assertRedirect('/dashboard') + ->assertSessionHas('error', 'You are not authorized to perform this action'); + expect(Audition::count())->toBe($precount); +}); diff --git a/tests/Feature/Pages/Setup/AuditionsIndexTest.php b/tests/Feature/Pages/Setup/AuditionsIndexTest.php new file mode 100644 index 0000000..ae18032 --- /dev/null +++ b/tests/Feature/Pages/Setup/AuditionsIndexTest.php @@ -0,0 +1,49 @@ +assertRedirect(route('home')); + actAsNormal(); + get((route('admin.auditions.index'))) + ->assertRedirect('/dashboard') + ->assertSessionHas('error', 'You are not authorized to perform this action'); + actasAdmin(); + get((route('admin.auditions.index'))) + ->assertOk(); +}); +it('has a link to add a new audition', function () { + // Arrange + actAsAdmin(); + // Act & Assert + get((route('admin.auditions.index'))) + ->assertOk() + ->assertSee('New Audition') + ->assertSee(route('admin.auditions.create')); +}); +it('shows audition data', function () { + // Arrange + $auditions = Audition::factory()->count(10)->create(); + actAsAdmin(); + // Act & Assert + $response = get((route('admin.auditions.index'))); + $response->assertOk(); + foreach ($auditions as $audition) { + $response->assertElementExists('#auditionRow-'.$audition->id, + function (AssertElement $element) use ($audition) { + $element->containsText($audition->event->name) + ->containsText($audition->name) + ->containsText($audition->entry_deadline) + ->containsText($audition->display_fee()) + ->containsText($audition->minimum_grade.' - '.$audition->maximum_grade) + ->containsText($audition->entries->count()); + }); + } +}); -- 2.39.5 From 916baa2c93579f7f64426cb72afb3354e224fc6e Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 4 Jul 2024 22:42:44 -0500 Subject: [PATCH 53/61] AuditionsEdit Page Tests --- .../Controllers/Admin/AuditionController.php | 6 +- app/Services/AuditionService.php | 3 - .../views/admin/auditions/edit.blade.php | 14 ++- .../delete-resource-modal.blade.php | 8 ++ .../Feature/Pages/Setup/AuditionsEditTest.php | 118 ++++++++++++++++++ 5 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 tests/Feature/Pages/Setup/AuditionsEditTest.php diff --git a/app/Http/Controllers/Admin/AuditionController.php b/app/Http/Controllers/Admin/AuditionController.php index 0dd0625..2f43b95 100644 --- a/app/Http/Controllers/Admin/AuditionController.php +++ b/app/Http/Controllers/Admin/AuditionController.php @@ -160,16 +160,12 @@ class AuditionController extends Controller public function destroy(Audition $audition) { - if (! Auth::user()->is_admin) { - abort(403); - } - // if($audition->entries->count() > 0) abort(403, 'Cannot delete an audition with entries.' if ($audition->entries->count() > 0) { return redirect()->route('admin.auditions.index')->with('error', 'Cannot delete an audition with entries.'); } $audition->delete(); - return redirect('/admin/auditions'); + return to_route('admin.auditions.index')->with('success', 'Audition deleted successfully'); } public function prepareDraw() diff --git a/app/Services/AuditionService.php b/app/Services/AuditionService.php index c45e064..6e2d9a9 100644 --- a/app/Services/AuditionService.php +++ b/app/Services/AuditionService.php @@ -69,9 +69,6 @@ class AuditionService public function clearCache(): void { - if (App::environment('local')) { - Session::flash('success', 'Audition Cache Cleared'); - } Cache::forget($this->cacheKey); } diff --git a/resources/views/admin/auditions/edit.blade.php b/resources/views/admin/auditions/edit.blade.php index 0f362ea..dc2be46 100644 --- a/resources/views/admin/auditions/edit.blade.php +++ b/resources/views/admin/auditions/edit.blade.php @@ -3,6 +3,13 @@ Edit Audition + + @if($audition->entries->count() == 0) + + Please confirm that you would like to delete the audition {{ $audition->name }}. This action cannot be undone. + + @endif + {{-- TODO implement a way to update multiple auditions as once --}} @@ -39,18 +46,13 @@ -
- @if($audition->entries->count() == 0) - - @endif -
+
Edit Audition
-
diff --git a/resources/views/components/delete-resource-modal.blade.php b/resources/views/components/delete-resource-modal.blade.php index b147988..6c444ea 100644 --- a/resources/views/components/delete-resource-modal.blade.php +++ b/resources/views/components/delete-resource-modal.blade.php @@ -1,3 +1,11 @@ +@php +/** + * @var int $size=20 Size of the icon + * @var string $title Title of the modal + * @var string $method='DELETE' method used by the form + * @var string $action action used for the form + */ +@endphp @props(['size' => 20,'title','method'=>'DELETE','action'])
audition = Audition::factory()->seatingOnly()->create(); + $this->newEvent = Event::factory()->create(); + $this->changes = [ + 'event_id' => $this->newEvent->id, + 'name' => 'New Name', + 'entry_deadline' => '1978-01-01', + 'entry_fee' => 10000, + 'minimum_grade' => 3, + 'maximum_grade' => 8, + 'for_advancement' => 'on', + ]; +}); + +it('allows only an admin to manage auditions', function () { + get(route('admin.auditions.edit', $this->audition)) + ->assertRedirect(route('home')); + actAsNormal(); + get(route('admin.auditions.edit', $this->audition)) + ->assertRedirect('/dashboard') + ->assertSessionHas('error', 'You are not authorized to perform this action'); + actasAdmin(); + get(route('admin.auditions.edit', $this->audition)) + ->assertOk(); +}); +it('shows necessary fields', function () { + // Arrange + actAsAdmin(); + // Act & Assert + get(route('admin.auditions.edit', $this->audition)) + ->assertOk() + ->assertSee(route('admin.auditions.store')) + ->assertSee('name="event_id"', false) + ->assertSee('name="name"', false) + ->assertSee('name="entry_deadline"', false) + ->assertSee('name="entry_fee"', false) + ->assertSee('name="minimum_grade"', false) + ->assertSee('name="maximum_grade"', false) + ->assertSee('name="for_seating"', false) + ->assertSee('name="for_advancement"', false); +}); +it('allows an administrator to modify auditions', function () { + actAsAdmin(); + // Act + $response = patch(route('admin.auditions.update', $this->audition), $this->changes); + // Assert + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.auditions.index')) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Audition updated successfully'); + $checkAudition = Audition::find($this->audition->id); + expect($checkAudition->event_id)->toBe($this->newEvent->id) + ->and($checkAudition->name)->toBe($this->changes['name']) + ->and($checkAudition->entry_deadline)->toBe($this->changes['entry_deadline']) + ->and($checkAudition->entry_fee)->toBe($this->changes['entry_fee'] * 100) + ->and($checkAudition->minimum_grade)->toBe($this->changes['minimum_grade']) + ->and($checkAudition->maximum_grade)->toBe($this->changes['maximum_grade']) + ->and($checkAudition->for_seating)->toBe(0) + ->and($checkAudition->for_advancement)->toBe(1); +}); +it('does not allow a normal user or guest to create an audition', function () { + $preCheck = Audition::find($this->audition->id); + // Act & Assert + patch(route('admin.auditions.update', $this->audition), $this->changes) + ->assertRedirect(route('home')); + actAsNormal(); + patch(route('admin.auditions.update', $this->audition), $this->changes) + ->assertRedirect('/dashboard') + ->assertSessionHas('error', 'You are not authorized to perform this action'); + $checkAudition = Audition::find($this->audition->id); + expect($checkAudition)->toEqual($preCheck); +}); +it('has a delete function for an audition that has no entries', function () { + // Arrange + actAsAdmin(); + // Act & Assert + get(route('admin.auditions.edit', $this->audition)) + ->assertOk() + ->assertSee(route('admin.auditions.destroy', $this->audition)); +}); +it('does not allow guests or normal users to delete an audition', function () { + delete(route('admin.auditions.destroy', $this->audition)) + ->assertRedirect(route('home')); + actAsNormal(); + delete(route('admin.auditions.destroy', $this->audition)) + ->assertRedirect('/dashboard') + ->assertSessionHas('error', 'You are not authorized to perform this action'); +}); +it('does not allow the deletion of an audition with entries', function () { + // Arrange + actAsAdmin(); + Entry::factory()->create(['audition_id' => $this->audition->id]); + // Act & Assert + delete(route('admin.auditions.destroy', $this->audition)) + ->assertRedirect(route('admin.auditions.index')) + ->assertSessionHas('error', 'Cannot delete an audition with entries.'); +}); +it('allows an administrator to delete an audition that has no entries', function () { + // Arrange + actAsAdmin(); + // Act & Assert + delete(route('admin.auditions.destroy', $this->audition)) + ->assertRedirect(route('admin.auditions.index')) + ->assertSessionHas('success', 'Audition deleted successfully'); + expect(Audition::find($this->audition->id))->toBeNull(); +}); -- 2.39.5 From 3a93aa0ea28f2685a5107dbbdd58616e779fabb5 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 5 Jul 2024 00:12:14 -0500 Subject: [PATCH 54/61] AdminEnsembles page tests --- .../Controllers/Admin/EnsembleController.php | 59 +++++--- .../ensembles/index-event-table.blade.php | 2 +- .../ensembles/index-rename-modal.blade.php | 2 +- routes/admin.php | 2 +- .../Pages/Setup/EnsemblesIndexTest.php | 134 ++++++++++++++++++ 5 files changed, 173 insertions(+), 26 deletions(-) create mode 100644 tests/Feature/Pages/Setup/EnsemblesIndexTest.php diff --git a/app/Http/Controllers/Admin/EnsembleController.php b/app/Http/Controllers/Admin/EnsembleController.php index 7574041..da056db 100644 --- a/app/Http/Controllers/Admin/EnsembleController.php +++ b/app/Http/Controllers/Admin/EnsembleController.php @@ -8,7 +8,7 @@ use App\Models\Event; use App\Models\SeatingLimit; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\DB; + use function redirect; class EnsembleController extends Controller @@ -16,16 +16,19 @@ class EnsembleController extends Controller public function index() { $events = Event::with('ensembles')->get(); - return view('admin.ensembles.index',compact('events')); + + return view('admin.ensembles.index', compact('events')); } public function store(Request $request) { - if(! Auth::user()->is_admin) abort(403); + if (! Auth::user()->is_admin) { + abort(403); + } request()->validate([ 'name' => 'required', - 'code' => ['required','max:6'], - 'event_id' => ['required','exists:events,id'] + 'code' => ['required', 'max:6'], + 'event_id' => ['required', 'exists:events,id'], ]); Ensemble::create([ @@ -34,29 +37,32 @@ class EnsembleController extends Controller 'event_id' => request('event_id'), ]); - return redirect()->route('admin.ensembles.index')->with('success','Ensemble created successfully'); + return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble created successfully'); } public function destroy(Request $request, Ensemble $ensemble) { - if(! Auth::user()->is_admin) abort(403); + if ($ensemble->seats->count() > 0) { + return redirect()->route('admin.ensembles.index')->with('error', 'Ensemble has students seated and cannot be deleted'); + } $ensemble->delete(); + return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble deleted successfully'); } public function updateEnsemble(Request $request, Ensemble $ensemble) { - if(! Auth::user()->is_admin) abort(403); request()->validate([ 'name' => 'required', - 'code' => 'required|max:6' + 'code' => 'required|max:6', ]); $ensemble->update([ 'name' => request('name'), - 'code' => request('code') + 'code' => request('code'), ]); - return redirect()->route('admin.ensembles.index')->with('success','Ensemble updated successfully'); + + return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble updated successfully'); } public function seatingLimits(Ensemble $ensemble) @@ -65,33 +71,40 @@ class EnsembleController extends Controller if ($ensemble->exists()) { $ensemble->load('seatingLimits'); } - return view('admin.ensembles.seatingLimits',compact('ensemble','ensembles')); + + return view('admin.ensembles.seatingLimits', compact('ensemble', 'ensembles')); } public function seatingLimitsSet(Request $request, Ensemble $ensemble) { $request->validate([ - 'audition' => 'required', - 'audition.*' => ['integer','min:0'] + 'audition' => 'required', + 'audition.*' => ['integer', 'min:0'], ]); - foreach($ensemble->auditions as $audition) { + foreach ($ensemble->auditions as $audition) { SeatingLimit::upsert( - [[ - 'ensemble_id' => $ensemble->id, - 'audition_id' => $audition->id, - 'maximum_accepted' => $request->audition[$audition->id] - ]], - uniqueBy: ['ensemble_id','audition_id'], + [ + [ + 'ensemble_id' => $ensemble->id, + 'audition_id' => $audition->id, + 'maximum_accepted' => $request->audition[$audition->id], + ], + ], + uniqueBy: ['ensemble_id', 'audition_id'], update: ['maximum_accepted'] ); } - return redirect()->route('admin.ensembles.seatingLimits')->with('success', 'Seating limits set for ' . $ensemble->name); + + return redirect()->route('admin.ensembles.seatingLimits')->with('success', + 'Seating limits set for '.$ensemble->name); } public function updateEnsembleRank(Request $request) { - if(! Auth::user()->is_admin) abort(403); + if (! Auth::user()->is_admin) { + abort(403); + } $order = $request->input('order'); $eventId = $request->input('event_id'); diff --git a/resources/views/admin/ensembles/index-event-table.blade.php b/resources/views/admin/ensembles/index-event-table.blade.php index baebc90..9f337ce 100644 --- a/resources/views/admin/ensembles/index-event-table.blade.php +++ b/resources/views/admin/ensembles/index-event-table.blade.php @@ -15,7 +15,7 @@ @foreach($event->ensembles as $ensemble) - +
diff --git a/tests/Feature/Pages/Setup/SeatingLimitsTest.php b/tests/Feature/Pages/Setup/SeatingLimitsTest.php new file mode 100644 index 0000000..a10ff9e --- /dev/null +++ b/tests/Feature/Pages/Setup/SeatingLimitsTest.php @@ -0,0 +1,174 @@ +assertRedirect(route('home')); + actAsNormal(); + get(route('admin.ensembles.seatingLimits')) + ->assertRedirect('/dashboard') + ->assertSessionHas('error', 'You are not authorized to perform this action'); + actAsAdmin(); + get(route('admin.ensembles.seatingLimits')) + ->assertOk(); +}); +it('shows a dropdown of ensembles', function () { + // Arrange + $ensembles = Ensemble::factory()->count(5)->create(); + actAsAdmin(); + // Act & Assert + $response = get(route('admin.ensembles.seatingLimits')); + $response->assertOk(); + foreach ($ensembles as $ensemble) { + $response->assertSeeInOrder([ + 'id.']"', 'value="', $limit, '"', + ], false); + } +}); + +it('allows an administrator to modify seating limits', function () { + // Arrange + $ensemble = Ensemble::factory()->create(); + $auditions = Audition::factory()->count(5)->create(['event_id' => $ensemble->event_id]); + foreach ($auditions as $audition) { + SeatingLimit::create([ + 'ensemble_id' => $ensemble->id, + 'audition_id' => $audition->id, + 'maximum_accepted' => fake()->numberBetween(1, 15), + ]); + } + $newData = [ + 'audition' => [], + ]; + foreach ($auditions as $audition) { + $newData['audition'][$audition->id] = 21; + } + // Act & Assert + actAsAdmin(); + $response = post(route('admin.ensembles.seatingLimits.ensemble.set', $ensemble), $newData); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Seating limits set for '.$ensemble->name) + ->assertRedirect(route('admin.ensembles.seatingLimits.ensemble', $ensemble)); + $check = Ensemble::find($ensemble->id); + foreach ($check->seatingLimits as $seatingLimit) { + expect($seatingLimit->maximum_accepted)->toBe(21); + } +}); +it('does not allow a guest to update seating limits', function () { + // Arrange + $ensemble = Ensemble::factory()->create(); + $auditions = Audition::factory()->count(5)->create(['event_id' => $ensemble->event_id]); + foreach ($auditions as $audition) { + SeatingLimit::create([ + 'ensemble_id' => $ensemble->id, + 'audition_id' => $audition->id, + 'maximum_accepted' => fake()->numberBetween(1, 15), + ]); + } + $newData = [ + 'audition' => [], + ]; + foreach ($auditions as $audition) { + $newData['audition'][$audition->id] = 21; + } + $response = post(route('admin.ensembles.seatingLimits.ensemble.set', $ensemble), $newData); + $response->assertRedirect(route('home')); + $check = Ensemble::find($ensemble->id); + foreach ($check->seatingLimits as $seatingLimit) { + expect($seatingLimit->maximum_accepted)->toBeLessThan(16); + } +}); +it('does not allow a normal user to update seating limits', function () { + // Arrange + $ensemble = Ensemble::factory()->create(); + $auditions = Audition::factory()->count(5)->create(['event_id' => $ensemble->event_id]); + foreach ($auditions as $audition) { + SeatingLimit::create([ + 'ensemble_id' => $ensemble->id, + 'audition_id' => $audition->id, + 'maximum_accepted' => fake()->numberBetween(1, 15), + ]); + } + $newData = [ + 'audition' => [], + ]; + foreach ($auditions as $audition) { + $newData['audition'][$audition->id] = 21; + } + actAsNormal(); + $response = post(route('admin.ensembles.seatingLimits.ensemble.set', $ensemble), $newData); + $response->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); + $check = Ensemble::find($ensemble->id); + foreach ($check->seatingLimits as $seatingLimit) { + expect($seatingLimit->maximum_accepted)->toBeLessThan(16); + } +}); -- 2.39.5 From df561164be6333613633198bec99dea18e1d5cfd Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 5 Jul 2024 11:38:50 -0500 Subject: [PATCH 56/61] ScoringGuideIndex Tests --- .../Admin/ScoringGuideController.php | 7 +- ...nsure_room_id_0_for_unassigned_entries.php | 7 +- ...ing_guide_id_column_to_auditions_table.php | 5 +- ...ex-scoring-guide-management-card.blade.php | 6 +- .../Pages/Setup/ScoringGuideIndexTest.php | 85 +++++++++++++++++++ 5 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 tests/Feature/Pages/Setup/ScoringGuideIndexTest.php diff --git a/app/Http/Controllers/Admin/ScoringGuideController.php b/app/Http/Controllers/Admin/ScoringGuideController.php index e07b1f7..0e84a5c 100644 --- a/app/Http/Controllers/Admin/ScoringGuideController.php +++ b/app/Http/Controllers/Admin/ScoringGuideController.php @@ -17,13 +17,10 @@ class ScoringGuideController extends Controller { public function index() { - if (! Auth::user()->is_admin) { - abort(403); - } DB::table('auditions') ->whereNull('scoring_guide_id') ->update(['scoring_guide_id' => 0]); - $guides = ScoringGuide::with('auditions')->orderBy('name')->get(); + $guides = ScoringGuide::with(['auditions'])->withCount('subscores')->orderBy('name')->get(); return view('admin.scoring.index', ['guides' => $guides]); } @@ -42,7 +39,7 @@ class ScoringGuideController extends Controller 'name' => request('name'), ]); - return redirect('/admin/scoring'); + return redirect(route('admin.scoring.index'))->with('success', 'Scoring guide created'); } public function edit(Request $request, ScoringGuide $guide) diff --git a/database/migrations/2024_06_04_205015_ensure_room_id_0_for_unassigned_entries.php b/database/migrations/2024_06_04_205015_ensure_room_id_0_for_unassigned_entries.php index 1c28c98..c2e0ac5 100644 --- a/database/migrations/2024_06_04_205015_ensure_room_id_0_for_unassigned_entries.php +++ b/database/migrations/2024_06_04_205015_ensure_room_id_0_for_unassigned_entries.php @@ -1,10 +1,7 @@ 0, - 'name' => 'No Guide Assigned' + 'name' => 'No Guide Assigned', ]); $room->update([ - 'id' => 0 + 'id' => 0, ]); } } diff --git a/database/migrations/2024_06_05_011858_add_scoring_guide_id_column_to_auditions_table.php b/database/migrations/2024_06_05_011858_add_scoring_guide_id_column_to_auditions_table.php index c1f7640..c66e6ee 100644 --- a/database/migrations/2024_06_05_011858_add_scoring_guide_id_column_to_auditions_table.php +++ b/database/migrations/2024_06_05_011858_add_scoring_guide_id_column_to_auditions_table.php @@ -1,6 +1,5 @@ 0, - 'name' => 'No Guide Assigned' + 'name' => 'No Guide Assigned', ]); $sg->update([ - 'id' => 0 + 'id' => 0, ]); } } diff --git a/resources/views/admin/scoring/index-scoring-guide-management-card.blade.php b/resources/views/admin/scoring/index-scoring-guide-management-card.blade.php index 8a57ab3..851cef3 100644 --- a/resources/views/admin/scoring/index-scoring-guide-management-card.blade.php +++ b/resources/views/admin/scoring/index-scoring-guide-management-card.blade.php @@ -17,14 +17,14 @@ @continue @endif - {{ $guide->name }} {{ $guide->subscores->count() }} subscores - Edit + {{ $guide->name }} {{ $guide->subscores_count }} subscores + Edit @endforeach - + diff --git a/tests/Feature/Pages/Setup/ScoringGuideIndexTest.php b/tests/Feature/Pages/Setup/ScoringGuideIndexTest.php new file mode 100644 index 0000000..92368f2 --- /dev/null +++ b/tests/Feature/Pages/Setup/ScoringGuideIndexTest.php @@ -0,0 +1,85 @@ +assertRedirect(route('home')); + actAsNormal(); + get(route('admin.scoring.index')) + ->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); + actAsAdmin(); + get(route('admin.scoring.index')) + ->assertOk() + ->assertViewIs('admin.scoring.index'); +}); +it('shows a list of scoring guides and their count of subscores', function () { + $scoringGuide = ScoringGuide::factory()->create(); + SubscoreDefinition::factory()->count(3)->create(['scoring_guide_id' => $scoringGuide->id]); + Audition::factory()->count(3)->create(['scoring_guide_id' => $scoringGuide->id]); + actAsAdmin(); + $response = get(route('admin.scoring.index')); + $response->assertOk() + ->assertSeeInOrder(['name, $scoringGuide->subscores()->count()], false); +}); +it('shows a link to edit each scoring guide', function () { + $scoringGuide = ScoringGuide::factory()->create(); + actAsAdmin(); + $response = get(route('admin.scoring.index')); + $response->assertOk() + ->assertSee(route('admin.scoring.edit', $scoringGuide)); +}); +it('shows auditions in groups with their scoring guide', function () { + $guides = ScoringGuide::factory()->count(2)->create(); + foreach ($guides as $guide) { + Audition::factory()->count(3)->create(['scoring_guide_id' => $guide->id]); + } + actAsAdmin(); + $response = get(route('admin.scoring.index')); + $response->assertOk(); + foreach (Audition::all() as $audition) { + $guide = $audition->scoringGuide; + $response->assertElementExists('#guide-'.$guide->id, function (AssertElement $element) use ($audition) { + $element->containsText($audition->name); + }); + } +}); +it('has a form for a new scoring guide', function () { + actAsAdmin(); + $response = get(route('admin.scoring.index')); + $response->assertOk() + ->assertSeeInOrder(['assertSee(route('admin.scoring.store')); +}); +it('creates a new scoring guide', function () { + $formData = ['name' => 'New Scoring Guide']; + actAsAdmin(); + $response = post(route('admin.scoring.store'), $formData); + /** @noinspection PhpUnhandledExceptionInspection */ + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('admin.scoring.index')) + ->assertSessionHas('success', 'Scoring guide created'); + $this->assertDatabaseHas('scoring_guides', $formData); +}); +it('only allows an admin to create a new scoring guide', function () { + // Arrange + $formData = ['name' => 'New Scoring Guide']; + // Act & Assert + $response = post(route('admin.scoring.store'), $formData); + $response->assertRedirect(route('home')); + actAsNormal(); + $response = post(route('admin.scoring.store'), $formData); + $response->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); +}); -- 2.39.5 From fb2c5724ec23c57842e553e0e71828ab94adcf57 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 5 Jul 2024 11:58:45 -0500 Subject: [PATCH 57/61] Catch up --- .../Controllers/Admin/AuditionController.php | 23 ++++++++++++++---- ...on-scoring-guide-assignment-card.blade.php | 2 +- routes/admin.php | 2 +- .../Pages/Setup/AuditionsCreateTest.php | 24 +++++++++++++++++++ .../Pages/Setup/ScoringGuideIndexTest.php | 9 +++++++ 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/app/Http/Controllers/Admin/AuditionController.php b/app/Http/Controllers/Admin/AuditionController.php index 2f43b95..9025bf0 100644 --- a/app/Http/Controllers/Admin/AuditionController.php +++ b/app/Http/Controllers/Admin/AuditionController.php @@ -5,6 +5,7 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Event; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -21,7 +22,8 @@ class AuditionController extends Controller { public function index() { - $auditions = Audition::with(['event'])->withCount('entries')->orderBy('score_order')->orderBy('created_at', 'desc')->get(); + $auditions = Audition::with(['event'])->withCount('entries')->orderBy('score_order')->orderBy('created_at', + 'desc')->get(); return view('admin.auditions.index', ['auditions' => $auditions]); } @@ -48,12 +50,16 @@ class AuditionController extends Controller 'entry_fee' => ['required', 'numeric'], 'minimum_grade' => ['required', 'integer'], 'maximum_grade' => 'required|numeric|gte:minimum_grade', + 'scoring_guide_id' => 'nullable|exists:scoring_guides,id', ], [ 'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.', ]); $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; + if (empty($alidData['scoring_guide_id'])) { + $validData['scoring_guide_id'] = 0; + } Audition::create([ 'event_id' => $validData['event_id'], @@ -64,6 +70,7 @@ class AuditionController extends Controller 'maximum_grade' => $validData['maximum_grade'], 'for_seating' => $validData['for_seating'], 'for_advancement' => $validData['for_advancement'], + 'scoring_guide_id' => $validData['scoring_guide_id'], ]); return to_route('admin.auditions.index')->with('success', 'Audition created successfully'); @@ -91,7 +98,7 @@ class AuditionController extends Controller 'entry_deadline' => ['required', 'date'], 'entry_fee' => ['required', 'numeric'], 'minimum_grade' => ['required', 'integer'], - 'maximum_grade' => 'required|numeric|gt:minimum_grade', + 'maximum_grade' => 'required | numeric | gt:minimum_grade', ], [ 'maximum_grade.gt' => 'The maximum grade must be greater than the minimum grade.', ]); @@ -142,6 +149,13 @@ class AuditionController extends Controller return response()->json(['status' => 'success']); } + /** + * Update the scoring guide for an audition + * Used by AJAX call on the scoring guide index page + * request should include scoring_guide_id and audition_id + * + * @return JsonResponse + */ public function scoringGuideUpdate(Request $request) { @@ -184,7 +198,8 @@ class AuditionController extends Controller return $audition->has_partial_draw(); }); - return view('admin.entries.prepare_draw', compact('nodraw_auditions', 'drawn_auditions', 'partial_draw_auditions')); + return view('admin.entries.prepare_draw', + compact('nodraw_auditions', 'drawn_auditions', 'partial_draw_auditions')); } public function runDraw(Request $request) @@ -197,6 +212,6 @@ class AuditionController extends Controller $audition->runDraw(); } - return redirect('/admin/auditions/run_draw'); + return redirect(' / admin / auditions / run_draw'); } } diff --git a/resources/views/admin/scoring/index-audition-scoring-guide-assignment-card.blade.php b/resources/views/admin/scoring/index-audition-scoring-guide-assignment-card.blade.php index 69dab01..16155a5 100644 --- a/resources/views/admin/scoring/index-audition-scoring-guide-assignment-card.blade.php +++ b/resources/views/admin/scoring/index-audition-scoring-guide-assignment-card.blade.php @@ -54,7 +54,7 @@ let auditionId = itemEl.getAttribute('data-id'); // Make an AJAX request to update the audition_guide_id - fetch('/admin/scoring/assign_guide_to_audition', { + fetch('{{route('ajax.assignScoringGuideToAudition')}}', { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/routes/admin.php b/routes/admin.php index 39897c1..4a2d5b9 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -8,7 +8,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::view('/', 'admin.dashboard')->name('admin.dashboard'); Route::post('/auditions/roomUpdate', [\App\Http\Controllers\Admin\AuditionController::class, 'roomUpdate']); // Endpoint for JS assigning auditions to rooms - Route::post('/scoring/assign_guide_to_audition', [\App\Http\Controllers\Admin\AuditionController::class, 'scoringGuideUpdate']); // Endpoint for JS assigning scoring guides to auditions + Route::post('/scoring/assign_guide_to_audition', [\App\Http\Controllers\Admin\AuditionController::class, 'scoringGuideUpdate'])->name('ajax.assignScoringGuideToAudition'); // Endpoint for JS assigning scoring guides to auditions Route::get('/settings', [\App\Http\Controllers\Admin\AuditionSettings::class, 'index'])->name('audition-settings'); Route::post('/settings', [\App\Http\Controllers\Admin\AuditionSettings::class, 'save'])->name('audition-settings-save'); diff --git a/tests/Feature/Pages/Setup/AuditionsCreateTest.php b/tests/Feature/Pages/Setup/AuditionsCreateTest.php index b913fc9..18b9dd3 100644 --- a/tests/Feature/Pages/Setup/AuditionsCreateTest.php +++ b/tests/Feature/Pages/Setup/AuditionsCreateTest.php @@ -66,6 +66,30 @@ it('allows an administrator to create auditions', function () { ->and($checkAudition->for_seating)->toBe(0) ->and($checkAudition->for_advancement)->toBe(1); }); +it('sets scoring_guide_id to 0 if none is set when creating an audition', function () { + // Arrange + $newEvent = Event::factory()->create(); + $changes = [ + 'event_id' => $newEvent->id, + 'name' => 'New Name', + 'entry_deadline' => '1978-01-01', + 'entry_fee' => 10000, + 'minimum_grade' => 3, + 'maximum_grade' => 8, + 'for_advancement' => 'on', + ]; + actAsAdmin(); + // Act + $response = post(route('admin.auditions.store'), $changes); + // Assert + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.auditions.index')) + ->assertSessionHasNoErrors() + ->assertSessionHas('success', 'Audition created successfully'); + $checkAudition = Audition::latest()->first(); + expect($checkAudition->scoring_guide_id)->toBe(0); + +}); it('does not allow a normal user or guest to create an audition', function () { // Arrange $precount = Audition::count(); diff --git a/tests/Feature/Pages/Setup/ScoringGuideIndexTest.php b/tests/Feature/Pages/Setup/ScoringGuideIndexTest.php index 92368f2..404c048 100644 --- a/tests/Feature/Pages/Setup/ScoringGuideIndexTest.php +++ b/tests/Feature/Pages/Setup/ScoringGuideIndexTest.php @@ -83,3 +83,12 @@ it('only allows an admin to create a new scoring guide', function () { $response->assertRedirect(route('dashboard')) ->assertSessionHas('error', 'You are not authorized to perform this action'); }); +it('can assign a scoring guide to an audition', function () { + // Arrange + $scoringGuide = ScoringGuide::factory()->create(); + $audition = Audition::factory()->create(); + // Act & Assert + expect($audition->scoring_guide_id)->toBeNull(); + $response = post(route('ajax.assignScoringGuideToAudition'), + ['audition_id' => $audition->id, 'new_guide_id' => $scoringGuide->id]); +}); -- 2.39.5 From d21324b71b716bd4957876357b7ca6314a14b7e7 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 5 Jul 2024 14:30:57 -0500 Subject: [PATCH 58/61] ScoringGuidePage Tests --- .../Admin/ScoringGuideController.php | 21 ++- .../views/admin/scoring/edit-tabs.blade.php | 7 +- routes/admin.php | 2 +- .../Setup/ScoringGuideEditDetailsTest.php | 155 ++++++++++++++++++ .../Pages/Setup/ScoringGuideOrderTabsTest.php | 54 ++++++ 5 files changed, 226 insertions(+), 13 deletions(-) create mode 100644 tests/Feature/Pages/Setup/ScoringGuideEditDetailsTest.php create mode 100644 tests/Feature/Pages/Setup/ScoringGuideOrderTabsTest.php diff --git a/app/Http/Controllers/Admin/ScoringGuideController.php b/app/Http/Controllers/Admin/ScoringGuideController.php index 0e84a5c..efbf8aa 100644 --- a/app/Http/Controllers/Admin/ScoringGuideController.php +++ b/app/Http/Controllers/Admin/ScoringGuideController.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use function abort; +use function auditionSetting; use function request; use function response; @@ -42,12 +43,11 @@ class ScoringGuideController extends Controller return redirect(route('admin.scoring.index'))->with('success', 'Scoring guide created'); } - public function edit(Request $request, ScoringGuide $guide) + public function edit(Request $request, ScoringGuide $guide, string $tab = 'detail') { if (! Auth::user()->is_admin) { abort(403); } - $tab = $request->query('tab') ?? 'detail'; if ($tab == 'tiebreakOrder') { $subscores = SubscoreDefinition::where('scoring_guide_id', $guide->id)->orderBy('tiebreak_order')->get(); } else { @@ -90,9 +90,6 @@ class ScoringGuideController extends Controller public function subscore_store(Request $request, ScoringGuide $guide) { - if (! Auth::user()->is_admin) { - abort(403); - } if (! $guide->exists()) { abort(409); } @@ -106,6 +103,9 @@ class ScoringGuideController extends Controller $for_seating = $request->has('for_seating') ? (bool) $request->input('for_seating') : false; $for_advance = $request->has('for_advance') ? (bool) $request->input('for_advance') : false; + if (! auditionSetting('advanceTo')) { + $for_seating = true; + } $display_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('display_order') + 1; $tiebreak_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('tiebreak_order') + 1; @@ -121,7 +121,7 @@ class ScoringGuideController extends Controller 'for_advance' => $for_advance, ]); - return redirect('/admin/scoring/guides/'.$guide->id.'/edit')->with('success', 'Subscore added'); + return redirect(route('admin.scoring.edit', $guide))->with('success', 'Subscore added'); } public function subscore_update(ScoringGuide $guide, SubscoreDefinition $subscore) @@ -146,6 +146,10 @@ class ScoringGuideController extends Controller $for_seating = request()->has('for_seating') ? (bool) request()->input('for_seating') : false; $for_advance = request()->has('for_advance') ? (bool) request()->input('for_advance') : false; + if (! auditionSetting('advanceTo')) { + $for_seating = true; + } + $subscore->update([ 'name' => $validateData['name'], 'max_score' => $validateData['max_score'], @@ -175,7 +179,6 @@ class ScoringGuideController extends Controller } - public function reorder_display(Request $request) { if (! Auth::user()->is_admin) { @@ -184,7 +187,7 @@ class ScoringGuideController extends Controller $order = $request->order; foreach ($order as $index => $id) { $subscore = SubscoreDefinition::find($id); - $subscore->update(['display_order' => $index]); + $subscore->update(['display_order' => $index + 1]); } return response()->json(['status' => 'success']); @@ -199,7 +202,7 @@ class ScoringGuideController extends Controller $order = $request->order; foreach ($order as $index => $id) { $subscore = SubscoreDefinition::find($id); - $subscore->update(['tiebreak_order' => $index]); + $subscore->update(['tiebreak_order' => $index + 1]); } return response()->json(['status' => 'success']); diff --git a/resources/views/admin/scoring/edit-tabs.blade.php b/resources/views/admin/scoring/edit-tabs.blade.php index 20ae7ba..dcd74b5 100644 --- a/resources/views/admin/scoring/edit-tabs.blade.php +++ b/resources/views/admin/scoring/edit-tabs.blade.php @@ -18,9 +18,10 @@
diff --git a/routes/admin.php b/routes/admin.php index 4a2d5b9..aae16f1 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -49,7 +49,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::prefix('scoring')->controller(\App\Http\Controllers\Admin\ScoringGuideController::class)->group(function () { Route::get('/', 'index')->name('admin.scoring.index'); // Scoring Setup Homepage Route::post('/guides', 'store')->name('admin.scoring.store'); // Save a new scoring guide - Route::get('/guides/{guide}/edit', 'edit')->name('admin.scoring.edit'); // Edit scoring guide + Route::get('/guides/{guide}/edit/{tab?}', 'edit')->name('admin.scoring.edit'); // Edit scoring guide Route::patch('/guides/{guide}/edit', 'update')->name('admin.scoring.update'); // Save changes to audition guide (rename) Route::post('/guides/{guide}/subscore', 'subscore_store')->name('admin.scoring.subscore_store'); // Save a new subscore Route::patch('/guides/{guide}/subscore/{subscore}', 'subscore_update')->name('admin.scoring.subscore_update'); // Modify a subscore diff --git a/tests/Feature/Pages/Setup/ScoringGuideEditDetailsTest.php b/tests/Feature/Pages/Setup/ScoringGuideEditDetailsTest.php new file mode 100644 index 0000000..a53cb82 --- /dev/null +++ b/tests/Feature/Pages/Setup/ScoringGuideEditDetailsTest.php @@ -0,0 +1,155 @@ +scoringGuide = ScoringGuide::factory()->create(); + $this->subscores = SubscoreDefinition::factory()->count(3)->create([ + 'scoring_guide_id' => $this->scoringGuide->id, + ]); +}); +it('shows the details of a scoring guide', function () { + actAsAdmin(); + $response = get(route('admin.scoring.edit', $this->scoringGuide)); + $response->assertOk(); + $response->assertSee($this->scoringGuide->name); + foreach ($this->subscores as $subscore) { + $response->assertSeeInOrder([ + 'name, '', + 'max_score, '', + 'weight, '', + 'for_seating) ? 'Yes' : 'No', '', + 'for_advance) ? 'Yes' : 'No', '', + ], false); + } + Settings::set('advanceTo', ''); + $response = get(route('admin.scoring.edit', $this->scoringGuide)); + $response->assertOk(); + $response->assertSee($this->scoringGuide->name); + foreach ($this->subscores as $subscore) { + $response->assertSeeInOrder([ + 'name, '', + 'max_score, '', + 'weight, '', + ], false); + $response->assertDontSee('Yes', false); + $response->assertDontSee('No', false); + } +}); +it('includes a form to add a subscore', function () { + // Arrange + actAsAdmin(); + // Act & Assert + $response = get(route('admin.scoring.edit', $this->scoringGuide)); + $response->assertOk(); + $response->assertSee(route('admin.scoring.subscore_store', $this->scoringGuide)); +}); +it('can create a subscore', function () { + // Arrange + actAsAdmin(); + $formData = [ + 'name' => 'New Subscore', + 'max_score' => 32, + 'weight' => 1, + 'for_seating' => 1, + ]; + // Act + $response = $this->post(route('admin.scoring.subscore_store', $this->scoringGuide), $formData); + // Assert + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertSessionHasNoErrors() + ->assertRedirect(route('admin.scoring.edit', $this->scoringGuide)); + $this->assertDatabaseHas('subscore_definitions', $formData); + +}); +it('sets for_seating true for new subscores when advancement is not enabled', function () { + Settings::set('advanceTo', ''); + actAsAdmin(); + $formData = [ + 'name' => 'New Subscore', + 'max_score' => 32, + 'weight' => 1, + ]; + $response = $this->post(route('admin.scoring.subscore_store', $this->scoringGuide), $formData); + $response->assertSessionHasNoErrors() + ->assertRedirect(route('admin.scoring.edit', $this->scoringGuide)); + $this->assertDatabaseHas('subscore_definitions', $formData); + $newSubscore = SubscoreDefinition::where('name', 'New Subscore')->first(); + expect($newSubscore->for_seating)->toBeTruthy(); +}); +it('only allows an admin to create a subscore', function () { + // Arrange + $formData = [ + 'name' => 'New Subscore', + 'max_score' => 32, + 'weight' => 1, + 'for_seating' => 1, + ]; + // Act & Assert + $response = post(route('admin.scoring.subscore_store', $this->scoringGuide), $formData); + $response->assertRedirect(route('home')); + actAsNormal(); + $response = $this->post(route('admin.scoring.subscore_store', $this->scoringGuide), $formData); + $response->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); +}); +it('allows a subscore to be modified', function () { + // Arrange + $subscore = SubscoreDefinition::factory()->create([ + 'scoring_guide_id' => $this->scoringGuide->id, + ]); + $formData = [ + 'name' => 'Changed Name', + 'max_score' => 32, + 'weight' => 1, + 'for_seating' => 1, + ]; + // Act & Assert + actAsAdmin(); + $response = $this->patch(route('admin.scoring.subscore_update', [$this->scoringGuide, $subscore]), $formData); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertSessionHasNoErrors() + ->assertRedirect(route('admin.scoring.edit', $this->scoringGuide)) + ->assertSessionHas('success', 'Subscore updated'); + $subscore = SubscoreDefinition::find($subscore->id); + expect($subscore->name)->toBe('Changed Name') + ->and($subscore->max_score)->toBe(32) + ->and($subscore->weight)->toBe(1) + ->and($subscore->for_seating)->toBeTruthy() + ->and($subscore->for_advance)->toBeFalsy(); +}); +it('sets for_seating true if advance is not enabled when modifying a subscore', function () { + // Arrange + Settings::set('advanceTo', ''); + $subscore = SubscoreDefinition::factory()->create([ + 'scoring_guide_id' => $this->scoringGuide->id, + ]); + $formData = [ + 'name' => 'Changed Name', + 'max_score' => 32, + 'weight' => 1, + ]; + // Act & Assert + actAsAdmin(); + $response = $this->patch(route('admin.scoring.subscore_update', [$this->scoringGuide, $subscore]), $formData); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertSessionHasNoErrors() + ->assertRedirect(route('admin.scoring.edit', $this->scoringGuide)) + ->assertSessionHas('success', 'Subscore updated'); + $subscore = SubscoreDefinition::find($subscore->id); + expect($subscore->name)->toBe('Changed Name') + ->and($subscore->max_score)->toBe(32) + ->and($subscore->weight)->toBe(1) + ->and($subscore->for_seating)->toBeTruthy() + ->and($subscore->for_advance)->toBeFalsy(); +}); diff --git a/tests/Feature/Pages/Setup/ScoringGuideOrderTabsTest.php b/tests/Feature/Pages/Setup/ScoringGuideOrderTabsTest.php new file mode 100644 index 0000000..fb0ee46 --- /dev/null +++ b/tests/Feature/Pages/Setup/ScoringGuideOrderTabsTest.php @@ -0,0 +1,54 @@ +scoringGuide = ScoringGuide::factory()->create(); + $this->subscores = SubscoreDefinition::factory()->count(6)->create([ + 'scoring_guide_id' => $this->scoringGuide->id, + ]); +}); + +it('shows subscores in display or tiebreak order', function () { + $scoringGuide = ScoringGuide::factory()->create(); + $fourthSubscore = SubscoreDefinition::factory()->create([ + 'scoring_guide_id' => $scoringGuide->id, + 'display_order' => 4, + 'tiebreak_order' => 3, + ]); + $firstSubscore = SubscoreDefinition::factory()->create([ + 'scoring_guide_id' => $scoringGuide->id, + 'display_order' => 1, + 'tiebreak_order' => 4, + ]); + $thirdSubscore = SubscoreDefinition::factory()->create([ + 'scoring_guide_id' => $scoringGuide->id, + 'display_order' => 3, + 'tiebreak_order' => 2, + ]); + $secondSubscore = SubscoreDefinition::factory()->create([ + 'scoring_guide_id' => $scoringGuide->id, + 'display_order' => 2, + 'tiebreak_order' => 1, + ]); + actAsAdmin(); + $response = get(route('admin.scoring.edit', ['guide' => $scoringGuide, 'tab' => 'displayOrder'])); + $response->assertOk()->assertSee('Subscore Display Order')->assertSeeInOrder([ + $firstSubscore->name, + $secondSubscore->name, + $thirdSubscore->name, + $fourthSubscore->name, + ]); + $response = get(route('admin.scoring.edit', ['guide' => $scoringGuide, 'tab' => 'tiebreakOrder'])); + $response->assertOk()->assertSee('Subscore Display Order')->assertSeeInOrder([ + $secondSubscore->name, + $thirdSubscore->name, + $fourthSubscore->name, + $firstSubscore->name, + ]); +}); -- 2.39.5 From cde4925368a4db70e20a4060b51521ba5a8ff3d8 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 5 Jul 2024 15:17:08 -0500 Subject: [PATCH 59/61] AdminRooms Page Tests --- app/Http/Controllers/Admin/RoomController.php | 2 +- .../admin/rooms/index-room-card.blade.php | 6 +- resources/views/admin/rooms/index.blade.php | 2 +- tests/Feature/Pages/Setup/RoomsIndexTest.php | 125 ++++++++++++++++++ 4 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/Pages/Setup/RoomsIndexTest.php diff --git a/app/Http/Controllers/Admin/RoomController.php b/app/Http/Controllers/Admin/RoomController.php index 9de1eed..a4be3cd 100644 --- a/app/Http/Controllers/Admin/RoomController.php +++ b/app/Http/Controllers/Admin/RoomController.php @@ -100,7 +100,7 @@ class RoomController extends Controller $room->description = $validData['description']; $room->save(); - return redirect()->route('admin.rooms.index')->with('success', 'Room updated.'); + return redirect()->route('admin.rooms.index')->with('success', 'Room updated successfully'); } public function destroy(Room $room) diff --git a/resources/views/admin/rooms/index-room-card.blade.php b/resources/views/admin/rooms/index-room-card.blade.php index 624c8ef..c543b04 100644 --- a/resources/views/admin/rooms/index-room-card.blade.php +++ b/resources/views/admin/rooms/index-room-card.blade.php @@ -1,10 +1,12 @@ - @include('admin.rooms.index-edit-room-modal') + @if($room->id != '0') + @include('admin.rooms.index-edit-room-modal') + @endif {{ $room->name }} {{ $room->description }} - @if($room->entries->count() === 0 and $room->id != '0') + @if($room->auditions->count() === 0 and $room->id != '0')
Rooms - New Room + New Room
diff --git a/tests/Feature/Pages/Setup/RoomsIndexTest.php b/tests/Feature/Pages/Setup/RoomsIndexTest.php new file mode 100644 index 0000000..7343fbd --- /dev/null +++ b/tests/Feature/Pages/Setup/RoomsIndexTest.php @@ -0,0 +1,125 @@ +count(3)->create(); + foreach ($rooms as $room) { + Audition::factory()->count(3)->create(['room_id' => $room->id]); + } + $auditions = Audition::all(); + foreach ($auditions as $audition) { + $n = fake()->numberBetween(1, 4); + Entry::factory()->count($n)->create(['audition_id' => $audition->id]); + } +} + +it('only allows an admin to manage rooms', function () { + get(route('admin.rooms.index')) + ->assertRedirect(route('home')); + actAsNormal(); + get(route('admin.rooms.index')) + ->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); + actAsAdmin(); + get(route('admin.rooms.index')) + ->assertOk(); +}); +it('has a link to create a new room', function () { + actAsAdmin(); + get(route('admin.rooms.index')) + ->assertSee('New Room') + ->assertSee(route('admin.rooms.create')); +}); +it('has a card for each room, including the total number of entries in that room', function () { + // Arrange + roomTestSetup(); + // Act & Assert + actAsAdmin(); + $response = get(route('admin.rooms.index')); + $response->assertOk(); + foreach (Room::all() as $room) { + $roomEntryCount = $room->entries()->count(); + $response->assertSeeInOrder(['name), '', $roomEntryCount], false); + } +}); +it('has a form for editing each room', function () { + // Arrange + roomTestSetup(); + // Act & Assert + actAsAdmin(); + $response = get(route('admin.rooms.index')); + $response->assertOk(); + foreach (Room::all() as $room) { + if ($room->id === 0) { + continue; + } + $response->assertSeeInOrder([ + route('admin.rooms.update', $room), + 'create(); + $formData = [ + 'name' => 'New Room Name', + 'description' => 'New Room Description', + ]; + actAsAdmin(); + $response = $this->patch(route('admin.rooms.update', $room), $formData); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.rooms.index')) + ->assertSessionHas('success', 'Room updated successfully') + ->assertSessionHasNoErrors(); + $this->assertDatabaseHas('rooms', $formData); +}); +it('can create a room', function () { + $formData = [ + 'name' => 'New Room Name', + 'description' => 'New Room Description', + ]; + actAsAdmin(); + $response = $this->post(route('admin.rooms.store'), $formData); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.rooms.index')) + ->assertSessionHas('success', 'Room created.') + ->assertSessionHasNoErrors(); + $this->assertDatabaseHas('rooms', $formData); +}); + +it('can delete a room', function () { + // Arrange + $room = Room::factory()->create(); + // Act & Assert + actAsAdmin(); + $response = $this->delete(route('admin.rooms.destroy', $room)); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.rooms.index')) + ->assertSessionHas('success', 'Room deleted.') + ->assertSessionHasNoErrors(); + $this->assertDatabaseMissing('rooms', ['id' => $room->id]); +}); +it('will not delete a room with auditions', function () { + $room = Room::factory()->create(); + Audition::factory()->create(['room_id' => $room->id]); + actAsAdmin(); + $response = $this->delete(route('admin.rooms.destroy', $room)); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.rooms.index')) + ->assertSessionHas('error', + 'Cannot delete room with auditions. First move the auditions to unassigned or another room') + ->assertSessionHasNoErrors(); +}); -- 2.39.5 From 33d0b6ca55fb7ba5d4b1488a5d631d2683487c78 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 5 Jul 2024 15:21:54 -0500 Subject: [PATCH 60/61] Require unique room name on factory --- database/factories/RoomFactory.php | 2 +- tests/Feature/Pages/EntriesIndexTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/database/factories/RoomFactory.php b/database/factories/RoomFactory.php index 46acabf..f20bf5a 100644 --- a/database/factories/RoomFactory.php +++ b/database/factories/RoomFactory.php @@ -17,7 +17,7 @@ class RoomFactory extends Factory public function definition(): array { return [ - 'name' => 'Room '.fake()->numberBetween(7, 500), + 'name' => 'Room '.fake()->unique()->numberBetween(7, 500), 'description' => fake()->sentence(), ]; } diff --git a/tests/Feature/Pages/EntriesIndexTest.php b/tests/Feature/Pages/EntriesIndexTest.php index e633133..e8b2d20 100644 --- a/tests/Feature/Pages/EntriesIndexTest.php +++ b/tests/Feature/Pages/EntriesIndexTest.php @@ -90,9 +90,9 @@ it('shows existing entries in a table', function () { $response-> assertSeeInOrder([ 'student->full_name(true), + e($entry->student->full_name(true)), $entry->student->grade, - $entry->audition->name, + e($entry->audition->name), '', ], false); } -- 2.39.5 From 252c7e758a2b139739e4851e18a13912325922a9 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Fri, 5 Jul 2024 16:07:35 -0500 Subject: [PATCH 61/61] AdminJudging Page --- app/Http/Controllers/Admin/RoomController.php | 3 - .../admin/rooms/judge_assignments.blade.php | 6 +- .../Feature/Pages/Setup/JudgingIndexTest.php | 74 +++++++++++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 tests/Feature/Pages/Setup/JudgingIndexTest.php diff --git a/app/Http/Controllers/Admin/RoomController.php b/app/Http/Controllers/Admin/RoomController.php index a4be3cd..4f1cee1 100644 --- a/app/Http/Controllers/Admin/RoomController.php +++ b/app/Http/Controllers/Admin/RoomController.php @@ -24,9 +24,6 @@ class RoomController extends Controller public function judgingAssignment() // Show form for assigning judges { - if (! Auth::user()->is_admin) { - abort(403); - } $usersWithoutRooms = User::doesntHave('rooms')->orderBy('last_name')->orderBy('first_name')->get(); $usersWithRooms = User::has('rooms')->orderBy('last_name')->orderBy('first_name')->get(); $rooms = Room::with(['judges.school', 'auditions'])->get(); diff --git a/resources/views/admin/rooms/judge_assignments.blade.php b/resources/views/admin/rooms/judge_assignments.blade.php index a42f3d3..6ae2aec 100644 --- a/resources/views/admin/rooms/judge_assignments.blade.php +++ b/resources/views/admin/rooms/judge_assignments.blade.php @@ -5,7 +5,7 @@ @if($room->id == 0) @continue @endif -
  • {{-- card wrapper --}} +
  • {{-- card wrapper --}}
    {{-- card header --}}

    {{ $room->name }}

    @@ -60,8 +60,8 @@
    {{-- Judge Line --}}

    - {{ $judge->full_name() }}, - {{ $judge->school->name }} + {{ $judge->full_name() }} + {{ $judge->school->name ?? '' }}

    {{ $judge->judging_preference }}

    diff --git a/tests/Feature/Pages/Setup/JudgingIndexTest.php b/tests/Feature/Pages/Setup/JudgingIndexTest.php new file mode 100644 index 0000000..13bf112 --- /dev/null +++ b/tests/Feature/Pages/Setup/JudgingIndexTest.php @@ -0,0 +1,74 @@ +get(route('admin.rooms.judgingAssignment')) + ->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.rooms.judgingAssignment')) + ->assertRedirect(route('dashboard')) + ->assertSessionHas('error', 'You are not authorized to perform this action'); + actAsAdmin(); + $this->get(route('admin.rooms.judgingAssignment')) + ->assertOk(); +}); +it('shows a card for each room with its judges', function () { + // Arrange + $rooms = Room::factory()->count(3)->create(); + foreach ($rooms as $room) { + $users = User::factory()->count(3)->create(); + foreach ($users as $user) { + $room->addJudge($user->id); + } + } + // Act & Assert + actAsAdmin(); + $response = $this->get(route('admin.rooms.judgingAssignment')); + $response->assertOk(); + foreach ($rooms as $room) { + $response->assertElementExists('#room-'.$room->id.'-card', function (AssertElement $element) use ($room) { + $element->is('li'); + $element->containsText($room->name); + foreach ($room->judges as $judge) { + $element->containsText($judge->full_name()); + } + }); + } +}); +it('can assign a judge', function () { + // Arrange + $room = Room::factory()->create(); + $judge = User::factory()->create(); + // Act & Assert + expect($room->judges->contains($judge))->toBeFalse(); + actAsAdmin(); + $response = $this->post(route('admin.rooms.updateJudgeAssignment', $room), ['judge' => $judge->id]); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.rooms.judgingAssignment')) + ->assertSessionHas('success', 'Assigned '.$judge->full_name().' to '.$room->name) + ->assertSessionHasNoErrors(); + $checkRoom = Room::find($room->id); + expect($checkRoom->judges->contains($judge))->toBeTrue(); +}); +it('can remove a judge', function () { + // Arrange + $room = Room::factory()->create(); + $judge = User::factory()->create(); + $room->addJudge($judge->id); + expect($room->judges->contains($judge))->toBeTrue(); + actAsAdmin(); + // Act & Assert + $response = $this->delete(route('admin.rooms.updateJudgeAssignment', $room), ['judge' => $judge->id]); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertRedirect(route('admin.rooms.judgingAssignment')) + ->assertSessionHas('success', 'Removed '.$judge->full_name().' from '.$room->name) + ->assertSessionHasNoErrors(); + $checkRoom = Room::find($room->id); + expect($checkRoom->judges->contains($judge))->toBeFalse(); +}); -- 2.39.5