Tets for EnterDoublerDecisionController

This commit is contained in:
Matt Young 2025-07-16 08:27:08 -05:00
parent d09d053b6a
commit 09f4ed6636
5 changed files with 482 additions and 12 deletions

View File

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

View File

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

View File

@ -1,4 +1,5 @@
<x-card.card class="px-3">
<x-card.heading class="-ml-3">Scored Entries</x-card.heading>
<x-table.table>
<thead>
<tr>
@ -66,3 +67,60 @@
</x-table.body>
</x-table.table>
</x-card.card>
<x-card.card class="px-3 mt-3">
<x-card.heading class="-ml-3">Unscored Entries</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Draw #</x-table.th>
<x-table.th>ID</x-table.th>
<x-table.th>Student</x-table.th>
<x-table.th>Judges Scored</x-table.th>
<x-table.th> </x-table.th>
</tr>
</thead>
@foreach($unscoredEntries as $entry)
<tr>
<x-table.td>{{ $entry->draw_number }}</x-table.td>
<x-table.td>{{ $entry->id }}</x-table.td>
<x-table.td>
<div>
<a href="{{ route('admin.students.edit',[$entry->student_id]) }}">{{ $entry->student->full_name() }}</a>
</div>
<div class="text-xs text-gray-400">{{ $entry->student->school->name }}</div>
</x-table.td>
<x-table.td>{{ $entry->scoreSheets->count() }}</x-table.td>
</tr>
@endforeach
</x-table.table>
</x-card.card>
<x-card.card class="px-3 mt-3">
<x-card.heading class="-ml-3">No-Show Entries</x-card.heading>
<x-table.table>
<thead>
<tr>
<x-table.th>Draw #</x-table.th>
<x-table.th>ID</x-table.th>
<x-table.th>Student</x-table.th>
<x-table.th> </x-table.th>
</tr>
</thead>
@foreach($noShowEntries as $entry)
<tr>
<x-table.td>{{ $entry->draw_number }}</x-table.td>
<x-table.td>{{ $entry->id }}</x-table.td>
<x-table.td>
<div>
<a href="{{ route('admin.students.edit',[$entry->student_id]) }}">{{ $entry->student->full_name() }}</a>
</div>
<div class="text-xs text-gray-400">{{ $entry->student->school->name }}</div>
</x-table.td>
</tr>
@endforeach
</x-table.table>
</x-card.card>

View File

@ -0,0 +1,213 @@
<?php
use App\Models\Audition;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\ScoringGuide;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
afterEach(function () {
\Illuminate\Support\Facades\Cache::flush();
});
describe('AdvancementController::status', function () {
beforeEach(function () {
$sg = ScoringGuide::factory()->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();
});
});

View File

@ -0,0 +1,201 @@
<?php
use App\Models\Audition;
use App\Models\Doubler;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\Event;
use App\Models\ScoringGuide;
use App\Models\Student;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->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();
});