Auditionadmin 64 #71

Merged
okorpheus merged 9 commits from auditionadmin-64 into master 2024-08-11 04:16:54 +00:00
22 changed files with 504 additions and 52 deletions

View File

@ -0,0 +1,40 @@
<?php
namespace App\Actions\Schools;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\User;
use function auditionLog;
use function is_null;
class SetHeadDirector
{
public function __construct()
{
}
public function __invoke(User $user, School $school): void
{
$this->setHeadDirector($user, $school);
}
/**
* @throws AuditionAdminException
*/
public function setHeadDirector(User $user): void
{
if (is_null($user->school_id)) {
throw new AuditionAdminException('User is not associated with a school');
}
foreach ($user->school->directors as $director) {
$director->removeFlag('head_director');
}
$user->addFlag('head_director');
$logMessage = 'Set '.$user->full_name().' as head director at '.$user->school->name;
$logAffected = ['users' => [$user->id], 'schools' => [$user->school_id]];
auditionLog($logMessage, $logAffected);
}
}

8
app/Enums/UserFlags.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace App\Enums;
enum UserFlags: string
{
case HEAD_DIRECTOR = 'head_director';
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\Exceptions;
use Exception;
class AuditionAdminException extends Exception
{
//
}

View File

@ -2,10 +2,12 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
use App\Models\School; use App\Models\School;
use App\Models\SchoolEmailDomain; use App\Models\SchoolEmailDomain;
use App\Models\User;
use App\Services\Invoice\InvoiceDataService; use App\Services\Invoice\InvoiceDataService;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
@ -176,4 +178,14 @@ class SchoolController extends Controller
return view('dashboard.invoice', compact('school', 'invoiceData')); return view('dashboard.invoice', compact('school', 'invoiceData'));
} }
public function setHeadDirector(School $school, User $user, SetHeadDirector $headSetter)
{
if ($user->school_id !== $school->id) {
return redirect()->back()->with('error', 'That user is not at that school');
}
$headSetter->setHeadDirector($user);
return redirect()->back()->with('success', 'Head director set');
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Actions\Schools\SetHeadDirector;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Mail\NewUserPassword; use App\Mail\NewUserPassword;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
@ -13,6 +14,8 @@ use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use function auditionLog;
class UserController extends Controller class UserController extends Controller
{ {
public function index() public function index()
@ -45,7 +48,7 @@ class UserController extends Controller
return view('admin.users.create', ['schools' => $schools]); return view('admin.users.create', ['schools' => $schools]);
} }
public function update(Request $request, User $user) public function update(Request $request, User $user, SetHeadDirector $headSetter)
{ {
if (! Auth::user()->is_admin) { if (! Auth::user()->is_admin) {
abort(403); abort(403);
@ -63,6 +66,7 @@ class UserController extends Controller
]); ]);
$validData['is_admin'] = $request->get('is_admin') == 'on' ? 1 : 0; $validData['is_admin'] = $request->get('is_admin') == 'on' ? 1 : 0;
$validData['is_tab'] = $request->get('is_tab') == 'on' ? 1 : 0; $validData['is_tab'] = $request->get('is_tab') == 'on' ? 1 : 0;
$validData['is_head'] = $request->get('is_head') == 'on' ? 1 : 0;
$user->update([ $user->update([
'first_name' => $validData['first_name'], 'first_name' => $validData['first_name'],
'last_name' => $validData['last_name'], 'last_name' => $validData['last_name'],
@ -106,6 +110,16 @@ class UserController extends Controller
'affected' => ['users' => [$user->id]], 'affected' => ['users' => [$user->id]],
]); ]);
} }
if ($user->hasFlag('head_director') != $validData['is_head'] && ! is_null($user->school_id)) {
if ($validData['is_head']) {
$headSetter->setHeadDirector($user);
} else {
$user->removeFlag('head_director');
$logMessage = 'Removed '.$user->full_name().' as head director at '.$user->school->name;
$logAffected = ['users' => [$user->id], 'schools' => [$user->school_id]];
auditionLog($logMessage, $logAffected);
}
}
return redirect('/admin/users'); return redirect('/admin/users');
} }

