Browse Source

is active for lessons and chapters updated

master
Mohsen Taba 2 months ago
parent
commit
cd90dfdc0c
  1. 18
      apps/course/access.py
  2. 39
      apps/course/serializers/lesson.py
  3. 133
      apps/course/tests/test_course_access_guards.py
  4. 7
      apps/course/views/lesson.py
  5. 5
      apps/quiz/serializers/participant.py
  6. 21
      apps/quiz/serializers/quiz.py
  7. 17
      apps/quiz/views/quiz.py

18
apps/course/access.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()

39
apps/course/serializers/lesson.py

@ -1,5 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from apps.course.models import Lesson, CourseLesson, Participant, LessonCompletion from apps.course.models import Lesson, CourseLesson, Participant, LessonCompletion
from apps.course.access import user_has_course_access
from apps.quiz.serializers import QuizListSerializer from apps.quiz.serializers import QuizListSerializer
@ -10,6 +11,7 @@ class LessonSerializer(serializers.ModelSerializer):
class CourseLessonSerializer(serializers.ModelSerializer): class CourseLessonSerializer(serializers.ModelSerializer):
is_active = serializers.SerializerMethodField()
is_complated = serializers.SerializerMethodField() is_complated = serializers.SerializerMethodField()
quizs = serializers.SerializerMethodField() quizs = serializers.SerializerMethodField()
permission = serializers.SerializerMethodField() permission = serializers.SerializerMethodField()
@ -21,27 +23,20 @@ class CourseLessonSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = CourseLesson model = CourseLesson
fields = ['id', 'title', 'priority', 'is_active', 'permission', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs'] fields = ['id', 'title', 'priority', 'is_active', 'permission', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs']
def get_is_active(self, obj):
return obj.is_active and self._has_access_for_object(obj)
def get_permission(self, obj): def get_permission(self, obj):
return self._has_access_for_object(obj)
def _has_access_for_object(self, obj):
if user := self._get_authenticated_user(): if user := self._get_authenticated_user():
return self._has_access(user, obj.course) return self._has_access(user, obj.course)
return False return False
def _has_access(self, user, course): def _has_access(self, user, course):
"""
Check if the user has access to the course content.
"""
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()
return user_has_course_access(user, course)
def _is_participant(self, student, course): def _is_participant(self, student, course):
"""Deprecated: use _has_access instead.""" """Deprecated: use _has_access instead."""
@ -58,6 +53,9 @@ class CourseLessonSerializer(serializers.ModelSerializer):
return False return False
user = request.user user = request.user
if not self._has_access(user, obj.course):
return False
# Use prefetched completions if available # Use prefetched completions if available
if hasattr(obj, 'completions') and obj.completions.all(): if hasattr(obj, 'completions') and obj.completions.all():
return any(completion.student_id == user.id for completion in obj.completions.all()) return any(completion.student_id == user.id for completion in obj.completions.all())
@ -93,14 +91,25 @@ class CourseChapterSerializer(serializers.ModelSerializer):
""" """
V2 API: Returns a Chapter and a nested list of all its active lessons V2 API: Returns a Chapter and a nested list of all its active lessons
""" """
is_active = serializers.SerializerMethodField()
lessons = serializers.SerializerMethodField() lessons = serializers.SerializerMethodField()
class Meta: class Meta:
model = CourseChapter model = CourseChapter
fields = ['id', 'title', 'priority', 'is_active', 'lessons'] fields = ['id', 'title', 'priority', 'is_active', 'lessons']
def get_is_active(self, obj):
if not obj.is_active:
return False
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return False
return user_has_course_access(request.user, obj.course)
def get_lessons(self, obj): def get_lessons(self, obj):
# Grab all active lessons tied to this specific chapter # Grab all active lessons tied to this specific chapter
chapter_lessons = obj.lessons.filter(is_active=True).order_by('priority') chapter_lessons = obj.lessons.filter(is_active=True).order_by('priority')
# Re-use the V1 CourseLessonSerializer to serialize the nested items # Re-use the V1 CourseLessonSerializer to serialize the nested items
return CourseLessonSerializer(chapter_lessons, many=True, context=self.context).data
return CourseLessonSerializer(chapter_lessons, many=True, context=self.context).data

133
apps/course/tests/test_course_access_guards.py

@ -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)

7
apps/course/views/lesson.py

