Browse Source

quiz logic changed with courses

master
Mohsen Taba 1 month ago
parent
commit
94919083c4
  1. 38
      apps/course/serializers/lesson.py
  2. 22
      apps/course/signals.py
  3. 135
      apps/course/views/admin.py
  4. 19
      apps/library/migrations/0003_alter_book_author.py
  5. 25
      apps/quiz/migrations/0006_quiz_lesson_number_alter_quiz_lesson.py
  6. 18
      apps/quiz/models/quiz.py
  7. 16
      apps/quiz/serializers/admin.py
  8. 30
      apps/quiz/serializers/quiz.py
  9. 17
      apps/quiz/views/admin.py

38
apps/course/serializers/lesson.py

@ -1,4 +1,5 @@
from rest_framework import serializers
from django.db.models import Q
from apps.course.models import Lesson, CourseLesson, Participant, LessonCompletion
from apps.course.access import user_has_course_access
from apps.quiz.serializers import QuizListSerializer
@ -100,11 +101,38 @@ class CourseLessonSerializer(serializers.ModelSerializer):
).exists()
def get_quizs(self, obj):
# Now quizzes are directly related to CourseLesson
# print(f'--> type:{type(obj)} obj:{obj.quizzes.all()}')
quizzes = obj.quizzes.all() if hasattr(obj, 'quizzes') else []
if quizzes:
return QuizListSerializer(quizzes, many=True, context=self.context).data
context_key = f'course_lessons_{obj.course_id}'
if context_key not in self.context:
lessons_list = list(CourseLesson.objects.filter(
course_id=obj.course_id,
is_active=True
).filter(
Q(chapter__isnull=True) | Q(chapter__is_active=True)
).order_by('chapter__priority', 'priority'))
self.context[context_key] = lessons_list
else:
lessons_list = self.context[context_key]
try:
pos = lessons_list.index(obj) + 1
except ValueError:
pos = None
quizzes_key = f'course_quizzes_{obj.course_id}'
if quizzes_key not in self.context:
from apps.quiz.models import Quiz
all_quizzes = list(Quiz.objects.filter(course_id=obj.course_id, status=True))
self.context[quizzes_key] = all_quizzes
else:
all_quizzes = self.context[quizzes_key]
matching_quizzes = []
for quiz in all_quizzes:
if (quiz.lesson_id == obj.id) or (pos is not None and quiz.lesson_number == pos):
matching_quizzes.append(quiz)
if matching_quizzes:
return QuizListSerializer(matching_quizzes, many=True, context=self.context).data
return None

22
apps/course/signals.py

@ -94,6 +94,26 @@ def _recalculate_course_lessons_count(course_id):
Course.recalculate_lessons_count_for_course(course_id)
def _sync_quiz_lesson_numbers(course_id):
if not course_id:
return
from apps.quiz.models import Quiz
from apps.course.models import CourseLesson
active_lessons_count = CourseLesson.objects.filter(
course_id=course_id,
is_active=True
).filter(
Q(chapter__isnull=True) | Q(chapter__is_active=True)
).count()
# Update quizzes where lesson_number > active_lessons_count to 0
Quiz.objects.filter(
course_id=course_id,
lesson_number__gt=active_lessons_count
).update(lesson_number=0)
@receiver(pre_save, sender=CourseLesson)
def capture_previous_course_lesson_state(sender, instance, **kwargs):
if not instance.pk:
@ -124,11 +144,13 @@ def sync_lessons_count_on_course_lesson_save(sender, instance, **kwargs):
for course_id in affected_course_ids:
_recalculate_course_lessons_count(course_id)
_sync_quiz_lesson_numbers(course_id)
@receiver(post_delete, sender=CourseLesson)
def sync_lessons_count_on_course_lesson_delete(sender, instance, **kwargs):
_recalculate_course_lessons_count(instance.course_id)
_sync_quiz_lesson_numbers(instance.course_id)
@receiver(post_save, sender=LiveSessionRecording)

135
apps/course/views/admin.py