View File

@ -2,20 +2,29 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Actions\Schools\SetHeadDirector;
use App\Exceptions\AuditionAdminException;
use App\Mail\NewUserPassword;
use App\Models\AuditLogEntry; use App\Models\AuditLogEntry;
use App\Models\School; use App\Models\School;
use App\Models\SchoolEmailDomain; use App\Models\SchoolEmailDomain;
use App\Models\User;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\RedirectResponse; use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use function abort; use function abort;
use function auditionLog;
use function redirect; use function redirect;
use function request; use function request;
class SchoolController extends Controller class SchoolController extends Controller
{ {
public function store(Request $request): RedirectResponse public function store(Request $request, SetHeadDirector $headSetter): RedirectResponse
{ {
if ($request->user()->cannot('create', School::class)) { if ($request->user()->cannot('create', School::class)) {
abort(403); abort(403);
@ -70,6 +79,13 @@ class SchoolController extends Controller
'schools' => [$school->id], 'schools' => [$school->id],
], ],
]); ]);
auth()->user()->refresh();
try {
$headSetter->setHeadDirector(auth()->user());
} catch (AuditionAdminException $e) {
redirect(route('schools.show', $school))->with('error', 'Could not set as head director');
}
} }
return redirect('/schools/'.$school->id); return redirect('/schools/'.$school->id);
@ -141,4 +157,101 @@ class SchoolController extends Controller
return redirect('/schools/create'); return redirect('/schools/create');
} }
public function addDirector(School $school)
{
if (auth()->user()->school_id !== $school->id) {
return redirect()->back()->with('error', 'No adding directors to another school');
}
if (! auth()->user()->hasFlag('head_director')) {
return redirect()->back()->with('error', 'Only the head director can add directors to a school');
}
$validData = request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'email' => ['required', 'email', 'unique:users'],
'cell_phone' => ['required'],
'judging_preference' => ['required'],
]);
// Generate a random password
$randomPassword = Str::random(12);
$newUser = User::create([
'first_name' => $validData['first_name'],
'last_name' => $validData['last_name'],
'email' => $validData['email'],
'cell_phone' => $validData['cell_phone'],
'judging_preference' => $validData['judging_preference'],
'password' => Hash::make($randomPassword),
'school_id' => auth()->user()->school_id,
]);
$logMessage = 'Created user '.$newUser->full_name().' - '.$newUser->email.' as a director at '.$newUser->school->name;
$logAffected = ['users' => [$newUser->id], 'schools' => [$newUser->school_id]];
auditionLog($logMessage, $logAffected);
Mail::to($newUser->email)->send(new NewUserPassword($newUser, $randomPassword));
return redirect()->back()->with('success', 'Director added');
}
public function setHeadDirector(School $school, User $user, SetHeadDirector $headSetter)
{
if (auth()->user()->school_id !== $school->id) {
return redirect()->back()->with('error', 'No setting the head director for another school');
}
if (! auth()->user()->hasFlag('head_director')) {
return redirect()->back()->with('error', 'Only the head director can name a new head director');
}
if ($school->id !== $user->school_id) {
return redirect()->back()->with('error', 'The proposed head director must be at your school');
}
try {
$headSetter->setHeadDirector($user);
} catch (AuditionAdminException $e) {
return redirect()->back()->with('error', $e->getMessage());
}
return redirect()->back()->with('success', 'New head director set');
}
public function addDomain(School $school)
{
if (auth()->user()->school_id !== $school->id) {
return redirect()->back()->with('error', 'No adding domains for another school');
}
if (! auth()->user()->hasFlag('head_director')) {
return redirect()->back()->with('error', 'Only the head director can add domains');
}
$verifiedData = request()->validate([
'domain' => ['required'],
]);
try {
SchoolEmailDomain::create([
'school_id' => $school->id,
'domain' => $verifiedData['domain'],
]);
} catch (UniqueConstraintViolationException $e) {
return redirect()->back()->with('error', 'That domain is already associated with your school');
}
$logMessage = 'Added domain '.$verifiedData['domain'].' to school '.$school->name;
$logAffected = ['schools' => [$school->id]];
auditionLog($logMessage, $logAffected);
return redirect()->back()->with('success', 'Domain added');
}
public function deleteDomain(SchoolEmailDomain $domain)
{
if (auth()->user()->school_id !== $domain->school_id) {
return redirect()->back()->with('error', 'No deleting domains for another school');
}
if (! auth()->user()->hasFlag('head_director')) {
return redirect()->back()->with('error', 'Only the head director can delete domains');
}
$logMessage = 'Deleted domain '.$domain->domain.' from school '.$domain->school->name;
$logAffected = ['schools' => [$domain->school_id]];
auditionLog($logMessage, $logAffected);
$domain->delete();
return redirect()->back()->with('success', 'Domain deleted');
}
} }

