Compare commits

..

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

436 changed files with 3648 additions and 18442 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,11 +1,11 @@
<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>
</component> </component>

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

@ -4,6 +4,7 @@ namespace App\Actions\Tabulation;
use App\Models\Audition; use App\Models\Audition;
use App\Models\Entry; use App\Models\Entry;
use Debugbar;
class CalculateAuditionScores class CalculateAuditionScores
{ {
@ -22,6 +23,7 @@ class CalculateAuditionScores
->with('audition.scoringGuide.subscores') ->with('audition.scoringGuide.subscores')
->get(); ->get();
foreach ($pending_entries as $entry) { foreach ($pending_entries as $entry) {
Debugbar::debug('Calculating scores for entry: '.$entry->id);
$totaler->__invoke($entry); $totaler->__invoke($entry);
} }
} }

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,21 +4,24 @@
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\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);
@ -35,18 +38,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

@ -9,8 +9,6 @@ use App\Models\EntryTotalScore;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use function auditionLog;
class EnterNoShow class EnterNoShow
{ {
/** /**
@ -53,13 +51,6 @@ class EnterNoShow
$msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').'; $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; 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,7 +6,6 @@
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\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
@ -34,17 +33,17 @@ class EnterScore
$scores = collect($scores); $scores = collect($scores);
// Basic Validity Checks // Basic Validity Checks
if (! User::where('id', $user->id)->exists()) { if (! $user->exists()) {
throw new AuditionAdminException('User does not exist'); throw new ScoreEntryException('User does not exist');
} }
if (! Entry::where('id', $entry->id)->exists()) { if (! $entry->exists()) {
throw new AuditionAdminException('Entry does not exist'); throw new ScoreEntryException('Entry does not exist');
} }
if ($entry->audition->hasFlag('seats_published')) { if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot score an entry in an audition where seats are published'); throw new ScoreEntryException('Cannot score an entry in an audition with published seats');
} }
if ($entry->audition->hasFlag('advancement_published')) { if ($entry->audition->hasFlag('advancement_published')) {
throw new AuditionAdminException('Cannot score an entry in an audition where advancement is published'); throw new ScoreEntryException('Cannot score an entry in an audition with published advancement');
} }
// Check that the specified user is assigned to judge this entry // Check that the specified user is assigned to judge this entry
@ -52,20 +51,20 @@ class EnterScore
->where('room_id', $entry->audition->room_id) ->where('room_id', $entry->audition->room_id)
->where('user_id', $user->id)->exists(); ->where('user_id', $user->id)->exists();
if (! $check) { if (! $check) {
throw new AuditionAdminException('This judge is not assigned to judge this entry'); throw new ScoreEntryException('This judge is not assigned to judge this entry');
} }
// Check if a score already exists // Check if a score already exists
if (! $scoreSheet) { if (! $scoreSheet) {
if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) { 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'); throw new ScoreEntryException('That judge has already entered scores for that entry');
} }
} else { } else {
if ($scoreSheet->user_id !== $user->id) { if ($scoreSheet->user_id !== $user->id) {
throw new AuditionAdminException('Existing score sheet is from a different judge'); throw new ScoreEntryException('Existing score sheet is from a different judge');
} }
if ($scoreSheet->entry_id !== $entry->id) { if ($scoreSheet->entry_id !== $entry->id) {
throw new AuditionAdminException('Existing score sheet is for a different entry'); throw new ScoreEntryException('Existing score sheet is for a different entry');
} }
} }
@ -77,17 +76,16 @@ class EnterScore
$advancementTotal = 0; $advancementTotal = 0;
$advancementMaxPossible = 0; $advancementMaxPossible = 0;
if ($scores->count() !== $subscoresRequired->count()) { if ($scores->count() !== $subscoresRequired->count()) {
throw new AuditionAdminException('Invalid number of scores'); throw new ScoreEntryException('Invalid number of scores');
} }
foreach ($subscoresRequired as $subscore) { foreach ($subscoresRequired as $subscore) {
// check that there is an element in the $scores collection with the key = $subscore->id // check that there is an element in the $scores collection with the key = $subscore->id
if (! $scores->keys()->contains($subscore->id)) { if (! $scores->keys()->contains($subscore->id)) {
throw new AuditionAdminException('Invalid Score Submission'); throw new ScoreEntryException('Invalid Score Submission');
} }
if ($scores[$subscore->id] > $subscore->max_score) { if ($scores[$subscore->id] > $subscore->max_score) {
throw new AuditionAdminException('Supplied subscore exceeds maximum allowed'); throw new ScoreEntryException('Supplied subscore exceeds maximum allowed');
} }
// Add subscore to the storage array // Add subscore to the storage array

View File

@ -4,9 +4,6 @@ namespace App\Actions\Tabulation;
use App\Models\Entry; use App\Models\Entry;
/**
* @codeCoverageIgnore
*/
class ForceRecalculateTotalScores class ForceRecalculateTotalScores
{ {
public function __invoke(): void public function __invoke(): void

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

@ -24,27 +24,23 @@ class RankAuditionEntries
* *
* @throws AuditionAdminException * @throws AuditionAdminException
*/ */
public function __invoke(Audition $audition, string $rank_type, bool $pullDeclinedEntries = true): Collection|Entry public function __invoke(Audition $audition, string $rank_type)
{ {
if ($rank_type !== 'seating' && $rank_type !== 'advancement') { if ($rank_type !== 'seating' && $rank_type !== 'advancement') {
throw new AuditionAdminException('Invalid rank type (must be seating or advancement)'); throw new AuditionAdminException('Invalid rank type: '.$rank_type.' (must be seating or advancement)');
} }
$cache_duration = 15; $cache_duration = 15;
if ($rank_type === 'seating') { if ($rank_type === 'seating') {
return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition, $pullDeclinedEntries) { return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition) {
return $this->get_seating_ranks($audition, $pullDeclinedEntries); return $this->get_seating_ranks($audition);
}); });
} }
return cache()->remember('rank_advancement_'.$audition->id, $cache_duration, function () use ($audition) {
return $this->get_advancement_ranks($audition);
});
} }
private function get_seating_ranks(Audition $audition, bool $pullDeclinedEntries = true): Collection|Entry private function get_seating_ranks(Audition $audition): Collection
{ {
if ($audition->bonusScore()->count() > 0) { if ($audition->bonusScore()->count() > 0) {
$totalColumn = 'seating_total_with_bonus'; $totalColumn = 'seating_total_with_bonus';
@ -53,7 +49,6 @@ class RankAuditionEntries
} }
$sortedEntries = $audition->entries() $sortedEntries = $audition->entries()
->where('for_seating', true)
->whereHas('totalScore') ->whereHas('totalScore')
->with('totalScore') ->with('totalScore')
->with('student.school') ->with('student.school')
@ -75,7 +70,7 @@ class RankAuditionEntries
$rankOn = 1; $rankOn = 1;
foreach ($sortedEntries as $entry) { foreach ($sortedEntries as $entry) {
if ($entry->hasFlag('declined') && $pullDeclinedEntries) { if ($entry->hasFlag('declined')) {
$entry->seatingRank = 'declined'; $entry->seatingRank = 'declined';
} else { } else {
$entry->seatingRank = $rankOn; $entry->seatingRank = $rankOn;
@ -86,9 +81,9 @@ class RankAuditionEntries
return $sortedEntries; return $sortedEntries;
} }
private function get_advancement_ranks(Audition $audition): Collection|Entry private function get_advancement_ranks(Audition $audition): Collection
{ {
$sortedEntries = $audition->entries() return $audition->entries()
->whereHas('totalScore') ->whereHas('totalScore')
->with('totalScore') ->with('totalScore')
->with('student.school') ->with('student.school')
@ -107,12 +102,5 @@ class RankAuditionEntries
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC') ->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC')
->select('entries.*') ->select('entries.*')
->get(); ->get();
$n = 1;
foreach ($sortedEntries as $entry) {
$entry->advancementRank = $n;
$n++;
}
return $sortedEntries;
} }
} }

View File

@ -33,19 +33,16 @@ class TotalEntryScores
// deal with seating scores // deal with seating scores
// TODO: Consider a rewrite to pull the scoreSheets from the entry model so they may be preloaded // 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(); $scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('seating_total', 'desc')->get();
// bail out if there are no score sheets
// bail out if there are not enough score sheets if ($scoreSheets->count() == 0) {
$assignedJudges = $entry->audition->judges()->count();
if ($scoreSheets->count() == 0 || $scoreSheets->count() < $assignedJudges) {
return; return;
} }
if (auditionSetting('olympic_scoring' && $scoreSheets->count() > 2)) {
if (auditionSetting('olympic_scoring') && $scoreSheets->count() > 2) {
// under olympic scoring, drop the first and last element // under olympic scoring, drop the first and last element
$scoreSheets->shift(); $scoreSheets->shift();
$scoreSheets->pop(); $scoreSheets->pop();
} }
$newTotaledScore->seating_total = round($scoreSheets->avg('seating_total'), 6); $newTotaledScore->seating_total = $scoreSheets->avg('seating_total');
$seatingSubscores = $requiredSubscores $seatingSubscores = $requiredSubscores
->filter(fn ($subscore) => $subscore->for_seating == true) ->filter(fn ($subscore) => $subscore->for_seating == true)
->sortBy('tiebreak_order'); ->sortBy('tiebreak_order');
@ -55,18 +52,18 @@ class TotalEntryScores
foreach ($scoreSheets as $scoreSheet) { foreach ($scoreSheets as $scoreSheet) {
$runningTotal += $scoreSheet->subscores[$subscore->id]['score']; $runningTotal += $scoreSheet->subscores[$subscore->id]['score'];
} }
$total_seating_subscores[] = round($runningTotal / $scoreSheets->count(), 4); $total_seating_subscores[] = $runningTotal / $scoreSheets->count();
} }
$newTotaledScore->seating_subscore_totals = $total_seating_subscores; $newTotaledScore->seating_subscore_totals = $total_seating_subscores;
// deal with advancement scores // deal with advancement scores
$scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('advancement_total', 'desc')->get(); $scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('advancement_total', 'desc')->get();
if (auditionSetting('olympic_scoring') && $scoreSheets->count() > 2) { if (auditionSetting('olympic_scoring' && $scoreSheets->count() > 2)) {
// under olympic scoring, drop the first and last element // under olympic scoring, drop the first and last element
$scoreSheets->shift(); $scoreSheets->shift();
$scoreSheets->pop(); $scoreSheets->pop();
} }
$newTotaledScore->advancement_total = round($scoreSheets->avg('advancement_total'), 6); $newTotaledScore->advancement_total = $scoreSheets->avg('advancement_total');
$advancement_subscores = $requiredSubscores $advancement_subscores = $requiredSubscores
->filter(fn ($subscore) => $subscore->for_advance == true) ->filter(fn ($subscore) => $subscore->for_advance == true)
->sortBy('tiebreak_order'); ->sortBy('tiebreak_order');
@ -76,7 +73,7 @@ class TotalEntryScores
foreach ($scoreSheets as $scoreSheet) { foreach ($scoreSheets as $scoreSheet) {
$runningTotal += $scoreSheet->subscores[$subscore->id]['score']; $runningTotal += $scoreSheet->subscores[$subscore->id]['score'];
} }
$total_advancement_subscores[] = round($runningTotal / $scoreSheets->count(), 4); $total_advancement_subscores[] = $runningTotal / $scoreSheets->count();
} }
$newTotaledScore->advancement_subscore_totals = $total_advancement_subscores; $newTotaledScore->advancement_subscore_totals = $total_advancement_subscores;

