7 changed files with 205 additions and 35 deletions
-
18apps/course/access.py
-
39apps/course/serializers/lesson.py
-
133apps/course/tests/test_course_access_guards.py
-
7apps/course/views/lesson.py
-
5apps/quiz/serializers/participant.py
-
21apps/quiz/serializers/quiz.py
-
17apps/quiz/views/quiz.py
@ -0,0 +1,18 @@ |
|||||
|
from apps.course.models import Participant |
||||
|
|
||||
|
|
||||
|
def user_has_course_access(user, course): |
||||
|
if not user or not getattr(user, 'is_authenticated', False): |
||||
|
return False |
||||
|
|
||||
|
if user.is_staff or user.is_superuser: |
||||
|
return True |
||||
|
|
||||
|
if course.professor_id == user.id: |
||||
|
return True |
||||
|
|
||||
|
return Participant.objects.filter( |
||||
|
student_id=user.id, |
||||
|
course=course, |
||||
|
is_active=True, |
||||
|
).exists() |
||||
@ -0,0 +1,133 @@ |
|||||
|
from django.core.files.uploadedfile import SimpleUploadedFile |
||||
|
from rest_framework import status |
||||
|
from rest_framework.test import APITestCase |
||||
|
|
||||
|
from apps.account.models import ProfessorUser, StudentUser |
||||
|
from apps.course.models.course import Course, CourseCategory |
||||
|
from apps.course.models.lesson import CourseChapter, CourseLesson, Lesson |
||||
|
from apps.quiz.models.quiz import Quiz, Question |
||||
|
|
||||
|
|
||||
|
class CourseAccessGuardsTests(APITestCase): |
||||
|
def setUp(self): |
||||
|
self.professor = ProfessorUser.objects.create( |
||||
|
email='prof-guard@example.com', |
||||
|
fullname='Professor Guard', |
||||
|
experience_years=5, |
||||
|
) |
||||
|
self.outsider = StudentUser.objects.create( |
||||
|
email='outsider@example.com', |
||||
|
fullname='Outsider Student', |
||||
|
) |
||||
|
self.category = CourseCategory.objects.create( |
||||
|
name='Guard Category', |
||||
|
slug='guard-category', |
||||
|
) |
||||
|
thumbnail = SimpleUploadedFile('guard.jpg', b'filecontent', content_type='image/jpeg') |
||||
|
self.course = Course.objects.create( |
||||
|
title='Guard Course', |
||||
|
slug='guard-course', |
||||
|
category=self.category, |
||||
|
professor=self.professor, |
||||
|
thumbnail=thumbnail, |
||||
|
video_type=Course.VedioTypeChoices.YOUTUBE_LINK, |
||||
|
video_link='https://example.com/video', |
||||
|
is_online=False, |
||||
|
level=Course.LevelChoices.BEGINNER, |
||||
|
duration=10, |
||||
|
lessons_count=0, |
||||
|
description='Description', |
||||
|
short_description='Short description', |
||||
|
status=Course.StatusChoices.ONGOING, |
||||
|
is_free=True, |
||||
|
) |
||||
|
self.chapter = CourseChapter.objects.create( |
||||
|
course=self.course, |
||||
|
title='Chapter 1', |
||||
|
priority=1, |
||||
|
is_active=True, |
||||
|
) |
||||
|
lesson = Lesson.objects.create( |
||||
|
title='Lesson 1', |
||||
|
content_type=Lesson.ContentTypeChoices.VIDEO_FILE, |
||||
|
duration=5, |
||||
|
) |
||||
|
self.course_lesson = CourseLesson.objects.create( |
||||
|
course=self.course, |
||||
|
chapter=self.chapter, |
||||
|
lesson=lesson, |
||||
|
priority=1, |
||||
|
is_active=True, |
||||
|
) |
||||
|
self.quiz = Quiz.objects.create( |
||||
|
lesson=self.course_lesson, |
||||
|
course=self.course, |
||||
|
title='Guard Quiz', |
||||
|
description='Quiz description', |
||||
|
each_question_timing=30, |
||||
|
status=True, |
||||
|
) |
||||
|
self.question = Question.objects.create( |
||||
|
quiz=self.quiz, |
||||
|
question='What is 2+2?', |
||||
|
option1='4', |
||||
|
option2='3', |
||||
|
option3='2', |
||||
|
option4='1', |
||||
|
correct_answer=1, |
||||
|
priority=1, |
||||
|
) |
||||
|
|
||||
|
def test_lesson_list_marks_lessons_and_quizzes_inactive_without_course_access(self): |
||||
|
self.client.force_authenticate(user=self.outsider) |
||||
|
|
||||
|
response = self.client.get(f'/api/courses/{self.course.slug}/lessons/') |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK) |
||||
|
lesson_data = response.data['results'][0] |
||||
|
self.assertFalse(lesson_data['permission']) |
||||
|
self.assertFalse(lesson_data['is_active']) |
||||
|
self.assertFalse(lesson_data['quizs'][0]['permission']) |
||||
|
|
||||
|
def test_lesson_list_v2_marks_chapter_inactive_without_course_access(self): |
||||
|
self.client.force_authenticate(user=self.outsider) |
||||
|
|
||||
|
response = self.client.get(f'/api/courses/v2/{self.course.slug}/lessons/') |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK) |
||||
|
chapter_data = response.data['results'][0] |
||||
|
self.assertFalse(chapter_data['is_active']) |
||||
|
self.assertFalse(chapter_data['lessons'][0]['is_active']) |
||||
|
|
||||
|
def test_quiz_detail_rejects_users_without_course_access(self): |
||||
|
self.client.force_authenticate(user=self.outsider) |
||||
|
|
||||
|
response = self.client.get(f'/api/quiz/{self.quiz.id}/') |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) |
||||
|
|
||||
|
def test_quiz_submit_rejects_users_without_course_access(self): |
||||
|
self.client.force_authenticate(user=self.outsider) |
||||
|
|
||||
|
payload = { |
||||
|
'quiz': self.quiz.id, |
||||
|
'started_at': '2026-06-02T10:00:00Z', |
||||
|
'ended_at': '2026-06-02T10:01:00Z', |
||||
|
'total_timing': 60, |
||||
|
'question_score': 1, |
||||
|
'timing_score': 1, |
||||
|
'total_score': 2, |
||||
|
'answers': [ |
||||
|
{ |
||||
|
'question': self.question.id, |
||||
|
'option_num': 1, |
||||
|
'at_time': '2026-06-02T10:00:30Z', |
||||
|
'answer_timing': 30, |
||||
|
} |
||||
|
], |
||||
|
} |
||||
|
|
||||
|
response = self.client.post('/api/quiz/submit-quiz/', payload, format='json') |
||||
|
|
||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) |
||||
|
self.assertIn('quiz', response.data) |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue