diff --git a/src/Dtos/Criteria/QuizAttemptCriteriaDto.php b/src/Dtos/Criteria/QuizAttemptCriteriaDto.php index 2dedb68..a1167fb 100644 --- a/src/Dtos/Criteria/QuizAttemptCriteriaDto.php +++ b/src/Dtos/Criteria/QuizAttemptCriteriaDto.php @@ -7,6 +7,9 @@ use EscolaLms\Core\Dtos\CriteriaDto as BaseCriteriaDto; use EscolaLms\Core\Repositories\Criteria\Primitives\DateCriterion; use EscolaLms\Core\Repositories\Criteria\Primitives\EqualCriterion; +use EscolaLms\TopicTypeGift\Enum\TopicTypeGiftPermissionEnum; +use EscolaLms\TopicTypeGift\Models\GiftQuiz; +use EscolaLms\TopicTypeGift\Repositories\Criterion\RawCriterion; use Illuminate\Http\Request; use Illuminate\Support\Collection; @@ -32,6 +35,38 @@ public static function instantiateFromRequest(Request $request): self $criteria->push(new DateCriterion('end_at', $request->get('date_to'), '<=')); } + if ($request->get('course_id')) { + $criteria->push( + new RawCriterion('EXISTS (SELECT tgg.id + FROM topic_gift_quizzes tgg + JOIN topics t ON tgg.id = t.topicable_id + JOIN lessons l ON t.lesson_id = l.id + WHERE t.topicable_type = ? + AND l.course_id = ? + AND topic_gift_quiz_attempts.topic_gift_quiz_id = tgg.id)', + [GiftQuiz::class, $request->get('course_id')] + ) + ); + } + + if ( + !$request->user()->can(TopicTypeGiftPermissionEnum::LIST_QUIZ_ATTEMPT) && + $request->user()->can(TopicTypeGiftPermissionEnum::LIST_SELF_QUIZ_ATTEMPT) + ) { + $criteria->push( + new RawCriterion('EXISTS (SELECT tgg.id + FROM topic_gift_quizzes tgg + JOIN topics t ON tgg.id = t.topicable_id + JOIN lessons l ON t.lesson_id = l.id + JOIN course_author ca ON l.course_id = ca.course_id + WHERE t.topicable_type = ? + AND ca.author_id = ? + AND topic_gift_quiz_attempts.topic_gift_quiz_id = tgg.id)', + [GiftQuiz::class, $request->user()->getKey()] + ) + ); + } + return new static($criteria); } } diff --git a/src/Enum/TopicTypeGiftPermissionEnum.php b/src/Enum/TopicTypeGiftPermissionEnum.php index d139147..d24ad1c 100644 --- a/src/Enum/TopicTypeGiftPermissionEnum.php +++ b/src/Enum/TopicTypeGiftPermissionEnum.php @@ -15,6 +15,8 @@ class TopicTypeGiftPermissionEnum extends BasicEnum public const UPDATE_QUIZ_ATTEMPT = 'quiz-attempt_update'; + public const LIST_SELF_QUIZ_ATTEMPT = 'quiz-attempt_list-self'; + public const READ_GIFT_QUIZ = 'gift-quiz_read'; public const UPDATE_GIFT_QUIZ = 'gift-quiz_update'; public const CREATE_GIFT_QUIZ_QUESTION = 'gift-quiz-question_create'; @@ -40,6 +42,7 @@ public static function tutorPermissions(): array TopicTypeGiftPermissionEnum::UPDATE_GIFT_QUIZ_QUESTION, TopicTypeGiftPermissionEnum::DELETE_GIFT_QUIZ_QUESTION, TopicTypeGiftPermissionEnum::DELETE_GIFT_QUIZ_QUESTION, + TopicTypeGiftPermissionEnum::LIST_SELF_QUIZ_ATTEMPT, ]; } } diff --git a/src/Http/Controllers/Swagger/QuizAttemptApiAdminSwagger.php b/src/Http/Controllers/Swagger/QuizAttemptApiAdminSwagger.php index 2ad5727..74e5de6 100644 --- a/src/Http/Controllers/Swagger/QuizAttemptApiAdminSwagger.php +++ b/src/Http/Controllers/Swagger/QuizAttemptApiAdminSwagger.php @@ -41,6 +41,14 @@ interface QuizAttemptApiAdminSwagger * ), * ), * @OA\Parameter( + * name="course_id", + * required=false, + * in="query", + * @OA\Schema( + * type="number", + * ), + * ), + * @OA\Parameter( * name="user_id", * required=false, * in="query", @@ -62,7 +70,7 @@ interface QuizAttemptApiAdminSwagger * @OA\Property( * property="data", * type="array", - * @OA\Items(@OA\Schema(ref="#/components/schemas/QuizAttemptSimpleResource")) + * @OA\Items(ref="#/components/schemas/QuizAttemptSimpleResource") * ), * @OA\Property( * property="message", diff --git a/src/Http/Resources/CourseSimpleResource.php b/src/Http/Resources/CourseSimpleResource.php new file mode 100644 index 0000000..23ec831 --- /dev/null +++ b/src/Http/Resources/CourseSimpleResource.php @@ -0,0 +1,30 @@ + $this->id, + 'title' => $this->title, + ]; + } +} diff --git a/src/Http/Resources/QuizAttemptSimpleResource.php b/src/Http/Resources/QuizAttemptSimpleResource.php index 7902554..a7f9e89 100644 --- a/src/Http/Resources/QuizAttemptSimpleResource.php +++ b/src/Http/Resources/QuizAttemptSimpleResource.php @@ -45,6 +45,16 @@ * description="min pass score", * type="number" * ), + * @OA\Property( + * property="user", + * description="user", + * type="object" + * ), + * @OA\Property( + * property="course", + * description="course", + * type="object" + * ), * ) * */ @@ -58,6 +68,7 @@ public function toArray($request): array { $maxScore = $this->giftQuiz->questions->sum('score'); $resultScore = $this->answers->sum('score'); + $course = $this->giftQuiz?->topic?->lesson?->course; return [ 'id' => $this->id, @@ -69,6 +80,8 @@ public function toArray($request): array 'min_pass_score' => $this->giftQuiz->min_pass_score, 'result_score' => $this->isEnded() ? $resultScore : null, 'is_ended' => $this->isEnded(), + 'user' => UserSimpleResource::make($this->user), + 'course' => $course ? CourseSimpleResource::make($course) : null, ]; } } diff --git a/src/Http/Resources/UserSimpleResource.php b/src/Http/Resources/UserSimpleResource.php new file mode 100644 index 0000000..3d23889 --- /dev/null +++ b/src/Http/Resources/UserSimpleResource.php @@ -0,0 +1,42 @@ + $this->id, + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'email' => $this->email, + ]; + } +} diff --git a/src/Policies/QuizAttemptPolicy.php b/src/Policies/QuizAttemptPolicy.php index 51180bd..4d17034 100644 --- a/src/Policies/QuizAttemptPolicy.php +++ b/src/Policies/QuizAttemptPolicy.php @@ -46,6 +46,9 @@ public function update(User $user, QuizAttempt $attempt): bool public function list(User $user): bool { - return $user->can(TopicTypeGiftPermissionEnum::LIST_QUIZ_ATTEMPT); + return $user->canAny([ + TopicTypeGiftPermissionEnum::LIST_QUIZ_ATTEMPT, + TopicTypeGiftPermissionEnum::LIST_SELF_QUIZ_ATTEMPT, + ]); } } diff --git a/src/Repositories/Criterion/RawCriterion.php b/src/Repositories/Criterion/RawCriterion.php new file mode 100644 index 0000000..7b9070b --- /dev/null +++ b/src/Repositories/Criterion/RawCriterion.php @@ -0,0 +1,25 @@ +sql = $sql; + $this->bindings = $bindings; + } + + public function apply(Builder $query): Builder + { + return $query->whereRaw($this->sql, $this->bindings); + } +} diff --git a/tests/Api/Admin/AdminListQuizAttemptApiTest.php b/tests/Api/Admin/AdminListQuizAttemptApiTest.php index f9d30a7..c1bc1f5 100644 --- a/tests/Api/Admin/AdminListQuizAttemptApiTest.php +++ b/tests/Api/Admin/AdminListQuizAttemptApiTest.php @@ -3,6 +3,10 @@ namespace EscolaLms\TopicTypeGift\Tests\Api\Admin; use EscolaLms\Core\Tests\CreatesUsers; +use EscolaLms\Courses\Models\Course; +use EscolaLms\Courses\Models\Lesson; +use EscolaLms\Courses\Models\Topic; +use EscolaLms\Courses\Models\User; use EscolaLms\TopicTypeGift\Database\Seeders\TopicTypeGiftPermissionSeeder; use EscolaLms\TopicTypeGift\Models\AttemptAnswer; use EscolaLms\TopicTypeGift\Models\GiftQuestion; @@ -54,6 +58,54 @@ public function testAdminQuizAttemptListFiltering(): void ->assertJsonCount(3, 'data'); } + public function testAdminQuizAttemptListFilteringByCourse(): void + { + $course1 = Course::factory()->create(); + $course2 = Course::factory()->create(); + + $lesson1 = Lesson::factory()->state(['course_id' => $course1->getKey()])->create(); + $lesson2 = Lesson::factory()->state(['course_id' => $course2->getKey()])->create(); + + $quiz1 = GiftQuiz::factory()->create(); + $quiz2 = GiftQuiz::factory()->create(); + $quiz3 = GiftQuiz::factory()->create(); + + $topic1 = Topic::factory()->state(['lesson_id' => $lesson1->getKey()])->create(); + $topic2 = Topic::factory()->state(['lesson_id' => $lesson2->getKey()])->create(); + $topic3 = Topic::factory()->state(['lesson_id' => $lesson2->getKey()])->create(); + + $topic1->topicable()->associate($quiz1)->save(); + $topic2->topicable()->associate($quiz2)->save(); + $topic3->topicable()->associate($quiz3)->save(); + + QuizAttempt::factory() + ->state(new Sequence( + ['topic_gift_quiz_id' => $quiz1->getKey()], // course1 + ['topic_gift_quiz_id' => $quiz1->getKey()], // course1 + ['topic_gift_quiz_id' => $quiz2->getKey()], // course2 + ['topic_gift_quiz_id' => $quiz3->getKey()], // course2 + ['topic_gift_quiz_id' => $quiz3->getKey()], // course2 + )) + ->count(5) + ->create(); + + $this->actingAs($this->makeAdmin(), 'api') + ->getJson('api/admin/quiz-attempts') + ->assertOk() + ->assertJsonCount(5, 'data'); + + $this->actingAs($this->makeAdmin(), 'api') + ->getJson('api/admin/quiz-attempts?course_id=' . $course1->getKey()) + ->assertOk() + ->assertJsonCount(2, 'data'); + + + $this->actingAs($this->makeAdmin(), 'api') + ->getJson('api/admin/quiz-attempts?course_id=' . $course2->getKey()) + ->assertOk() + ->assertJsonCount(3, 'data'); + } + public function testAdminQuizAttemptListSorting(): void { $student = $this->makeStudent(); @@ -166,4 +218,43 @@ public function testAdminQuizAttemptListSorting(): void $this->assertTrue($response->json('data.0.id') === $attempt2->getKey()); $this->assertTrue($response->json('data.1.id') === $attempt1->getKey()); } + + public function testTutorQuizAttemptList(): void + { + // tutor + $tutor = $this->makeInstructor(); + $course = Course::factory()->create(); + $course->authors()->sync($tutor); + $lesson = Lesson::factory()->state(['course_id' => $course->getKey()])->create(); + $quiz = GiftQuiz::factory()->create(); + $topic = Topic::factory()->state(['lesson_id' => $lesson->getKey()])->create(); + $topic->topicable()->associate($quiz)->save(); + + // other + $otherTopic = Topic::factory() + ->for(Lesson::factory() + ->for(Course::factory())) + ->create(); + + $otherTopic->lesson->course->authors()->sync($this->makeInstructor()); + $otherQuiz = GiftQuiz::factory()->create(); + $otherTopic->topicable()->associate($otherQuiz)->save(); + + + QuizAttempt::factory() + ->state(new Sequence( + ['topic_gift_quiz_id' => $quiz->getKey()], + ['topic_gift_quiz_id' => $quiz->getKey()], + ['topic_gift_quiz_id' => $quiz->getKey()], + ['topic_gift_quiz_id' => $otherQuiz->getKey()], + ['topic_gift_quiz_id' => $otherQuiz->getKey()], + )) + ->count(5) + ->create(); + + $this->actingAs($tutor, 'api') + ->getJson('api/admin/quiz-attempts') + ->assertOk() + ->assertJsonCount(3, 'data'); + } }