View File

@ -9,6 +9,10 @@ use Carbon\Carbon;
class RecordHistoricalSeats class RecordHistoricalSeats
{ {
public function __construct()
{
}
public function __invoke(): void public function __invoke(): void
{ {
$this->saveSeats(); $this->saveSeats();

View File

@ -6,26 +6,24 @@ use App\Exceptions\AuditionAdminException;
use App\Models\AuditionFlag; use App\Models\AuditionFlag;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
use App\Models\BonusScore; use App\Models\BonusScore;
use App\Models\Doubler; use App\Models\CalculatedScore;
use App\Models\DoublerRequest; use App\Models\DoublerRequest;
use App\Models\EntryFlag; use App\Models\EntryFlag;
use App\Models\EntryTotalScore;
use App\Models\JudgeAdvancementVote; use App\Models\JudgeAdvancementVote;
use App\Models\NominationEnsembleEntry; use App\Models\NominationEnsembleEntry;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
use App\Models\Seat; use App\Models\Seat;
use App\Models\Student; use App\Models\Student;
use App\Models\UserFlag;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use function auth; use function auth;
/**
* @codeCoverageIgnore
*/
// TODO: figure out how to test YearEndCleanup
class YearEndCleanup class YearEndCleanup
{ {
public function __construct()
{
}
public function __invoke(?array $options = []): void public function __invoke(?array $options = []): void
{ {
$this->cleanup($options); $this->cleanup($options);
@ -51,9 +49,8 @@ class YearEndCleanup
AuditLogEntry::truncate(); AuditLogEntry::truncate();
AuditionFlag::truncate(); AuditionFlag::truncate();
BonusScore::truncate(); BonusScore::truncate();
EntryTotalScore::truncate(); CalculatedScore::truncate();
DoublerRequest::truncate(); DoublerRequest::truncate();
Doubler::truncate();
EntryFlag::truncate(); EntryFlag::truncate();
ScoreSheet::truncate(); ScoreSheet::truncate();
Seat::truncate(); Seat::truncate();
@ -65,20 +62,19 @@ class YearEndCleanup
if (is_array($options)) { if (is_array($options)) {
if (in_array('deleteRooms', $options)) { if (in_array('deleteRooms', $options)) {
DB::table('auditions')->update(['room_id' => 0]); DB::table('auditions')->update(['room_id' => null]);
DB::table('auditions')->update(['order_in_room' => '0']); DB::table('auditions')->update(['order_in_room' => '0']);
DB::table('room_user')->truncate(); DB::table('room_user')->truncate();
DB::table('rooms')->where('id', '>', 0)->delete(); DB::table('rooms')->delete();
} }
if (in_array('removeAuditionsFromRoom', $options)) { if (in_array('removeAuditionsFromRoom', $options)) {
DB::table('auditions')->update(['room_id' => 0]); DB::table('auditions')->update(['room_id' => null]);
DB::table('auditions')->update(['order_in_room' => '0']); DB::table('auditions')->update(['order_in_room' => '0']);
} }
if (in_array('unassignJudges', $options)) { if (in_array('unassignJudges', $options)) {
DB::table('room_user')->truncate(); DB::table('room_user')->truncate();
UserFlag::where('flag', 'monitor')->delete();
} }
} }

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

@ -5,17 +5,14 @@ namespace App\Console\Commands;
use App\Actions\Tabulation\ForceRecalculateTotalScores; use App\Actions\Tabulation\ForceRecalculateTotalScores;
use Illuminate\Console\Command; use Illuminate\Console\Command;
/** class RecalculateScores extends Command
* @codeCoverageIgnore
*/
class RecalculateTotalScores extends Command
{ {
/** /**
* The name and signature of the console command. * The name and signature of the console command.
* *
* @var string * @var string
*/ */
protected $signature = 'audition:recalculate-total-scores'; protected $signature = 'audition:recalculate-scores';
/** /**
* The console command description. * The console command description.

View File

@ -2,13 +2,10 @@
namespace App\Console\Commands; namespace App\Console\Commands;
use App\Actions\Tabulation\DoublerSync; use App\Models\Doubler;
use App\Models\Event; use App\Models\Event;
use Illuminate\Console\Command; use Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class SyncDoublers extends Command class SyncDoublers extends Command
{ {
/** /**
@ -30,13 +27,12 @@ class SyncDoublers extends Command
*/ */
public function handle() public function handle()
{ {
$syncer = app(DoublerSync::class);
if ($eventId = $this->argument('event')) { if ($eventId = $this->argument('event')) {
$event = Event::findOrFail($eventId); $event = Event::findOrFail($eventId);
$syncer($event); Doubler::syncForEvent($event);
$this->info("Synced doublers for event {$event->name}"); $this->info("Synced doublers for event {$event->name}");
} else { } else {
$syncer(); Doubler::syncDoublers();
$this->info('Synced doublers for all events'); $this->info('Synced doublers for all events');
} }
} }

View File

@ -10,78 +10,43 @@ use Illuminate\Console\Command;
class fictionalize extends Command class fictionalize extends Command
{ {
protected $signature = 'audition:fictionalize /**
{--students : Fictionalize student names} * The name and signature of the console command.
{--schools : Fictionalize school names} *
{--users : Fictionalize user data} * @var string
{--all : Fictionalize all data types}'; */
protected $signature = 'audition:fictionalize';
protected $description = 'Replace real names with fictional data for specified entity types'; /**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle() public function handle()
{ {
$faker = Factory::create(); $faker = Factory::create();
foreach (Student::all() as $student) {
// If no options are specified or --all is used, process everything $student->first_name = $faker->firstName();
$processAll = $this->option('all') || $student->last_name = $faker->lastName();
(! $this->option('students') && ! $this->option('schools') && ! $this->option('users')); $student->save();
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')) { foreach (School::all() as $school) {
$this->info('Fictionalizing schools...'); $school->name = $faker->city().' High School';
$bar = $this->output->createProgressBar(School::count()); $school->save();
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')) { foreach (User::where('email', '!=', 'matt@mattyoung.us')->get() as $user) {
$this->info('Fictionalizing users...'); $user->email = $faker->email();
$bar = $this->output->createProgressBar(User::where('email', '!=', 'matt@mattyoung.us')->count()); $user->first_name = $faker->firstName();
$user->last_name = $faker->lastName();
User::where('email', '!=', 'matt@mattyoung.us') $user->cell_phone = $faker->phoneNumber();
->chunk(100, function ($users) use ($faker, $bar) { $user->save();
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
@ -27,15 +25,11 @@ 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
@ -66,6 +60,6 @@ class PrintCards extends Controller
} }
$cards = $cards->sortBy($sorts); $cards = $cards->sortBy($sorts);
$printer->print($cards); $printer->print($cards);
// return view('admin.print_cards.print', compact('cards')); //return view('admin.print_cards.print', compact('cards'));
} }
} }

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

@ -8,19 +8,18 @@ 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) {
abort(403);
}
$rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get(); $rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get();
// Check if room id 0 exists, if not, create it and assign all unassigned auditions to it
if (! $rooms->contains('id', 0)) { if (! $rooms->contains('id', 0)) {
$unassignedRoom = Room::create([ $unassignedRoom = Room::create([
'id' => 0, 'id' => 0,
@ -30,7 +29,7 @@ class RoomController extends Controller
$unassignedRoom->id = 0; $unassignedRoom->id = 0;
$unassignedRoom->save(); $unassignedRoom->save();
$auditionsToUpdate = Audition::whereNull('room_id')->get(); $auditionsToUpdate = Audition::where('room_id', null)->get();
foreach ($auditionsToUpdate as $audition) { foreach ($auditionsToUpdate as $audition) {
$audition->room_id = 0; $audition->room_id = 0;
$audition->save(); $audition->save();
@ -54,6 +53,9 @@ class RoomController extends Controller
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 +69,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 +105,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,6 +122,10 @@ 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');

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;
});
// Check if doubler status can change
foreach ($event_entries[$event->id] as $entry) {
$entry->doubler_decision_frozen = $this->isDoublerStatusFrozen($entry, $event_entries[$event->id]);
}
}
return view('admin.students.edit', return view('admin.students.edit',
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries', 'logEntries')); compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries'));
} }
public function update(StudentStoreRequest $request, Student $student) 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(),
} else { 'message' => $message,
$user->removeFlag('head_director'); '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) {
// Deal with privileges $messageStart = $user->is_tab ? 'Granted tabulation privileges to ' : 'Revoked tabulation privileges from ';
if ($request->has('is_admin')) { AuditLogEntry::create([
$privilegesUpdater($user, 'grant', 'admin'); 'user' => auth()->user()->email,
} else { 'ip_address' => request()->ip(),
$privilegesUpdater($user, 'revoke', 'admin'); '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 ($request->has('is_tab')) { if ($validData['is_head']) {
$privilegesUpdater($user, 'grant', 'tab'); $headSetter->setHeadDirector($user);
} else { } else {
$privilegesUpdater($user, 'revoke', 'tab'); $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);
}
} }
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

@ -16,7 +16,7 @@ class YearEndResetController extends Controller
public function execute() public function execute()
{ {
$cleanUpProcedure = app(YearEndCleanup::class); $cleanUpProcedure = new YearEndCleanup;
$options = request()->options; $options = request()->options;
$cleanUpProcedure($options); $cleanUpProcedure($options);
auditionLog('Executed year end reset.', []); auditionLog('Executed year end reset.', []);

View File

@ -32,7 +32,7 @@ class DashboardController extends Controller
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)
{
foreach ($request->getDoublerRequests() as $thisRequest) {
if (! $thisRequest['request']) {
DoublerRequest::where('event_id', $thisRequest['event_id'])
->where('student_id', $thisRequest['student_id'])->delete();
continue;
}
DoublerRequest::upsert([
'event_id' => $thisRequest['event_id'],
'student_id' => $thisRequest['student_id'],
'request' => $thisRequest['request'],
],
uniqueBy: ['event_id', 'student_id'],
update: ['request']
);
} }
return to_route('doubler_request.index')->with('success', 'Recorded doubler requests'); return view('doubler_request.index', compact('events', 'doublers', 'students', 'existingRequests'));
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function makeRequest()
{
foreach (request()->get('doubler_requests') as $event_id => $requests) {
if (! Event::find($event_id)->exists()) {
return to_route('doubler_request.index')->with('error', 'Invalid event id specified');
}
$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;
}
DoublerRequest::upsert([
'event_id' => $event_id,
'student_id' => $student_id,
'request' => $request,
],
uniqueBy: ['event_id', 'student_id'],
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');
} }
} }

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,25 +31,19 @@ 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');
return view('judging.index', compact('rooms', 'bonusScoresToJudge')); return view('judging.index', compact('rooms', 'bonusScoresToJudge'));
} }
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';
$auditions = Audition::orderBy('score_order')->with('flags')->get(); return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
$audition = null;
return view('monitor.index', compact('audition', 'auditions'));
} }
public function auditionStatus(Audition $audition) public function flagForm()
{ {
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
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 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');
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'); $entry->removeFlag('no_show');
return redirect()->back()->with('success', 'No Show Flag Cleared'); return true;
} }
$entry->addFlag('no_show');
return redirect()->back()->with('success', 'No Show Entered'); return false;
} }
} }

View File

@ -9,10 +9,6 @@ use Illuminate\Http\Request;
use function auditionSetting; use function auditionSetting;
/**
* @codeCoverageIgnore
* TODO: Figure out testing for printing
*/
class PdfInvoiceController extends Controller class PdfInvoiceController extends Controller
{ {
protected $pdf; protected $pdf;

View File

@ -6,8 +6,6 @@ use App\Actions\Tabulation\RankAuditionEntries;
use App\Models\Audition; use App\Models\Audition;
use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\App;
/** @codeCoverageIgnore */
// TODO: Rewrite Recap
class RecapController extends Controller class RecapController extends Controller
{ {
public function selectAudition() public function selectAudition()

View File

@ -8,7 +8,6 @@ use App\Models\Ensemble;
use App\Models\Entry; use App\Models\Entry;
use App\Models\Seat; use App\Models\Seat;
use App\Services\AuditionService; use App\Services\AuditionService;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\View; use Illuminate\Support\Facades\View;
@ -29,11 +28,9 @@ class ResultsPage extends Controller
*/ */
public function __invoke(Request $request) public function __invoke(Request $request)
{ {
Model::preventLazyLoading(false);
$cacheKey = 'publicResultsPage'; $cacheKey = 'publicResultsPage';
if (Cache::has($cacheKey)) { if (Cache::has($cacheKey)) {
/** @codeCoverageIgnore */
return response(Cache::get($cacheKey)); return response(Cache::get($cacheKey));
} }
@ -94,4 +91,9 @@ class ResultsPage extends Controller
return response($content); return response($content);
} }
private function generateResultsPage()
{
}
} }

View File

@ -2,48 +2,93 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Schools\AddSchoolEmailDomain;
use App\Actions\Schools\AssignUserToSchool;
use App\Actions\Schools\CreateSchool;
use App\Actions\Schools\SetHeadDirector; use App\Actions\Schools\SetHeadDirector;
use App\Http\Requests\SchoolStoreRequest; use App\Exceptions\AuditionAdminException;
use App\Mail\NewUserPassword; use App\Mail\NewUserPassword;
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 Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use function abort; use function abort;
use function auditionLog;
use function redirect; use function redirect;
use function request; use function request;
class SchoolController extends Controller class SchoolController extends Controller
{ {
public function store(SchoolStoreRequest $request, SetHeadDirector $headSetter): RedirectResponse public function store(Request $request, SetHeadDirector $headSetter): RedirectResponse
{ {
$creator = app(CreateSchool::class); if ($request->user()->cannot('create', School::class)) {
abort(403);
}
request()->validate([
'name' => ['required', 'min:3', 'max:30'],
'address' => ['required'],
'city' => ['required'],
'state' => ['required', 'min:2', 'max:2'],
'zip' => ['required', 'min:5', 'max:10'],
]);
$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]],
]);
$assigner = app(AssignUserToSchool::class); if (! Auth::user()->school) {
$assigner(auth()->user(), $school); Auth::user()->update([
'school_id' => $school->id,
]);
$message = 'Set user '.auth()->user()->full_name().' ('.auth()->user()->email.') as a director at '.$school->name.'(#'.$school->id.')';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'users' => [auth()->user()->id],
'schools' => [$school->id],
],
]);
SchoolEmailDomain::create([
'school_id' => $school->id,
'domain' => Auth::user()->emailDomain(),
]);
$message = 'Added '.auth()->user()->emailDomain().' as an email domain for '.$school->name.' (#'.$school->id.')';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => [
'schools' => [$school->id],
],
]);
auth()->user()->refresh();
try {
$headSetter->setHeadDirector(auth()->user());
} catch (AuditionAdminException $e) {
redirect(route('schools.show', $school))->with('error', 'Could not set as head director');
}
auth()->user()->refresh(); }
$headSetter->setHeadDirector(auth()->user()); return redirect('/schools/'.$school->id);
return redirect(route('schools.show', $school));
} }
public function show(Request $request, School $school) public function show(Request $request, School $school)
@ -73,14 +118,25 @@ class SchoolController extends Controller
return view('schools.edit', ['school' => $school]); return view('schools.edit', ['school' => $school]);
} }
public function update(SchoolStoreRequest $request, School $school) public function update(Request $request, School $school)
{ {
if ($request->user()->cannot('update', $school)) {
abort(403);
}
request()->validate([
'name' => ['required', 'min:3', 'max:30'],
'address' => ['required'],
'city' => ['required'],
'state' => ['required', 'min:2', 'max:2'],
'zip' => ['required', 'min:5', 'max:10'],
]);
$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; $message = 'Modified school #'.$school->id.' - '.$school->name.' with address <br>'.$school->address.'<br>'.$school->city.', '.$school->state.' '.$school->zip;
AuditLogEntry::create([ AuditLogEntry::create([
@ -93,26 +149,46 @@ class SchoolController extends Controller
return redirect()->route('schools.show', $school->id)->with('success', 'School details updated'); return redirect()->route('schools.show', $school->id)->with('success', 'School details updated');
} }
public function my_school()
{
if (Auth::user()->school) {
return redirect('/schools/'.Auth::user()->school->id);
}
return redirect('/schools/create');
}
public function addDirector(School $school) public function addDirector(School $school)
{ {
if (auth()->user()->school_id !== $school->id) { if (auth()->user()->school_id !== $school->id) {
abort(403); return redirect()->back()->with('error', 'No adding directors to another school');
} }
if (! auth()->user()->hasFlag('head_director')) { if (! auth()->user()->hasFlag('head_director')) {
abort(403); return redirect()->back()->with('error', 'Only the head director can add directors to a school');
} }
$validData = request()->validate([
$userCreator = app(CreateNewUser::class); 'first_name' => ['required'],
$randomPassword = Str::random(12); 'last_name' => ['required'],
$data = request()->all(); 'email' => ['required', 'email', 'unique:users'],
$data['password'] = $randomPassword; 'cell_phone' => ['required'],
$data['password_confirmation'] = $randomPassword; 'judging_preference' => ['required'],
$newDirector = $userCreator->create($data);
$newDirector->update([
'school_id' => $school->id,
]); ]);
// Generate a random password
Mail::to($newDirector->email)->send(new NewUserPassword($newDirector, $randomPassword)); $randomPassword = Str::random(12);
$newUser = User::create([
'first_name' => $validData['first_name'],
'last_name' => $validData['last_name'],
'email' => $validData['email'],
'cell_phone' => $validData['cell_phone'],
'judging_preference' => $validData['judging_preference'],
'password' => Hash::make($randomPassword),
'school_id' => auth()->user()->school_id,
]);
$logMessage = 'Created user '.$newUser->full_name().' - '.$newUser->email.' as a director at '.$newUser->school->name;
$logAffected = ['users' => [$newUser->id], 'schools' => [$newUser->school_id]];
auditionLog($logMessage, $logAffected);
Mail::to($newUser->email)->send(new NewUserPassword($newUser, $randomPassword));
return redirect()->back()->with('success', 'Director added'); return redirect()->back()->with('success', 'Director added');
} }
@ -120,49 +196,62 @@ class SchoolController extends Controller
public function setHeadDirector(School $school, User $user, SetHeadDirector $headSetter) public function setHeadDirector(School $school, User $user, SetHeadDirector $headSetter)
{ {
if (auth()->user()->school_id !== $school->id) { if (auth()->user()->school_id !== $school->id) {
abort(403); return redirect()->back()->with('error', 'No setting the head director for another school');
} }
if (! auth()->user()->hasFlag('head_director')) { if (! auth()->user()->hasFlag('head_director')) {
abort(403); return redirect()->back()->with('error', 'Only the head director can name a new head director');
} }
if ($school->id !== $user->school_id) { if ($school->id !== $user->school_id) {
abort(403); return redirect()->back()->with('error', 'The proposed head director must be at your school');
}
try {
$headSetter->setHeadDirector($user);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
} }
$headSetter->setHeadDirector($user); return redirect()->back()->with('success', 'New head director set');
return redirect()->route('schools.show', $school)->with('success', 'New head director set');
} }
public function addDomain(School $school) public function addDomain(School $school)
{ {
if (auth()->user()->school_id !== $school->id) { if (auth()->user()->school_id !== $school->id) {
abort(403); return redirect()->back()->with('error', 'No adding domains for another school');
} }
if (! auth()->user()->hasFlag('head_director')) { if (! auth()->user()->hasFlag('head_director')) {
abort(403); return redirect()->back()->with('error', 'Only the head director can add domains');
} }
$verifiedData = request()->validate([ $verifiedData = request()->validate([
'domain' => ['required'], 'domain' => ['required'],
]); ]);
app(AddSchoolEmailDomain::class)->addDomain($school, $verifiedData['domain']); try {
SchoolEmailDomain::create([
'school_id' => $school->id,
'domain' => $verifiedData['domain'],
]);
} catch (UniqueConstraintViolationException $e) {
return redirect()->back()->with('error', 'That domain is already associated with your school');
}
$logMessage = 'Added domain '.$verifiedData['domain'].' to school '.$school->name;
$logAffected = ['schools' => [$school->id]];
auditionLog($logMessage, $logAffected);
return redirect()->route('schools.show', $school)->with('success', 'Domain added'); return redirect()->back()->with('success', 'Domain added');
} }
public function deleteDomain(SchoolEmailDomain $domain) public function deleteDomain(SchoolEmailDomain $domain)
{ {
if (auth()->user()->school_id !== $domain->school_id) { if (auth()->user()->school_id !== $domain->school_id) {
abort(403); return redirect()->back()->with('error', 'No deleting domains for another school');
} }
if (! auth()->user()->hasFlag('head_director')) { if (! auth()->user()->hasFlag('head_director')) {
abort(403); return redirect()->back()->with('error', 'Only the head director can delete domains');
} }
$logMessage = 'Deleted domain '.$domain->domain.' from school '.$domain->school->name;
$logAffected = ['schools' => [$domain->school_id]];
auditionLog($logMessage, $logAffected);
$domain->delete(); $domain->delete();
return redirect() return redirect()->back()->with('success', 'Domain deleted');
->route('schools.show', auth()->user()->school)
->with('success', 'Domain deleted');
} }
} }

View File

@ -2,10 +2,10 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Students\CreateStudent;
use App\Http\Requests\StudentStoreRequest;
use App\Models\Audition; use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Student; use App\Models\Student;
use App\Rules\UniqueFullNameAtSchool;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -31,24 +31,70 @@ class StudentController extends Controller
['students' => $students, 'auditions' => $auditions, 'shirtSizes' => $shirtSizes]); ['students' => $students, 'auditions' => $auditions, 'shirtSizes' => $shirtSizes]);
} }
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
*/ */
public function store(StudentStoreRequest $request) public function store(Request $request)
{ {
$creator = app(CreateStudent::class); if ($request->user()->cannot('create', Student::class)) {
/** @noinspection PhpUnhandledExceptionInspection */ abort(403);
$creator([ }
'first_name' => $request['first_name'], $request->validate([
'last_name' => $request['last_name'], 'first_name' => ['required'],
'grade' => $request['grade'], 'last_name' => [
'optional_data' => $request->optional_data, 'required',
new UniqueFullNameAtSchool(request('first_name'), request('last_name'), Auth::user()->school_id),
],
'grade' => ['required', 'integer'],
'shirt_size' => [
'nullable',
function ($attribute, $value, $fail) {
if (! array_key_exists($value, Student::$shirtSizes)) {
$fail("The selected $attribute is invalid.");
}
},
],
]);
$student = Student::create([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
'school_id' => Auth::user()->school_id,
]);
if (request('shirt_size') !== 'none') {
$student->update(['optional_data->shirt_size' => $request['shirt_size']]);
}
$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],
],
]); ]);
/** @codeCoverageIgnoreEnd */
return redirect('/students')->with('success', 'Student Created'); return redirect('/students')->with('success', 'Student Created');
} }
/**
* Display the specified resource.
*/
public function show(Request $request, Student $student)
{
//
}
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
*/ */
@ -66,17 +112,52 @@ class StudentController extends Controller
/** /**
* Update the specified resource in storage. * Update the specified resource in storage.
*/ */
public function update(StudentStoreRequest $request, Student $student) public function update(Request $request, Student $student)
{ {
if ($request->user()->cannot('update', $student)) { if ($request->user()->cannot('update', $student)) {
abort(403); abort(403);
} }
request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
'shirt_size' => [
'nullable',
function ($attribute, $value, $fail) {
if (! array_key_exists($value, Student::$shirtSizes)) {
$fail("The selected $attribute is invalid.");
}
},
],
]);
if (Student::where('first_name', request('first_name'))
->where('last_name', request('last_name'))
->where('school_id', Auth::user()->school_id)
->where('id', '!=', $student->id)
->exists()) {
return redirect()->route('students.edit', $student)->with('error',
'A student with that name already exists at your 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'),
'optional_data' => $request->optional_data, ]);
$student->update(['optional_data->shirt_size' => $request['shirt_size']]);
$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('/students')->with('success', 'Student updated successfully.'); return redirect('/students')->with('success', 'Student updated successfully.');
@ -90,7 +171,16 @@ class StudentController extends Controller
if ($request->user()->cannot('delete', $student)) { if ($request->user()->cannot('delete', $student)) {
abort(403); abort(403);
} }
$message = 'Deleted 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],
],
]);
$student->delete(); $student->delete();
return redirect(route('students.index')); return redirect(route('students.index'));