@ -1,8 +1,9 @@
import logging
from django.db import transaction
from django.db.models import Q
from django.utils import timezone
from rest_framework import viewsets
from rest_framework import serializers, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import action
@ -40,6 +41,7 @@ from apps.course.serializers.admin import (
AdminLiveSessionListSerializer,
AdminLiveSessionDetailSerializer
)
from utils import FileFieldSerializer
logger = logging.getLogger(__name__)
@ -247,6 +249,76 @@ class AdminCourseAttachmentViewSet(viewsets.ModelViewSet):
queryset = queryset.filter(course_id=course_id)
return queryset.order_by('-id')
def create(self, request, *args, **kwargs):
attachment_ids = request.data.get('attachment_ids')
new_attachments = request.data.get('new_attachments')
course_id = request.data.get('course')
if not course_id or (not attachment_ids and not new_attachments):
return super().create(request, *args, **kwargs)
if is_professor(request):
course = Course.objects.filter(id=course_id, professor_id=request.user.id).first()
else:
course = Course.objects.filter(id=course_id).first()
if not course:
raise PermissionDenied("Course not found or access denied.")
created_links = []
seen_attachment_ids = set()
file_field = FileFieldSerializer()
file_field.bind('file', None)
with transaction.atomic():
for attachment_id in attachment_ids or []:
try:
parsed_id = int(attachment_id)
except (TypeError, ValueError):
continue
if parsed_id in seen_attachment_ids:
continue
seen_attachment_ids.add(parsed_id)
attachment = Attachment.objects.filter(pk=parsed_id).first()
if not attachment:
continue
course_attachment, created = CourseAttachment.objects.get_or_create(
course=course,
attachment=attachment,
)
if created:
created_links.append(course_attachment)
for item in new_attachments or []:
title = item.get('title')
file_data = item.get('file')
if not title or not file_data:
continue
try:
normalized_file = file_field.to_internal_value(file_data)
except serializers.ValidationError as exc:
raise serializers.ValidationError({
'new_attachments': [exc.detail]
})
attachment = Attachment.objects.create(
title=title,
file=normalized_file,
)
created_links.append(
CourseAttachment.objects.create(
course=course,
attachment=attachment,
)
)
serializer = self.get_serializer(created_links, many=True)
return Response(serializer.data, status=201)
def perform_destroy(self, instance):
# Prevent orphans: delete base Attachment along with junction model
attachment = instance.attachment
@ -277,6 +349,67 @@ class AdminCourseGlossaryViewSet(viewsets.ModelViewSet):
queryset = queryset.filter(course_id=course_id)
return queryset.order_by('-id')
def create(self, request, *args, **kwargs):
glossary_ids = request.data.get('glossary_ids')
new_glossaries = request.data.get('new_glossaries')
course_id = request.data.get('course')
if not course_id or (not glossary_ids and not new_glossaries):
return super().create(request, *args, **kwargs)
if is_professor(request):
course = Course.objects.filter(id=course_id, professor_id=request.user.id).first()
else:
course = Course.objects.filter(id=course_id).first()
if not course:
raise PermissionDenied("Course not found or access denied.")
created_links = []
seen_glossary_ids = set()
with transaction.atomic():
for glossary_id in glossary_ids or []:
try:
parsed_id = int(glossary_id)
except (TypeError, ValueError):
continue
if parsed_id in seen_glossary_ids:
continue
seen_glossary_ids.add(parsed_id)
glossary = Glossary.objects.filter(pk=parsed_id).first()
if not glossary:
continue
course_glossary, created = CourseGlossary.objects.get_or_create(
course=course,
glossary=glossary,
)
if created:
created_links.append(course_glossary)
for item in new_glossaries or []:
title = item.get('title')
description = item.get('description')
if not title or not description:
continue
glossary = Glossary.objects.create(
title=title,
description=description,
)
created_links.append(
CourseGlossary.objects.create(
course=course,
glossary=glossary,
)
)
serializer = self.get_serializer(created_links, many=True)
return Response(serializer.data, status=201)
def perform_destroy(self, instance):
# Prevent orphans: delete base Glossary along with junction model
glossary = instance.glossary

