diff --git a/app/Filament/Resources/Learning/Assignments/Pages/ListAssignments.php b/app/Filament/Resources/Learning/Assignments/Pages/ListAssignments.php index 18411b7..61bc771 100644 --- a/app/Filament/Resources/Learning/Assignments/Pages/ListAssignments.php +++ b/app/Filament/Resources/Learning/Assignments/Pages/ListAssignments.php @@ -68,6 +68,15 @@ public function deleteAssignmentAction(): Action public function assignments(): Collection { $studentProfile = auth()->user()->student; + + if (! $studentProfile) { + return Assignment::with(['course', 'assignmentSubmissions', 'studyGroups.students']) + ->whereHas('course', function ($q) { + $q->where('semester', app(GeneralSettings::class)->current_semester); + }) + ->get(); + } + $pinnedIds = $this->pinnedIds; $assignments = Assignment::with(['course', 'assignmentSubmissions' => function ($q) use ($studentProfile) { @@ -130,6 +139,11 @@ private function getAssignmentPriority($assignment): int public function assignmentCards() { $studentProfile = auth()->user()->student; + + if (! $studentProfile) { + return collect(); + } + $pinnedIds = $this->pinnedIds; return $this->assignments()->map(function ($assignment) use ($studentProfile, $pinnedIds) { @@ -224,6 +238,10 @@ public function pinnedIds(): array { $studentProfile = auth()->user()->student; + if (! $studentProfile) { + return []; + } + return AssignmentPin::where('student_id', $studentProfile->id) ->pluck('assignment_id') ->toArray(); diff --git a/database/factories/AssignmentFactory.php b/database/factories/AssignmentFactory.php new file mode 100644 index 0000000..d3862c4 --- /dev/null +++ b/database/factories/AssignmentFactory.php @@ -0,0 +1,25 @@ + Course::factory(), + 'class_session_id' => null, + 'title' => $this->faker->sentence(), + 'description' => $this->faker->paragraph(), + 'due_date' => $this->faker->dateTimeBetween('now', '+1 month'), + 'type' => $this->faker->randomElement(AssignmentType::cases()), + ]; + } +} diff --git a/tests/Feature/AssignmentTest.php b/tests/Feature/AssignmentTest.php new file mode 100644 index 0000000..c94c70d --- /dev/null +++ b/tests/Feature/AssignmentTest.php @@ -0,0 +1,243 @@ +value); + Role::findOrCreate(RoleEnum::Kosma->value); + Role::findOrCreate(RoleEnum::Student->value); + + $permissions = [ + 'ViewAny:Assignment', + 'View:Assignment', + 'Create:Assignment', + 'Update:Assignment', + 'Delete:Assignment', + ]; + + foreach ($permissions as $permission) { + Permission::findOrCreate($permission); + } + + Storage::fake('public'); + $this->currentSemester = app(GeneralSettings::class)->current_semester; +}); + +describe('Assignment Authorization', function () { + it('restricts actions to Developer and Kosma, while allowing students to view and submit', function () { + $developer = User::factory()->create(); + $developer->assignRole(RoleEnum::Developer); + $developer->givePermissionTo(['ViewAny:Assignment', 'Create:Assignment']); + + $studentUser = User::factory()->create(); + $studentUser->assignRole(RoleEnum::Student); + $studentUser->givePermissionTo(['ViewAny:Assignment']); + $student = Student::factory()->create(['user_id' => $studentUser->id]); + + $this->actingAs($developer); + Livewire::test(ListAssignments::class) + ->assertActionVisible('createAssignment'); + + $this->actingAs($studentUser); + Livewire::test(ListAssignments::class) + ->assertActionHidden('createAssignment'); + }); +}); + +describe('Assignment Model', function () { + it('has relationships and supports soft deletes', function () { + $assignment = Assignment::factory()->create(); + + expect($assignment->course)->toBeInstanceOf(Course::class); + expect($assignment->assignmentSubmissions())->toBeInstanceOf(\Illuminate\Database\Eloquent\Relations\HasMany::class); + + $assignment->delete(); + expect($assignment->refresh()->trashed())->toBeTrue(); + }); +}); + +describe('Assignment CRUD and Rules', function () { + beforeEach(function () { + $user = User::factory()->create(); + $user->assignRole(RoleEnum::Developer); + $user->givePermissionTo(['ViewAny:Assignment', 'Create:Assignment', 'Update:Assignment', 'Delete:Assignment']); + $this->actingAs($user); + }); + + it('can create an assignment', function () { + $course = Course::factory()->create(['semester' => $this->currentSemester]); + $students = Student::factory()->count(2)->create(); + + Livewire::test(CreateAssignment::class) + ->fillForm([ + 'title' => 'New Test Assignment', + 'course_id' => $course->id, + 'due_date' => now()->addDays(7)->toDateTimeString(), + 'type' => AssignmentType::Individual->value, + 'student_ids' => $students->pluck('id')->toArray(), + ]) + ->call('create') + ->assertHasNoFormErrors(); + + $this->assertDatabaseHas('assignments', [ + 'title' => 'New Test Assignment', + 'course_id' => $course->id, + ]); + }); + + it('enforces one assignment per session rule', function () { + $course = Course::factory()->create(['semester' => $this->currentSemester]); + $session = ClassSession::factory()->create(['course_id' => $course->id]); + + Assignment::factory()->create([ + 'course_id' => $course->id, + 'class_session_id' => $session->id, + 'type' => AssignmentType::Individual, + ]); + + Livewire::test(CreateAssignment::class) + ->set('data.course_id', $course->id) + ->assertFormSet(['class_session_id' => null]); + }); +}); + +describe('Assignment Submission Logic', function () { + it('allows individual submission', function () { + $user = User::factory()->create(); + $user->assignRole(RoleEnum::Student); + $user->givePermissionTo(['ViewAny:Assignment']); + $student = Student::factory()->create(['user_id' => $user->id]); + + $assignment = Assignment::factory()->create([ + 'type' => AssignmentType::Individual, + 'due_date' => now()->addDays(1), + ]); + $assignment->students()->attach($student->id); + $assignment->course->update(['semester' => $this->currentSemester]); + + $file = UploadedFile::fake()->create('submission.pdf', 100, 'application/pdf'); + + $this->actingAs($user); + Livewire::test(SubmitAssignmentPage::class, ['record' => $assignment]) + ->assertSuccessful() + ->set('file', $file) + ->call('submit') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('assignment_submissions', [ + 'assignment_id' => $assignment->id, + 'student_id' => $student->id, + ]); + }); + + it('only allows group leader to submit group assignments', function () { + $course = Course::factory()->create(['semester' => $this->currentSemester]); + + $leaderUser = User::factory()->create(); + $leaderUser->assignRole(RoleEnum::Student); + $leaderUser->givePermissionTo(['ViewAny:Assignment']); + $leader = Student::factory()->create(['user_id' => $leaderUser->id]); + + $memberUser = User::factory()->create(); + $memberUser->assignRole(RoleEnum::Student); + $memberUser->givePermissionTo(['ViewAny:Assignment']); + $member = Student::factory()->create(['user_id' => $memberUser->id]); + + $group = StudyGroup::factory()->create([ + 'leader_id' => $leader->id, + ]); + $group->courses()->attach($course->id); + $group->students()->attach($member->id); + + $assignment = Assignment::factory()->create([ + 'course_id' => $course->id, + 'type' => AssignmentType::Group, + 'due_date' => now()->addDays(1), + ]); + $assignment->studyGroups()->attach($group->id); + + // Test Leader can submit + $this->actingAs($leaderUser); + Livewire::test(SubmitAssignmentPage::class, ['record' => $assignment]) + ->assertSuccessful() + ->assertSet('canSubmit', true); + + // Test Member cannot submit + $this->actingAs($memberUser); + Livewire::test(SubmitAssignmentPage::class, ['record' => $assignment]) + ->assertSuccessful() + ->assertSet('canSubmit', false); + }); +}); + +describe('Assignment Pins and Interactions', function () { + beforeEach(function () { + $user = User::factory()->create(); + $user->assignRole(RoleEnum::Student); + $user->givePermissionTo(['ViewAny:Assignment']); + $this->studentProfile = Student::factory()->create(['user_id' => $user->id]); + $this->actingAs($user); + $this->currentSemester = app(GeneralSettings::class)->current_semester; + }); + + it('can pin and unpin an assignment', function () { + $assignment = Assignment::factory()->create(); + $assignment->course->update(['semester' => $this->currentSemester]); + $assignment->students()->attach($this->studentProfile->id); + + Livewire::test(ListAssignments::class) + ->callAction('pin', [], ['record' => $assignment->id]); + + $this->assertDatabaseHas('assignment_pins', [ + 'student_id' => $this->studentProfile->id, + 'assignment_id' => $assignment->id, + ]); + + Livewire::test(ListAssignments::class) + ->callAction('pin', [], ['record' => $assignment->id]); + + $this->assertDatabaseMissing('assignment_pins', [ + 'student_id' => $this->studentProfile->id, + 'assignment_id' => $assignment->id, + ]); + }); +}); + +describe('Assignment Details and Preview', function () { + beforeEach(function () { + $user = User::factory()->create(); + $user->assignRole(RoleEnum::Developer); + $user->givePermissionTo(['ViewAny:Assignment', 'View:Assignment']); + $this->actingAs($user); + $this->currentSemester = app(GeneralSettings::class)->current_semester; + }); + + it('can view submission detail page', function () { + $assignment = Assignment::factory()->create(); + $assignment->course->update(['semester' => $this->currentSemester]); + + Livewire::test(SubmissionDetailPage::class, ['record' => $assignment]) + ->assertSuccessful() + ->assertSee($assignment->title); + }); +});