diff --git a/app/Actions/Development/FakeScoresForEntry.php b/app/Actions/Development/FakeScoresForEntry.php new file mode 100644 index 0000000..4125178 --- /dev/null +++ b/app/Actions/Development/FakeScoresForEntry.php @@ -0,0 +1,28 @@ +audition->scoringGuide; + $subscores = $scoringGuide->subscores; + $judges = $entry->audition->judges; + foreach ($judges as $judge) { + $scoringArray = []; + foreach ($subscores as $subscore) { + $scoringArray[$subscore->id] = mt_rand(0, $subscore->max_score); + } + $scoreScribe($judge, $entry, $scoringArray); + } + } +} diff --git a/app/Actions/Entries/CreateEntry.php b/app/Actions/Entries/CreateEntry.php index 971b947..175d85a 100644 --- a/app/Actions/Entries/CreateEntry.php +++ b/app/Actions/Entries/CreateEntry.php @@ -28,7 +28,7 @@ class CreateEntry /** * @throws ManageEntryException */ - public function createEntry(Student|int $student, Audition|int $audition, string|array|null $entry_for = null) + public function createEntry(Student|int $student, Audition|int $audition, string|array|null $entry_for = null): Entry { if (is_int($student)) { $student = Student::find($student); diff --git a/app/Actions/Entries/DoublerDecision.php b/app/Actions/Entries/DoublerDecision.php index 54158be..77828fe 100644 --- a/app/Actions/Entries/DoublerDecision.php +++ b/app/Actions/Entries/DoublerDecision.php @@ -27,11 +27,6 @@ class DoublerDecision 'decline' => $this->decline($entry), default => throw new AuditionAdminException('Invalid decision specified') }; - - if ($decision != 'accept' && $decision != 'decline') { - throw new AuditionAdminException('Invalid decision specified'); - } - } /** @@ -56,9 +51,6 @@ class DoublerDecision if ($entry->audition->hasFlag('seats_published')) { throw new AuditionAdminException('Cannot accept an entry in an audition where seats are published'); } - if ($entry->audition->hasFlag('advancement_published')) { - throw new AuditionAdminException('Cannot accept an entry in an audition where advancement is published'); - } Cache::forget('rank_seating_'.$entry->audition_id); // Process student entries @@ -87,6 +79,13 @@ class DoublerDecision if ($entry->hasFlag('declined')) { throw new AuditionAdminException('Entry '.$entry->id.' is already declined'); } + if (! $entry->totalScore) { + throw new AuditionAdminException('Cannot decline an unscored entry'); + } + if ($entry->audition->hasFlag('seats_published')) { + throw new AuditionAdminException('Cannot decline an entry in an audition where seats are published'); + } + // Flag this entry $entry->addFlag('declined'); diff --git a/database/factories/ScoringGuideFactory.php b/database/factories/ScoringGuideFactory.php index 9efa3d0..87ca4fb 100644 --- a/database/factories/ScoringGuideFactory.php +++ b/database/factories/ScoringGuideFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use App\Models\SubscoreDefinition; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -20,4 +21,18 @@ class ScoringGuideFactory extends Factory 'name' => $this->faker->sentence(3), ]; } + + /** + * Configure the model factory. + * + * @return $this + */ + public function configure() + { + return $this->afterCreating(function ($scoringGuide) { + SubscoreDefinition::factory() + ->count(5) + ->create(['scoring_guide_id' => $scoringGuide->id]); + }); + } } diff --git a/database/factories/SubscoreDefinitionFactory.php b/database/factories/SubscoreDefinitionFactory.php index b84d521..55486db 100644 --- a/database/factories/SubscoreDefinitionFactory.php +++ b/database/factories/SubscoreDefinitionFactory.php @@ -17,10 +17,13 @@ class SubscoreDefinitionFactory extends Factory */ public function definition(): array { - $sg = ScoringGuide::factory()->create(); return [ - 'scoring_guide_id' => $sg->id, + 'scoring_guide_id' => function (array $attributes) { + return array_key_exists('scoring_guide_id', $attributes) + ? $attributes['scoring_guide_id'] + : ScoringGuide::factory()->create()->id; + }, 'name' => $this->faker->word, 'max_score' => 100, 'weight' => $this->faker->numberBetween(1, 4), @@ -30,6 +33,7 @@ class SubscoreDefinitionFactory extends Factory 'for_advance' => 1, ]; } + public function seatingOnly(): self { return $this->state( @@ -57,5 +61,4 @@ class SubscoreDefinitionFactory extends Factory fn (array $attributes) => ['tiebreak_order' => 0] ); } - } diff --git a/tests/Feature/app/Actions/Entries/DoublerDecisionTest.php b/tests/Feature/app/Actions/Entries/DoublerDecisionTest.php index 26b7e1b..d1e8b98 100644 --- a/tests/Feature/app/Actions/Entries/DoublerDecisionTest.php +++ b/tests/Feature/app/Actions/Entries/DoublerDecisionTest.php @@ -1,11 +1,191 @@ get('/'); +beforeEach(function () { + $this->decider = app(DoublerDecision::class); + $this->entryScribe = app(CreateEntry::class); + $this->scoreFaker = app(FakeScoresForEntry::class); - $response->assertStatus(200); + // Setup doubler + $this->scoringGuide = ScoringGuide::factory()->create(); + $this->judge1 = User::factory()->create(); + $this->judge2 = User::factory()->create(); + $this->room = Room::factory()->create(); + $this->room->addJudge($this->judge1); + $this->room->addJudge($this->judge2); + $this->audition1 = Audition::factory()->create( + [ + 'minimum_grade' => 9, 'maximum_grade' => 12, + 'scoring_guide_id' => $this->scoringGuide->id, + 'room_id' => $this->room->id, + 'order_in_room' => 1, + 'name' => 'Flute', + ]); + $this->audition2 = Audition::factory()->create( + [ + 'minimum_grade' => 9, 'maximum_grade' => 12, + 'scoring_guide_id' => $this->scoringGuide->id, + 'room_id' => $this->room->id, + 'order_in_room' => 2, + 'event_id' => $this->audition1->event_id, + 'name' => 'Trumpet', + ]); + $this->audition3 = Audition::factory()->create( + [ + 'minimum_grade' => 9, 'maximum_grade' => 12, + 'scoring_guide_id' => $this->scoringGuide->id, + 'room_id' => $this->room->id, + 'order_in_room' => 3, + 'event_id' => $this->audition1->event_id, + 'name' => 'Trombone', + ]); + $this->otherEventAudition = Audition::factory()->create([ + 'minimum_grade' => 9, 'maximum_grade' => 12, + 'scoring_guide_id' => $this->scoringGuide->id, + 'room_id' => $this->room->id, + 'order_in_room' => 4, + 'name' => 'Jazz Trumpet', + ]); + $this->student = Student::factory()->create([ + 'grade' => 9, 'first_name' => 'Percy', + 'last_name' => 'Grainger', + ]); + $this->entry1 = $this->entryScribe->createEntry($this->student, $this->audition1); + $this->entry2 = $this->entryScribe->createEntry($this->student, $this->audition2); + $this->entry3 = $this->entryScribe->createEntry($this->student, $this->audition3); + $this->otherEventEntry = $this->entryScribe->createEntry($this->student, $this->otherEventAudition); }); + +it('is invokable', function () { + + ($this->scoreFaker)($this->entry1); + ($this->scoreFaker)($this->entry2); + ($this->scoreFaker)($this->entry3); + ($this->decider)($this->entry2, 'decline'); + expect($this->entry2->hasFlag('declined'))->toBeTruthy(); +}); + +it('cannot be called with an invalid decision', function () { + + ($this->scoreFaker)($this->entry1); + ($this->scoreFaker)($this->entry2); + ($this->scoreFaker)($this->entry3); + ($this->decider)($this->entry2, 'idunno'); +})->throws(AuditionAdminException::class, 'Invalid decision specified'); + +it('can decline an entry', function () { + + ($this->scoreFaker)($this->entry1); + ($this->scoreFaker)($this->entry2); + ($this->scoreFaker)($this->entry3); + $this->decider->decline($this->entry2); + expect($this->entry2->hasFlag('declined'))->toBeTruthy(); +}); + +it('will not decline an entry with no scores', function () { + $this->decider->decline($this->entry2); +})->throws(AuditionAdminException::class, 'Cannot decline an unscored entry'); + +it('will not decline an entry that is already declined', function () { + ($this->scoreFaker)($this->entry2); + $this->entry2->addFlag('declined'); + $this->decider->decline($this->entry2); +})->throws(AuditionAdminException::class, 'Entry 2 is already declined'); + +it('will not decline an entry in a published event', function () { + ($this->scoreFaker)($this->entry2); + $this->audition2->addFlag('seats_published'); + $this->entry2->refresh(); + $this->decider->decline($this->entry2); +})->throws(AuditionAdminException::class, 'Cannot decline an entry in an audition where seats are published'); + +it('accepts an entry and declines others in the same event', function () { + ($this->scoreFaker)($this->entry1); + ($this->scoreFaker)($this->entry2); + ($this->scoreFaker)($this->entry3); + $this->decider->accept($this->entry2); + $this->entry1->refresh(); + $this->entry2->refresh(); + $this->entry3->refresh(); + expect($this->entry1->hasFlag('declined'))->toBeTruthy() + ->and($this->entry3->hasFlag('declined'))->toBeTruthy(); + $doubler = Doubler::findDoubler($this->entry2->student_id, $this->audition2->event_id); + expect($doubler->accepted_entry)->toBe($this->entry2->id); +}); + +it('will not accept an entry into an event with seats published', function () { + ($this->scoreFaker)($this->entry1); + ($this->scoreFaker)($this->entry2); + ($this->scoreFaker)($this->entry3); + $this->audition2->addFlag('seats_published'); + $this->audition2->refresh(); + $this->entry2->refresh(); + $this->decider->accept($this->entry2); + $this->entry1->refresh(); + $this->entry2->refresh(); + $this->entry3->refresh(); + expect($this->entry1->hasFlag('declined'))->toBeTruthy() + ->and($this->entry3->hasFlag('declined'))->toBeTruthy(); + $doubler = Doubler::findDoubler($this->entry2->student_id, $this->audition2->event_id); + expect($doubler->accepted_entry)->toBe($this->entry2->id); +})->throws(AuditionAdminException::class, 'Cannot accept an entry in an audition where seats are published'); + +it('will not accept an entry that has already been declined', function () { + ($this->scoreFaker)($this->entry1); + ($this->scoreFaker)($this->entry2); + ($this->scoreFaker)($this->entry3); + $this->entry2->addFlag('declined'); + $this->decider->accept($this->entry2); + $this->entry1->refresh(); + $this->entry2->refresh(); + $this->entry3->refresh(); + expect($this->entry1->hasFlag('declined'))->toBeTruthy() + ->and($this->entry3->hasFlag('declined'))->toBeTruthy(); + $doubler = Doubler::findDoubler($this->entry2->student_id, $this->audition2->event_id); + expect($doubler->accepted_entry)->toBe($this->entry2->id); +})->throws(AuditionAdminException::class, 'Entry 2 is already declined'); + +it('when accepting a seat, does not decline no_show or failed_prelim entries', function () { + ($this->scoreFaker)($this->entry2); + $this->entry1->addFlag('no_show'); + $this->entry3->addFlag('failed_prelim'); + $this->decider->accept($this->entry2); + $this->entry1->refresh(); + $this->entry2->refresh(); + $this->entry3->refresh(); + expect($this->entry1->hasFlag('declined'))->toBeFalsy() + ->and($this->entry3->hasFlag('declined'))->toBeFalsy(); + $doubler = Doubler::findDoubler($this->entry2->student_id, $this->audition2->event_id); + expect($doubler->accepted_entry)->toBe($this->entry2->id); +}); + +it('when accepting an entry, does not decline seats in other events', function () { + ($this->scoreFaker)($this->entry1); + ($this->scoreFaker)($this->entry2); + ($this->scoreFaker)($this->entry3); + $this->decider->accept($this->entry2); + $this->entry1->refresh(); + $this->entry2->refresh(); + $this->entry3->refresh(); + expect($this->otherEventEntry->hasFlag('declined'))->toBeFalsy(); +}); + +it('will not accept an entry if the student has unscored entries', function () { + $this->decider->accept($this->entry2); +})->throws(AuditionAdminException::class, + 'Cannot accept seating for Percy Grainger because student has unscored entries');