19
apps/library/migrations/0003_alter_book_author.py

@ -0,0 +1,19 @@
# Generated by Django 5.2.12 on 2026-06-16 16:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('library', '0002_author_and_book_author_fk'),
]
operations = [
migrations.AlterField(
model_name='book',
name='author',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='books', to='library.author', verbose_name='Author'),
),
]

25
apps/quiz/migrations/0006_quiz_lesson_number_alter_quiz_lesson.py

@ -0,0 +1,25 @@
# Generated by Django 5.2.12 on 2026-06-16 16:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0015_rename_course_cour_ended_a_0f47b4_idx_course_cour_ended_a_32eaaa_idx_and_more'),
('quiz', '0005_multilingual_quiz_fields'),
]
operations = [
migrations.AddField(
model_name='quiz',
name='lesson_number',
field=models.PositiveIntegerField(default=0, verbose_name='Lesson Number'),
),
migrations.AlterField(
model_name='quiz',
name='lesson',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='quizzes', to='course.courselesson', verbose_name='Lesson'),
),
]

18
apps/quiz/models/quiz.py

@ -6,8 +6,9 @@ from apps.course.models.course import extract_text_from_json, format_multilingua
class Quiz(models.Model):
lesson = models.ForeignKey("course.CourseLesson", verbose_name=_('Lesson'), related_name='quizzes', on_delete=models.CASCADE)
lesson = models.ForeignKey("course.CourseLesson", verbose_name=_('Lesson'), related_name='quizzes', on_delete=models.SET_NULL, null=True, blank=True)
course = models.ForeignKey("course.Course", verbose_name=_('Course'), related_name='quizzes', on_delete=models.CASCADE , null=True, blank=True)
lesson_number = models.PositiveIntegerField(default=0, verbose_name=_("Lesson Number"))
title = models.JSONField(default=list, verbose_name=_('Title'), help_text=_("Quiz Title"))
description = models.JSONField(default=list, blank=True, verbose_name=_("Description"))
@ -26,6 +27,21 @@ class Quiz(models.Model):
def __repr__(self):
return f"Quiz(id={self.id})"
@property
def lesson_computed(self):
if self.lesson_number and self.lesson_number > 0 and self.course_id:
from apps.course.models import CourseLesson
from django.db.models import Q
lessons = list(CourseLesson.objects.filter(
course_id=self.course_id,
is_active=True
).filter(
Q(chapter__isnull=True) | Q(chapter__is_active=True)
).order_by('chapter__priority', 'priority'))
if len(lessons) >= self.lesson_number:
return lessons[self.lesson_number - 1]
return self.lesson
def save(self, *args, **kwargs):
# Automatically set the course based on the selected lesson
if self.lesson_id and hasattr(self.lesson, 'course'):

16
apps/quiz/serializers/admin.py

