Compare commits

..

No commits in common. "master" and "meobda_nomination_ensemble" have entirely different histories.

477 changed files with 3795 additions and 20613 deletions

3
.gitignore vendored
View File

@ -20,6 +20,3 @@ yarn-error.log
/.vscode /.vscode
/app/Http/Controllers/TestController.php /app/Http/Controllers/TestController.php
/resources/views/test.blade.php /resources/views/test.blade.php
/reports
/--cache-directory
/storage/debug.html

View File

@ -1,10 +1,10 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests - parallel" type="PestRunConfigurationType"> <configuration default="false" name="tests - paralell" type="PestRunConfigurationType">
<option name="pestRunnerSettings"> <option name="pestRunnerSettings">
<PestRunner directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases --coverage" /> <PestRunner directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases" />
</option> </option>
<option name="runnerSettings"> <option name="runnerSettings">
<PhpTestRunnerSettings directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases --coverage" /> <PhpTestRunnerSettings directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases" />
</option> </option>
<method v="2" /> <method v="2" />
</configuration> </configuration>

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions\Development;
use App\Actions\Tabulation\EnterScore;
use App\Models\Entry;
class FakeScoresForEntry
{
public function __construct()
{
}
public function __invoke(Entry $entry): void
{
$scoreScribe = app(EnterScore::class);
$scoringGuide = $entry->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);
}
}
}

View File

@ -1,39 +0,0 @@
<?php
namespace App\Actions\Draw;
use App\Models\Audition;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use function auditionLog;
class ClearDraw
{
/** @codeCoverageIgnore */
public function __invoke(Audition|collection $auditions): void
{
if ($auditions instanceof Audition) {
$this->clearDraw($auditions);
}
if ($auditions instanceof Collection) {
$this->clearDraws($auditions);
}
}
public function clearDraw(Audition $audition): void
{
$audition->removeFlag('drawn');
DB::table('entries')->where('audition_id', $audition->id)->update(['draw_number' => null]);
$message = 'Cleared draw for audition #'.$audition->id.' '.$audition->name;
$affected['auditions'] = [$audition->id];
auditionLog($message, $affected);
}
public function clearDraws(Collection $auditions): void
{
foreach ($auditions as $audition) {
$this->clearDraw($audition);
}
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Actions\Draw;
use App\Models\Audition;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class RunDraw
{
public function __invoke(Audition|Collection $auditions): void
{
if ($auditions instanceof Audition) {
// Single audition, run draw directly
$this->runDraw($auditions);
return;
} elseif ($auditions instanceof Collection) {
$this->runDrawMultiple($auditions);
return;
}
}
public function runDraw(Audition $audition): void
{
// start off by clearing any existing draw numbers in the audition
DB::table('entries')->where('audition_id', $audition->id)->update(['draw_number' => null]);
$randomizedEntries = $audition->entries->shuffle();
// Move entries flagged as no show to the end
[$noShowEntries, $otherEntries] = $randomizedEntries->partition(function ($entry) {
return $entry->hasFlag('no_show');
});
$randomizedEntries = $otherEntries->merge($noShowEntries);
// Save draw numbers back to the entries\
$nextNumber = 1;
foreach ($randomizedEntries as $index => $entry) {
$entry->update(['draw_number' => $nextNumber]);
$nextNumber++;
}
$audition->addFlag('drawn');
}
public function runDrawMultiple(Collection $auditions): void
{
// Eager load the 'entries' relationship on all auditions if not already loaded
$auditions->loadMissing('entries');
$auditions->each(fn ($audition) => $this->runDraw($audition));
}
}

View File

@ -2,12 +2,14 @@
namespace App\Actions\Entries; namespace App\Actions\Entries;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ManageEntryException; use App\Exceptions\ManageEntryException;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
use App\Models\Student; use App\Models\Student;
use function auth;
class CreateEntry class CreateEntry
{ {
public function __construct() public function __construct()
@ -17,49 +19,49 @@ class CreateEntry
/** /**
* @throws ManageEntryException * @throws ManageEntryException
*/ */
public function __invoke( public function __invoke(Student|int $student, Audition|int $audition, string|array|null $entry_for = null)
Student|int $student, {
Audition|int $audition, return $this->createEntry($student, $audition, $entry_for);
$for_seating = false,
$for_advancement = false,
$late_fee_waived = false
) {
return $this->createEntry($student, $audition, $for_seating, $for_advancement, $late_fee_waived);
} }
/** /**
* @throws ManageEntryException * @throws ManageEntryException
*/ */
public function createEntry( public function createEntry(Student|int $student, Audition|int $audition, string|array|null $entry_for = null)
Student|int $student, {
Audition|int $audition,
$for_seating = false,
$for_advancement = false,
$late_fee_waived = false
): Entry {
if (is_int($student)) { if (is_int($student)) {
$student = Student::find($student); $student = Student::find($student);
} }
if (is_int($audition)) { if (is_int($audition)) {
$audition = Audition::find($audition); $audition = Audition::find($audition);
} }
$this->verifySubmission($student, $audition);
if (! $for_advancement && ! $for_seating) { if (! $entry_for) {
$for_seating = true; $entry_for = ['seating', 'advancement'];
$for_advancement = true;
} }
$entry_for = collect($entry_for);
$this->verifySubmission($student, $audition);
$entry = Entry::make([ $entry = Entry::make([
'student_id' => $student->id, 'student_id' => $student->id,
'audition_id' => $audition->id, 'audition_id' => $audition->id,
'draw_number' => $this->checkDraw($audition), 'draw_number' => $this->checkDraw($audition),
'for_seating' => $for_seating, 'for_seating' => $entry_for->contains('seating'),
'for_advancement' => $for_advancement, 'for_advancement' => $entry_for->contains('advancement'),
]); ]);
$entry->save(); $entry->save();
if (auth()->user()) {
if ($late_fee_waived) { $message = 'Entered '.$entry->student->full_name().' from '.$entry->student->school->name.' in '.$entry->audition->name.'.';
$entry->addFlag('late_fee_waived'); AuditLogEntry::create([
$entry->refresh(); 'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'entries' => [$entry->id],
'students' => [$entry->student_id],
'auditions' => [$entry->audition_id],
'schools' => [$entry->student->school_id],
],
]);
} }
return $entry; return $entry;
@ -81,29 +83,29 @@ class CreateEntry
{ {
// Make sure it's a valid student // Make sure it's a valid student
if (! $student || ! $student->exists()) { if (! $student || ! $student->exists()) {
throw new AuditionAdminException('Invalid student provided'); throw new ManageEntryException('Invalid student provided');
} }
// Make sure the audition is valid // Make sure the audition is valid
if (! $audition || ! $audition->exists()) { if (! $audition || ! $audition->exists()) {
throw new AuditionAdminException('Invalid audition provided'); throw new ManageEntryException('Invalid audition provided');
} }
// A student can't enter the same audition twice // A student can't enter the same audition twice
if (Entry::where('student_id', $student->id)->where('audition_id', $audition->id)->exists()) { if (Entry::where('student_id', $student->id)->where('audition_id', $audition->id)->exists()) {
throw new AuditionAdminException('That student is already entered in that audition'); throw new ManageEntryException('That student is already entered in that audition');
} }
// Can't enter a published audition // Can't enter a published audition
if ($audition->hasFlag('seats_published')) { if ($audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot add an entry to an audition where seats are published'); throw new ManageEntryException('Cannot add an entry to an audition where seats are published');
} }
if ($audition->hasFlag('advancement_published')) { if ($audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot add an entry to an audition where advancement is published'); throw new ManageEntryException('Cannot add an entry to an audition where advancement is published');
} }
// Verify the grade of the student is in range for the audition // Verify the grade of the student is in range for the audition
if ($student->grade > $audition->maximum_grade) { if ($student->grade > $audition->maximum_grade) {
throw new AuditionAdminException('The grade of the student exceeds the maximum for that audition'); throw new ManageEntryException('The grade of the student exceeds the maximum for that audition');
} }
if ($student->grade < $audition->minimum_grade) { if ($student->grade < $audition->minimum_grade) {
throw new AuditionAdminException('The grade of the student does not meet the minimum for that audition'); throw new ManageEntryException('The grade of the student does not meet the minimum for that audition');
} }
} }
} }

View File

@ -3,12 +3,19 @@
namespace App\Actions\Entries; namespace App\Actions\Entries;
use App\Exceptions\AuditionAdminException; use App\Exceptions\AuditionAdminException;
use App\Models\Doubler;
use App\Models\Entry; use App\Models\Entry;
use App\Services\DoublerService;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
class DoublerDecision class DoublerDecision
{ {
protected DoublerService $doublerService;
public function __construct(DoublerService $doublerService)
{
$this->doublerService = $doublerService;
}
/** /**
* @throws AuditionAdminException * @throws AuditionAdminException
*/ */
@ -27,45 +34,25 @@ class DoublerDecision
'decline' => $this->decline($entry), 'decline' => $this->decline($entry),
default => throw new AuditionAdminException('Invalid decision specified') default => throw new AuditionAdminException('Invalid decision specified')
}; };
if ($decision != 'accept' && $decision != 'decline') {
throw new AuditionAdminException('Invalid decision specified');
}
} }
/**
* Accepts an entry for the given audition.
*
* This method ensures the entry is not already declined, and that the
* audition is not in a state where seats or advancement are published.
* If the entry is already declined, this method does nothing.
* If the audition is in a state where seats or advancement are published,
* this method throws an exception.
*
* This method also declines all other entries in the same audition,
* clearing the rank cache for the audition.
*
* @throws AuditionAdminException
*/
public function accept(Entry $entry): void public function accept(Entry $entry): void
{ {
if ($entry->hasFlag('declined')) { Cache::forget('audition'.$entry->audition_id.'seating');
throw new AuditionAdminException('Entry '.$entry->id.' is already declined'); Cache::forget('audition'.$entry->audition_id.'advancement');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot accept an entry in an audition where seats are published');
}
Cache::forget('rank_seating_'.$entry->audition_id);
// Process student entries // Decline all other entries and clear rank cache
$doublerData = Doubler::findDoubler($entry->student_id, $entry->audition->event_id); $doublerInfo = $this->doublerService->simpleDoubleInfo($entry);
// Check each entry and see if it is unscored. We can't accept this entry if that is the case. foreach ($doublerInfo as $doublerEntry) {
foreach ($doublerData->entries() as $doublerEntry) { Cache::forget('audition'.$doublerEntry->audition_id.'seating');
if (! $doublerEntry->totalScore && ! $doublerEntry->hasFlag('declined') && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim')) { /** @var Entry $doublerEntry */
throw new AuditionAdminException('Cannot accept seating for '.$entry->student->full_name().' because student has unscored entries'); if ($doublerEntry->id !== $entry->id) {
} $doublerEntry->addFlag('declined');
}
// Decline all other entries
foreach ($doublerData->entries() as $doublerEntry) {
Cache::forget('rank_seating_'.$doublerEntry->audition_id);
if ($doublerEntry->id !== $entry->id && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim') && ! $doublerEntry->hasFlag('declined')) {
$this->decline($doublerEntry);
} }
} }
} }
@ -75,21 +62,12 @@ class DoublerDecision
*/ */
public function decline($entry): void public function decline($entry): void
{ {
// Entry cannot decline a seat twice Cache::forget('audition'.$entry->audition_id.'seating');
Cache::forget('audition'.$entry->audition_id.'advancement');
if ($entry->hasFlag('declined')) { if ($entry->hasFlag('declined')) {
throw new AuditionAdminException('Entry '.$entry->id.' is already declined'); throw new AuditionAdminException('Entry is already declined');
} }
if (! $entry->totalScore) { Cache::forget('audition'.$entry->audition_id.'seating');
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'); $entry->addFlag('declined');
// Clear rank cache
Cache::forget('rank_seating_'.$entry->audition_id);
} }
} }

View File

@ -0,0 +1,44 @@
<?php
namespace App\Actions\Entries;
use App\Models\Entry;
use App\Models\Seat;
class GetEntrySeatingResult
{
public function __construct()
{
}
public function __invoke(Entry $entry): string
{
return $this->getResult($entry);
}
public function getResult(Entry $entry): string
{
if ($entry->hasFlag('failed_prelim')) {
return 'Failed Prelim';
}
if ($entry->hasFlag('no_show')) {
return 'No Show';
}
if ($entry->hasFlag('declined')) {
return 'Declined';
}
if ($entry->hasFlag('failed_prelim')) {
return 'Did not pass prelim';
}
$seat = Seat::where('entry_id', $entry->id)->first();
if ($seat) {
return $seat->ensemble->name.' '.$seat->seat;
}
return 'Entry not seated';
}
}

View File

@ -2,7 +2,6 @@
namespace App\Actions\Entries; namespace App\Actions\Entries;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ManageEntryException; use App\Exceptions\ManageEntryException;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
@ -28,7 +27,7 @@ class UpdateEntry
/** /**
* @throws ManageEntryException * @throws ManageEntryException
*/ */
public function __invoke(Entry|int $entry, array $updateData): void public function __invoke(Entry $entry, array $updateData): void
{ {
$this->updateEntry($entry, $updateData); $this->updateEntry($entry, $updateData);
} }
@ -42,7 +41,7 @@ class UpdateEntry
$entry = Entry::find($entry); $entry = Entry::find($entry);
} }
if (! $entry || ! $entry->exists) { if (! $entry || ! $entry->exists) {
throw new AuditionAdminException('Invalid entry provided'); throw new ManageEntryException('Invalid entry provided');
} }
$this->entry = $entry; $this->entry = $entry;
if (array_key_exists('for_seating', $updateData)) { if (array_key_exists('for_seating', $updateData)) {
@ -82,34 +81,34 @@ class UpdateEntry
$audition = Audition::find($audition); $audition = Audition::find($audition);
} }
if (! $audition || ! $audition->exists) { if (! $audition || ! $audition->exists) {
throw new AuditionAdminException('Invalid audition provided'); throw new ManageEntryException('Invalid audition provided');
} }
if ($this->entry->audition->hasFlag('seats_published')) { if ($this->entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot change the audition for an entry where seating for that entry\'s current audition is published'); throw new ManageEntryException('Cannot change the audition for an entry where seating for that entry\'s current audition is published');
} }
if ($this->entry->audition->hasFlag('advancement_published')) { if ($this->entry->audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot change the audition for an entry where advancement for that entry\'s current audition is published'); throw new ManageEntryException('Cannot change the audition for an entry where advancement for that entry\'s current audition is published');
} }
if ($audition->hasFlag('seats_published')) { if ($audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot change the entry to an audition with published seating'); throw new ManageEntryException('Cannot change the entry to an audition with published seating');
} }
if ($audition->hasFlag('advancement_published')) { if ($audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot change the entry to an audition with published advancement'); throw new ManageEntryException('Cannot change the entry to an audition with published advancement');
} }
if ($this->entry->student->grade > $audition->maximum_grade) { if ($this->entry->student->grade > $audition->maximum_grade) {
throw new AuditionAdminException('The student is too old to enter that audition'); throw new ManageEntryException('The grade of the student exceeds the maximum for that audition');
} }
if ($this->entry->student->grade < $audition->minimum_grade) { if ($this->entry->student->grade < $audition->minimum_grade) {
throw new AuditionAdminException('The student is too young to enter that audition'); throw new ManageEntryException('The grade of the student does not meet the minimum for that audition');
} }
if ($this->entry->scoreSheets()->count() > 0) { if ($this->entry->scoreSheets()->count() > 0) {
throw new AuditionAdminException('Cannot change the audition for an entry with scores'); throw new ManageEntryException('Cannot change the audition for an entry with scores');
} }
if ($audition->id !== $this->entry->audition_id && if ($audition->id !== $this->entry->audition_id &&
Entry::where('student_id', $this->entry->student_id) Entry::where('student_id', $this->entry->student_id)
->where('audition_id', $audition->id)->exists()) { ->where('audition_id', $audition->id)->exists()) {
throw new AuditionAdminException('That student is already entered in that audition'); throw new ManageEntryException('That student is already entered in that audition');
} }
// Escape if we're not actually making a change // Escape if we're not actually making a change
if ($this->entry->audition_id == $audition->id) { if ($this->entry->audition_id == $audition->id) {
@ -140,13 +139,13 @@ class UpdateEntry
} }
if ($forSeating) { if ($forSeating) {
if ($this->entry->audition->hasFlag('seats_published')) { if ($this->entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot add seating to an entry in an audition where seats are published'); throw new ManageEntryException('Cannot add seating to an entry in an audition where seats are published');
} }
$this->entry->for_seating = 1; $this->entry->for_seating = 1;
$this->log_message .= 'Entry '.$this->entry->id.' is entered for seating '.'<br>'; $this->log_message .= 'Entry '.$this->entry->id.' is entered for seating '.'<br>';
} else { } else {
if ($this->entry->audition->hasFlag('seats_published')) { if ($this->entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot remove seating from an entry in an audition where seats are published'); throw new ManageEntryException('Cannot remove seating from an entry in an audition where seats are published');
} }
$this->entry->for_seating = 0; $this->entry->for_seating = 0;
$this->log_message .= 'Entry '.$this->entry->id.' is NOT entered for seating '.'<br>'; $this->log_message .= 'Entry '.$this->entry->id.' is NOT entered for seating '.'<br>';
@ -163,13 +162,13 @@ class UpdateEntry
} }
if ($forAdvancement) { if ($forAdvancement) {
if ($this->entry->audition->hasFlag('advancement_published')) { if ($this->entry->audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot add advancement to an entry in an audition where advancement is published'); throw new ManageEntryException('Cannot add advancement to an entry in an audition where advancement is published');
} }
$this->entry->for_advancement = 1; $this->entry->for_advancement = 1;
$this->log_message .= 'Entry '.$this->entry->id.' is entered for '.auditionSetting('advanceTo').'<br>'; $this->log_message .= 'Entry '.$this->entry->id.' is entered for '.auditionSetting('advanceTo').'<br>';
} else { } else {
if ($this->entry->audition->hasFlag('advancement_published')) { if ($this->entry->audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot remove advancement from an entry in an audition where advancement is published'); throw new ManageEntryException('Cannot remove advancement from an entry in an audition where advancement is published');
} }
$this->entry->for_advancement = 0; $this->entry->for_advancement = 0;
$this->log_message .= 'Entry '.$this->entry->id.' is NOT entered for '.auditionSetting('advanceTo').'<br>'; $this->log_message .= 'Entry '.$this->entry->id.' is NOT entered for '.auditionSetting('advanceTo').'<br>';

View File

@ -2,6 +2,7 @@
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\AuditLogEntry;
use App\Models\User; use App\Models\User;
use App\Rules\ValidRegistrationCode; use App\Rules\ValidRegistrationCode;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
@ -52,6 +53,17 @@ class CreateNewUser implements CreatesNewUsers
'password' => Hash::make($input['password']), 'password' => Hash::make($input['password']),
]); ]);
$message = 'New User Registered - '.$input['email']
.'<br>Name: '.$input['first_name'].' '.$input['last_name']
.'<br>Judging Pref: '.$input['judging_preference']
.'<br>Cell Phone: '.$input['cell_phone'];
AuditLogEntry::create([
'user' => $input['email'],
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => $user->id],
]);
return $user; return $user;
} }
} }

View File

@ -2,6 +2,7 @@
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\AuditLogEntry;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -25,6 +26,11 @@ class ResetUserPassword implements ResetsUserPasswords
$user->forceFill([ $user->forceFill([
'password' => Hash::make($input['password']), 'password' => Hash::make($input['password']),
])->save(); ])->save();
AuditLogEntry::create([
'user' => $user->email,
'ip_address' => request()->ip(),
'message' => 'Reset Password',
'affected' => ['users' => [$user->id]],
]);
} }
} }

View File

@ -1,55 +0,0 @@
<?php
namespace App\Actions\Fortify;
use App\Exceptions\AuditionAdminException;
use App\Models\User;
class UpdateUserPrivileges
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(User|int $user, string $action, string $privilege): void
{
$this->setPrivilege($user, $action, $privilege);
}
/**
* @throws AuditionAdminException
*/
public function setPrivilege(User|int $user, string $action, string $privilege): void
{
if (is_int($user)) {
$user = User::findOrFail($user);
}
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (! in_array($action, ['grant', 'revoke'])) {
throw new AuditionAdminException('Invalid Action');
}
$field = match ($privilege) {
'admin' => 'is_admin',
'tab' => 'is_tab',
default => throw new AuditionAdminException('Invalid Privilege'),
};
if ($user->$field == 1 && $action == 'revoke') {
$user->$field = 0;
$user->save();
}
if ($user->$field == 0 && $action == 'grant') {
$user->$field = 1;
$user->save();
}
}
}

View File