View File

@ -2,6 +2,7 @@
namespace App\Models; namespace App\Models;
use App\Enums\UserFlags;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
@ -158,6 +159,41 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->hasMany(ScoreSheet::class); return $this->hasMany(ScoreSheet::class);
} }
public function flags(): HasMany
{
return $this->hasMany(UserFlag::class);
}
public function hasFlag($flag): bool
{
$flags = [];
foreach ($this->flags as $checkFlag) {
$flags[] = $checkFlag->flag_name->value;
}
return in_array($flag, $flags);
}
public function addFlag($flag): void
{
if ($this->hasFlag($flag)) {
return;
}
$enum = match ($flag) {
'head_director' => UserFlags::HEAD_DIRECTOR,
};
$this->flags()->create(['flag_name' => $enum]);
$this->load('flags');
}
public function removeFlag($flag): void
{
// remove related userFlag where flag_name = $flag
$this->flags()->where('flag_name', $flag)->delete();
$this->load('flags');
}
public function scoresForEntry($entry) public function scoresForEntry($entry)
{ {
return $this->scoreSheets->where('entry_id', '=', $entry)->first()?->subscores; return $this->scoreSheets->where('entry_id', '=', $entry)->first()?->subscores;

21
app/Models/UserFlag.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App\Models;
use App\Enums\UserFlags;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserFlag extends Model
{
protected $guarded = [];
protected $casts = [
'flag_name' => UserFlags::class,
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -2,6 +2,9 @@
namespace App\Providers; namespace App\Providers;
use App\Actions\Entries\CreateEntry;
use App\Actions\Entries\UpdateEntry;
use App\Actions\Schools\SetHeadDirector;
use App\Actions\Tabulation\AllowForOlympicScoring; use App\Actions\Tabulation\AllowForOlympicScoring;
use App\Actions\Tabulation\CalculateEntryScore; use App\Actions\Tabulation\CalculateEntryScore;
use App\Actions\Tabulation\CalculateScoreSheetTotal; use App\Actions\Tabulation\CalculateScoreSheetTotal;
@ -32,7 +35,6 @@ use App\Services\DoublerService;
use App\Services\DrawService; use App\Services\DrawService;
use App\Services\EntryService; use App\Services\EntryService;
use App\Services\ScoreService; use App\Services\ScoreService;
use App\Services\StudentService;
use App\Services\UserService; use App\Services\UserService;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
@ -52,6 +54,10 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton(ScoreService::class, ScoreService::class); $this->app->singleton(ScoreService::class, ScoreService::class);
$this->app->singleton(UserService::class, UserService::class); $this->app->singleton(UserService::class, UserService::class);
$this->app->singleton(DoublerService::class, DoublerService::class); $this->app->singleton(DoublerService::class, DoublerService::class);
$this->app->singleton(CreateEntry::class, CreateEntry::class);
$this->app->singleton(UpdateEntry::class, UpdateEntry::class);
$this->app->singleton(SetHeadDirector::class, SetHeadDirector::class);
} }
/** /**

View File

@ -2,6 +2,7 @@
use App\Actions\Tabulation\EnterScore; use App\Actions\Tabulation\EnterScore;
use App\Exceptions\ScoreEntryException; use App\Exceptions\ScoreEntryException;
use App\Models\AuditLogEntry;
use App\Models\Entry; use App\Models\Entry;
use App\Models\User; use App\Models\User;
use App\Settings; use App\Settings;
@ -35,11 +36,22 @@ function auditionSetting($key)
return Settings::get($key); return Settings::get($key);
} }
function auditionLog(string $message, array $affected)
{
AuditLogEntry::create([
'user' => auth()->user()->email ?? 'no user',
'ip_address' => request()->ip(),
'message' => $message,
'affected' => $affected,
]);
}
/** /**
* @throws ScoreEntryException * @throws ScoreEntryException
*/ */
function enterScore(User $user, Entry $entry, array $scores): \App\Models\ScoreSheet function enterScore(User $user, Entry $entry, array $scores): \App\Models\ScoreSheet
{ {
$scoreEntry = App::make(EnterScore::class); $scoreEntry = App::make(EnterScore::class);
return $scoreEntry($user, $entry, $scores); return $scoreEntry($user, $entry, $scores);
} }

View File

@ -0,0 +1,30 @@
<?php
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('user_flags', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(User::class)->constrained()->cascadeOnUpdate()->cascadeOnDelete();
$table->string('flag_name');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_flags');
}
};

View File

@ -98,7 +98,7 @@
@if($bonusScore->judges->contains($judge->id)) @if($bonusScore->judges->contains($judge->id))
@continue @continue
@endif @endif
<option value="{{ $judge->id }}">{{ $judge->full_name() }} <option value="{{ $judge->id }}">{{ $judge->full_name() }}@if($judge->hasFlag('head_director')) (H) @endif
- {{ $judge->judging_preference }}</option> - {{ $judge->judging_preference }}</option>
@endforeach @endforeach
</select> </select>

View File

@ -102,7 +102,7 @@
<option>Add a judge</option> <option>Add a judge</option>
<optgroup label="Unassigned Judges"> <optgroup label="Unassigned Judges">
@foreach($usersWithoutRooms as $judge) {{-- skip judges alrady assigned to this audition --}} @foreach($usersWithoutRooms as $judge) {{-- skip judges alrady assigned to this audition --}}
<option value="{{ $judge->id }}">{{ $judge->full_name() }} <option value="{{ $judge->id }}">{{ $judge->full_name() }}@if($judge->hasFlag('head_director')) (H) @endif
- {{ $judge->judging_preference }}</option> - {{ $judge->judging_preference }}</option>
@endforeach @endforeach
</optgroup> </optgroup>
@ -111,7 +111,7 @@
@if($room->judges->contains($judge->id)) @if($room->judges->contains($judge->id))
@continue @continue
@endif @endif
<option value="{{ $judge->id }}">{{ $judge->full_name() }} <option value="{{ $judge->id }}">{{ $judge->full_name() }}@if($judge->hasFlag('head_director')) (H) @endif
- {{ $judge->judging_preference }}</option> - {{ $judge->judging_preference }}</option>
@endforeach @endforeach
</optgroup> </optgroup>

View File

@ -37,7 +37,16 @@
<div class="grid md:grid-cols-3 gap-3 mt-3"> <div class="grid md:grid-cols-3 gap-3 mt-3">
@foreach($school->directors as $director) @foreach($school->directors as $director)
<x-card.card> <x-card.card>
<x-card.heading>{{ $director->full_name() }}</x-card.heading> <x-card.heading>
<a href="{{route('admin.users.edit',$director)}}">{{ $director->full_name() }}</a>
@if($director->hasFlag('head_director'))
<x-slot:right_side><x-badge_pill>Head Director</x-badge_pill></x-slot:right_side>
@else
<x-slot:subheading>
<a href="{{route('admin.schools.set_head_director',['school'=>$school, 'user'=>$director])}}">[ Make Head Director ]</a>
</x-slot:subheading>
@endif
</x-card.heading>
<div class="ml-6"> <div class="ml-6">
<p class="py-2">{{ $director->cell_phone }}</p> <p class="py-2">{{ $director->cell_phone }}</p>
<p class="py-2"> <p class="py-2">

View File

@ -22,7 +22,7 @@
<x-form.field name="cell_phone" label_text="Cell Phone" colspan="3" value="{{ $user->cell_phone }}"/> <x-form.field name="cell_phone" label_text="Cell Phone" colspan="3" value="{{ $user->cell_phone }}"/>
<x-form.field name="judging_preference" label_text="Judging Preference" colspan="6" <x-form.field name="judging_preference" label_text="Judging Preference" colspan="6"
value="{{ $user->judging_preference }}"/> value="{{ $user->judging_preference }}"/>
<x-form.select name="school_id" colspan="6"> <x-form.select name="school_id" colspan="4">
<x-slot:label>School</x-slot:label> <x-slot:label>School</x-slot:label>
<option value="">No School</option> <option value="">No School</option>
@foreach ($schools as $school) @foreach ($schools as $school)
@ -31,6 +31,11 @@
@endforeach @endforeach
</x-form.select> </x-form.select>
<div class="col-span-2 pt-7">
<x-form.checkbox name="is_head" :checked="$user->hasFlag('head_director')">
<x-slot:label>Head Director</x-slot:label>
</x-form.checkbox>
</div>
<div class="col-span-3"> <div class="col-span-3">
<x-form.checkbox name="is_admin" :checked="$user->is_admin"> <x-form.checkbox name="is_admin" :checked="$user->is_admin">
<x-slot:label>Administrator</x-slot:label> <x-slot:label>Administrator</x-slot:label>

View File

@ -5,9 +5,10 @@
</head> </head>
<body> <body>
<h1>Hello, {{ $user->first_name }} {{ $user->last_name }}</h1> <h1>Hello, {{ $user->first_name }} {{ $user->last_name }}</h1>
<p>Your account has been created. Here are your login details:</p> <p>Your AuditionAdmin account for {{ auditionSetting('auditionAbbreviation') }} has been created. Here are your login details:</p>
<p><strong>Email:</strong> {{ $user->email }}</p> <p><strong>Email:</strong> {{ $user->email }}</p>
<p><strong>Password:</strong> {{ $password }}</p> <p><strong>Password:</strong> {{ $password }}</p>
<p><strong>Login at: </strong> {{route('login')}}</p>
<p>Please change your password after logging in for the first time.</p> <p>Please change your password after logging in for the first time.</p>
</body> </body>
</html> </html>

View File

@ -5,7 +5,7 @@
</x-card.heading> </x-card.heading>
<x-form.form method="POST" action="/schools" class="!mt-4"> <x-form.form method="POST" action="{{route('schools.store')}}" class="!mt-4">
<x-form.body-grid columns="6" class="max-w-full"> <x-form.body-grid columns="6" class="max-w-full">
<x-form.field name="name" label_text="School Name" colspan="6"/> <x-form.field name="name" label_text="School Name" colspan="6"/>
<x-form.field name="address" label_text="School Address" colspan="6"/> <x-form.field name="address" label_text="School Address" colspan="6"/>

View File

@ -1,4 +1,5 @@
<x-layout.app> <div x-data="{ showAddDirectorForm: false, changeHeadDirectorForm: false}">
<x-layout.app>
<x-slot:page_title>School Info - {{ $school->name }}</x-slot:page_title> <x-slot:page_title>School Info - {{ $school->name }}</x-slot:page_title>
<div class="mx-auto max-w-xl"> <div class="mx-auto max-w-xl">
@ -20,20 +21,97 @@
<x-card.info.row row_name="Directors"> <x-card.info.row row_name="Directors">
<ul> <ul>
@foreach($school->directors as $director) @foreach($school->directors as $director)
<li>{{ $director->full_name() }} - <a class='text-indigo-600' href="mailto:{{ $director->email }}">{{ $director->email }}</a></li> <li>
{{ $director->full_name() }}
@if($director->hasFlag('head_director')) <span class="font-semibold">(head)</span> @endif
-
<a class='text-indigo-600' href="mailto:{{ $director->email }}">{{ $director->email }}</a>
</li>
@endforeach @endforeach
</ul> </ul>
@if(auth()->user()->hasFlag('head_director'))
<div class="grid grid-cols-2 gap-2 mt-3">
<x-form.button type="button" @click="showAddDirectorForm=true">Add Director</x-form.button>
<x-form.button type="button" @click="changeHeadDirectorForm=true">Change Head</x-form.button>
</div>
@endif
</x-card.info.row> </x-card.info.row>
@if(auth()->user()->hasFlag('head_director'))
<x-card.info.row row_name="Associated Email Domains"> <x-card.info.row row_name="Associated Email Domains">
<p class="text-sm text-gray-400 border-b">Users with emails in these domains (the part after the @) that don't already have a school
will be able to join your school.</p>
<ul> <ul>
@foreach($school->emailDomains as $domain) @foreach($school->emailDomains as $domain)
<li>{{ $domain->domain }}</li> <li class="flex my-2">
<a href="{{route('schools.delete_domain',$domain)}}">
<x-icons.circled-x color="red" />
</a>
{{ $domain->domain }}
</li>
@endforeach @endforeach
<li class="border-t">
<p class="text-base">Add Domain</p>
<x-form.form method="post" action="{{route('schools.add_domain',$school)}}" class="-ml-8">
<div class="grid grid-cols-3">
<x-form.field name="domain" label_text="Domain (do not include @)" colspan="2" />
<x-form.button class="mt-6 ml-2">Add</x-form.button>
</div>
</x-form.form>
</li>
</ul> </ul>
</x-card.info.row> </x-card.info.row>
@endif
</x-card.info.body> </x-card.info.body>
</x-card.card> </x-card.card>
</div> </div>
</x-layout.app> </x-layout.app>
@if(auth()->user()->hasFlag('head_director'))
<x-modal-body showVar="showAddDirectorForm">
<x-slot:title>Add Director</x-slot:title>
<x-form.form method="POST" action="{{route('schools.add_director', $school)}}">
<x-form.body-grid>
<x-form.field name="first_name" label_text="First Name" colspan="3" />
<x-form.field name="last_name" label_text="Last Name" colspan="3" />
<x-form.field name="email" type="email" label_text="Email Address" colspan="3" />
<x-form.field name="cell_phone" label_text="Cell Phone" colspan="3" />
<x-form.field name="judging_preference" label_text="Judging Preference" colspan="6" />
</x-form.body-grid>
<x-form.footer submit-button-text="Add Director" />
</x-form.form>
</x-modal-body>
<x-modal-body showVar="changeHeadDirectorForm">
<x-slot:title>Change Head Director</x-slot:title>
{{-- Warming Message --}}
<div class="rounded-md bg-red-50 p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.06 1.06L10 11.06l1.72 1.72a.75.75 0 101.06-1.06L11.06 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.94 8.28 7.22z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">WARNING!!!</h3>
<div class="mt-2 text-sm text-red-700">
<p>After making another director head, you will no longer have access to head director functions</p>
<p>This action cannot be undone</p>
</div>
</div>
</div>
</div>
<p class="mt-3 border-b border-t py-3">Which director should be the head director at {{$school->name}}? (click to set)</p>
<x-card.list.body>
@foreach($school->directors as $director)
@continue($director->id === auth()->user()->id)
<x-card.list.row>
<a href="{{route('schools.set_head_director',['school'=>$school,'user'=>$director])}}">
{{ $director->full_name() }} < {{$director->email }} >
</a>
</x-card.list.row>
@endforeach
</x-card.list.body>
</x-modal-body>
@endif
</div>

View File

@ -150,6 +150,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
Route::post('/', 'store')->name('admin.schools.store'); Route::post('/', 'store')->name('admin.schools.store');
Route::delete('/domain/{domain}', 'destroy_domain')->name('admin.schools.destroy_domain'); Route::delete('/domain/{domain}', 'destroy_domain')->name('admin.schools.destroy_domain');
Route::delete('/{school}', 'destroy')->name('admin.schools.destroy'); Route::delete('/{school}', 'destroy')->name('admin.schools.destroy');
Route::get('/{school}/set_head_director/{user}', 'setHeadDirector')->name('admin.schools.set_head_director');
}); });

