Compare commits

..

No commits in common. "master" and "bugfix/Admin-EntryController-Fix-For-New-Scoring" have entirely different histories.

432 changed files with 3534 additions and 18285 deletions

3
.gitignore vendored
View File

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

View File

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

View File

@ -1,28 +0,0 @@
<?php
namespace App\Actions\Development;
use App\Actions\Tabulation\EnterScore;
use App\Models\Entry;
class FakeScoresForEntry
{
public function __construct()
{
}
public function __invoke(Entry $entry): void
{
$scoreScribe = app(EnterScore::class);
$scoringGuide = $entry->audition->scoringGuide;
$subscores = $scoringGuide->subscores;
$judges = $entry->audition->judges;
foreach ($judges as $judge) {
$scoringArray = [];
foreach ($subscores as $subscore) {
$scoringArray[$subscore->id] = mt_rand(0, $subscore->max_score);
}
$scoreScribe($judge, $entry, $scoringArray);
}
}
}

View File

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

View File

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

View File

@ -2,12 +2,14 @@
namespace App\Actions\Entries;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ManageEntryException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\Student;
use function auth;
class CreateEntry
{
public function __construct()
@ -17,49 +19,49 @@ class CreateEntry
/**
* @throws ManageEntryException
*/
public function __invoke(
Student|int $student,
Audition|int $audition,
$for_seating = false,
$for_advancement = false,
$late_fee_waived = false
) {
return $this->createEntry($student, $audition, $for_seating, $for_advancement, $late_fee_waived);
public function __invoke(Student|int $student, Audition|int $audition, string|array|null $entry_for = null)
{
return $this->createEntry($student, $audition, $entry_for);
}
/**
* @throws ManageEntryException
*/
public function createEntry(
Student|int $student,
Audition|int $audition,
$for_seating = false,
$for_advancement = false,
$late_fee_waived = false
): Entry {
public function createEntry(Student|int $student, Audition|int $audition, string|array|null $entry_for = null)
{
if (is_int($student)) {
$student = Student::find($student);
}
if (is_int($audition)) {
$audition = Audition::find($audition);
}
$this->verifySubmission($student, $audition);
if (! $for_advancement && ! $for_seating) {
$for_seating = true;
$for_advancement = true;
if (! $entry_for) {
$entry_for = ['seating', 'advancement'];
}
$entry_for = collect($entry_for);
$this->verifySubmission($student, $audition);
$entry = Entry::make([
'student_id' => $student->id,
'audition_id' => $audition->id,
'draw_number' => $this->checkDraw($audition),
'for_seating' => $for_seating,
'for_advancement' => $for_advancement,
'for_seating' => $entry_for->contains('seating'),
'for_advancement' => $entry_for->contains('advancement'),
]);
$entry->save();
if ($late_fee_waived) {
$entry->addFlag('late_fee_waived');
$entry->refresh();
if (auth()->user()) {
$message = 'Entered '.$entry->student->full_name().' from '.$entry->student->school->name.' in '.$entry->audition->name.'.';
AuditLogEntry::create([
'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;
@ -81,29 +83,29 @@ class CreateEntry
{
// Make sure it's a valid student
if (! $student || ! $student->exists()) {
throw new AuditionAdminException('Invalid student provided');
throw new ManageEntryException('Invalid student provided');
}
// Make sure the audition is valid
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
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
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')) {
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
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) {
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;
use App\Exceptions\AuditionAdminException;
use App\Models\Doubler;
use App\Models\Entry;
use App\Services\DoublerService;
use Illuminate\Support\Facades\Cache;
class DoublerDecision
{
protected DoublerService $doublerService;
public function __construct(DoublerService $doublerService)
{
$this->doublerService = $doublerService;
}
/**
* @throws AuditionAdminException
*/
@ -27,45 +34,25 @@ class DoublerDecision
'decline' => $this->decline($entry),
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
{
if ($entry->hasFlag('declined')) {
throw new AuditionAdminException('Entry '.$entry->id.' is already declined');
}
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);
Cache::forget('audition'.$entry->audition_id.'seating');
Cache::forget('audition'.$entry->audition_id.'advancement');
// Process student entries
$doublerData = Doubler::findDoubler($entry->student_id, $entry->audition->event_id);
// Check each entry and see if it is unscored. We can't accept this entry if that is the case.
foreach ($doublerData->entries() as $doublerEntry) {
if (! $doublerEntry->totalScore && ! $doublerEntry->hasFlag('declined') && ! $doublerEntry->hasFlag('no_show') && ! $doublerEntry->hasFlag('failed_prelim')) {
throw new AuditionAdminException('Cannot accept seating for '.$entry->student->full_name().' because student has unscored entries');
}
}
// 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);
// Decline all other entries and clear rank cache
$doublerInfo = $this->doublerService->simpleDoubleInfo($entry);
foreach ($doublerInfo as $doublerEntry) {
Cache::forget('audition'.$doublerEntry->audition_id.'seating');
/** @var Entry $doublerEntry */
if ($doublerEntry->id !== $entry->id) {
$doublerEntry->addFlag('declined');
}
}
}
@ -75,21 +62,12 @@ class DoublerDecision
*/
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')) {
throw new AuditionAdminException('Entry '.$entry->id.' is already declined');
throw new AuditionAdminException('Entry is already declined');
}
if (! $entry->totalScore) {
throw new AuditionAdminException('Cannot decline an unscored entry');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot decline an entry in an audition where seats are published');
}
// Flag this entry
Cache::forget('audition'.$entry->audition_id.'seating');
$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;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ManageEntryException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
@ -28,7 +27,7 @@ class UpdateEntry
/**
* @throws ManageEntryException
*/
public function __invoke(Entry|int $entry, array $updateData): void
public function __invoke(Entry $entry, array $updateData): void
{
$this->updateEntry($entry, $updateData);
}
@ -42,7 +41,7 @@ class UpdateEntry
$entry = Entry::find($entry);
}
if (! $entry || ! $entry->exists) {
throw new AuditionAdminException('Invalid entry provided');
throw new ManageEntryException('Invalid entry provided');
}
$this->entry = $entry;
if (array_key_exists('for_seating', $updateData)) {
@ -82,34 +81,34 @@ class UpdateEntry
$audition = Audition::find($audition);
}
if (! $audition || ! $audition->exists) {
throw new AuditionAdminException('Invalid audition provided');
throw new ManageEntryException('Invalid audition provided');
}
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')) {
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')) {
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')) {
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) {
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) {
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) {
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 &&
Entry::where('student_id', $this->entry->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');
}
// Escape if we're not actually making a change
if ($this->entry->audition_id == $audition->id) {
@ -140,13 +139,13 @@ class UpdateEntry
}
if ($forSeating) {
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->log_message .= 'Entry '.$this->entry->id.' is entered for seating '.'<br>';
} else {
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->log_message .= 'Entry '.$this->entry->id.' is NOT entered for seating '.'<br>';
@ -163,13 +162,13 @@ class UpdateEntry
}
if ($forAdvancement) {
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->log_message .= 'Entry '.$this->entry->id.' is entered for '.auditionSetting('advanceTo').'<br>';
} else {
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->log_message .= 'Entry '.$this->entry->id.' is NOT entered for '.auditionSetting('advanceTo').'<br>';

View File

@ -2,6 +2,7 @@
namespace App\Actions\Fortify;
use App\Models\AuditLogEntry;
use App\Models\User;
use App\Rules\ValidRegistrationCode;
use Illuminate\Support\Facades\Hash;
@ -52,6 +53,17 @@ class CreateNewUser implements CreatesNewUsers
'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;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Actions\Fortify;
use App\Models\AuditLogEntry;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
@ -25,6 +26,11 @@ class ResetUserPassword implements ResetsUserPasswords
$user->forceFill([
'password' => Hash::make($input['password']),
])->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;
use App\Models\AuditLogEntry;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
@ -44,7 +45,16 @@ class UpdateUserProfileInformation implements UpdatesUserProfileInformation
'email' => $input['email'],
])->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,
])->save();
$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();
}

View File

@ -6,10 +6,6 @@ use App\Models\Entry;
use App\Models\Room;
use Illuminate\Support\Collection;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for PrintSignInSheets
class PrintSignInSheets
{
protected $pdf;
@ -78,15 +74,11 @@ class PrintSignInSheets
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['instrument'], $this->bodyRowHeight, $entry->audition->name, 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(0, $this->bodyRowHeight, ' ', 1, 1, 'L');
}

View File

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

View File

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

View File

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

View File

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

View File

@ -7,10 +7,6 @@ use App\Models\Event;
use App\Models\Seat;
use Illuminate\Support\Facades\App;
/**
* @codeCoverageIgnore
*/
// TODO figure out testing for GetExportData
class GetExportData
{
public function __construct()
@ -33,7 +29,7 @@ class GetExportData
foreach ($events as $event) {
$auditions = $event->auditions;
foreach ($auditions as $audition) {
$entries = $ranker($audition, 'seating');
$entries = $ranker->rank('seating', $audition);
foreach ($entries as $entry) {
$thisRow = $audition->name.',';
$thisRow .= $entry->raw_rank ?? '';
@ -41,7 +37,7 @@ class GetExportData
$thisRow .= $entry->student->full_name().',';
$thisRow .= $entry->student->school->name.',';
$thisRow .= $entry->student->grade.',';
$thisRow .= $entry->totalScore->seating_total ?? '';
$thisRow .= $entry->score_totals[0] ?? '';
$thisRow .= ',';
if ($entry->hasFlag('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;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\User;
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
{
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)) {
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\Entry;
use Debugbar;
class CalculateAuditionScores
{
@ -22,6 +23,7 @@ class CalculateAuditionScores
->with('audition.scoringGuide.subscores')
->get();
foreach ($pending_entries as $entry) {
Debugbar::debug('Calculating scores for entry: '.$entry->id);
$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;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ScoreEntryException;
use App\Models\BonusScore;
use App\Models\Entry;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\App;
class EnterBonusScore
{
public function __construct()
{
}
public function __invoke(User $judge, Entry $entry, int $score): void
{
$getRelatedEntries = App::make(GetBonusScoreRelatedEntries::class);
// Verify there is a need for a bonus score
if ($entry->audition->bonusScore->count() === 0) {
throw new AuditionAdminException('The entries audition does not accept bonus scores');
}
$this->basicValidations($judge, $entry);
$this->validateJudgeValidity($judge, $entry, $score);
$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
{
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();
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) {
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 Illuminate\Support\Facades\DB;
use function auditionLog;
class EnterNoShow
{
/**
@ -53,13 +51,6 @@ class EnterNoShow
$msg = 'No Show has been entered for '.$entry->audition->name.' #'.$entry->draw_number.' (ID: '.$entry->id.').';
}
$affected = [
['entries', [$entry->id]],
['auditions', [$entry->audition_id]],
['students', [$entry->student_id]],
];
auditionLog($msg, $affected);
return $msg;
}
}

View File

@ -1,137 +0,0 @@
<?php
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Entry;
use App\Models\PrelimDefinition;
use App\Models\PrelimScoreSheet;
use App\Models\User;
use DB;
use function auditionLog;
class EnterPrelimScore
{
public function __invoke(
User $user,
Entry $entry,
array $scores,
PrelimScoreSheet|false $prelimScoreSheet = false
): PrelimScoreSheet {
$scores = collect($scores);
// Basic Validity Checks
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
if (! Entry::where('id', $entry->id)->exists()) {
throw new AuditionAdminException('Entry does not exist');
}
if ($entry->audition->hasFlag('seats_published')) {
throw new AuditionAdminException('Cannot score an entry in an audition where seats are published');
}
// Check if the entries audition has a prelim definition
if (! PrelimDefinition::where('audition_id', $entry->audition->id)->exists()) {
throw new AuditionAdminException('The entries audition does not have a prelim');
}
$prelimDefinition = PrelimDefinition::where('audition_id', $entry->audition->id)->first();
// Don't allow changes to prelims scores if the entry has a finals score
if ($entry->scoreSheets()->count() > 0) {
throw new AuditionAdminException('Cannot change prelims scores for an entry that has finals scores');
}
// Check that the specified user is assigned to judge this entry in prelims
$check = DB::table('room_user')
->where('user_id', $user->id)
->where('room_id', $prelimDefinition->room_id)->exists();
if (! $check) {
throw new AuditionAdminException('This judge is not assigned to judge this entry in prelims');
}
// Check if a score already exists
if (! $prelimScoreSheet) {
if (PrelimScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
throw new AuditionAdminException('That judge has already entered a prelim score for that entry');
}
} else {
if ($prelimScoreSheet->user_id != $user->id) {
throw new AuditionAdminException('Existing score sheet is from a different judge');
}
if ($prelimScoreSheet->entry_id != $entry->id) {
throw new AuditionAdminException('Existing score sheet is for a different entry');
}
}
// Check the validity of submitted subscores, format array for storage, and sum score
$subscoresRequired = $prelimDefinition->scoringGuide->subscores;
$subscoresStorageArray = [];
$totalScore = 0;
$maxPossibleTotal = 0;
if ($scores->count() !== $subscoresRequired->count()) {
throw new AuditionAdminException('Invalid number of scores');
}
foreach ($subscoresRequired as $subscore) {
// check that there is an element in the $scores collection with the key = $subscore->id
if (! $scores->keys()->contains($subscore->id)) {
throw new AuditionAdminException('Invalid Score Submission');
}
if ($scores[$subscore->id] > $subscore->max_score) {
throw new AuditionAdminException('Supplied subscore exceeds maximum allowed');
}
// Add subscore to the storage array
$subscoresStorageArray[$subscore->id] = [
'score' => $scores[$subscore->id],
'subscore_id' => $subscore->id,
'subscore_name' => $subscore->name,
];
// Multiply subscore by weight then add to total
$totalScore += ($subscore->weight * $scores[$subscore->id]);
$maxPossibleTotal += ($subscore->weight * $subscore->max_score);
}
$finalTotalScore = ($maxPossibleTotal === 0) ? 0 : (($totalScore / $maxPossibleTotal) * 100);
$entry->removeFlag('no_show');
if ($prelimScoreSheet instanceof PrelimScoreSheet) {
$prelimScoreSheet->update([
'user_id' => $user->id,
'entry_id' => $entry->id,
'subscores' => $subscoresStorageArray,
'total' => $finalTotalScore,
]);
} else {
$prelimScoreSheet = PrelimScoreSheet::create([
'user_id' => $user->id,
'entry_id' => $entry->id,
'subscores' => $subscoresStorageArray,
'total' => $finalTotalScore,
]);
}
// Log the prelim score entry
$log_message = 'Entered prelim score for entry id '.$entry->id.'.<br />';
$log_message .= 'Judge: '.$user->full_name().'<br />';
foreach ($prelimScoreSheet->subscores as $subscore) {
$log_message .= $subscore['subscore_name'].': '.$subscore['score'].'<br />';
}
$log_message .= 'Total :'.$prelimScoreSheet->total.'<br />';
auditionLog($log_message, [
'entries' => [$entry->id],
'users' => [$user->id],
'auditions' => [$entry->audition_id],
'students' => [$entry->student_id],
]);
// Check if we can make a status decision
$checker = app(CheckPrelimResult::class);
$checker($entry, true);
return $prelimScoreSheet;
}
}

View File

@ -6,7 +6,6 @@
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ScoreEntryException;
use App\Models\AuditLogEntry;
use App\Models\Entry;
@ -34,17 +33,17 @@ class EnterScore
$scores = collect($scores);
// Basic Validity Checks
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
if (! $user->exists()) {
throw new ScoreEntryException('User does not exist');
}
if (! Entry::where('id', $entry->id)->exists()) {
throw new AuditionAdminException('Entry does not exist');
if (! $entry->exists()) {
throw new ScoreEntryException('Entry does not exist');
}
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')) {
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
@ -52,20 +51,20 @@ class EnterScore
->where('room_id', $entry->audition->room_id)
->where('user_id', $user->id)->exists();
if (! $check) {
throw new AuditionAdminException('This judge is not assigned to judge this entry');
throw new ScoreEntryException('This judge is not assigned to judge this entry');
}
// Check if a score already exists
if (! $scoreSheet) {
if (ScoreSheet::where('user_id', $user->id)->where('entry_id', $entry->id)->exists()) {
throw new AuditionAdminException('That judge has already entered scores for that entry');
throw new ScoreEntryException('That judge has already entered scores for that entry');
}
} else {
if ($scoreSheet->user_id !== $user->id) {
throw new AuditionAdminException('Existing score sheet is from a different judge');
throw new ScoreEntryException('Existing score sheet is from a different judge');
}
if ($scoreSheet->entry_id !== $entry->id) {
throw new AuditionAdminException('Existing score sheet is for a different entry');
throw new ScoreEntryException('Existing score sheet is for a different entry');
}
}
@ -77,17 +76,16 @@ class EnterScore
$advancementTotal = 0;
$advancementMaxPossible = 0;
if ($scores->count() !== $subscoresRequired->count()) {
throw new AuditionAdminException('Invalid number of scores');
throw new ScoreEntryException('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');
throw new ScoreEntryException('Invalid Score Submission');
}
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

View File

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

View File

@ -2,15 +2,11 @@
namespace App\Actions\Tabulation;
use App\Exceptions\AuditionAdminException;
use App\Models\Audition;
use App\Models\Ensemble;
use App\Models\Seat;
use function dd;
/**
* @codeCoverageIgnore
*/
// TODO delete if truly depricated
class GetAuditionSeats
{
public function __construct()
@ -24,7 +20,6 @@ class GetAuditionSeats
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();
$seats = Seat::with('student.school')->where('audition_id', $audition->id)->orderBy('seat')->get();
$return = [];

View File

@ -14,27 +14,6 @@ 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
{
if (count($seats) === 0) {

View File

@ -24,27 +24,29 @@ class RankAuditionEntries
*
* @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') {
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;
if ($rank_type === 'seating') {
return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition, $pullDeclinedEntries) {
return $this->get_seating_ranks($audition, $pullDeclinedEntries);
return cache()->remember('rank_seating_'.$audition->id, $cache_duration, function () use ($audition) {
return $this->get_seating_ranks($audition);
});
}
if ($rank_type === 'advancement') {
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) {
$totalColumn = 'seating_total_with_bonus';
@ -53,7 +55,6 @@ class RankAuditionEntries
}
$sortedEntries = $audition->entries()
->where('for_seating', true)
->whereHas('totalScore')
->with('totalScore')
->with('student.school')
@ -75,7 +76,7 @@ class RankAuditionEntries
$rankOn = 1;
foreach ($sortedEntries as $entry) {
if ($entry->hasFlag('declined') && $pullDeclinedEntries) {
if ($entry->hasFlag('declined')) {
$entry->seatingRank = 'declined';
} else {
$entry->seatingRank = $rankOn;
@ -86,9 +87,9 @@ class RankAuditionEntries
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')
->with('totalScore')
->with('student.school')
@ -107,12 +108,5 @@ class RankAuditionEntries
->orderByRaw('COALESCE(JSON_EXTRACT(entry_total_scores.advancement_subscore_totals, "$[9]"), -999999) DESC')
->select('entries.*')
->get();
$n = 1;
foreach ($sortedEntries as $entry) {
$entry->advancementRank = $n;
$n++;
}
return $sortedEntries;
}
}

View File

@ -19,6 +19,7 @@ class TotalEntryScores
public function __invoke(Entry $entry, bool $force_recalculation = false): void
{
// TODO Verify accuracy of calculations, particularly for olympic scoring
if ($force_recalculation) {
EntryTotalScore::where('entry_id', $entry->id)->delete();
}
@ -33,19 +34,17 @@ class TotalEntryScores
// deal with seating scores
// TODO: Consider a rewrite to pull the scoreSheets from the entry model so they may be preloaded
$scoreSheets = ScoreSheet::where('entry_id', $entry->id)->orderBy('seating_total', 'desc')->get();
// bail out if there are not enough score sheets
$assignedJudges = $entry->audition->judges()->count();
if ($scoreSheets->count() == 0 || $scoreSheets->count() < $assignedJudges) {
return;
}
if (auditionSetting('olympic_scoring') && $scoreSheets->count() > 2) {
if (auditionSetting('olympic_scoring' && $scoreSheets->count() > 2)) {
// under olympic scoring, drop the first and last element
$scoreSheets->shift();
$scoreSheets->pop();
}
$newTotaledScore->seating_total = round($scoreSheets->avg('seating_total'), 6);
$newTotaledScore->seating_total = $scoreSheets->avg('seating_total');
$seatingSubscores = $requiredSubscores
->filter(fn ($subscore) => $subscore->for_seating == true)
->sortBy('tiebreak_order');
@ -55,18 +54,18 @@ class TotalEntryScores
foreach ($scoreSheets as $scoreSheet) {
$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;
// deal with advancement scores
$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
$scoreSheets->shift();
$scoreSheets->pop();
}
$newTotaledScore->advancement_total = round($scoreSheets->avg('advancement_total'), 6);
$newTotaledScore->advancement_total = $scoreSheets->avg('advancement_total');
$advancement_subscores = $requiredSubscores
->filter(fn ($subscore) => $subscore->for_advance == true)
->sortBy('tiebreak_order');
@ -76,7 +75,7 @@ class TotalEntryScores
foreach ($scoreSheets as $scoreSheet) {
$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;

View File

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

View File

@ -6,26 +6,24 @@ use App\Exceptions\AuditionAdminException;
use App\Models\AuditionFlag;
use App\Models\AuditLogEntry;
use App\Models\BonusScore;
use App\Models\Doubler;
use App\Models\CalculatedScore;
use App\Models\DoublerRequest;
use App\Models\EntryFlag;
use App\Models\EntryTotalScore;
use App\Models\JudgeAdvancementVote;
use App\Models\NominationEnsembleEntry;
use App\Models\ScoreSheet;
use App\Models\Seat;
use App\Models\Student;
use App\Models\UserFlag;
use Illuminate\Support\Facades\DB;
use function auth;
/**
* @codeCoverageIgnore
*/
// TODO: figure out how to test YearEndCleanup
class YearEndCleanup
{
public function __construct()
{
}
public function __invoke(?array $options = []): void
{
$this->cleanup($options);
@ -51,9 +49,8 @@ class YearEndCleanup
AuditLogEntry::truncate();
AuditionFlag::truncate();
BonusScore::truncate();
EntryTotalScore::truncate();
CalculatedScore::truncate();
DoublerRequest::truncate();
Doubler::truncate();
EntryFlag::truncate();
ScoreSheet::truncate();
Seat::truncate();
@ -65,20 +62,19 @@ class YearEndCleanup
if (is_array($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('room_user')->truncate();
DB::table('rooms')->where('id', '>', 0)->delete();
DB::table('rooms')->delete();
}
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']);
}
if (in_array('unassignJudges', $options)) {
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 Illuminate\Console\Command;
/**
* @codeCoverageIgnore
*/
class RecalculateTotalScores extends Command
class RecalculateScores extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'audition:recalculate-total-scores';
protected $signature = 'audition:recalculate-scores';
/**
* The console command description.

View File

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

View File

@ -10,78 +10,43 @@ use Illuminate\Console\Command;
class fictionalize extends Command
{
protected $signature = 'audition:fictionalize
{--students : Fictionalize student names}
{--schools : Fictionalize school names}
{--users : Fictionalize user data}
{--all : Fictionalize all data types}';
/**
* The name and signature of the console command.
*
* @var string
*/
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()
{
$faker = Factory::create();
// If no options are specified or --all is used, process everything
$processAll = $this->option('all') ||
(! $this->option('students') && ! $this->option('schools') && ! $this->option('users'));
if ($processAll || $this->option('students')) {
$this->info('Fictionalizing students...');
$bar = $this->output->createProgressBar(Student::count());
Student::chunk(100, function ($students) use ($faker, $bar) {
foreach ($students as $student) {
$student->update([
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
foreach (Student::all() as $student) {
$student->first_name = $faker->firstName();
$student->last_name = $faker->lastName();
$student->save();
}
if ($processAll || $this->option('schools')) {
$this->info('Fictionalizing schools...');
$bar = $this->output->createProgressBar(School::count());
School::chunk(100, function ($schools) use ($faker, $bar) {
foreach ($schools as $school) {
$school->update([
'name' => $faker->city().' High School',
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
foreach (School::all() as $school) {
$school->name = $faker->city().' High School';
$school->save();
}
if ($processAll || $this->option('users')) {
$this->info('Fictionalizing users...');
$bar = $this->output->createProgressBar(User::where('email', '!=', 'matt@mattyoung.us')->count());
User::where('email', '!=', 'matt@mattyoung.us')
->chunk(100, function ($users) use ($faker, $bar) {
foreach ($users as $user) {
$user->update([
'email' => $faker->unique()->email(),
'first_name' => $faker->firstName(),
'last_name' => $faker->lastName(),
'cell_phone' => $faker->phoneNumber(),
]);
$bar->advance();
}
});
$bar->finish();
$this->newLine();
}
$this->info('Fictionalization complete!');
foreach (User::where('email', '!=', 'matt@mattyoung.us')->get() as $user) {
$user->email = $faker->email();
$user->first_name = $faker->firstName();
$user->last_name = $faker->lastName();
$user->cell_phone = $faker->phoneNumber();
$user->save();
}
}
}

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 NO_SHOW = 'no_show';
case FAILED_PRELIM = 'failed_prelim';
case PASSED_PRELIM = 'passed_prelim';
case LATE_FEE_WAIVED = 'late_fee_waived';
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,15 +3,13 @@
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\AuditionStoreOrUpdateRequest;
use App\Http\Requests\BulkAuditionEditRequest;
use App\Models\Audition;
use App\Models\Event;
use App\Models\Room;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function compact;
use function abort;
use function redirect;
use function request;
use function response;
@ -30,20 +28,38 @@ class AuditionController extends Controller
public function create()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$events = Event::orderBy('name')->get();
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['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([
'event_id' => $validData['event_id'],
'name' => $validData['name'],
@ -55,7 +71,7 @@ class AuditionController extends Controller
'for_advancement' => $validData['for_advancement'],
'scoring_guide_id' => $validData['scoring_guide_id'],
'room_id' => 0,
'score_order' => $validData['score_order'],
'score_order' => $new_score_order,
]);
return to_route('admin.auditions.index')->with('success', 'Audition created successfully');
@ -63,14 +79,33 @@ class AuditionController extends Controller
public function edit(Audition $audition)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$events = Event::orderBy('name')->get();
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([
'event_id' => $validData['event_id'],
@ -86,59 +121,11 @@ class AuditionController extends Controller
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)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order;
foreach ($order as $index => $id) {
$audition = Audition::find($id);
@ -151,15 +138,9 @@ class AuditionController extends Controller
public function roomUpdate(Request $request)
{
$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) {
$a = Audition::where('id', $audition['id'])
Audition::where('id', $audition['id'])
->update([
'room_id' => $audition['room_id'],
'order_in_room' => $audition['room_order'],

View File

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

View File

@ -27,7 +27,7 @@ class BonusScoreDefinitionController extends Controller
public function store()
{
$validData = request()->validate([
'name' => 'required|unique:bonus_score_definitions,name',
'name' => 'required',
'max_score' => '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');
}
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)
{
if ($bonusScore->auditions()->count() > 0) {
@ -63,7 +49,6 @@ class BonusScoreDefinitionController extends Controller
public function assignAuditions(Request $request)
{
// TODO: add pivot model to log changes to assignments
$validData = $request->validate([
'bonus_score_id' => 'required|exists:bonus_score_definitions,id',
'audition' => 'required|array',
@ -85,8 +70,12 @@ class BonusScoreDefinitionController extends Controller
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();
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()
{
//TODO Need to show if judge is assigned, and show bonus assignments or normal judging page
$bonusScores = BonusScoreDefinition::all();
$users = User::orderBy('last_name')->orderBy('first_name')->get();
@ -103,6 +91,9 @@ class BonusScoreDefinitionController extends Controller
public function assignJudge(BonusScoreDefinition $bonusScore)
{
if (! $bonusScore->exists()) {
return redirect()->route('admin.bonus-scores.judges')->with('error', 'Bonus Score not found');
}
$validData = request()->validate([
'judge' => 'required|exists:users,id',
]);
@ -113,6 +104,9 @@ class BonusScoreDefinitionController extends Controller
public function removeJudge(BonusScoreDefinition $bonusScore)
{
if (! $bonusScore->exists()) {
return redirect()->route('admin.bonus-scores.judges')->with('error', 'Bonus Score not found');
}
$validData = request()->validate([
'judge' => 'required|exists:users,id',
]);

View File

@ -2,24 +2,30 @@
namespace App\Http\Controllers\Admin;
use App\Actions\Draw\ClearDraw;
use App\Actions\Draw\RunDraw;
use App\Http\Controllers\Controller;
use App\Http\Requests\ClearDrawRequest;
use App\Http\Requests\RunDrawRequest;
use App\Models\Audition;
use App\Models\Event;
use App\Services\DrawService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use function array_keys;
use function to_route;
class DrawController extends Controller
{
protected $drawService;
public function __construct(DrawService $drawService)
{
$this->drawService = $drawService;
}
public function index(Request $request)
{
$events = Event::with('auditions.flags')->get();
// $drawnAuditionsExist is true if any audition->hasFlag('drawn') is true
$drawnAuditionsExist = Audition::whereHas('flags', function ($query) {
$query->where('flag_name', 'drawn');
@ -30,23 +36,18 @@ class DrawController extends Controller
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', [])));
if ($auditions->contains(fn ($audition) => $audition->hasFlag('drawn'))) {
if ($this->drawService->checkCollectionForDrawnAuditions($auditions)) {
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)
{
$drawnAuditions = Audition::whereHas('flags', function ($query) {
@ -56,17 +57,12 @@ class DrawController extends Controller
return view('admin.draw.edit', compact('drawnAuditions'));
}
/**
* Clears the draw for auditions
*/
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', [])));
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;
use App\Http\Controllers\Controller;
use App\Http\Requests\EnsembleStoreOrUpdateRequest;
use App\Models\Ensemble;
use App\Models\Event;
use App\Models\SeatingLimit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use function redirect;
@ -22,24 +21,30 @@ class EnsembleController extends Controller
return view('admin.ensembles.index', compact('events'));
}
public function store(EnsembleStoreOrUpdateRequest $request)
public function store(Request $request)
{
Log::channel('file')->warning('hello');
$validated = $request->validated();
// get the maximum value of rank from the ensemble table where event_id is equal to the request event_id
if (! Auth::user()->is_admin) {
abort(403);
}
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');
Ensemble::create([
'name' => $validated['name'],
'code' => $validated['code'],
'event_id' => $validated['event_id'],
'name' => request('name'),
'code' => request('code'),
'event_id' => request('event_id'),
'rank' => $maxCode + 1,
]);
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) {
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');
}
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([
'name' => $valid['name'],
'code' => $valid['code'],
'name' => request('name'),
'code' => request('code'),
]);
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)
{
$limits = [];
/**
* 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.
*/
$ensembles = Ensemble::with(['event'])->orderBy('event_id')->get();
if ($ensemble->exists()) {
$ensemble->load('seatingLimits');
foreach ($ensemble->seatingLimits as $lim) {
@ -114,6 +112,10 @@ class EnsembleController extends Controller
public function updateEnsembleRank(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->input('order');
$eventId = $request->input('event_id');

View File

@ -4,15 +4,17 @@ namespace App\Http\Controllers\Admin;
use App\Actions\Entries\CreateEntry;
use App\Actions\Entries\UpdateEntry;
use App\Exceptions\ManageEntryException;
use App\Http\Controllers\Controller;
use App\Http\Requests\EntryStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\School;
use App\Models\Seat;
use App\Models\Student;
use App\Services\ScoreService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function auditionSetting;
use function compact;
@ -22,6 +24,9 @@ class EntryController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$perPage = 25;
$filters = session('adminEntryFilters') ?? null;
$minGrade = Audition::min('minimum_grade');
@ -32,31 +37,31 @@ class EntryController extends Controller
$entries = Entry::with(['student.school', 'audition']);
$entries->orderBy('id', 'DESC');
if ($filters) {
if ($filters['id'] ?? false) {
if ($filters['id']) {
$entries->where('id', $filters['id']);
}
if ($filters['audition'] ?? false) {
if ($filters['audition']) {
$entries->where('audition_id', $filters['audition']);
}
if ($filters['school'] ?? false) {
if ($filters['school']) {
$entries->whereHas('student', function ($query) use ($filters) {
$query->where('school_id', '=', $filters['school']);
});
}
if ($filters['grade'] ?? false) {
if ($filters['grade']) {
$entries->whereHas('student', function ($query) use ($filters) {
$query->where('grade', $filters['grade']);
});
}
if ($filters['first_name'] ?? false) {
if ($filters['first_name']) {
$entries->whereHas('student', function ($query) use ($filters) {
$query->where('first_name', 'like', '%'.$filters['first_name'].'%');
});
}
if ($filters['last_name'] ?? false) {
if ($filters['last_name']) {
$entries->whereHas('student', function ($query) use ($filters) {
$query->where('last_name', 'like', '%'.$filters['last_name'].'%');
});
@ -65,6 +70,7 @@ class EntryController extends Controller
if (isset($filters['entry_type']) && $filters['entry_type']) {
// TODO define actions for each possible type filter from index.blade.php of the admin entry
match ($filters['entry_type']) {
'all' => null,
'seats' => $entries->where('for_seating', true),
'advancement' => $entries->where('for_advancement', true),
'seatsOnly' => $entries->where('for_seating', true)->where('for_advancement', false),
@ -103,19 +109,32 @@ class EntryController extends Controller
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 */
$entry = $creator(
student: $validData['student_id'],
audition: $validData['audition_id'],
for_seating: $validData['for_seating'],
for_advancement: $validData['for_advancement'],
late_fee_waived: $validData['late_fee_waived'],
);
$validData['for_seating'] = $request->get('for_seating') ? 1 : 0;
$validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0;
$validData['late_fee_waived'] = $request->get('late_fee_waived') ? 1 : 0;
$enter_for = [];
if ($validData['for_seating']) {
$enter_for[] = 'seating';
}
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']) {
$entry->addFlag('late_fee_waived');
}
@ -123,7 +142,7 @@ class EntryController extends Controller
return redirect(route('admin.entries.index'))->with('success', 'The entry has been added.');
}
public function edit(Entry $entry)
public function edit(Entry $entry, ScoreService $scoreService)
{
if ($entry->audition->hasFlag('seats_published')) {
return to_route('admin.entries.index')->with('error',
@ -137,35 +156,31 @@ class EntryController extends Controller
$students = Student::with('school')->orderBy('last_name')->orderBy('first_name')->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', 'entry')->get();
$scores = $entry->scoreSheets()->with('audition', 'judge')->get();
foreach ($scores as $score) {
$score->entry = $entry;
$score->valid = $scoreService->isScoreSheetValid($score);
$score->seating_total_score = $score->seating_total ?? 0;
$score->advancement_total_score = $score->advancement_total ?? 0;
}
$logEntries = AuditLogEntry::whereJsonContains('affected->entries', $entry->id)->orderBy('created_at', 'desc')->get();
return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores', 'logEntries'));
return view('admin.entries.edit', compact('entry', 'students', 'auditions', 'scores'));
}
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') || $entry->audition->hasFlag('advancement_published')) {
if ($entry->audition->hasFlag('seats_published')) {
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([
'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_advancement'] = $request->get('for_advancement') ? 1 : 0;
@ -175,10 +190,11 @@ class EntryController extends Controller
if (! auditionSetting('advanceTo')) {
$validData['for_seating'] = 1;
}
/** @noinspection PhpUnhandledExceptionInspection */
try {
$updater($entry, $validData);
} catch (ManageEntryException $e) {
return redirect()->route('admin.entries.index')->with('error', $e->getMessage());
}
if ($validData['late_fee_waived']) {
$entry->addFlag('late_fee_waived');
} else {
@ -188,13 +204,17 @@ class EntryController extends Controller
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',
'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()) {
return redirect()->route('admin.entries.index')->with('error', 'Cannot delete an entry that is seated');
}
@ -203,7 +223,21 @@ class EntryController extends Controller
return redirect()->route('admin.entries.index')->with('error',
'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();
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\Models\Event;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function abort;
use function compact;
class EventController extends Controller
@ -13,16 +15,15 @@ class EventController extends Controller
public function index()
{
$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)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'name' => ['required', 'unique:events,name'],
]);
@ -34,21 +35,6 @@ class EventController extends Controller
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)
{
if ($event->auditions()->count() > 0) {
@ -60,4 +46,3 @@ class EventController extends Controller
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\Response;
// TODO: Printing testing
/** @codeCoverageIgnore */
class ExportEntriesController extends Controller
{
public function __invoke()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,19 +8,19 @@ use App\Models\BonusScoreDefinition;
use App\Models\Room;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
use function auditionLog;
use function redirect;
class RoomController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$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)) {
$unassignedRoom = Room::create([
'id' => 0,
@ -30,7 +30,7 @@ class RoomController extends Controller
$unassignedRoom->id = 0;
$unassignedRoom->save();
$auditionsToUpdate = Audition::whereNull('room_id')->get();
$auditionsToUpdate = Audition::where('room_id', null)->get();
foreach ($auditionsToUpdate as $audition) {
$audition->room_id = 0;
$audition->save();
@ -54,6 +54,9 @@ class RoomController extends Controller
public function updateJudgeAssignment(Request $request, Room $room)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([
'judge' => 'exists:users,id',
]);
@ -67,21 +70,27 @@ class RoomController extends Controller
// detach judge on delete
$room->removeJudge($judge->id);
$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()
{
if (! Auth::user()->is_admin) {
abort(403);
}
return view('admin.rooms.create');
}
public function store(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([
'name' => 'required|unique:rooms,name',
'description' => 'nullable',
@ -97,6 +106,9 @@ class RoomController extends Controller
public function update(Request $request, Room $room)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$validData = $request->validate([
'name' => ['required', Rule::unique('rooms', 'name')->ignore($room->id)],
'description' => 'nullable',
@ -111,6 +123,10 @@ class RoomController extends Controller
public function destroy(Room $room)
{
if (! Auth::user()->is_admin) {
abort(403);
}
if ($room->auditions()->count() > 0) {
return redirect()->route('admin.rooms.index')->with('error',
'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;
use App\Actions\Schools\CreateSchool;
use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller;
use App\Http\Requests\SchoolStoreRequest;
use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\SchoolEmailDomain;
use App\Models\User;
use App\Services\Invoice\InvoiceDataService;
use Illuminate\Support\Facades\Auth;
use function abort;
use function redirect;
use function request;
@ -38,26 +38,46 @@ class SchoolController extends Controller
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)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$school->loadCount('students');
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([
'name' => $request['name'],
'address' => $request['address'],
'city' => $request['city'],
'state' => $request['state'],
'zip' => $request['zip'],
'name' => request('name'),
'address' => request('address'),
'city' => request('city'),
'state' => request('state'),
'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',
@ -66,30 +86,54 @@ class SchoolController extends Controller
public function create()
{
if (! Auth::user()->is_admin) {
abort(403);
}
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(
$request['name'],
$request['address'],
$request['city'],
$request['state'],
$request['zip'],
);
$school = School::create([
'name' => request('name'),
'address' => request('address'),
'city' => request('city'),
'state' => request('state'),
'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)
{
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();
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)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
// validate that the combination of school and domain is unique on the school_email_domains table
'domain' => ['required'],
@ -105,6 +152,12 @@ class SchoolController extends Controller
'school_id' => $school->id,
'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');
@ -116,11 +169,9 @@ class SchoolController extends Controller
$domain->delete();
// 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)
{
$invoiceData = $this->invoiceService->allData($school->id);
@ -133,9 +184,8 @@ class SchoolController extends Controller
if ($user->school_id !== $school->id) {
return redirect()->back()->with('error', 'That user is not at that school');
}
/** @noinspection PhpUnhandledExceptionInspection */
$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;
use App\Http\Controllers\Controller;
use App\Http\Requests\SubscoreDefinitionRequest;
use App\Models\ScoringGuide;
use App\Models\SubscoreDefinition;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use function abort;
use function auditionSetting;
use function request;
use function response;
@ -27,19 +28,26 @@ class ScoringGuideController extends Controller
public function store()
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'name' => ['required', 'unique:scoring_guides'],
]);
ScoringGuide::create([
$guide = ScoringGuide::create([
'name' => request('name'),
]);
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') {
$subscores = SubscoreDefinition::where('scoring_guide_id', $guide->id)->orderBy('tiebreak_order')->get();
} else {
@ -51,6 +59,9 @@ class ScoringGuideController extends Controller
public function update(ScoringGuide $guide)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'name' => ['required', 'unique:scoring_guides'],
]);
@ -64,9 +75,12 @@ class ScoringGuideController extends Controller
public function destroy(ScoringGuide $guide)
{
if (! Auth::user()->is_admin) {
abort(403);
}
if ($guide->auditions()->count() > 0) {
return redirect('/admin/scoring')->with('error',
'Cannot delete scoring guide being used by one or more auditions');
return redirect('/admin/scoring')->with('error', 'Cannot delete scoring guide with auditions');
}
$guide->delete();
@ -74,64 +88,89 @@ class ScoringGuideController extends Controller
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;
$tiebreak_order = SubscoreDefinition::where('scoring_guide_id', '=', $guide->id)->max('tiebreak_order') + 1;
if (! auditionSetting('advanceTo')) {
$validateData['for_advance'] = 0;
$validateData['for_seating'] = 1;
}
SubscoreDefinition::create([
$subscore = SubscoreDefinition::create([
'scoring_guide_id' => $guide->id,
'name' => $validateData['name'],
'max_score' => $validateData['max_score'],
'weight' => $validateData['weight'],
'display_order' => $display_order,
'tiebreak_order' => $tiebreak_order,
'for_seating' => $validateData['for_seating'],
'for_advance' => $validateData['for_advance'],
'for_seating' => $for_seating,
'for_advance' => $for_advance,
]);
return redirect(route('admin.scoring.edit', $guide))->with('success', 'Subscore added');
}
public function subscore_update(
SubscoreDefinitionRequest $request,
ScoringGuide $guide,
SubscoreDefinition $subscore
) {
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');
public function subscore_update(ScoringGuide $guide, SubscoreDefinition $subscore)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$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')) {
$validateData['for_advance'] = 0;
$validateData['for_seating'] = 1;
$for_seating = true;
}
$subscore->update([
'name' => $validateData['name'],
'max_score' => $validateData['max_score'],
'weight' => $validateData['weight'],
'for_seating' => $validateData['for_seating'],
'for_advance' => $validateData['for_advance'],
'for_seating' => $for_seating,
'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)
{
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
return redirect(route('admin.scoring.edit', $subscore->scoring_guide_id))->with('error',
'Cannot delete a subscore for a different scoring guide');
abort(409);
}
$subscore->delete();
@ -142,6 +181,9 @@ class ScoringGuideController extends Controller
public function reorder_display(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order;
foreach ($order as $index => $id) {
$subscore = SubscoreDefinition::find($id);
@ -154,6 +196,9 @@ class ScoringGuideController extends Controller
public function reorder_tiebreak(Request $request)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->order;
foreach ($order as $index => $id) {
$subscore = SubscoreDefinition::find($id);

View File

@ -2,16 +2,17 @@
namespace App\Http\Controllers\Admin;
use App\Actions\Students\CreateStudent;
use App\Http\Controllers\Controller;
use App\Http\Requests\StudentStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use App\Models\Event;
use App\Models\NominationEnsemble;
use App\Models\School;
use App\Models\Student;
use Illuminate\Support\Facades\Auth;
use function abort;
use function auth;
use function compact;
use function max;
@ -24,6 +25,9 @@ class StudentController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$filters = session('adminStudentFilters') ?? null;
$schools = School::orderBy('name')->get();
$students = Student::with(['school'])->withCount('entries')->orderBy('last_name')->orderBy('first_name');
@ -50,52 +54,155 @@ class StudentController extends Controller
public function create()
{
$minGrade = $this->minimumGrade();
$maxGrade = $this->maximumGrade();
if (! Auth::user()->is_admin) {
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();
return view('admin.students.create', ['schools' => $schools, 'minGrade' => $minGrade, 'maxGrade' => $maxGrade]);
}
public function store(StudentStoreRequest $request, CreateStudent $creator)
public function store()
{
/** @noinspection PhpUnhandledExceptionInspection */
$creator([
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'school_id' => $request['school_id'],
'optional_data' => $request->optional_data,
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'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)
{
$minGrade = $this->minimumGrade();
$maxGrade = $this->maximumGrade();
if (! Auth::user()->is_admin) {
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();
$student->loadCount('entries');
$event_entries = $student->entries()->with('audition.flags')->get()->groupBy('audition.event_id');
$entries = $student->entries()->with('audition.flags')->get();
$events = Event::all();
$logEntries = AuditLogEntry::whereJsonContains('affected->students', $student->id)->orderBy('created_at',
'desc')->get();
return view('admin.students.edit',
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries', 'logEntries'));
$event_entries = [];
foreach ($events as $event) {
$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]);
}
}
public function update(StudentStoreRequest $request, Student $student)
return view('admin.students.edit',
compact('student', 'schools', 'minGrade', 'maxGrade', 'events', 'event_entries'));
}
private function isDoublerStatusFrozen(Entry $entry, $entries)
{
// Can't change decision if results are published
if ($entry->audition->hasFlag('seats_published')) {
return true;
}
// Can't change decision if this is the only entry
if ($entries->count() === 1) {
return true;
}
// Can't change decision if this is the only entry with results not published
$unpublished = $entries->reject(function ($entry) {
return $entry->audition->hasFlag('seats_published');
});
if ($unpublished->count() < 2) {
return true;
}
// Can't change decision if we've accepted another audition
foreach ($entries as $checkEntry) {
if ($checkEntry->audition->hasFlag('seats_published') && ! $checkEntry->hasFlag('declined')) {
return true;
}
}
return false;
}
public function update(Student $student)
{
if (! Auth::user()->is_admin) {
abort(403);
}
request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
'school_id' => ['required', 'exists:schools,id'],
]);
foreach ($student->entries as $entry) {
if ($entry->audition->minimum_grade > request('grade') || $entry->audition->maximum_grade < request('grade')) {
return redirect('/admin/students/'.$student->id.'/edit')->with('error',
'This student is entered in an audition that is not available to their new grade.');
}
}
if (Student::where('first_name', request('first_name'))
->where('last_name', request('last_name'))
->where('school_id', request('school_id'))
->where('id', '!=', $student->id)
->exists()) {
return redirect('/admin/students/'.$student->id.'/edit')->with('error',
'A student with that name already exists at that school');
}
$student->update([
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'school_id' => $request['school_id'],
'optional_data' => $request->optional_data,
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
'school_id' => request('school_id'),
]);
$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');
@ -105,7 +212,7 @@ class StudentController extends Controller
public function destroy(Student $student)
{
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();
$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.');
}
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
/** @noinspection PhpUnhandledExceptionInspection */
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\Http\Controllers\Controller;
use App\Mail\NewUserPassword;
@ -15,6 +9,7 @@ use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
@ -25,6 +20,9 @@ class UserController extends Controller
{
public function index()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$users = User::with('school')->with('flags')->orderBy('last_name')->orderBy('first_name')->get();
return view('admin.users.index', ['users' => $users]);
@ -32,65 +30,95 @@ class UserController extends Controller
public function edit(User $user)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$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()
{
if (! Auth::user()->is_admin) {
abort(403);
}
$schools = School::orderBy('name')->get();
return view('admin.users.create', ['schools' => $schools]);
}
public function update(
Request $request,
User $user,
SetHeadDirector $headSetter,
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'));
public function update(Request $request, User $user, SetHeadDirector $headSetter)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$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
if ($request->has('head_director')) {
$headSetter($user);
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $message,
'affected' => ['users' => [$user->id]],
]);
if ($user->is_admin != $wasAdmin) {
$messageStart = $user->is_admin ? 'Granted admin privileges to ' : 'Revoked admin privileges from ';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $messageStart.$user->full_name().' - '.$user->email,
'affected' => ['users' => [$user->id]],
]);
}
if ($user->is_tab != $wasTab) {
$messageStart = $user->is_tab ? 'Granted tabulation privileges to ' : 'Revoked tabulation privileges from ';
AuditLogEntry::create([
'user' => auth()->user()->email,
'ip_address' => request()->ip(),
'message' => $messageStart.$user->full_name().' - '.$user->email,
'affected' => ['users' => [$user->id]],
]);
}
if ($user->hasFlag('head_director') != $validData['is_head'] && ! is_null($user->school_id)) {
if ($validData['is_head']) {
$headSetter->setHeadDirector($user);
} else {
$user->removeFlag('head_director');
$logMessage = 'Removed '.$user->full_name().' as head director at '.$user->school->name;
$logAffected = ['users' => [$user->id], 'schools' => [$user->school_id]];
auditionLog($logMessage, $logAffected);
}
// Deal with privileges
if ($request->has('is_admin')) {
$privilegesUpdater($user, 'grant', 'admin');
} else {
$privilegesUpdater($user, 'revoke', 'admin');
}
if ($request->has('is_tab')) {
$privilegesUpdater($user, 'grant', 'tab');
} else {
$privilegesUpdater($user, 'revoke', 'tab');
}
return redirect('/admin/users');
@ -98,23 +126,60 @@ class UserController extends Controller
public function store(Request $request)
{
$userCreator = app(CreateNewUser::class);
$randomPassword = Str::random(12);
$data = request()->all();
$data['password'] = $randomPassword;
$data['password_confirmation'] = $randomPassword;
$newDirector = $userCreator->create($data);
$newDirector->update([
'school_id' => $request->get('school_id') ?? null,
$request->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email', 'unique:users'],
]);
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)
{
if (! Auth::user()->is_admin) {
abort(403);
}
$message = 'Deleted user '.$user->email;
AuditLogEntry::create([
'user' => auth()->user()->email,
@ -126,22 +191,4 @@ class UserController extends Controller
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()
{
$cleanUpProcedure = app(YearEndCleanup::class);
$cleanUpProcedure = new YearEndCleanup;
$options = request()->options;
$cleanUpProcedure($options);
auditionLog('Executed year end reset.', []);

View File

@ -32,7 +32,7 @@ class DashboardController extends Controller
public function my_school()
{
if (Auth::user()->school) {
return redirect(route('schools.show', auth()->user()->school));
return redirect('/schools/'.Auth::user()->school->id);
}
$possibilities = Auth::user()->possibleSchools();
if (count($possibilities) < 1) {

View File

@ -2,67 +2,88 @@
namespace App\Http\Controllers;
use App\Http\Requests\DoublerRequestsStoreRequest;
use App\Models\AuditLogEntry;
use App\Models\DoublerRequest;
use App\Models\Event;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\View\Factory;
use Illuminate\Contracts\View\View;
use App\Models\Student;
use App\Services\DoublerService;
use Barryvdh\Debugbar\Facades\Debugbar;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use function auth;
use function compact;
use function request;
use function to_route;
class DoublerRequestController extends Controller
{
/**
* 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()
public function index(DoublerService $doublerService)
{
$events = Event::all();
$existingRequests = auth()->user()->school->doublerRequests
->groupBy('event_id')
->map(function ($requestsForEvent) {
return $requestsForEvent->keyBy('student_id');
});
$doublers = auth()->user()->school->doublers()
->with('student')
->with('event')
->get()
->groupBy('event_id');
return view('doubler_request.index', compact('events', 'doublers', 'existingRequests'));
$students = auth()->user()->school->students;
$studentIds = $students->pluck('id');
$existingRequests = DoublerRequest::whereIn('student_id', $studentIds)->get();
$doublers = [];
foreach ($events as $event) {
$event_doublers = $doublerService->doublersForEvent($event);
$doublers[$event->id] = $event_doublers;
}
public function makeRequest(DoublerRequestsStoreRequest $request)
return view('doubler_request.index', compact('events', 'doublers', 'students', 'existingRequests'));
}
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function makeRequest()
{
foreach ($request->getDoublerRequests() as $thisRequest) {
if (! $thisRequest['request']) {
DoublerRequest::where('event_id', $thisRequest['event_id'])
->where('student_id', $thisRequest['student_id'])->delete();
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' => $thisRequest['event_id'],
'student_id' => $thisRequest['student_id'],
'request' => $thisRequest['request'],
'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;
use App\Actions\Entries\CreateEntry;
use App\Http\Requests\EntryStoreRequest;
use App\Exceptions\ManageEntryException;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Entry;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -15,19 +17,11 @@ class EntryController extends Controller
{
public function index()
{
if (! auth()->user()->school_id) {
abort(403);
}
$entries = Auth::user()->entries()
->select('entries.*')
->join('students as s', 's.id', '=', 'entries.student_id')
->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();
$entries = Auth::user()->entries()->with(['student', 'audition'])->get();
$entries = $entries->sortBy(function ($entry) {
return $entry->student->last_name.$entry->student->first_name.$entry->audition->score_order;
});
$auditions = Audition::open()->get();
$students = Auth::user()->students;
$students->load('school');
@ -35,15 +29,37 @@ class EntryController extends Controller
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();
$creator(
$validData['student_id'],
$validData['audition_id'],
for_seating: $validData['for_seating'],
for_advancement: $validData['for_advancement'],
);
if ($request->user()->cannot('create', Entry::class)) {
abort(403);
}
$validData = $request->validate([
'student_id' => ['required', 'exists:students,id'],
'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.');
}
@ -53,7 +69,21 @@ class EntryController extends Controller
if ($request->user()->cannot('delete', $entry)) {
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();
return redirect()->route('entries.index')->with('success',

View File

@ -20,14 +20,14 @@ class FilterController extends Controller
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)
{
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)
@ -40,7 +40,7 @@ class FilterController extends Controller
session(['adminStudentFilters' => $filters]);
return redirect(route('admin.students.index'))->with('success', 'Filters Applied');
return redirect()->back()->with('success', 'Filters Applied');
}
public function clearAdminStudentFilter()

View File

@ -12,23 +12,16 @@ use function redirect;
class BonusScoreEntryController extends Controller
{
/**
* Displays a form for a judge to enter a bonus score for an 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()) {
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('error',
'You have already judged that entry');
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('error', 'You have already judged that entry');
}
/** @var BonusScoreDefinition $bonusScore */
$bonusScore = $entry->audition->bonusScore()->first();
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;
$bonusName = $bonusScore->name;

View File

@ -10,15 +10,12 @@ use Illuminate\Support\Facades\Auth;
class BonusScoreEntryListController extends Controller
{
/**
* Lists entries for a bonus score so the judge may select one to score.
*/
public function __invoke(Audition $audition)
{
/** @var BonusScoreDefinition $bonusScore */
$bonusScore = $audition->bonusScore()->first();
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 = $entries->reject(fn ($entry) => $entry->hasFlag('no_show'));

View File

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

View File

@ -3,19 +3,22 @@
namespace App\Http\Controllers\Judging;
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\AuditionServiceException;
use App\Exceptions\ScoreEntryException;
use App\Http\Controllers\Controller;
use App\Models\Audition;
use App\Models\Entry;
use App\Models\JudgeAdvancementVote;
use App\Models\ScoreSheet;
use App\Services\AuditionService;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use function compact;
use function redirect;
use function url;
class JudgingController extends Controller
{
@ -28,7 +31,7 @@ class JudgingController extends Controller
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();
//$rooms->load('auditions');
@ -37,16 +40,10 @@ class JudgingController extends Controller
public function auditionEntryList(Request $request, Audition $audition)
{
// TODO: Add error message if scoring guide is not set
if ($request->user()->cannot('judge', $audition)) {
return redirect()->route('judging.index')->with('error', 'You are not assigned to judge 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();
// 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();
$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',
'The requested entry is marked as a no-show. Scores cannot be entered.');
}
// Turn away users if the entry is flagged as a failed-prelim
if ($entry->hasFlag('failed_prelim')) {
return redirect()->route('judging.auditionEntryList', $entry->audition)->with('error',
'The requested entry is marked as having failed a prelim. Scores cannot be entered.');
}
$oldSheet = ScoreSheet::where('user_id', Auth::id())->where('entry_id', $entry->id)->value('subscores') ?? null;
$oldVote = JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->first();
$oldVote = $oldVote ? $oldVote->vote : 'noVote';
@ -88,11 +78,15 @@ class JudgingController extends Controller
public function saveScoreSheet(Request $request, Entry $entry, EnterScore $enterScore)
{
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
$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 = [];
foreach ($subscores as $subscore) {
$validationChecks['score'.'.'.$subscore->id] = 'required|integer|max:'.$subscore->max_score;
@ -100,17 +94,16 @@ class JudgingController extends Controller
$validatedData = $request->validate($validationChecks);
// Enter the score
/** @noinspection PhpUnhandledExceptionInspection */
try {
$enterScore(Auth::user(), $entry, $validatedData['score']);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
} catch (ScoreEntryException $e) {
return redirect()->back()->with('error', 'Error saving score - '.$e->getMessage());
}
// Deal with an advancement vote if needed
$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);
}
@ -118,10 +111,8 @@ class JudgingController extends Controller
public function updateScoreSheet(Request $request, Entry $entry, EnterScore $enterScore)
{
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();
if (! $scoreSheet) {
return redirect()->back()->with('error', 'Attempt to edit non existent score sheet');
@ -129,8 +120,11 @@ class JudgingController extends Controller
Gate::authorize('update', $scoreSheet);
// Validate form data
$subscores = $entry->audition->subscoreDefinitions;
try {
$subscores = $this->auditionService->getSubscores($entry->audition, 'all');
} catch (AuditionServiceException $e) {
return redirect()->back()->with('error', 'Error getting subscores - '.$e->getMessage());
}
$validationChecks = [];
foreach ($subscores as $subscore) {
@ -139,29 +133,38 @@ class JudgingController extends Controller
$validatedData = $request->validate($validationChecks);
// Enter the score
try {
$enterScore(Auth::user(), $entry, $validatedData['score'], $scoreSheet);
} catch (ScoreEntryException $e) {
return redirect()->back()->with('error', 'Error updating score - '.$e->getMessage());
}
$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);
}
protected function advancementVote(Request $request, Entry $entry)
{
if ($request->user()->cannot('judge', $entry->audition)) {
abort(403, 'You are not assigned to judge this entry');
}
if ($entry->for_advancement and auditionSetting('advanceTo')) {
$request->validate([
'advancement-vote' => ['required', 'in:yes,no,dq'],
]);
try {
JudgeAdvancementVote::where('user_id', Auth::id())->where('entry_id', $entry->id)->delete();
JudgeAdvancementVote::create([
'user_id' => Auth::user()->id,
'entry_id' => $entry->id,
'vote' => $request->input('advancement-vote'),
]);
} catch (Exception) {
return redirect(url()->previous())->with('error', 'Error saving advancement vote');
}
}
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;
use App\Models\Audition;
use App\Models\Entry;
use function compact;
class MonitorController extends Controller
{
public function index()
{
if (! auth()->user()->hasFlag('monitor')) {
abort(403);
return redirect()->route('dashboard')->with('error', 'You are not assigned as a monitor');
}
$method = 'GET';
$formRoute = 'monitor.enterFlag';
$title = 'Flag Entry';
return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
}
$auditions = Audition::orderBy('score_order')->with('flags')->get();
$audition = null;
return view('monitor.index', compact('audition', 'auditions'));
}
public function auditionStatus(Audition $audition)
public function flagForm()
{
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')) {
return redirect()->route('monitor.index')->with('error', 'Results for that audition 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');
}
$auditions = Audition::orderBy('score_order')->with('flags')->get();
$entries = $audition->entries()->with('flags')->with('student.school')->withCount([
'prelimScoreSheets', 'scoreSheets',
])->orderBy('draw_number')->get();
return view('monitor.index', compact('audition', 'auditions', 'entries'));
return view('monitor_entry_flag_form', compact('entry'));
}
public function toggleNoShow(Entry $entry)
public function storeFlag(Entry $entry)
{
if ($entry->audition->hasFlag('seats_published') || $entry->audition->hasFlag('advancement_published')) {
return redirect()->route('monitor.index')->with('error', 'Results for that audition are published');
if (! auth()->user()->hasFlag('monitor')) {
return redirect()->route('dashboard')->with('error', 'You are not assigned as a monitor');
}
if ($entry->hasFlag('no_show')) {
$entry->removeFlag('no_show');
return redirect()->back()->with('success', 'No Show Flag Cleared');
// 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 redirect()->back()->with('success', 'No Show Entered');
return true;
}
if ($flag === 'failed_prelim') {
$entry->addFlag('failed_prelim');
$entry->addFlag('no_show');
return true;
}
if ($flag === 'clear') {
$entry->removeFlag('failed_prelim');
$entry->removeFlag('no_show');
return true;
}
return false;
}
}

View File

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

View File

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

View File

@ -33,7 +33,6 @@ class ResultsPage extends Controller
$cacheKey = 'publicResultsPage';
if (Cache::has($cacheKey)) {
/** @codeCoverageIgnore */
return response(Cache::get($cacheKey));
}
@ -94,4 +93,9 @@ class ResultsPage extends Controller
return response($content);
}
private function generateResultsPage()
{
}
}

View File

@ -2,48 +2,93 @@
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\Http\Requests\SchoolStoreRequest;
use App\Exceptions\AuditionAdminException;
use App\Mail\NewUserPassword;
use App\Models\AuditLogEntry;
use App\Models\School;
use App\Models\SchoolEmailDomain;
use App\Models\User;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use function abort;
use function auditionLog;
use function redirect;
use function request;
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(
$request['name'],
$request['address'],
$request['city'],
$request['state'],
$request['zip'],
);
$assigner = app(AssignUserToSchool::class);
$assigner(auth()->user(), $school);
$school = School::create([
'name' => request('name'),
'address' => request('address'),
'city' => request('city'),
'state' => request('state'),
'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]],
]);
if (! 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');
}
return redirect(route('schools.show', $school));
}
return redirect('/schools/'.$school->id);
}
public function show(Request $request, School $school)
@ -73,14 +118,25 @@ class SchoolController extends Controller
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([
'name' => $request['name'],
'address' => $request['address'],
'city' => $request['city'],
'state' => $request['state'],
'zip' => $request['zip'],
'name' => request('name'),
'address' => request('address'),
'city' => request('city'),
'state' => request('state'),
'zip' => request('zip'),
]);
$message = 'Modified school #'.$school->id.' - '.$school->name.' with address <br>'.$school->address.'<br>'.$school->city.', '.$school->state.' '.$school->zip;
AuditLogEntry::create([
@ -93,26 +149,46 @@ class SchoolController extends Controller
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)
{
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')) {
abort(403);
return redirect()->back()->with('error', 'Only the head director can add directors to a school');
}
$userCreator = app(CreateNewUser::class);
$randomPassword = Str::random(12);
$data = request()->all();
$data['password'] = $randomPassword;
$data['password_confirmation'] = $randomPassword;
$newDirector = $userCreator->create($data);
$newDirector->update([
'school_id' => $school->id,
$validData = request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email', 'unique:users'],
'cell_phone' => ['required'],
'judging_preference' => ['required'],
]);
Mail::to($newDirector->email)->send(new NewUserPassword($newDirector, $randomPassword));
// Generate a random password
$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');
}
@ -120,49 +196,62 @@ class SchoolController extends Controller
public function setHeadDirector(School $school, User $user, SetHeadDirector $headSetter)
{
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')) {
abort(403);
return redirect()->back()->with('error', 'Only the head director can name a new head director');
}
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()->route('schools.show', $school)->with('success', 'New head director set');
return redirect()->back()->with('success', 'New head director set');
}
public function addDomain(School $school)
{
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')) {
abort(403);
return redirect()->back()->with('error', 'Only the head director can add domains');
}
$verifiedData = request()->validate([
'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)
{
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')) {
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();
return redirect()
->route('schools.show', auth()->user()->school)
->with('success', 'Domain deleted');
return redirect()->back()->with('success', 'Domain deleted');
}
}

View File

@ -2,10 +2,10 @@
namespace App\Http\Controllers;
use App\Actions\Students\CreateStudent;
use App\Http\Requests\StudentStoreRequest;
use App\Models\Audition;
use App\Models\AuditLogEntry;
use App\Models\Student;
use App\Rules\UniqueFullNameAtSchool;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
@ -31,24 +31,70 @@ class StudentController extends Controller
['students' => $students, 'auditions' => $auditions, 'shirtSizes' => $shirtSizes]);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*/
public function store(StudentStoreRequest $request)
public function store(Request $request)
{
$creator = app(CreateStudent::class);
/** @noinspection PhpUnhandledExceptionInspection */
$creator([
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'optional_data' => $request->optional_data,
if ($request->user()->cannot('create', Student::class)) {
abort(403);
}
$request->validate([
'first_name' => ['required'],
'last_name' => [
'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');
}
/**
* Display the specified resource.
*/
public function show(Request $request, Student $student)
{
//
}
/**
* Show the form for editing the specified resource.
*/
@ -66,17 +112,52 @@ class StudentController extends Controller
/**
* 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)) {
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([
'first_name' => $request['first_name'],
'last_name' => $request['last_name'],
'grade' => $request['grade'],
'optional_data' => $request->optional_data,
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
]);
$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.');
@ -90,7 +171,16 @@ class StudentController extends Controller
if ($request->user()->cannot('delete', $student)) {
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();
return redirect(route('students.index'));

View File

@ -73,19 +73,11 @@ class AdvancementController extends Controller
$entries = $ranker($audition, 'advancement');
$entries->load(['advancementVotes', 'totalScore', 'student.school']);
$unscoredEntries = $audition->entries()->where('for_advancement', true)->orderBy('draw_number')->get()->filter(function ($entry) {
return ! $entry->totalScore && ! $entry->hasFlag('no_show');
});
$noShowEntries = $audition->entries()->orderBy('draw_number')->get()->filter(function ($entry) {
return $entry->hasFlag('no_show');
});
$scoringComplete = $audition->entries->where('for_advancement', true)->every(function ($entry) {
$scoringComplete = $entries->every(function ($entry) {
return $entry->totalScore || $entry->hasFlag('no_show');
});
return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete', 'unscoredEntries', 'noShowEntries'));
return view('tabulation.advancement.ranking', compact('audition', 'entries', 'scoringComplete'));
}
public function setAuditionPassers(Request $request, Audition $audition)

View File

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

View File

@ -34,11 +34,7 @@ class DoublerDecisionController extends Controller
// $doublerEntry->addFlag('declined');
// }
// }
try {
$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;
$this->clearCache($entry);

View File

@ -9,9 +9,6 @@ use Illuminate\Http\Request;
use function to_route;
/**
* Used for tabulation enter noshow menu option
*/
class EntryFlagController extends Controller
{
public function noShowSelect()
@ -33,11 +30,11 @@ class EntryFlagController extends Controller
// If any results are published, get gone
if ($entry->audition->hasFlag('seats_published')) {
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')) {
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')) {
@ -46,12 +43,6 @@ class EntryFlagController extends Controller
$submitRouteName = 'entry-flags.undoNoShow';
$cardHeading = 'Undo No-Show';
$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 {
$formId = 'no-show-confirmation-form';
$buttonName = 'Confirm No Show';
@ -94,32 +85,21 @@ class EntryFlagController extends Controller
{
if ($entry->audition->hasFlag('seats_published')) {
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')) {
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('failed_prelim');
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)
{
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');
return redirect()->back()->with('success', 'Decline cleared');

View File

@ -2,21 +2,16 @@
namespace App\Http\Controllers\Tabulation;
use App\Actions\Tabulation\EnterPrelimScore;
use App\Actions\Tabulation\EnterScore;
use App\Exceptions\AuditionAdminException;
use App\Exceptions\ScoreEntryException;
use App\Http\Controllers\Controller;
use App\Models\Entry;
use App\Models\EntryTotalScore;
use App\Models\PrelimScoreSheet;
use App\Models\ScoreSheet;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Session;
/**
* Provides functionality for entering judge scores from the admin side.
*/
class ScoreController extends Controller
{
public function chooseEntry()
@ -30,6 +25,7 @@ class ScoreController extends Controller
public function destroyScore(ScoreSheet $score)
{
EntryTotalScore::where('entry_id', $score->entry_id)->delete();
if ($score->entry->audition->hasFlag('seats_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',
'Cannot delete scores for an entry where advancement is published');
}
EntryTotalScore::where('entry_id', $score->entry_id)->delete();
$score->delete();
return redirect()->back()->with('success', 'Score Deleted');
@ -46,7 +41,7 @@ class ScoreController extends Controller
public function entryScoreSheet(Request $request)
{
$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);
if ($publishedCheck) {
@ -63,31 +58,16 @@ class ScoreController extends Controller
}
$scoring_guide = $entry->audition->scoringGuide;
$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')) {
session()->flash('error',
'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',
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)
@ -96,32 +76,20 @@ class ScoreController extends Controller
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 = ScoreSheet::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) {
} catch (ScoreEntryException $e) {
return redirect()->route('scores.entryScoreSheet', ['entry_id' => $entry->id])
->with('error', $e->getMessage());
}
@ -133,50 +101,6 @@ class ScoreController extends Controller
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)
{
// We're not going to enter scores if seats are published

View File

@ -0,0 +1,282 @@
<?php
namespace App\Http\Controllers\Tabulation;
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', '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')
->withCount('scoreSheets')
->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'],
];
}
}
}
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]);
}
}

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