diff --git a/app/Http/Controllers/Judging/JudgingController.php b/app/Http/Controllers/Judging/JudgingController.php
index 7318d16..2b2b506 100644
--- a/app/Http/Controllers/Judging/JudgingController.php
+++ b/app/Http/Controllers/Judging/JudgingController.php
@@ -3,22 +3,18 @@
namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterScore;
-use App\Exceptions\AuditionServiceException;
-use App\Exceptions\ScoreEntryException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\JudgeAdvancementVote;
use App\Models\ScoreSheet;
use App\Services\AuditionService;
-use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use function compact;
use function redirect;
-use function url;
class JudgingController extends Controller
{
@@ -40,8 +36,9 @@ class JudgingController extends Controller
public function auditionEntryList(Request $request, Audition $audition)
{
+ // TODO: Add error message if scoring guide is not set
if ($request->user()->cannot('judge', $audition)) {
- return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this audition');
+ return redirect()->route('judging.index')->with('error', 'You are not assigned to judge that audition');
}
$entries = Entry::where('audition_id', '=', $audition->id)->orderBy('draw_number')->with('audition')->get();
$subscores = $audition->scoringGuide->subscores()->orderBy('display_order')->get();
@@ -68,6 +65,13 @@ class JudgingController extends Controller
return redirect()->route('judging.auditionEntryList', $entry->audition)->with('error',
'The requested entry is marked as a no-show. Scores cannot be entered.');
}
+
+ // Turn away users if the entry is flagged as a failed-prelim
+ if ($entry->hasFlag('failed_prelim')) {
+ return redirect()->route('judging.auditionEntryList', $entry->audition)->with('error',
+ 'The requested entry is marked as having failed a prelim. Scores cannot be entered.');
+ }
+
$oldSheet = ScoreSheet::where('user_id', Auth::id())->where('entry_id', $entry->id)->value('subscores') ?? null;
$oldVote = JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->first();
$oldVote = $oldVote ? $oldVote->vote : 'noVote';
@@ -78,15 +82,11 @@ class JudgingController extends Controller
public function saveScoreSheet(Request $request, Entry $entry, EnterScore $enterScore)
{
if ($request->user()->cannot('judge', $entry->audition)) {
- abort(403, 'You are not assigned to judge this entry');
+ return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this entry');
}
// Validate form data
- try {
- $subscores = $this->auditionService->getSubscores($entry->audition, 'all');
- } catch (AuditionServiceException $e) {
- return redirect()->back()->with('error', 'Unable to get subscores - '.$e->getMessage());
- }
+ $subscores = $entry->audition->subscoreDefinitions;
$validationChecks = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
@@ -94,16 +94,13 @@ class JudgingController extends Controller
$validatedData = $request->validate($validationChecks);
// Enter the score
- try {
- $enterScore(Auth::user(), $entry, $validatedData['score']);
- } catch (ScoreEntryException $e) {
- return redirect()->back()->with('error', 'Error saving score - '.$e->getMessage());
- }
+ /** @noinspection PhpUnhandledExceptionInspection */
+ $enterScore(Auth::user(), $entry, $validatedData['score']);
// Deal with an advancement vote if needed
$this->advancementVote($request, $entry);
- return redirect('/judging/audition/'.$entry->audition_id)->with('success',
+ return redirect(route('judging.auditionEntryList', $entry->audition))->with('success',
'Entered scores for '.$entry->audition->name.' '.$entry->draw_number);
}
@@ -111,8 +108,10 @@ class JudgingController extends Controller
public function updateScoreSheet(Request $request, Entry $entry, EnterScore $enterScore)
{
if ($request->user()->cannot('judge', $entry->audition)) {
- abort(403, 'You are not assigned to judge this entry');
+ return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this entry');
}
+
+ // We can't update a scoresheet that doesn't exist
$scoreSheet = ScoreSheet::where('user_id', Auth::id())->where('entry_id', $entry->id)->first();
if (! $scoreSheet) {
return redirect()->back()->with('error', 'Attempt to edit non existent score sheet');
@@ -120,11 +119,8 @@ class JudgingController extends Controller
Gate::authorize('update', $scoreSheet);
// Validate form data
- try {
- $subscores = $this->auditionService->getSubscores($entry->audition, 'all');
- } catch (AuditionServiceException $e) {
- return redirect()->back()->with('error', 'Error getting subscores - '.$e->getMessage());
- }
+
+ $subscores = $entry->audition->subscoreDefinitions;
$validationChecks = [];
foreach ($subscores as $subscore) {
@@ -133,38 +129,29 @@ class JudgingController extends Controller
$validatedData = $request->validate($validationChecks);
// Enter the score
- try {
- $enterScore(Auth::user(), $entry, $validatedData['score'], $scoreSheet);
- } catch (ScoreEntryException $e) {
- return redirect()->back()->with('error', 'Error updating score - '.$e->getMessage());
- }
+
+ $enterScore(Auth::user(), $entry, $validatedData['score'], $scoreSheet);
$this->advancementVote($request, $entry);
- return redirect('/judging/audition/'.$entry->audition_id)->with('success',
+ return redirect(route('judging.auditionEntryList', $entry->audition))->with('success',
'Updated scores for '.$entry->audition->name.' '.$entry->draw_number);
}
protected function advancementVote(Request $request, Entry $entry)
{
- if ($request->user()->cannot('judge', $entry->audition)) {
- abort(403, 'You are not assigned to judge this entry');
- }
-
if ($entry->for_advancement and auditionSetting('advanceTo')) {
$request->validate([
'advancement-vote' => ['required', 'in:yes,no,dq'],
]);
- try {
- JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->delete();
- JudgeAdvancementVote::create([
- 'user_id' => Auth::user()->id,
- 'entry_id' => $entry->id,
- 'vote' => $request->input('advancement-vote'),
- ]);
- } catch (Exception) {
- return redirect(url()->previous())->with('error', 'Error saving advancement vote');
- }
+
+ JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->delete();
+ JudgeAdvancementVote::create([
+ 'user_id' => Auth::user()->id,
+ 'entry_id' => $entry->id,
+ 'vote' => $request->input('advancement-vote'),
+ ]);
+
}
return null;
diff --git a/app/Models/Audition.php b/app/Models/Audition.php
index 5070235..6c86d5f 100644
--- a/app/Models/Audition.php
+++ b/app/Models/Audition.php
@@ -55,6 +55,29 @@ class Audition extends Model
return $this->belongsTo(ScoringGuide::class);
}
+ public function subscoreDefinitions()
+ {
+ // TODO: Consider cache. Look at how often this is called.
+ return $this->hasManyThrough(
+ SubscoreDefinition::class, // Final related model
+ ScoringGuide::class, // Intermediate model
+ 'id', // Foreign key on ScoringGuide table (primary key)
+ 'scoring_guide_id', // Foreign key on SubscoreDefinition table
+ 'scoring_guide_id', // Foreign key on Audition table (local key)
+ 'id' // Local key on ScoringGuide table (primary key)
+ );
+ }
+
+ public function getSeatingSubscores()
+ {
+ return $this->subscoreDefinitions()->where('for_seating', '1')->orderBy('display_order')->get();
+ }
+
+ public function getAdvancementSubscores()
+ {
+ return $this->subscoreDefinitions()->where('for_advance', '1')->orderBy('display_order')->get();
+ }
+
public function bonusScore(): BelongsToMany
{
return $this->belongsToMany(BonusScoreDefinition::class, 'bonus_score_audition_assignment');
diff --git a/resources/views/judging/entry_score_sheet.blade.php b/resources/views/judging/entry_score_sheet.blade.php
index 58fec8c..e4ef21a 100644
--- a/resources/views/judging/entry_score_sheet.blade.php
+++ b/resources/views/judging/entry_score_sheet.blade.php
@@ -11,7 +11,7 @@
- All Scores must be complete
- You may enter zero
- - Whole numbrers only
+ - Whole numbers only
diff --git a/tests/Feature/app/Http/Controllers/Judging/JudgingControllerTest.php b/tests/Feature/app/Http/Controllers/Judging/JudgingControllerTest.php
new file mode 100644
index 0000000..38a855d
--- /dev/null
+++ b/tests/Feature/app/Http/Controllers/Judging/JudgingControllerTest.php
@@ -0,0 +1,284 @@
+get(route('judging.index'));
+ $response->assertRedirect(route('dashboard'));
+ $response->assertSessionHas('error', 'You are not assigned to judge.');
+ });
+ it('shows a dashboard showing rooms and bonus scores the user is assigned to judge', function () {
+ $judge = User::factory()->create();
+ $room = Room::factory()->create();
+ $room->judges()->attach($judge);
+ $bonusScoreDefinition = BonusScoreDefinition::factory()->create();
+ $bonusScoreDefinition->judges()->attach($judge);
+ $response = $this->actingAs($judge)->get(route('judging.index'));
+ $response->assertOk();
+ $response->assertSee($room->name);
+ $response->assertSee($bonusScoreDefinition->name);
+ });
+});
+
+describe('JudgingController::auditionEntryList', function () {
+ it('denies access to non-judges', function () {
+ actAsNormal();
+ $audition = Audition::factory()->create();
+ $response = $this->get(route('judging.auditionEntryList', $audition->id));
+ $response->assertRedirect(route('dashboard'));
+ $response->assertSessionHas('error', 'You are not assigned to judge.');
+ });
+ it('denies access if were not assigned to the auditions room', function () {
+ $judge = User::factory()->create();
+ $room = Room::factory()->create();
+ $otherRoom = Room::factory()->create();
+ $otherRoom->judges()->attach($judge);
+ $audition = Audition::factory()->create(['room_id' => $room->id]);
+ $response = $this->actingAs($judge)->get(route('judging.auditionEntryList', $audition->id));
+ $response->assertRedirect(route('judging.index'));
+ $response->assertSessionHas('error', 'You are not assigned to judge that audition');
+ });
+ it('gives us an entry list from which we may select an entry to score', function () {
+ $scoringGuide = ScoringGuide::factory()->create();
+ $judge = User::factory()->create();
+ $room = Room::factory()->create();
+ $room->judges()->attach($judge);
+ $audition = Audition::factory()->create(['room_id' => $room->id, 'scoring_guide_id' => $scoringGuide->id]);
+ $entries = Entry::factory()->count(5)->forAudition($audition)->create();
+ $response = $this->actingAs($judge)->get(route('judging.auditionEntryList', $audition->id));
+ $response->assertOk();
+ $response->assertViewIs('judging.audition_entry_list');
+ $response->assertViewHas('audition', $audition);
+ $response->assertViewHas('entries', $entries);
+ foreach ($entries as $entry) {
+ $response->assertSee($entry->audition->name.' '.$entry->draw_number);
+ $response->assertDontSee($entry->student->full_name());
+ $response->assertDontSee($entry->student->full_name(true));
+ }
+ foreach (SubscoreDefinition::all() as $subscoreDefinition) {
+ $response->assertSee($subscoreDefinition->name);
+ }
+ });
+});
+
+describe('JudgingController::entryScoreSheet', function () {
+ it('denies access to non-judges', function () {
+ actAsNormal();
+ $entry = Entry::factory()->create();
+ $response = $this->get(route('judging.entryScoreSheet', $entry));
+ $response->assertRedirect(route('dashboard'));
+ $response->assertSessionHas('error', 'You are not assigned to judge.');
+ });
+
+ it('denies access if were not assigned to the auditions room', function () {
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $entry = Entry::factory()->create();
+ $this->actingAs($judge);
+ $response = $this->get(route('judging.entryScoreSheet', $entry));
+ $response->assertRedirect(route('judging.index'));
+ $response->assertSessionHas('error', 'You are not assigned to judge this entry');
+ });
+
+ it('denies access if the audition is published', function () {
+ $room = Room::factory()->create();
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $audition = Audition::factory()->create(['room_id' => $room->id]);
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $audition->addFlag('seats_published');
+ $this->actingAs($judge);
+ $response = $this->get(route('judging.entryScoreSheet', $entry));
+ $response->assertRedirect(route('judging.auditionEntryList', $audition));
+ $response->assertSessionHas('error', 'Scores for entries in published auditions cannot be modified');
+ });
+
+ it('denies access if the entry is flagged as a no-show', function () {
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $audition = Audition::factory()->create(['room_id' => $room->id]);
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $entry->addFlag('no_show');
+ $this->actingAs($judge);
+ $response = $this->get(route('judging.entryScoreSheet', $entry));
+ $response->assertRedirect(route('judging.auditionEntryList', $audition));
+ $response->assertSessionHas('error', 'The requested entry is marked as a no-show. Scores cannot be entered.');
+ });
+
+ it('denies access if the entry is flagged as a failed-prelim', function () {
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $audition = Audition::factory()->create(['room_id' => $room->id]);
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $entry->addFlag('failed_prelim');
+ $this->actingAs($judge);
+ $response = $this->get(route('judging.entryScoreSheet', $entry));
+ $response->assertRedirect(route('judging.auditionEntryList', $audition));
+ $response->assertSessionHas('error',
+ 'The requested entry is marked as having failed a prelim. Scores cannot be entered.');
+ });
+
+ it('gives us a form to enter a score for an entry', function () {
+ $scoringGuide = ScoringGuide::factory()->create();
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $audition = Audition::factory()->create(['room_id' => $room->id, 'scoring_guide_id' => $scoringGuide->id]);
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $this->actingAs($judge);
+ $response = $this->get(route('judging.entryScoreSheet', $entry));
+ $response->assertOk();
+ $response->assertViewIs('judging.entry_score_sheet');
+ $response->assertDontSee($entry->student->full_name());
+ foreach (SubscoreDefinition::all() as $subscoreDefinition) {
+ $response->assertSee($subscoreDefinition->name);
+ $response->assertSee('score['.$subscoreDefinition->id.']');
+ $response->assertSee('max: '.$subscoreDefinition->maximum_score);
+ }
+ });
+});
+
+describe('JudgingController::saveScoreSheet', function () {
+ it('denies access to non-judges', function () {
+ actAsNormal();
+ $audition = Audition::factory()->create();
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $response = $this->post(route('judging.saveScoreSheet', $entry));
+ $response->assertRedirect(route('dashboard'));
+ $response->assertSessionHas('error', 'You are not assigned to judge.');
+ });
+
+ it('denies access to judges not assigned to the audition', function () {
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $audition = Audition::factory()->create();
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $this->actingAs($judge);
+ $response = $this->post(route('judging.saveScoreSheet', $entry));
+ $response->assertRedirect(route('judging.index'));
+ $response->assertSessionHas('error', 'You are not assigned to judge this entry');
+ });
+
+ it('saves a score sheet', function () {
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $scoringGuide = ScoringGuide::factory()->create();
+ $audition = Audition::factory()->create(['room_id' => $room->id, 'scoring_guide_id' => $scoringGuide->id]);
+ $subscoreIds = SubscoreDefinition::all()->pluck('id');
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $submitData = [
+ 'score' => [
+ $subscoreIds[0] => 10,
+ $subscoreIds[1] => 20,
+ $subscoreIds[2] => 30,
+ $subscoreIds[3] => 40,
+ $subscoreIds[4] => 50,
+ ],
+ 'advancement-vote' => 'yes',
+ ];
+ $this->actingAs($judge);
+ $response = $this->post(route('judging.saveScoreSheet', $entry), $submitData);
+ $response->assertRedirect(route('judging.auditionEntryList', $audition));
+ $response->assertSessionHas('success');
+ expect(ScoreSheet::where('entry_id', $entry->id)->first())->toBeInstanceOf(ScoreSheet::class)
+ ->and(JudgeAdvancementVote::where('entry_id',
+ $entry->id)->first())->toBeInstanceOf(JudgeAdvancementVote::class)
+ ->and(JudgeAdvancementVote::first()->vote)->toBe('yes');
+ });
+});
+
+describe('JudgingController::updateScoreSheet', function () {
+ it('denies access to non-judges', function () {
+ actAsNormal();
+ $audition = Audition::factory()->create();
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $response = $this->patch(route('judging.updateScoreSheet', $entry));
+ $response->assertRedirect(route('dashboard'));
+ $response->assertSessionHas('error', 'You are not assigned to judge.');
+ });
+ it('denies access to judges not assigned to the audition', function () {
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $audition = Audition::factory()->create();
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $this->actingAs($judge);
+ $response = $this->patch(route('judging.updateScoreSheet', $entry));
+ $response->assertRedirect(route('judging.index'));
+ $response->assertSessionHas('error', 'You are not assigned to judge this entry');
+ });
+ it('will not update a non-existent score sheet', function () {
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $audition = Audition::factory()->create(['room_id' => $room->id]);
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $this->actingAs($judge);
+ $response = $this->patch(route('judging.updateScoreSheet', $entry));
+ $response->assertRedirect()->assertSessionHas('error', 'Attempt to edit non existent score sheet');
+ });
+ it('will update a score sheet', function () {
+ $room = Room::factory()->create();
+ $judge = User::factory()->create();
+ $room->addJudge($judge);
+ $scoringGuide = ScoringGuide::factory()->create();
+ $audition = Audition::factory()->create(['room_id' => $room->id, 'scoring_guide_id' => $scoringGuide->id]);
+ $subscoreIds = SubscoreDefinition::all()->pluck('id');
+ $entry = Entry::factory()->forAudition($audition)->create();
+ $submitData = [
+ 'score' => [
+ $subscoreIds[0] => 10,
+ $subscoreIds[1] => 20,
+ $subscoreIds[2] => 30,
+ $subscoreIds[3] => 40,
+ $subscoreIds[4] => 50,
+ ],
+ 'advancement-vote' => 'yes',
+ ];
+ $this->actingAs($judge);
+ $this->post(route('judging.saveScoreSheet', $entry), $submitData);
+ $newSubmitData = [
+ 'score' => [
+ $subscoreIds[0] => 5,
+ $subscoreIds[1] => 15,
+ $subscoreIds[2] => 25,
+ $subscoreIds[3] => 35,
+ $subscoreIds[4] => 45,
+ ],
+ 'advancement-vote' => 'no',
+ ];
+ $response = $this->patch(route('judging.updateScoreSheet', $entry), $newSubmitData);
+ $response->assertRedirect(route('judging.auditionEntryList', $audition))
+ ->assertSessionHas('success');
+ expect(ScoreSheet::count())->toEqual(1);
+ $ss = ScoreSheet::first();
+ expect($ss->getSubscore($subscoreIds[0]))->toEqual(5);
+ expect($ss->getSubscore($subscoreIds[1]))->toEqual(15);
+ expect($ss->getSubscore($subscoreIds[2]))->toEqual(25);
+ expect($ss->getSubscore($subscoreIds[3]))->toEqual(35);
+ expect($ss->getSubscore($subscoreIds[4]))->toEqual(45);
+ expect(JudgeAdvancementVote::count())->toEqual(1);
+ $vote = JudgeAdvancementVote::first();
+ expect($vote->vote)->toEqual('no');
+
+ });
+});
diff --git a/tests/Feature/app/Models/AuditionTest.php b/tests/Feature/app/Models/AuditionTest.php
index 8b41be1..786dc1b 100644
--- a/tests/Feature/app/Models/AuditionTest.php
+++ b/tests/Feature/app/Models/AuditionTest.php
@@ -4,8 +4,10 @@ use App\Models\Audition;
use App\Models\Ensemble;
use App\Models\Entry;
use App\Models\Room;
+use App\Models\ScoringGuide;
use App\Models\Seat;
use App\Models\SeatingLimit;
+use App\Models\SubscoreDefinition;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
@@ -62,6 +64,33 @@ it('can return its scoring guide if one is set', function () {
expect($this->audition->scoringGuide)->toBeInstanceOf(App\Models\ScoringGuide::class);
});
+it('can return the subscore definitions for its scoring guide if one is set', function () {
+ expect($this->audition->scoringGuide)->toBeNull();
+ $guide = ScoringGuide::factory()->create();
+ $subscores = SubscoreDefinition::orderBy('display_order')->get();
+ $n = 1;
+ foreach ($subscores as $subscore) {
+ $subscore->update(['display_order' => $n]);
+ $n++;
+ }
+ $subscores[0]->update(['for_seating' => 0]);
+ $subscores[4]->update(['for_advance' => 0]);
+ $this->audition->scoringGuide()->associate($guide);
+ $this->audition->save();
+ expect($this->audition->scoringGuide)->toBeInstanceOf(ScoringGuide::class)
+ ->and($this->audition->getSeatingSubscores()->contains('id', $subscores[0]->id))->toBeFalse()
+ ->and($this->audition->getSeatingSubscores()->contains('id', $subscores[1]->id))->toBeTrue()
+ ->and($this->audition->getSeatingSubscores()->contains('id', $subscores[2]->id))->toBeTrue()
+ ->and($this->audition->getSeatingSubscores()->contains('id', $subscores[3]->id))->toBeTrue()
+ ->and($this->audition->getSeatingSubscores()->contains('id', $subscores[4]->id))->toBeTrue()
+ ->and($this->audition->getAdvancementSubscores()->contains('id', $subscores[4]->id))->toBeFalse()
+ ->and($this->audition->getAdvancementSubscores()->contains('id', $subscores[3]->id))->toBeTrue()
+ ->and($this->audition->getAdvancementSubscores()->contains('id', $subscores[2]->id))->toBeTrue()
+ ->and($this->audition->getAdvancementSubscores()->contains('id', $subscores[1]->id))->toBeTrue()
+ ->and($this->audition->getAdvancementSubscores()->contains('id', $subscores[0]->id))->toBeTrue();
+
+});
+
it('can return its bonus score definition if one is set', function () {
expect($this->audition->bonusScore()->count())->toBe(0);
$definition = App\Models\BonusScoreDefinition::factory()->create();