View File

@ -52,6 +52,10 @@ Route::middleware(['auth', 'verified'])->controller(SchoolController::class)->gr
Route::get('/schools/{school}/edit', 'edit')->name('schools.edit'); Route::get('/schools/{school}/edit', 'edit')->name('schools.edit');
Route::get('/schools/{school}', 'show')->name('schools.show'); Route::get('/schools/{school}', 'show')->name('schools.show');
Route::patch('/schools/{school}', 'update')->name('schools.update'); Route::patch('/schools/{school}', 'update')->name('schools.update');
Route::post('schools/{school}/add_director', 'addDirector')->name('schools.add_director');
Route::get('/schools/{school}/set_head_director/{user}', 'setHeadDirector')->name('schools.set_head_director');
Route::post('/schools/{school}/add_domain', 'addDomain')->name('schools.add_domain');
Route::get('/schools/delete_domain/{domain}', 'deleteDomain')->name('schools.delete_domain');
}); });
// Doubler Related Routes // Doubler Related Routes

View File

@ -0,0 +1,51 @@
<?php
use App\Actions\Schools\SetHeadDirector;
use App\Exceptions\AuditionAdminException;
use App\Models\School;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->setter = app(SetHeadDirector::class);
});
it('sets a head director flag for a user with a school', function () {
// Arrange
$school = School::factory()->create();
$user = User::factory()->create(['school_id' => $school->id]);
$this->setter->setHeadDirector($user);
$this->assertDatabaseHas('user_flags', [
'user_id' => $user->id,
'flag_name' => 'head_director',
]);
});
it('throws an error if the user has no school', function () {
// Arrange
$user = User::factory()->create();
// Act & Assert
$this->setter->setHeadDirector($user);
})->throws(AuditionAdminException::class, 'User is not associated with a school');
it('removes the head director flag from any other users as the school', function () {
// Arrange
$school = School::factory()->create();
$oldHead = User::factory()->create(['school_id' => $school->id]);
$newHead = User::factory()->create(['school_id' => $school->id]);
$oldHead->addFlag('head_director');
$this->assertDatabaseHas('user_flags', [
'user_id' => $oldHead->id,
'flag_name' => 'head_director',
]);
// Act
$this->setter->setHeadDirector($newHead);
$this->assertDatabaseHas('user_flags', [
'user_id' => $newHead->id,
'flag_name' => 'head_director',
]);
$this->assertDatabaseMissing('user_flags', [
'user_id' => $oldHead->id,
'flag_name' => 'head_director',
]);
});

