From 5da60b0560e7137a4bb758ed7e27f3f59143f9c0 Mon Sep 17 00:00:00 2001 From: Tomasz Smolarek Date: Wed, 13 Mar 2024 16:57:02 +0100 Subject: [PATCH 1/2] Add an end date to the course assignment to the user --- ...d_end_date_column_to_course_user_table.php | 22 +++ src/Http/Resources/ProgressResource.php | 1 + src/Models/Course.php | 5 +- src/Models/CourseUserPivot.php | 1 + src/Policies/CoursesPolicy.php | 1 + src/ValueObjects/CourseProgressCollection.php | 16 +- tests/APIs/CourseProgramApiTest.php | 11 ++ tests/APIs/CourseProgressApiTest.php | 153 ++++++++++++++++-- 8 files changed, 196 insertions(+), 14 deletions(-) create mode 100644 database/migrations/2024_03_12_220149_add_end_date_column_to_course_user_table.php diff --git a/database/migrations/2024_03_12_220149_add_end_date_column_to_course_user_table.php b/database/migrations/2024_03_12_220149_add_end_date_column_to_course_user_table.php new file mode 100644 index 00000000..674fca1a --- /dev/null +++ b/database/migrations/2024_03_12_220149_add_end_date_column_to_course_user_table.php @@ -0,0 +1,22 @@ +dateTime('end_date')->nullable(); + }); + } + + public function down(): void + { + Schema::table('course_user', function (Blueprint $table) { + $table->dropColumn('end_date'); + }); + } +} diff --git a/src/Http/Resources/ProgressResource.php b/src/Http/Resources/ProgressResource.php index e8adfa12..fb7956f0 100644 --- a/src/Http/Resources/ProgressResource.php +++ b/src/Http/Resources/ProgressResource.php @@ -40,6 +40,7 @@ public function toArray($request) 'start_date' => $this->getResource()->getStartDate(), 'finish_date' => $this->getResource()->isFinished() ? $this->getResource()->getFinishDate() : null, 'deadline' => $this->getResource()->getDeadline(), + 'end_date' => $this->getResource()->getEndDate(), 'total_spent_time' => $this->getResource()->getTotalSpentTime() ?? 0, ]; } diff --git a/src/Models/Course.php b/src/Models/Course.php index fd18167d..c329746f 100644 --- a/src/Models/Course.php +++ b/src/Models/Course.php @@ -420,7 +420,10 @@ public function hasUser(CoreUser|User $user): bool ->where('user_id', $user->getKey()) ->exists(); - return $this->users()->where('users.id', $user->getKey())->exists() || $inGroup; + return $this->users() + ->where('users.id', $user->getKey()) + ->where(fn(Builder $query) => $query->whereNull('end_date')->orWhereDate('end_date', '>=', Carbon::now())) + ->exists() || $inGroup; // todo sprawdzić dostęp do kursu } private function getChildGroups(array $groupIds): array diff --git a/src/Models/CourseUserPivot.php b/src/Models/CourseUserPivot.php index 5a0aa81f..6a336d11 100644 --- a/src/Models/CourseUserPivot.php +++ b/src/Models/CourseUserPivot.php @@ -12,6 +12,7 @@ class CourseUserPivot extends Pivot protected $casts = [ 'deadline' => 'datetime', + 'end_date' => 'datetime', ]; public function user(): BelongsTo diff --git a/src/Policies/CoursesPolicy.php b/src/Policies/CoursesPolicy.php index 2a97fe6a..ef254894 100644 --- a/src/Policies/CoursesPolicy.php +++ b/src/Policies/CoursesPolicy.php @@ -99,6 +99,7 @@ public function attend(?User $user, Course $course): bool if ($user->can(CoursesPermissionsEnum::COURSE_ATTEND_OWNED)) { return $course->hasAuthor($user); } + return $course->is_published && $course->hasUser($user); } diff --git a/src/ValueObjects/CourseProgressCollection.php b/src/ValueObjects/CourseProgressCollection.php index 17327c2c..136ba6ff 100644 --- a/src/ValueObjects/CourseProgressCollection.php +++ b/src/ValueObjects/CourseProgressCollection.php @@ -34,6 +34,7 @@ class CourseProgressCollection extends ValueObject implements ValueObjectContrac private ?Carbon $startDate; private ?Carbon $finishDate; private ?Carbon $deadline; + private ?Carbon $endDate; public function __construct( CourseProgressRepositoryContract $courseProgressRepositoryContract @@ -50,6 +51,7 @@ public function build(Authenticatable $user, Course $course): self $this->finishDate = null; $this->pivot = CourseUserPivot::query()->where('user_id', $user->getKey())->where('course_id', $course->getKey())->first(); $this->deadline = $this->pivot ? $this->pivot->deadline : null; + $this->endDate = $this->pivot ? $this->pivot->end_date : null; $this->topics = $this->getActiveTopicIdsFromCourses(); $this->progress = $this->buildProgress(); @@ -205,9 +207,19 @@ public function getDeadline(): ?Carbon return $this->deadline; } + public function getEndDate(): ?Carbon + { + return $this->endDate; + } + public function afterDeadline(): bool { - return $this->getDeadline() ? Carbon::now()->greaterThanOrEqualTo($this->getDeadline()) : false; + return $this->getDeadline() && Carbon::now()->greaterThanOrEqualTo($this->getDeadline()); + } + + public function afterEndDate(): bool + { + return $this->getEndDate() && Carbon::now()->greaterThanOrEqualTo($this->getEndDate()); } public function toArray(): array @@ -217,7 +229,7 @@ public function toArray(): array public function topicCanBeProgressed(Topic $topic): bool { - return $this->courseCanBeProgressed() && $topic->active; + return $this->courseCanBeProgressed() && $topic->active && !$this->afterEndDate(); } public function courseCanBeProgressed(): bool diff --git a/tests/APIs/CourseProgramApiTest.php b/tests/APIs/CourseProgramApiTest.php index 37e222a2..655d1452 100644 --- a/tests/APIs/CourseProgramApiTest.php +++ b/tests/APIs/CourseProgramApiTest.php @@ -90,4 +90,15 @@ public function testShowCourseProgramForLessonAvailableBetweenDates(): void 'topicable' => null, ]); } + + public function testCannotShowCourseProgramWhenEndDateIsOverdue(): void + { + $student = $this->makeStudent(); + $course = Course::factory()->state(['status' => CourseStatusEnum::PUBLISHED])->create(); + $course->users()->attach($student, ['end_date' => Carbon::now()->subDay()]); + + $this->actingAs($student, 'api') + ->getJson('api/courses/' . $course->getKey() . '/program') + ->assertForbidden(); + } } diff --git a/tests/APIs/CourseProgressApiTest.php b/tests/APIs/CourseProgressApiTest.php index 47bffbc1..e4c95e6a 100644 --- a/tests/APIs/CourseProgressApiTest.php +++ b/tests/APIs/CourseProgressApiTest.php @@ -39,7 +39,7 @@ class CourseProgressApiTest extends TestCase use CreatesUsers, WithFaker, ProgressConfigurable, MakeServices; use DatabaseTransactions; - public function test_show_progress_courses() + public function test_show_progress_courses(): void { $user = User::factory()->create(); $course = Course::factory()->create(['status' => CourseStatusEnum::PUBLISHED]); @@ -74,7 +74,7 @@ public function test_show_progress_courses() ]); } - public function test_show_progress_courses_paginated_ordered() + public function test_show_progress_courses_paginated_ordered(): void { $user = User::factory()->create(); @@ -200,7 +200,7 @@ public function test_show_progress_courses_paginated_ordered() $this->assertTrue($this->response->json('data.3.course.id') === $course1->getKey()); } - public function test_show_progress_courses_paginated_filtered_planned() + public function test_show_progress_courses_paginated_filtered_planned(): void { $user = User::factory()->create(); @@ -269,7 +269,7 @@ public function test_show_progress_courses_paginated_filtered_planned() ]); } - public function test_show_progress_courses_paginated_filtered_finished() + public function test_show_progress_courses_paginated_filtered_finished(): void { $user = User::factory()->create(); @@ -325,7 +325,7 @@ public function test_show_progress_courses_paginated_filtered_finished() ]); } - public function test_show_progress_courses_paginated_filtered_started() + public function test_show_progress_courses_paginated_filtered_started(): void { $user = User::factory()->create(); @@ -395,7 +395,7 @@ public function test_show_progress_courses_paginated_filtered_started() ]); } - public function test_show_progress_courses_ordered_by_latest_purchased() + public function test_show_progress_courses_ordered_by_latest_purchased(): void { $user = User::factory()->create(); $courseOne = Course::factory()->create(['status' => CourseStatusEnum::PUBLISHED]); @@ -415,7 +415,7 @@ public function test_show_progress_courses_ordered_by_latest_purchased() $this->assertEquals($courseOne->getKey(), $this->response->json('data.1.course.id')); } - public function test_show_progress_course_from_group() + public function test_show_progress_course_from_group(): void { $user = User::factory()->create(); $course = Course::factory()->create(['status' => CourseStatusEnum::PUBLISHED]); @@ -464,6 +464,51 @@ public function test_show_progress_course_from_parent_group(): void ]); } + public function test_show_progress_courses_with_end_date(): void + { + $student1 = $this->makeStudent(); + $student2 = $this->makeStudent(); + $endDate = Carbon::now()->startOfDay()->subDay(); + $course = Course::factory()->create(['status' => CourseStatusEnum::PUBLISHED]); + + $student1->courses()->attach($course->getKey(), ['end_date' => $endDate]); + $student2->courses()->attach($course->getKey()); + + $this->actingAs($student1, 'api') + ->getJson('/api/courses/progress') + ->assertStatus(200) + ->assertJsonFragment([ + 'end_date' => $endDate + ]) + ->assertJsonStructure([ + 'data' => [[ + 'course', + 'progress', + 'categories', + 'tags', + 'finish_date', + 'end_date' + ]] + ]); + + $this->actingAs($student2, 'api') + ->getJson('/api/courses/progress') + ->assertStatus(200) + ->assertJsonFragment([ + 'end_date' => null + ]) + ->assertJsonStructure([ + 'data' => [[ + 'course', + 'progress', + 'categories', + 'tags', + 'finish_date', + 'end_date' + ]] + ]); + } + public function test_update_course_progress(): void { Mail::fake(); @@ -682,7 +727,7 @@ public function test_verify_course_started(): void Event::assertDispatched(CourseAccessStarted::class); } - public function test_ping_progress_course() + public function test_ping_progress_course(): void { /** @var User $user */ $user = User::factory()->create(); @@ -753,7 +798,7 @@ public function test_ping_should_not_dispatch_topic_finished_event_again(): void Event::assertNotDispatched(TopicFinished::class); } - public function test_ping_complete_topic() + public function test_ping_complete_topic(): void { /** @var User $user */ $user = User::factory()->create(); @@ -812,6 +857,92 @@ public function test_ping_complete_topic() ]); } + public function test_ping_complete_topic_when_end_date_is_overdue(): void + { + $user = $this->makeStudent(); + $course = Course::factory()->create(['status' => CourseStatusEnum::PUBLISHED]); + $lesson = Lesson::factory()->create(['course_id' => $course->getKey()]); + $topic = Topic::factory()->create([ + 'active' => true, + 'lesson_id' => $lesson->getKey(), + ]); + + $user->courses()->attach($course->getKey(), ['end_date' => Carbon::now()->subDay()]); + + CourseProgress::create([ + 'user_id' => $user->getKey(), + 'topic_id' => $topic->getKey(), + 'status' => ProgressStatus::COMPLETE, + 'seconds' => 10, + ]); + + $this->actingAs($user, 'api') + ->putJson('/api/courses/progress/' . $topic->getKey() . '/ping') + ->assertOk() + ->assertJsonFragment([ + 'status' => true + ]); + + sleep(5); + + $this->actingAs($user, 'api') + ->putJson('/api/courses/progress/' . $topic->getKey() . '/ping') + ->assertOk() + ->assertJsonFragment([ + 'status' => true + ]); + + $this->assertDatabaseHas('course_progress', [ + 'user_id' => $user->getKey(), + 'topic_id' => $topic->getKey(), + 'status' => ProgressStatus::COMPLETE, + 'seconds' => 10, + ]); + } + + public function test_ping_complete_topic_when_end_date_is_current(): void + { + $user = $this->makeStudent(); + $course = Course::factory()->create(['status' => CourseStatusEnum::PUBLISHED]); + $lesson = Lesson::factory()->create(['course_id' => $course->getKey()]); + $topic = Topic::factory()->create([ + 'active' => true, + 'lesson_id' => $lesson->getKey(), + ]); + + $user->courses()->syncWithPivotValues($course->getKey(), ['end_date' => Carbon::now()->addDay()]); + + CourseProgress::create([ + 'user_id' => $user->getKey(), + 'topic_id' => $topic->getKey(), + 'status' => ProgressStatus::COMPLETE, + 'seconds' => 10, + ]); + + $this->actingAs($user, 'api') + ->putJson('/api/courses/progress/' . $topic->getKey() . '/ping') + ->assertOk() + ->assertJsonFragment([ + 'status' => true + ]); + + sleep(5); + + $this->actingAs($user, 'api') + ->putJson('/api/courses/progress/' . $topic->getKey() . '/ping') + ->assertOk() + ->assertJsonFragment([ + 'status' => true + ]); + + $this->assertDatabaseHas('course_progress', [ + 'user_id' => $user->getKey(), + 'topic_id' => $topic->getKey(), + 'status' => ProgressStatus::COMPLETE, + 'seconds' => 15, + ]); + } + public function test_adding_new_topic_will_reset_finished_status(): void { Mail::fake(); @@ -859,7 +990,7 @@ public function test_adding_new_topic_will_reset_finished_status(): void $this->assertFalse($courseProgress->isFinished()); } - public function test_active_to() + public function test_active_to(): void { $user = User::factory()->create(); $course = Course::factory()->create(['status' => CourseStatusEnum::PUBLISHED, 'active_to' => Carbon::now()->subDay()]); @@ -904,7 +1035,7 @@ public function test_active_to() $this->assertEquals($this->response->json('data.0.course.active_to'), $this->response->json('data.0.deadline')); } - public function test_deadline() + public function test_deadline(): void { $user = User::factory()->create(); From 7194563686718235f0195c744e903d6d79cd9d96 Mon Sep 17 00:00:00 2001 From: Tomasz Smolarek Date: Wed, 13 Mar 2024 17:23:23 +0100 Subject: [PATCH 2/2] additional test, fixes --- src/Models/Course.php | 3 ++- tests/APIs/CourseProgramApiTest.php | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Models/Course.php b/src/Models/Course.php index c329746f..1d000f7d 100644 --- a/src/Models/Course.php +++ b/src/Models/Course.php @@ -423,7 +423,8 @@ public function hasUser(CoreUser|User $user): bool return $this->users() ->where('users.id', $user->getKey()) ->where(fn(Builder $query) => $query->whereNull('end_date')->orWhereDate('end_date', '>=', Carbon::now())) - ->exists() || $inGroup; // todo sprawdzić dostęp do kursu + ->exists() + || $inGroup; } private function getChildGroups(array $groupIds): array diff --git a/tests/APIs/CourseProgramApiTest.php b/tests/APIs/CourseProgramApiTest.php index 655d1452..186bcd68 100644 --- a/tests/APIs/CourseProgramApiTest.php +++ b/tests/APIs/CourseProgramApiTest.php @@ -101,4 +101,15 @@ public function testCannotShowCourseProgramWhenEndDateIsOverdue(): void ->getJson('api/courses/' . $course->getKey() . '/program') ->assertForbidden(); } + + public function testCannotShowCourseProgramWhenEndDateIsCurrent(): void + { + $student = $this->makeStudent(); + $course = Course::factory()->state(['status' => CourseStatusEnum::PUBLISHED])->create(); + $course->users()->attach($student, ['end_date' => Carbon::now()->addDay()]); + + $this->actingAs($student, 'api') + ->getJson('api/courses/' . $course->getKey() . '/program') + ->assertOk(); + } }