@ -13,6 +13,7 @@ from apps.course.serializers import (
CourseChapterSerializer CourseChapterSerializer
) )
from apps.course.models import Course, CourseLesson, LessonCompletion from apps.course.models import Course, CourseLesson, LessonCompletion
from apps.course.access import user_has_course_access
from apps.course.doc import * from apps.course.doc import *
from utils.exceptions import AppAPIException from utils.exceptions import AppAPIException
from utils.pagination import StandardResultsSetPagination from utils.pagination import StandardResultsSetPagination
@ -165,6 +166,9 @@ class LessonCompletionToggleAPIView(GenericAPIView):
course_lessons = CourseLesson.objects.filter(id__in=lesson_ids) course_lessons = CourseLesson.objects.filter(id__in=lesson_ids)
found_ids = set(course_lessons.values_list('id', flat=True)) found_ids = set(course_lessons.values_list('id', flat=True))
if any(not user_has_course_access(student, course_lesson.course) for course_lesson in course_lessons):
return Response({'error': 'You do not have access to one or more lessons.'}, status=status.HTTP_403_FORBIDDEN)
existing_completions = LessonCompletion.objects.filter( existing_completions = LessonCompletion.objects.filter(
student=student, student=student,
course_lesson_id__in=found_ids course_lesson_id__in=found_ids
@ -201,6 +205,9 @@ class LessonCompletionToggleAPIView(GenericAPIView):
except CourseLesson.DoesNotExist: except CourseLesson.DoesNotExist:
return Response({'error': 'Lesson not found.'}, status=status.HTTP_404_NOT_FOUND) return Response({'error': 'Lesson not found.'}, status=status.HTTP_404_NOT_FOUND)
if not user_has_course_access(student, course_lesson.course):
return Response({'error': 'You do not have access to this lesson.'}, status=status.HTTP_403_FORBIDDEN)
# TOGGLE LOGIC # TOGGLE LOGIC
completion = LessonCompletion.objects.filter(student=student, course_lesson=course_lesson).first() completion = LessonCompletion.objects.filter(student=student, course_lesson=course_lesson).first()

5
apps/quiz/serializers/participant.py

@ -1,5 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from apps.course.access import user_has_course_access
from apps.quiz.models import QuizParticipant, ParticipantAnswer from apps.quiz.models import QuizParticipant, ParticipantAnswer
@ -18,6 +19,10 @@ class QuizParticipantSerializer(serializers.ModelSerializer):
if QuizParticipant.objects.filter(quiz=obj, user=self.context['request'].user).exists(): if QuizParticipant.objects.filter(quiz=obj, user=self.context['request'].user).exists():
raise serializers.ValidationError('you have already participated in the quiz') raise serializers.ValidationError('you have already participated in the quiz')
course = getattr(obj, 'course', None)
if course and not user_has_course_access(self.context['request'].user, course):
raise serializers.ValidationError('you do not have access to this quiz')
return obj return obj
class Meta: class Meta:

21
apps/quiz/serializers/quiz.py

@ -1,7 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from apps.course.access import user_has_course_access
from apps.quiz.models import Question, Quiz, QuizParticipant from apps.quiz.models import Question, Quiz, QuizParticipant
from apps.course.models import Participant
@ -21,27 +21,20 @@ class QuizListSerializer(serializers.ModelSerializer):
request = self.context.get('request') request = self.context.get('request')
if not request or not request.user.is_authenticated: if not request or not request.user.is_authenticated:
return False return False
# Check if the user has participated in this quiz
user = request.user user = request.user
# obj.lesson is now CourseLesson directly
course_lesson = obj.lesson course_lesson = obj.lesson
if not course_lesson: if not course_lesson:
return False return False
course = course_lesson.course course = course_lesson.course
if not self._is_participant(user, course):
if not user_has_course_access(user, course):
return False return False
participated = QuizParticipant.objects.filter(user=user, quiz=obj).exists() participated = QuizParticipant.objects.filter(user=user, quiz=obj).exists()
return not participated return not participated
def _is_participant(self, student, course):
"""Helper method to check if a student is a participant in the given course."""
return Participant.objects.filter(student=student, course=course).exists()
def get_is_complated(self, obj): def get_is_complated(self, obj):
request = self.context.get('request') request = self.context.get('request')
if not request or not request.user.is_authenticated: if not request or not request.user.is_authenticated:
@ -82,19 +75,11 @@ class QuizSerializer(serializers.ModelSerializer):
request = self.context.get('request') request = self.context.get('request')
if not request or not request.user.is_authenticated: if not request or not request.user.is_authenticated:
return False return False
# Check if the user has participated in this quiz
user = request.user user = request.user
# obj.lesson is now CourseLesson directly
course_lesson = obj.lesson course_lesson = obj.lesson
if not course_lesson: if not course_lesson:
return False return False
course = course_lesson.course course = course_lesson.course
# Check if user is a participant in the course
if not Participant.objects.filter(student=user, course=course).exists():
return False
participated = QuizParticipant.objects.filter(user=user, quiz=obj).exists()
return participated
return user_has_course_access(user, course)

17
apps/quiz/views/quiz.py

@ -1,13 +1,16 @@
from django.db.models import Value from django.db.models import Value
from django.shortcuts import get_object_or_404
from rest_framework.generics import RetrieveAPIView from rest_framework.generics import RetrieveAPIView
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication from rest_framework.authentication import TokenAuthentication
from drf_yasg.utils import swagger_auto_schema from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from apps.course.access import user_has_course_access
from apps.quiz.models import Quiz from apps.quiz.models import Quiz
from apps.quiz.serializers.quiz import QuizSerializer from apps.quiz.serializers.quiz import QuizSerializer
from apps.quiz.doc import * from apps.quiz.doc import *
from utils.exceptions import AppAPIException
@ -25,8 +28,18 @@ class QuizDetailAPIView(RetrieveAPIView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_object(self): def get_object(self):
return Quiz.objects.filter(
quiz = get_object_or_404(
Quiz.objects.filter(
id=self.kwargs['quiz_id'], id=self.kwargs['quiz_id'],
).annotate( ).annotate(
lesson__has_quiz=Value(True) lesson__has_quiz=Value(True)
).select_related('lesson').first()
).select_related('lesson', 'course')
)
if quiz.course and not user_has_course_access(self.request.user, quiz.course):
raise AppAPIException(
{'message': 'You do not have access to this quiz.'},
status_code=403,
)
return quiz
Loading…
Cancel
Save