feat: implement Assignment and Submission management; add controllers, requests, and services for assignment functionality
This commit is contained in:
parent
024dc4dd5a
commit
573d834905
@ -178,7 +178,42 @@ ## 4. Controller, Request, dan Service
|
||||
```
|
||||
- Otorisasi berbasis permission (Spatie) dicek di dua tempat:
|
||||
- Route-level: middleware `permission:create-xxx` dipasang per-route/per-group.
|
||||
- Object-level (mis. user hanya boleh mengubah record miliknya sendiri): di `authorize()` milik FormRequest, kombinasikan `$this->user()->can('permission-name')` dengan pengecekan kepemilikan record.
|
||||
- Object-level (mis. user hanya boleh mengubah record miliknya sendiri): di `authorize()` milik FormRequest, kombinasikan `$this->user()->can('permission-name')` dengan pengecekan kepemilikan record. Untuk route tanpa FormRequest (mis. `destroy()` yang tidak butuh validasi input), pengecekan yang sama dilakukan inline dengan `abort_if()`/`abort_unless()` di Controller.
|
||||
- **Predikat kepemilikan itu sendiri ditaruh sebagai method di Model (`User`, atau model pemilik lain yang relevan), bukan didefinisikan ulang di tiap Controller/FormRequest yang butuh.** Satu aturan bisnis harus punya satu sumber kebenaran — supaya konsisten dan gampang di-test. Penamaan: `is<Peran>Of($target)` untuk peran yang punya nama (mis. `isAdvisorOf`), atau `canManage<Model>($target)` untuk gate umum "boleh mengubah/menghapus record ini".
|
||||
```php
|
||||
// Bad — predikat yang sama diulang di FormRequest DAN Controller
|
||||
// (FormRequest)
|
||||
public function authorize(): bool
|
||||
{
|
||||
$assignment = $this->route('assignment');
|
||||
return $this->user()->can('update-assignments')
|
||||
&& (! $this->user()->hasRole('dosen') || $assignment->courseClass->lecturer_id === $this->user()->lecturer?->id);
|
||||
}
|
||||
// (Controller::destroy(), butuh predikat yang sama karena tidak ada FormRequest)
|
||||
private function abortUnlessLecturerOwnsAssignment(Assignment $assignment): void
|
||||
{
|
||||
$user = request()->user();
|
||||
abort_if($user->hasRole('dosen') && $assignment->courseClass?->lecturer_id !== $user->lecturer?->id, 403);
|
||||
}
|
||||
|
||||
// Good — predikat di Model, dipakai dari FormRequest maupun Controller
|
||||
// (User model)
|
||||
public function canManageAssignment(Assignment $assignment): bool
|
||||
{
|
||||
if ($this->hasRole(UserRole::Dosen->value)) {
|
||||
return $assignment->courseClass->lecturer_id === $this->lecturer?->id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// (FormRequest)
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update-assignments')
|
||||
&& $this->user()->canManageAssignment($this->route('assignment'));
|
||||
}
|
||||
// (Controller::destroy())
|
||||
abort_unless(request()->user()->canManageAssignment($assignment), 403);
|
||||
```
|
||||
- Constructor injection untuk dependency (Service, Model), jangan `new Xxx` langsung di dalam method:
|
||||
```php
|
||||
// Bad
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses;
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\AcademicClasses\AssignmentRequest;
|
||||
use App\Http\Requests\Admin\AcademicClasses\Assignment\AssignmentRequest;
|
||||
use App\Http\Requests\PaginatedRequest;
|
||||
use App\Models\Assignment;
|
||||
use App\Services\Admin\AcademicClasses\AssignmentService;
|
||||
use App\Services\Admin\Manage\CourseClassService;
|
||||
use App\Services\Admin\AcademicClasses\Assignment\AssignmentService;
|
||||
use App\Services\Admin\Manage\CourseClass\CourseClassService;
|
||||
use App\Services\Admin\Master\AcademicTermService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
@ -63,6 +63,8 @@ public function update(AssignmentRequest $request, Assignment $assignment): Redi
|
||||
|
||||
public function destroy(Assignment $assignment): RedirectResponse
|
||||
{
|
||||
abort_unless(request()->user()->canManageAssignment($assignment), 403);
|
||||
|
||||
$this->service->delete($assignment);
|
||||
|
||||
return Inertia::flash('toast', ['type' => 'success', 'message' => 'Tugas berhasil dihapus.'])->back();
|
||||
@ -1,13 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses;
|
||||
namespace App\Http\Controllers\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\AcademicClasses\GradeSubmissionRequest;
|
||||
use App\Http\Requests\Admin\AcademicClasses\SubmitAssignmentRequest;
|
||||
use App\Http\Requests\Admin\AcademicClasses\Assignment\SubmitAssignmentRequest;
|
||||
use App\Http\Requests\Admin\AcademicClasses\Assignment\GradeSubmissionRequest;
|
||||
use App\Models\Assignment;
|
||||
use App\Models\Submission;
|
||||
use App\Services\Admin\AcademicClasses\SubmissionService;
|
||||
use App\Services\Admin\AcademicClasses\Assignment\SubmissionService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@ -20,7 +20,7 @@ public function __construct(
|
||||
|
||||
public function index(Assignment $assignment): Response
|
||||
{
|
||||
$this->abortUnlessLecturerOwnsAssignment($assignment);
|
||||
abort_unless(request()->user()->canManageAssignment($assignment), 403);
|
||||
|
||||
$assignment->load(['courseClass.course', 'courseClass.lecturer.user.profile', 'courseClass.academicTerm']);
|
||||
|
||||
@ -45,17 +45,4 @@ public function submit(SubmitAssignmentRequest $request, Assignment $assignment)
|
||||
|
||||
return to_route('admin.academic-classes.assignments.index');
|
||||
}
|
||||
|
||||
/**
|
||||
* A dosen may only manage submissions for classes they lecture.
|
||||
*/
|
||||
private function abortUnlessLecturerOwnsAssignment(Assignment $assignment): void
|
||||
{
|
||||
$user = request()->user();
|
||||
|
||||
abort_if(
|
||||
$user->hasRole('dosen') && $assignment->courseClass?->lecturer_id !== $user->lecturer?->id,
|
||||
403,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses;
|
||||
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Enums\AssignmentStatus;
|
||||
use App\Enums\UserRole;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
@ -10,7 +11,12 @@ class AssignmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can($this->isMethod('post') ? 'create-assignments' : 'update-assignments');
|
||||
if ($this->isMethod('post')) {
|
||||
return $this->user()->can('create-assignments');
|
||||
}
|
||||
|
||||
return $this->user()->can('update-assignments')
|
||||
&& $this->user()->canManageAssignment($this->route('assignment'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
@ -19,7 +25,7 @@ public function rules(): array
|
||||
|
||||
$courseClassRule = Rule::exists('course_classes', 'id');
|
||||
|
||||
if ($user->hasRole('dosen')) {
|
||||
if ($user->hasRole(UserRole::Dosen->value)) {
|
||||
$courseClassRule->where('lecturer_id', $user->lecturer?->id);
|
||||
}
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class GradeSubmissionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return $this->user()->can('update-assignment-submissions')
|
||||
&& $this->user()->canManageAssignment($this->route('assignment'));
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'score' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses;
|
||||
namespace App\Http\Requests\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Enums\AssignmentStatus;
|
||||
use App\Enums\RegistrationStatus;
|
||||
@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin\AcademicClasses;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class GradeSubmissionRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
if (! $this->user()->can('update-assignment-submissions')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$user = $this->user();
|
||||
$assignment = $this->route('assignment');
|
||||
|
||||
if ($user->hasRole('dosen') && $assignment?->courseClass?->lecturer_id !== $user->lecturer?->id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'score' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -121,6 +121,19 @@ public function canManageMaterial(Material $material): bool
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* A dosen may only manage assignments (update/delete) and their submissions
|
||||
* (view/grade) for classes they lecture.
|
||||
*/
|
||||
public function canManageAssignment(Assignment $assignment): bool
|
||||
{
|
||||
if ($this->hasRole(UserRole::Dosen->value)) {
|
||||
return $assignment->courseClass->lecturer_id === $this->lecturer?->id;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isAdvisorOf(Student $student): bool
|
||||
{
|
||||
return $this->hasRole(UserRole::Dosen->value) && $this->lecturer && $student->academic_advisor_id === $this->lecturer->id;
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\AcademicClasses;
|
||||
namespace App\Services\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Enums\AssignmentStatus;
|
||||
use App\Enums\RegistrationStatus;
|
||||
use App\Enums\UserRole;
|
||||
use App\Models\Assignment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
@ -21,15 +22,15 @@ public function paginated(User $user, int $perPage = 25, string $search = '', ?i
|
||||
])
|
||||
->with(['courseClass' => fn ($q) => $q->withCount('enrollments')
|
||||
->with(['course:id,code,name', 'academicTerm:id,academic_year,semester,start_date,end_date'])])
|
||||
->when($user->hasRole('mahasiswa'), fn ($q) => $q->with(['submissions' => function ($q) use ($user) {
|
||||
->when($user->hasRole(UserRole::Mahasiswa->value), fn ($q) => $q->with(['submissions' => function ($q) use ($user) {
|
||||
$q->select(['id', 'assignment_id', 'student_id', 'notes', 'status', 'submitted_at'])
|
||||
->where('student_id', $user->student?->id);
|
||||
}]))
|
||||
->when($search, fn ($q) => $q->where('title', 'like', "%{$search}%"))
|
||||
->when($courseClassId, fn ($q) => $q->where('course_class_id', $courseClassId))
|
||||
->when($academicTermId, fn ($q) => $q->whereHas('courseClass', fn ($q) => $q->where('academic_term_id', $academicTermId)))
|
||||
->when($user->hasRole('dosen'), fn ($q) => $q->whereHas('courseClass', fn ($q) => $q->where('lecturer_id', $user->lecturer?->id)))
|
||||
->when($user->hasRole('mahasiswa'), fn ($q) => $q->whereHas('courseClass', function ($q) use ($user) {
|
||||
->when($user->hasRole(UserRole::Dosen->value), fn ($q) => $q->whereHas('courseClass', fn ($q) => $q->where('lecturer_id', $user->lecturer?->id)))
|
||||
->when($user->hasRole(UserRole::Mahasiswa->value), fn ($q) => $q->whereHas('courseClass', function ($q) use ($user) {
|
||||
$q->whereHas('registrations', function ($q) use ($user) {
|
||||
$q->where('student_id', $user->student?->id)
|
||||
->whereHas('submission', fn ($q) => $q->where('status', RegistrationStatus::Approved));
|
||||
@ -58,12 +59,13 @@ public function create(array $data, ?UploadedFile $file): Assignment
|
||||
|
||||
public function update(Assignment $assignment, array $data, ?UploadedFile $file): Assignment
|
||||
{
|
||||
$assignment->course_class_id = $data['course_class_id'];
|
||||
$assignment->title = $data['title'];
|
||||
$assignment->description = $data['description'] ?? null;
|
||||
$assignment->deadline = $data['deadline'];
|
||||
$assignment->status = $data['status'] ?? $assignment->status;
|
||||
$assignment->update();
|
||||
$assignment->update([
|
||||
'course_class_id' => $data['course_class_id'],
|
||||
'title' => $data['title'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'deadline' => $data['deadline'],
|
||||
'status' => $data['status'] ?? $assignment->status,
|
||||
]);
|
||||
|
||||
if ($file) {
|
||||
$assignment->addMedia($file)->toMediaCollection('assignment_attachment');
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin\AcademicClasses;
|
||||
namespace App\Services\Admin\AcademicClasses\Assignment;
|
||||
|
||||
use App\Enums\SubmissionStatus;
|
||||
use App\Models\Assignment;
|
||||
@ -94,15 +94,17 @@ export default function SubmissionIndex({ assignment, submissions }: Props) {
|
||||
|
||||
const columns: ColumnDef<Submission>[] = [
|
||||
{
|
||||
accessorKey: 'student.student_number',
|
||||
id: 'identity',
|
||||
header: () => <span>NIM</span>,
|
||||
cell: ({ row }) => row.original.student?.student_number ?? '-',
|
||||
},
|
||||
{
|
||||
accessorKey: 'student.user.profile.full_name',
|
||||
header: () => <span>Nama Mahasiswa</span>,
|
||||
cell: ({ row }) =>
|
||||
row.original.student?.user?.profile?.full_name ?? '-',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-col">
|
||||
<span>{row.original.student?.student_number ?? '-'}</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{row.original.student?.user?.profile?.full_name ??
|
||||
'-'}
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
|
||||
@ -42,6 +42,4 @@ export type Assignment = {
|
||||
graded_submissions_count: number;
|
||||
/** Only present for the logged-in mahasiswa: their own submission, if any. */
|
||||
submissions?: MySubmission[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\AcademicClasses\AssignmentController;
|
||||
use App\Http\Controllers\Admin\AcademicClasses\Assignment\AssignmentController;
|
||||
use App\Http\Controllers\Admin\AcademicClasses\Assignment\SubmissionController;
|
||||
use App\Http\Controllers\Admin\AcademicClasses\AttendanceController;
|
||||
use App\Http\Controllers\Admin\AcademicClasses\MaterialController;
|
||||
use App\Http\Controllers\Admin\AcademicClasses\ScheduleController;
|
||||
use App\Http\Controllers\Admin\AcademicClasses\SubmissionController;
|
||||
use App\Http\Controllers\Admin\Developer\LogController;
|
||||
use App\Http\Controllers\Admin\Developer\RolePermissionController;
|
||||
use App\Http\Controllers\Admin\FeedbackController;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user