@ -2,6 +2,7 @@
namespace App\Actions\Fortify; namespace App\Actions\Fortify;
use App\Models\AuditLogEntry;
use App\Models\User; use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@ -44,7 +45,16 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
'email' => $input['email'], 'email' => $input['email'],
])->save(); ])->save();
} }
$message = 'Updated user #'.$user->id.' - '.$user->email
.'<br>Name: '.$user->full_name()
.'<br>Judging Pref: '.$user->judging_preference
.'<br>Cell Phone: '.$user->cell_phone;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
} }
/** /**
@ -64,6 +74,17 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
'email_verified_at' => null, 'email_verified_at' => null,
])->save(); ])->save();
$user->refresh(); $user->refresh();
$message = 'Updated user #'.$user->id.' - '.$oldEmail
.'<br>Name: '.$user->full_name()
.'<br>Email: '.$user->email
.'<br>Judging Pref: '.$user->judging_preference
.'<br>Cell Phone: '.$user->cell_phone;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
$user->sendEmailVerificationNotification(); $user->sendEmailVerificationNotification();
} }

View File

@ -6,10 +6,6 @@ use App\Models\Entry;
use App\Models\Room; use App\Models\Room;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for PrintSignInSheets
class PrintSignInSheets class PrintSignInSheets
{ {
protected $pdf; protected $pdf;
@ -78,15 +74,11 @@ class PrintSignInSheets
public function addEntryRow(Entry $entry) public function addEntryRow(Entry $entry)
{ {
$nameLine = $entry->student->full_name();
if ($entry->student->isDoublerInEvent($entry->audition->event_id)) {
$nameLine .= ' (D)';
}
$this->pdf->Cell($this->columnWidth['id'], $this->bodyRowHeight, $entry->id, 1, 0, 'L'); $this->pdf->Cell($this->columnWidth['id'], $this->bodyRowHeight, $entry->id, 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['instrument'], $this->bodyRowHeight, $entry->audition->name, 1, 0, $this->pdf->Cell($this->columnWidth['instrument'], $this->bodyRowHeight, $entry->audition->name, 1, 0,
'L'); 'L');
$this->pdf->Cell($this->columnWidth['drawNumber'], $this->bodyRowHeight, $entry->draw_number, 1, 0, 'L'); $this->pdf->Cell($this->columnWidth['drawNumber'], $this->bodyRowHeight, $entry->draw_number, 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['name'], $this->bodyRowHeight, $nameLine, 1, 0, 'L'); $this->pdf->Cell($this->columnWidth['name'], $this->bodyRowHeight, $entry->student->full_name(), 1, 0, 'L');
$this->pdf->Cell($this->columnWidth['school'], $this->bodyRowHeight, $entry->student->school->name, 1, 0, 'L'); $this->pdf->Cell($this->columnWidth['school'], $this->bodyRowHeight, $entry->student->school->name, 1, 0, 'L');
$this->pdf->Cell(0, $this->bodyRowHeight, ' ', 1, 1, 'L'); $this->pdf->Cell(0, $this->bodyRowHeight, ' ', 1, 1, 'L');
} }

View File

@ -5,10 +5,6 @@ namespace App\Actions\Print;
use App\Models\Ensemble; use App\Models\Ensemble;
use Codedge\Fpdf\Fpdf\Fpdf; use Codedge\Fpdf\Fpdf\Fpdf;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for PrintStandNameTags
class PrintStandNameTags class PrintStandNameTags
{ {
public function __construct() public function __construct()

View File

@ -8,10 +8,6 @@ use Illuminate\Support\Collection;
use function auditionSetting; use function auditionSetting;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for QuarterPageCards
class QuarterPageCards implements PrintCards class QuarterPageCards implements PrintCards
{ {
protected $pdf; protected $pdf;
@ -58,37 +54,17 @@ class QuarterPageCards implements PrintCards
$this->pdf->Cell(4.5, .5, $entry->audition->name.' #'.$entry->draw_number); $this->pdf->Cell(4.5, .5, $entry->audition->name.' #'.$entry->draw_number);
// Fill in student information // Fill in student information
$nameLine = $entry->student->full_name();
if ($entry->student->isDoublerInEvent($entry->audition->event_id)) {
$nameLine .= ' (D)';
}
$this->pdf->SetFont('Arial', '', 10); $this->pdf->SetFont('Arial', '', 10);
$xLoc = $this->offset[$this->quadOn][0] + 1; $xLoc = $this->offset[$this->quadOn][0] + 1;
$yLoc = $this->offset[$this->quadOn][1] + 3.1; $yLoc = $this->offset[$this->quadOn][1] + 3.1;
$this->pdf->setXY($xLoc, $yLoc); $this->pdf->setXY($xLoc, $yLoc);
$this->pdf->Cell(4.5, .25, $nameLine); $this->pdf->Cell(4.5, .25, $entry->student->full_name());
$this->pdf->setXY($xLoc, $yLoc + .25); $this->pdf->setXY($xLoc, $yLoc + .25);
$this->pdf->Cell(4.5, .25, $entry->student->school->name); $this->pdf->Cell(4.5, .25, $entry->student->school->name);
$this->pdf->setXY($xLoc, $yLoc + .5); $this->pdf->setXY($xLoc, $yLoc + .5);
if (! is_null($entry->audition->room_id)) { if (! is_null($entry->audition->room_id)) {
$this->pdf->Cell(4.5, .25, $entry->audition->room->name); $this->pdf->Cell(4.5, .25, $entry->audition->room->name);
} }
if (auditionSetting('advanceTo')) {
$as = false;
$this->pdf->setXY($xLoc, $yLoc - 1);
$auditioningFor = 'Auditioning for: ';
if ($entry->for_seating) {
$auditioningFor .= auditionSetting('auditionAbbreviation');
$as = true;
}
if ($entry->for_advancement) {
if ($as) {
$auditioningFor .= ' / ';
}
$auditioningFor .= auditionSetting('advanceTo');
}
$this->pdf->Cell(4.5, .25, $auditioningFor);
}
$this->quadOn++; $this->quadOn++;
} }

View File

@ -4,10 +4,6 @@ namespace App\Actions\Print;
use Codedge\Fpdf\Fpdf\Fpdf; use Codedge\Fpdf\Fpdf\Fpdf;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for signInPDF
class signInPDF extends Fpdf class signInPDF extends Fpdf
{ {
public $roomOn; public $roomOn;

View File

@ -6,10 +6,6 @@ use App\Actions\Tabulation\RankAuditionEntries;
use App\Models\Room; use App\Models\Room;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for ExportEntryData
class ExportEntryData class ExportEntryData
{ {
public function __construct() public function __construct()

View File

@ -7,10 +7,6 @@ use App\Models\Event;
use App\Models\Seat; use App\Models\Seat;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for GetExportData
class GetExportData class GetExportData
{ {
public function __construct() public function __construct()
@ -33,7 +29,7 @@ class GetExportData
foreach ($events as $event) { foreach ($events as $event) {
$auditions = $event->auditions; $auditions = $event->auditions;
foreach ($auditions as $audition) { foreach ($auditions as $audition) {
$entries = $ranker($audition, 'seating'); $entries = $ranker->rank('seating', $audition);
foreach ($entries as $entry) { foreach ($entries as $entry) {
$thisRow = $audition->name.','; $thisRow = $audition->name.',';
$thisRow .= $entry->raw_rank ?? ''; $thisRow .= $entry->raw_rank ?? '';
@ -41,7 +37,7 @@ class GetExportData
$thisRow .= $entry->student->full_name().','; $thisRow .= $entry->student->full_name().',';
$thisRow .= $entry->student->school->name.','; $thisRow .= $entry->student->school->name.',';
$thisRow .= $entry->student->grade.','; $thisRow .= $entry->student->grade.',';
$thisRow .= $entry->totalScore->seating_total ?? ''; $thisRow .= $entry->score_totals[0] ?? '';
$thisRow .= ','; $thisRow .= ',';
if ($entry->hasFlag('failed_prelim')) { if ($entry->hasFlag('failed_prelim')) {
$thisRow .= 'Failed Prelim'; $thisRow .= 'Failed Prelim';

View File

@ -1,35 +0,0 @@
<?php
namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\SchoolEmailDomain;
class AddSchoolEmailDomain
{
public function __construct()
{
}
public function __invoke(School $school, string $domain): void
{
$this->addDomain($school, $domain);
}
public function addDomain(School $school, string $domain): void
{
if (! School::where('id', $school->id)->exists()) {
throw new AuditionAdminException('School does not exist');
}
if (SchoolEmailDomain::where('domain', $domain)->where('school_id', $school->id)->exists()) {
return;
}
SchoolEmailDomain::create([
'domain' => $domain,
'school_id' => $school->id,
]);
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\User;
class AssignUserToSchool
{
public function __invoke(User $user, School|int|null $school): void
{
$this->assign($user, $school);
}
public function assign(User $user, School|int|null $school, bool $addDomainToSchool = true): void
{
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (is_int($school)) {
$school = School::find($school);
}
if (is_null($school)) {
$user->update([
'school_id' => null,
]);
return;
}
if (is_null($school) || ! School::where('id', $school->id)->exists()) {
throw new AuditionAdminException('School does not exist');
}
$domainRecorder = app(AddSchoolEmailDomain::class);
if ($addDomainToSchool) {
$domainRecorder($school, $user->emailDomain());
}
$user->update([
'school_id' => $school->id,
]);
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
class CreateSchool
{
public function __invoke(
string $name,
?string $address = null,
?string $city = null,
?string $state = null,
?string $zip = null
): School {
return $this->create($name, $address, $city, $state, $zip);
}
public function create(
string $name,
?string $address = null,
?string $city = null,
?string $state = null,
?string $zip = null
): School {
if (School::where('name', $name)->exists()) {
throw new AuditionAdminException('The school '.$name.' already exists');
}
$newSchool = School::create([
'name' => $name,
'address' => $address,
'city' => $city,
'state' => $state,
'zip' => $zip,
]);
if (auth()->user()) {
$message = 'Created school '.$newSchool->name;
$affects = ['schools' => [$newSchool->id]];
auditionLog($message, $affects);
}
return $newSchool;
}
}

View File

@ -3,6 +3,7 @@
namespace App\Actions\Schools; namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException; use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\User; use App\Models\User;
use function auditionLog; use function auditionLog;
@ -14,9 +15,9 @@ class SetHeadDirector
{ {
} }
public function __invoke(User $user): void public function __invoke(User $user, School $school): void
{ {
$this->setHeadDirector($user); $this->setHeadDirector($user, $school);
} }
/** /**
@ -24,14 +25,6 @@ class SetHeadDirector
*/ */
public function setHeadDirector(User $user): void public function setHeadDirector(User $user): void
{ {
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if ($user->hasFlag('head_director')) {
return;
}
if (is_null($user->school_id)) { if (is_null($user->school_id)) {
throw new AuditionAdminException('User is not associated with a school'); throw new AuditionAdminException('User is not associated with a school');
} }

View File

@ -1,44 +0,0 @@
<?php
namespace App\Actions\Students;
use App\Exceptions\AuditionAdminException;
use App\Models\Student;
use Arr;
class CreateStudent
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(array $newData): Student
{
// $newData[] must include keys first_name, last_name, grade - throw an exception if it does not
foreach (['first_name', 'last_name', 'grade'] as $key) {
if (! Arr::has($newData, $key)) {
throw new AuditionAdminException('Missing required data');
}
}
if (! Arr::has($newData, 'school_id')) {
$newData['school_id'] = auth()->user()->school_id;
}
if (Student::where('first_name', $newData['first_name'])->where('last_name', $newData['last_name'])
->where('school_id', $newData['school_id'])->exists()) {
throw new AuditionAdminException('Student already exists');
}
return Student::create([
'first_name' => $newData['first_name'],
'last_name' => $newData['last_name'],
'grade' => $newData['grade'],
'school_id' => $newData['school_id'],
'optional_data' => $newData['optional_data'] ?? null,
]);
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Actions\Students;
use App\Exceptions\AuditionAdminException;
use App\Models\Student;
use Arr;
class UpdateStudent
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(Student $student, array $newData): bool
{
// $newData[] must include keys first_name, last_name, grade - throw an exception if it does not
foreach (['first_name', 'last_name', 'grade'] as $key) {
if (! Arr::has($newData, $key)) {
throw new AuditionAdminException('Missing required data');
}
}
if (! Arr::has($newData, 'school_id')) {
$newData['school_id'] = auth()->user()->school_id;
}
if (Student::where('first_name', $newData['first_name'])
->where('last_name', $newData['last_name'])
->where('school_id', $newData['school_id'])
->where('id', '!=', $student->id)
->exists()) {
throw new AuditionAdminException('Student already exists');
}
return $student->update([
'first_name' => $newData['first_name'],
'last_name' => $newData['last_name'],
'grade' => $newData['grade'],
'school_id' => $newData['school_id'],
'optional_data' => $newData['optional_data'] ?? null,
]);
}
}

View File

@ -0,0 +1,99 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Services\AuditionService;
use App\Services\EntryService;
use Illuminate\Support\Facades\Cache;
class AllJudgesCount implements CalculateEntryScore
{
protected CalculateScoreSheetTotal $calculator;
protected AuditionService $auditionService;
protected EntryService $entryService;
public function __construct(CalculateScoreSheetTotal $calculator, AuditionService $auditionService, EntryService $entryService)
{
$this->calculator = $calculator;
$this->auditionService = $auditionService;
$this->entryService = $entryService;
}
public function calculate(string $mode, Entry $entry): array
{
$cacheKey = 'entryScore-'.$entry->id.'-'.$mode;
return Cache::remember($cacheKey, 300, function () use ($mode, $entry) {
$this->isEntryANoShow($entry);
$this->basicValidation($mode, $entry);
$this->areAllJudgesIn($entry);
$this->areAllJudgesValid($entry);
return $this->getJudgeTotals($mode, $entry);
});
}
protected function getJudgeTotals($mode, Entry $entry)
{
$scores = [];
foreach ($this->auditionService->getJudges($entry->audition) as $judge) {
$scores[] = $this->calculator->__invoke($mode, $entry, $judge);
}
$sums = [];
// Sum each subscore from the judges
foreach ($scores as $score) {
$index = 0;
foreach ($score as $value) {
$sums[$index] = $sums[$index] ?? 0;
$sums[$index] += $value;
$index++;
}
}
return $sums;
}
protected function basicValidation($mode, $entry): void
{
if ($mode !== 'seating' && $mode !== 'advancement') {
throw new TabulationException('Mode must be seating or advancement');
}
if (! $this->entryService->entryExists($entry)) {
throw new TabulationException('Invalid entry specified');
}
}
protected function areAllJudgesIn(Entry $entry): void
{
$assignedJudgeCount = $this->auditionService->getJudges($entry->audition)->count();
if ($entry->scoreSheets->count() !== $assignedJudgeCount) {
throw new TabulationException('Not all score sheets are in');
}
}
protected function areAllJudgesValid(Entry $entry): void
{
$validJudgeIds = $this->auditionService->getJudges($entry->audition)->pluck('id')->sort()->toArray();
$existingJudgeIds = $entry->scoreSheets->pluck('user_id')->sort()->toArray();
if ($validJudgeIds !== $existingJudgeIds) {
throw new TabulationException('Score exists from a judge not assigned to this audition');
}
}
protected function isEntryANoShow(Entry $entry): void
{
if ($entry->hasFlag('no_show')) {
throw new TabulationException('No Show');
}
}
}

View File

@ -0,0 +1,161 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\BonusScore;
use App\Models\CalculatedScore;
use App\Models\Entry;
use App\Services\AuditionService;
use App\Services\EntryService;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use function auditionSetting;
class AllowForOlympicScoring implements CalculateEntryScore
{
protected CalculateScoreSheetTotal $calculator;
protected AuditionService $auditionService;
protected EntryService $entryService;
public function __construct(
CalculateScoreSheetTotal $calculator,
AuditionService $auditionService,
EntryService $entryService
) {
$this->calculator = $calculator;
$this->auditionService = $auditionService;
$this->entryService = $entryService;
}
public function calculate(string $mode, Entry $entry): array
{
$calculated = CalculatedScore::where('entry_id', $entry->id)->where('mode', $mode)->first();
if ($calculated) {
return $calculated->calculatedScore;
}
$cacheKey = 'entryScore-'.$entry->id.'-'.$mode;
return Cache::remember($cacheKey, 300, function () use ($mode, $entry) {
$this->basicValidation($mode, $entry);
$this->isEntryANoShow($entry);
$this->areAllJudgesIn($entry);
$this->areAllJudgesValid($entry);
$calculatedScores = $this->getJudgeTotals($mode, $entry);
CalculatedScore::create([
'entry_id' => $entry->id,
'mode' => $mode,
'calculatedScore' => $calculatedScores,
]);
return $calculatedScores;
// return $this->getJudgeTotals($mode, $entry);
});
}
protected function getJudgeTotals($mode, Entry $entry): array
{
$scores = [];
foreach ($this->auditionService->getJudges($entry->audition) as $judge) {
$scores[] = $this->calculator->__invoke($mode, $entry, $judge);
}
// sort the scores array by the total score
usort($scores, function ($a, $b) {
return $a[0] <=> $b[0];
});
// we can only really do olympic scoring if there are at least 3 scores
if (count($scores) >= 3 && auditionSetting('olympic_scoring')) {
// remove the highest and lowest scores
array_pop($scores);
array_shift($scores);
}
$sums = [];
// Sum each subscore from the judges
foreach ($scores as $score) {
$index = 0;
foreach ($score as $value) {
$sums[$index] = $sums[$index] ?? 0;
$sums[$index] += $value;
$index++;
}
}
// add the bonus points for a seating mode
if ($mode === 'seating' && $sums) {
$sums[0] += $this->getBonusPoints($entry);
}
return $sums;
}
protected function getBonusPoints(Entry $entry)
{
$bonusScoreDefinition = $entry->audition->bonusScore()->first();
if (! $bonusScoreDefinition) {
return 0;
}
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
$bonusJudges = $bonusScoreDefinition->judges;
$bonusScoreSheets = BonusScore::where('entry_id', $entry->id)->get();
foreach ($bonusScoreSheets as $sheet) {
if (! $bonusJudges->contains($sheet->user_id)) {
throw new TabulationException('Entry has a bonus score from unassigned judge');
}
}
// sum the score property of the $bonusScoreSheets
return $bonusScoreSheets->sum('score');
}
protected function basicValidation($mode, $entry): void
{
if ($mode !== 'seating' && $mode !== 'advancement') {
throw new TabulationException('Mode must be seating or advancement');
}
if (! $this->entryService->entryExists($entry)) {
throw new TabulationException('Invalid entry specified');
}
}
protected function areAllJudgesIn(Entry $entry): void
{
$assignedJudgeCount = $this->auditionService->getJudges($entry->audition)->count();
if ($entry->scoreSheets->count() !== $assignedJudgeCount) {
throw new TabulationException('Not all score sheets are in');
}
}
protected function areAllJudgesValid(Entry $entry): void
{
$validJudgeIds = $this->auditionService->getJudges($entry->audition)->pluck('id')->toArray();
$existingJudgeIds = $entry->scoreSheets->pluck('user_id')->toArray();
if (array_diff($existingJudgeIds, $validJudgeIds)) {
Log::debug('EntryID: '.$entry->id);
Log::debug('Valid judge ids: ('.gettype($validJudgeIds).') '.json_encode($validJudgeIds));
Log::debug('Existing judge ids: ('.gettype($existingJudgeIds).') '.json_encode($existingJudgeIds));
throw new TabulationException('Score exists from a judge not assigned to this audition');
}
}
protected function isEntryANoShow(Entry $entry): void
{
if ($entry->hasFlag('failed_prelim')) {
throw new TabulationException('Failed Prelim');
}
if ($entry->hasFlag('no_show')) {
throw new TabulationException('No Show');
}
}
}

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Audition;
use App\Models\Entry;
class CalculateAuditionScores
{
public function __construct()
{
}
public function __invoke(Audition $audition): void
{
$totaler = app(TotalEntryScores::class);
$scores_required = $audition->judges->count();
$pending_entries = Entry::where('audition_id', $audition->id)
->has('scoreSheets', '=', $scores_required)
->whereDoesntHave('totalScore')
->with('audition.scoringGuide.subscores')
->get();
foreach ($pending_entries as $entry) {
$totaler->__invoke($entry);
}
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Entry;
interface CalculateEntryScore
{
public function calculate(string $mode, Entry $entry): array;
}

View File

@ -0,0 +1,11 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Entry;
use App\Models\User;
interface CalculateScoreSheetTotal
{
public function __invoke(string $mode, Entry $entry, User $judge): array;
}

View File

@ -0,0 +1,67 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Models\ScoreSheet;
use App\Models\User;
use App\Services\AuditionService;
use App\Services\EntryService;
use App\Services\UserService;
class CalculateScoreSheetTotalDivideByTotalWeights implements CalculateScoreSheetTotal
{
protected AuditionService $auditionService;
protected EntryService $entryService;
protected UserService $userService;
public function __construct(AuditionService $auditionService, EntryService $entryService, UserService $userService)
{
$this->auditionService = $auditionService;
$this->entryService = $entryService;
$this->userService = $userService;
}
public function __invoke(string $mode, Entry $entry, User $judge): array
{
$this->basicValidations($mode, $entry, $judge);
$scoreSheet = ScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
if (! $scoreSheet) {
throw new TabulationException('No score sheet by that judge for that entry');
}
$subscores = $this->auditionService->getSubscores($entry->audition, $mode);
$scoreTotal = 0;
$weightsTotal = 0;
$scoreArray = [];
foreach ($subscores as $subscore) {
$weight = $subscore['weight'];
$score = $scoreSheet->subscores[$subscore->id]['score'];
$scoreArray[] = $score;
$scoreTotal += ($score * $weight);
$weightsTotal += $weight;
}
$finalScore = $scoreTotal / $weightsTotal;
// put $final score at the beginning of the $ScoreArray
array_unshift($scoreArray, $finalScore);
return $scoreArray;
}
protected function basicValidations($mode, $entry, $judge): void
{
if ($mode !== 'seating' and $mode !== 'advancement') {
throw new TabulationException('Invalid mode requested. Mode must be seating or advancement');
}
if (! $this->entryService->entryExists($entry)) {
throw new TabulationException('Invalid entry provided');
}
if (! $this->userService->userExists($judge)) {
throw new TabulationException('Invalid judge provided');
}
}
}

View File

@ -0,0 +1,74 @@
<?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Actions\Tabulation;
use App\Exceptions\TabulationException;
use App\Models\Entry;
use App\Models\ScoreSheet;
use App\Models\User;
use App\Services\AuditionService;
use App\Services\EntryService;
use App\Services\UserService;
class CalculateScoreSheetTotalDivideByWeightedPossible implements CalculateScoreSheetTotal
{
protected AuditionService $auditionService;
protected EntryService $entryService;
protected UserService $userService;
public function __construct(AuditionService $auditionService, EntryService $entryService, UserService $userService)
{
$this->auditionService = $auditionService;
$this->entryService = $entryService;
$this->userService = $userService;
}
public function __invoke(string $mode, Entry $entry, User $judge): array
{
$this->basicValidations($mode, $entry, $judge);
$scoreSheet = ScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
if (! $scoreSheet) {
throw new TabulationException('No score sheet by that judge for that entry');
}
$subscores = $this->auditionService->getSubscores($entry->audition, $mode);
$scoreTotal = 0;
$weightsTotal = 0;
$weightedMaxPossible = 0;
$scoreArray = [];
foreach ($subscores as $subscore) {
$weight = $subscore['weight'];
$score = $scoreSheet->subscores[$subscore->id]['score'];
$maxPossible = $subscore['max_score'];
$scoreArray[] = $score;
$scoreTotal += ($score * $weight);
$weightsTotal += $weight;
$weightedMaxPossible += $maxPossible;
}
if ($weightedMaxPossible > 0) {
$finalScore = ($scoreTotal / $weightedMaxPossible) * 100;
} else {
$finalScore = 0;
}
// put $final score at the beginning of the $ScoreArray
array_unshift($scoreArray, $finalScore);
return $scoreArray;
}
protected function basicValidations($mode, $entry, $judge): void
{
if ($mode !== 'seating' and $mode !== 'advancement') {
throw new TabulationException('Invalid mode requested. Mode must be seating or advancement');
}
if (! $this->entryService->entryExists($entry)) {
throw new TabulationException('Invalid entry provided');
}
if (! $this->userService->userExists($judge)) {
throw new TabulationException('Invalid judge provided');
}
}
}

View File

@ -1,61 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Entry;
class CheckPrelimResult
{
public function __construct()
{
}
/**
* @throws AuditionAdminException
*/
public function __invoke(Entry $entry, bool $recalc = false): string
{
if ($recalc) {
$entry->removeFlag('passed_prelim');
$entry->removeFlag('failed_prelim');
}
if (! $entry->exists) {
throw new AuditionAdminException('Entry does not exist');
}
if (! $entry->audition->prelimDefinition) {
throw new AuditionAdminException('Entry does not have a prelim');
}
if ($entry->hasFlag('failed_prelim') || $entry->hasFlag('passed_prelim')) {
return 'noChange';
}
if (! $entry->audition->prelimDefinition->room || $entry->audition->prelimDefinition->room->judges()->count() == 0) {
return 'noJudgesAssigned';
}
$scoresRequired = $entry->audition->prelimDefinition->room->judges()->count();
$scoresAssigned = $entry->prelimScoreSheets()->count();
if ($scoresAssigned < $scoresRequired) {
return 'missing'.$scoresRequired - $scoresAssigned.'scores';
}
$totalScore = 0;
foreach ($entry->prelimScoreSheets as $scoreSheet) {
$totalScore += $scoreSheet->total;
}
$averageScore = $totalScore / $scoresAssigned;
if ($averageScore >= $entry->audition->prelimDefinition->passing_score) {
$entry->addFlag('passed_prelim');
return 'markedPassed';
} else {
$entry->addFlag('failed_prelim');
return 'markedFailed';
}
}
}

View File

@ -1,85 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Doubler;
use App\Models\Event;
use App\Models\Student;
use function collect;
class DoublerSync
{
public function __construct()
{
}
/**
* Sync the Doubler records for the given event. If no event is provided, sync Doubler records for all events.
*/
public function __invoke(Event|int|null $event = null): void
{
if ($event) {
$this->syncForEvent($event);
} else {
$this->syncAllDoublers();
}
}
public function syncForEvent(Event|int $eventId): void
{
if ($eventId instanceof Event) {
$eventId = $eventId->id;
}
// Get students with multiple entries in this event's auditions
$studentsWithMultipleEntries = Student::query()
->select('students.id')
->join('entries', 'students.id', '=', 'entries.student_id')
->join('auditions', 'entries.audition_id', '=', 'auditions.id')
->where('auditions.event_id', $eventId)
->groupBy('students.id')
->havingRaw('COUNT(entries.id) > 1')
->with('entries')
->get();
Doubler::where('event_id', $eventId)->delete();
foreach ($studentsWithMultipleEntries as $student) {
// Get entries that are not declined. If only one, they're our accepted entry.
$entryList = collect(); // List of entry ids for th is student in this event
$undecidedEntries = collect(); // List of entry ids that are not declined, no-show, or failed prelim
$entryList = $student->entriesForEvent($eventId)->pluck('id');
$undecidedEntries = $student->entriesForEvent($eventId)->filter(function ($entry) {
return ! $entry->hasFlag('declined')
&& ! $entry->hasFlag('no_show')
&& ! $entry->hasFlag('failed_prelim');
})->pluck('id');
if ($undecidedEntries->count() < 2) {
$acceptedEntryId = $undecidedEntries->first();
} else {
$acceptedEntryId = null;
}
// Create or update the doubler record
Doubler::create([
'student_id' => $student->id,
'event_id' => $eventId,
'entries' => $entryList,
'accepted_entry' => $acceptedEntryId,
]);
}
// remove doubler records for students who no longer have multiple entries
Doubler::where('event_id', $eventId)
->whereNotIn('student_id', $studentsWithMultipleEntries->pluck('id'))
->delete();
}
public function syncAllDoublers(): void
{
$events = Event::all();
foreach ($events as $event) {
$this->syncForEvent($event);
}
}
}

View File

@ -4,27 +4,32 @@
namespace App\Actions\Tabulation; namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException; use App\Exceptions\ScoreEntryException;
use App\Models\BonusScore; use App\Models\BonusScore;
use App\Models\CalculatedScore;
use App\Models\Entry; use App\Models\Entry;
use App\Models\User; use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
class EnterBonusScore class EnterBonusScore
{ {
public function __construct()
{
}
public function __invoke(User $judge, Entry $entry, int $score): void public function __invoke(User $judge, Entry $entry, int $score): void
{ {
$getRelatedEntries = App::make(GetBonusScoreRelatedEntries::class); $getRelatedEntries = App::make(GetBonusScoreRelatedEntries::class);
// Verify there is a need for a bonus score $this->basicValidations($judge, $entry);
if ($entry->audition->bonusScore->count() === 0) {
throw new AuditionAdminException('The entries audition does not accept bonus scores');
}
$this->validateJudgeValidity($judge, $entry, $score); $this->validateJudgeValidity($judge, $entry, $score);
$entries = $getRelatedEntries($entry); $entries = $getRelatedEntries($entry);
// Create the score for each related entry // Create the score for each related entry
foreach ($entries as $relatedEntry) { foreach ($entries as $relatedEntry) {
// Also delete any cached scores // Also delete any cached scores
CalculatedScore::where('entry_id', $relatedEntry->id)->delete();
BonusScore::create([ BonusScore::create([
'entry_id' => $relatedEntry->id, 'entry_id' => $relatedEntry->id,
'user_id' => $judge->id, 'user_id' => $judge->id,
@ -35,18 +40,43 @@ class EnterBonusScore
} }
protected function getRelatedEntries(Entry $entry): Collection
{
$bonusScore = $entry->audition->bonusScore->first();
$relatedAuditions = $bonusScore->auditions;
// Get all entries that have a student_id equal to that of entry and an audition_id in the related auditions
return Entry::where('student_id', $entry->student_id)
->whereIn('audition_id', $relatedAuditions->pluck('id'))
->get();
}
protected function basicValidations(User $judge, Entry $entry): void
{
if (! $judge->exists) {
throw new ScoreEntryException('Invalid judge provided');
}
if (! $entry->exists) {
throw new ScoreEntryException('Invalid entry provided');
}
if ($entry->audition->bonusScore->count() === 0) {
throw new ScoreEntryException('Entry does not have a bonus score');
}
}
protected function validateJudgeValidity(User $judge, Entry $entry, $score): void protected function validateJudgeValidity(User $judge, Entry $entry, $score): void
{ {
if (BonusScore::where('entry_id', $entry->id)->where('user_id', $judge->id)->exists()) { if (BonusScore::where('entry_id', $entry->id)->where('user_id', $judge->id)->exists()) {
throw new AuditionAdminException('That judge has already scored that entry'); throw new ScoreEntryException('That judge has already scored that entry');
} }
$bonusScore = $entry->audition->bonusScore->first(); $bonusScore = $entry->audition->bonusScore->first();
if (! $bonusScore->judges->contains($judge)) { if (! $bonusScore->judges->contains($judge)) {
throw new AuditionAdminException('That judge is not assigned to judge that bonus score'); throw new ScoreEntryException('That judge is not assigned to judge that bonus score');
} }
if ($score > $bonusScore->max_score) { if ($score > $bonusScore->max_score) {
throw new AuditionAdminException('That score exceeds the maximum'); throw new ScoreEntryException('That score exceeds the maximum');
} }
} }
} }

View File

@ -1,65 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\BonusScore;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\ScoreSheet;
use Illuminate\Support\Facades\DB;
use function auditionLog;
class EnterNoShow
{
/**
* Handles the no-show or failed-prelim flagging for a given entry.
*
* This method ensures the specified flag type is valid and validates
* that the action can be performed based on the associated audition's state.
* Deletes related score records and applies the specified flag ('no_show'
* or 'failed_prelim') to the entry, returning a success message.
*
* @param Entry $entry The entry being flagged.
* @param string $flagType The type of flag to apply ('no-show' or 'failed-prelim').
* @return string A confirmation message about the flagging operation.
*
* @throws AuditionAdminException If an invalid flag type is provided,
* or the action violates business rules.
*/
public function __invoke(Entry $entry, string $flagType = 'noshow'): string
{
if ($flagType !== 'noshow' && $flagType !== 'failprelim') {
throw new AuditionAdminException('Invalid flag type');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot enter a no-show for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot enter a no-show for an entry in an audition where advancement is published');
}
DB::table('score_sheets')->where('entry_id', $entry->id)->delete();
ScoreSheet::where('entry_id', $entry->id)->delete();
BonusScore::where('entry_id', $entry->id)->delete();
EntryTotalScore::where('entry_id', $entry->id)->delete();
if ($flagType == 'failprelim') {
$msg = 'Failed prelim has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').';
$entry->addFlag('failed_prelim');
} else {
$entry->addFlag('no_show');
$msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').';
}
$affected = [
['entries', [$entry->id]],
['auditions', [$entry->audition_id]],
['students', [$entry->student_id]],
];
auditionLog($msg, $affected);
return $msg;
}
}

View File

@ -1,137 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use App\Models\User;
use DB;
use function auditionLog;
class EnterPrelimScore
{
public function __invoke(
User $user,
Entry $entry,
array $scores,
PrelimScoreSheet|false $prelimScoreSheet = false
): PrelimScoreSheet {
$scores = collect($scores);
// Basic Validity Checks
if (! User::where('id', $user->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();
// Don't allow changes to prelims scores if the entry has a finals score
if ($entry->scoreSheets()->count() > 0) {
throw new AuditionAdminException('Cannot change prelims scores for an entry that has finals scores');
}
// 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.'.<br />';
$log_message .= 'Judge: '.$user->full_name().'<br />';
foreach ($prelimScoreSheet->subscores as $subscore) {
$log_message .= $subscore['subscore_name'].': '.$subscore['score'].'<br />';
}
$log_message .= 'Total :'.$prelimScoreSheet->total.'<br />';
auditionLog($log_message, [
'entries' => [$entry->id],
'users' => [$user->id],
'auditions' => [$entry->audition_id],
'students' => [$entry->student_id],
]);
// Check if we can make a status decision
$checker = app(CheckPrelimResult::class);
$checker($entry, true);
return $prelimScoreSheet;
}
}

View File

@ -6,151 +6,121 @@
namespace App\Actions\Tabulation; namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ScoreEntryException; use App\Exceptions\ScoreEntryException;
use App\Models\AuditLogEntry; use App\Models\CalculatedScore;
use App\Models\Entry; use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use function auth;
class EnterScore class EnterScore
{ {
/** /**
* @param User $user A user acting as the judge for this sheet * @param User $user A user acting as the judge for this sheet
* @param Entry $entry An entry to which this score should be assigned * @param Entry $entry An entry to which this score should be assigned
* @param array $scores Scores to be entered in the form of SubscoreID => score * @param array $scores Scores to be entered in the form of SubscoreID => score
* @param ScoreSheet|false $scoreSheet If this is an update to an existing scoresheet, pass it here
* @return ScoreSheet The scoresheet that was created or updated
* *
* @throws ScoreEntryException * @throws ScoreEntryException
*/ */
public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet public function __invoke(User $user, Entry $entry, array $scores, ScoreSheet|false $scoreSheet = false): ScoreSheet
{ {
EntryTotalScore::where('entry_id', $entry->id)->delete(); CalculatedScore::where('entry_id', $entry->id)->delete();
$scores = collect($scores); $scores = collect($scores);
$this->basicChecks($user, $entry, $scores);
// Basic Validity Checks $this->checkJudgeAssignment($user, $entry);
if (! User::where('id', $user->id)->exists()) { $this->checkForExistingScore($user, $entry, $scoreSheet);
throw new AuditionAdminException('User does not exist'); $this->validateScoresSubmitted($entry, $scores);
}
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');
}
if ($entry->audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot score an entry in an audition where advancement is published');
}
// Check that the specified user is assigned to judge this entry
$check = DB::table('room_user')
->where('room_id', $entry->audition->room_id)
->where('user_id', $user->id)->exists();
if (! $check) {
throw new AuditionAdminException('This judge is not assigned to judge this entry');
}
// Check if a score already exists
if (! $scoreSheet) {
if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
throw new AuditionAdminException('That judge has already entered scores for that entry');
}
} else {
if ($scoreSheet->user_id !== $user->id) {
throw new AuditionAdminException('Existing score sheet is from a different judge');
}
if ($scoreSheet->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 = $entry->audition->scoringGuide->subscores;
$subscoresStorageArray = [];
$seatingTotal = 0;
$seatingMaxPossible = 0;
$advancementTotal = 0;
$advancementMaxPossible = 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,
];
// If included in seating, multiply by weight and add to the total and max possible
if ($subscore->for_seating) {
$seatingTotal += ($subscore->weight * $scores[$subscore->id]);
$seatingMaxPossible += ($subscore->weight * $subscore->max_score);
}
// If included in advancement, multiply by weight and add to the total and max possible
if ($subscore->for_advance) {
$advancementTotal += ($subscore->weight * $scores[$subscore->id]);
$advancementMaxPossible += ($subscore->weight * $subscore->max_score);
}
}
$finalSeatingTotal = ($seatingMaxPossible === 0) ? 0 : (($seatingTotal / $seatingMaxPossible) * 100);
$finalAdvancementTotal = ($advancementMaxPossible === 0) ? 0 : (($advancementTotal / $advancementMaxPossible) * 100);
$entry->removeFlag('no_show'); $entry->removeFlag('no_show');
if ($scoreSheet instanceof ScoreSheet) { if ($scoreSheet instanceof ScoreSheet) {
$scoreSheet->update([ $scoreSheet->update([
'user_id' => $user->id, 'user_id' => $user->id,
'entry_id' => $entry->id, 'entry_id' => $entry->id,
'subscores' => $subscoresStorageArray, 'subscores' => $this->subscoresForStorage($entry, $scores),
'seating_total' => $finalSeatingTotal,
'advancement_total' => $finalAdvancementTotal,
]); ]);
} else { } else {
$scoreSheet = ScoreSheet::create([ $scoreSheet = ScoreSheet::create([
'user_id' => $user->id, 'user_id' => $user->id,
'entry_id' => $entry->id, 'entry_id' => $entry->id,
'subscores' => $subscoresStorageArray, 'subscores' => $this->subscoresForStorage($entry, $scores),
'seating_total' => $finalSeatingTotal,
'advancement_total' => $finalAdvancementTotal,
]); ]);
} }
// Log the score entry
$log_message = 'Entered Score for entry id '.$entry->id.'.<br />';
$log_message .= 'Judge: '.$user->full_name().'<br />';
foreach ($scoreSheet->subscores as $subscore) {
$log_message .= $subscore['subscore_name'].': '.$subscore['score'].'<br />';
}
$log_message .= 'Seating Total: '.$scoreSheet->seating_total.'<br />';
$log_message .= 'Advancement Total: '.$scoreSheet->advancement_total.'<br />';
AuditLogEntry::create([
'user' => auth()->user()->email ?? 'no user',
'ip_address' => request()->ip(),
'message' => $log_message,
'affected' => [
'entries' => [$entry->id],
'users' => [$user->id],
'auditions' => [$entry->audition_id],
'students' => [$entry->student_id],
],
]);
return $scoreSheet; return $scoreSheet;
} }
protected function subscoresForStorage(Entry $entry, Collection $scores)
{
$subscores = [];
foreach ($entry->audition->scoringGuide->subscores as $subscore) {
$subscores[$subscore->id] = [
'score' => $scores[$subscore->id],
'subscore_id' => $subscore->id,
'subscore_name' => $subscore->name,
];
}
return $subscores;
}
protected function checkForExistingScore(User $user, Entry $entry, $existingScoreSheet)
{
if (! $existingScoreSheet) {
if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
throw new ScoreEntryException('That judge has already entered scores for that entry');
}
} else {
if ($existingScoreSheet->user_id !== $user->id) {
throw new ScoreEntryException('Existing score sheet is from a different judge');
}
if ($existingScoreSheet->entry_id !== $entry->id) {
throw new ScoreEntryException('Existing score sheet is for a different entry');
}
}
}
protected function validateScoresSubmitted(Entry $entry, Collection $scores)
{
$subscoresRequired = $entry->audition->scoringGuide->subscores;
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 ScoreEntryException('Invalid Score Submission');
}
if ($scores[$subscore->id] > $subscore->max_score) {
throw new ScoreEntryException('Supplied subscore exceeds maximum allowed');
}
}
}
protected function checkJudgeAssignment(User $user, Entry $entry)
{
$check = DB::table('room_user')
->where('room_id', $entry->audition->room_id)
->where('user_id', $user->id)->exists();
if (! $check) {
throw new ScoreEntryException('This judge is not assigned to judge this entry');
}
}
protected function basicChecks(User $user, Entry $entry, Collection $scores)
{
if (! $user->exists()) {
throw new ScoreEntryException('User does not exist');
}
if (! $entry->exists()) {
throw new ScoreEntryException('Entry does not exist');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new ScoreEntryException('Cannot score an entry in an audition with published seats');
}
if ($entry->audition->hasFlag('advancement_published')) {
throw new ScoreEntryException('Cannot score an entry in an audition with published advancement');
}
$requiredScores = $entry->audition->scoringGuide->subscores()->count();
if ($scores->count() !== $requiredScores) {
throw new ScoreEntryException('Invalid number of scores');
}
}
} }

View File

@ -1,19 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\Entry;
/**
* @codeCoverageIgnore
*/
class ForceRecalculateTotalScores
{
public function __invoke(): void
{
$calculator = app(TotalEntryScores::class);
foreach (Entry::all() as $entry) {
$calculator($entry, true);
}
}
}

View File

@ -2,15 +2,11 @@
namespace App\Actions\Tabulation; namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Ensemble; use App\Models\Ensemble;
use App\Models\Seat; use App\Models\Seat;
use function dd;
/**
* @codeCoverageIgnore
*/
// TODO delete if truly depricated
class GetAuditionSeats class GetAuditionSeats
{ {
public function __construct() public function __construct()
@ -24,7 +20,6 @@ class GetAuditionSeats
protected function getSeats(Audition $audition) protected function getSeats(Audition $audition)
{ {
throw new AuditionAdminException('This method is being considered for deletion.');
$ensembles = Ensemble::where('event_id', $audition->event_id)->orderBy('rank')->get(); $ensembles = Ensemble::where('event_id', $audition->event_id)->orderBy('rank')->get();
$seats = Seat::with('student.school')->where('audition_id', $audition->id)->orderBy('seat')->get(); $seats = Seat::with('student.school')->where('audition_id', $audition->id)->orderBy('seat')->get();
$return = []; $return = [];

View File

@ -2,7 +2,6 @@
namespace App\Actions\Tabulation; namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Seat; use App\Models\Seat;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
@ -14,32 +13,8 @@ class PublishSeats
// //
} }
/**
* Publishes the given audition with the provided seats.
*
* This method first validates that the seats array is not empty. If the array is empty,
* an AuditionAdminException is thrown.
*
* Next, it deletes existing records in the `seats` table associated with the provided audition
* using the `audition_id`.
*
* Then, it iterates through the provided seats array to create new records in the `seats` table
* with the specified `ensemble_id`, `audition_id`, `seat`, and `entry_id`.
*
* Finally, it marks the audition as having its seats published by adding a relevant flag
* to the audition, and clears cached data associated with the results seat list and
* public results page entries in the cache store.
*
* @param Audition $audition The audition instance to be published.
* @param array $seats An array of seat data to be associated with the audition.
*
* @throws AuditionAdminException If the provided seats array is empty.
*/
public function __invoke(Audition $audition, array $seats): void public function __invoke(Audition $audition, array $seats): void
{ {
if (count($seats) === 0) {
throw new AuditionAdminException('Cannot publish an audition with no seats.');
}
// Delete from the seats table where audition_id = $audition->id // Delete from the seats table where audition_id = $audition->id
Seat::where('audition_id', $audition->id)->delete(); Seat::where('audition_id', $audition->id)->delete();
foreach ($seats as $seat) { foreach ($seats as $seat) {

View File

@ -4,115 +4,105 @@
namespace App\Actions\Tabulation; namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException; use App\Exceptions\TabulationException;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Entry; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache;
use function is_numeric;
class RankAuditionEntries class RankAuditionEntries
{ {
protected CalculateEntryScore $calculator;
public function __construct(CalculateEntryScore $calculator)
{
$this->calculator = $calculator;
}
public function rank(string $mode, Audition $audition): Collection
{
$cacheKey = 'audition'.$audition->id.$mode;
return Cache::remember($cacheKey, 300, function () use ($mode, $audition) {
return $this->calculateRank($mode, $audition);
});
}
/** /**
* Get ranked entries for the provided audition for either seating or advancement. * For a given audition, return a collection of entries ranked by total score. Each entry will have a
* property rank that either is their rank or a flag reflecting no-show, declined, or failed-prelim status
* *
* If the rank_type is seating, the ranked entries are returned in descending order of seating total. * @throws TabulationException
* If the rank_type is advancement, the ranked entries are returned in descending order of advancement total.
*
* The ranked entries are returned as a Collection of Entry objects.
*
* @param string $rank_type advancement|seating
* @return Collection<Entry>|void
*
* @throws AuditionAdminException
*/ */
public function __invoke(Audition $audition, string $rank_type, bool $pullDeclinedEntries = true): Collection|Entry public function calculateRank(string $mode, Audition $audition): Collection
{ {
if ($rank_type !== 'seating' && $rank_type !== 'advancement') { $this->basicValidation($mode, $audition);
throw new AuditionAdminException('Invalid rank type (must be seating or advancement)'); $entries = match ($mode) {
'seating' => $audition->entries()->forSeating()->with('scoreSheets')->withCount('bonusScores')->get(),
'advancement' => $audition->entries()->forAdvancement()->with('scoreSheets')->get(),
};
foreach ($entries as $entry) {
$entry->setRelation('audition', $audition);
try {
$entry->score_totals = $this->calculator->calculate($mode, $entry);
} catch (TabulationException $ex) {
$entry->score_totals = [-1];
$entry->score_message = $ex->getMessage();
}
}
// Sort entries based on their total score, then by subscores in tiebreak order
$entries = $entries->sort(function ($a, $b) {
for ($i = 0; $i < count($a->score_totals); $i++) {
if (! array_key_exists($i, $a->score_totals)) {
return -1;
}
if (! array_key_exists($i, $b->score_totals)) {
return -1;
}
if ($a->score_totals[$i] > $b->score_totals[$i]) {
return -1;
} elseif ($a->score_totals[$i] < $b->score_totals[$i]) {
return 1;
}
} }
$cache_duration = 15; return 0;
if ($rank_type === 'seating') {
return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition, $pullDeclinedEntries) {
return $this->get_seating_ranks($audition, $pullDeclinedEntries);
}); });
$rank = 1;
$rawRank = 1;
foreach ($entries as $entry) {
$entry->rank = $rank;
$entry->raw_rank = $rawRank;
// We don't really get a rank for seating if we have certain flags
if ($mode === 'seating') {
if ($entry->hasFlag('failed_prelim')) {
$entry->rank = 'Failed Prelim';
} elseif ($entry->hasFlag('declined')) {
$entry->rank = 'Declined';
} elseif ($entry->hasFlag('no_show')) {
$entry->rank = 'No Show';
}
} }
return cache()->remember('rank_advancement_'.$audition->id, $cache_duration, function () use ($audition) { if (is_numeric($entry->rank)) {
return $this->get_advancement_ranks($audition); $rank++;
}); }
$rawRank++;
} }
private function get_seating_ranks(Audition $audition, bool $pullDeclinedEntries = true): Collection|Entry return $entries;
}
protected function basicValidation($mode, Audition $audition): void
{ {
if ($audition->bonusScore()->count() > 0) { if ($mode !== 'seating' && $mode !== 'advancement') {
$totalColumn = 'seating_total_with_bonus'; throw new TabulationException('Mode must be seating or advancement');
} else {
$totalColumn = 'seating_total';
} }
if (! $audition->exists()) {
$sortedEntries = $audition->entries() throw new TabulationException('Invalid audition provided');
->where('for_seating', true)
->whereHas('totalScore')
->with('totalScore')
->with('student.school')
->with('audition')
->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id')
->orderBy('entry_total_scores.'.$totalColumn, 'desc')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[0]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[1]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[2]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[3]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[4]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[5]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[6]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[7]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[8]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.seating_subscore_totals, "$[9]"), -999999) DESC')
->select('entries.*')
->get();
$rankOn = 1;
foreach ($sortedEntries as $entry) {
if ($entry->hasFlag('declined') && $pullDeclinedEntries) {
$entry->seatingRank = 'declined';
} else {
$entry->seatingRank = $rankOn;
$rankOn++;
} }
} }
return $sortedEntries;
}
private function get_advancement_ranks(Audition $audition): Collection|Entry
{
$sortedEntries = $audition->entries()
->whereHas('totalScore')
->with('totalScore')
->with('student.school')
->with('audition')
->join('entry_total_scores', 'entries.id', '=', 'entry_total_scores.entry_id')
->orderBy('entry_total_scores.advancement_total', 'desc')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[0]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[1]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[2]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[3]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[4]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[5]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[6]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[7]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[8]"), -999999) DESC')
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC')
->select('entries.*')
->get();
$n = 1;
foreach ($sortedEntries as $entry) {
$entry->advancementRank = $n;
$n++;
}
return $sortedEntries;
}
} }

View File

@ -1,92 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Models\BonusScore;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\ScoreSheet;
/**
* Handles the calculation of a total score for an entry, including seating and advancement scores,
* based on scoring sheets and subscores defined in the audition's scoring guide.
*/
class TotalEntryScores
{
public function __construct()
{
}
public function __invoke(Entry $entry, bool $force_recalculation = false): void
{
if ($force_recalculation) {
EntryTotalScore::where('entry_id', $entry->id)->delete();
}
// bail out if a total score is already calculated
if (EntryTotalScore::where('entry_id', $entry->id)->count() > 0) {
return;
}
$requiredSubscores = $entry->audition->scoringGuide->subscores;
$newTotaledScore = EntryTotalScore::make();
$newTotaledScore->entry_id = $entry->id;
// deal with seating scores
// TODO: Consider a rewrite to pull the scoreSheets from the entry model so they may be preloaded
$scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('seating_total', 'desc')->get();
// bail out if there are not enough score sheets
$assignedJudges = $entry->audition->judges()->count();
if ($scoreSheets->count() == 0 || $scoreSheets->count() < $assignedJudges) {
return;
}
if (auditionSetting('olympic_scoring') && $scoreSheets->count() > 2) {
// under olympic scoring, drop the first and last element
$scoreSheets->shift();
$scoreSheets->pop();
}
$newTotaledScore->seating_total = round($scoreSheets->avg('seating_total'), 6);
$seatingSubscores = $requiredSubscores
->filter(fn ($subscore) => $subscore->for_seating == true)
->sortBy('tiebreak_order');
$total_seating_subscores = [];
foreach ($seatingSubscores as $subscore) {
$runningTotal = 0;
foreach ($scoreSheets as $scoreSheet) {
$runningTotal += $scoreSheet->subscores[$subscore->id]['score'];
}
$total_seating_subscores[] = round($runningTotal / $scoreSheets->count(), 4);
}
$newTotaledScore->seating_subscore_totals = $total_seating_subscores;
// deal with advancement scores
$scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('advancement_total', 'desc')->get();
if (auditionSetting('olympic_scoring') && $scoreSheets->count() > 2) {
// under olympic scoring, drop the first and last element
$scoreSheets->shift();
$scoreSheets->pop();
}
$newTotaledScore->advancement_total = round($scoreSheets->avg('advancement_total'), 6);
$advancement_subscores = $requiredSubscores
->filter(fn ($subscore) => $subscore->for_advance == true)
->sortBy('tiebreak_order');
$total_advancement_subscores = [];
foreach ($advancement_subscores as $subscore) {
$runningTotal = 0;
foreach ($scoreSheets as $scoreSheet) {
$runningTotal += $scoreSheet->subscores[$subscore->id]['score'];
}
$total_advancement_subscores[] = round($runningTotal / $scoreSheets->count(), 4);
}
$newTotaledScore->advancement_subscore_totals = $total_advancement_subscores;
// pull in bonus scores
$bonusScores = BonusScore::where('entry_id', $entry->id)
->selectRaw('SUM(score) as total')
->value('total');
$newTotaledScore->bonus_total = $bonusScores;
$newTotaledScore->save();
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Actions\YearEndProcedures;
use App\Exceptions\AuditionAdminException;
use App\Models\HistoricalSeat;
use App\Models\Seat;
use Carbon\Carbon;
class RecordHistoricalSeats
{
public function __invoke(): void
{
$this->saveSeats();
}
/**
* @throws AuditionAdminException
*/
public function saveSeats()
{
if (! auth()->user() or ! auth()->user()->is_admin) {
throw new AuditionAdminException('Only administrators may perform this action');
}
$seats = Seat::all();
if ($seats->count() > 0) {
foreach ($seats as $seat) {
$student_id = $seat->student->id;
$year = Carbon::now()->year;
$seat_description = $seat->ensemble->name.' - '.$seat->audition->name.' - '.$seat->seat;
HistoricalSeat::create([
'student_id' => $student_id,
'year' => $year,
'seat_description' => $seat_description,
]);
}
}
return true;
}
}

View File

@ -1,88 +0,0 @@
<?php
namespace App\Actions\YearEndProcedures;
use App\Exceptions\AuditionAdminException;
use App\Models\AuditionFlag;
use App\Models\AuditLogEntry;
use App\Models\BonusScore;
use App\Models\Doubler;
use App\Models\DoublerRequest;
use App\Models\EntryFlag;
use App\Models\EntryTotalScore;
use App\Models\JudgeAdvancementVote;
use App\Models\NominationEnsembleEntry;
use App\Models\ScoreSheet;
use App\Models\Seat;
use App\Models\Student;
use App\Models\UserFlag;
use Illuminate\Support\Facades\DB;
use function auth;
/**
* @codeCoverageIgnore
*/
// TODO: figure out how to test YearEndCleanup
class YearEndCleanup
{
public function __invoke(?array $options = []): void
{
$this->cleanup($options);
}
/**
* @param $options array array of reset options - possible values are deleteRooms
* removeAuditionsFromRoom unassignJudges
*
* @throws AuditionAdminException
*/
public function cleanup(?array $options = []): true
{
if (! auth()->user() or ! auth()->user()->is_admin) {
throw new AuditionAdminException('Only administrators may perform this action');
}
$historian = new RecordHistoricalSeats;
$historian();
// Delete all records in the audit_log_entries table
AuditLogEntry::truncate();
AuditionFlag::truncate();
BonusScore::truncate();
EntryTotalScore::truncate();
DoublerRequest::truncate();
Doubler::truncate();
EntryFlag::truncate();
ScoreSheet::truncate();
Seat::truncate();
JudgeAdvancementVote::truncate();
DB::table('entries')->delete();
NominationEnsembleEntry::truncate();
Student::query()->increment('grade');
if (is_array($options)) {
if (in_array('deleteRooms', $options)) {
DB::table('auditions')->update(['room_id' => 0]);
DB::table('auditions')->update(['order_in_room' => '0']);
DB::table('room_user')->truncate();
DB::table('rooms')->where('id', '>', 0)->delete();
}
if (in_array('removeAuditionsFromRoom', $options)) {
DB::table('auditions')->update(['room_id' => 0]);
DB::table('auditions')->update(['order_in_room' => '0']);
}
if (in_array('unassignJudges', $options)) {
DB::table('room_user')->truncate();
UserFlag::where('flag', 'monitor')->delete();
}
}
return true;
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Actions\Tabulation\EnterScore;
use App\Models\ScoreSheet;
use Illuminate\Console\Command;
class RecalculateJudgeTotalsCommand extends Command
{
protected $signature = 'audition:recalculate-judge-totals';
protected $description = 'Recalculates total scores for all score sheets for unpubished auditions';
public function handle(): void
{
$this->info('Starting score recalculation...');
$scoreSheets = ScoreSheet::all();
foreach ($scoreSheets as $scoreSheet) {
if ($scoreSheet->entry->audition->hasFlag('seats_published')) {
continue;
}
$this->recalculate($scoreSheet);
}
$this->info('Score recalculation completed successfully.');
}
private function recalculate(ScoreSheet|int $scoreSheet): void
{
if (is_int($scoreSheet)) {
$scoreSheet = ScoreSheet::findOrFail($scoreSheet);
}
$scribe = app()->make(EnterScore::class);
$scoreSubmission = [];
foreach ($scoreSheet->subscores as $subscore) {
$scoreSubmission[$subscore['subscore_id']] = $subscore['score'];
}
$scribe($scoreSheet->judge, $scoreSheet->entry, $scoreSubmission, $scoreSheet);
}
}

View File

@ -1,38 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Actions\Tabulation\ForceRecalculateTotalScores;
use Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class RecalculateTotalScores extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:recalculate-total-scores';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Forces the recalculation of total scores for all entries';
/**
* Execute the console command.
*/
public function handle(ForceRecalculateTotalScores $action): void
{
$this->info('Starting score recalculation...');
$action();
$this->info('Score recalculation completed successfully.');
}
}

View File

@ -1,43 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Actions\Tabulation\DoublerSync;
use App\Models\Event;
use Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class SyncDoublers extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:sync-doublers {event? : Optional event ID}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update doublers table based on current entries';
/**
* Execute the console command.
*/
public function handle()
{
$syncer = app(DoublerSync::class);
if ($eventId = $this->argument('event')) {
$event = Event::findOrFail($eventId);
$syncer($event);
$this->info("Synced doublers for event {$event->name}");
} else {
$syncer();
$this->info('Synced doublers for all events');
}
}
}

View File

@ -1,87 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\School;
use App\Models\Student;
use App\Models\User;
use Faker\Factory;
use Illuminate\Console\Command;
class fictionalize extends Command
{
protected $signature = 'audition:fictionalize
{--students : Fictionalize student names}
{--schools : Fictionalize school names}
{--users : Fictionalize user data}
{--all : Fictionalize all data types}';
protected $description = 'Replace real names with fictional data for specified entity types';
public function handle()
{
$faker = Factory::create();
// If no options are specified or --all is used, process everything
$processAll = $this->option('all') ||
(! $this->option('students') && ! $this->option('schools') && ! $this->option('users'));
if ($processAll || $this->option('students')) {
$this->info('Fictionalizing students...');
$bar = $this->output->createProgressBar(Student::count());
Student::chunk(100, function ($students) use ($faker, $bar) {
foreach ($students as $student) {
$student->update([
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
if ($processAll || $this->option('schools')) {
$this->info('Fictionalizing schools...');
$bar = $this->output->createProgressBar(School::count());
School::chunk(100, function ($schools) use ($faker, $bar) {
foreach ($schools as $school) {
$school->update([
'name' => $faker->city().' High School',
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
if ($processAll || $this->option('users')) {
$this->info('Fictionalizing users...');
$bar = $this->output->createProgressBar(User::where('email', '!=', 'matt@mattyoung.us')->count());
User::where('email', '!=', 'matt@mattyoung.us')
->chunk(100, function ($users) use ($faker, $bar) {
foreach ($users as $user) {
$user->update([
'email' => $faker->unique()->email(),
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
'cell_phone' => $faker->phoneNumber(),
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
$this->info('Fictionalization complete!');
}
}

View File

@ -1,124 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Audition;
use App\Models\Event;
use App\Models\Room;
use App\Models\ScoringGuide;
use App\Services\CsvImportService;
use Carbon\Carbon;
use Illuminate\Console\Command;
use function auditionSetting;
use function Laravel\Prompts\select;
class importCheckAuditionsCommand extends Command
{
protected $signature = 'import:check-auditions';
protected $description = 'Check the import file for auditions that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$lowestPossibleGrade = 1;
$highestPossibleGrade = 12;
$events = Event::all();
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedAuditions = collect();
foreach ($rows as $row) {
if ($checkedAuditions->contains($row['Instrument'])) {
continue;
}
$checkedAuditions->push($row['Instrument']);
if (Audition::where('name', $row['Instrument'])->count() > 0) {
$this->info('Audition '.$row['Instrument'].' already exists');
} else {
$this->newLine();
$this->alert('Audition '.$row['Instrument'].' does not exist');
if ($events->count() === 1) {
$newEventId = $events->first()->id;
} else {
$newEventId = select(
label: 'Which event does this audition belong to?',
options: $events->pluck('name', 'id')->toArray(),
);
}
$newEventName = $row['Instrument'];
$newEventScoreOrder = Audition::max('score_order') + 1;
$newEventEntryDeadline = Carbon::yesterday('America/Chicago')->format('Y-m-d');
$newEventEntryFee = Audition::max('entry_fee');
$newEventMinimumGrade = select(
label: 'What is the minimum grade for this audition?',
options: range($lowestPossibleGrade, $highestPossibleGrade)
);
$newEventMaximumGrade = select(
label: 'What is the maximum grade for this audition?',
options: range($newEventMinimumGrade, $highestPossibleGrade)
);
$newEventRoomId = select(
label: 'Which room does this audition belong to?',
options: Room::pluck('name', 'id')->toArray(),
);
$newEventScoringGuideId = select(
label: 'Which scoring guide should this audition use',
options: ScoringGuide::pluck('name', 'id')->toArray(),
);
if (auditionSetting('advanceTo')) {
$newEventForSeating = select(
label: 'Is this audition for seating?',
options: [
1 => 'Yes',
0 => 'No',
]
);
$newEventForAdvance = select(
label: 'Is this audition for '.auditionSetting('advanceTo').'?',
options: [
1 => 'Yes',
0 => 'No',
]
);
} else {
$newEventForSeating = 1;
$newEventForAdvance = 0;
}
$this->info('New event ID: '.$newEventId);
$this->info('New event name: '.$newEventName);
$this->info('New event score order: '.$newEventScoreOrder);
$this->info('New event entry deadline: '.$newEventEntryDeadline);
$this->info('New event entry fee: '.$newEventEntryFee);
$this->info('New event minimum grade: '.$newEventMinimumGrade);
$this->info('New event maximum grade: '.$newEventMaximumGrade);
$this->info('New event room ID: '.$newEventRoomId);
$this->info('New event scoring guide ID: '.$newEventScoringGuideId);
$this->info('New event for seating: '.$newEventForSeating);
$this->info('New event for advance: '.$newEventForAdvance);
Audition::create([
'event_id' => $newEventId,
'name' => $newEventName,
'score_order' => $newEventScoreOrder,
'entry_deadline' => $newEventEntryDeadline,
'entry_fee' => $newEventEntryFee,
'minimum_grade' => $newEventMinimumGrade,
'maximum_grade' => $newEventMaximumGrade,
'room_id' => $newEventRoomId,
'scoring_guide_id' => $newEventScoringGuideId,
'for_seating' => $newEventForSeating,
'for_advancement' => $newEventForAdvance,
]);
}
}
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Console\Commands;
use const PHP_EOL;
use App\Models\School;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importCheckSchoolsCommand extends Command
{
protected $signature = 'import:check-schools';
protected $description = 'Check the import file for schools that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedSchools = collect();
foreach ($rows as $row) {
if ($checkedSchools->contains($row['School'])) {
continue;
}
$checkedSchools->push($row['School']);
if (School::where('name', $row['School'])->count() > 0) {
$this->info('School '.$row['School'].' already exists');
} else {
$this->newLine();
$this->alert('School '.$row['School'].' does not exist'.PHP_EOL.'Creating school...');
School::create(['name' => $row['School']]);
$this->info('School '.$row['School'].' created');
}
}
}
}

View File

@ -1,67 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importCheckStudentsCommand extends Command
{
protected $signature = 'import:check-students';
protected $description = 'Check the import file for students that do not exist in the database';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$purge = $this->confirm('Do you want to purge the database of existing students and entries?', false);
if ($purge) {
Entry::all()->map(function ($entry) {
$entry->delete();
});
Student::all()->map(function ($student) {
$student->delete();
});
$this->info('Database purged');
}
$schools = School::pluck('id', 'name');
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
$checkedStudents = collect();
foreach ($rows as $row) {
$uniqueData = $row['School'].$row['LastName'].$row['LastName'];
if ($checkedStudents->contains($uniqueData)) {
// continue;
}
$checkedStudents->push($uniqueData);
$currentFirstName = $row['FirstName'];
$currentLastName = $row['LastName'];
$currentSchoolName = $row['School'];
$currentSchoolId = $schools[$currentSchoolName];
if (Student::where('first_name', $currentFirstName)->where('last_name',
$currentLastName)->where('school_id', $currentSchoolId)->count() > 0) {
$this->info('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' already exists');
} else {
$this->alert('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' does not exist');
$newStudent = Student::create([
'school_id' => $currentSchoolId,
'first_name' => $currentFirstName,
'last_name' => $currentLastName,
'grade' => $row['Grade'],
]);
$this->info('Student '.$currentFirstName.' '.$currentLastName.' from '.$currentSchoolName.' created with id of: '.$newStudent->id);
}
}
}
}

View File

@ -1,74 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\School;
use App\Models\Student;
use App\Services\CsvImportService;
use Illuminate\Console\Command;
class importImportEntriesCommand extends Command
{
protected $signature = 'import';
protected $description = 'Import entries from the import.csv file. First check schools, then students, then auditions, then run this import command';
protected $csvImporter;
public function __construct(CsvImportService $csvImporter)
{
parent::__construct();
$this->csvImporter = $csvImporter;
}
public function handle(): void
{
$checkAuditions = $this->confirm('Do you want to check the auditions in the import for validity first?', true);
if ($checkAuditions) {
$this->call('import:check-auditions');
}
$checkSchools = $this->confirm('Do you want to check the schools in the import for validity first?', true);
if ($checkSchools) {
$this->call('import:check-schools');
}
$checkStudents = $this->confirm('Do you want to check the students in the import for validity first?', true);
if ($checkStudents) {
$this->call('import:check-students');
}
$purge = $this->confirm('Do you want to purge the database of existing entries?', false);
if ($purge) {
Entry::all()->map(function ($entry) {
$entry->delete();
});
$this->info('Database purged');
}
$schools = School::pluck('id', 'name');
$auditions = Audition::pluck('id', 'name');
$rows = $this->csvImporter->readCsv(storage_path('app/import/import.csv'));
foreach ($rows as $row) {
$schoolId = $schools[$row['School']];
$student = Student::where('first_name', $row['FirstName'])->where('last_name',
$row['LastName'])->where('school_id', $schoolId)->first();
if (! $student) {
$this->error('Student '.$row['FirstName'].' '.$row['LastName'].' from '.$row['School'].' does not exist');
return;
}
$auditionId = $auditions[$row['Instrument']];
try {
Entry::create([
'student_id' => $student->id,
'audition_id' => $auditionId,
]);
} catch (\Exception $e) {
$this->warn('Entry already exists for student '.$student->full_name().' in audition '.$row['Instrument']);
}
$this->info('Entry created for student '.$student->full_name().' in audition '.$row['Instrument']);
}
}
}

View File

@ -8,6 +8,5 @@ enum EntryFlags: string
case DECLINED = 'declined'; case DECLINED = 'declined';
case NO_SHOW = 'no_show'; case NO_SHOW = 'no_show';
case FAILED_PRELIM = 'failed_prelim'; case FAILED_PRELIM = 'failed_prelim';
case PASSED_PRELIM = 'passed_prelim';
case LATE_FEE_WAIVED = 'late_fee_waived'; case LATE_FEE_WAIVED = 'late_fee_waived';
} }

View File

@ -6,5 +6,5 @@ use Exception;
class AuditionServiceException extends Exception class AuditionServiceException extends Exception
{ {
//TODO: Fully depricate this class //
} }

View File

@ -1,24 +1,19 @@
<?php <?php
namespace App\Exceptions; namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable; use Throwable;
use App\Exceptions\TabulationException;
/**
* @codeCoverageIgnore
*/
//TODO: Fully depricate this class
class Handler extends ExceptionHandler class Handler extends ExceptionHandler
{ {
public function render($request, Throwable $e) public function render($request, Throwable $e)
{ {
if ($e instanceof TabulationException) { if ($e instanceof TabulationException) {
dd('here'); dd('here');
return redirect('/tabulation/status')->with('warning', $e->getMessage()); return redirect('/tabulation/status')->with('warning', $e->getMessage());
} }
return parent::render($request, $e); return parent::render($request, $e);
} }
} }

View File

@ -6,5 +6,4 @@ use Exception;
class ManageEntryException extends Exception class ManageEntryException extends Exception
{ {
//TODO: Fully depricate this class
} }

View File

@ -6,5 +6,5 @@ use Exception;
class ScoreEntryException extends Exception class ScoreEntryException extends Exception
{ {
//TODO: Fully depricate this class //
} }

View File

@ -3,24 +3,20 @@
namespace App\Exceptions; namespace App\Exceptions;
use Exception; use Exception;
use Throwable;
use function dd; use function dd;
use function redirect; use function redirect;
/**
* @codeCoverageIgnore
*/
class TabulationException extends Exception class TabulationException extends Exception
{ {
public function report(): void public function report(): void
{ {
//TODO: Fully depricate this class //
} }
public function render($request) public function render($request)
{ {
dd('in the render'); dd('in the render');
return redirect('/tabulation/status')->with('error', $this->getMessage()); return redirect('/tabulation/status')->with('error', $this->getMessage());
} }
} }

View File

@ -3,15 +3,13 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\AuditionStoreOrUpdateRequest;
use App\Http\Requests\BulkAuditionEditRequest;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Event; use App\Models\Event;
use App\Models\Room;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function compact; use function abort;
use function redirect; use function redirect;
use function request; use function request;
use function response; use function response;
@ -30,20 +28,38 @@ class AuditionController extends Controller
public function create() public function create()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$events = Event::orderBy('name')->get(); $events = Event::orderBy('name')->get();
return view('admin.auditions.create', ['events' => $events]); return view('admin.auditions.create', ['events' => $events]);
} }
public function store(AuditionStoreOrUpdateRequest $request) public function store(Request $request)
{ {
$validData = $request->validated(); if (! Auth::user()->is_admin) {
abort(403);
}
$validData = request()->validate([
'event_id' => ['required', 'exists:events,id'],
'name' => ['required'],
'entry_deadline' => ['required', 'date'],
'entry_fee' => ['required', 'numeric'],
'minimum_grade' => ['required', 'integer'],
'maximum_grade' => 'required|numeric|gte:minimum_grade',
'scoring_guide_id' => 'nullable|exists:scoring_guides,id',
], [
'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.',
]);
if (empty($validData['scoring_guide_id'])) { $validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
if (empty($alidData['scoring_guide_id'])) {
$validData['scoring_guide_id'] = 0; $validData['scoring_guide_id'] = 0;
} }
$validData['score_order'] = Audition::max('score_order') + 1; $new_score_order = Audition::max('score_order') + 1;
// TODO Check if room 0 exists, create if not
Audition::create([ Audition::create([
'event_id' => $validData['event_id'], 'event_id' => $validData['event_id'],
'name' => $validData['name'], 'name' => $validData['name'],
@ -55,7 +71,7 @@ class AuditionController extends Controller
'for_advancement' => $validData['for_advancement'], 'for_advancement' => $validData['for_advancement'],
'scoring_guide_id' => $validData['scoring_guide_id'], 'scoring_guide_id' => $validData['scoring_guide_id'],
'room_id' => 0, 'room_id' => 0,
'score_order' => $validData['score_order'], 'score_order' => $new_score_order,
]); ]);
return to_route('admin.auditions.index')->with('success', 'Audition created successfully'); return to_route('admin.auditions.index')->with('success', 'Audition created successfully');
@ -63,14 +79,33 @@ class AuditionController extends Controller
public function edit(Audition $audition) public function edit(Audition $audition)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$events = Event::orderBy('name')->get(); $events = Event::orderBy('name')->get();
return view('admin.auditions.edit', ['audition' => $audition, 'events' => $events]); return view('admin.auditions.edit', ['audition' => $audition, 'events' => $events]);
} }
public function update(AuditionStoreOrUpdateRequest $request, Audition $audition) public function update(Request $request, Audition $audition)
{ {
$validData = $request->validated(); if (! Auth::user()->is_admin) {
abort(403);
}
$validData = request()->validate([
'event_id' => ['required', 'exists:events,id'],
'name' => ['required'],
'entry_deadline' => ['required', 'date'],
'entry_fee' => ['required', 'numeric'],
'minimum_grade' => ['required', 'integer'],
'maximum_grade' => 'required | numeric | gte:minimum_grade',
], [
'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.',
]);
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
$audition->update([ $audition->update([
'event_id' => $validData['event_id'], 'event_id' => $validData['event_id'],
@ -86,59 +121,11 @@ class AuditionController extends Controller
return to_route('admin.auditions.index')->with('success', 'Audition updated successfully'); return to_route('admin.auditions.index')->with('success', 'Audition updated successfully');
} }
public function bulkEditForm()
{
$auditions = Audition::with(['event'])->withCount('entries')->orderBy('score_order')->orderBy('created_at',
'desc')->get()->groupBy('event_id');
$events = Event::orderBy('name')->get();
return view('admin.auditions.bulk_edit_form', compact('auditions', 'events'));
}
public function bulkUpdate(BulkAuditionEditRequest $request)
{
$validated = collect($request->validated());
$auditions = Audition::whereIn('id', $validated['auditions'])->get();
foreach ($auditions as $audition) {
if ($validated->has('event_id')) {
$audition->event_id = $validated['event_id'];
}
if ($validated->has('entry_deadline')) {
$audition->entry_deadline = $validated['entry_deadline'];
}
if ($validated->has('entry_fee')) {
$audition->entry_fee = $validated['entry_fee'];
}
if ($validated->has('minimum_grade')) {
$originalMinimumGrade = $audition->minimum_grade;
$audition->minimum_grade = $validated['minimum_grade'];
}
if ($validated->has('maximum_grade')) {
$originalMaximumGrade = $audition->maximum_grade;
$audition->maximum_grade = $validated['maximum_grade'];
}
if ($validated->has('for_seating')) {
$audition->for_seating = $validated['for_seating'];
}
if ($validated->has('for_advancement')) {
$audition->for_advancement = $validated['for_advancement'];
}
if ($audition->minimum_grade > $audition->maximum_grade) {
$audition->minimum_grade = $originalMinimumGrade;
$audition->maximum_grade = $originalMaximumGrade;
}
$audition->save();
}
return to_route('admin.auditions.index')->with('success', $auditions->count().' Auditions updated successfully');
}
public function reorder(Request $request) public function reorder(Request $request)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order; $order = $request->order;
foreach ($order as $index => $id) { foreach ($order as $index => $id) {
$audition = Audition::find($id); $audition = Audition::find($id);
@ -151,15 +138,9 @@ class AuditionController extends Controller
public function roomUpdate(Request $request) public function roomUpdate(Request $request)
{ {
$auditions = $request->all(); $auditions = $request->all();
/**
* $auditions will be an array of arrays with the following structure:
* [
* ['id' => 1, 'room_id' => 1, 'room_order' => 1],
* ]
* is is an audition id
*/
foreach ($auditions as $audition) { foreach ($auditions as $audition) {
$a = Audition::where('id', $audition['id']) Audition::where('id', $audition['id'])
->update([ ->update([
'room_id' => $audition['room_id'], 'room_id' => $audition['room_id'],
'order_in_room' => $audition['room_order'], 'order_in_room' => $audition['room_order'],

View File

@ -16,17 +16,15 @@ class AuditionSettings extends Controller
return view('admin.audition-settings'); return view('admin.audition-settings');
} }
/** @codeCoverageIgnore */
public function save(Request $request) public function save(Request $request)
{ {
// TODO update validation rules to match the settings table
$validData = $request->validate([ $validData = $request->validate([
'auditionName' => ['required'], 'auditionName' => ['required'],
'auditionAbbreviation' => ['required', 'max:10'], 'auditionAbbreviation' => ['required', 'max:10'],
'organizerName' => ['required'], 'organizerName' => ['required'],
'organizerEmail' => ['required', 'email'], 'organizerEmail' => ['required', 'email'],
'registrationCode' => ['required'], 'registrationCode' => ['required'],
'fee_structure' => ['required', 'in:oneFeePerEntry,oneFeePerStudent,oneFeePerStudentPerEvent'], 'fee_structure' => ['required', 'in:oneFeePerEntry,oneFeePerStudent'],
// Options should align with the boot method of InvoiceDataServiceProvider // Options should align with the boot method of InvoiceDataServiceProvider
'late_fee' => ['nullable', 'numeric', 'min:0'], 'late_fee' => ['nullable', 'numeric', 'min:0'],
'school_fee' => ['nullable', 'numeric', 'min:0'], 'school_fee' => ['nullable', 'numeric', 'min:0'],

View File

@ -27,7 +27,7 @@ class BonusScoreDefinitionController extends Controller
public function store() public function store()
{ {
$validData = request()->validate([ $validData = request()->validate([
'name' => 'required|unique:bonus_score_definitions,name', 'name' => 'required',
'max_score' => 'required|numeric', 'max_score' => 'required|numeric',
'weight' => 'required|numeric', 'weight' => 'required|numeric',
]); ]);
@ -37,20 +37,6 @@ class BonusScoreDefinitionController extends Controller
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Created'); return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Created');
} }
public function update(BonusScoreDefinition $bonusScore)
{
$validData = request()->validate([
'name' => 'required|unique:bonus_score_definitions,name,'.$bonusScore->id,
'max_score' => 'required|numeric',
'weight' => 'required|numeric',
]);
$bonusScore->update($validData);
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Updated');
}
public function destroy(BonusScoreDefinition $bonusScore) public function destroy(BonusScoreDefinition $bonusScore)
{ {
if ($bonusScore->auditions()->count() > 0) { if ($bonusScore->auditions()->count() > 0) {
@ -63,7 +49,6 @@ class BonusScoreDefinitionController extends Controller
public function assignAuditions(Request $request) public function assignAuditions(Request $request)
{ {
// TODO: add pivot model to log changes to assignments
$validData = $request->validate([ $validData = $request->validate([
'bonus_score_id' => 'required|exists:bonus_score_definitions,id', 'bonus_score_id' => 'required|exists:bonus_score_definitions,id',
'audition' => 'required|array', 'audition' => 'required|array',
@ -85,8 +70,12 @@ class BonusScoreDefinitionController extends Controller
public function unassignAudition(Audition $audition) public function unassignAudition(Audition $audition)
{ {
// TODO: add pivot model to log changes to assignments if (! $audition->exists()) {
return redirect()->route('admin.bonus-scores.index')->with('error', 'Audition not found');
}
if (! $audition->bonusScore()->count() > 0) {
return redirect()->route('admin.bonus-scores.index')->with('error', 'Audition does not have a bonus score');
}
$audition->bonusScore()->detach(); $audition->bonusScore()->detach();
return redirect()->route('admin.bonus-scores.index')->with('success', 'Audition unassigned from bonus score'); return redirect()->route('admin.bonus-scores.index')->with('success', 'Audition unassigned from bonus score');
@ -94,7 +83,6 @@ class BonusScoreDefinitionController extends Controller
public function judges() public function judges()
{ {
//TODO Need to show if judge is assigned, and show bonus assignments or normal judging page
$bonusScores = BonusScoreDefinition::all(); $bonusScores = BonusScoreDefinition::all();
$users = User::orderBy('last_name')->orderBy('first_name')->get(); $users = User::orderBy('last_name')->orderBy('first_name')->get();
@ -103,6 +91,9 @@ class BonusScoreDefinitionController extends Controller
public function assignJudge(BonusScoreDefinition $bonusScore) public function assignJudge(BonusScoreDefinition $bonusScore)
{ {
if (! $bonusScore->exists()) {
return redirect()->route('admin.bonus-scores.judges')->with('error', 'Bonus Score not found');
}
$validData = request()->validate([ $validData = request()->validate([
'judge' => 'required|exists:users,id', 'judge' => 'required|exists:users,id',
]); ]);
@ -113,6 +104,9 @@ class BonusScoreDefinitionController extends Controller
public function removeJudge(BonusScoreDefinition $bonusScore) public function removeJudge(BonusScoreDefinition $bonusScore)
{ {
if (! $bonusScore->exists()) {
return redirect()->route('admin.bonus-scores.judges')->with('error', 'Bonus Score not found');
}
$validData = request()->validate([ $validData = request()->validate([
'judge' => 'required|exists:users,id', 'judge' => 'required|exists:users,id',
]); ]);

View File

@ -2,24 +2,30 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Actions\Draw\ClearDraw;
use App\Actions\Draw\RunDraw;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\ClearDrawRequest; use App\Http\Requests\ClearDrawRequest;
use App\Http\Requests\RunDrawRequest; use App\Http\Requests\RunDrawRequest;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Event; use App\Models\Event;
use App\Services\DrawService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use function array_keys; use function array_keys;
use function to_route; use function to_route;
class DrawController extends Controller class DrawController extends Controller
{ {
protected $drawService;
public function __construct(DrawService $drawService)
{
$this->drawService = $drawService;
}
public function index(Request $request) public function index(Request $request)
{ {
$events = Event::with('auditions.flags')->get(); $events = Event::with('auditions.flags')->get();
// $drawnAuditionsExist is true if any audition->hasFlag('drawn') is true // $drawnAuditionsExist is true if any audition->hasFlag('drawn') is true
$drawnAuditionsExist = Audition::whereHas('flags', function ($query) { $drawnAuditionsExist = Audition::whereHas('flags', function ($query) {
$query->where('flag_name', 'drawn'); $query->where('flag_name', 'drawn');
@ -30,23 +36,18 @@ class DrawController extends Controller
public function store(RunDrawRequest $request) public function store(RunDrawRequest $request)
{ {
// Request will contain audition which is an array of audition IDs all with a value of 1
// Code below results in a collection of auditions that were checked on the form
$auditions = Audition::with('flags')->findMany(array_keys($request->input('audition', []))); $auditions = Audition::with('flags')->findMany(array_keys($request->input('audition', [])));
if ($auditions->contains(fn ($audition) => $audition->hasFlag('drawn'))) { if ($this->drawService->checkCollectionForDrawnAuditions($auditions)) {
return to_route('admin.draw.index')->with('error', return to_route('admin.draw.index')->with('error',
'Cannot run draw. Some auditions have already been drawn.'); 'Invalid attempt to draw an audition that has already been drawn');
} }
app(RunDraw::class)($auditions); $this->drawService->runDrawsOnCollection($auditions);
return to_route('admin.draw.index')->with('success', 'Draw completed successfully'); return to_route('admin.draw.index')->with('status', 'Draw completed successfully');
} }
/**
* generates the page with checkboxes for each drawn audition with an intent to clear them
*/
public function edit(Request $request) public function edit(Request $request)
{ {
$drawnAuditions = Audition::whereHas('flags', function ($query) { $drawnAuditions = Audition::whereHas('flags', function ($query) {
@ -56,17 +57,12 @@ class DrawController extends Controller
return view('admin.draw.edit', compact('drawnAuditions')); return view('admin.draw.edit', compact('drawnAuditions'));
} }
/**
* Clears the draw for auditions
*/
public function destroy(ClearDrawRequest $request) public function destroy(ClearDrawRequest $request)
{ {
// Request will contain audition which is an array of audition IDs all with a value of 1
// Code below results in a collection of auditions that were checked on the form
$auditions = Audition::with('flags')->findMany(array_keys($request->input('audition', []))); $auditions = Audition::with('flags')->findMany(array_keys($request->input('audition', [])));
app(ClearDraw::class)($auditions); $this->drawService->clearDrawsOnCollection($auditions);
return to_route('admin.draw.index')->with('success', 'Draws cleared successfully'); return to_route('admin.draw.index')->with('status', 'Draw completed successfully');
} }
} }

View File

@ -3,13 +3,12 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\EnsembleStoreOrUpdateRequest;
use App\Models\Ensemble; use App\Models\Ensemble;
use App\Models\Event; use App\Models\Event;
use App\Models\SeatingLimit; use App\Models\SeatingLimit;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use function redirect; use function redirect;
@ -22,24 +21,30 @@ class EnsembleController extends Controller
return view('admin.ensembles.index', compact('events')); return view('admin.ensembles.index', compact('events'));
} }
public function store(EnsembleStoreOrUpdateRequest $request) public function store(Request $request)
{ {
Log::channel('file')->warning('hello'); if (! Auth::user()->is_admin) {
$validated = $request->validated(); abort(403);
// get the maximum value of rank from the ensemble table where event_id is equal to the request event_id }
request()->validate([
'name' => 'required',
'code' => ['required', 'max:6'],
'event_id' => ['required', 'exists:events,id'],
]);
// get the maximum value of rank from the ensembles table where event_id is equal to the request event_id
$maxCode = Ensemble::where('event_id', request('event_id'))->max('rank'); $maxCode = Ensemble::where('event_id', request('event_id'))->max('rank');
Ensemble::create([ Ensemble::create([
'name' => $validated['name'], 'name' => request('name'),
'code' => $validated['code'], 'code' => request('code'),
'event_id' => $validated['event_id'], 'event_id' => request('event_id'),
'rank' => $maxCode + 1, 'rank' => $maxCode + 1,
]); ]);
return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble created successfully'); return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble created successfully');
} }
public function destroy(Ensemble $ensemble) public function destroy(Request $request, Ensemble $ensemble)
{ {
if ($ensemble->seats->count() > 0) { if ($ensemble->seats->count() > 0) {
return redirect()->route('admin.ensembles.index')->with('error', return redirect()->route('admin.ensembles.index')->with('error',
@ -50,32 +55,25 @@ class EnsembleController extends Controller
return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble deleted successfully'); return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble deleted successfully');
} }
public function update(EnsembleStoreOrUpdateRequest $request, Ensemble $ensemble) public function updateEnsemble(Request $request, Ensemble $ensemble)
{ {
$valid = $request->validated(); request()->validate([
'name' => 'required',
'code' => 'required|max:6',
]);
$ensemble->update([ $ensemble->update([
'name' => $valid['name'], 'name' => request('name'),
'code' => $valid['code'], 'code' => request('code'),
]); ]);
return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble updated successfully'); return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble updated successfully');
} }
//TODO Consider moving seating limit related functions to their own controller with index, edit, and update methods
public function seatingLimits(Ensemble $ensemble) public function seatingLimits(Ensemble $ensemble)
{ {
$limits = []; $limits = [];
/** $ensembles = Ensemble::with(['event'])->orderBy('event_id')->get();
* If we weren't called with an ensemble, we're going to use an array of ensembles to fill a drop-down and
* choose one. The user will be sent back here, this time with the chosen audition.
*/
$ensembles = Ensemble::with(['event'])->orderBy('event_id')->orderBy('rank')->get();
/**
* If we were called with an ensemble, we need to load existing seating limits. We will put them in an array
* indexed by audition_id for easy use in the form to set seating limits.
*/
if ($ensemble->exists()) { if ($ensemble->exists()) {
$ensemble->load('seatingLimits'); $ensemble->load('seatingLimits');
foreach ($ensemble->seatingLimits as $lim) { foreach ($ensemble->seatingLimits as $lim) {
@ -114,6 +112,10 @@ class EnsembleController extends Controller
public function updateEnsembleRank(Request $request) public function updateEnsembleRank(Request $request)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->input('order'); $order = $request->input('order');
$eventId = $request->input('event_id'); $eventId = $request->input('event_id');

View File

@ -4,15 +4,18 @@ namespace App\Http\Controllers\Admin;
use App\Actions\Entries\CreateEntry; use App\Actions\Entries\CreateEntry;
use App\Actions\Entries\UpdateEntry; use App\Actions\Entries\UpdateEntry;
use App\Actions\Tabulation\CalculateScoreSheetTotal;
use App\Exceptions\ManageEntryException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\EntryStoreRequest;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
use App\Models\School; use App\Models\School;
use App\Models\Seat; use App\Models\Seat;
use App\Models\Student; use App\Models\Student;
use App\Services\ScoreService;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function auditionSetting; use function auditionSetting;
use function compact; use function compact;
@ -22,6 +25,9 @@ class EntryController extends Controller
{ {
public function index() public function index()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$perPage = 25; $perPage = 25;
$filters = session('adminEntryFilters') ?? null; $filters = session('adminEntryFilters') ?? null;
$minGrade = Audition::min('minimum_grade'); $minGrade = Audition::min('minimum_grade');
@ -32,31 +38,31 @@ class EntryController extends Controller
$entries = Entry::with(['student.school', 'audition']); $entries = Entry::with(['student.school', 'audition']);
$entries->orderBy('id', 'DESC'); $entries->orderBy('id', 'DESC');
if ($filters) { if ($filters) {
if ($filters['id'] ?? false) { if ($filters['id']) {
$entries->where('id', $filters['id']); $entries->where('id', $filters['id']);
} }
if ($filters['audition'] ?? false) { if ($filters['audition']) {
$entries->where('audition_id', $filters['audition']); $entries->where('audition_id', $filters['audition']);
} }
if ($filters['school'] ?? false) { if ($filters['school']) {
$entries->whereHas('student', function ($query) use ($filters) { $entries->whereHas('student', function ($query) use ($filters) {
$query->where('school_id', '=', $filters['school']); $query->where('school_id', '=', $filters['school']);
}); });
} }
if ($filters['grade'] ?? false) { if ($filters['grade']) {
$entries->whereHas('student', function ($query) use ($filters) { $entries->whereHas('student', function ($query) use ($filters) {
$query->where('grade', $filters['grade']); $query->where('grade', $filters['grade']);
}); });
} }
if ($filters['first_name'] ?? false) { if ($filters['first_name']) {
$entries->whereHas('student', function ($query) use ($filters) { $entries->whereHas('student', function ($query) use ($filters) {
$query->where('first_name', 'like', '%'.$filters['first_name'].'%'); $query->where('first_name', 'like', '%'.$filters['first_name'].'%');
}); });
} }
if ($filters['last_name'] ?? false) { if ($filters['last_name']) {
$entries->whereHas('student', function ($query) use ($filters) { $entries->whereHas('student', function ($query) use ($filters) {
$query->where('last_name', 'like', '%'.$filters['last_name'].'%'); $query->where('last_name', 'like', '%'.$filters['last_name'].'%');
}); });
@ -65,6 +71,7 @@ class EntryController extends Controller
if (isset($filters['entry_type']) && $filters['entry_type']) { if (isset($filters['entry_type']) && $filters['entry_type']) {
// TODO define actions for each possible type filter from index.blade.php of the admin entry // TODO define actions for each possible type filter from index.blade.php of the admin entry
match ($filters['entry_type']) { match ($filters['entry_type']) {
'all' => null,
'seats' => $entries->where('for_seating', true), 'seats' => $entries->where('for_seating', true),
'advancement' => $entries->where('for_advancement', true), 'advancement' => $entries->where('for_advancement', true),
'seatsOnly' => $entries->where('for_seating', true)->where('for_advancement', false), 'seatsOnly' => $entries->where('for_seating', true)->where('for_advancement', false),
@ -103,19 +110,32 @@ class EntryController extends Controller
return view('admin.entries.create', ['students' => $students, 'auditions' => $auditions]); return view('admin.entries.create', ['students' => $students, 'auditions' => $auditions]);
} }
public function store(EntryStoreRequest $request, CreateEntry $creator) public function store(Request $request, CreateEntry $creator)
{ {
$validData = $request->validatedWithEnterFor(); if (! Auth::user()->is_admin) {
abort(403);
}
$validData = request()->validate([
'student_id' => ['required', 'exists:students,id'],
'audition_id' => ['required', 'exists:auditions,id'],
]);
/** @noinspection PhpUnhandledExceptionInspection */ $validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$entry = $creator( $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
student: $validData['student_id'], $validData['late_fee_waived'] = $request->get('late_fee_waived') ? 1 : 0;
audition: $validData['audition_id'], $enter_for = [];
for_seating: $validData['for_seating'], if ($validData['for_seating']) {
for_advancement: $validData['for_advancement'], $enter_for[] = 'seating';
late_fee_waived: $validData['late_fee_waived'], }
); if ($validData['for_advancement']) {
$enter_for[] = 'advancement';
}
try {
$entry = $creator($validData['student_id'], $validData['audition_id'], $enter_for);
} catch (ManageEntryException $ex) {
return redirect()->route('admin.entries.index')->with('error', $ex->getMessage());
}
if ($validData['late_fee_waived']) { if ($validData['late_fee_waived']) {
$entry->addFlag('late_fee_waived'); $entry->addFlag('late_fee_waived');
} }
@ -123,7 +143,7 @@ class EntryController extends Controller
return redirect(route('admin.entries.index'))->with('success', 'The entry has been added.'); return redirect(route('admin.entries.index'))->with('success', 'The entry has been added.');
} }
public function edit(Entry $entry) public function edit(Entry $entry, CalculateScoreSheetTotal $calculator, ScoreService $scoreService)
{ {
if ($entry->audition->hasFlag('seats_published')) { if ($entry->audition->hasFlag('seats_published')) {
return to_route('admin.entries.index')->with('error', return to_route('admin.entries.index')->with('error',
@ -137,35 +157,31 @@ class EntryController extends Controller
$students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get(); $students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->get();
$auditions = Audition::orderBy('score_order')->get(); $auditions = Audition::orderBy('score_order')->get();
// TODO: When updating Laravel, can we use the chaperone method I heard about ot load the entry back into the score $scores = $entry->scoreSheets()->with('audition', 'judge')->get();
$scores = $entry->scoreSheets()->with('audition', 'judge', 'entry')->get(); foreach ($scores as $score) {
$score->entry = $entry;
$score->valid = $scoreService->isScoreSheetValid($score);
$score->seating_total_score = $calculator('seating', $entry, $score->judge)[0];
$score->advancement_total_score = $calculator('advancement', $entry, $score->judge)[0];
}
$logEntries = AuditLogEntry::whereJsonContains('affected->entries', $entry->id)->orderBy('created_at', 'desc')->get(); return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores'));
return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores', 'logEntries'));
} }
public function update(Request $request, Entry $entry, UpdateEntry $updater) public function update(Request $request, Entry $entry, UpdateEntry $updater)
{ {
// If the entry's current audition is published, we can't change it if ($entry->audition->hasFlag('seats_published')) {
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error', return to_route('admin.entries.index')->with('error',
'Entries in published auditions cannot be modified'); 'Entries in auditions with seats published cannot be modified');
} }
if ($entry->audition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error',
'Entries in auditions with advancement results published cannot be modified');
}
$validData = request()->validate([ $validData = request()->validate([
'audition_id' => ['required', 'exists:auditions,id'], 'audition_id' => ['required', 'exists:auditions,id'],
'late_fee_waived' => ['sometimes'],
'for_seating' => ['sometimes'],
'for_advancement' => ['sometimes'],
]); ]);
$proposedAudition = Audition::find($validData['audition_id']);
// If the entry's new audition is published, we can't change it
if ($proposedAudition->hasFlag('seats_published') || $proposedAudition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error',
'Entries cannot be moved to published auditions');
}
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0; $validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
@ -175,10 +191,11 @@ class EntryController extends Controller
if (! auditionSetting('advanceTo')) { if (! auditionSetting('advanceTo')) {
$validData['for_seating'] = 1; $validData['for_seating'] = 1;
} }
try {
/** @noinspection PhpUnhandledExceptionInspection */
$updater($entry, $validData); $updater($entry, $validData);
} catch (ManageEntryException $e) {
return redirect()->route('admin.entries.index')->with('error', $e->getMessage());
}
if ($validData['late_fee_waived']) { if ($validData['late_fee_waived']) {
$entry->addFlag('late_fee_waived'); $entry->addFlag('late_fee_waived');
} else { } else {
@ -188,13 +205,17 @@ class EntryController extends Controller
return to_route('admin.entries.index')->with('success', 'Entry updated successfully'); return to_route('admin.entries.index')->with('success', 'Entry updated successfully');
} }
public function destroy(Entry $entry) public function destroy(Request $request, Entry $entry)
{ {
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) { if ($entry->audition->hasFlag('seats_published')) {
return to_route('admin.entries.index')->with('error', return to_route('admin.entries.index')->with('error',
'Entries in published auditions cannot be deleted'); 'Entries in auditions with seats published cannot be deleted');
} }
if ($entry->audition->hasFlag('advancement_published')) {
return to_route('admin.entries.index')->with('error',
'Entries in auditions with advancement results published cannot be deleted');
}
if (Seat::where('entry_id', $entry->id)->exists()) { if (Seat::where('entry_id', $entry->id)->exists()) {
return redirect()->route('admin.entries.index')->with('error', 'Cannot delete an entry that is seated'); return redirect()->route('admin.entries.index')->with('error', 'Cannot delete an entry that is seated');
} }
@ -203,7 +224,21 @@ class EntryController extends Controller
return redirect()->route('admin.entries.index')->with('error', return redirect()->route('admin.entries.index')->with('error',
'Cannot delete an entry that has been scored'); 'Cannot delete an entry that has been scored');
} }
if (auth()->user()) {
$message = 'Deleted entry '.$entry->id;
$affected = [
'entries' => [$entry->id],
'auditions' => [$entry->audition_id],
'schools' => [$entry->student->school_id],
'students' => [$entry->student_id],
];
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => $affected,
]);
}
$entry->delete(); $entry->delete();
return redirect()->route('admin.entries.index')->with('success', 'Entry Deleted'); return redirect()->route('admin.entries.index')->with('success', 'Entry Deleted');

View File

@ -5,7 +5,9 @@ namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Event; use App\Models\Event;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function abort;
use function compact; use function compact;
class EventController extends Controller class EventController extends Controller
@ -13,16 +15,15 @@ class EventController extends Controller
public function index() public function index()
{ {
$events = Event::all(); $events = Event::all();
$renameModalXdata = '';
foreach ($events as $event) {
$renameModalXdata .= 'showRenameModal_'.$event->id.': false, ';
}
return view('admin.event.index', compact('events', 'renameModalXdata')); return view('admin.event.index', compact('events'));
} }
public function store(Request $request) public function store(Request $request)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([ request()->validate([
'name' => ['required', 'unique:events,name'], 'name' => ['required', 'unique:events,name'],
]); ]);
@ -34,21 +35,6 @@ class EventController extends Controller
return redirect()->route('admin.events.index')->with('success', 'Event created successfully'); return redirect()->route('admin.events.index')->with('success', 'Event created successfully');
} }
public function update(Request $request, Event $event)
{
if ($request->name !== $event->name) {
$validated = request()->validate([
'name' => ['required', 'unique:events,name'],
]);
$event->update([
'name' => $validated['name'],
]);
}
return redirect()->route('admin.events.index')->with('success', 'Event renamed successfully');
}
public function destroy(Request $request, Event $event) public function destroy(Request $request, Event $event)
{ {
if ($event->auditions()->count() > 0) { if ($event->auditions()->count() > 0) {
@ -60,4 +46,3 @@ class EventController extends Controller
return redirect()->route('admin.events.index')->with('success', 'Event deleted successfully'); return redirect()->route('admin.events.index')->with('success', 'Event deleted successfully');
} }
} }
// TODO add form to modify an event

View File

@ -7,8 +7,6 @@ use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
// TODO: Printing testing
/** @codeCoverageIgnore */
class ExportEntriesController extends Controller class ExportEntriesController extends Controller
{ {
public function __invoke() public function __invoke()

View File

@ -7,8 +7,6 @@ use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Response; use Illuminate\Support\Facades\Response;
// TODO: Printing testing
/** @codeCoverageIgnore */
class ExportResultsController extends Controller class ExportResultsController extends Controller
{ {
public function __invoke() public function __invoke()

View File

@ -1,70 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\PrelimDefinitionStoreOrUpdateRequest;
use App\Models\Audition;
use App\Models\PrelimDefinition;
use App\Models\Room;
use App\Models\ScoringGuide;
use function view;
class PrelimDefinitionController extends Controller
{
public function index()
{
$prelims = PrelimDefinition::all();
return view('admin.prelim_definitions.index', compact('prelims'));
}
public function create()
{
$auditions = Audition::doesntHave('prelimDefinition')->get();
$rooms = Room::all();
$guides = ScoringGuide::all();
$method = 'POST';
$action = route('admin.prelim_definitions.store');
$prelim = false;
return view('admin.prelim_definitions.createOrUpdate', compact('auditions', 'rooms', 'guides', 'method', 'action', 'prelim'));
}
public function store(PrelimDefinitionStoreOrUpdateRequest $request)
{
$validated = $request->validated();
PrelimDefinition::create($validated);
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition created');
}
public function edit(PrelimDefinition $prelimDefinition)
{
$auditions = Audition::doesntHave('prelimDefinition')->get();
$rooms = Room::all();
$guides = ScoringGuide::all();
$method = 'PATCH';
$action = route('admin.prelim_definitions.update', $prelimDefinition);
$prelim = $prelimDefinition;
return view('admin.prelim_definitions.createOrUpdate', compact('auditions', 'rooms', 'guides', 'method', 'action', 'prelim'));
}
public function update(PrelimDefinition $prelimDefinition, PrelimDefinitionStoreOrUpdateRequest $request)
{
$validated = $request->validated();
$prelimDefinition->update($validated);
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition updated');
}
public function destroy(PrelimDefinition $prelimDefinition)
{
$prelimDefinition->delete();
return redirect()->route('admin.prelim_definitions.index')->with('success', 'Prelim definition deleted');
}
}

View File

@ -7,8 +7,6 @@ use App\Models\Entry;
use App\Models\Event; use App\Models\Event;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
// TODO: Printing testing
/** @codeCoverageIgnore */
class PrintCards extends Controller class PrintCards extends Controller
{ {
public function index() // Display a form to select which cards to print public function index() // Display a form to select which cards to print
@ -28,14 +26,10 @@ class PrintCards extends Controller
public function print(\App\Actions\Print\PrintCards $printer) public function print(\App\Actions\Print\PrintCards $printer)
{ {
//dump(request()->all()); //dump(request()->all());
// if (request()->audition == null) { if (request()->audition == null) {
// return redirect()->back()->with('error', 'You must specify at least one audition'); return redirect()->back()->with('error', 'You must specify at least one audition');
// }
if (request()->audition) {
$selectedAuditionIds = array_keys(request()->audition);
} else {
$selectedAuditionIds = [];
} }
$selectedAuditionIds = array_keys(request()->audition);
$cardQuery = Entry::whereIn('audition_id', $selectedAuditionIds); $cardQuery = Entry::whereIn('audition_id', $selectedAuditionIds);
// Process Filters // Process Filters

View File

@ -8,9 +8,6 @@ use Codedge\Fpdf\Fpdf\Fpdf;
use function auditionSetting; use function auditionSetting;
// TODO: Printing testing
/** @codeCoverageIgnore */
class PrintRoomAssignmentsController extends Controller class PrintRoomAssignmentsController extends Controller
{ {
private $pdf; private $pdf;
@ -97,7 +94,7 @@ class PrintRoomAssignmentsController extends Controller
} }
} }
/** @codeCoverageIgnore */
class reportPDF extends FPDF class reportPDF extends FPDF
{ {
public function getPageBreakTrigger() public function getPageBreakTrigger()

View File

@ -9,8 +9,6 @@ use App\Models\Room;
use function array_keys; use function array_keys;
use function request; use function request;
// TODO: Printing testing
/** @codeCoverageIgnore */
class PrintSignInSheetsController extends Controller class PrintSignInSheetsController extends Controller
{ {
public function index() public function index()

View File

@ -6,8 +6,6 @@ use App\Actions\Print\PrintStandNameTags;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
// TODO: Printing testing
/** @codeCoverageIgnore */
class PrintStandNameTagsController extends Controller class PrintStandNameTagsController extends Controller
{ {
public function __invoke() public function __invoke()

View File

@ -7,8 +7,6 @@ use App\Http\Controllers\Controller;
use App\Models\Audition; use App\Models\Audition;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
// TODO: Rewrite Recap to work with new scoring code
/** @codeCoverageIgnore */
class RecapController extends Controller class RecapController extends Controller
{ {
public function selectAudition() public function selectAudition()

View File

@ -3,40 +3,22 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\BonusScoreDefinition; use App\Models\BonusScoreDefinition;
use App\Models\Room; use App\Models\Room;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Validation\Rule; use Illuminate\Support\Facades\Auth;
use function auditionLog;
use function redirect; use function redirect;
class RoomController extends Controller class RoomController extends Controller
{ {
public function index() public function index()
{ {
if (! Auth::user()->is_admin) {
$rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get(); abort(403);
// Check if room id 0 exists, if not, create it and assign all unassigned auditions to it
if (! $rooms->contains('id', 0)) {
$unassignedRoom = Room::create([
'id' => 0,
'name' => 'Unassigned',
'description' => 'Auditions that have not been assigned to a room',
]);
$unassignedRoom->id = 0;
$unassignedRoom->save();
$auditionsToUpdate = Audition::whereNull('room_id')->get();
foreach ($auditionsToUpdate as $audition) {
$audition->room_id = 0;
$audition->save();
} }
$rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get(); $rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get();
}
return view('admin.rooms.index', ['rooms' => $rooms]); return view('admin.rooms.index', ['rooms' => $rooms]);
} }
@ -48,12 +30,14 @@ class RoomController extends Controller
$rooms = Room::with(['judges.school', 'auditions'])->get(); $rooms = Room::with(['judges.school', 'auditions'])->get();
$bonusScoresExist = BonusScoreDefinition::count() > 0; $bonusScoresExist = BonusScoreDefinition::count() > 0;
return view('admin.rooms.judge_assignments', return view('admin.rooms.judge_assignments', compact('usersWithoutRooms', 'usersWithRooms', 'rooms', 'bonusScoresExist'));
compact('usersWithoutRooms', 'usersWithRooms', 'rooms', 'bonusScoresExist'));
} }
public function updateJudgeAssignment(Request $request, Room $room) public function updateJudgeAssignment(Request $request, Room $room)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([ $validData = $request->validate([
'judge' => 'exists:users,id', 'judge' => 'exists:users,id',
]); ]);
@ -67,23 +51,29 @@ class RoomController extends Controller
// detach judge on delete // detach judge on delete
$room->removeJudge($judge->id); $room->removeJudge($judge->id);
$message = 'Removed '.$judge->full_name().' from '.$room->name; $message = 'Removed '.$judge->full_name().' from '.$room->name;
} else {
return redirect('/admin/rooms/judging_assignments')->with('error', 'Invalid request method.');
} }
$affected['users'] = [$judge->id];
$affected['rooms'] = [$room->id];
auditionLog($message, $affected);
return redirect(route('admin.rooms.judgingAssignment'))->with('success', $message); return redirect('/admin/rooms/judging_assignments')->with('success', $message);
} }
public function create() public function create()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
return view('admin.rooms.create'); return view('admin.rooms.create');
} }
public function store(Request $request) public function store(Request $request)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([ $validData = $request->validate([
'name' => 'required|unique:rooms,name', 'name' => 'required',
'description' => 'nullable', 'description' => 'nullable',
]); ]);
@ -97,8 +87,11 @@ class RoomController extends Controller
public function update(Request $request, Room $room) public function update(Request $request, Room $room)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([ $validData = $request->validate([
'name' => ['required', Rule::unique('rooms', 'name')->ignore($room->id)], 'name' => 'required',
'description' => 'nullable', 'description' => 'nullable',
]); ]);
@ -111,9 +104,12 @@ class RoomController extends Controller
public function destroy(Room $room) public function destroy(Room $room)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
if ($room->auditions()->count() > 0) { if ($room->auditions()->count() > 0) {
return redirect()->route('admin.rooms.index')->with('error', return redirect()->route('admin.rooms.index')->with('error', 'Cannot delete room with auditions. First move the auditions to unassigned or another room');
'Cannot delete room with auditions. First move the auditions to unassigned or another room');
} }
$room->delete(); $room->delete();

View File

@ -2,16 +2,16 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Actions\Schools\CreateSchool;
use App\Actions\Schools\SetHeadDirector; use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\SchoolStoreRequest;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
use App\Models\School; use App\Models\School;
use App\Models\SchoolEmailDomain; use App\Models\SchoolEmailDomain;
use App\Models\User; use App\Models\User;
use App\Services\Invoice\InvoiceDataService; use App\Services\Invoice\InvoiceDataService;
use Illuminate\Support\Facades\Auth;
use function abort;
use function redirect; use function redirect;
use function request; use function request;
@ -38,26 +38,46 @@ class SchoolController extends Controller
public function show(School $school) public function show(School $school)
{ {
$logEntries = AuditLogEntry::whereJsonContains('affected->schools', $school->id)->orderBy('created_at', 'desc')->get(); if (! Auth::user()->is_admin) {
abort(403);
}
return view('admin.schools.show', compact('school', 'logEntries')); return view('admin.schools.show', ['school' => $school]);
} }
public function edit(School $school) public function edit(School $school)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$school->loadCount('students'); $school->loadCount('students');
return view('admin.schools.edit', ['school' => $school]); return view('admin.schools.edit', ['school' => $school]);
} }
public function update(SchoolStoreRequest $request, School $school) public function update(School $school)
{ {
request()->validate([
'name' => ['required'],
'address' => ['required'],
'city' => ['required'],
'state' => ['required'],
'zip' => ['required'],
]);
$school->update([ $school->update([
'name' => $request['name'], 'name' => request('name'),
'address' => $request['address'], 'address' => request('address'),
'city' => $request['city'], 'city' => request('city'),
'state' => $request['state'], 'state' => request('state'),
'zip' => $request['zip'], 'zip' => request('zip'),
]);
$message = 'Modified school #'.$school->id.' - '.$school->name.' with address <br>'.$school->address.'<br>'.$school->city.', '.$school->state.' '.$school->zip;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['schools' => [$school->id]],
]); ]);
return redirect()->route('admin.schools.show', ['school' => $school->id])->with('success', return redirect()->route('admin.schools.show', ['school' => $school->id])->with('success',
@ -66,30 +86,54 @@ class SchoolController extends Controller
public function create() public function create()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
return view('admin.schools.create'); return view('admin.schools.create');
} }
public function store(SchoolStoreRequest $request) public function store()
{ {
$creator = app(CreateSchool::class); request()->validate([
'name' => ['required'],
'address' => ['required'],
'city' => ['required'],
'state' => ['required'],
'zip' => ['required'],
]);
$school = $creator( $school = School::create([
$request['name'], 'name' => request('name'),
$request['address'], 'address' => request('address'),
$request['city'], 'city' => request('city'),
$request['state'], 'state' => request('state'),
$request['zip'], 'zip' => request('zip'),
); ]);
$message = 'Created school #'.$school->id.' - '.$school->name.' with address <br>'.$school->address.'<br>'.$school->city.', '.$school->state.' '.$school->zip;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['schools' => [$school->id]],
]);
return redirect(route('admin.schools.index'))->with('success', 'School '.$school->name.' created'); return redirect('/admin/schools')->with('success', 'School '.$school->name.' created');
} }
public function destroy(School $school) public function destroy(School $school)
{ {
if ($school->students()->count() > 0) { if ($school->students()->count() > 0) {
return to_route('admin.schools.index')->with('error', 'You cannot delete a school that has students.'); return to_route('admin.schools.index')->with('error', 'You cannot delete a school with students.');
} }
$name = $school->name;
$message = 'Delete school #'.$school->id.' - '.$school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['schools' => [$school->id]],
]);
$school->delete(); $school->delete();
return to_route('admin.schools.index')->with('success', 'School '.$school->name.' deleted'); return to_route('admin.schools.index')->with('success', 'School '.$school->name.' deleted');
@ -97,6 +141,9 @@ class SchoolController extends Controller
public function add_domain(School $school) public function add_domain(School $school)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([ request()->validate([
// validate that the combination of school and domain is unique on the school_email_domains table // validate that the combination of school and domain is unique on the school_email_domains table
'domain' => ['required'], 'domain' => ['required'],
@ -105,6 +152,12 @@ class SchoolController extends Controller
'school_id' => $school->id, 'school_id' => $school->id,
'domain' => request('domain'), 'domain' => request('domain'),
]); ]);
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => 'Added '.request('domain').' as an email domain for school #'.$school->id.' - '.$school->name,
'affected' => ['schools' => [$school->id]],
]);
return redirect()->route('admin.schools.show', $school)->with('success', 'Domain Added'); return redirect()->route('admin.schools.show', $school)->with('success', 'Domain Added');
@ -116,11 +169,9 @@ class SchoolController extends Controller
$domain->delete(); $domain->delete();
// return a redirect to the previous URL // return a redirect to the previous URL
return redirect()->back()->with('success', 'Domain removed successfully.'); return redirect()->back();
} }
// TODO: Add testing for invoicing
/** @codeCoverageIgnore */
public function viewInvoice(School $school) public function viewInvoice(School $school)
{ {
$invoiceData = $this->invoiceService->allData($school->id); $invoiceData = $this->invoiceService->allData($school->id);
@ -133,9 +184,8 @@ class SchoolController extends Controller
if ($user->school_id !== $school->id) { if ($user->school_id !== $school->id) {
return redirect()->back()->with('error', 'That user is not at that school'); return redirect()->back()->with('error', 'That user is not at that school');
} }
/** @noinspection PhpUnhandledExceptionInspection */
$headSetter->setHeadDirector($user); $headSetter->setHeadDirector($user);
return redirect()->back()->with('success', 'Head director set successfully.'); return redirect()->back()->with('success', 'Head director set');
} }
} }

View File

@ -1,16 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\School;
class SchoolEmailDomainController extends Controller
{
public function index()
{
$schools = School::with('emailDomains')->get();
return view('admin.schools.email_domains_index', compact('schools'));
}
}

View File

@ -3,12 +3,13 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\SubscoreDefinitionRequest;
use App\Models\ScoringGuide; use App\Models\ScoringGuide;
use App\Models\SubscoreDefinition; use App\Models\SubscoreDefinition;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use function abort;
use function auditionSetting; use function auditionSetting;
use function request; use function request;
use function response; use function response;
@ -27,19 +28,26 @@ class ScoringGuideController extends Controller
public function store() public function store()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([ request()->validate([
'name' => ['required', 'unique:scoring_guides'], 'name' => ['required', 'unique:scoring_guides'],
]); ]);
ScoringGuide::create([ $guide = ScoringGuide::create([
'name' => request('name'), 'name' => request('name'),
]); ]);
return redirect(route('admin.scoring.index'))->with('success', 'Scoring guide created'); return redirect(route('admin.scoring.index'))->with('success', 'Scoring guide created');
} }
public function edit(ScoringGuide $guide, string $tab = 'detail') public function edit(Request $request, ScoringGuide $guide, string $tab = 'detail')
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
if ($tab == 'tiebreakOrder') { if ($tab == 'tiebreakOrder') {
$subscores = SubscoreDefinition::where('scoring_guide_id', $guide->id)->orderBy('tiebreak_order')->get(); $subscores = SubscoreDefinition::where('scoring_guide_id', $guide->id)->orderBy('tiebreak_order')->get();
} else { } else {
@ -51,6 +59,9 @@ class ScoringGuideController extends Controller
public function update(ScoringGuide $guide) public function update(ScoringGuide $guide)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([ request()->validate([
'name' => ['required', 'unique:scoring_guides'], 'name' => ['required', 'unique:scoring_guides'],
]); ]);
@ -64,9 +75,12 @@ class ScoringGuideController extends Controller
public function destroy(ScoringGuide $guide) public function destroy(ScoringGuide $guide)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
if ($guide->auditions()->count() > 0) { if ($guide->auditions()->count() > 0) {
return redirect('/admin/scoring')->with('error', return redirect('/admin/scoring')->with('error', 'Cannot delete scoring guide with auditions');
'Cannot delete scoring guide being used by one or more auditions');
} }
$guide->delete(); $guide->delete();
@ -74,64 +88,89 @@ class ScoringGuideController extends Controller
return redirect('/admin/scoring')->with('success', 'Scoring guide deleted'); return redirect('/admin/scoring')->with('success', 'Scoring guide deleted');
} }
public function subscore_store(SubscoreDefinitionRequest $request, ScoringGuide $guide) public function subscore_store(Request $request, ScoringGuide $guide)
{ {
if (! $guide->exists()) {
abort(409);
}
$validateData = request()->validate([
'name' => ['required'],
'max_score' => ['required', 'integer'],
'weight' => ['required', 'integer'],
'for_seating' => ['nullable', 'boolean'],
'for_advance' => ['nullable', 'boolean'],
]);
$validateData = $request->validated(); $for_seating = $request->has('for_seating') ? (bool) $request->input('for_seating') : false;
$for_advance = $request->has('for_advance') ? (bool) $request->input('for_advance') : false;
if (! auditionSetting('advanceTo')) {
$for_seating = true;
}
// Put the new subscore at the end of the list for both display and tiebreak order
$display_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('display_order') + 1; $display_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('display_order') + 1;
$tiebreak_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('tiebreak_order') + 1; $tiebreak_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('tiebreak_order') + 1;
if (! auditionSetting('advanceTo')) {
$validateData['for_advance'] = 0; $subscore = SubscoreDefinition::create([
$validateData['for_seating'] = 1;
}
SubscoreDefinition::create([
'scoring_guide_id' => $guide->id, 'scoring_guide_id' => $guide->id,
'name' => $validateData['name'], 'name' => $validateData['name'],
'max_score' => $validateData['max_score'], 'max_score' => $validateData['max_score'],
'weight' => $validateData['weight'], 'weight' => $validateData['weight'],
'display_order' => $display_order, 'display_order' => $display_order,
'tiebreak_order' => $tiebreak_order, 'tiebreak_order' => $tiebreak_order,
'for_seating' => $validateData['for_seating'], 'for_seating' => $for_seating,
'for_advance' => $validateData['for_advance'], 'for_advance' => $for_advance,
]); ]);
return redirect(route('admin.scoring.edit', $guide))->with('success', 'Subscore added'); return redirect(route('admin.scoring.edit', $guide))->with('success', 'Subscore added');
} }
public function subscore_update( public function subscore_update(ScoringGuide $guide, SubscoreDefinition $subscore)
SubscoreDefinitionRequest $request, {
ScoringGuide $guide, if (! Auth::user()->is_admin) {
SubscoreDefinition $subscore abort(403);
) {
if ($subscore->scoring_guide_id !== $guide->id) { // Make sure the subscore were updating belongs to the guide
return redirect('/admin/scoring/guides/'.$subscore->scoring_guide_id.'/edit')->with('error',
'Cannot update a subscore for a different scoring guide');
} }
$validateData = $validateData = $request->validated(); if (! $guide->exists() || ! $subscore->exists()) {
abort(409);
}
if ($subscore->scoring_guide_id !== $guide->id) { // Make sure the subscore were updating belongs to the guide
abort(409);
}
$validateData = request()->validate([
'name' => ['required'],
'max_score' => ['required', 'integer'],
'weight' => ['required', 'integer'],
'for_seating' => ['nullable', 'boolean'],
'for_advance' => ['nullable', 'boolean'],
]);
$for_seating = request()->has('for_seating') ? (bool) request()->input('for_seating') : false;
$for_advance = request()->has('for_advance') ? (bool) request()->input('for_advance') : false;
if (! auditionSetting('advanceTo')) { if (! auditionSetting('advanceTo')) {
$validateData['for_advance'] = 0; $for_seating = true;
$validateData['for_seating'] = 1;
} }
$subscore->update([ $subscore->update([
'name' => $validateData['name'], 'name' => $validateData['name'],
'max_score' => $validateData['max_score'], 'max_score' => $validateData['max_score'],
'weight' => $validateData['weight'], 'weight' => $validateData['weight'],
'for_seating' => $validateData['for_seating'], 'for_seating' => $for_seating,
'for_advance' => $validateData['for_advance'], 'for_advance' => $for_advance,
]); ]);
return redirect(route('admin.scoring.edit', $guide))->with('success', 'Subscore updated'); return redirect('/admin/scoring/guides/'.$guide->id.'/edit')->with('success', 'Subscore updated');
} }
public function subscore_destroy(ScoringGuide $guide, SubscoreDefinition $subscore) public function subscore_destroy(ScoringGuide $guide, SubscoreDefinition $subscore)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
if (! $guide->exists() || ! $subscore->exists()) {
abort(409);
}
if ($subscore->scoring_guide_id !== $guide->id) { // Make sure the subscore were updating belongs to the guide if ($subscore->scoring_guide_id !== $guide->id) { // Make sure the subscore were updating belongs to the guide
abort(409);
return redirect(route('admin.scoring.edit', $subscore->scoring_guide_id))->with('error',
'Cannot delete a subscore for a different scoring guide');
} }
$subscore->delete(); $subscore->delete();
@ -142,6 +181,9 @@ class ScoringGuideController extends Controller
public function reorder_display(Request $request) public function reorder_display(Request $request)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order; $order = $request->order;
foreach ($order as $index => $id) { foreach ($order as $index => $id) {
$subscore = SubscoreDefinition::find($id); $subscore = SubscoreDefinition::find($id);
@ -154,6 +196,9 @@ class ScoringGuideController extends Controller
public function reorder_tiebreak(Request $request) public function reorder_tiebreak(Request $request)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order; $order = $request->order;
foreach ($order as $index => $id) { foreach ($order as $index => $id) {
$subscore = SubscoreDefinition::find($id); $subscore = SubscoreDefinition::find($id);

View File

@ -2,16 +2,17 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Actions\Students\CreateStudent;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\StudentStoreRequest;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\Event; use App\Models\Event;
use App\Models\NominationEnsemble; use App\Models\NominationEnsemble;
use App\Models\School; use App\Models\School;
use App\Models\Student; use App\Models\Student;
use Illuminate\Support\Facades\Auth;
use function abort;
use function auth; use function auth;
use function compact; use function compact;
use function max; use function max;
@ -24,6 +25,9 @@ class StudentController extends Controller
{ {
public function index() public function index()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$filters = session('adminStudentFilters') ?? null; $filters = session('adminStudentFilters') ?? null;
$schools = School::orderBy('name')->get(); $schools = School::orderBy('name')->get();
$students = Student::with(['school'])->withCount('entries')->orderBy('last_name')->orderBy('first_name'); $students = Student::with(['school'])->withCount('entries')->orderBy('last_name')->orderBy('first_name');
@ -50,52 +54,155 @@ class StudentController extends Controller
public function create() public function create()
{ {
$minGrade = $this->minimumGrade(); if (! Auth::user()->is_admin) {
$maxGrade = $this->maximumGrade(); abort(403);
}
$minGrade = min(Audition::min('minimum_grade'), NominationEnsemble::min('minimum_grade'));
$maxGrade = max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade'));
$schools = School::orderBy('name')->get(); $schools = School::orderBy('name')->get();
return view('admin.students.create', ['schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]); return view('admin.students.create', ['schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]);
} }
public function store(StudentStoreRequest $request, CreateStudent $creator) public function store()
{ {
/** @noinspection PhpUnhandledExceptionInspection */ if (! Auth::user()->is_admin) {
$creator([ abort(403);
'first_name' => $request['first_name'], }
'last_name' => $request['last_name'], request()->validate([
'grade' => $request['grade'], 'first_name' => ['required'],
'school_id' => $request['school_id'], 'last_name' => ['required'],
'optional_data' => $request->optional_data, 'grade' => ['required', 'integer'],
'school_id' => ['required', 'exists:schools,id'],
]); ]);
return redirect(route('admin.students.index'))->with('success', 'Student created successfully'); if (Student::where('first_name', request('first_name'))
->where('last_name', request('last_name'))
->where('school_id', request('school_id'))
->exists()) {
return redirect('/admin/students/create')->with('error', 'This student already exists.');
}
$student = Student::create([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
'school_id' => request('school_id'),
]);
$message = 'Created student #'.$student->id.' - '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
]);
return redirect('/admin/students')->with('success', 'Created student successfully');
} }
public function edit(Student $student) public function edit(Student $student)
{ {
$minGrade = $this->minimumGrade(); if (! Auth::user()->is_admin) {
$maxGrade = $this->maximumGrade(); abort(403);
}
$minGrade = min(Audition::min('minimum_grade'), NominationEnsemble::min('minimum_grade'));
$maxGrade = max(Audition::max('maximum_grade'), NominationEnsemble::max('maximum_grade'));
$schools = School::orderBy('name')->get(); $schools = School::orderBy('name')->get();
$student->loadCount('entries'); $student->loadCount('entries');
$event_entries = $student->entries()->with('audition.flags')->get()->groupBy('audition.event_id'); $entries = $student->entries;
$events = Event::all(); $events = Event::all();
$event_entries = [];
$logEntries = AuditLogEntry::whereJsonContains('affected->students', $student->id)->orderBy('created_at', foreach ($events as $event) {
'desc')->get(); $event_entries[$event->id] = $entries->filter(function ($entry) use ($event) {
return $event->id === $entry->audition->event_id;
return view('admin.students.edit', });
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries', 'logEntries')); // Check if doubler status can change
foreach ($event_entries[$event->id] as $entry) {
$entry->doubler_decision_frozen = $this->isDoublerStatusFrozen($entry, $event_entries[$event->id]);
}
} }
public function update(StudentStoreRequest $request, Student $student) return view('admin.students.edit',
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries'));
}
private function isDoublerStatusFrozen(Entry $entry, $entries)
{ {
// Can't change decision if results are published
if ($entry->audition->hasFlag('seats_published')) {
return true;
}
// Can't change decision if this is the only entry
if ($entries->count() === 1) {
return true;
}
// Can't change decision if this is the only entry with results not published
$unpublished = $entries->reject(function ($entry) {
return $entry->audition->hasFlag('seats_published');
});
if ($unpublished->count() < 2) {
return true;
}
// Can't change decision if we've accepted another audition
foreach ($entries as $checkEntry) {
if ($checkEntry->audition->hasFlag('seats_published') && ! $checkEntry->hasFlag('declined')) {
return true;
}
}
return false;
}
public function update(Student $student)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
'school_id' => ['required', 'exists:schools,id'],
]);
foreach ($student->entries as $entry) {
if ($entry->audition->minimum_grade > request('grade') || $entry->audition->maximum_grade < request('grade')) {
return redirect('/admin/students/'.$student->id.'/edit')->with('error',
'This student is entered in an audition that is not available to their new grade.');
}
}
if (Student::where('first_name', request('first_name'))
->where('last_name', request('last_name'))
->where('school_id', request('school_id'))
->where('id', '!=', $student->id)
->exists()) {
return redirect('/admin/students/'.$student->id.'/edit')->with('error',
'A student with that name already exists at that school');
}
$student->update([ $student->update([
'first_name' => $request['first_name'], 'first_name' => request('first_name'),
'last_name' => $request['last_name'], 'last_name' => request('last_name'),
'grade' => $request['grade'], 'grade' => request('grade'),
'school_id' => $request['school_id'], 'school_id' => request('school_id'),
'optional_data' => $request->optional_data, ]);
$message = 'Updated student #'.$student->id.'<br>Name: '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'students' => [$student->id],
'schools' => [$student->school_id],
],
]); ]);
return redirect('/admin/students')->with('success', 'Student updated'); return redirect('/admin/students')->with('success', 'Student updated');
@ -105,7 +212,7 @@ class StudentController extends Controller
public function destroy(Student $student) public function destroy(Student $student)
{ {
if ($student->entries()->count() > 0) { if ($student->entries()->count() > 0) {
return to_route('admin.students.index')->with('error', 'Student has entries and cannot be deleted'); return to_route('admin.students.index')->with('error', 'You cannot delete a student with entries.');
} }
$name = $student->full_name(); $name = $student->full_name();
$message = 'Deleted student #'.$student->id.'<br>Name: '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name; $message = 'Deleted student #'.$student->id.'<br>Name: '.$student->full_name().'<br>Grade: '.$student->grade.'<br>School: '.$student->school->name;
@ -123,33 +230,8 @@ class StudentController extends Controller
return to_route('admin.students.index')->with('success', 'Student '.$name.' deleted successfully.'); return to_route('admin.students.index')->with('success', 'Student '.$name.' deleted successfully.');
} }
private function minimumGrade(): int public function set_filter()
{ {
$nomMin = NominationEnsemble::min('minimum_grade'); //
$normMin = Audition::min('minimum_grade');
if (is_null($nomMin)) {
$minGrade = $normMin;
} else {
$minGrade = min($nomMin, $normMin);
}
return $minGrade;
}
private function maximumGrade(): int
{
$nomMax = NominationEnsemble::max('maximum_grade');
$normMax = Audition::max('maximum_grade');
if (is_null($nomMax)) {
$maxGrade = $normMax;
} else {
$maxGrade = max($nomMax, $normMax);
}
return $maxGrade;
} }
} }

View File

@ -1,13 +1,7 @@
<?php <?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\UpdateUserPrivileges;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Actions\Schools\AssignUserToSchool;
use App\Actions\Schools\SetHeadDirector; use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Mail\NewUserPassword; use App\Mail\NewUserPassword;
@ -15,6 +9,7 @@ use App\Models\AuditLogEntry;
use App\Models\School; use App\Models\School;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -25,6 +20,9 @@ class UserController extends Controller
{ {
public function index() public function index()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$users = User::with('school')->with('flags')->orderBy('last_name')->orderBy('first_name')->get(); $users = User::with('school')->with('flags')->orderBy('last_name')->orderBy('first_name')->get();
return view('admin.users.index', ['users' => $users]); return view('admin.users.index', ['users' => $users]);
@ -32,65 +30,95 @@ class UserController extends Controller
public function edit(User $user) public function edit(User $user)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$schools = School::orderBy('name')->get(); $schools = School::orderBy('name')->get();
$logEntries = AuditLogEntry::whereJsonContains('affected->users', $user->id)->orderBy('created_at',
'desc')->get();
$userActions = AuditLogEntry::where('user', $user->email)->orderBy('created_at', 'desc')->get();
return view('admin.users.edit', compact('user', 'schools', 'logEntries', 'userActions')); return view('admin.users.edit', ['user' => $user, 'schools' => $schools]);
} }
public function create() public function create()
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$schools = School::orderBy('name')->get(); $schools = School::orderBy('name')->get();
return view('admin.users.create', ['schools' => $schools]); return view('admin.users.create', ['schools' => $schools]);
} }
public function update( public function update(Request $request, User $user, SetHeadDirector $headSetter)
Request $request, {
User $user, if (! Auth::user()->is_admin) {
SetHeadDirector $headSetter, abort(403);
UpdateUserProfileInformation $profileUpdater,
AssignUserToSchool $schoolAssigner,
UpdateUserPrivileges $privilegesUpdater
) {
// Update basic profile data
$profileData = [
'first_name' => $request->get('first_name'),
'last_name' => $request->get('last_name'),
'email' => $request->get('email'),
'cell_phone' => $request->get('cell_phone'),
'judging_preference' => $request->get('judging_preference'),
];
$profileUpdater->update($user, $profileData);
// Deal with school assignment
dump($request->get('school_id'));
if ($user->school_id != $request->get('school_id')) {
$schoolAssigner($user, $request->get('school_id'));
} }
$oldEmail = $user->email;
$wasAdmin = $user->is_admin;
$wasTab = $user->is_tab;
$validData = $request->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email'],
'cell_phone' => ['required'],
'judging_preference' => ['required'],
'school_id' => ['nullable', 'exists:schools,id'],
]);
$validData['is_admin'] = $request->get('is_admin') == 'on' ? 1 : 0;
$validData['is_tab'] = $request->get('is_tab') == 'on' ? 1 : 0;
$validData['is_head'] = $request->get('is_head') == 'on' ? 1 : 0;
$user->update([
'first_name' => $validData['first_name'],
'last_name' => $validData['last_name'],
'email' => $validData['email'],
'cell_phone' => $validData['cell_phone'],
'judging_preference' => $validData['judging_preference'],
'school_id' => $validData['school_id'],
'is_admin' => $validData['is_admin'],
'is_tab' => $validData['is_tab'],
]);
$user->refresh();
$logged_school = $user->school_id ? $user->school->name : 'No School';
$message = 'Updated user #'.$user->id.' - '.$oldEmail
.'<br>Name: '.$user->full_name()
.'<br>Email: '.$user->email
.'<br>Cell Phone: '.$user->cell_phone
.'<br>Judging Pref: '.$user->judging_preference
.'<br>School: '.$logged_school;
// Deal with the head director flag AuditLogEntry::create([
if ($request->has('head_director')) { 'user' => auth()->user()->email,
$headSetter($user); 'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
if ($user->is_admin != $wasAdmin) {
$messageStart = $user->is_admin ? 'Granted admin privileges to ' : 'Revoked admin privileges from ';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $messageStart.$user->full_name().' - '.$user->email,
'affected' => ['users' => [$user->id]],
]);
}
if ($user->is_tab != $wasTab) {
$messageStart = $user->is_tab ? 'Granted tabulation privileges to ' : 'Revoked tabulation privileges from ';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $messageStart.$user->full_name().' - '.$user->email,
'affected' => ['users' => [$user->id]],
]);
}
if ($user->hasFlag('head_director') != $validData['is_head'] && ! is_null($user->school_id)) {
if ($validData['is_head']) {
$headSetter->setHeadDirector($user);
} else { } else {
$user->removeFlag('head_director'); $user->removeFlag('head_director');
$logMessage = 'Removed '.$user->full_name().' as head director at '.$user->school->name;
$logAffected = ['users' => [$user->id], 'schools' => [$user->school_id]];
auditionLog($logMessage, $logAffected);
} }
// Deal with privileges
if ($request->has('is_admin')) {
$privilegesUpdater($user, 'grant', 'admin');
} else {
$privilegesUpdater($user, 'revoke', 'admin');
}
if ($request->has('is_tab')) {
$privilegesUpdater($user, 'grant', 'tab');
} else {
$privilegesUpdater($user, 'revoke', 'tab');
} }
return redirect('/admin/users'); return redirect('/admin/users');
@ -98,23 +126,60 @@ class UserController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$userCreator = app(CreateNewUser::class); $request->validate([
$randomPassword = Str::random(12); 'first_name' => ['required'],
$data = request()->all(); 'last_name' => ['required'],
$data['password'] = $randomPassword; 'email' => ['required', 'email', 'unique:users'],
$data['password_confirmation'] = $randomPassword;
$newDirector = $userCreator->create($data);
$newDirector->update([
'school_id' => $request->get('school_id') ?? null,
]); ]);
Mail::to($newDirector->email)->send(new NewUserPassword($newDirector, $randomPassword)); // Generate a random password
$randomPassword = Str::random(12);
return redirect(route('admin.users.index'))->with('success', 'Director added'); $user = User::make([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'email' => request('email'),
'cell_phone' => request('cell_phone'),
'judging_preference' => request('judging_preference'),
'password' => Hash::make($randomPassword),
]);
if (! is_null(request('school_id'))) {
$request->validate([
'school_id' => ['exists:schools,id'],
]);
}
$user->school_id = request('school_id');
$user->save();
$message = 'Created user '.$user->email.' - '.$user->full_name().'<br>Cell Phone: '.$user->cell_phone.'<br>Judging Pref: '.$user->judging_preference;
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
if ($user->school_id) {
$message = 'Set user '.$user->full_name().' ('.$user->email.') as a director at '.$user->school->name.'(#'.$user->school->id.')';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'users' => [$user->id],
'schools' => [$user->id],
],
]);
}
Mail::to($user->email)->send(new NewUserPassword($user, $randomPassword));
return redirect('/admin/users');
} }
public function destroy(User $user) public function destroy(User $user)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$message = 'Deleted user '.$user->email; $message = 'Deleted user '.$user->email;
AuditLogEntry::create([ AuditLogEntry::create([
'user' => auth()->user()->email, 'user' => auth()->user()->email,
@ -126,22 +191,4 @@ class UserController extends Controller
return redirect()->route('admin.users.index')->with('success', 'User deleted successfully'); return redirect()->route('admin.users.index')->with('success', 'User deleted successfully');
} }
public function setPassword(User $user, Request $request)
{
$validated = $request->validate([
'admin_password' => ['required', 'string', 'current_password:web'],
'new_password' => ['required', 'string', 'confirmed', 'min:8'],
]);
$user->forceFill([
'password' => Hash::make($validated['new_password']),
])->save();
auditionLog('Manually set password for '.$user->email, [
'users' => [$user->id],
]);
return redirect()->route('admin.users.index')->with('success',
'Password changed successfully for '.$user->email);
}
} }

View File

@ -1,26 +0,0 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Actions\YearEndProcedures\YearEndCleanup;
use App\Http\Controllers\Controller;
use function auditionLog;
class YearEndResetController extends Controller
{
public function index()
{
return view('admin.year_end_reset');
}
public function execute()
{
$cleanUpProcedure = app(YearEndCleanup::class);
$options = request()->options;
$cleanUpProcedure($options);
auditionLog('Executed year end reset.', []);
return redirect()->route('dashboard')->with('success', 'Year end reset completed');
}
}

View File

@ -2,6 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Entries\GetEntrySeatingResult;
use App\Actions\Tabulation\CalculateEntryScore;
use App\Actions\Tabulation\RankAuditionEntries;
use App\Models\AuditionFlag;
use App\Models\School; use App\Models\School;
use App\Services\Invoice\InvoiceDataService; use App\Services\Invoice\InvoiceDataService;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -24,15 +28,40 @@ class DashboardController extends Controller
} }
public function dashboard( public function dashboard(
CalculateEntryScore $scoreCalc,
GetEntrySeatingResult $resultGenerator,
RankAuditionEntries $ranker
) { ) {
return view('dashboard.dashboard');
// Info for director results report
$entries = Auth::user()->entries;
$entries = $entries->filter(function ($entry) {
return $entry->audition->hasFlag('seats_published');
});
$entries = $entries->sortBy(function ($entry) {
return $entry->student->full_name(true);
});
$scores = [];
$results = [];
$ranks = [];
foreach ($entries as $entry) {
$results[$entry->id] = $resultGenerator->getResult($entry);
if (! $entry->hasFlag('no_show') && ! $entry->hasFlag('failed_prelim')) {
$scores[$entry->id] = $scoreCalc->calculate('seating', $entry);
$auditionResults = $ranker->rank('seating', $entry->audition);
$ranks[$entry->id] = $auditionResults->firstWhere('id', $entry->id)->raw_rank;
}
}
$showRecapLink = AuditionFlag::where('flag_name', 'seats_published')->count() > 0;
return view('dashboard.dashboard', compact('entries', 'scores', 'results', 'ranks', 'showRecapLink'));
// return view('dashboard.dashboard'); // return view('dashboard.dashboard');
} }
public function my_school() public function my_school()
{ {
if (Auth::user()->school) { if (Auth::user()->school) {
return redirect(route('schools.show', auth()->user()->school)); return redirect('/schools/'.Auth::user()->school->id);
} }
$possibilities = Auth::user()->possibleSchools(); $possibilities = Auth::user()->possibleSchools();
if (count($possibilities) < 1) { if (count($possibilities) < 1) {

View File

@ -2,67 +2,88 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Http\Requests\DoublerRequestsStoreRequest; use App\Models\AuditLogEntry;
use App\Models\DoublerRequest; use App\Models\DoublerRequest;
use App\Models\Event; use App\Models\Event;
use Illuminate\Contracts\Foundation\Application; use App\Models\Student;
use Illuminate\Contracts\View\Factory; use App\Services\DoublerService;
use Illuminate\Contracts\View\View; use Barryvdh\Debugbar\Facades\Debugbar;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use function auth; use function auth;
use function compact; use function compact;
use function request;
use function to_route; use function to_route;
class DoublerRequestController extends Controller class DoublerRequestController extends Controller
{ {
/** public function index(DoublerService $doublerService)
* Display a listing of the resource.
*
* Data sent to view:
* - events - all existing events
* - existingRequests - previously made requests for each event, keyed by student id
* existingRequest[eventId][student id]-> Request
* - doublers - existing doublers, grouped by event. Keyed by event_id and student_id
*
* @return Application|Factory|View|\Illuminate\Foundation\Application|\Illuminate\View\View
*/
public function index()
{ {
$events = Event::all(); $events = Event::all();
$existingRequests = auth()->user()->school->doublerRequests $students = auth()->user()->school->students;
->groupBy('event_id') $studentIds = $students->pluck('id');
->map(function ($requestsForEvent) { $existingRequests = DoublerRequest::whereIn('student_id', $studentIds)->get();
return $requestsForEvent->keyBy('student_id'); $doublers = [];
}); foreach ($events as $event) {
$doublers = auth()->user()->school->doublers() $event_doublers = $doublerService->doublersForEvent($event);
->with('student') $doublers[$event->id] = $event_doublers;
->with('event')
->get()
->groupBy('event_id');
return view('doubler_request.index', compact('events', 'doublers', 'existingRequests'));
} }
public function makeRequest(DoublerRequestsStoreRequest $request) return view('doubler_request.index', compact('events', 'doublers', 'students', 'existingRequests'));
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function makeRequest()
{ {
foreach ($request->getDoublerRequests() as $thisRequest) { foreach (request()->get('doubler_requests') as $event_id => $requests) {
if (! $thisRequest['request']) { if (! Event::find($event_id)->exists()) {
DoublerRequest::where('event_id', $thisRequest['event_id']) return to_route('doubler_request.index')->with('error', 'Invalid event id specified');
->where('student_id', $thisRequest['student_id'])->delete(); }
$thisEvent = Event::find($event_id);
foreach ($requests as $student_id => $request) {
if (! Student::find($student_id)->exists()) {
return to_route('doubler_request.index')->with('error', 'Invalid student id specified');
}
$thisStudent = Student::find($student_id);
if (! $request) {
$oldRequest = DoublerRequest::where('student_id', $student_id)
->where('event_id', $event_id)
->first();
if ($oldRequest) {
Debugbar::info('hit');
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => 'Removed doubler request for '.$thisStudent->full_name().' in '.$thisEvent->name,
'affected' => ['students' => [$student_id]],
]);
$oldRequest->delete();
}
continue; continue;
} }
DoublerRequest::upsert([ DoublerRequest::upsert([
'event_id' => $thisRequest['event_id'], 'event_id' => $event_id,
'student_id' => $thisRequest['student_id'], 'student_id' => $student_id,
'request' => $thisRequest['request'], 'request' => $request,
], ],
uniqueBy: ['event_id', 'student_id'], uniqueBy: ['event_id', 'student_id'],
update: ['request'] update: ['request']
); );
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => 'Made doubler request for '.$thisStudent->full_name().' in '.$thisEvent->name.'<br>Request: '.$request,
'affected' => ['students' => [$student_id]],
]);
} }
}
echo 'hi';
return to_route('doubler_request.index')->with('success', 'Recorded doubler requests'); return to_route('doubler_request.index')->with('success', 'Recorded doubler requests');
} }
} }

View File

@ -3,9 +3,11 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Entries\CreateEntry; use App\Actions\Entries\CreateEntry;
use App\Http\Requests\EntryStoreRequest; use App\Exceptions\ManageEntryException;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
use Carbon\Carbon;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -15,19 +17,11 @@ class EntryController extends Controller
{ {
public function index() public function index()
{ {
if (! auth()->user()->school_id) {
abort(403);
}
$entries = Auth::user()->entries() $entries = Auth::user()->entries()->with(['student', 'audition'])->get();
->select('entries.*') $entries = $entries->sortBy(function ($entry) {
->join('students as s', 's.id', '=', 'entries.student_id') return $entry->student->last_name.$entry->student->first_name.$entry->audition->score_order;
->join('auditions as a', 'a.id', '=', 'entries.audition_id') });
->with(['student', 'audition'])
->orderBy('s.last_name')
->orderBy('s.first_name')
->orderBy('a.score_order')
->get();
$auditions = Audition::open()->get(); $auditions = Audition::open()->get();
$students = Auth::user()->students; $students = Auth::user()->students;
$students->load('school'); $students->load('school');
@ -35,15 +29,37 @@ class EntryController extends Controller
return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]); return view('entries.index', ['entries' => $entries, 'students' => $students, 'auditions' => $auditions]);
} }
public function store(EntryStoreRequest $request, CreateEntry $creator) public function store(Request $request, CreateEntry $creator)
{ {
$validData = $request->validatedWithEnterFor(); if ($request->user()->cannot('create', Entry::class)) {
$creator( abort(403);
$validData['student_id'], }
$validData['audition_id'], $validData = $request->validate([
for_seating: $validData['for_seating'], 'student_id' => ['required', 'exists:students,id'],
for_advancement: $validData['for_advancement'], 'audition_id' => ['required', 'exists:auditions,id'],
); ]);
$audition = Audition::find($validData['audition_id']);
$currentDate = Carbon::now('America/Chicago');
$currentDate = $currentDate->format('Y-m-d');
if ($audition->entry_deadline < $currentDate) {
return redirect()->route('entries.index')->with('error', 'The entry deadline for that audition has passed');
}
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
$enter_for = [];
if ($validData['for_seating']) {
$enter_for[] = 'seating';
}
if ($validData['for_advancement']) {
$enter_for[] = 'advancement';
}
try {
$creator($validData['student_id'], $validData['audition_id'], $enter_for);
} catch (ManageEntryException $ex) {
return redirect()->route('entries.index')->with('error', $ex->getMessage());
}
return redirect()->route('entries.index')->with('success', 'The entry has been added.'); return redirect()->route('entries.index')->with('success', 'The entry has been added.');
} }
@ -53,7 +69,21 @@ class EntryController extends Controller
if ($request->user()->cannot('delete', $entry)) { if ($request->user()->cannot('delete', $entry)) {
abort(403); abort(403);
} }
if (auth()->user()) {
$message = 'Deleted entry '.$entry->id;
$affected = [
'entries' => [$entry->id],
'auditions' => [$entry->audition_id],
'schools' => [$entry->student->school_id],
'students' => [$entry->student_id],
];
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => $affected,
]);
}
$entry->delete(); $entry->delete();
return redirect()->route('entries.index')->with('success', return redirect()->route('entries.index')->with('success',

View File

@ -20,14 +20,14 @@ class FilterController extends Controller
session(['adminEntryFilters' => $filters]); session(['adminEntryFilters' => $filters]);
return redirect(route('admin.entries.index'))->with('success', 'Filters Applied'); return redirect('/admin/entries')->with('success', 'Filters Applied');
} }
public function clearAdminEntryFilter(Request $request) public function clearAdminEntryFilter(Request $request)
{ {
session()->forget('adminEntryFilters'); session()->forget('adminEntryFilters');
return redirect(route('admin.entries.index'))->with('success', 'Filters Cleared'); return redirect('/admin/entries')->with('success', 'Filters Cleared');
} }
public function adminStudentFilter(Request $request) public function adminStudentFilter(Request $request)
@ -40,7 +40,7 @@ class FilterController extends Controller
session(['adminStudentFilters' => $filters]); session(['adminStudentFilters' => $filters]);
return redirect(route('admin.students.index'))->with('success', 'Filters Applied'); return redirect()->back()->with('success', 'Filters Applied');
} }
public function clearAdminStudentFilter() public function clearAdminStudentFilter()

View File

@ -12,23 +12,16 @@ use function redirect;
class BonusScoreEntryController extends Controller class BonusScoreEntryController extends Controller
{ {
/**
* Displays a form for a judge to enter a bonus score for an entry.
*/
public function __invoke(Entry $entry) public function __invoke(Entry $entry)
{ {
// We can't submit another bonus score for this entry if we have already submitted one.
if (BonusScore::where('entry_id', $entry->id)->where('user_id', Auth::user()->id)->exists()) { if (BonusScore::where('entry_id', $entry->id)->where('user_id', Auth::user()->id)->exists()) {
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('error', return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('error', 'You have already judged that entry');
'You have already judged that entry');
} }
/** @var BonusScoreDefinition $bonusScore */ /** @var BonusScoreDefinition $bonusScore */
$bonusScore = $entry->audition->bonusScore()->first(); $bonusScore = $entry->audition->bonusScore()->first();
if (! $bonusScore->judges->contains(auth()->id())) { if (! $bonusScore->judges->contains(auth()->id())) {
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge that entry'); return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this entry');
} }
$maxScore = $bonusScore->max_score; $maxScore = $bonusScore->max_score;
$bonusName = $bonusScore->name; $bonusName = $bonusScore->name;

View File

@ -10,15 +10,12 @@ use Illuminate\Support\Facades\Auth;
class BonusScoreEntryListController extends Controller class BonusScoreEntryListController extends Controller
{ {
/**
* Lists entries for a bonus score so the judge may select one to score.
*/
public function __invoke(Audition $audition) public function __invoke(Audition $audition)
{ {
/** @var BonusScoreDefinition $bonusScore */ /** @var BonusScoreDefinition $bonusScore */
$bonusScore = $audition->bonusScore()->first(); $bonusScore = $audition->bonusScore()->first();
if (! $bonusScore->judges->contains(auth()->id())) { if (! $bonusScore->judges->contains(auth()->id())) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that bonus score'); return redirect()->route('dashboard')->with('error', 'You are not assigned to judge this bonus score');
} }
$entries = $audition->entries()->orderBy('draw_number')->get(); $entries = $audition->entries()->orderBy('draw_number')->get();
$entries = $entries->reject(fn ($entry) => $entry->hasFlag('no_show')); $entries = $entries->reject(fn ($entry) => $entry->hasFlag('no_show'));

View File

@ -3,7 +3,7 @@
namespace App\Http\Controllers\Judging; namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterBonusScore; use App\Actions\Tabulation\EnterBonusScore;
use App\Exceptions\AuditionAdminException; use App\Exceptions\ScoreEntryException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Entry; use App\Models\Entry;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
@ -14,17 +14,15 @@ class BonusScoreRecordController extends Controller
public function __invoke(Entry $entry) public function __invoke(Entry $entry)
{ {
$enterBonusScore = App::make(EnterBonusScore::class); $enterBonusScore = App::make(EnterBonusScore::class);
$max = $entry->audition->bonusScore()->first()->max_score;
$validData = request()->validate([ $validData = request()->validate([
'score' => 'required|integer|min:0|max:'.$max, 'score' => 'required|integer',
]); ]);
try { try {
$enterBonusScore(Auth::user(), $entry, $validData['score']); $enterBonusScore(Auth::user(), $entry, $validData['score']);
} catch (AuditionAdminException $ex) { } catch (ScoreEntryException $ex) {
return redirect(route('dashboard'))->with('error', 'Score Entry Error - '.$ex->getMessage()); return redirect()->back()->with('error', 'Score Entry Error - '.$ex->getMessage());
} }
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('success', return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('Score Recorded Successfully');
'Score Recorded Successfully');
} }
} }

View File

@ -3,19 +3,22 @@
namespace App\Http\Controllers\Judging; namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterScore; use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionAdminException; use App\Exceptions\AuditionServiceException;
use App\Exceptions\ScoreEntryException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Entry; use App\Models\Entry;
use App\Models\JudgeAdvancementVote; use App\Models\JudgeAdvancementVote;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
use App\Services\AuditionService; use App\Services\AuditionService;
use Exception;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Gate;
use function compact; use function compact;
use function redirect; use function redirect;
use function url;
class JudgingController extends Controller class JudgingController extends Controller
{ {
@ -28,7 +31,7 @@ class JudgingController extends Controller
public function index() public function index()
{ {
$rooms = Auth::user()->judgingAssignments()->with('auditions')->with('prelimAuditions')->get(); $rooms = Auth::user()->judgingAssignments()->with('auditions')->get();
$bonusScoresToJudge = Auth::user()->bonusJudgingAssignments()->with('auditions')->get(); $bonusScoresToJudge = Auth::user()->bonusJudgingAssignments()->with('auditions')->get();
//$rooms->load('auditions'); //$rooms->load('auditions');
@ -37,16 +40,10 @@ class JudgingController extends Controller
public function auditionEntryList(Request $request, Audition $audition) public function auditionEntryList(Request $request, Audition $audition)
{ {
// TODO: Add error message if scoring guide is not set
if ($request->user()->cannot('judge', $audition)) { if ($request->user()->cannot('judge', $audition)) {
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge that audition'); return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this audition');
} }
$entries = Entry::where('audition_id', '=', $audition->id)->orderBy('draw_number')->with('audition')->get(); $entries = Entry::where('audition_id', '=', $audition->id)->orderBy('draw_number')->with('audition')->get();
// If there is a prelim audition, only show entries that have passed the prelim
if ($audition->prelimDefinition) {
$entries = $entries->reject(fn ($entry) => ! $entry->hasFlag('passed_prelim'));
}
$subscores = $audition->scoringGuide->subscores()->orderBy('display_order')->get(); $subscores = $audition->scoringGuide->subscores()->orderBy('display_order')->get();
$votes = JudgeAdvancementVote::where('user_id', Auth::id())->get(); $votes = JudgeAdvancementVote::where('user_id', Auth::id())->get();
@ -71,13 +68,6 @@ class JudgingController extends Controller
return redirect()->route('judging.auditionEntryList', $entry->audition)->with('error', return redirect()->route('judging.auditionEntryList', $entry->audition)->with('error',
'The requested entry is marked as a no-show. Scores cannot be entered.'); '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; $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 = JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->first();
$oldVote = $oldVote ? $oldVote->vote : 'noVote'; $oldVote = $oldVote ? $oldVote->vote : 'noVote';
@ -88,11 +78,15 @@ class JudgingController extends Controller
public function saveScoreSheet(Request $request, Entry $entry, EnterScore $enterScore) public function saveScoreSheet(Request $request, Entry $entry, EnterScore $enterScore)
{ {
if ($request->user()->cannot('judge', $entry->audition)) { if ($request->user()->cannot('judge', $entry->audition)) {
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this entry'); abort(403, 'You are not assigned to judge this entry');
} }
// Validate form data // Validate form data
$subscores = $entry->audition->subscoreDefinitions; try {
$subscores = $this->auditionService->getSubscores($entry->audition, 'all');
} catch (AuditionServiceException $e) {
return redirect()->back()->with('error', 'Unable to get subscores - '.$e->getMessage());
}
$validationChecks = []; $validationChecks = [];
foreach ($subscores as $subscore) { foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score; $validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
@ -100,17 +94,16 @@ class JudgingController extends Controller
$validatedData = $request->validate($validationChecks); $validatedData = $request->validate($validationChecks);
// Enter the score // Enter the score
/** @noinspection PhpUnhandledExceptionInspection */
try { try {
$enterScore(Auth::user(), $entry, $validatedData['score']); $enterScore(Auth::user(), $entry, $validatedData['score']);
} catch (AuditionAdminException $e) { } catch (ScoreEntryException $e) {
return redirect()->back()->with('error', $e->getMessage()); return redirect()->back()->with('error', 'Error saving score - '.$e->getMessage());
} }
// Deal with an advancement vote if needed // Deal with an advancement vote if needed
$this->advancementVote($request, $entry); $this->advancementVote($request, $entry);
return redirect(route('judging.auditionEntryList', $entry->audition))->with('success', return redirect('/judging/audition/'.$entry->audition_id)->with('success',
'Entered scores for '.$entry->audition->name.' '.$entry->draw_number); 'Entered scores for '.$entry->audition->name.' '.$entry->draw_number);
} }
@ -118,10 +111,8 @@ class JudgingController extends Controller
public function updateScoreSheet(Request $request, Entry $entry, EnterScore $enterScore) public function updateScoreSheet(Request $request, Entry $entry, EnterScore $enterScore)
{ {
if ($request->user()->cannot('judge', $entry->audition)) { if ($request->user()->cannot('judge', $entry->audition)) {
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge this entry'); abort(403, '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(); $scoreSheet = ScoreSheet::where('user_id', Auth::id())->where('entry_id', $entry->id)->first();
if (! $scoreSheet) { if (! $scoreSheet) {
return redirect()->back()->with('error', 'Attempt to edit non existent score sheet'); return redirect()->back()->with('error', 'Attempt to edit non existent score sheet');
@ -129,8 +120,11 @@ class JudgingController extends Controller
Gate::authorize('update', $scoreSheet); Gate::authorize('update', $scoreSheet);
// Validate form data // Validate form data
try {
$subscores = $entry->audition->subscoreDefinitions; $subscores = $this->auditionService->getSubscores($entry->audition, 'all');
} catch (AuditionServiceException $e) {
return redirect()->back()->with('error', 'Error getting subscores - '.$e->getMessage());
}
$validationChecks = []; $validationChecks = [];
foreach ($subscores as $subscore) { foreach ($subscores as $subscore) {
@ -139,29 +133,38 @@ class JudgingController extends Controller
$validatedData = $request->validate($validationChecks); $validatedData = $request->validate($validationChecks);
// Enter the score // Enter the score
try {
$enterScore(Auth::user(), $entry, $validatedData['score'], $scoreSheet); $enterScore(Auth::user(), $entry, $validatedData['score'], $scoreSheet);
} catch (ScoreEntryException $e) {
return redirect()->back()->with('error', 'Error updating score - '.$e->getMessage());
}
$this->advancementVote($request, $entry); $this->advancementVote($request, $entry);
return redirect(route('judging.auditionEntryList', $entry->audition))->with('success', return redirect('/judging/audition/'.$entry->audition_id)->with('success',
'Updated scores for '.$entry->audition->name.' '.$entry->draw_number); 'Updated scores for '.$entry->audition->name.' '.$entry->draw_number);
} }
protected function advancementVote(Request $request, Entry $entry) 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')) { if ($entry->for_advancement and auditionSetting('advanceTo')) {
$request->validate([ $request->validate([
'advancement-vote' => ['required', 'in:yes,no,dq'], 'advancement-vote' => ['required', 'in:yes,no,dq'],
]); ]);
try {
JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->delete(); JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->delete();
JudgeAdvancementVote::create([ JudgeAdvancementVote::create([
'user_id' => Auth::user()->id, 'user_id' => Auth::user()->id,
'entry_id' => $entry->id, 'entry_id' => $entry->id,
'vote' => $request->input('advancement-vote'), 'vote' => $request->input('advancement-vote'),
]); ]);
} catch (Exception) {
return redirect(url()->previous())->with('error', 'Error saving advancement vote');
}
} }
return null; return null;

View File

@ -1,111 +0,0 @@
<?php
namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterPrelimScore;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class PrelimJudgingController extends Controller
{
public function prelimEntryList(PrelimDefinition $prelimDefinition)
{
if (auth()->user()->cannot('judge', $prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
$entries = $prelimDefinition->audition->entries;
$subscores = $prelimDefinition->scoringGuide->subscores()->orderBy('display_order')->get();
$published = $prelimDefinition->audition->hasFlag('seats_published');
$prelimScoresheets = PrelimScoreSheet::where('user_id', Auth::id())->get()->keyBy('entry_id');
return view('judging.prelim_entry_list',
compact('prelimDefinition', 'entries', 'subscores', 'published', 'prelimScoresheets'));
}
public function prelimScoreEntryForm(Entry $entry)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
if ($entry->audition->hasFlag('seats_published')) {
return redirect()->route('dashboard')->with('error',
'Scores for entries in published auditions cannot be modified.');
}
if ($entry->hasFlag('no_show')) {
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('error',
'The requested entry is marked as a no-show. Scores cannot be entered.');
}
$oldSheet = PrelimScoreSheet::where('user_id', Auth::id())->where('entry_id',
$entry->id)->value('subscores') ?? null;
if ($oldSheet) {
$formRoute = 'update.savePrelimScoreSheet';
$formMethod = 'PATCH';
} else {
$formRoute = 'judging.savePrelimScoreSheet';
$formMethod = 'POST';
}
return view('judging.prelim_entry_form', compact('entry', 'oldSheet', 'formRoute', 'formMethod'));
}
/**
* @throws AuditionAdminException
*/
public function savePrelimScoreSheet(Entry $entry, Request $request, EnterPrelimScore $scribe)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
// Validate form data
$subscores = $entry->audition->prelimDefinition->scoringGuide->subscores;
$validationChecks = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
}
$validatedData = $request->validate($validationChecks);
// Enter the score
$scribe(auth()->user(), $entry, $validatedData['score']);
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('success',
'Entered prelim scores for '.$entry->audition->name.' '.$entry->draw_number);
}
public function updatePrelimScoreSheet(Entry $entry, Request $request, EnterPrelimScore $scribe)
{
if (auth()->user()->cannot('judge', $entry->audition->prelimDefinition)) {
return redirect()->route('dashboard')->with('error', 'You are not assigned to judge that prelim audition.');
}
// Validate form data
$subscores = $entry->audition->prelimDefinition->scoringGuide->subscores;
$validationChecks = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
}
$validatedData = $request->validate($validationChecks);
// Get the existing score
$scoreSheet = PrelimScoreSheet::where('user_id', auth()->user()->id)->where('entry_id', $entry->id)->first();
if (! $scoreSheet) {
return redirect()->back()->with('error', 'No score sheet exists.');
}
// Update the score
$scribe(auth()->user(), $entry, $validatedData['score'], $scoreSheet);
return redirect()->route('judging.prelimEntryList', $entry->audition->prelimDefinition)->with('success',
'Updated prelim scores for '.$entry->audition->name.' '.$entry->draw_number);
}
}

View File

@ -2,56 +2,97 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Audition;
use App\Models\Entry; use App\Models\Entry;
use function compact;
class MonitorController extends Controller class MonitorController extends Controller
{ {
public function index() public function index()
{ {
if (! auth()->user()->hasFlag('monitor')) { if (! auth()->user()->hasFlag('monitor')) {
abort(403); return redirect()->route('dashboard')->with('error', 'You are not assigned as a monitor');
}
$method = 'GET';
$formRoute = 'monitor.enterFlag';
$title = 'Flag Entry';
return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
} }
$auditions = Audition::orderBy('score_order')->with('flags')->get(); public function flagForm()
$audition = null;
return view('monitor.index', compact('audition', 'auditions'));
}
public function auditionStatus(Audition $audition)
{ {
if (! auth()->user()->hasFlag('monitor')) { if (! auth()->user()->hasFlag('monitor')) {
abort(403); return redirect()->route('dashboard')->with('error', 'You are not assigned as a monitor');
}
$validData = request()->validate([
'entry_id' => ['required', 'integer', 'exists:entries,id'],
]);
$entry = Entry::find($validData['entry_id']);
// If the entries audition is published, bounce out
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advance_published')) {
return redirect()->route('monitor.index')->with('error', 'Cannot set flags while results are published');
} }
if ($audition->hasFlag('seats_published') || $audition->hasFlag('advancement_published')) { // If entry has scores, bounce on out
return redirect()->route('monitor.index')->with('error', 'Results for that audition are published'); if ($entry->scoreSheets()->count() > 0) {
return redirect()->route('monitor.index')->with('error', 'That entry has existing scores');
} }
$auditions = Audition::orderBy('score_order')->with('flags')->get(); return view('monitor_entry_flag_form', compact('entry'));
$entries = $audition->entries()->with('flags')->with('student.school')->withCount([
'prelimScoreSheets', 'scoreSheets',
])->orderBy('draw_number')->get();
return view('monitor.index', compact('audition', 'auditions', 'entries'));
} }
public function toggleNoShow(Entry $entry) public function storeFlag(Entry $entry)
{ {
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) { if (! auth()->user()->hasFlag('monitor')) {
return redirect()->route('monitor.index')->with('error', 'Results for that audition are published'); return redirect()->route('dashboard')->with('error', 'You are not assigned as a monitor');
} }
if ($entry->hasFlag('no_show')) { // If the entries audition is published, bounce out
$entry->removeFlag('no_show'); if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advance_published')) {
return redirect()->route('monitor.index')->with('error', 'Cannot set flags while results are published');
return redirect()->back()->with('success', 'No Show Flag Cleared');
} }
// If entry has scores, bounce on out
if ($entry->scoreSheets()->count() > 0) {
return redirect()->route('monitor.index')->with('error', 'That entry has existing scores');
}
$action = request()->input('action');
$result = match ($action) {
'failed-prelim' => $this->setFlag($entry, 'failed_prelim'),
'no-show' => $this->setFlag($entry, 'no_show'),
'clear' => $this->setFlag($entry, 'clear'),
default => redirect()->route('monitor.index')->with('error', 'Invalid action requested'),
};
if (! $result) {
return redirect()->route('monitor.index')->with('error', 'Failed to set flag');
}
return redirect()->route('monitor.index')->with('success', 'Flag set for entry #'.$entry->id);
}
private function setFlag(Entry $entry, string $flag)
{
if ($flag === 'no_show') {
$entry->removeFlag('failed_prelim');
$entry->addFlag('no_show'); $entry->addFlag('no_show');
return redirect()->back()->with('success', 'No Show Entered'); return true;
}
if ($flag === 'failed_prelim') {
$entry->addFlag('failed_prelim');
$entry->addFlag('no_show');
return true;
}
if ($flag === 'clear') {
$entry->removeFlag('failed_prelim');
$entry->removeFlag('no_show');
return true;
}
return false;
} }
} }

View File

@ -6,10 +6,6 @@ use App\Http\Controllers\Controller;
use App\Models\NominationEnsemble; use App\Models\NominationEnsemble;
use App\Models\NominationEnsembleEntry; use App\Models\NominationEnsembleEntry;
use App\Models\School; use App\Models\School;
use App\Models\Student;
use Illuminate\Validation\Rule;
use function redirect;
class MeobdaNominationAdminController extends Controller implements NominationAdminController class MeobdaNominationAdminController extends Controller implements NominationAdminController
{ {
@ -72,19 +68,12 @@ class MeobdaNominationAdminController extends Controller implements NominationAd
} }
if ($filterData['split'] ?? false) { if ($filterData['split'] ?? false) {
if ($filterData['split'] == 'NO-SPLIT-ASSIGNED') {
$nominations = $nominations->whereNull('data->split');
} else {
$splitFilter = explode('---', $filterData['split']); $splitFilter = explode('---', $filterData['split']);
$nominations = $nominations->where('nomination_ensemble_id', $splitFilter[0]); $nominations = $nominations->where('nomination_ensemble_id', $splitFilter[0]);
if ($splitFilter[1] != 'all') { if ($splitFilter[1] != 'all') {
$nominations = $nominations->where('data->split', $splitFilter[1]); $nominations = $nominations->where('data->split', $splitFilter[1]);
} }
} }
}
// Sort
$nominations = $nominations->orderBy('id', 'desc');
$nominations = $nominations->paginate(50); $nominations = $nominations->paginate(50);
@ -92,158 +81,33 @@ class MeobdaNominationAdminController extends Controller implements NominationAd
compact('nominations', 'schools', 'filterData', 'ensembles', 'sections', 'splits')); compact('nominations', 'schools', 'filterData', 'ensembles', 'sections', 'splits'));
} }
public function show(NominationEnsembleEntry $nominationEnsembleEntry) public function show(NominationEnsembleEntry $entry)
{ {
// TODO: Implement show() method. // TODO: Implement show() method.
} }
public function create() public function create()
{ {
$target_ensemble = null; // TODO: Implement create() method.
$instrumentation = null;
$students = null;
if (request()->get('ensemble')) {
$validData = request()->validate([
'ensemble' => 'nullable|exists:nomination_ensembles,id',
]);
$target_ensemble = NominationEnsemble::find($validData['ensemble']);
// Get viable students for entering
$students = Student::where('grade', '<=', $target_ensemble->maximum_grade)
->where('grade', '>=', $target_ensemble->minimum_grade)
->with('school')
->join('schools', 'schools.id', '=', 'students.school_id')
->orderBy('schools.name', 'asc')
->orderBy('students.last_name', 'asc')
->orderBy('students.first_name', 'asc')
->get(['students.*']);
// Remove students already nominated
$nominated_student_ids = NominationEnsembleEntry::where('nomination_ensemble_id',
$target_ensemble->id)->pluck('student_id')->all();
$students = $students->reject(function ($student) use ($nominated_student_ids) {
return in_array($student->id, $nominated_student_ids);
});
// Get current instrumentation of target ensemble
$instrumentation = $this->get_ensemble_instrumentation($target_ensemble);
}
$ensembles = NominationEnsemble::all();
return view('nomination_ensembles.meobda.admin.nomination-create',
compact('ensembles', 'target_ensemble', 'students', 'instrumentation'));
} }
public function store() public function store()
{ {
// Initial Validation // TODO: Implement store() method.
$validData = request()->validate([
'ensemble' => 'required|exists:nomination_ensembles,id',
'student' => 'required|exists:students,id',
'instrument' => 'required|string',
'split' => 'nullable|string',
'seat' => 'nullable|integer',
]);
$proposed_ensemble = NominationEnsemble::find($validData['ensemble']);
// Check if $validData['instrument'] is a valid instrument for the proposed ensemble
$validInstruments = array_column($proposed_ensemble->data['instruments'], 'name');
if (! in_array($validData['instrument'], $validInstruments)) {
return redirect()->back()->with('error', 'Invalid Instrument Specified');
} }
$data['instrument'] = $validData['instrument']; public function edit(NominationEnsembleEntry $entry)
if ($validData['seat'] > 0) {
$data['seat'] = $validData['seat'];
}
if ($validData['split'] != '---') {
$data['split'] = $validData['split'];
// Check if $validData['split'] is a valid split for the proposed ensemble
$validSplits = $proposed_ensemble->data['split_names'];
if (! in_array($validData['split'], $validSplits)) {
return redirect()->back()->with('error', 'Invalid Split Specified');
}
}
$newNomination = NominationEnsembleEntry::make([
'student_id' => $validData['student'],
'nomination_ensemble_id' => $validData['ensemble'],
'data' => $data,
]);
$newNomination->save();
return redirect()->route('nomination.admin.index')->with('success', 'New Nomination created');
}
public function edit(NominationEnsembleEntry $nominationEnsembleEntry)
{ {
$students = Student::with('school')->get() // TODO: Implement edit() method.
->sortBy('school.name');
if (! isset($nominationEnsembleEntry->data['seat'])) {
$data = $nominationEnsembleEntry->data;
$data['seat'] = null;
$nominationEnsembleEntry->data = $data;
} }
$instrumentation = $this->get_ensemble_instrumentation($nominationEnsembleEntry->ensemble); public function update(NominationEnsembleEntry $entry)
return view('nomination_ensembles.meobda.admin.nomination-edit',
compact('nominationEnsembleEntry', 'students', 'instrumentation'));
}
public function update(NominationEnsembleEntry $nominationEnsembleEntry)
{ {
$ensemble = $nominationEnsembleEntry->ensemble; // TODO: Implement update() method.
$validSplits = $ensemble->data['split_names'];
$validInstruments = [];
foreach ($ensemble->data['instruments'] as $instrument) {
$validInstruments[] = $instrument['name'];
}
$validData = request()->validate([
'instrument' => ['nullable', Rule::in($validInstruments)],
'split' => ['nullable', Rule::in($validSplits)],
'seat' => ['nullable', 'integer'],
]);
$data = $nominationEnsembleEntry->data;
$data['instrument'] = $validData['instrument'];
$data['split'] = $validData['split'];
$data['seat'] = $validData['seat'];
$nominationEnsembleEntry->update([
'data' => $data,
]);
return redirect()->route('nomination.admin.index')->with('success', 'Nomination updated');
} }
public function destroy(NominationEnsembleEntry $nominationEnsembleEntry) public function destroy(NominationEnsembleEntry $entry)
{ {
// TODO: Implement destroy() method. // TODO: Implement destroy() method.
} }
private function get_ensemble_instrumentation(NominationEnsemble $ensemble)
{
$entries = NominationEnsembleEntry::where('nomination_ensemble_id', $ensemble->id)->get();
$splits = $ensemble->data['split_names'];
$instruments = [];
foreach ($ensemble->data['instruments'] as $instrument) {
$instruments[] = $instrument['name'];
}
$counts = [];
foreach ($splits as $split) {
$counts[$split] = [];
foreach ($instruments as $instrument) {
$counts[$split][$instrument] = 0;
}
}
foreach ($entries as $entry) {
if (! isset($entry->data['split'])) {
continue;
}
$counts[$entry->data['split']][$entry->data['instrument']] += 1;
}
return $counts;
}
} }

View File

@ -1,81 +0,0 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsembleEntry;
use Codedge\Fpdf\Fpdf\Fpdf;
class MeobdaNominationAdminUtilitiesController extends Controller implements NominationAdminUtilitiesController
{
public function __invoke(string $action)
{
match ($action) {
'name_tags' => $this->printNameTags(),
default => $this->invalidAction(),
};
}
private function printNameTags()
{
$pdf = new Fpdf('P', 'in', 'letter');
$pdf->SetFont('Arial', 'B', 36);
$nominations = NominationEnsembleEntry::with('ensemble')
->with('student.school')
->orderBy('nomination_ensemble_id')
->orderBy('data->split')
->orderBy('data->instrument')
->orderByRaw('CAST(data->"$.seat" AS UNSIGNED)')
->get();
// echo '<table><tr><th>Ensemble</th><th>Split</th><th>Instrument</th><th>Seat</th><th>Name</th><th>School</th></tr>';
// foreach ($nominations as $nomination) {
// if (! isset($nomination->data['split'])) {
// $temp = $nomination->data;
// $temp['split'] = '---';
// $nomination->data = $temp;
// }
// if (! isset($nomination->data['seat'])) {
// $temp = $nomination->data;
// $temp['seat'] = '---';
// $nomination->data = $temp;
// }
// echo '<tr>';
// echo '<td>'.$nomination->ensemble->name.'</td>';
// echo '<td>'.$nomination->data['split'] ?? 'none'.'</td>';
// echo '<td>'.$nomination->data['instrument'].'</td>';
// echo '<td>'.$nomination->data['seat'].'</td>';
// echo '<td>'.$nomination->student->full_name().'</td>';
// echo '<td>'.$nomination->student->school->name.'</td>';
// echo '</tr>';
// }
// echo '</table>';
foreach ($nominations as $nomination) {
if (! isset($nomination->data['split'])) {
$temp = $nomination->data;
$temp['split'] = '---';
$nomination->data = $temp;
}
if (! isset($nomination->data['seat'])) {
$temp = $nomination->data;
$temp['seat'] = '---';
$nomination->data = $temp;
}
$pdf->AddPage();
$pdf->SetY('6');
$pdf->Cell(0, .8, $nomination->student->full_name(), 0, 1, 'C');
$pdf->Cell(0, .8, $nomination->student->school->name, 0, 1, 'C');
$pdf->Cell(0, .8, $nomination->data['split'], 0, 1, 'C');
$pdf->Cell(0, .8, $nomination->data['instrument'].' - '.$nomination->data['seat'], 0, 1, 'C');
}
$pdf->Output('D', 'StandNameTags.pdf');
}
private function invalidAction()
{
return redirect()->back()->with('error', 'Invalid Action');
}
}

View File

@ -1,56 +0,0 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\NominationEnsembleEntry;
use Illuminate\Support\Facades\Response;
class MeobdaNominationExportController extends Controller implements NominationExportController
{
public function __invoke()
{
$data = $this->getData();
// Create a callback to write the CSV data
$callback = function () use ($data) {
$file = fopen('php://output', 'w');
foreach ($data as $line) {
// Convert the string into an array
$fields = explode(',', $line);
// Write the array to the CSV file
fputcsv($file, $fields);
}
fclose($file);
};
// Return a response with the CSV content
return Response::stream($callback, 200, [
'Content-Type' => 'text/csv',
'Content-Disposition' => 'attachment; filename="audition_entries_export.csv"',
]);
dd($this->getData());
}
private function getData()
{
// Room, Audition, Draw Number, Name, School
$exportRows = [
'Ensemble,Split,Instrument,Seat,First Name, Last Name,School',
];
$nominations = NominationEnsembleEntry::with('ensemble')->with('student.school')->get();
foreach ($nominations as $nomination) {
$ensemble = $nomination->ensemble->name;
$split = $nomination->data['split'];
$instrument = $nomination->data['instrument'];
$seat = $nomination->data['seat'];
$firstName = $nomination->student->first_name;
$lastName = $nomination->student->last_name;
$schoolName = $nomination->student->school->name;
$exportRows[] = "$ensemble, $split, $instrument, $seat, $firstName, $lastName, $schoolName";
}
return $exportRows;
}
}

View File

@ -8,15 +8,15 @@ interface NominationAdminController
{ {
public function index(); public function index();
public function show(NominationEnsembleEntry $nominationEnsembleEntry); public function show(NominationEnsembleEntry $entry);
public function create(); public function create();
public function store(); public function store();
public function edit(NominationEnsembleEntry $nominationEnsembleEntry); public function edit(NominationEnsembleEntry $entry);
public function update(NominationEnsembleEntry $nominationEnsembleEntry); public function update(NominationEnsembleEntry $entry);
public function destroy(NominationEnsembleEntry $nominationEnsembleEntry); public function destroy(NominationEnsembleEntry $entry);
} }

View File

@ -1,8 +0,0 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
interface NominationAdminUtilitiesController
{
public function __invoke(string $action);
}

View File

@ -1,8 +0,0 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
interface NominationExportController
{
public function __invoke();
}

View File

@ -14,7 +14,7 @@ class ScobdaNominationAdminController extends Controller implements NominationAd
return view('nomination_ensembles.scobda.admin.index', compact('nominations')); return view('nomination_ensembles.scobda.admin.index', compact('nominations'));
} }
public function show(NominationEnsembleEntry $nominationEnsembleEntry) public function show(NominationEnsembleEntry $entry)
{ {
// TODO: Implement show() method. // TODO: Implement show() method.
} }
@ -29,17 +29,17 @@ class ScobdaNominationAdminController extends Controller implements NominationAd
// TODO: Implement store() method. // TODO: Implement store() method.
} }
public function edit(NominationEnsembleEntry $nominationEnsembleEntry) public function edit(NominationEnsembleEntry $entry)
{ {
// TODO: Implement edit() method. // TODO: Implement edit() method.
} }
public function update(NominationEnsembleEntry $nominationEnsembleEntry) public function update(NominationEnsembleEntry $entry)
{ {
// TODO: Implement update() method. // TODO: Implement update() method.
} }
public function destroy(NominationEnsembleEntry $nominationEnsembleEntry) public function destroy(NominationEnsembleEntry $entry)
{ {
// TODO: Implement destroy() method. // TODO: Implement destroy() method.
} }

View File

@ -1,66 +0,0 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
use App\Models\School;
use Codedge\Fpdf\Fpdf\Fpdf;
class ScobdaNominationAdminUtilitiesController extends Controller implements NominationAdminUtilitiesController
{
public function __invoke(string $action)
{
match ($action) {
'school_shirt_distribution_report' => $this->printShirtDistributionLists(),
default => $this->invalidAction(),
};
}
private function printShirtDistributionLists()
{
$output = '';
$schools = School::with('nominations.student')->orderBy('name')->get();
$pdf = new Fpdf('P', 'in', 'letter');
foreach ($schools as $school) {
if ($school->nominations->count() < 1) {
continue;
}
$pdf->AddPage();
$pdf->SetFont('Arial', 'B', 16);
$pdf->Cell(0, .3, $school->name, 1, 1, 'L');
$director_text = 'Directors: ';
$first_director = true;
foreach ($school->users as $user) {
if (! $first_director) {
$director_text .= ', ';
}
$director_text .= $user->full_name();
$first_director = false;
}
$pdf->SetFont('Arial', 'B', 12);
$pdf->MultiCell(0, .3, $director_text, 0, 'L', 0);
$pdf->SetFont('Arial', '', 10);
$nominations = $school->nominations;
$nominations = $nominations->sortBy(function ($entry) {
return $entry->student->full_name(true);
});
foreach ($nominations as $nomination) {
$accepted = $nomination->data['accepted'] ?? false;
if (! $accepted) {
continue;
}
$text = $nomination->student->full_name().' - ';
if ($nomination->student->optional_data && array_key_exists('shirt_size',
$nomination->student->optional_data)) {
$text .= $nomination->student->optional_data['shirt_size'];
} else {
$text .= 'No size provided';
}
$pdf->Cell(0, .25, $text, 0, 1, 'L');
}
}
$pdf->Output('D', 'ShirtRosters.pdf');
}
}

View File

@ -1,13 +0,0 @@
<?php
namespace App\Http\Controllers\NominationEnsembles;
use App\Http\Controllers\Controller;
class ScobdaNominationExportController extends Controller implements NominationExportController
{
public function __invoke()
{
}
}

Some files were not shown because too many files have changed in this diff Show More