diff --git a/app/Actions/Tabulation/EnterPrelimScore.php b/app/Actions/Tabulation/EnterPrelimScore.php
new file mode 100644
index 0000000..bf9d0c5
--- /dev/null
+++ b/app/Actions/Tabulation/EnterPrelimScore.php
@@ -0,0 +1,128 @@
+id)->exists()) {
+ throw new AuditionAdminException('User does not exist');
+ }
+ if (! Entry::where('id', $entry->id)->exists()) {
+ throw new AuditionAdminException('Entry does not exist');
+ }
+ if ($entry->audition->hasFlag('seats_published')) {
+ throw new AuditionAdminException('Cannot score an entry in an audition where seats are published');
+ }
+
+ // Check if the entries audition has a prelim definition
+ if (! PrelimDefinition::where('audition_id', $entry->audition->id)->exists()) {
+ throw new AuditionAdminException('The entries audition does not have a prelim');
+ }
+ $prelimDefinition = PrelimDefinition::where('audition_id', $entry->audition->id)->first();
+
+ // Check that the specified user is assigned to judge this entry in prelims
+ $check = DB::table('room_user')
+ ->where('user_id', $user->id)
+ ->where('room_id', $prelimDefinition->room_id)->exists();
+ if (! $check) {
+ throw new AuditionAdminException('This judge is not assigned to judge this entry in prelims');
+ }
+
+ // Check if a score already exists
+ if (! $prelimScoreSheet) {
+ if (PrelimScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
+ throw new AuditionAdminException('That judge has already entered a prelim score for that entry');
+ }
+ } else {
+ if ($prelimScoreSheet->user_id != $user->id) {
+ throw new AuditionAdminException('Existing score sheet is from a different judge');
+ }
+ if ($prelimScoreSheet->entry_id != $entry->id) {
+ throw new AuditionAdminException('Existing score sheet is for a different entry');
+ }
+ }
+
+ // Check the validity of submitted subscores, format array for storage, and sum score
+ $subscoresRequired = $prelimDefinition->scoringGuide->subscores;
+ $subscoresStorageArray = [];
+ $totalScore = 0;
+ $maxPossibleTotal = 0;
+ if ($scores->count() !== $subscoresRequired->count()) {
+ throw new AuditionAdminException('Invalid number of scores');
+ }
+
+ foreach ($subscoresRequired as $subscore) {
+ // check that there is an element in the $scores collection with the key = $subscore->id
+ if (! $scores->keys()->contains($subscore->id)) {
+ throw new AuditionAdminException('Invalid Score Submission');
+ }
+
+ if ($scores[$subscore->id] > $subscore->max_score) {
+ throw new AuditionAdminException('Supplied subscore exceeds maximum allowed');
+ }
+
+ // Add subscore to the storage array
+ $subscoresStorageArray[$subscore->id] = [
+ 'score' => $scores[$subscore->id],
+ 'subscore_id' => $subscore->id,
+ 'subscore_name' => $subscore->name,
+ ];
+
+ // Multiply subscore by weight then add to total
+ $totalScore += ($subscore->weight * $scores[$subscore->id]);
+ $maxPossibleTotal += ($subscore->weight * $subscore->max_score);
+ }
+ $finalTotalScore = ($maxPossibleTotal === 0) ? 0 : (($totalScore / $maxPossibleTotal) * 100);
+
+ $entry->removeFlag('no_show');
+ if ($prelimScoreSheet instanceof PrelimScoreSheet) {
+ $prelimScoreSheet->update([
+ 'user_id' => $user->id,
+ 'entry_id' => $entry->id,
+ 'subscores' => $subscoresStorageArray,
+ 'total' => $finalTotalScore,
+ ]);
+ } else {
+ $prelimScoreSheet = PrelimScoreSheet::create([
+ 'user_id' => $user->id,
+ 'entry_id' => $entry->id,
+ 'subscores' => $subscoresStorageArray,
+ 'total' => $finalTotalScore,
+ ]);
+ }
+
+ // Log the prelim score entry
+ $log_message = 'Entered prelim score for entry id '.$entry->id.'.
';
+ $log_message .= 'Judge: '.$user->full_name().'
';
+ foreach ($prelimScoreSheet->subscores as $subscore) {
+ $log_message .= $subscore['subscore_name'].': '.$subscore['score'].'
';
+ }
+ $log_message .= 'Total :'.$prelimScoreSheet->total.'
';
+ auditionLog($log_message, [
+ 'entries' => [$entry->id],
+ 'users' => [$user->id],
+ 'auditions' => [$entry->audition_id],
+ 'students' => [$entry->student_id],
+ ]);
+
+ return $prelimScoreSheet;
+ }
+}
diff --git a/app/Models/Entry.php b/app/Models/Entry.php
index 9cbc7c3..ad57b42 100644
--- a/app/Models/Entry.php
+++ b/app/Models/Entry.php
@@ -136,6 +136,11 @@ class Entry extends Model
}
+ public function prelimScoreSheets(): HasMany
+ {
+ return $this->hasMany(PrelimScoreSheet::class);
+ }
+
public function bonusScores(): HasMany
{
return $this->hasMany(BonusScore::class);
diff --git a/app/Models/PrelimScoreSheet.php b/app/Models/PrelimScoreSheet.php
index d81fd71..2502e25 100644
--- a/app/Models/PrelimScoreSheet.php
+++ b/app/Models/PrelimScoreSheet.php
@@ -7,6 +7,15 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
class PrelimScoreSheet extends Model
{
+ protected $fillable = [
+ 'user_id',
+ 'entry_id',
+ 'subscores',
+ 'total',
+ ];
+
+ protected $casts = ['subscores' => 'json'];
+
public function user(): HasOne
{
return $this->hasOne(User::class);
diff --git a/tests/Feature/app/Actions/Tabulation/EnterPrelimScoreTest.php b/tests/Feature/app/Actions/Tabulation/EnterPrelimScoreTest.php
new file mode 100644
index 0000000..bc4587d
--- /dev/null
+++ b/tests/Feature/app/Actions/Tabulation/EnterPrelimScoreTest.php
@@ -0,0 +1,166 @@
+prelimScoringGuide = ScoringGuide::factory()->create(['id' => 1000]);
+ SubscoreDefinition::create([
+ 'id' => 1001,
+ 'scoring_guide_id' => $this->prelimScoringGuide->id,
+ 'name' => 'Scale',
+ 'max_score' => 100,
+ 'weight' => 1,
+ 'display_order' => 1,
+ 'tiebreak_order' => 3,
+ 'for_seating' => '1',
+ 'for_advance' => '0',
+ ]);
+ SubscoreDefinition::create([
+ 'id' => 1002,
+ 'scoring_guide_id' => $this->prelimScoringGuide->id,
+ 'name' => 'Etude 1',
+ 'max_score' => 100,
+ 'weight' => 2,
+ 'display_order' => 2,
+ 'tiebreak_order' => 1,
+ 'for_seating' => '1',
+ 'for_advance' => '1',
+ ]);
+ SubscoreDefinition::create([
+ 'id' => 1003,
+ 'scoring_guide_id' => $this->prelimScoringGuide->id,
+ 'name' => 'Etude 2',
+ 'max_score' => 100,
+ 'weight' => 2,
+ 'display_order' => 3,
+ 'tiebreak_order' => 2,
+ 'for_seating' => '0',
+ 'for_advance' => '1',
+ ]);
+ SubscoreDefinition::where('id', '<', 900)->delete();
+ $this->finalsRoom = Room::factory()->create();
+ $this->prelimRoom = Room::factory()->create();
+ $this->audition = Audition::factory()->create(['room_id' => $this->finalsRoom->id]);
+ $this->prelimDefinition = PrelimDefinition::create([
+ 'room_id' => $this->prelimRoom->id,
+ 'audition_id' => $this->audition->id,
+ 'scoring_guide_id' => $this->prelimScoringGuide->id,
+ 'passing_score' => 60,
+ ]);
+
+ $this->judge1 = User::factory()->create();
+ $this->judge2 = User::factory()->create();
+ $this->prelimRoom->judges()->attach($this->judge1->id);
+ $this->prelimRoom->judges()->attach($this->judge2->id);
+ $this->entry1 = Entry::factory()->create(['audition_id' => $this->audition->id]);
+ $this->entry2 = Entry::factory()->create(['audition_id' => $this->audition->id]);
+ $this->scribe = app(EnterPrelimScore::class);
+ $this->possibleScoreArray = [
+ 1001 => 10,
+ 1002 => 11,
+ 1003 => 12,
+ ];
+ $this->anotherPossibleScoreArray = [
+ 1001 => 20,
+ 1002 => 21,
+ 1003 => 22,
+ ];
+
+});
+
+it('can enter a prelim score', function () {
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+ expect($this->entry1->prelimScoreSheets()->count())->toBe(1)
+ ->and($this->entry1->prelimScoreSheets()->first()->total)->toBe(11.2);
+
+ ($this->scribe)($this->judge1, $this->entry2, $this->anotherPossibleScoreArray);
+ expect($this->entry2->prelimScoreSheets()->count())->toBe(1)
+ ->and($this->entry2->prelimScoreSheets()->first()->total)->toBe(21.2);
+});
+
+it('will not enter a score for a judge that does not exist', function () {
+ $fakeJudge = User::factory()->make();
+ ($this->scribe)($fakeJudge, $this->entry1, $this->possibleScoreArray);
+})->throws(AuditionAdminException::class, 'User does not exist');
+
+it('will not enter a score for an entry that does not exist', function () {
+ $fakeEntry = Entry::factory()->make();
+ ($this->scribe)($this->judge1, $fakeEntry, $this->possibleScoreArray);
+})->throws(AuditionAdminException::class, 'Entry does not exist');
+
+it('will not score an entry if the audition seats are published', function () {
+ $this->audition->addFlag('seats_published');
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+})->throws(AuditionAdminException::class, 'Cannot score an entry in an audition where seats are published');
+
+it('will not score an entry if the judge is not assigned to judge the entry', function () {
+ $fakeJudge = User::factory()->create();
+ ($this->scribe)($fakeJudge, $this->entry1, $this->possibleScoreArray);
+})->throws(AuditionAdminException::class, 'This judge is not assigned to judge this entry');
+
+it('can modify an existing score sheet', function () {
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+ $scoreSheet = PrelimScoreSheet::first();
+ ($this->scribe)($this->judge1, $this->entry1, $this->anotherPossibleScoreArray, $scoreSheet);
+ expect($this->entry1->prelimScoreSheets()->count())->toBe(1)
+ ->and($this->entry1->prelimScoreSheets()->first()->total)->toBe(21.2);
+});
+
+it('will not change the judge on a score sheet', function () {
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+ $scoreSheet = PrelimScoreSheet::first();
+ ($this->scribe)($this->judge2, $this->entry1, $this->anotherPossibleScoreArray, $scoreSheet);
+})->throws(AuditionAdminException::class, 'Existing score sheet is from a different judge');
+
+it('will not accept a second score sheet for a judge ane entry', function () {
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+ ($this->scribe)($this->judge1, $this->entry1, $this->anotherPossibleScoreArray);
+})->throws(AuditionAdminException::class, 'That judge has already entered a prelim score for that entry');
+
+it('will not change the entry on a score sheet', function () {
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+ $scoreSheet = PrelimScoreSheet::first();
+ ($this->scribe)($this->judge1, $this->entry2, $this->anotherPossibleScoreArray, $scoreSheet);
+})->throws(AuditionAdminException::class, 'Existing score sheet is for a different entry');
+
+it('will not accept an incorrect number of subscores', function () {
+ array_pop($this->possibleScoreArray);
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+})->throws(AuditionAdminException::class, 'Invalid number of scores');
+
+it('will not accept an invalid subscores', function () {
+ array_pop($this->possibleScoreArray);
+ $this->possibleScoreArray[3001] = 100;
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+})->throws(AuditionAdminException::class, 'Invalid Score Submission');
+
+it('will. not accept a subscore in excess of its maximum', function () {
+ $this->possibleScoreArray[1001] = 1500;
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+})->throws(AuditionAdminException::class, 'Supplied subscore exceeds maximum allowed');
+
+it('removes a no-show flag from an entry', function () {
+ $this->entry1->addFlag('no_show');
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+ expect($this->entry1->hasFlag('no_show'))->toBeFalse();
+});
+
+it('logs score entry', function () {
+ ($this->scribe)($this->judge1, $this->entry1, $this->possibleScoreArray);
+ $logEntry = AuditLogEntry::orderBy('id', 'desc')->first();
+ expect($logEntry->message)->toStartWith('Entered prelim score for entry id ');
+});