From 09f4ed6636dcab6ad20bc1c3138d572984f36853 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 16 Jul 2025 08:27:08 -0500 Subject: [PATCH] Tets for EnterDoublerDecisionController --- .../Tabulation/AdvancementController.php | 12 +- .../EnterDoublerDecisionsController.php | 10 - .../advancement/results-table.blade.php | 58 +++++ .../Tabulation/AdvancementControllerTest.php | 213 ++++++++++++++++++ .../EnterDoublerDecisionControllerTest.php | 201 +++++++++++++++++ 5 files changed, 482 insertions(+), 12 deletions(-) create mode 100644 tests/Feature/app/Http/Controllers/Tabulation/AdvancementControllerTest.php create mode 100644 tests/Feature/app/Http/Controllers/Tabulation/Seating/EnterDoublerDecisionControllerTest.php diff --git a/app/Http/Controllers/Tabulation/AdvancementController.php b/app/Http/Controllers/Tabulation/AdvancementController.php index f783fde..957c17f 100644 --- a/app/Http/Controllers/Tabulation/AdvancementController.php +++ b/app/Http/Controllers/Tabulation/AdvancementController.php @@ -73,11 +73,19 @@ class AdvancementController extends Controller $entries = $ranker($audition, 'advancement'); $entries->load(['advancementVotes', 'totalScore', 'student.school']); - $scoringComplete = $entries->every(function ($entry) { + $unscoredEntries = $audition->entries()->orderBy('draw_number')->get()->filter(function ($entry) { + return ! $entry->totalScore && ! $entry->hasFlag('no_show'); + }); + + $noShowEntries = $audition->entries()->orderBy('draw_number')->get()->filter(function ($entry) { + return $entry->hasFlag('no_show'); + }); + + $scoringComplete = $audition->entries->every(function ($entry) { return $entry->totalScore || $entry->hasFlag('no_show'); }); - return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete')); + return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete', 'unscoredEntries', 'noShowEntries')); } public function setAuditionPassers(Request $request, Audition $audition) diff --git a/app/Http/Controllers/Tabulation/Seating/EnterDoublerDecisionsController.php b/app/Http/Controllers/Tabulation/Seating/EnterDoublerDecisionsController.php index 0149f03..91a981e 100644 --- a/app/Http/Controllers/Tabulation/Seating/EnterDoublerDecisionsController.php +++ b/app/Http/Controllers/Tabulation/Seating/EnterDoublerDecisionsController.php @@ -8,7 +8,6 @@ use App\Exceptions\AuditionAdminException; use App\Http\Controllers\Controller; use App\Models\Audition; use App\Models\Entry; -use Debugbar; use Illuminate\Support\Facades\Cache; use function redirect; @@ -59,25 +58,16 @@ class EnterDoublerDecisionsController extends Controller } $scored_entries->load(['student.doublers', 'student.school']); foreach ($scored_entries as $entry) { - Debugbar::info('Starting entry '.$entry->student->full_name()); if ($entry->seatingRank < $validData['decline-below']) { - Debugbar::info('Skipping '.$entry->student->full_name().' because they are ranked above decline threshold'); - continue; } if ($entry->hasFlag('declined')) { - Debugbar::info('Skipping '.$entry->student->full_name().' because they have already been declined'); - continue; } if (! $entry->student->isDoublerInEvent($audition->event_id)) { - Debugbar::info('Skipping '.$entry->student->full_name().' because they are not a doubler'); - continue; } if ($entry->student->doublers->where('event_id', $audition->event_id)->first()->accepted_entry) { - Debugbar::info('Skipping '.$entry->student->full_name().' because they have already accepted a seat'); - continue; } try { diff --git a/resources/views/tabulation/advancement/results-table.blade.php b/resources/views/tabulation/advancement/results-table.blade.php index 9d3f81e..5782783 100644 --- a/resources/views/tabulation/advancement/results-table.blade.php +++ b/resources/views/tabulation/advancement/results-table.blade.php @@ -1,4 +1,5 @@ + Scored Entries @@ -66,3 +67,60 @@ + + + + Unscored Entries + + + + Draw # + ID + Student + Judges Scored + + + + @foreach($unscoredEntries as $entry) + + {{ $entry->draw_number }} + {{ $entry->id }} + +
+ {{ $entry->student->full_name() }} +
+
{{ $entry->student->school->name }}
+
+ {{ $entry->scoreSheets->count() }} + + + @endforeach +
+
+ + + No-Show Entries + + + + Draw # + ID + Student + + + + @foreach($noShowEntries as $entry) + + {{ $entry->draw_number }} + {{ $entry->id }} + +
+ {{ $entry->student->full_name() }} +
+
{{ $entry->student->school->name }}
+
+ + + @endforeach +
+
diff --git a/tests/Feature/app/Http/Controllers/Tabulation/AdvancementControllerTest.php b/tests/Feature/app/Http/Controllers/Tabulation/AdvancementControllerTest.php new file mode 100644 index 0000000..69fc4ca --- /dev/null +++ b/tests/Feature/app/Http/Controllers/Tabulation/AdvancementControllerTest.php @@ -0,0 +1,213 @@ +create(); + $this->audition1 = Audition::factory()->create(['scoring_guide_id' => $sg->id, 'score_order' => 1]); + $this->audition2 = Audition::factory()->create(['scoring_guide_id' => $sg->id, 'score_order' => 2]); + + // Create 15 entries for audition 1 and score them all + $a1Entries = Entry::factory()->count(15)->create(['audition_id' => $this->audition1->id]); + foreach ($a1Entries as $entry) { + EntryTotalScore::create([ + 'entry_id' => $entry->id, + 'seating_total' => 34, + 'advancement_total' => 4, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + } + + // Create 10 entries for audition2 and score one of them + $a2Entries = Entry::factory()->count(10)->create(['audition_id' => $this->audition2->id]); + EntryTotalScore::create([ + 'entry_id' => $a2Entries->first()->id, + 'seating_total' => 34, + 'advancement_total' => 4, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + }); + it('denies access to regular users and guests', function () { + $this->get(route('advancement.status'))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('advancement.status'))->assertRedirect(route('dashboard')); + }); + it('returns the correct status', function () { + actAsAdmin(); + $response = $this->get(route('advancement.status')); + $response->assertOk()->assertViewIs('tabulation.advancement.status'); + $data = $response->viewData('auditionData'); + expect($data)->toHaveCount(2) + ->and($data[0]['name'])->toEqual($this->audition1->name) + ->and($data[0]['entries_count'])->toEqual(15) + ->and($data[0]['unscored_entries_count'])->toEqual(0) + ->and($data[0]['scored_entries_count'])->toEqual(15) + ->and($data[0]['scored_percentage'])->toEqual(100) + ->and($data[0]['scoring_complete'])->toBeTruthy() + ->and($data[0]['published'])->toBeFalsy() + ->and($data[1]['name'])->toEqual($this->audition2->name) + ->and($data[1]['entries_count'])->toEqual(10) + ->and($data[1]['unscored_entries_count'])->toEqual(9) + ->and($data[1]['scored_entries_count'])->toEqual(1) + ->and($data[1]['scored_percentage'])->toEqual(10) + ->and($data[1]['scoring_complete'])->toBeFalsy() + ->and($data[1]['published'])->toBeFalsy(); + }); +}); + +describe('AdvancementController::ranking', function () { + beforeEach(function () { + $this->audition = Audition::factory()->create(); + $this->entries = Entry::factory()->count(3)->create(['audition_id' => $this->audition->id]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 34, + 'advancement_total' => 20, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 34, + 'advancement_total' => 10, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 34, + 'advancement_total' => 30, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + }); + it('denies access to regular users and guests', function () { + $this->get(route('advancement.ranking', $this->audition))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('advancement.ranking', $this->audition))->assertRedirect(route('dashboard')); + }); + it('returns a list of entries', function () { + actAsAdmin(); + $response = $this->get(route('advancement.ranking', $this->audition)); + $response->assertOk()->assertViewIs('tabulation.advancement.ranking'); + $response->assertSeeInOrder([ + $this->entries[2]->student->full_name(), + $this->entries[0]->student->full_name(), + $this->entries[1]->student->full_name(), + ]); + }); + it('shows a form to set accepted entries if scoring is complete', function () { + actAsAdmin(); + $response = $this->get(route('advancement.ranking', $this->audition)); + $response->assertOk()->assertViewIs('tabulation.advancement.ranking'); + $response->assertSee('Mark entries ranked'); + }); + it('does not show the form if there are unseated entries', function () { + actAsAdmin(); + $newEntry = Entry::factory()->create(['audition_id' => $this->audition->id]); + $response = $this->get(route('advancement.ranking', $this->audition)); + $response->assertOk()->assertViewIs('tabulation.advancement.ranking'); + $response->assertDontSee('Mark entries ranked'); + }); + +}); + +describe('AdvancementController::setAuditionPassers', function () { + beforeEach(function () { + $this->audition = Audition::factory()->create(); + $this->entries = Entry::factory()->count(3)->create(['audition_id' => $this->audition->id]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 34, + 'advancement_total' => 20, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 34, + 'advancement_total' => 10, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 34, + 'advancement_total' => 30, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + }); + it('will not publish advancement with no advancing students', function () { + actAsAdmin(); + $response = $this->post(route('advancement.setAuditionPassers', $this->audition)); + $response->assertRedirect(route('advancement.ranking', $this->audition)); + $response->assertSessionHas('error', 'Cannot publish advancement if no entries advance'); + }); + it('adds appropriate flags to the audition and passing entries', function () { + actAsAdmin(); + $response = $this->post(route('advancement.setAuditionPassers', $this->audition), + [ + 'pass' => [ + $this->entries[0]->id => 'on', + ], + ], + ); + expect($this->audition->fresh()->hasFlag('advancement_published'))->toBeTrue(); + expect($this->entries[0]->fresh()->hasFlag('will_advance'))->toBeTrue(); + expect($this->entries[1]->fresh()->hasFlag('will_advance'))->toBeFalse(); + expect($this->entries[2]->fresh()->hasFlag('will_advance'))->toBeFalse(); + }); +}); + +describe('AdvancementController::clearAuditionPassers', function () { + beforeEach(function () { + $this->audition = Audition::factory()->create(); + $this->entries = Entry::factory()->count(3)->create(['audition_id' => $this->audition->id]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[0]->id, + 'seating_total' => 34, + 'advancement_total' => 20, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[1]->id, + 'seating_total' => 34, + 'advancement_total' => 10, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + EntryTotalScore::create([ + 'entry_id' => $this->entries[2]->id, + 'seating_total' => 34, + 'advancement_total' => 30, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + }); + it('clears passers', function () { + $this->audition->addFlag('advancement_published'); + $this->entries[0]->addFlag('will_advance'); + $this->entries[1]->addFlag('will_advance'); + actAsAdmin(); + $response = $this->delete(route('advancement.clearAuditionPassers', $this->audition)); + expect($this->audition->fresh()->hasFlag('advancement_published'))->toBeFalse(); + expect($this->entries[0]->fresh()->hasFlag('will_advance'))->toBeFalse(); + expect($this->entries[1]->fresh()->hasFlag('will_advance'))->toBeFalse(); + expect($this->entries[2]->fresh()->hasFlag('will_advance'))->toBeFalse(); + }); +}); diff --git a/tests/Feature/app/Http/Controllers/Tabulation/Seating/EnterDoublerDecisionControllerTest.php b/tests/Feature/app/Http/Controllers/Tabulation/Seating/EnterDoublerDecisionControllerTest.php new file mode 100644 index 0000000..7507d83 --- /dev/null +++ b/tests/Feature/app/Http/Controllers/Tabulation/Seating/EnterDoublerDecisionControllerTest.php @@ -0,0 +1,201 @@ +event = Event::factory()->create(); + $this->ASaudition = Audition::factory()->create(['event_id' => $this->event->id, 'name' => 'Alto Sax']); + $this->TSaudition = Audition::factory()->create(['event_id' => $this->event->id, 'name' => 'Tenor Sax']); + $this->BSaudition = Audition::factory()->create(['event_id' => $this->event->id, 'name' => 'Bari Sax']); + $this->student = Student::factory()->create(); + $this->ASentry = Entry::factory()->create([ + 'audition_id' => $this->ASaudition->id, 'student_id' => $this->student->id, + ]); + $this->TSentry = Entry::factory()->create([ + 'audition_id' => $this->TSaudition->id, 'student_id' => $this->student->id, + ]); + $this->BSentry = Entry::factory()->create([ + 'audition_id' => $this->BSaudition->id, 'student_id' => $this->student->id, + ]); + DB::table('entry_total_scores')->insert([ + 'entry_id' => $this->ASentry->id, + 'seating_total' => 34, + 'advancement_total' => 4, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + DB::table('entry_total_scores')->insert([ + 'entry_id' => $this->TSentry->id, + 'seating_total' => 34, + 'advancement_total' => 4, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + // DB::table('entry_total_scores')->insert([ + // 'entry_id' => $this->BSentry->id, + // 'seating_total' => 34, + // 'advancement_total' => 4, + // 'seating_subscore_totals' => json_encode([22, 2]), + // 'advancement_subscore_totals' => json_encode([22, 2]), + // ]); +}); + +it('can mark an entry as a no-show', function () { + actAsAdmin(); + $response = $this->post(route('seating.audition.noshow', [$this->BSaudition->id, $this->BSentry->id])); + $response->assertRedirect()->assertSessionHas('success'); + expect($this->BSentry->fresh()->hasFlag('no_show'))->toBeTrue(); + $response->assertRedirect(route('seating.audition', [$this->BSaudition->id])); +}); + +it('passes exceptions from the enter noshow action', function () { + $this->BSaudition->addFlag('seats_published'); + actAsAdmin(); + $response = $this->post(route('seating.audition.noshow', [$this->BSaudition->id, $this->BSentry->id])); + $response->assertRedirect()->assertSessionHas('error'); + expect($this->BSentry->fresh()->hasFlag('no_show'))->toBeFalse(); +}); + +it('can decline an entry', function () { + actAsAdmin(); + $response = $this->post(route('seating.audition.decline', [$this->TSaudition->id, $this->TSentry->id])); + $response->assertRedirect()->assertSessionHas('success'); + expect($this->TSentry->fresh()->hasFlag('declined'))->toBeTrue(); + $response->assertRedirect(route('seating.audition', [$this->TSaudition->id])); +}); + +it('passes exceptions from the enter decline action', function () { + actAsAdmin(); + $response = $this->post(route('seating.audition.decline', [$this->BSaudition->id, $this->BSentry->id])); + $response->assertRedirect()->assertSessionHas('error'); + expect($this->BSentry->fresh()->hasFlag('declined'))->toBeFalse(); +}); + +it('can accept an entry', function () { + DB::table('entry_total_scores')->insert([ + 'entry_id' => $this->BSentry->id, + 'seating_total' => 34, + 'advancement_total' => 4, + 'seating_subscore_totals' => json_encode([22, 2]), + 'advancement_subscore_totals' => json_encode([22, 2]), + ]); + actAsAdmin(); + $response = $this->post(route('seating.audition.accept', [$this->BSaudition->id, $this->BSentry->id])); + $response->assertRedirect()->assertSessionHas('success'); + $response->assertRedirect(route('seating.audition', [$this->BSaudition->id])); + expect($this->ASentry->fresh()->hasFlag('declined'))->toBeTrue() + ->and($this->TSentry->fresh()->hasFlag('declined'))->toBeTrue() + ->and(Doubler::findDoubler($this->student->id, + $this->event->id)->getAcceptedEntry()->id)->toEqual($this->BSentry->id); +}); + +it('passes exceptions from the enter accept action', function () { + actAsAdmin(); + $response = $this->post(route('seating.audition.accept', [$this->BSaudition->id, $this->BSentry->id])); + $response->assertRedirect()->assertSessionHas('error'); + expect($this->BSentry->fresh()->hasFlag('declined'))->toBeFalse(); + expect($this->TSentry->fresh()->hasFlag('declined'))->toBeFalse(); + expect($this->ASentry->fresh()->hasFlag('declined'))->toBeFalse() + ->and(Doubler::findDoubler($this->student->id, + $this->event->id)->accepted_entry)->toBeNull(); +}); + +it('can mass decline', function () { + $sg = ScoringGuide::factory()->create(); + $audition = Audition::factory()->create(['event_id' => $this->event->id, 'scoring_guide_id' => $sg->id]); + $otherAudition = Audition::factory()->create(['event_id' => $this->event->id, 'scoring_guide_id' => $sg->id]); + + // Scored entry that won't be declining + $entry1 = Entry::factory()->create(['audition_id' => $audition->id, 'student_id' => $this->student->id]); + $entry1Doubler = Entry::factory()->create([ + 'audition_id' => $otherAudition->id, + 'student_id' => $entry1->student->id]); + EntryTotalScore::create([ + 'entry_id' => $entry1->id, + 'seating_total' => 100, + 'advancement_total' => 100, + 'seating_subscore_totals' => json_encode([100, 100]), + 'advancement_subscore_totals' => json_encode([100, 100]), + ]); + + // Create some space + $spacerEntries = Entry::factory()->count(5)->create(['audition_id' => $audition->id]); + foreach ($spacerEntries as $spacerEntry) { + EntryTotalScore::create([ + 'entry_id' => $spacerEntry->id, + 'seating_total' => 95, + 'advancement_total' => 95, + 'seating_subscore_totals' => json_encode([95, 95]), + 'advancement_subscore_totals' => json_encode([95, 95]), + ]); + } + + // Scored entry that will already be declined + $entry2 = Entry::factory()->create(['audition_id' => $audition->id]); + $entry2Doubler = Entry::factory()->create([ + 'audition_id' => $otherAudition->id, + 'student_id' => $entry2->student->id]); + EntryTotalScore::create([ + 'entry_id' => $entry2->id, + 'seating_total' => 90, + 'advancement_total' => 90, + 'seating_subscore_totals' => json_encode([90, 90]), + 'advancement_subscore_totals' => json_encode([90, 90]), + ]); + $entry2->addFlag('declined'); + $entry2->refresh(); + + // Scored entry that is not a doubler + $entry3 = Entry::factory()->create(['audition_id' => $audition->id]); + EntryTotalScore::create([ + 'entry_id' => $entry3->id, + 'seating_total' => 80, + 'advancement_total' => 80, + 'seating_subscore_totals' => json_encode([99, 99]), + 'advancement_subscore_totals' => json_encode([99, 99]), + ]); + + // Scored entry that has accepted + $entry4 = Entry::factory()->create(['audition_id' => $audition->id]); + $entry4Doubler = Entry::factory()->create([ + 'audition_id' => $otherAudition->id, + 'student_id' => $entry4->student->id]); + EntryTotalScore::create([ + 'entry_id' => $entry4->id, + 'seating_total' => 75, + 'advancement_total' => 75, + 'seating_subscore_totals' => json_encode([75, 75]), + 'advancement_subscore_totals' => json_encode([75, 75]), + ]); + EntryTotalScore::create([ + 'entry_id' => $entry4Doubler->id, + 'seating_total' => 75, + 'advancement_total' => 75, + 'seating_subscore_totals' => json_encode([75, 75]), + 'advancement_subscore_totals' => json_encode([75, 75]), + ]); + $doubler = Doubler::findDoubler($entry4->student->id, $audition->event_id); + $doubler->update(['accepted_entry' => $entry4->id]); + $entry4Doubler->addFlag('declined'); + $entry4Doubler->refresh(); + + // ACT + actAsAdmin(); + $response = $this->post(route('seating.audition.mass_decline', [$audition->id]), [ + 'decline-below' => 3, + ]); + $response->assertRedirect(route('seating.audition', [$audition->id])); + expect($entry1->fresh()->hasFlag('declined'))->toBeFalse(); + expect($entry2->fresh()->hasFlag('declined'))->toBeTrue(); + expect($entry3->fresh()->hasFlag('declined'))->toBeFalse(); + expect($entry4->fresh()->hasFlag('declined'))->toBeFalse(); +});