Merge pull request #25 from okorpheus/auditionadmin-20
Auditionadmin 20 - Bonus scores are fully functional Close #20
This commit is contained in:
commit
248a2ec4f9
|
|
@ -5,20 +5,27 @@
|
|||
namespace App\Actions\Tabulation;
|
||||
|
||||
use App\Exceptions\TabulationException;
|
||||
use App\Models\BonusScore;
|
||||
use App\Models\Entry;
|
||||
use App\Services\AuditionService;
|
||||
use App\Services\EntryService;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
use function auditionSetting;
|
||||
|
||||
class AllowForOlympicScoring implements CalculateEntryScore
|
||||
{
|
||||
protected CalculateScoreSheetTotal $calculator;
|
||||
|
||||
protected AuditionService $auditionService;
|
||||
|
||||
protected EntryService $entryService;
|
||||
|
||||
public function __construct(CalculateScoreSheetTotal $calculator, AuditionService $auditionService, EntryService $entryService)
|
||||
{
|
||||
public function __construct(
|
||||
CalculateScoreSheetTotal $calculator,
|
||||
AuditionService $auditionService,
|
||||
EntryService $entryService
|
||||
) {
|
||||
$this->calculator = $calculator;
|
||||
$this->auditionService = $auditionService;
|
||||
$this->entryService = $entryService;
|
||||
|
|
@ -28,6 +35,7 @@ class AllowForOlympicScoring implements CalculateEntryScore
|
|||
{
|
||||
|
||||
$cacheKey = 'entryScore-'.$entry->id.'-'.$mode;
|
||||
|
||||
return Cache::remember($cacheKey, 10, function () use ($mode, $entry) {
|
||||
$this->basicValidation($mode, $entry);
|
||||
$this->areAllJudgesIn($entry);
|
||||
|
|
@ -38,7 +46,7 @@ class AllowForOlympicScoring implements CalculateEntryScore
|
|||
|
||||
}
|
||||
|
||||
protected function getJudgeTotals($mode, Entry $entry)
|
||||
protected function getJudgeTotals($mode, Entry $entry): array
|
||||
{
|
||||
|
||||
$scores = [];
|
||||
|
|
@ -55,7 +63,7 @@ class AllowForOlympicScoring implements CalculateEntryScore
|
|||
// remove the highest and lowest scores
|
||||
array_pop($scores);
|
||||
array_shift($scores);
|
||||
}
|
||||
}
|
||||
$sums = [];
|
||||
// Sum each subscore from the judges
|
||||
foreach ($scores as $score) {
|
||||
|
|
@ -66,9 +74,35 @@ class AllowForOlympicScoring implements CalculateEntryScore
|
|||
$index++;
|
||||
}
|
||||
}
|
||||
// add the bonus points for a seating mode
|
||||
if ($mode === 'seating' && $sums) {
|
||||
|
||||
$sums[0] += $this->getBonusPoints($entry);
|
||||
}
|
||||
|
||||
return $sums;
|
||||
}
|
||||
|
||||
protected function getBonusPoints(Entry $entry)
|
||||
{
|
||||
|
||||
$bonusScoreDefinition = $entry->audition->bonusScore()->first();
|
||||
if (! $bonusScoreDefinition) {
|
||||
return 0;
|
||||
}
|
||||
/** @noinspection PhpPossiblePolymorphicInvocationInspection */
|
||||
$bonusJudges = $bonusScoreDefinition->judges;
|
||||
$bonusScoreSheets = BonusScore::where('entry_id', $entry->id)->get();
|
||||
foreach ($bonusScoreSheets as $sheet) {
|
||||
if (! $bonusJudges->contains($sheet->user_id)) {
|
||||
throw new TabulationException('Entry has a bonus score from unassigned judge');
|
||||
}
|
||||
}
|
||||
|
||||
// sum the score property of the $bonusScoreSheets
|
||||
return $bonusScoreSheets->sum('score');
|
||||
}
|
||||
|
||||
protected function basicValidation($mode, $entry): void
|
||||
{
|
||||
if ($mode !== 'seating' && $mode !== 'advancement') {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
/** @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
namespace App\Actions\Tabulation;
|
||||
|
||||
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);
|
||||
$this->basicValidations($judge, $entry);
|
||||
$this->validateJudgeValidity($judge, $entry, $score);
|
||||
$entries = $getRelatedEntries($entry);
|
||||
|
||||
// Create the score for each related entry
|
||||
foreach ($entries as $relatedEntry) {
|
||||
BonusScore::create([
|
||||
'entry_id' => $relatedEntry->id,
|
||||
'user_id' => $judge->id,
|
||||
'originally_scored_entry' => $entry->id,
|
||||
'score' => $score,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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 ScoreEntryException('That judge has already scored that entry');
|
||||
}
|
||||
|
||||
$bonusScore = $entry->audition->bonusScore->first();
|
||||
if (! $bonusScore->judges->contains($judge)) {
|
||||
throw new ScoreEntryException('That judge is not assigned to judge that bonus score');
|
||||
}
|
||||
if ($score > $bonusScore->max_score) {
|
||||
throw new ScoreEntryException('That score exceeds the maximum');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Actions\Tabulation;
|
||||
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
|
||||
class GetBonusScoreRelatedEntries
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function __invoke(Entry $entry): Collection
|
||||
{
|
||||
return $this->getRelatedEntries($entry);
|
||||
}
|
||||
|
||||
public 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Audition;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\User;
|
||||
use App\Rules\ValidateAuditionKey;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
use function redirect;
|
||||
use function to_route;
|
||||
|
||||
class BonusScoreDefinitionController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$bonusScores = BonusScoreDefinition::with('auditions')->get();
|
||||
// Set auditions equal to the collection of auditions that do not have a related bonus score
|
||||
$unassignedAuditions = Audition::orderBy('score_order')->doesntHave('bonusScore')->get();
|
||||
|
||||
return view('admin.bonus-scores.index', compact('bonusScores', 'unassignedAuditions'));
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$validData = request()->validate([
|
||||
'name' => 'required',
|
||||
'max_score' => 'required|numeric',
|
||||
'weight' => 'required|numeric',
|
||||
]);
|
||||
|
||||
BonusScoreDefinition::create($validData);
|
||||
|
||||
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Created');
|
||||
}
|
||||
|
||||
public function destroy(BonusScoreDefinition $bonusScore)
|
||||
{
|
||||
if ($bonusScore->auditions()->count() > 0) {
|
||||
return to_route('admin.bonus-scores.index')->with('error', 'Bonus Score has auditions attached');
|
||||
}
|
||||
$bonusScore->delete();
|
||||
|
||||
return to_route('admin.bonus-scores.index')->with('success', 'Bonus Score Deleted');
|
||||
}
|
||||
|
||||
public function assignAuditions(Request $request)
|
||||
{
|
||||
$validData = $request->validate([
|
||||
'bonus_score_id' => 'required|exists:bonus_score_definitions,id',
|
||||
'audition' => 'required|array',
|
||||
'audition.*' => ['required', new ValidateAuditionKey()],
|
||||
]);
|
||||
$bonusScore = BonusScoreDefinition::find($validData['bonus_score_id']);
|
||||
|
||||
foreach ($validData['audition'] as $auditionId => $value) {
|
||||
try {
|
||||
$bonusScore->auditions()->attach($auditionId);
|
||||
} catch (Exception) {
|
||||
return redirect()->route('admin.bonus-scores.index')->with('error',
|
||||
'Error assigning auditions to bonus score');
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('admin.bonus-scores.index')->with('success', 'Auditions assigned to bonus score');
|
||||
}
|
||||
|
||||
public function unassignAudition(Audition $audition)
|
||||
{
|
||||
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');
|
||||
}
|
||||
|
||||
public function judges()
|
||||
{
|
||||
$bonusScores = BonusScoreDefinition::all();
|
||||
$users = User::orderBy('last_name')->orderBy('first_name')->get();
|
||||
|
||||
return view('admin.bonus-scores.judge-assignments', compact('bonusScores', 'users'));
|
||||
}
|
||||
|
||||
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',
|
||||
]);
|
||||
$bonusScore->judges()->attach($validData['judge']);
|
||||
|
||||
return redirect()->route('admin.bonus-scores.judges')->with('success', 'Judge assigned to bonus score');
|
||||
}
|
||||
|
||||
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',
|
||||
]);
|
||||
$bonusScore->judges()->detach($validData['judge']);
|
||||
|
||||
return redirect()->route('admin.bonus-scores.judges')->with('success', 'Judge removed from bonus score');
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -17,7 +18,7 @@ class RoomController extends Controller
|
|||
if (! Auth::user()->is_admin) {
|
||||
abort(403);
|
||||
}
|
||||
$rooms = Room::with('auditions.entries','entries')->orderBy('name')->get();
|
||||
$rooms = Room::with('auditions.entries', 'entries')->orderBy('name')->get();
|
||||
|
||||
return view('admin.rooms.index', ['rooms' => $rooms]);
|
||||
}
|
||||
|
|
@ -27,8 +28,9 @@ class RoomController extends Controller
|
|||
$usersWithoutRooms = User::doesntHave('rooms')->orderBy('last_name')->orderBy('first_name')->get();
|
||||
$usersWithRooms = User::has('rooms')->orderBy('last_name')->orderBy('first_name')->get();
|
||||
$rooms = Room::with(['judges.school', 'auditions'])->get();
|
||||
$bonusScoresExist = BonusScoreDefinition::count() > 0;
|
||||
|
||||
return view('admin.rooms.judge_assignments', compact('usersWithoutRooms', 'usersWithRooms', 'rooms'));
|
||||
return view('admin.rooms.judge_assignments', compact('usersWithoutRooms', 'usersWithRooms', 'rooms', 'bonusScoresExist'));
|
||||
}
|
||||
|
||||
public function updateJudgeAssignment(Request $request, Room $room)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Judging;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BonusScore;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
use function redirect;
|
||||
|
||||
class BonusScoreEntryController extends Controller
|
||||
{
|
||||
public function __invoke(Entry $entry)
|
||||
{
|
||||
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');
|
||||
}
|
||||
/** @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 this entry');
|
||||
}
|
||||
$maxScore = $bonusScore->max_score;
|
||||
$bonusName = $bonusScore->name;
|
||||
|
||||
return view('judging.bonus_entry_score_sheet', compact('entry', 'maxScore', 'bonusName'));
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Judging;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Audition;
|
||||
use App\Models\BonusScore;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class BonusScoreEntryListController extends Controller
|
||||
{
|
||||
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 this bonus score');
|
||||
}
|
||||
$entries = $audition->entries()->orderBy('draw_number')->get();
|
||||
$entries = $entries->reject(fn ($entry) => $entry->hasFlag('no_show'));
|
||||
$entries->each(fn ($entry) => $entry->audition = $audition);
|
||||
|
||||
$scores = BonusScore::where('user_id', Auth::user()->id)
|
||||
->with('entry.audition')
|
||||
->with('originallyScoredEntry.audition')
|
||||
->get()
|
||||
->keyBy('entry_id');
|
||||
|
||||
return view('judging.bonus_score_entry_list', compact('audition', 'entries', 'scores'));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Judging;
|
||||
|
||||
use App\Actions\Tabulation\EnterBonusScore;
|
||||
use App\Exceptions\ScoreEntryException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Entry;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class BonusScoreRecordController extends Controller
|
||||
{
|
||||
public function __invoke(Entry $entry)
|
||||
{
|
||||
$enterBonusScore = App::make(EnterBonusScore::class);
|
||||
$validData = request()->validate([
|
||||
'score' => 'required|integer',
|
||||
]);
|
||||
try {
|
||||
$enterBonusScore(Auth::user(), $entry, $validData['score']);
|
||||
} catch (ScoreEntryException $ex) {
|
||||
return redirect()->back()->with('error', 'Score Entry Error - '.$ex->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->route('judging.bonusScore.EntryList', $entry->audition)->with('Score Recorded Successfully');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
namespace App\Http\Controllers\Judging;
|
||||
|
||||
use App\Actions\Tabulation\EnterScore;
|
||||
use App\Exceptions\AuditionServiceException;
|
||||
use App\Exceptions\ScoreEntryException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Audition;
|
||||
use App\Models\Entry;
|
||||
use App\Models\JudgeAdvancementVote;
|
||||
|
|
@ -30,10 +31,11 @@ class JudgingController extends Controller
|
|||
|
||||
public function index()
|
||||
{
|
||||
$rooms = Auth::user()->judgingAssignments;
|
||||
$rooms->load('auditions');
|
||||
$rooms = Auth::user()->judgingAssignments()->with('auditions')->get();
|
||||
$bonusScoresToJudge = Auth::user()->bonusJudgingAssignments()->with('auditions')->get();
|
||||
|
||||
return view('judging.index', compact('rooms'));
|
||||
//$rooms->load('auditions');
|
||||
return view('judging.index', compact('rooms', 'bonusScoresToJudge'));
|
||||
}
|
||||
|
||||
public function auditionEntryList(Request $request, Audition $audition)
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Tabulation;
|
||||
|
||||
use App\Actions\Tabulation\EnterBonusScore;
|
||||
use App\Actions\Tabulation\GetBonusScoreRelatedEntries;
|
||||
use App\Exceptions\ScoreEntryException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\BonusScore;
|
||||
use App\Models\Entry;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
use function request;
|
||||
|
||||
class BonusScoreController extends Controller
|
||||
{
|
||||
public function chooseEntry()
|
||||
{
|
||||
$method = 'GET';
|
||||
$formRoute = 'bonus-scores.entryBonusScoreSheet';
|
||||
$title = 'Enter Bonus Scores';
|
||||
|
||||
return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
|
||||
}
|
||||
|
||||
public function entryBonusScoreSheet(GetBonusScoreRelatedEntries $getRelatedEntries)
|
||||
{
|
||||
$validData = request()->validate([
|
||||
'entry_id' => 'required|exists:entries,id',
|
||||
]);
|
||||
$entry = Entry::find($validData['entry_id']);
|
||||
$bonusScoreDefinition = $entry->audition->bonusScore->first();
|
||||
$assignedJudges = $bonusScoreDefinition->judges;
|
||||
$relatedEntries = $getRelatedEntries($entry);
|
||||
$existingScores = [];
|
||||
foreach ($relatedEntries as $related) {
|
||||
$existingScores[$related->id] = BonusScore::where('entry_id', $related->id)
|
||||
->with('judge')
|
||||
->with('entry')
|
||||
->with('originallyScoredEntry')
|
||||
->get();
|
||||
}
|
||||
|
||||
return view('tabulation.bonus-score-sheet',
|
||||
compact('entry', 'bonusScoreDefinition', 'assignedJudges', 'existingScores', 'relatedEntries'));
|
||||
}
|
||||
|
||||
public function saveEntryBonusScoreSheet(Entry $entry, GetBonusScoreRelatedEntries $getRelatedEntries, EnterBonusScore $saveBonusScore)
|
||||
{
|
||||
$validData = request()->validate([
|
||||
'judge_id' => 'required|exists:users,id',
|
||||
'entry_id' => 'required|exists:entries,id',
|
||||
'score' => 'nullable|numeric',
|
||||
]);
|
||||
|
||||
$judge = User::find($validData['judge_id']);
|
||||
$entry = Entry::find($validData['entry_id']);
|
||||
$relatedEntries = $getRelatedEntries($entry);
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
|
||||
// Delete existing bonus scores for the entries by the judge
|
||||
foreach ($relatedEntries as $related) {
|
||||
BonusScore::where('entry_id', $related->id)->where('user_id', $judge->id)->delete();
|
||||
}
|
||||
|
||||
// If no score was submitted, were going to just stop at deleting the scores
|
||||
if (! $validData['score'] == null) {
|
||||
// Set the new score
|
||||
try {
|
||||
$saveBonusScore($judge, $entry, $validData['score']);
|
||||
} catch (ScoreEntryException $ex) {
|
||||
DB::rollBack();
|
||||
|
||||
return redirect()->route('bonus-scores.entryBonusScoreSheet',
|
||||
['entry_id' => $entry->id])->with('error', 'Error entering score - '.$ex->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
} catch (\Exception) {
|
||||
DB::rollBack();
|
||||
|
||||
return redirect()->route('bonus-scores.entryBonusScoreSheet', ['entry_id' => $entry->id])->with('error', 'Error entering score - '.$ex->getMessage());
|
||||
}
|
||||
|
||||
return redirect()->route('bonus-scores.entryBonusScoreSheet', ['entry_id' => $entry->id])->with('success', 'New bonus score entered');
|
||||
}
|
||||
|
||||
public function destroyBonusScore()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -15,8 +15,9 @@ class EntryFlagController extends Controller
|
|||
{
|
||||
$method = 'GET';
|
||||
$formRoute = 'entry-flags.confirmNoShow';
|
||||
$title = 'No Show';
|
||||
|
||||
return view('tabulation.choose_entry', compact('method', 'formRoute'));
|
||||
return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
|
||||
}
|
||||
|
||||
public function noShowConfirm(Request $request)
|
||||
|
|
|
|||
|
|
@ -14,8 +14,9 @@ class ScoreController extends Controller
|
|||
{
|
||||
$method = 'GET';
|
||||
$formRoute = 'scores.entryScoreSheet';
|
||||
$title = 'Enter Scores';
|
||||
|
||||
return view('tabulation.choose_entry', compact('method', 'formRoute'));
|
||||
return view('tabulation.choose_entry', compact('method', 'formRoute', 'title'));
|
||||
}
|
||||
|
||||
public function destroyScore(ScoreSheet $score)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,11 @@ class Audition extends Model
|
|||
return $this->belongsTo(ScoringGuide::class);
|
||||
}
|
||||
|
||||
public function bonusScore(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(BonusScoreDefinition::class, 'bonus_score_audition_assignment');
|
||||
}
|
||||
|
||||
public function display_fee(): string
|
||||
{
|
||||
return '$'.number_format($this->entry_fee / 100, 2);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class BonusScore extends Model
|
||||
{
|
||||
protected $guarded = [];
|
||||
|
||||
public function entry(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Entry::class);
|
||||
}
|
||||
|
||||
public function judge(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'user_id');
|
||||
}
|
||||
|
||||
public function originallyScoredEntry(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Entry::class, 'originally_scored_entry');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class BonusScoreDefinition extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = ['name', 'max_score', 'weight'];
|
||||
|
||||
public function auditions(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Audition::class, 'bonus_score_audition_assignment')->orderBy('score_order');
|
||||
}
|
||||
|
||||
public function judges(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(User::class, 'bonus_score_judge_assignment');
|
||||
}
|
||||
}
|
||||
|
|
@ -7,10 +7,10 @@ use Illuminate\Database\Eloquent\Builder;
|
|||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class Entry extends Model
|
||||
{
|
||||
|
|
@ -55,6 +55,11 @@ class Entry extends Model
|
|||
|
||||
}
|
||||
|
||||
public function bonusScores(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(BonusScore::class);
|
||||
}
|
||||
|
||||
public function advancementVotes(): HasMany
|
||||
{
|
||||
return $this->hasMany(JudgeAdvancementVote::class);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
|||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Collection;
|
||||
use App\Models\ScoreSheet;
|
||||
|
||||
class User extends Authenticatable implements MustVerifyEmail
|
||||
{
|
||||
|
|
@ -118,6 +117,11 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
return $this->rooms();
|
||||
}
|
||||
|
||||
public function bonusJudgingAssignments(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(BonusScoreDefinition::class, 'bonus_score_judge_assignment');
|
||||
}
|
||||
|
||||
public function advancementVotes(): HasMany
|
||||
{
|
||||
return $this->hasMany(JudgeAdvancementVote::class);
|
||||
|
|
@ -125,7 +129,7 @@ class User extends Authenticatable implements MustVerifyEmail
|
|||
|
||||
public function isJudge(): bool
|
||||
{
|
||||
return $this->judgingAssignments()->count() > 0;
|
||||
return $this->judgingAssignments()->count() > 0 || $this->bonusJudgingAssignments()->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ValidateAuditionKey implements ValidationRule
|
||||
{
|
||||
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||
{
|
||||
// Extract the key from the attribute
|
||||
$key = explode('.', $attribute)[1];
|
||||
if (! DB::table('auditions')->where('id', $key)->exists()) {
|
||||
$fail('Invalid audition id provided');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\BonusScoreDefinition>
|
||||
*/
|
||||
class BonusScoreDefinitionFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->word,
|
||||
'max_score' => $this->faker->randomNumber(2),
|
||||
'weight' => $this->faker->randomFloat(2, 0, 2),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bonus_score_definitions', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->integer('max_score');
|
||||
$table->float('weight');
|
||||
$table->boolean('for_seating')->default(true);
|
||||
$table->boolean('for_attendance')->default(false);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bonus_score_definitions');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Audition;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bonus_score_audition_assignment', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(BonusScoreDefinition::class)
|
||||
->constrained('bonus_score_definitions', 'id', 'bs_audition_assignment_bonus_score_definition_id')
|
||||
->onDelete('cascade')->onUpdate('cascade');
|
||||
$table->foreignIdFor(Audition::class)->unique()
|
||||
->constrained()->onDelete('cascade')->onUpdate('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bonus_score_audition_assignment');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Entry;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bonus_scores', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(Entry::class);
|
||||
$table->foreignIdFor(User::class);
|
||||
$table->foreignId('originally_scored_entry')->nullable()->constrained('entries')->nullOnDelete();
|
||||
$table->integer('score');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bonus_scores');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('bonus_score_judge_assignment', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignIdFor(BonusScoreDefinition::class)
|
||||
->constrained('bonus_score_definitions', 'id', 'bs_judge_assignment_bonus_score_definition_id')
|
||||
->onDelete('cascade')->onUpdate('cascade');
|
||||
$table->foreignIdFor(User::class)
|
||||
->constrained()->onDelete('cascade')->onUpdate('cascade');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('bonus_score_judge_assignment');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<x-modal-body showVar="showAddAuditionModal">
|
||||
<x-slot:title>Add auditions to <span x-text="addAuditionToName"></span></x-slot:title>
|
||||
<x-form.form method="POST" action="{{ route('admin.bonus-scores.addAuditions') }}">
|
||||
<input type="hidden" name=bonus_score_id x-bind:value="addAuditionTo">
|
||||
<div class="grid grid-cols-3">
|
||||
@foreach($unassignedAuditions as $audition)
|
||||
<div class="mx-5 my-1">
|
||||
<x-form.checkbox name="audition[{{$audition->id}}]" label="{{ $audition->name }}" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<x-form.button class="mt-3" type="submit">Add Checked Auditions</x-form.button>
|
||||
</x-form.form>
|
||||
|
||||
</x-modal-body>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<x-modal-body show-var="showAddBonusScoreModal">
|
||||
<x-slot:title>
|
||||
Add Bonus Score
|
||||
</x-slot:title>
|
||||
<x-form.form id="create-bonus-score-form" action="{{ route('admin.bonus-scores.store') }}">
|
||||
<x-form.body-grid columns="12">
|
||||
<x-form.field name="name" label_text="Name" colspan="8" />
|
||||
<x-form.field name="max_score" type="number" label_text="Max Points" colspan="2" />
|
||||
<x-form.field name="weight" label_text="Weight" colspan="2" />
|
||||
<div class="col-start-9 col-span-4 row-start-2">
|
||||
<x-form.button >Create Bonus Score</x-form.button>
|
||||
</div>
|
||||
|
||||
</x-form.body-grid>
|
||||
</x-form.form>
|
||||
</x-modal-body>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<x-help-modal title="Bonus Score Definitions">
|
||||
<p class="mb-5">Bonus scores are most often used for an improvisation score for jazz band auditions. A bonus score
|
||||
earned by an entry will be directly added
|
||||
to that entries final score. When you create a bonus score, you will also specify to which auditions that bonus
|
||||
score should apply. When a student
|
||||
earns a bonus score for one entry, that bonus will be applied to all entries that receive that bonus score.</p>
|
||||
|
||||
<p class="mb-5">
|
||||
Let's say you create a bonus score called, "Saxophone Improvisation," and assign Jazz Alto, Jazz Tenor, and Jazz
|
||||
Bari auditions to that bonus
|
||||
score. If a student is entered on all three saxes, when they receive an improv score on one sax, that score will
|
||||
apply to all 3. The system
|
||||
will not allow another improv score to be assigned by the same judge unless the first one is deleted. If you
|
||||
want that student to improv on each instrument
|
||||
separately, you will need to create a separate bonus score for each instrument.
|
||||
</p>
|
||||
|
||||
<P>
|
||||
The weight allows you to control how much influence the bonus score has on the outcome of the audition. The
|
||||
bonus score is
|
||||
multiplied by the weight then added to the final score. The weight may be any positive number, including
|
||||
decimals.
|
||||
</P>
|
||||
</x-help-modal>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<div class="text-center">
|
||||
<svg class="mx-auto w-12 h-12 text-gray-400 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z"/>
|
||||
</svg>
|
||||
|
||||
|
||||
<h3 class="mt-2 text-sm font-semibold text-gray-900">No bonus scores have been created</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">Get started by creating a new bonus score.</p>
|
||||
<div class="mt-6">
|
||||
<button type="button"
|
||||
x-on:click="showAddBonusScoreModal = true"
|
||||
class="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
|
||||
<svg class="-ml-0.5 mr-1.5 h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"/>
|
||||
</svg>
|
||||
New Bonus Score
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<x-layout.app x-data="{ showAddBonusScoreModal: false, addAuditionTo: null, showAddAuditionModal: false, addAuditionToName: null }">
|
||||
<x-slot:page_title>Bonus Score Management</x-slot:page_title>
|
||||
<x-slot:title_bar_right>
|
||||
@include('admin.bonus-scores.index-help-modal')
|
||||
</x-slot:title_bar_right>
|
||||
@if($bonusScores->count() === 0)
|
||||
@include('admin.bonus-scores.index-no-bonus-scores-message')
|
||||
@endif
|
||||
|
||||
@foreach($bonusScores as $bonusScore)
|
||||
<x-card.card class="mx-auto max-w-xl mb-5">
|
||||
<x-card.heading>
|
||||
|
||||
{{ $bonusScore->name }}
|
||||
<x-slot:subheading>
|
||||
Max Points: {{ $bonusScore->max_score }} | Weight: {{ $bonusScore->weight }}
|
||||
</x-slot:subheading>
|
||||
<x-slot:right_side>
|
||||
@if($bonusScore->auditions()->count() === 0)
|
||||
<x-delete-resource-modal title="Delete Bonus Score" action="{{route('admin.bonus-scores.destroy', $bonusScore)}}">
|
||||
Confirm you want to delete the bonus score {{ $bonusScore->name }}
|
||||
</x-delete-resource-modal>
|
||||
@endif
|
||||
</x-slot:right_side>
|
||||
</x-card.heading>
|
||||
<div class="grid grid-cols-3 mx-5 my-2">
|
||||
@foreach($bonusScore->auditions as $audition)
|
||||
<div class="flex gap-x-2">
|
||||
<form method="post" id="unassign{{$audition->id}}" action="{{ route('admin.bonus-scores.unassignAudition', $audition) }}">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit">
|
||||
<x-icons.circled-x color="crimson" />
|
||||
</button>
|
||||
</form>
|
||||
{{ $audition->name }}
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
<x-form.button
|
||||
x-on:click="showAddAuditionModal=true; addAuditionTo={{ $bonusScore->id }}; addAuditionToName='{{ $bonusScore->name }}'"
|
||||
class="mx-auto max-w-sm mb-3">
|
||||
Add Auditions to {{ $bonusScore->name }}
|
||||
</x-form.button>
|
||||
</x-card.card>
|
||||
@endforeach
|
||||
@if($bonusScores->count() !== 0)
|
||||
<x-form.button class="mx-auto max-w-xs mt-5" x-on:click="showAddBonusScoreModal=true">Add Bonus Score</x-form.button>
|
||||
@endif
|
||||
@include('admin.bonus-scores.index-add-auditions-to-bonus-modal')
|
||||
@include('admin.bonus-scores.index-add-bonus-score-modal')
|
||||
</x-layout.app>
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
<x-layout.app>
|
||||
<div class="bg-white pt-3 pb-1 px-3 rounded-md">
|
||||
<div class="mb-3">
|
||||
<nav class="flex space-x-4" aria-label="Tabs">
|
||||
<!-- Current: "bg-indigo-100 text-indigo-700", Default: "text-gray-500 hover:text-gray-700" -->
|
||||
<a href="{{route('admin.rooms.judgingAssignment')}}" class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700">Room Judges</a>
|
||||
<a href="{{route('admin.bonus-scores.judges')}}" class="rounded-md px-3 py-2 text-sm font-medium bg-indigo-100 text-indigo-700">Bonus Judges</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="grid md:grid-cols-4 gap-5 mt-5">
|
||||
|
||||
@foreach($bonusScores as $bonusScore)
|
||||
<li id="bonus-{{$bonusScore->id}}-card" class=" rounded-xl border border-gray-200 bg-gray-50 "> {{-- card wrapper --}}
|
||||
<div class="flex items-center gap-x-4 border-b border-gray-900/5 bg-white pt-2 pb-6 px-6"> {{-- card header --}}
|
||||
<div class="text-sm font-medium leading-6 text-gray-900">
|
||||
<p class="text-sm font-medium leading-6 text-gray-900">{{ $bonusScore->name }}</p>
|
||||
</div>
|
||||
|
||||
<div class="relative ml-auto" x-data="{ open: false }"> {{-- Auditions Dropdown --}}
|
||||
<button type="button"
|
||||
class="-m-2.5 block p-2.5 text-gray-400 hover:text-gray-500"
|
||||
id="options-menu-0-button"
|
||||
aria-expanded="false"
|
||||
aria-haspopup="true"
|
||||
x-on:click="open = ! open">
|
||||
<span class="sr-only">Open details</span>
|
||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path
|
||||
d="M3 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM8.5 10a1.5 1.5 0 113 0 1.5 1.5 0 01-3 0zM15.5 8.5a1.5 1.5 0 100 3 1.5 1.5 0 000-3z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!--
|
||||
Dropdown menu, show/hide based on menu state.
|
||||
|
||||
Entering: "transition ease-out duration-100"
|
||||
From: "transform opacity-0 scale-95"
|
||||
To: "transform opacity-100 scale-100"
|
||||
Leaving: "transition ease-in duration-75"
|
||||
From: "transform opacity-100 scale-100"
|
||||
To: "transform opacity-0 scale-95"
|
||||
-->
|
||||
<div
|
||||
class="absolute right-5 -top-4 z-10 mt-0.5 w-32 origin-top-right rounded-md bg-white py-0.5 shadow-lg ring-1 ring-gray-900/5 focus:outline-none overflow-y-auto max-h-64"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="options-menu-0-button"
|
||||
tabindex="-1"
|
||||
x-show="open"
|
||||
x-cloak>
|
||||
|
||||
<!-- Active: "bg-gray-50", Not Active: "" -->
|
||||
@foreach($bonusScore->auditions as $audition)
|
||||
<p class="block px-3 py-0.5 text-xs leading-6 text-gray-900">{{ $audition->name }}</p>
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div> {{-- End Card Header --}}
|
||||
|
||||
|
||||
<dl class="-my-3 divide-y divide-gray-100 px-6 pb-4 pt-1 text-sm leading-6 bg-gray-50"> {{-- Judge Listing --}}
|
||||
@foreach($bonusScore->judges as $judge)
|
||||
<div class="flex justify-between items-center gap-x-4 py-1"> {{-- Judge Line --}}
|
||||
<dt>
|
||||
<p>
|
||||
<span class="text-gray-700">{{ $judge->full_name() }} </span>
|
||||
<span class="text-gray-500 text-xs">{{ $judge->school->name ?? '' }}</span>
|
||||
</p>
|
||||
<p class="text-gray-500 text-xs">{{ $judge->judging_preference }}</p>
|
||||
</dt>
|
||||
<dd class="text-gray-500 text-xs">
|
||||
<form method="POST" action="{{route('admin.bonus-scores.judges.remove', $bonusScore) }}" id="removeJudgeFromRoom{{ $bonusScore->id }}">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<input type="hidden" name="judge" value="{{ $judge->id }}">
|
||||
<button>
|
||||
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="#d1d5db" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 9-6 6m0-6 6 6m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div class="pt-3"> {{-- Add Judge Form --}}
|
||||
<form method="POST" action="{{route('admin.bonus-scores.judges.assign', $bonusScore)}}" id="assignJudgeToRoom{{ $bonusScore->id }}">
|
||||
@csrf
|
||||
<select name="judge"
|
||||
id="judge"
|
||||
class="block w-full rounded-md border-0 py-1.5 pl-3 pr-10 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-indigo-600 sm:text-sm sm:leading-6"
|
||||
onchange="document.getElementById('assignJudgeToRoom{{ $bonusScore->id }}').submit()">
|
||||
<option>Add a judge</option>
|
||||
@foreach($users as $judge)
|
||||
@if($bonusScore->judges->contains($judge->id))
|
||||
@continue
|
||||
@endif
|
||||
<option value="{{ $judge->id }}">{{ $judge->full_name() }}
|
||||
- {{ $judge->judging_preference }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</x-layout.app>
|
||||
|
|
@ -1,5 +1,17 @@
|
|||
<x-layout.app>
|
||||
<ul class="grid md:grid-cols-4 gap-5">
|
||||
@if($bonusScoresExist)
|
||||
<div class="bg-white pt-3 pb-1 px-3 rounded-md">
|
||||
<div class="mb-3">
|
||||
<nav class="flex space-x-4" aria-label="Tabs">
|
||||
<!-- Current: "bg-indigo-100 text-indigo-700", Default: "text-gray-500 hover:text-gray-700" -->
|
||||
<a href="{{route('admin.rooms.judgingAssignment')}}" class="rounded-md px-3 py-2 text-sm font-medium bg-indigo-100 text-indigo-700">Room Judges</a>
|
||||
<a href="{{route('admin.bonus-scores.judges')}}" class="rounded-md px-3 py-2 text-sm font-medium text-gray-500 hover:text-gray-700">Bonus Judges</a>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<ul class="grid md:grid-cols-4 gap-5 mt-5">
|
||||
|
||||
@foreach($rooms as $room)
|
||||
@if($room->id == 0)
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
'type' => 'text',
|
||||
'label' => false,
|
||||
'colspan' => '1',
|
||||
'label_text' => false
|
||||
'label_text' => false,
|
||||
'id'=>null
|
||||
])
|
||||
@php
|
||||
$label_classes = "block text-sm font-medium leading-6 text-gray-900";
|
||||
$inputClasses = "block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6";
|
||||
$inputAttributes = [
|
||||
'id' => $name,
|
||||
'id' => $id ?? $name,
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'class' => $inputClasses,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
@props(['color' => 'currentColor', 'title'=>false])
|
||||
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="{{$color}}" viewBox="0 0 24 24">
|
||||
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm7.707-3.707a1 1 0 0 0-1.414 1.414L10.586 12l-2.293 2.293a1 1 0 1 0 1.414 1.414L12 13.414l2.293 2.293a1 1 0 0 0 1.414-1.414L13.414 12l2.293-2.293a1 1 0 0 0-1.414-1.414L12 10.586 9.707 8.293Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
|
|
@ -26,6 +26,7 @@
|
|||
<x-layout.navbar.menus.menu-item :href="route('admin.ensembles.index')">Ensembles</x-layout.navbar.menus.menu-item>
|
||||
<x-layout.navbar.menus.menu-item :href="route('admin.ensembles.seatingLimits')">Seating Limits</x-layout.navbar.menus.menu-item>
|
||||
<x-layout.navbar.menus.menu-item :href="route('admin.scoring.index')">Scoring</x-layout.navbar.menus.menu-item>
|
||||
<x-layout.navbar.menus.menu-item :href="route('admin.bonus-scores.index')">Bonus Scores</x-layout.navbar.menus.menu-item>
|
||||
<x-layout.navbar.menus.menu-item :href="route('admin.rooms.index')">Rooms</x-layout.navbar.menus.menu-item>
|
||||
<x-layout.navbar.menus.menu-item :href="route('admin.rooms.judgingAssignment')">Judges</x-layout.navbar.menus.menu-item>
|
||||
<x-layout.navbar.menus.menu-item :href="route('admin.draw.index')">Run Draw</x-layout.navbar.menus.menu-item>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
<div class="absolute left-1/2 z-10 mt-5 flex w-screen max-w-min -translate-x-1/2 px-4" x-show="open" x-cloak>
|
||||
<div class="w-56 shrink rounded-xl bg-white p-4 text-sm font-semibold leading-6 text-gray-900 shadow-lg ring-1 ring-gray-900/5">
|
||||
<a href="{{ route('scores.chooseEntry') }}" class="block p-2 hover:text-indigo-600">Enter Scores</a>
|
||||
<a href="{{ route('bonus-scores.chooseEntry') }}" class="block p-2 hover:text-indigo-600">Enter Bonus Scores</a>
|
||||
<a href="{{ route('entry-flags.noShowSelect') }}" class="block p-2 hover:text-indigo-600">Enter No-Shows</a>
|
||||
<a href="{{ route('seating.status') }}" class="block p-2 hover:text-indigo-600">Audition Status</a>
|
||||
<a href="{{ route('advancement.status') }}" class="block p-2 hover:text-indigo-600">{{ auditionSetting('advanceTo') }} Status</a>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
@props(['title'=>false])
|
||||
@props(['title'=>false, 'showVar'=>'showModal'])
|
||||
<div
|
||||
class="fixed inset-0 z-30 flex items-center justify-center overflow-auto bg-black bg-opacity-50"
|
||||
x-show="showModal" x-cloak
|
||||
x-show="{{ $showVar }}" x-cloak
|
||||
>
|
||||
<!-- Modal inner -->
|
||||
<div
|
||||
class="max-w-3xl px-6 py-4 mx-auto text-left bg-white rounded shadow-lg"
|
||||
@click.away="showModal = false"
|
||||
@click.away="{{ $showVar }} = false"
|
||||
x-transition:enter="motion-safe:ease-out duration-300"
|
||||
x-transition:enter-start="opacity-0 scale-90"
|
||||
x-transition:enter-end="opacity-100 scale-100"
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
<h5 {{ $title->attributes->merge(['class' => 'mr-3 text-black max-w-none']) }}>{{ $title ?? '' }}</h5>
|
||||
@endif
|
||||
|
||||
<button type="button" class="z-50 cursor-pointer" @click="showModal = false">
|
||||
<button type="button" class="z-50 cursor-pointer" @click="{{ $showVar}} = false">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<div>
|
||||
@if($with_title_area)
|
||||
<div class="mb-4 mt-4 sm:px 4 sm:flex sm:items-center">
|
||||
<div class="mb-2 mt-4 sm:px 4 sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto sm:items-center">
|
||||
@if($title)<h1 {{ $title->attributes->merge(['class' => 'text-base font-semibold leading-6 text-gray-900']) }}>{{ $title }}</h1>@endif
|
||||
@if($subtitle)<p {{ $subtitle->attributes->merge(['class' => 'mt-2 text-sm text-gray-700']) }}>{{ $subtitle }}</p>@endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
<x-layout.app>
|
||||
<x-slot:page_title>Enter {{ $bonusName }} Score</x-slot:page_title>
|
||||
<x-card.card class="mx-auto max-w-sm">
|
||||
<x-card.heading>{{ $entry->audition->name }} {{$entry->draw_number}}</x-card.heading>
|
||||
<x-form.form method="POST"
|
||||
id="score-entry-for-{{$entry->id}}"
|
||||
action="{{route('judging.bonusScores.recordScore', $entry)}}">
|
||||
<x-form.field name="score"
|
||||
class="mb-5"
|
||||
label_text="{{ $bonusName }} Score"
|
||||
type="number"
|
||||
max="{{ $maxScore }}"/>
|
||||
<x-form.button type="submit" class="mb-5">Enter {{ $bonusName }} Score</x-form.button>
|
||||
</x-form.form>
|
||||
</x-card.card>
|
||||
</x-layout.app>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
@php use Carbon\Carbon; @endphp
|
||||
<x-layout.app>
|
||||
<x-slot:page_title>Judging Dashboard - Bonus Scores</x-slot:page_title>
|
||||
<x-table.table>
|
||||
<x-slot:title>{{ $audition->name }} - Bonus Score</x-slot:title>
|
||||
<x-table.body>
|
||||
<thead>
|
||||
<tr>
|
||||
<x-table.th>Entry</x-table.th>
|
||||
<x-table.th>Score</x-table.th>
|
||||
<x-table.th>Scored On</x-table.th>
|
||||
<x-table.th>Score Timestamp</x-table.th>
|
||||
</tr>
|
||||
</thead>
|
||||
<x-table.body>
|
||||
@foreach($entries as $entry)
|
||||
<tr>
|
||||
@if($scores->has($entry->id))
|
||||
<x-table.td>{{ $audition->name }} {{ $entry->draw_number }}</x-table.td>
|
||||
<x-table.td>{{ $scores[$entry->id]->score }}</x-table.td>
|
||||
<x-table.td>{{ $scores[$entry->id]->originallyScoredEntry->audition->name }}</x-table.td>
|
||||
<x-table.td>{{ Carbon::create($scores[$entry->id]->created_at)->setTimezone('America/Chicago')->format('m/d/y H:i') }}</x-table.td>
|
||||
@else
|
||||
|
||||
<x-table.td>
|
||||
<a href="{{ route('judging.bonusScore.entry', $entry) }}">
|
||||
{{ $audition->name }} {{ $entry->draw_number }}
|
||||
</a>
|
||||
</x-table.td>
|
||||
|
||||
@endif
|
||||
</tr>
|
||||
@endforeach
|
||||
</x-table.body>
|
||||
</x-table.body>
|
||||
</x-table.table>
|
||||
</x-layout.app>
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
<x-layout.app>
|
||||
<x-slot:page_title>Judging Dashboard</x-slot:page_title>
|
||||
|
||||
<h2 class="overflow-hidden mx-auto max-w-md py-3 px-1 text-base font-semibold leading-7 text-gray-900">Choose auditon to judge</h2>
|
||||
<h2 class="overflow-hidden mx-auto max-w-md py-3 px-1 text-base font-semibold leading-7 text-gray-900">Choose
|
||||
audition to judge</h2>
|
||||
|
||||
@foreach($rooms as $room)
|
||||
<x-card.card class="mx-auto max-w-md mb-3">
|
||||
|
|
@ -16,5 +17,20 @@
|
|||
</x-card.card>
|
||||
@endforeach
|
||||
|
||||
@foreach($bonusScoresToJudge as $bonusScore)
|
||||
<x-card.card class="mx-auto max-w-md mb-3">
|
||||
<x-card.heading>{{ $bonusScore->name }}</x-card.heading>
|
||||
<x-card.list.body>
|
||||
@foreach($bonusScore->auditions as $audition)
|
||||
<a href="{{ route('judging.bonusScore.EntryList', $audition) }}">
|
||||
<x-card.list.row class="!py-3 ml-3">
|
||||
{{ $audition->name }}
|
||||
</x-card.list.row>
|
||||
</a>
|
||||
@endforeach
|
||||
</x-card.list.body>
|
||||
</x-card.card>
|
||||
@endforeach
|
||||
|
||||
|
||||
</x-layout.app>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
@php use Illuminate\Support\Carbon; @endphp
|
||||
<x-layout.app>
|
||||
<x-slot:page_title>Enter {{ $bonusScoreDefinition->name }} Score</x-slot:page_title>
|
||||
<x-card.card>
|
||||
<x-card.heading>
|
||||
{{ $entry->student->full_name() }}
|
||||
<x-slot:subheading>{{ $entry->student->school->name }}</x-slot:subheading>
|
||||
</x-card.heading>
|
||||
</x-card.card>
|
||||
<div class="grid grid-cols-3 gap-3 w-full mt-5">
|
||||
|
||||
<x-card.card class="col-span-2">
|
||||
<x-card.heading>Existing Scores</x-card.heading>
|
||||
<div class="px-6">
|
||||
@foreach($relatedEntries as $related)
|
||||
<x-table.table class="mb-8 ">
|
||||
<x-slot:title class="border rounded-md pl-3 bg-gray-100">
|
||||
{{ $related->audition->name }} #{{ $related->draw_number }}
|
||||
</x-slot:title>
|
||||
<x-slot:subtitle>
|
||||
Entry ID: {{ $related->id }}
|
||||
</x-slot:subtitle>
|
||||
<thead>
|
||||
<tr>
|
||||
<x-table.th>Judge</x-table.th>
|
||||
<x-table.th>Audition Scored</x-table.th>
|
||||
<x-table.th>Score</x-table.th>
|
||||
<x-table.th>Timestamp</x-table.th>
|
||||
</tr>
|
||||
</thead>
|
||||
<x-table.body>
|
||||
@foreach($existingScores[$related->id] as $score)
|
||||
<tr>
|
||||
<td>{{ $score->judge->full_name() }}</td>
|
||||
<td>{{ $score->originallyScoredEntry->audition->name }}</td>
|
||||
<td>{{ $score->score }}</td>
|
||||
<td>{{ Carbon::create($score->created_at)->setTimezone('America/Chicago')->format('m/d/y H:i') }}</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</x-table.body>
|
||||
</x-table.table>
|
||||
@endforeach
|
||||
</div>
|
||||
</x-card.card>
|
||||
|
||||
<x-card.card >
|
||||
<x-card.heading>Enter Score</x-card.heading>
|
||||
<div class="mx-5 border-b text-sm">
|
||||
<p class="mb-3">NOTE: Entering score will delete any existing scores for that entry by that judge</p>
|
||||
<p>Submitting the form with no score value will delete scores by that judge</p>
|
||||
</div>
|
||||
<x-form.form class="my-3" method="POST" action="{{route('bonus-scores.saveEntryBonusScoreSheet', $entry)}}" x-data="{ judgeChanged: false }">
|
||||
<x-form.select name="judge_id" class="mb-5" required x-on:change="judgeChanged=true">
|
||||
<x-slot:label>Judge</x-slot:label>
|
||||
<option value="" x-bind:disabled="judgeChanged">Choose Judge</option>
|
||||
@foreach($assignedJudges as $judge)
|
||||
<option value="{{ $judge->id }}">{{ $judge->full_name() }}</option>
|
||||
@endforeach
|
||||
</x-form.select>
|
||||
<x-form.select name="entry_id" class="mb-5" required>
|
||||
<x-slot:label>Scored Audition</x-slot:label>
|
||||
@foreach($relatedEntries as $related)
|
||||
<option value="{{$related->id}}" {{$related->id == $entry->id ? 'selected':' '}}>{{ $related->audition->name }}</option>
|
||||
@endforeach
|
||||
</x-form.select>
|
||||
<x-form.field label_text="Score" name="score" type="number" max="{{ $bonusScoreDefinition->max_score }}"/>
|
||||
<x-form.button class="mt-5" x-show="judgeChanged">Enter Score</x-form.button>
|
||||
</x-form.form>
|
||||
</x-card.card>
|
||||
</div>
|
||||
</x-layout.app>
|
||||
|
|
@ -2,13 +2,14 @@
|
|||
/**
|
||||
* @var string $method Method for the select form
|
||||
* @var string $formRoute Route for the form action. Should be a route name
|
||||
* @var string $title Title of the page
|
||||
*/
|
||||
@endphp
|
||||
|
||||
<x-layout.app>
|
||||
<x-slot:page_title>Choose Entry</x-slot:page_title>
|
||||
<x-slot:page_title>{{ $title }}</x-slot:page_title>
|
||||
<x-card.card class="mx-auto max-w-sm">
|
||||
<x-card.heading>Choose Entry</x-card.heading>
|
||||
<x-card.heading>{{ $title }} - Choose Entry</x-card.heading>
|
||||
<div class="">
|
||||
<x-form.form method="{{ $method }}" action="{{ route($formRoute) }}" class="mb-4 mt-3" id="entry-select-form">
|
||||
<x-form.field name="entry_id" label_text="Entry ID"></x-form.field>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,51 @@
|
|||
<?php
|
||||
|
||||
// Admin Routes
|
||||
use App\Http\Controllers\Admin\AuditionController;
|
||||
use App\Http\Controllers\Admin\AuditionSettings;
|
||||
use App\Http\Controllers\Admin\BonusScoreDefinitionController;
|
||||
use App\Http\Controllers\Admin\DrawController;
|
||||
use App\Http\Controllers\Admin\EnsembleController;
|
||||
use App\Http\Controllers\Admin\EntryController;
|
||||
use App\Http\Controllers\Admin\EventController;
|
||||
use App\Http\Controllers\Admin\RoomController;
|
||||
use App\Http\Controllers\Admin\SchoolController;
|
||||
use App\Http\Controllers\Admin\ScoringGuideController;
|
||||
use App\Http\Controllers\Admin\StudentController;
|
||||
use App\Http\Controllers\Admin\UserController;
|
||||
use App\Http\Middleware\CheckIfAdmin;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->group(function () {
|
||||
Route::view('/', 'admin.dashboard')->name('admin.dashboard');
|
||||
|
||||
Route::post('/auditions/roomUpdate', [\App\Http\Controllers\Admin\AuditionController::class, 'roomUpdate']); // Endpoint for JS assigning auditions to rooms
|
||||
Route::post('/scoring/assign_guide_to_audition', [\App\Http\Controllers\Admin\AuditionController::class, 'scoringGuideUpdate'])->name('ajax.assignScoringGuideToAudition'); // Endpoint for JS assigning scoring guides to auditions
|
||||
Route::post('/auditions/roomUpdate', [
|
||||
AuditionController::class, 'roomUpdate',
|
||||
]); // Endpoint for JS assigning auditions to rooms
|
||||
Route::post('/scoring/assign_guide_to_audition', [
|
||||
AuditionController::class, 'scoringGuideUpdate',
|
||||
])->name('ajax.assignScoringGuideToAudition'); // Endpoint for JS assigning scoring guides to auditions
|
||||
|
||||
Route::get('/settings', [\App\Http\Controllers\Admin\AuditionSettings::class, 'index'])->name('audition-settings');
|
||||
Route::post('/settings', [\App\Http\Controllers\Admin\AuditionSettings::class, 'save'])->name('audition-settings-save');
|
||||
Route::get('/settings', [AuditionSettings::class, 'index'])->name('audition-settings');
|
||||
Route::post('/settings',
|
||||
[AuditionSettings::class, 'save'])->name('audition-settings-save');
|
||||
|
||||
// Admin Bonus Scores Routes
|
||||
Route::prefix('bonus-scores')->controller(BonusScoreDefinitionController::class)->group(function (
|
||||
) {
|
||||
Route::get('/', 'index')->name('admin.bonus-scores.index');
|
||||
Route::post('/', 'store')->name('admin.bonus-scores.store');
|
||||
Route::post('/assign_auditions', 'assignAuditions')->name('admin.bonus-scores.addAuditions');
|
||||
Route::delete('/{audition}/unassign_audition', 'unassignAudition')->name('admin.bonus-scores.unassignAudition');
|
||||
Route::delete('/{bonusScore}', 'destroy')->name('admin.bonus-scores.destroy');
|
||||
Route::get('/judges', 'judges')->name('admin.bonus-scores.judges');
|
||||
Route::delete('{bonusScore}/judges/', 'removeJudge')->name('admin.bonus-scores.judges.remove');
|
||||
Route::post('{bonusScore}/judges/', 'assignJudge')->name('admin.bonus-scores.judges.assign');
|
||||
|
||||
});
|
||||
|
||||
// Admin Ensemble Routes
|
||||
Route::prefix('ensembles')->controller(\App\Http\Controllers\Admin\EnsembleController::class)->group(function () {
|
||||
Route::prefix('ensembles')->controller(EnsembleController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.ensembles.index');
|
||||
Route::post('/', 'store')->name('admin.ensembles.store');
|
||||
Route::delete('/{ensemble}', 'destroy')->name('admin.ensembles.destroy');
|
||||
|
|
@ -22,45 +53,52 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
|
|||
Route::patch('/{ensemble}', 'updateEnsemble')->name('admin.ensembles.update');
|
||||
Route::get('/seating-limits', 'seatingLimits')->name('admin.ensembles.seatingLimits');
|
||||
Route::get('/seating-limits/{ensemble}', 'seatingLimits')->name('admin.ensembles.seatingLimits.ensemble');
|
||||
Route::post('/seating-limits/{ensemble}', 'seatingLimitsSet')->name('admin.ensembles.seatingLimits.ensemble.set');
|
||||
Route::post('/seating-limits/{ensemble}',
|
||||
'seatingLimitsSet')->name('admin.ensembles.seatingLimits.ensemble.set');
|
||||
});
|
||||
|
||||
// Admin Event Routes
|
||||
Route::prefix('events')->controller(\App\Http\Controllers\Admin\EventController::class)->group(function () {
|
||||
Route::prefix('events')->controller(EventController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.events.index');
|
||||
Route::post('/', 'store')->name('admin.events.store');
|
||||
Route::delete('/{event}', 'destroy')->name('admin.events.destroy');
|
||||
});
|
||||
|
||||
// Admin Rooms Routes
|
||||
Route::prefix('rooms')->controller(\App\Http\Controllers\Admin\RoomController::class)->group(function () {
|
||||
Route::prefix('rooms')->controller(RoomController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.rooms.index');
|
||||
Route::get('/create', 'create')->name('admin.rooms.create');
|
||||
Route::post('/', 'store')->name('admin.rooms.store');
|
||||
Route::post('/{room}/edit', 'edit')->name('admin.rooms.edit');
|
||||
Route::patch('/{room}', 'update')->name('admin.rooms.update');
|
||||
Route::delete('/{room}', 'destroy')->name('admin.rooms.destroy');
|
||||
Route::get('/judging_assignments', 'judgingAssignment')->name('admin.rooms.judgingAssignment'); // Screen to assign judges to rooms
|
||||
Route::match(['post', 'delete'], '/{room}/judge', 'updateJudgeAssignment')->name('admin.rooms.updateJudgeAssignment');
|
||||
Route::get('/judging_assignments',
|
||||
'judgingAssignment')->name('admin.rooms.judgingAssignment'); // Screen to assign judges to rooms
|
||||
Route::match(['post', 'delete'], '/{room}/judge',
|
||||
'updateJudgeAssignment')->name('admin.rooms.updateJudgeAssignment');
|
||||
|
||||
});
|
||||
|
||||
// Admin Scoring Guides
|
||||
Route::prefix('scoring')->controller(\App\Http\Controllers\Admin\ScoringGuideController::class)->group(function () {
|
||||
Route::prefix('scoring')->controller(ScoringGuideController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.scoring.index'); // Scoring Setup Homepage
|
||||
Route::post('/guides', 'store')->name('admin.scoring.store'); // Save a new scoring guide
|
||||
Route::get('/guides/{guide}/edit/{tab?}', 'edit')->name('admin.scoring.edit'); // Edit scoring guide
|
||||
Route::patch('/guides/{guide}/edit', 'update')->name('admin.scoring.update'); // Save changes to audition guide (rename)
|
||||
Route::post('/guides/{guide}/subscore', 'subscore_store')->name('admin.scoring.subscore_store'); // Save a new subscore
|
||||
Route::patch('/guides/{guide}/subscore/{subscore}', 'subscore_update')->name('admin.scoring.subscore_update'); // Modify a subscore
|
||||
Route::delete('/guides/{guide}/subscore/{subscore}', 'subscore_destroy')->name('admin.scoring.subscore_destroy'); // Delete a subscore
|
||||
Route::patch('/guides/{guide}/edit',
|
||||
'update')->name('admin.scoring.update'); // Save changes to audition guide (rename)
|
||||
Route::post('/guides/{guide}/subscore',
|
||||
'subscore_store')->name('admin.scoring.subscore_store'); // Save a new subscore
|
||||
Route::patch('/guides/{guide}/subscore/{subscore}',
|
||||
'subscore_update')->name('admin.scoring.subscore_update'); // Modify a subscore
|
||||
Route::delete('/guides/{guide}/subscore/{subscore}',
|
||||
'subscore_destroy')->name('admin.scoring.subscore_destroy'); // Delete a subscore
|
||||
Route::post('/reorder-display', 'reorder_display')->name('admin.scoring.reorder_display');
|
||||
Route::post('/reorder-tiebreak', 'reorder_tiebreak')->name('admin.scoring.reorder_tiebreak');
|
||||
Route::delete('/guides/{guide}', 'destroy')->name('admin.scoring.destroy'); // Delete a scoring guide
|
||||
});
|
||||
|
||||
// Admin Auditions Routes
|
||||
Route::prefix('auditions')->controller(\App\Http\Controllers\Admin\AuditionController::class)->group(function () {
|
||||
Route::prefix('auditions')->controller(AuditionController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.auditions.index');
|
||||
Route::get('/create', 'create')->name('admin.auditions.create');
|
||||
Route::post('/', 'store')->name('admin.auditions.store');
|
||||
|
|
@ -71,15 +109,16 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
|
|||
});
|
||||
|
||||
// Admin Audition Draw Routes
|
||||
Route::prefix('draw')->controller(\App\Http\Controllers\Admin\DrawController::class)->group(function () {
|
||||
Route::prefix('draw')->controller(DrawController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.draw.index');
|
||||
Route::post('/', 'store')->name('admin.draw.store');
|
||||
Route::get('/clear', 'edit')->name('admin.draw.edit'); // Select auditions for which the user would like to clear the draw
|
||||
Route::get('/clear',
|
||||
'edit')->name('admin.draw.edit'); // Select auditions for which the user would like to clear the draw
|
||||
Route::delete('/', 'destroy')->name('admin.draw.destroy'); // Clear the draw for the selected auditions
|
||||
});
|
||||
|
||||
// Admin Entries Routes
|
||||
Route::prefix('entries')->controller(\App\Http\Controllers\Admin\EntryController::class)->group(function () {
|
||||
Route::prefix('entries')->controller(EntryController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.entries.index');
|
||||
Route::get('/create', 'create')->name('admin.entries.create');
|
||||
Route::post('/', 'store')->name('admin.entries.store');
|
||||
|
|
@ -90,7 +129,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
|
|||
});
|
||||
|
||||
// Admin Student Routes
|
||||
Route::prefix('students')->controller(\App\Http\Controllers\Admin\StudentController::class)->group(function () {
|
||||
Route::prefix('students')->controller(StudentController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.students.index');
|
||||
Route::get('/create', 'create')->name('admin.students.create');
|
||||
Route::post('/', 'store')->name('admin.students.store');
|
||||
|
|
@ -100,7 +139,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
|
|||
});
|
||||
|
||||
// Admin School Routes
|
||||
Route::prefix('schools')->controller(\App\Http\Controllers\Admin\SchoolController::class)->group(function () {
|
||||
Route::prefix('schools')->controller(SchoolController::class)->group(function () {
|
||||
Route::post('/{school}/add_domain', 'add_domain')->name('admin.schools.add_domain');
|
||||
Route::get('/', 'index')->name('admin.schools.index');
|
||||
Route::get('/create', 'create')->name('admin.schools.create');
|
||||
|
|
@ -115,7 +154,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
|
|||
});
|
||||
|
||||
// Admin User Routes
|
||||
Route::prefix('users')->controller(\App\Http\Controllers\Admin\UserController::class)->group(function () {
|
||||
Route::prefix('users')->controller(UserController::class)->group(function () {
|
||||
Route::get('/', 'index')->name('admin.users.index');
|
||||
Route::get('/create', 'create')->name('admin.users.create');
|
||||
Route::post('/', 'store')->name('admin.users.store');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<?php
|
||||
|
||||
// Judging Routes
|
||||
use App\Http\Controllers\JudgingController;
|
||||
use App\Http\Controllers\Judging\BonusScoreEntryController;
|
||||
use App\Http\Controllers\Judging\BonusScoreEntryListController;
|
||||
use App\Http\Controllers\Judging\BonusScoreRecordController;
|
||||
use App\Http\Controllers\Judging\JudgingController;
|
||||
use App\Http\Middleware\CheckIfCanJudge;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
|
|
@ -11,3 +15,10 @@ Route::middleware(['auth', 'verified', CheckIfCanJudge::class])->prefix('judging
|
|||
Route::post('/entry/{entry}', 'saveScoreSheet')->name('judging.saveScoreSheet');
|
||||
Route::patch('/entry/{entry}', 'updateScoreSheet')->name('judging.updateScoreSheet');
|
||||
});
|
||||
|
||||
// Bonus score judging routes
|
||||
Route::middleware(['auth', 'verified', CheckIfCanJudge::class])->prefix('judging/bonus_scores')->group(function () {
|
||||
Route::get('/{audition}', BonusScoreEntryListController::class)->name('judging.bonusScore.EntryList'); // List of entries in an audition
|
||||
Route::get('/entries/{entry}', BonusScoreEntryController::class)->name('judging.bonusScore.entry'); // Form to enter an entries score
|
||||
Route::post('/entries/{entry}', BonusScoreRecordController::class)->name('judging.bonusScores.recordScore'); // Record the score
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@
|
|||
|
||||
// Tabulation Routes
|
||||
use App\Http\Controllers\Tabulation\AdvancementController;
|
||||
use App\Http\Controllers\Tabulation\BonusScoreController;
|
||||
use App\Http\Controllers\Tabulation\DoublerDecisionController;
|
||||
use App\Http\Controllers\Tabulation\EntryFlagController;
|
||||
use App\Http\Controllers\Tabulation\ScoreController;
|
||||
use App\Http\Controllers\Tabulation\SeatAuditionFormController;
|
||||
use App\Http\Controllers\Tabulation\SeatingPublicationController;
|
||||
use App\Http\Controllers\Tabulation\SeatingStatusController;
|
||||
use App\Http\Controllers\Tabulation\TabulationController;
|
||||
use App\Http\Middleware\CheckIfCanTab;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
|
|
@ -22,6 +22,14 @@ Route::middleware(['auth', 'verified', CheckIfCanTab::class])->group(function ()
|
|||
Route::delete('/{score}', 'destroyScore')->name('scores.destroy');
|
||||
});
|
||||
|
||||
// Bonus Score Management
|
||||
Route::prefix('bonus-scores/')->controller(BonusScoreController::class)->group(function () {
|
||||
Route::get('/choose_entry', 'chooseEntry')->name('bonus-scores.chooseEntry');
|
||||
Route::get('/entry', 'entryBonusScoreSheet')->name('bonus-scores.entryBonusScoreSheet');
|
||||
Route::post('/entry/{entry}', 'saveEntryBonusScoreSheet')->name('bonus-scores.saveEntryBonusScoreSheet');
|
||||
Route::delete('/{bonusScore}', 'destroyBonusScore')->name('bonus-scores.destroy');
|
||||
});
|
||||
|
||||
// Entry Flagging
|
||||
Route::prefix('entry-flags/')->controller(EntryFlagController::class)->group(function () {
|
||||
Route::get('/choose_no_show', 'noShowSelect')->name('entry-flags.noShowSelect');
|
||||
|
|
|
|||
|
|
@ -1,19 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\DashboardController;
|
||||
use App\Http\Controllers\EntryController;
|
||||
use App\Http\Controllers\FilterController;
|
||||
use App\Http\Controllers\JudgingController;
|
||||
use App\Http\Controllers\PdfInvoiceController;
|
||||
use App\Http\Controllers\SchoolController;
|
||||
use App\Http\Controllers\StudentController;
|
||||
use App\Http\Controllers\Tabulation\DoublerDecisionController;
|
||||
use App\Http\Controllers\TestController;
|
||||
use App\Http\Controllers\UserController;
|
||||
use App\Http\Middleware\CheckIfAdmin;
|
||||
use App\Http\Middleware\CheckIfCanJudge;
|
||||
use App\Http\Middleware\CheckIfCanTab;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
require __DIR__.'/admin.php';
|
||||
|
|
@ -23,7 +11,6 @@ require __DIR__.'/user.php';
|
|||
|
||||
Route::get('/test', [TestController::class, 'flashTest'])->middleware('auth', 'verified');
|
||||
|
||||
|
||||
Route::view('/', 'welcome')->middleware('guest')->name('home');
|
||||
Route::get('/results', [App\Http\Controllers\ResultsPage::class, '__invoke'])->name('results');
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
use App\Actions\Tabulation\EnterBonusScore;
|
||||
use App\Exceptions\ScoreEntryException;
|
||||
use App\Models\Audition;
|
||||
use App\Models\BonusScore;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\Entry;
|
||||
use App\Models\Student;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\App;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->enterBonusScore = App::make(EnterBonusScore::class);
|
||||
});
|
||||
|
||||
it('rejects a non existent entry', function () {
|
||||
$judge = User::factory()->create();
|
||||
$entry = Entry::factory()->make();
|
||||
$this->enterBonusScore->__invoke($judge, $entry, 42);
|
||||
})->throws(ScoreEntryException::class, 'Invalid entry provided');
|
||||
it('rejects a non existent judge', function () {
|
||||
$judge = User::factory()->make();
|
||||
$entry = Entry::factory()->create();
|
||||
$this->enterBonusScore->__invoke($judge, $entry, 42);
|
||||
})->throws(ScoreEntryException::class, 'Invalid judge provided');
|
||||
it('rejects a submission if the entries audition does not have a bonus score', function () {
|
||||
$judge = User::factory()->create();
|
||||
$entry = Entry::factory()->create();
|
||||
$this->enterBonusScore->__invoke($judge, $entry, 42);
|
||||
})->throws(ScoreEntryException::class, 'Entry does not have a bonus score');
|
||||
it('rejects a submission if the entry already has a score from the given judge', function () {
|
||||
// Arrange
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$entry = Entry::factory()->create();
|
||||
$entry->audition->bonusScore()->attach($bonusScore->id);
|
||||
$score = BonusScore::create([
|
||||
'entry_id' => $entry->id,
|
||||
'user_id' => $judge->id,
|
||||
'originally_scored_entry' => $entry->id,
|
||||
'score' => 42,
|
||||
]);
|
||||
// Act & Assert
|
||||
$this->enterBonusScore->__invoke($judge, $entry, 43);
|
||||
})->throws(ScoreEntryException::class, 'That judge has already scored that entry');
|
||||
it('rejects a submission for a judge not assigned to judge that bonus score', function () {
|
||||
// Arrange
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$entry = Entry::factory()->create();
|
||||
$entry->audition->bonusScore()->attach($bonusScore->id);
|
||||
// Act & Assert
|
||||
$this->enterBonusScore->__invoke($judge, $entry, 43);
|
||||
})->throws(ScoreEntryException::class, 'That judge is not assigned to judge that bonus score');
|
||||
it('rejects a submission for a score that exceeds the maximum', function () {
|
||||
// Arrange
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore = BonusScoreDefinition::factory()->create(['max_score' => 50]);
|
||||
$bonusScore->judges()->attach($judge);
|
||||
$entry = Entry::factory()->create();
|
||||
$entry->audition->bonusScore()->attach($bonusScore->id);
|
||||
// Act & Assert
|
||||
$this->enterBonusScore->__invoke($judge, $entry, 51);
|
||||
})->throws(ScoreEntryException::class, 'That score exceeds the maximum');
|
||||
|
||||
it('records a valid bonus score submission on the submitted entry', function () {
|
||||
// Arrange
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore = BonusScoreDefinition::factory()->create(['max_score' => 100]);
|
||||
$entry = Entry::factory()->create();
|
||||
$entry->audition->bonusScore()->attach($bonusScore->id);
|
||||
$bonusScore->judges()->attach($judge);
|
||||
// Act & Assert
|
||||
$this->enterBonusScore->__invoke($judge, $entry, 42);
|
||||
expect(
|
||||
BonusScore::where('entry_id', $entry->id)
|
||||
->where('user_id', $judge->id)
|
||||
->where('score', 42)->exists())
|
||||
->toBeTrue();
|
||||
});
|
||||
it('records a valid bonus score on all related entries', function () {
|
||||
// Arrange
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore = BonusScoreDefinition::factory()->create(['name' => 'Saxophone Improvisation', 'max_score' => 100]);
|
||||
$bonusScore->judges()->attach($judge);
|
||||
$jazzAltoAudition = Audition::factory()->create(['name' => 'Jazz Alto Saxophone']);
|
||||
$jazzTenorAudition = Audition::factory()->create(['name' => 'Jazz Tenor Saxophone']);
|
||||
$jazzBariAudition = Audition::factory()->create(['name' => 'Jazz Bari Saxophone']);
|
||||
$bonusScore->auditions()->attach($jazzAltoAudition->id);
|
||||
$bonusScore->auditions()->attach($jazzTenorAudition->id);
|
||||
$bonusScore->auditions()->attach($jazzBariAudition->id);
|
||||
$saxStudent = Student::factory()->create();
|
||||
$jazzAltoEntry = Entry::factory()->create([
|
||||
'student_id' => $saxStudent->id, 'audition_id' => $jazzAltoAudition->id,
|
||||
]);
|
||||
$jazzTenorEntry = Entry::factory()->create(['student_id' => $saxStudent->id,
|
||||
'audition_id' => $jazzTenorAudition->id,
|
||||
]);
|
||||
$jazzBariEntry = Entry::factory()->create(['student_id' => $saxStudent->id, 'audition_id' => $jazzBariAudition->id,
|
||||
]);
|
||||
Entry::factory()->count(4)->create(['audition_id' => $jazzAltoAudition->id]);
|
||||
Entry::factory()->count(4)->create(['audition_id' => $jazzTenorAudition->id]);
|
||||
Entry::factory()->count(4)->create(['audition_id' => $jazzBariAudition->id]);
|
||||
// Act
|
||||
$this->enterBonusScore->__invoke($judge, $jazzAltoEntry, 42);
|
||||
// Assert
|
||||
expect(
|
||||
BonusScore::where('entry_id', $jazzAltoEntry->id)
|
||||
->where('user_id', $judge->id)
|
||||
->where('originally_scored_entry', $jazzAltoEntry->id)
|
||||
->where('score', 42)->exists())
|
||||
->toBeTrue()
|
||||
|
||||
->and(BonusScore::count())->toBe(3)
|
||||
|
||||
->and(
|
||||
BonusScore::where('entry_id', $jazzTenorEntry->id)
|
||||
->where('user_id', $judge->id)
|
||||
->where('originally_scored_entry', $jazzAltoEntry->id)
|
||||
->where('score', 42)->exists())
|
||||
->toBeTrue()
|
||||
|
||||
->and(
|
||||
BonusScore::where('entry_id', $jazzBariEntry->id)
|
||||
->where('user_id', $judge->id)
|
||||
->where('originally_scored_entry', $jazzAltoEntry->id)
|
||||
->where('score', 42)->exists())
|
||||
->toBeTrue();
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Audition;
|
||||
use App\Models\BonusScore;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\Entry;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('denies access go a guest', function () {
|
||||
$response = $this->get(route('judging.bonusScore.EntryList', 1));
|
||||
$response->assertRedirect(route('home'));
|
||||
});
|
||||
it('denies access to a user not assigned to judge this bonus score', function () {
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$audition = Audition::factory()->create();
|
||||
$audition->bonusScore()->attach($bonusScore->id);
|
||||
$room = Room::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$room->addJudge($user->id);
|
||||
actingAs($user);
|
||||
$this->get(route('judging.bonusScore.EntryList', $audition->id))
|
||||
->assertRedirect(route('dashboard'))
|
||||
->assertSessionHas('error', 'You are not assigned to judge this bonus score');
|
||||
});
|
||||
it('shows all entries to an authorized judge', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$audition = Audition::factory()->create();
|
||||
$audition->bonusScore()->attach($bonusScore->id);
|
||||
$entries[1] = Entry::factory()->create(['audition_id' => $audition->id, 'draw_number' => 1]);
|
||||
$entries[2] = Entry::factory()->create(['audition_id' => $audition->id, 'draw_number' => 2]);
|
||||
$entries[3] = Entry::factory()->create(['audition_id' => $audition->id, 'draw_number' => 3]);
|
||||
$entries[4] = Entry::factory()->create(['audition_id' => $audition->id, 'draw_number' => 4]);
|
||||
$entries = collect($entries);
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore->judges()->attach($judge->id);
|
||||
actingAs($judge);
|
||||
// Act & Assert
|
||||
$response = $this->get(route('judging.bonusScore.EntryList', $audition->id));
|
||||
$response->assertOk();
|
||||
$entries->each(fn ($entry) => $response->assertSee($entry->audition->name.' '.$entry->draw_number));
|
||||
});
|
||||
it('shows existing scores for an entry', function () {
|
||||
$bonusScore = BonusScoreDefinition::factory()->create(['max_score' => 100]);
|
||||
$audition = Audition::factory()->create();
|
||||
$audition->bonusScore()->attach($bonusScore->id);
|
||||
$entry = Entry::factory()->create(['audition_id' => $audition->id, 'draw_number' => 1]);
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore->judges()->attach($judge->id);
|
||||
BonusScore::create([
|
||||
'entry_id' => $entry->id,
|
||||
'user_id' => $judge->id,
|
||||
'originally_scored_entry' => $entry->id,
|
||||
'score' => 42,
|
||||
]);
|
||||
actingAs($judge);
|
||||
// Act
|
||||
$response = $this->get(route('judging.bonusScore.EntryList', $audition));
|
||||
$response->assertOk()
|
||||
->assertSeeInOrder(['<tr>', e($audition->name), $entry->draw_number, 42, '</tr>'], false);
|
||||
});
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Audition;
|
||||
use App\Models\BonusScore;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\Entry;
|
||||
use App\Models\Room;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Sinnbeck\DomAssertions\Asserts\AssertForm;
|
||||
|
||||
use function Pest\Laravel\actingAs;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('denies access go a guest', function () {
|
||||
$response = $this->get(route('judging.bonusScore.entry', 1));
|
||||
$response->assertRedirect(route('home'));
|
||||
});
|
||||
it('denies access to a user not assigned to judge this bonus score', function () {
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$audition = Audition::factory()->create();
|
||||
$audition->bonusScore()->attach($bonusScore->id);
|
||||
$entry = Entry::factory()->create(['audition_id' => $audition->id]);
|
||||
$room = Room::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$room->addJudge($user->id);
|
||||
actingAs($user);
|
||||
$this->get(route('judging.bonusScore.entry', $entry->id))
|
||||
->assertRedirect(route('judging.index'))
|
||||
->assertSessionHas('error', 'You are not assigned to judge this entry');
|
||||
});
|
||||
it('denies access if a score already exists for the entry by the user', function () {
|
||||
$entry = Entry::factory()->create();
|
||||
$judge = User::factory()->create();
|
||||
$bonusScoreDefinition = BonusScoreDefinition::factory()->create();
|
||||
$bonusScoreDefinition->judges()->attach($judge->id);
|
||||
BonusScore::create([
|
||||
'entry_id' => $entry->id,
|
||||
'user_id' => $judge->id,
|
||||
'originally_scored_entry' => $entry->id,
|
||||
'score' => 42,
|
||||
]);
|
||||
actingAs($judge);
|
||||
$this->get(route('judging.bonusScore.entry', $entry))
|
||||
->assertRedirect(route('judging.bonusScore.EntryList', $entry->audition))
|
||||
->assertSessionHas('error', 'You have already judged that entry');
|
||||
});
|
||||
it('has a proper score entry form for a valid request', function () {
|
||||
// Arrange
|
||||
$audition = Audition::factory()->create();
|
||||
$bonusScore = BonusScoreDefinition::factory()->create(['max_score' => 100]);
|
||||
$bonusScore->auditions()->attach($audition->id);
|
||||
$entry = Entry::factory()->create(['audition_id' => $audition->id]);
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore->judges()->attach($judge->id);
|
||||
actingAs($judge);
|
||||
// Act & Assert
|
||||
$request = $this->get(route('judging.bonusScore.entry', $entry));
|
||||
$request->assertOk()
|
||||
->assertFormExists('#score-entry-for-'.$entry->id, function (AssertForm $form) use ($entry) {
|
||||
$form->hasCSRF()
|
||||
->hasMethod('POST')
|
||||
->hasAction(route('judging.bonusScores.recordScore', $entry))
|
||||
->containsInput(['name' => 'score'])
|
||||
->containsButton(['type' => 'submit']);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Audition;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\Entry;
|
||||
use App\Models\Room;
|
||||
use App\Models\ScoringGuide;
|
||||
|
|
@ -113,3 +114,37 @@ it('does not show the user room and auditions they are not assigned to judge', f
|
|||
->assertDontSee($otherRoom->name)
|
||||
->assertDontSee($otherAudition->name);
|
||||
});
|
||||
it('shows bonus scores the user is assigned to judge', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore->judges()->attach($judge);
|
||||
$this->actingAs($judge);
|
||||
// Act & Assert
|
||||
$this->get(route('judging.index'))
|
||||
->assertSee($bonusScore->name);
|
||||
});
|
||||
it('does not show bonus scores the user is not assigned to judge', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$otherBonusScore = BonusScoreDefinition::factory()->create();
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore->judges()->attach($judge);
|
||||
$this->actingAs($judge);
|
||||
// Act & Assert
|
||||
$this->get(route('judging.index'))
|
||||
->assertSee($bonusScore->name)
|
||||
->assertDontSee($otherBonusScore->name);
|
||||
});
|
||||
it('shows auditions in a bonus score assignment', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$audition = Audition::factory()->create();
|
||||
$bonusScore->auditions()->attach($audition);
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore->judges()->attach($judge);
|
||||
$this->actingAs($judge);
|
||||
// Act & Assert
|
||||
$this->get(route('judging.index'))
|
||||
->assertSee($audition->name);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Audition;
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Sinnbeck\DomAssertions\Asserts\AssertForm;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('denies access to guests and non administrators', function () {
|
||||
$this->get(route('admin.bonus-scores.index'))
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
actAsNormal();
|
||||
$this->get(route('admin.bonus-scores.index'))
|
||||
->assertRedirect(route('dashboard'))
|
||||
->assertSessionHas('error', 'You are not authorized to perform this action');
|
||||
|
||||
actAsTab();
|
||||
$this->get(route('admin.bonus-scores.index'))
|
||||
->assertRedirect(route('dashboard'))
|
||||
->assertSessionHas('error', 'You are not authorized to perform this action');
|
||||
});
|
||||
it('grants access to an administrator', function () {
|
||||
// Arrange
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->get(route('admin.bonus-scores.index'))
|
||||
->assertOk()
|
||||
->assertViewIs('admin.bonus-scores.index');
|
||||
});
|
||||
it('if no bonus scores exist, show a create bonus score message', function () {
|
||||
// Arrange
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->get(route('admin.bonus-scores.index'))
|
||||
->assertOk()
|
||||
->assertSee('No bonus scores have been created');
|
||||
});
|
||||
it('includes a form to add a new bonus score', function () {
|
||||
// Arrange
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->get(route('admin.bonus-scores.index'))
|
||||
->assertOk()
|
||||
->assertFormExists('#create-bonus-score-form', function (AssertForm $form) {
|
||||
/** @noinspection PhpUndefinedMethodInspection */
|
||||
$form->hasCSRF()
|
||||
->hasMethod('POST')
|
||||
->hasAction(route('admin.bonus-scores.store'))
|
||||
->containsInput(['name' => 'name'])
|
||||
->containsInput(['name' => 'max_score', 'type' => 'number'])
|
||||
->containsInput(['name' => 'weight']);
|
||||
});
|
||||
});
|
||||
it('can create a new bonus score', function () {
|
||||
// Arrange
|
||||
$submissionData = [
|
||||
'name' => 'New Bonus Score',
|
||||
'max_score' => 10,
|
||||
'weight' => 1,
|
||||
];
|
||||
// Act & Assert
|
||||
actAsAdmin();
|
||||
$this->post(route('admin.bonus-scores.store'), $submissionData)
|
||||
->assertRedirect(route('admin.bonus-scores.index'))
|
||||
->assertSessionHas('success', 'Bonus Score Created');
|
||||
$test = BonusScoreDefinition::where('name', 'New Bonus Score')->first();
|
||||
expect($test->exists())->toBeTrue();
|
||||
});
|
||||
it('shows existing bonus scores', function () {
|
||||
// Arrange
|
||||
$bonusScores = BonusScoreDefinition::factory()->count(3)->create();
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$response = $this->get(route('admin.bonus-scores.index'));
|
||||
$response->assertOk();
|
||||
$bonusScores->each(fn ($bonusScore) => $response->assertSee($bonusScore->name));
|
||||
});
|
||||
it('can delete a bonus score with no auditions', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->delete(route('admin.bonus-scores.destroy', $bonusScore))
|
||||
->assertRedirect(route('admin.bonus-scores.index'))
|
||||
->assertSessionHas('success', 'Bonus Score Deleted');
|
||||
expect(BonusScoreDefinition::count())->toBe(0);
|
||||
});
|
||||
it('will not delete a bonus score that has auditions attached', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->hasAuditions(1)->create();
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->delete(route('admin.bonus-scores.destroy', $bonusScore))
|
||||
->assertRedirect(route('admin.bonus-scores.index'))
|
||||
->assertSessionHas('error', 'Bonus Score has auditions attached');
|
||||
expect(BonusScoreDefinition::count())->toBe(1);
|
||||
});
|
||||
it('can assign auditions to a bonus score', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$auditions = Audition::factory()->count(3)->create();
|
||||
$submissionData = [
|
||||
'bonus_score_id' => $bonusScore->id,
|
||||
];
|
||||
foreach ($auditions as $audition) {
|
||||
$submissionData['audition'][$audition->id] = 'on';
|
||||
}
|
||||
// Act & Assert
|
||||
actAsAdmin();
|
||||
$this->post(route('admin.bonus-scores.addAuditions'), $submissionData)
|
||||
->assertRedirect(route('admin.bonus-scores.index'))
|
||||
->assertSessionHas('success', 'Auditions assigned to bonus score');
|
||||
$bonusScore->refresh();
|
||||
$auditions->each(fn ($audition) => expect($bonusScore->auditions->contains($audition))->toBeTrue());
|
||||
});
|
||||
it('can unassign auditions from a bonus score', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->hasAuditions(3)->create();
|
||||
$audition = $bonusScore->auditions->first();
|
||||
// Act & Assert
|
||||
actAsAdmin();
|
||||
$this->delete(route('admin.bonus-scores.unassignAudition', $audition))
|
||||
->assertRedirect(route('admin.bonus-scores.index'))
|
||||
->assertSessionHas('success', 'Audition unassigned from bonus score');
|
||||
$bonusScore->refresh();
|
||||
expect($bonusScore->auditions->contains($audition))->toBeFalse();
|
||||
});
|
||||
it('sends a message when attempting to unassign an audition that is not assigned', function () {
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$audition = Audition::factory()->create();
|
||||
actAsAdmin();
|
||||
$this->delete(route('admin.bonus-scores.unassignAudition', $audition))
|
||||
->assertRedirect(route('admin.bonus-scores.index'))
|
||||
->assertSessionHas('error', 'Audition does not have a bonus score');
|
||||
});
|
||||
it('will not allow an audition to be assigned to multiple bonus scores', function () {
|
||||
$bonusScore1 = BonusScoreDefinition::factory()->create();
|
||||
$bonusScore2 = BonusScoreDefinition::factory()->create();
|
||||
$audition = Audition::factory()->create();
|
||||
$bonusScore1->auditions()->attach($audition);
|
||||
$submissionData = [
|
||||
'bonus_score_id' => $bonusScore2->id,
|
||||
'audition' => [$audition->id => 'on'],
|
||||
];
|
||||
actAsAdmin();
|
||||
$this->post(route('admin.bonus-scores.addAuditions'), $submissionData)
|
||||
->assertRedirect(route('admin.bonus-scores.index'))
|
||||
->assertSessionHas('error', 'Error assigning auditions to bonus score');
|
||||
});
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
use App\Models\BonusScoreDefinition;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('denies access to guests and non administrators', function () {
|
||||
$this->get(route('admin.bonus-scores.judges'))
|
||||
->assertRedirect(route('home'));
|
||||
|
||||
actAsNormal();
|
||||
$this->get(route('admin.bonus-scores.judges'))
|
||||
->assertRedirect(route('dashboard'))
|
||||
->assertSessionHas('error', 'You are not authorized to perform this action');
|
||||
|
||||
actAsTab();
|
||||
$this->get(route('admin.bonus-scores.judges'))
|
||||
->assertRedirect(route('dashboard'))
|
||||
->assertSessionHas('error', 'You are not authorized to perform this action');
|
||||
});
|
||||
it('grants access to an administrator', function () {
|
||||
// Arrange
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->get(route('admin.bonus-scores.judges'))
|
||||
->assertOk()
|
||||
->assertViewIs('admin.bonus-scores.judge-assignments');
|
||||
});
|
||||
it('shows a link to the room judge assignment screen', function () {
|
||||
// Arrange
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->get(route('admin.bonus-scores.judges'))
|
||||
->assertOk()
|
||||
->assertSee(route('admin.rooms.judgingAssignment'));
|
||||
});
|
||||
it('shows a card for each bonus score', function () {
|
||||
// Arrange
|
||||
$bonusScores = BonusScoreDefinition::factory()->count(3)->create();
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$response = $this->get(route('admin.bonus-scores.judges'));
|
||||
$response->assertOk();
|
||||
$bonusScores->each(fn ($bonus) => $response->assertElementExists('#bonus-'.$bonus->id.'-card'));
|
||||
});
|
||||
it('can assign a judge to a bonus score', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$judge = User::factory()->create();
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->post(route('admin.bonus-scores.judges.assign', $bonusScore), ['judge' => $judge->id])
|
||||
->assertRedirect(route('admin.bonus-scores.judges'))
|
||||
->assertSessionHas('success', 'Judge assigned to bonus score');
|
||||
expect($bonusScore->judges()->count())->toBe(1);
|
||||
});
|
||||
it('can assign a judge to a room', function () {
|
||||
// Arrange
|
||||
$bonusScore = BonusScoreDefinition::factory()->create();
|
||||
$judge = User::factory()->create();
|
||||
$bonusScore->judges()->attach($judge->id);
|
||||
actAsAdmin();
|
||||
// Act & Assert
|
||||
$this->delete(route('admin.bonus-scores.judges.remove', $bonusScore), ['judge' => $judge->id])
|
||||
->assertRedirect(route('admin.bonus-scores.judges'))
|
||||
->assertSessionHas('success', 'Judge removed from bonus score');
|
||||
expect($bonusScore->judges()->count())->toBe(0);
|
||||
});
|
||||
Loading…
Reference in New Issue