@ -56,6 +56,7 @@ class AdminQuizListSerializer(serializers.ModelSerializer):
'course',
'course_title',
'lesson',
'lesson_number',
'lesson_title',
'questions_count',
'participants_count'
@ -74,10 +75,11 @@ class AdminQuizListSerializer(serializers.ModelSerializer):
return get_localized_field(lang, obj.course.title)
def get_lesson_title(self, obj):
if not obj.lesson:
lesson = obj.lesson_computed
if not lesson:
return None
lang = get_request_lang(self)
raw_title = obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None)
raw_title = lesson.title or (lesson.lesson.title if lesson.lesson else None)
return get_localized_field(lang, raw_title)
def to_representation(self, instance):
@ -85,6 +87,8 @@ class AdminQuizListSerializer(serializers.ModelSerializer):
lang = get_request_lang(self)
ret['title'] = get_localized_field(lang, instance.title)
ret['description'] = get_localized_field(lang, instance.description)
computed = instance.lesson_computed
ret['lesson'] = computed.id if computed else None
return ret
@ -109,6 +113,7 @@ class AdminQuizDetailSerializer(serializers.ModelSerializer):
'course',
'course_title',
'lesson',
'lesson_number',
'lesson_title',
'questions_count',
'participants_count'
@ -128,10 +133,11 @@ class AdminQuizDetailSerializer(serializers.ModelSerializer):
return get_localized_field(lang, obj.course.title)
def get_lesson_title(self, obj):
if not obj.lesson:
lesson = obj.lesson_computed
if not lesson:
return None
lang = get_request_lang(self)
raw_title = obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None)
raw_title = lesson.title or (lesson.lesson.title if lesson.lesson else None)
return get_localized_field(lang, raw_title)
def to_representation(self, instance):
@ -139,6 +145,8 @@ class AdminQuizDetailSerializer(serializers.ModelSerializer):
lang = get_request_lang(self)
ret['title'] = get_localized_field(lang, instance.title)
ret['description'] = get_localized_field(lang, instance.description)
computed = instance.lesson_computed
ret['lesson'] = computed.id if computed else None
return ret

30
apps/quiz/serializers/quiz.py

@ -36,11 +36,14 @@ class QuizListSerializer(serializers.ModelSerializer):
return False
user = request.user
course_lesson = obj.lesson
if not course_lesson:
course = obj.course
if not course:
course_lesson = obj.lesson_computed
if course_lesson:
course = course_lesson.course
if not course:
return False
course = course_lesson.course
if not user_has_course_access(user, course):
return False
@ -78,13 +81,14 @@ class QuestionSerializer(serializers.ModelSerializer):
} for i in range(1, 5)
]
class Meta:
model = Question
fields = ['id', 'question', 'options', 'correct_answer']
class QuizSerializer(serializers.ModelSerializer):
lesson = serializers.PrimaryKeyRelatedField(read_only=True)
lesson = serializers.SerializerMethodField()
questions = QuestionSerializer(many=True)
permission = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
@ -92,7 +96,7 @@ class QuizSerializer(serializers.ModelSerializer):
class Meta:
model = Quiz
fields = ['id', 'permission', 'lesson', 'title', 'description', 'each_question_timing', 'questions']
fields = ['id', 'permission', 'lesson', 'lesson_number', 'title', 'description', 'each_question_timing', 'questions']
def get_title(self, obj):
request = self.context.get('request')
@ -104,15 +108,23 @@ class QuizSerializer(serializers.ModelSerializer):
lang = getattr(request, 'LANGUAGE_CODE', None) or 'en'
return get_localized_field(lang, obj.description)
def get_lesson(self, obj):
computed = obj.lesson_computed
return computed.id if computed else None
def get_permission(self, obj):
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return False
user = request.user
course_lesson = obj.lesson
if not course_lesson:
course = obj.course
if not course:
course_lesson = obj.lesson_computed
if course_lesson:
course = course_lesson.course
if not course:
return False
course = course_lesson.course
return user_has_course_access(user, course)

17
apps/quiz/views/admin.py

@ -133,7 +133,22 @@ class AdminQuizViewSet(ModelViewSet):
# Handle Lesson Filter
lesson_id = self.request.query_params.get('lesson', None)
if lesson_id:
queryset = queryset.filter(lesson_id=lesson_id)
try:
lesson_obj = CourseLesson.objects.get(id=lesson_id)
course = lesson_obj.course
lessons = list(CourseLesson.objects.filter(
course=course,
is_active=True
).order_by('chapter__priority', 'priority'))
try:
pos = lessons.index(lesson_obj) + 1
queryset = queryset.filter(
Q(lesson_id=lesson_id) | (Q(course=course) & Q(lesson_number=pos))
)
except ValueError:
queryset = queryset.filter(lesson_id=lesson_id)
except CourseLesson.DoesNotExist:
queryset = queryset.filter(lesson_id=lesson_id)
# Handle Status Filter
status_param = self.request.query_params.get('status', None)

Loading…
Cancel
Save