Finally, we get to the actual point of this small application: Task management.
Compared to the Task Model in previous lessons of this course, we added a few more fields: assigned_to_user_id
(clinic doctor/staff) and patient_id
:
Tasks Migration:
$table->foreignId('assigned_to_user_id')->constrained('users');$table->foreignId('patient_id')->constrained('users');
Then, I added them to the Model, too:
app/Models/Task.php
use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo;use Illuminate\Database\Eloquent\Factories\HasFactory; class Task extends Model{ use HasFactory; protected $fillable = [ 'name', 'due_date', 'assigned_to_user_id', 'patient_id', 'team_id', ]; public function assignee(): BelongsTo { return $this->belongsTo(User::class, 'assigned_to_user_id'); } public function patient(): BelongsTo { return $this->belongsTo(User::class, 'patient_id'); }}
Then, we also changed the Factory with the new columns in mind.
database/factories/TaskFactory.php
use App\Models\User;use Illuminate\Database\Eloquent\Factories\Factory; class TaskFactory extends Factory{ public function definition(): array { $randomAssignee = collect([ User::factory()->doctor(), User::factory()->staff(), ])->random(); return [ 'name' => fake()->text(30), 'due_date' => now()->addDays(rand(1, 100)), 'assigned_to_user_id' => $randomAssignee, 'patient_id' => User::factory()->patient(), ]; }}
Now, who can manage tasks? Traditionally, let's start with Policy:
app/Policies/TaskPolicy.php
use App\Enums\Role;use App\Models\Task;use App\Models\User;use App\Enums\Permission; class TaskPolicy{ public function viewAny(User $user): bool { return $user->hasPermissionTo(Permission::LIST_TASK); } public function create(User $user): bool { return $user->hasPermissionTo(Permission::CREATE_TASK); } public function update(User $user, Task $task): bool { return $user->hasPermissionTo(Permission::EDIT_TASK); } public function delete(User $user, Task $task): bool { return $user->hasPermissionTo(Permission::DELETE_TASK); }}
You don't see the filter by team here, right? The approach we took here is to filter them on the Eloquent level, with global scope.
In fact, it's a 2-in-1 scope:
tasks.team_id
tasks.patient_id
app/Models/Task.php
use App\Enums\Role;use Illuminate\Database\Eloquent\Builder; class Task extends Model{ // ... protected static function booted(): void { static::addGlobalScope('team-tasks', function (Builder $query) { if (auth()->check()) { $query->where('team_id', auth()->user()->current_team_id); if (auth()->user()->hasRole(Role::Patient)) { $query->where('patient_id', auth()->user()->id); } } }); }}
As you can see, we're checking for the Patient role and filtering the patient_id
in that case.
Next, the Controller, which will look similar to the TeamController
and UserController
from before:
app/Http/Controllers/TaskController.php
use App\Enums\Role;use App\Models\Task;use App\Models\User;use Illuminate\View\View;use Illuminate\Http\Request;use Illuminate\Support\Facades\Gate;use Illuminate\Http\RedirectResponse; class TaskController extends Controller{ public function index(): View { Gate::authorize('viewAny', Task::class); $tasks = Task::with('assignee', 'patient')->get(); return view('tasks.index', compact('tasks')); } public function create(): View { Gate::authorize('create', Task::class); $assignees = User::whereRelation('roles', 'name', '=', Role::Doctor->value) ->orWhereRelation('roles', 'name', '=', Role::Staff->value) ->pluck('name', 'id'); $patients = User::whereRelation('roles', 'name', '=', Role::Patient->value)->pluck('name', 'id'); return view('tasks.create', compact('patients', 'assignees')); } public function store(Request $request): RedirectResponse { Gate::authorize('create', Task::class); Task::create($request->only('name', 'due_date', 'assigned_to_user_id', 'patient_id')); return redirect()->route('tasks.index'); } public function edit(Task $task): View { Gate::authorize('update', $task); $assignees = User::whereRelation('roles', 'name', '=', Role::Doctor->value) ->orWhereRelation('roles', 'name', '=', Role::Staff->value) ->pluck('name', 'id'); $patients = User::whereRelation('roles', 'name', '=', Role::Patient->value)->pluck('name', 'id'); return view('tasks.edit', compact('task', 'assignees', 'patients')); } public function update(Request $request, Task $task): RedirectResponse { Gate::authorize('update', $task); $task->update($request->only('name', 'due_date', 'assigned_to_user_id', 'patient_id')); return redirect()->route('tasks.index'); } public function destroy(Task $task): RedirectResponse { Gate::authorize('delete', $task); $task->delete(); return redirect()->route('tasks.index'); }}
By now, you probably recognize the pattern of using Gate::authorize()
in Controllers.
Finally, we write the tests for all of the scenarios. This will be one of the longer Test files, but it is pretty readable and self-explanatory.
tests/Feature/TaskTest.php:
use App\Models\User;use App\Models\Team;use App\Models\Task;use Illuminate\Support\Collection;use function Pest\Laravel\actingAs; it('allows clinic admin and staff to access create task page', function (User $user) { actingAs($user) ->get(route('tasks.create')) ->assertOk();})->with([ fn () => User::factory()->clinicAdmin()->create(), fn () => User::factory()->doctor()->create(), fn () => User::factory()->staff()->create(),]); it('does not allow patient to access create task page', function () { $user = User::factory()->patient()->create(); actingAs($user) ->get(route('tasks.create')) ->assertForbidden();}); it('allows clinic admin and staff to enter update page for any task in their team', function (User $user) { $team = Team::first(); $clinicAdmin = User::factory()->clinicAdmin()->create(); $clinicAdmin->update(['current_team_id' => $team->id]); setPermissionsTeamId($team->id); $clinicAdmin->unsetRelation('roles')->unsetRelation('permissions'); $task = Task::factory()->create([ 'team_id' => $team->id, ]); actingAs($user) ->get(route('tasks.edit', $task)) ->assertOk();})->with([ fn () => User::factory()->clinicAdmin()->create(), fn () => User::factory()->doctor()->create(), fn () => User::factory()->staff()->create(),]); it('does not allow administrator and manager to enter update page for other teams task', function (User $user) { $team = Team::factory()->create(); $task = Task::factory()->create([ 'team_id' => $team->id, ]); actingAs($user) ->get(route('tasks.edit', $task)) ->assertNotFound();})->with([ fn () => User::factory()->clinicAdmin()->create(), fn () => User::factory()->doctor()->create(), fn () => User::factory()->staff()->create(),]); it('allows administrator and manager to update any task in their team', function (User $user) { $team = Team::first(); $otherUser = User::factory()->clinicAdmin()->create(); $otherUser->update(['current_team_id' => $team->id]); setPermissionsTeamId($team->id); $otherUser->unsetRelation('roles')->unsetRelation('permissions'); $task = Task::factory()->create([ 'team_id' => $team->id, ]); actingAs($user) ->put(route('tasks.update', $task), [ 'name' => 'updated task name', ]) ->assertRedirect(); expect($task->refresh()->name)->toBe('updated task name');})->with([ fn () => User::factory()->clinicAdmin()->create(), fn () => User::factory()->doctor()->create(), fn () => User::factory()->staff()->create(),]); it('allows clinic admin and staff to delete task for his team', function (User $user) { User::factory()->create(['current_team_id' => $user->current_team_id]); $task = Task::factory()->create([ 'team_id' => $user->current_team_id, ]); actingAs($user) ->delete(route('tasks.destroy', $task)) ->assertRedirect(); expect(Task::count())->toBeInt()->toBe(0);})->with([ fn () => User::factory()->clinicAdmin()->create(), fn () => User::factory()->staff()->create(),]); it('does not allow doctor to delete tasks', function () { $doctor = User::factory()->doctor()->create(); User::factory()->create(['current_team_id' => $doctor->current_team_id]); $task = Task::factory()->create([ 'team_id' => $doctor->current_team_id, ]); actingAs($doctor) ->delete(route('tasks.destroy', $task)) ->assertForbidden(); expect(Task::count())->toBeInt()->toBe(1);}); it('does not allow super admin and admin to delete task for other team', function (User $user) { $team = Team::factory()->create(); $taskUser = User::factory()->clinicAdmin()->create(); $taskUser->update(['current_team_id' => $team->id]); $task = Task::factory()->create([ 'team_id' => $taskUser->current_team_id, ]); actingAs($user) ->delete(route('tasks.destroy', $task)) ->assertNotFound();})->with([ fn () => User::factory()->clinicAdmin()->create(), fn () => User::factory()->doctor()->create(), fn () => User::factory()->staff()->create(),]); it('shows users with a role of doctor and staff as assignees', function () { $doctor = User::factory()->doctor()->create(); $staff = User::factory()->staff()->create(); $clinicAdmin = User::factory()->clinicAdmin()->create(); $masterAdmin = User::factory()->masterAdmin()->create(); $patient = User::factory()->patient()->create(); actingAs($clinicAdmin) ->get(route('tasks.create')) ->assertViewHas('assignees', function (Collection $assignees) use ($doctor, $staff, $masterAdmin, $clinicAdmin, $patient): bool { return $assignees->contains(fn (string $assignee) => $assignee === $doctor->name || $assignee === $staff->name ) && $assignees->doesntContain(fn (string $assignee) => $assignee === $masterAdmin->name || $assignee === $clinicAdmin->name || $assignee === $patient->name ); });}); it('shows users with a role of patient as patients', function () { $doctor = User::factory()->doctor()->create(); $staff = User::factory()->staff()->create(); $clinicAdmin = User::factory()->clinicAdmin()->create(); $masterAdmin = User::factory()->masterAdmin()->create(); $patient = User::factory()->patient()->create(); actingAs($clinicAdmin) ->get(route('tasks.create')) ->assertViewHas('patients', function (Collection $patients) use ($patient, $doctor, $staff, $masterAdmin, $clinicAdmin): bool { return $patients->contains(fn (string $value) => $value === $patient->name) && $patients->doesntContain(fn (string $value) => $value === $doctor->name || $value === $staff->name || $value === $masterAdmin->name || $value === $clinicAdmin->name ); });}); it('shows only teams tasks for doctor, staff, and clinic admin', function (User $user) { $seeTask = Task::factory()->create(['team_id' => $user->current_team_id]); $dontSeeTask = Task::factory()->create(['team_id' => Team::factory()->create()->id]); actingAs($user) ->get(route('tasks.index')) ->assertOk() ->assertSeeText($seeTask->name) ->assertDontSeeText($dontSeeTask->name);})->with([ fn() => User::factory()->clinicAdmin()->create(), fn() => User::factory()->doctor()->create(), fn() => User::factory()->staff()->create(),]); it('shows patient only his tasks', function () { $patient = User::factory()->patient()->create(); $seeTask = Task::factory()->create([ 'team_id' => $patient->current_team_id, 'patient_id' => $patient->id, ]); $dontSeeTask = Task::factory()->create(['team_id' => Team::factory()->create()->id]); actingAs($patient) ->get(route('tasks.index')) ->assertOk() ->assertSeeText($seeTask->name) ->assertDontSeeText($dontSeeTask->name);});
Complete code in the GitHub repository.