View File

@ -2,64 +2,50 @@
namespace App\Http\Controllers\Tabulation; namespace App\Http\Controllers\Tabulation;
use App\Actions\Tabulation\CalculateAuditionScores;
use App\Actions\Tabulation\RankAuditionEntries; use App\Actions\Tabulation\RankAuditionEntries;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Audition; use App\Models\Audition;
use App\Models\EntryFlag; use App\Models\Entry;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use function is_null;
class AdvancementController extends Controller class AdvancementController extends Controller
{ {
protected RankAuditionEntries $ranker;
public function __construct(RankAuditionEntries $ranker)
{
$this->ranker = $ranker;
}
public function status() public function status()
{ {
// Total auditions scores if we haven't done it lately
if (! Cache::has('advancement_status_audition_totaler_throttle')) {
$lock = Cache::lock('advancement_status_audition_totaler_lock');
if ($lock->get()) {
try {
$totaler = app(CalculateAuditionScores::class);
foreach (Audition::forAdvancement()->with('judges')->get() as $audition) {
$totaler($audition);
}
// set throttle
Cache::put('advancement_status_audition_totaler_throttle', true, 15);
} finally {
$lock->release();
}
}
}
$auditions = Audition::forAdvancement() $auditions = Audition::forAdvancement()
->with('flags') ->with('flags')
->withCount([ ->withCount([
'entries' => function ($query) { 'entries' => function ($query) {
$query->where('for_advancement', true); $query->where('for_advancement', 1);
}, },
]) ])
->withCount([ ->withCount([
'unscoredEntries' => function ($query) { 'unscoredEntries' => function ($query) {
$query->where('for_advancement', true); $query->where('for_advancement', 1);
}, },
]) ])
->orderBy('score_order')
->get(); ->get();
$auditionData = []; $auditionData = [];
$auditions->each(function (Audition $audition) use (&$auditionData) { $auditions->each(function ($audition) use (&$auditionData) {
$scoredPercent = ($audition->entries_count > 0) ?
round((($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count) * 100)
: 100;
$auditionData[] = [ $auditionData[] = [
'id' => $audition->id, 'id' => $audition->id,
'name' => $audition->name, 'name' => $audition->name,
'entries_count' => $audition->entries_count, 'entries_count' => $audition->entries_count,
'unscored_entries_count' => $audition->unscored_entries_count, 'unscored_entries_count' => $audition->unscored_entries_count,
'scored_entries_count' => $audition->entries_count - $audition->unscored_entries_count, 'scored_entries_count' => $audition->entries_count - $audition->unscored_entries_count,
'scored_percentage' => $audition->entries_count > 0 ? ((($audition->entries_count - $audition->unscored_entries_count) / $audition->entries_count) * 100) : 0, 'scored_percentage' => $scoredPercent,
'scoring_complete' => $audition->unscored_entries_count === 0, 'scoring_complete' => $audition->unscored_entries_count == 0,
'published' => $audition->hasFlag('advancement_published'), 'published' => $audition->hasFlag('advancement_published'),
]; ];
}); });
@ -69,51 +55,29 @@ class AdvancementController extends Controller
public function ranking(Request $request, Audition $audition) public function ranking(Request $request, Audition $audition)
{ {
$ranker = app(RankAuditionEntries::class); $entries = $this->ranker->rank('advancement', $audition);
$entries = $ranker($audition, 'advancement'); $entries->load('advancementVotes');
$entries->load(['advancementVotes', 'totalScore', 'student.school']);
$unscoredEntries = $audition->entries()->where('for_advancement', true)->orderBy('draw_number')->get()->filter(function ($entry) { $scoringComplete = $entries->every(function ($entry) {
return ! $entry->totalScore && ! $entry->hasFlag('no_show'); return $entry->score_totals[0] >= 0 || $entry->hasFlag('no_show');
}); });
$noShowEntries = $audition->entries()->orderBy('draw_number')->get()->filter(function ($entry) { return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete'));
return $entry->hasFlag('no_show');
});
$scoringComplete = $audition->entries->where('for_advancement', true)->every(function ($entry) {
return $entry->totalScore || $entry->hasFlag('no_show');
});
return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete', 'unscoredEntries', 'noShowEntries'));
} }
public function setAuditionPassers(Request $request, Audition $audition) public function setAuditionPassers(Request $request, Audition $audition)
{ {
$passingEntries = $request->input('pass'); $passingEntries = $request->input('pass');
if (is_null($passingEntries) || count($passingEntries) < 1) {
return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('error',
'Cannot publish advancement if no entries advance');
}
$audition->addFlag('advancement_published'); $audition->addFlag('advancement_published');
if (! is_null($passingEntries)) { if (! is_null($passingEntries)) {
$passEntries = collect(array_keys($passingEntries)); $passingEntries = array_keys($passingEntries);
EntryFlag::insert( $entries = Entry::whereIn('id', $passingEntries)->get();
$passEntries foreach ($entries as $entry) {
->map(fn ($entryId) => [ $entry->addFlag('will_advance');
'entry_id' => $entryId, }
'flag_name' => 'will_advance',
'created_at' => now(),
'updated_at' => now(),
])->toArray()
);
} }
Cache::forget('audition'.$audition->id.'advancement'); Cache::forget('audition'.$audition->id.'advancement');
Cache::forget('publicResultsPage'); Cache::forget('publicResultsPage');
Cache::forget('rank_advancement_'.$audition->id);
return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success', return redirect()->route('advancement.ranking', ['audition' => $audition->id])->with('success',
'Passers have been set successfully'); 'Passers have been set successfully');
@ -122,10 +86,9 @@ class AdvancementController extends Controller
public function clearAuditionPassers(Request $request, Audition $audition) public function clearAuditionPassers(Request $request, Audition $audition)
{ {
$audition->removeFlag('advancement_published'); $audition->removeFlag('advancement_published');
$audition->entries foreach ($audition->entries as $entry) {
->filter(fn ($entry) => $entry->hasFlag('will_advance')) $entry->removeFlag('will_advance');
->each(fn ($entry) => $entry->removeFlag('will_advance')); }
Cache::forget('audition'.$audition->id.'advancement'); Cache::forget('audition'.$audition->id.'advancement');
Cache::forget('publicResultsPage'); Cache::forget('publicResultsPage');

View File

@ -1,17 +1,14 @@
<?php <?php
/** @noinspection PhpUnhandledExceptionInspection */
namespace App\Http\Controllers\Tabulation; namespace App\Http\Controllers\Tabulation;
use App\Actions\Tabulation\EnterBonusScore; use App\Actions\Tabulation\EnterBonusScore;
use App\Actions\Tabulation\GetBonusScoreRelatedEntries; use App\Actions\Tabulation\GetBonusScoreRelatedEntries;
use App\Exceptions\AuditionAdminException; use App\Exceptions\ScoreEntryException;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\BonusScore; use App\Models\BonusScore;
use App\Models\Entry; use App\Models\Entry;
use App\Models\User; use App\Models\User;
use Exception;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use function request; use function request;
@ -76,25 +73,26 @@ class BonusScoreController extends Controller
// Set the new score // Set the new score
try { try {
$saveBonusScore($judge, $entry, $validData['score']); $saveBonusScore($judge, $entry, $validData['score']);
} catch (ScoreEntryException $ex) {
} catch (AuditionAdminException $ex) {
DB::rollBack(); DB::rollBack();
return redirect()->route('bonus-scores.entryBonusScoreSheet', return redirect()->route('bonus-scores.entryBonusScoreSheet',
['entry_id' => $entry->id])->with('error', 'Error entering score - '.$ex->getMessage()); ['entry_id' => $entry->id])->with('error', 'Error entering score - '.$ex->getMessage());
} }
} }
DB::commit(); DB::commit();
/* @codeCoverageIgnoreStart */ } catch (\Exception) {
} catch (Exception $ex) {
DB::rollBack(); DB::rollBack();
return redirect()->route('bonus-scores.entryBonusScoreSheet', ['entry_id' => $entry->id])->with('error', 'Error entering score - '.$ex->getMessage()); return redirect()->route('bonus-scores.entryBonusScoreSheet', ['entry_id' => $entry->id])->with('error', 'Error entering score - '.$ex->getMessage());
} }
/* @codeCoverageIgnoreEnd */
return redirect()->route('bonus-scores.entryBonusScoreSheet', ['entry_id' => $entry->id])->with('success', 'New bonus score entered'); return redirect()->route('bonus-scores.entryBonusScoreSheet', ['entry_id' => $entry->id])->with('success', 'New bonus score entered');
} }
public function destroyBonusScore()
{
}
} }