View File

@ -7,6 +7,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\actingAs; use function Pest\Laravel\actingAs;
use function Pest\Laravel\assertDatabaseHas; use function Pest\Laravel\assertDatabaseHas;
use function Pest\Laravel\delete; use function Pest\Laravel\delete;
use function Pest\Laravel\from;
use function Pest\Laravel\get; use function Pest\Laravel\get;
use function Pest\Laravel\patch; use function Pest\Laravel\patch;
@ -150,7 +151,7 @@ it('allows an administrator to modify a user', function () {
'school_id' => $newSchool->id, 'school_id' => $newSchool->id,
]; ];
// Act // Act
$response = patch(route('admin.users.update', $this->users[0]), $newData); $response = from(route('admin.users.index'))->patch(route('admin.users.update', $this->users[0]), $newData);
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
$response $response
->assertSessionHasNoErrors() ->assertSessionHasNoErrors()
@ -186,7 +187,7 @@ it('allows a users school to be set to no school', function () {
]; ];
actAsAdmin(); actAsAdmin();
// Act & Assert // Act & Assert
$response = patch(route('admin.users.update', $user), $newData); $response = from(route('admin.users.index'))->patch(route('admin.users.update', $user), $newData);
/** @noinspection PhpUnhandledExceptionInspection */ /** @noinspection PhpUnhandledExceptionInspection */
$response $response
->assertSessionHasNoErrors() ->assertSessionHasNoErrors()