View File

@ -34,11 +34,7 @@ class DoublerDecisionController extends Controller
// $doublerEntry->addFlag('declined'); // $doublerEntry->addFlag('declined');
// } // }
// } // }
try { $this->decider->accept($entry);
$this->decider->accept($entry);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
}
$returnMessage = $entry->student->full_name().' accepted seating in '.$entry->audition->name; $returnMessage = $entry->student->full_name().' accepted seating in '.$entry->audition->name;
$this->clearCache($entry); $this->clearCache($entry);

View File

@ -9,9 +9,6 @@ use Illuminate\Http\Request;
use function to_route; use function to_route;
/**
* Used for tabulation enter noshow menu option
*/
class EntryFlagController extends Controller class EntryFlagController extends Controller
{ {
public function noShowSelect() public function noShowSelect()
@ -33,11 +30,11 @@ class EntryFlagController extends Controller
// If any results are published, get gone // If any results are published, get gone
if ($entry->audition->hasFlag('seats_published')) { if ($entry->audition->hasFlag('seats_published')) {
return to_route('entry-flags.noShowSelect')->with('error', return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show or failed-prelim for an entry in an audition where seats are published'); 'Cannot enter a no-show for an entry in an audition where seats are published');
} }
if ($entry->audition->hasFlag('advancement_published')) { if ($entry->audition->hasFlag('advancement_published')) {
return to_route('entry-flags.noShowSelect')->with('error', return to_route('entry-flags.noShowSelect')->with('error',
'Cannot enter a no-show or failed-prelim for an entry in an audition where advancement is published'); 'Cannot enter a no-show for an entry in an audition where advancement is published');
} }
if ($entry->hasFlag('no_show')) { if ($entry->hasFlag('no_show')) {
@ -46,12 +43,6 @@ class EntryFlagController extends Controller
$submitRouteName = 'entry-flags.undoNoShow'; $submitRouteName = 'entry-flags.undoNoShow';
$cardHeading = 'Undo No-Show'; $cardHeading = 'Undo No-Show';
$method = 'DELETE'; $method = 'DELETE';
} elseif ($entry->hasFlag('failed_prelim')) {
$formId = 'no-show-cancellation-form';
$buttonName = 'Remove Failed Prelim';
$submitRouteName = 'entry-flags.undoNoShow';
$cardHeading = 'Undo Failed-Prelim';
$method = 'DELETE';
} else { } else {
$formId = 'no-show-confirmation-form'; $formId = 'no-show-confirmation-form';
$buttonName = 'Confirm No Show'; $buttonName = 'Confirm No Show';
@ -94,32 +85,21 @@ class EntryFlagController extends Controller
{ {
if ($entry->audition->hasFlag('seats_published')) { if ($entry->audition->hasFlag('seats_published')) {
return to_route('entry-flags.noShowSelect')->with('error', return to_route('entry-flags.noShowSelect')->with('error',
'Cannot undo a no-show or failed-prelim for an entry in an audition where seats are published'); 'Cannot undo a no-show for an entry in an audition where seats are published');
} }
if ($entry->audition->hasFlag('advancement_published')) { if ($entry->audition->hasFlag('advancement_published')) {
return to_route('entry-flags.noShowSelect')->with('error', return to_route('entry-flags.noShowSelect')->with('error',
'Cannot undo a no-show or failed-prelim for an entry in an audition where advancement is published'); 'Cannot undo a no-show for an entry in an audition where advancement is published');
} }
$entry->removeFlag('no_show'); $entry->removeFlag('no_show');
$entry->removeFlag('failed_prelim');
return to_route('entry-flags.noShowSelect')->with('success', return to_route('entry-flags.noShowSelect')->with('success',
$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.') may now be scored.'); 'No Show status has been removed for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').');
} }
public function undoDecline(Entry $entry) public function undoDecline(Entry $entry)
{ {
if ($entry->audition->hasFlag('seats_published')) {
return redirect()->back()
->with('error', 'Cannot undo a decline for an entry in an audition where seats are published');
}
if ($entry->audition->hasFlag('advancement_published')) {
return redirect()->back()
->with('error', 'Cannot undo a no-show or failed-prelim for an entry in an audition where advancement is published');
}
$entry->removeFlag('declined'); $entry->removeFlag('declined');
return redirect()->back()->with('success', 'Decline cleared'); return redirect()->back()->with('success', 'Decline cleared');

View File

@ -2,21 +2,16 @@
namespace App\Http\Controllers\Tabulation; namespace App\Http\Controllers\Tabulation;
use App\Actions\Tabulation\EnterPrelimScore;
use App\Actions\Tabulation\EnterScore; use App\Actions\Tabulation\EnterScore;
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 App\Models\EntryTotalScore; use App\Models\EntryTotalScore;
use App\Models\PrelimScoreSheet;
use App\Models\ScoreSheet; use App\Models\ScoreSheet;
use App\Models\User; use App\Models\User;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
/**
* Provides functionality for entering judge scores from the admin side.
*/
class ScoreController extends Controller class ScoreController extends Controller
{ {
public function chooseEntry() public function chooseEntry()
@ -30,6 +25,7 @@ class ScoreController extends Controller
public function destroyScore(ScoreSheet $score) public function destroyScore(ScoreSheet $score)
{ {
EntryTotalScore::where('entry_id', $score->entry_id)->delete();
if ($score->entry->audition->hasFlag('seats_published')) { if ($score->entry->audition->hasFlag('seats_published')) {
return redirect()->back()->with('error', 'Cannot delete scores for an entry where seats are published'); return redirect()->back()->with('error', 'Cannot delete scores for an entry where seats are published');
} }
@ -37,7 +33,6 @@ class ScoreController extends Controller
return redirect()->back()->with('error', return redirect()->back()->with('error',
'Cannot delete scores for an entry where advancement is published'); 'Cannot delete scores for an entry where advancement is published');
} }
EntryTotalScore::where('entry_id', $score->entry_id)->delete();
$score->delete(); $score->delete();
return redirect()->back()->with('success', 'Score Deleted'); return redirect()->back()->with('success', 'Score Deleted');
@ -46,7 +41,7 @@ class ScoreController extends Controller
public function entryScoreSheet(Request $request) public function entryScoreSheet(Request $request)
{ {
$existing_sheets = []; $existing_sheets = [];
$entry = Entry::with(['student', 'audition.room.judges'])->findOrFail($request->input('entry_id')); $entry = Entry::with(['student', 'audition.room.judges'])->find($request->input('entry_id'));
$publishedCheck = $this->checkIfPublished($entry); $publishedCheck = $this->checkIfPublished($entry);
if ($publishedCheck) { if ($publishedCheck) {
@ -63,31 +58,16 @@ class ScoreController extends Controller
} }
$scoring_guide = $entry->audition->scoringGuide; $scoring_guide = $entry->audition->scoringGuide;
$subscores = $entry->audition->scoringGuide->subscores->sortBy('display_order'); $subscores = $entry->audition->scoringGuide->subscores->sortBy('display_order');
if (! $entry) {
return redirect()->route('tabulation.chooseEntry')->with('error', 'Entry not found');
}
if ($entry->hasFlag('no_show')) { if ($entry->hasFlag('no_show')) {
session()->flash('error', session()->flash('error',
'This entry is marked as a no-show. Entering a score will remove the no-show flag'); 'This entry is marked as a no-show. Entering a score will remove the no-show flag');
} }
if ($entry->audition->prelimDefinition) {
$existing_prelim_sheets = [];
$prelim_subscores = $entry->audition->prelimDefinition->scoringGuide->subscores->sortBy('display_order');
$prelim_judges = $entry->audition->prelimDefinition->room->judges;
foreach ($prelim_judges as $judge) {
$prelim_scoreSheet = PrelimScoreSheet::where('entry_id', $entry->id)->where('user_id', $judge->id)->first();
if ($prelim_scoreSheet) {
Session::flash('caution', 'Prelim scores exist for this entry. Now editing existing scores');
$existing_prelim_sheets[$judge->id] = $prelim_scoreSheet;
}
}
} else {
$prelim_subscores = null;
$prelim_judges = null;
$existing_prelim_sheets = null;
}
return view('tabulation.entry_score_sheet', return view('tabulation.entry_score_sheet',
compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets', 'prelim_subscores', 'prelim_judges', 'existing_prelim_sheets')); compact('entry', 'judges', 'scoring_guide', 'subscores', 'existing_sheets'));
} }
public function saveEntryScoreSheet(Request $request, Entry $entry, EnterScore $scoreRecorder) public function saveEntryScoreSheet(Request $request, Entry $entry, EnterScore $scoreRecorder)
@ -96,32 +76,20 @@ class ScoreController extends Controller
if ($publishedCheck) { if ($publishedCheck) {
return $publishedCheck; return $publishedCheck;
} }
/**
* Here we process the submission from the scoring form.
* We're expecting submitted data to include an array for each judge.
* Each array should be called judge+ the judges ID number
* The array should have a key for each subscore and the value of the score submitted
*/
foreach ($request->all() as $key => $value) { foreach ($request->all() as $key => $value) {
// We're not interested in submission values that don't have judge in the name
if (! str_contains($key, 'judge')) { if (! str_contains($key, 'judge')) {
continue; continue;
} }
// Extract the judge ID from the field name and load the user
$judge_id = str_replace('judge', '', $key); $judge_id = str_replace('judge', '', $key);
$judge = User::find($judge_id); $judge = User::find($judge_id);
// Check for existing scores, if so, tell EnterScores action that we're updating it, otherwise a new score
$existingScore = ScoreSheet::where('entry_id', $entry->id) $existingScore = ScoreSheet::where('entry_id', $entry->id)
->where('user_id', $judge->id)->first(); ->where('user_id', $judge->id)->first();
if ($existingScore === null) { if ($existingScore === null) {
$existingScore = false; $existingScore = false;
} }
try { try {
$scoreRecorder($judge, $entry, $value, $existingScore); $scoreRecorder($judge, $entry, $value, $existingScore);
} catch (AuditionAdminException $e) { } catch (ScoreEntryException $e) {
return redirect()->route('scores.entryScoreSheet', ['entry_id' => $entry->id]) return redirect()->route('scores.entryScoreSheet', ['entry_id' => $entry->id])
->with('error', $e->getMessage()); ->with('error', $e->getMessage());
} }
@ -133,50 +101,6 @@ class ScoreController extends Controller
return redirect()->route('scores.chooseEntry')->with('success', 'Scores saved'); return redirect()->route('scores.chooseEntry')->with('success', 'Scores saved');
} }
public function savePrelimEntryScoreSheet(Request $request, Entry $entry, EnterPrelimScore $scoreRecorder)
{
$publishedCheck = $this->checkIfPublished($entry);
if ($publishedCheck) {
return $publishedCheck;
}
/**
* Here we process the submission from the scoring form.
* We're expecting submitted data to include an array for each judge.
* Each array should be called judge+ the judges ID number
* The array should have a key for each subscore and the value of the score submitted
*/
foreach ($request->all() as $key => $value) {
// We're not interested in submission values that don't have judge in the name
if (! str_contains($key, 'judge')) {
continue;
}
// Extract the judge ID from the field name and load the user
$judge_id = str_replace('judge', '', $key);
$judge = User::find($judge_id);
// Check for existing scores, if so, tell EnterScores action that we're updating it, otherwise a new score
$existingScore = PrelimScoreSheet::where('entry_id', $entry->id)
->where('user_id', $judge->id)->first();
if ($existingScore === null) {
$existingScore = false;
}
try {
$scoreRecorder($judge, $entry, $value, $existingScore);
} catch (AuditionAdminException $e) {
return redirect()->route('scores.entryScoreSheet', ['entry_id' => $entry->id])
->with('error', $e->getMessage());
}
}
// Since we're entering a score, this apparently isn't a no show.
$entry->removeFlag('no_show');
return redirect()->route('scores.chooseEntry')->with('success', 'Prelim Scores Saved');
}
protected function checkIfPublished($entry) protected function checkIfPublished($entry)
{ {
// We're not going to enter scores if seats are published // We're not going to enter scores if seats are published

View File

@ -0,0 +1,302 @@
<?php
namespace App\Http\Controllers\Tabulation;
use App\Actions\Tabulation\GetAuditionSeats;
use App\Actions\Tabulation\RankAuditionEntries;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Doubler;
use App\Models\Ensemble;
use App\Models\Entry;
use App\Models\Seat;
use Debugbar;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use function redirect;
class SeatAuditionFormController extends Controller
{
public function showForm(Request $request, Audition $audition)
{
$seatingProposal = (session('proposedSeatingArray-'.$audition->id));
if ($audition->hasFlag('seats_published')) {
$publishedSeats = Seat::where('audition_id', $audition->id)
->join('ensembles', 'seats.ensemble_id', '=', 'ensembles.id')
->orderBy('ensembles.rank')
->orderBy('seats.seat')
->select('seats.*')
->with(['ensemble', 'entry.student.school'])
->get();
} else {
$publishedSeats = false;
}
$ranker = app(RankAuditionEntries::class);
// Get scored entries in order
$scored_entries = $ranker($audition, 'seating');
$scored_entries->load(['student.doublers', 'student.school']);
// Get unscored entries sorted by draw number
$unscored_entries = $audition->entries()
->whereDoesntHave('totalScore')
->whereDoesntHave('flags', function ($query) {
$query->where('flag_name', 'no_show');
})
->whereDoesntHave('flags', function ($query) {
$query->where('flag_name', 'failed_prelim');
})
->with('student.school')
->orderBy('draw_number', 'asc')
->get();
// Get no show entries sorted by draw number
$noshow_entries = $audition->entries()
->whereDoesntHave('totalScore')
->whereHas('flags', function ($query) {
$query->where('flag_name', 'no_show');
})
->with('student.school')
->orderBy('draw_number', 'asc')
->get();
// Get failed prelim entries sorted by draw number
$failed_prelim_entries = $audition->entries()
->whereDoesntHave('totalScore')
->whereHas('flags', function ($query) {
$query->where('flag_name', 'failed_prelim');
})
->with('student.school')
->orderBy('draw_number', 'asc')
->get();
// Get Doublers
$doublerData = Doubler::where('event_id', $audition->event_id)
->whereIn('student_id', $scored_entries->pluck('student_id'))
->get()
->keyBy('student_id');
$auditionHasUnresolvedDoublers = false;
foreach ($doublerData as $doubler) {
if (! is_null($doubler->accepted_entry)) {
continue;
}
foreach ($doubler->entries() as $entry) {
if ($entry->audition_id === $audition->id && $entry->hasFlag('declined')) {
continue 2;
}
}
$auditionHasUnresolvedDoublers = true;
}
$canSeat = ! $auditionHasUnresolvedDoublers && $unscored_entries->count() === 0;
return view('tabulation.auditionSeating',
compact('audition',
'scored_entries',
'unscored_entries',
'noshow_entries',
'failed_prelim_entries',
'doublerData',
'auditionHasUnresolvedDoublers',
'canSeat',
'seatingProposal',
'publishedSeats',
)
);
}
public function declineSeat(Audition $audition, Entry $entry)
{
$entry->addFlag('declined');
Cache::forget('rank_seating_'.$entry->audition_id);
return redirect()->route('seating.audition', ['audition' => $audition->id])->with('success',
$entry->student->full_name().' has declined '.$audition->name);
}
public function massDecline(Audition $audition)
{
$validData = request()->validate([
'decline-below' => ['required', 'integer', 'min:0'],
]);
$ranker = app(RankAuditionEntries::class);
// Get scored entries in order
$scored_entries = $ranker($audition, 'seating');
$scored_entries->load(['student.doublers', 'student.school']);
foreach ($scored_entries as $entry) {
Debugbar::info('Starting entry '.$entry->student->full_name());
if ($entry->hasFlag('declined')) {
Debugbar::info('Skipping '.$entry->student->full_name().' because they have already been declined');
continue;
}
if (! $entry->student->isDoublerInEvent($audition->event_id)) {
Debugbar::info('Skipping '.$entry->student->full_name().' because they are not a doubler');
continue;
}
if ($entry->student->doublers->where('event_id', $audition->event_id)->first()->accepted_entry) {
Debugbar::info('Skipping '.$entry->student->full_name().' because they have already accepted a seat');
continue;
}
$entry->addFlag('declined');
}
Cache::forget('rank_seating_'.$entry->audition_id);
return redirect()->route('seating.audition', ['audition' => $audition->id]);
}
public function acceptSeat(
Audition $audition,
Entry $entry
) {
$doublerData = Doubler::findDoubler($entry->student_id, $audition->event_id);
foreach ($doublerData->entries() as $doublerEntry) {
if (! $doublerEntry->totalScore && ! $doublerEntry->hasFlag('declined') && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim')) {
return redirect()->route('seating.audition', ['audition' => $audition->id])->with('error',
'Cannot accept seating for '.$entry->student->full_name().' because student has unscored 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')) {
$doublerEntry->addFlag('declined');
}
}
return redirect()->route('seating.audition', ['audition' => $audition->id])->with('success',
$entry->student->full_name().' has accepted '.$audition->name);
}
public function noshow(
Audition $audition,
Entry $entry
) {
$recorder = app('App\Actions\Tabulation\EnterNoShow');
try {
$msg = $recorder($entry);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
}
return redirect()->route('seating.audition', [$audition])->with('success', $msg);
}
public function draftSeats(
Audition $audition,
Request $request
) {
$ranker = app(RankAuditionEntries::class);
$validated = $request->validate([
'ensemble' => ['required', 'array'],
'ensemble.*' => ['required', 'integer', 'min:0'],
]);
$proposedSeatingArray = [];
$rankedEntries = $ranker($audition, 'seating');
$rankedEntries = $rankedEntries->reject(function ($entry) {
return $entry->hasFlag('declined');
});
$rankedEntries->load(['student.school']);
$rankedEnembles = Ensemble::orderBy('rank')->where('event_id', $audition->event_id)->get();
$ensembleRankOn = 1;
foreach ($rankedEnembles as $ensemble) {
if (! Arr::has($validated['ensemble'], $ensemble->id)) {
continue;
}
$proposedSeatingArray[$ensembleRankOn]['ensemble_id'] = $ensemble->id;
$proposedSeatingArray[$ensembleRankOn]['ensemble_name'] = $ensemble->name;
$proposedSeatingArray[$ensembleRankOn]['accept_count'] = $validated['ensemble'][$ensemble->id];
for ($n = 1; $n <= $validated['ensemble'][$ensemble->id]; $n++) {
// Escape the loop if we're out of entries
if ($rankedEntries->isEmpty()) {
break;
}
$thisEntry = $rankedEntries->shift();
$proposedSeatingArray[$ensembleRankOn]['seats'][$n]['seat'] = $n;
$proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_id'] = $thisEntry->id;
$proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_name'] = $thisEntry->student->full_name();
$proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_school'] = $thisEntry->student->school->name;
}
$ensembleRankOn++;
}
$sessionKeyName = 'proposedSeatingArray-'.$audition->id;
$request->session()->put($sessionKeyName, $proposedSeatingArray, 10);
return redirect()->route('seating.audition', ['audition' => $audition->id]);
}
public function clearDraft(
Audition $audition
) {
session()->forget('proposedSeatingArray-'.$audition->id);
return redirect()->route('seating.audition', ['audition' => $audition->id]);
}
public function publishSeats(
Audition $audition
) {
$publisher = app('App\Actions\Tabulation\PublishSeats');
$seatingProposal = (session('proposedSeatingArray-'.$audition->id));
$proposal = [];
foreach ($seatingProposal as $ensemble) {
$ensembleId = $ensemble['ensemble_id'];
if (isset($ensemble['seats'])) {
foreach ($ensemble['seats'] as $seat) {
$proposal[] = [
'ensemble_id' => $ensembleId,
'audition_id' => $audition->id,
'seat' => $seat['seat'],
'entry_id' => $seat['entry_id'],
];
}
}
}
$publisher($audition, $proposal);
session()->forget('proposedSeatingArray-'.$audition->id);
return redirect()->route('seating.audition', [$audition]);
}
public function unpublishSeats(
Audition $audition
) {
$unpublisher = app('App\Actions\Tabulation\UnpublishSeats');
$unpublisher($audition);
session()->forget('proposedSeatingArray-'.$audition->id);
return redirect()->route('seating.audition', [$audition]);
}
protected function pickRightPanel(
Audition $audition,
array $seatable
) {
if ($audition->hasFlag('seats_published')) {
$resultsWindow = new GetAuditionSeats;
$rightPanel['view'] = 'tabulation.auditionSeating-show-published-seats';
$rightPanel['data'] = $resultsWindow($audition);
return $rightPanel;
}
if ($seatable['allScored'] == false || $seatable['doublersResolved'] == false) {
$rightPanel['view'] = 'tabulation.auditionSeating-unable-to-seat-card';
$rightPanel['data'] = $seatable;
return $rightPanel;
}
$rightPanel['view'] = 'tabulation.auditionSeating-right-complete-not-published';
$rightPanel['data'] = $this->auditionService->getSeatingLimits($audition);
return $rightPanel;
}
}

View File

@ -0,0 +1,180 @@
<?php
namespace App\Http\Controllers\Tabulation;
use App\Actions\Entries\DoublerDecision;
use App\Actions\Tabulation\CalculateEntryScore;
use App\Actions\Tabulation\GetAuditionSeats;
use App\Actions\Tabulation\RankAuditionEntries;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Services\AuditionService;
use App\Services\DoublerService;
use App\Services\EntryService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use function redirect;
class SeatAuditionFormControllerOLD extends Controller
{
protected CalculateEntryScore $calc;
protected DoublerService $doublerService;
protected RankAuditionEntries $ranker;
protected EntryService $entryService;
protected AuditionService $auditionService;
protected DoublerDecision $decider;
public function __construct(
CalculateEntryScore $calc,
RankAuditionEntries $ranker,
DoublerService $doublerService,
EntryService $entryService,
AuditionService $auditionService,
DoublerDecision $decider,
) {
$this->calc = $calc;
$this->ranker = $ranker;
$this->doublerService = $doublerService;
$this->entryService = $entryService;
$this->auditionService = $auditionService;
$this->decider = $decider;
}
public function __invoke(Request $request, Audition $audition)
{
// If a seating proposal was posted, deal wth it
if ($request->method() == 'POST' && $request->input('ensembleAccept')) {
$requestedEnsembleAccepts = $request->input('ensembleAccept');
} else {
$requestedEnsembleAccepts = false;
}
// Deal with a mass no-show request
if ($request->input('mass-no-show')) {
$entries = $audition->entries()->forSeating()->withCount('scoreSheets')->with('flags')->get();
foreach ($entries as $entry) {
if ($entry->scoreSheets_count == 0 && ! $entry->hasFlag('no_show')) {
$entry->addFlag('no_show');
}
Cache::forget('entryScore-'.$entry->id.'-seating');
Cache::forget('entryScore-'.$entry->id.'-advancement');
}
Cache::forget('audition'.$audition->id.'seating');
Cache::forget('audition'.$audition->id.'advancement');
}
$entryData = [];
$entries = $this->ranker->rank('seating', $audition);
// Deal with mass decline doubler request
if ($request->input('decline-below')) {
Cache::forget('audition'.$audition->id.'seating');
$changes_made = false;
foreach ($entries as $entry) {
$doublerData = $this->doublerService->entryDoublerData($entry);
if ($doublerData && ! $entry->hasFlag('declined') && $entry->rank > $request->input('decline-below')) {
try {
$this->decider->decline($entry);
$changes_made = true;
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
}
}
}
if ($changes_made) {
$cache_key = 'event'.$audition->event_id.'doublers-seating';
Cache::forget($cache_key);
return redirect()->back();
}
}
$entries->load('student.school');
$entries->load('student.doublerRequests');
$seatable = [
'allScored' => true,
'doublersResolved' => true,
];
foreach ($entries as $entry) {
$totalScoreColumn = 'No Score';
$fullyScored = false;
if ($entry->score_totals) {
$totalScoreColumn = $entry->score_totals[0] >= 0 ? $entry->score_totals[0] : $entry->score_message;
$fullyScored = $entry->score_totals[0] >= 0;
}
// No Shows are fully scored
if ($entry->hasFlag('no_show')) {
$fullyScored = true;
}
$doublerData = $this->doublerService->entryDoublerData($entry);
$entryData[] = [
'rank' => $entry->rank,
'id' => $entry->id,
'studentName' => $entry->student->full_name(),
'schoolName' => $entry->student->school->name,
'drawNumber' => $entry->draw_number,
'totalScore' => $totalScoreColumn,
'fullyScored' => $fullyScored,
'hasBonusScores' => $entry->bonus_scores_count > 0,
'doubleData' => $doublerData,
'doublerRequest' => $entry->student->doublerRequests()->where('event_id',
$audition->event_id)->first()?->request,
];
// If this entries double decision isn't made, block seating
if ($doublerData && $doublerData[$entry->id]['status'] == 'undecided') {
$seatable['doublersResolved'] = false;
}
// If entry is unscored, block seating
if (! $fullyScored) {
$seatable['allScored'] = false;
}
}
$rightPanel = $this->pickRightPanel($audition, $seatable);
$seatableEntries = [];
if ($seatable['doublersResolved'] && $seatable['allScored']) {
$seatableEntries = $entries->reject(function ($entry) {
if ($entry->hasFlag('declined')) {
return true;
}
if ($entry->hasFlag('no_show')) {
return true;
}
if ($entry->hasFlag('failed_prelim')) {
return true;
}
return false;
});
}
return view('tabulation.auditionSeating',
compact('entryData', 'audition', 'rightPanel', 'seatableEntries', 'requestedEnsembleAccepts'));
}
protected function pickRightPanel(Audition $audition, array $seatable)
{
if ($audition->hasFlag('seats_published')) {
$resultsWindow = new GetAuditionSeats;
$rightPanel['view'] = 'tabulation.auditionSeating-show-published-seats';
$rightPanel['data'] = $resultsWindow($audition);
return $rightPanel;
}
if ($seatable['allScored'] == false || $seatable['doublersResolved'] == false) {
$rightPanel['view'] = 'tabulation.auditionSeating-unable-to-seat-card';
$rightPanel['data'] = $seatable;
return $rightPanel;
}
$rightPanel['view'] = 'tabulation.auditionSeating-right-complete-not-published';
$rightPanel['data'] = $this->auditionService->getSeatingLimits($audition);
return $rightPanel;
}
}

View File

@ -1,99 +0,0 @@
<?php
namespace App\Http\Controllers\Tabulation\Seating;
use App\Actions\Entries\DoublerDecision;
use App\Actions\Tabulation\RankAuditionEntries;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Entry;
use Illuminate\Support\Facades\Cache;
use function redirect;
class EnterDoublerDecisionsController extends Controller
{
public function noshow(
Audition $audition,
Entry $entry
) {
$recorder = app('App\Actions\Tabulation\EnterNoShow');
try {
$msg = $recorder($entry);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
}
return redirect()->route('seating.audition', [$audition])->with('success', $msg);
}
public function declineSeat(Audition $audition, Entry $entry)
{
$decider = app(DoublerDecision::class);
try {
$decider->decline($entry);
} catch (AuditionAdminException $e) {
return redirect()->route('seating.audition', ['audition' => $audition->id])
->with('error', $e->getMessage());
}
return redirect()->route('seating.audition', ['audition' => $audition->id])->with('success',
$entry->student->full_name().' has declined '.$audition->name);
}
public function massDecline(Audition $audition)
{
$decider = app(DoublerDecision::class);
$validData = request()->validate([
'decline-below' => ['required', 'integer', 'min:0'],
]);
$ranker = app(RankAuditionEntries::class);
// Get scored entries in order
try {
$scored_entries = $ranker($audition, 'seating');
} catch (AuditionAdminException $e) {
return redirect()->route('seating.audition', ['audition' => $audition->id])
->with('error', $e->getMessage());
}
$scored_entries->load(['student.doublers', 'student.school']);
foreach ($scored_entries as $entry) {
if ($entry->seatingRank < $validData['decline-below']) {
continue;
}
if ($entry->hasFlag('declined')) {
continue;
}
if (! $entry->student->isDoublerInEvent($audition->event_id)) {
continue;
}
if ($entry->student->doublers->where('event_id', $audition->event_id)->first()->accepted_entry) {
continue;
}
try {
$decider->decline($entry);
} catch (AuditionAdminException $e) {
return redirect()->route('seating.audition', ['audition' => $audition->id])
->with('error', $e->getMessage());
}
}
Cache::forget('rank_seating_'.$audition->id);
return redirect()->route('seating.audition', ['audition' => $audition->id]);
}
public function acceptSeat(Audition $audition, Entry $entry)
{
$decider = app(DoublerDecision::class);
try {
$decider->accept($entry);
} catch (AuditionAdminException $e) {
return redirect()->route('seating.audition', ['audition' => $audition->id])
->with('error', $e->getMessage());
}
return redirect()->route('seating.audition', ['audition' => $audition->id])->with('success',
$entry->student->full_name().' has accepted '.$audition->name);
}
}

View File

@ -1,90 +0,0 @@
<?php
namespace App\Http\Controllers\Tabulation\Seating;
use App\Actions\Tabulation\RankAuditionEntries;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Ensemble;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use function redirect;
/**
* Selects entries for seating in an audition and saves them to the session
* for later formal seating.
*/
class MakeSeatingDecisionsController extends Controller
{
public function draftSeats(
Audition $audition,
Request $request
) {
$ranker = app(RankAuditionEntries::class);
$validated = $request->validate([
'ensemble' => ['required', 'array'],
'ensemble.*' => ['required', 'integer', 'min:0'],
]);
$proposedSeatingArray = [];
try {
$rankedEntries = $ranker($audition, 'seating');
} catch (AuditionAdminException $e) {
return redirect()->route('seating.audition', ['audition' => $audition->id])
->with('error', $e->getMessage());
}
// Pull out entries that have declined a seat in this audition
$rankedEntries = $rankedEntries->reject(function ($entry) {
return $entry->hasFlag('declined');
});
$rankedEntries->load(['student.school']);
$rankedEnsembles = Ensemble::orderBy('rank')->where('event_id', $audition->event_id)->get();
$ensembleRankOn = 1;
// Iterate over all ensembles that exist for the event
foreach ($rankedEnsembles as $ensemble) {
// If the user didn't ask for any seats in this ensemble, skip it
if (! Arr::has($validated['ensemble'], $ensemble->id)) {
continue;
}
// Set up an entry in the session for each ensemble we're going to seat
$proposedSeatingArray[$ensembleRankOn]['ensemble_id'] = $ensemble->id;
$proposedSeatingArray[$ensembleRankOn]['ensemble_name'] = $ensemble->name;
$proposedSeatingArray[$ensembleRankOn]['accept_count'] = $validated['ensemble'][$ensemble->id];
// Pull the top rated entry for each seat in order
for ($n = 1; $n <= $validated['ensemble'][$ensemble->id]; $n++) {
// Escape the loop if we're out of entries
if ($rankedEntries->isEmpty()) {
break;
}
$thisEntry = $rankedEntries->shift();
$proposedSeatingArray[$ensembleRankOn]['seats'][$n]['seat'] = $n;
$proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_id'] = $thisEntry->id;
$proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_name'] = $thisEntry->student->full_name();
$proposedSeatingArray[$ensembleRankOn]['seats'][$n]['entry_school'] = $thisEntry->student->school->name;
}
$ensembleRankOn++;
}
// Save the data to the session
$sessionKeyName = 'proposedSeatingArray-'.$audition->id;
$request->session()->put($sessionKeyName, $proposedSeatingArray);
return redirect()->route('seating.audition', ['audition' => $audition->id]);
}
public function clearDraft(
Audition $audition
) {
session()->forget('proposedSeatingArray-'.$audition->id);
return redirect()->route('seating.audition', ['audition' => $audition->id]);
}
}

View File

@ -1,51 +0,0 @@
<?php
namespace App\Http\Controllers\Tabulation\Seating;
use App\Exceptions\AuditionAdminException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use function redirect;
class PublishSeatingController extends Controller
{
public function publishSeats(
Audition $audition
) {
$publisher = app('App\Actions\Tabulation\PublishSeats');
$seatingProposal = (session('proposedSeatingArray-'.$audition->id));
$proposal = [];
foreach ($seatingProposal as $ensemble) {
$ensembleId = $ensemble['ensemble_id'];
if (isset($ensemble['seats'])) {
foreach ($ensemble['seats'] as $seat) {
$proposal[] = [
'ensemble_id' => $ensembleId,
'audition_id' => $audition->id,
'seat' => $seat['seat'],
'entry_id' => $seat['entry_id'],
];
}
}
}
try {
$publisher($audition, $proposal);
} catch (AuditionAdminException $e) {
return redirect()->route('seating.audition', [$audition])->with('error', $e->getMessage());
}
session()->forget('proposedSeatingArray-'.$audition->id);
return redirect()->route('seating.audition', [$audition]);
}
public function unpublishSeats(
Audition $audition
) {
$unpublisher = app('App\Actions\Tabulation\UnpublishSeats');
$unpublisher($audition);
session()->forget('proposedSeatingArray-'.$audition->id);
return redirect()->route('seating.audition', [$audition]);
}
}

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