Browse Source

quiz multilanguage added

master
Mohsen Taba 1 month ago
parent
commit
a0b52ed41a
  1. 8
      apps/quiz/admin/question.py
  2. 25
      apps/quiz/admin/quiz.py
  3. 137
      apps/quiz/migrations/0005_multilingual_quiz_fields.py
  4. 24
      apps/quiz/models/quiz.py
  5. 46
      apps/quiz/serializers/admin.py
  6. 35
      apps/quiz/serializers/quiz.py
  7. 21
      apps/quiz/views/admin.py

8
apps/quiz/admin/question.py

@ -1,9 +1,11 @@
from django import forms
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from unfold.admin import ModelAdmin, TabularInline, StackedInline
from unfold.forms import forms
from apps.course.models.course import extract_text_from_json
from apps.quiz.models import Question
from utils.admin import project_admin_site
@ -35,7 +37,7 @@ from utils.admin import project_admin_site
# )
@admin.register(Question)
class QuestionAdmin(ModelAdmin):
list_display = ('question', 'correct_answer', 'quiz', 'priority')
list_display = ('question_display', 'correct_answer', 'quiz', 'priority')
ordering = ("priority", "id",)
search_fields = ('question', 'quiz__title')
list_filter = ('quiz',)
@ -59,6 +61,10 @@ class QuestionAdmin(ModelAdmin):
)
)
@admin.display(description=_('Question'))
def question_display(self, obj):
return extract_text_from_json(obj.question)
class QuestionAdminInline(StackedInline):
model = Question
ordering = ("priority", "id",)

25
apps/quiz/admin/quiz.py

@ -20,7 +20,7 @@ from utils.admin import project_admin_site, admin_url_generator
class QuizAdmin(ModelAdmin):
search_fields = ['title', 'description']
list_display = ['title', 'description', 'lesson', 'each_question_timing', 'status_display', 'questions_display']
list_display = ['title_display', 'description_display', 'lesson', 'each_question_timing', 'status_display', 'questions_display']
list_filter = ['each_question_timing', 'status']
inlines = [QuestionAdminInline, QuizParticipantInline]
compressed_fields = True
@ -117,14 +117,33 @@ class QuizAdmin(ModelAdmin):
queryset, use_distinct = super().get_search_results(request, queryset, search_term)
matching_course_ids = self._find_matching_course_ids(search_term)
matching_lesson_ids = self._find_matching_lesson_ids(search_term)
matching_quiz_ids = []
if matching_course_ids or matching_lesson_ids:
normalized = (search_term or "").strip().lower()
if normalized:
for quiz in self.model.objects.all().only("id", "title", "description"):
title_text = extract_text_from_json(quiz.title).lower()
description_text = extract_text_from_json(quiz.description).lower()
if normalized in title_text or normalized in description_text:
matching_quiz_ids.append(quiz.id)
if matching_course_ids or matching_lesson_ids or matching_quiz_ids:
queryset = queryset | self.model.objects.filter(
Q(course_id__in=matching_course_ids) | Q(lesson_id__in=matching_lesson_ids)
Q(id__in=matching_quiz_ids) |
Q(course_id__in=matching_course_ids) |
Q(lesson_id__in=matching_lesson_ids)
)
use_distinct = True
return queryset, use_distinct
@display(description=_('Title'))
def title_display(self, obj):
return extract_text_from_json(obj.title)
@display(description=_('Description'))
def description_display(self, obj):
return extract_text_from_json(obj.description)
@display(description=_('Status'), ordering='status')
def status_display(self, obj):

137
apps/quiz/migrations/0005_multilingual_quiz_fields.py

@ -0,0 +1,137 @@
# Generated by Codex on 2026-06-11
from django.db import migrations, models
def _is_multilingual_list(value):
return isinstance(value, list) and all(
isinstance(item, dict) and item.get("language_code")
for item in value
)
def _normalize_multilingual(value):
if value in (None, "", []):
return []
if _is_multilingual_list(value):
normalized_items = []
for item in value:
title = item.get("title") or item.get("text") or item.get("value") or item.get("name")
language_code = item.get("language_code") or "en"
if title:
normalized_items.append({"title": title, "language_code": language_code})
return normalized_items
if isinstance(value, dict):
language_map_keys = {"fa", "en", "ru", "ar", "tr"}
if any(key in value for key in language_map_keys):
return [
{"title": title, "language_code": language_code}
for language_code, title in value.items()
if title
]
title = value.get("title") or value.get("text") or value.get("value") or value.get("name")
language_code = value.get("language_code") or "en"
return [{"title": title, "language_code": language_code}] if title else []
if isinstance(value, list):
normalized_items = []
for item in value:
if isinstance(item, dict):
title = item.get("title") or item.get("text") or item.get("value") or item.get("name")
language_code = item.get("language_code") or "en"
if title:
normalized_items.append({"title": title, "language_code": language_code})
elif item not in (None, ""):
normalized_items.append({"title": str(item), "language_code": "en"})
return normalized_items
return [{"title": str(value), "language_code": "en"}]
def forwards(apps, schema_editor):
Quiz = apps.get_model("quiz", "Quiz")
Question = apps.get_model("quiz", "Question")
for quiz in Quiz.objects.all().iterator():
quiz.title_i18n = _normalize_multilingual(quiz.title)
quiz.description_i18n = _normalize_multilingual(quiz.description)
quiz.save(update_fields=["title_i18n", "description_i18n"])
for question in Question.objects.all().iterator():
question.question_i18n = _normalize_multilingual(question.question)
question.option1_i18n = _normalize_multilingual(question.option1)
question.option2_i18n = _normalize_multilingual(question.option2)
question.option3_i18n = _normalize_multilingual(question.option3)
question.option4_i18n = _normalize_multilingual(question.option4)
question.save(
update_fields=[
"question_i18n",
"option1_i18n",
"option2_i18n",
"option3_i18n",
"option4_i18n",
]
)
class Migration(migrations.Migration):
dependencies = [
("quiz", "0004_alter_question_options"),
]
operations = [
migrations.AddField(
model_name="quiz",
name="title_i18n",
field=models.JSONField(default=list, help_text="Quiz Title", verbose_name="Title"),
),
migrations.AddField(
model_name="quiz",
name="description_i18n",
field=models.JSONField(blank=True, default=list, verbose_name="Description"),
),
migrations.AddField(
model_name="question",
name="question_i18n",
field=models.JSONField(default=list, verbose_name="Question"),
),
migrations.AddField(
model_name="question",
name="option1_i18n",
field=models.JSONField(default=list, verbose_name="Option 1"),
),
migrations.AddField(
model_name="question",
name="option2_i18n",
field=models.JSONField(default=list, verbose_name="Option 2"),
),
migrations.AddField(
model_name="question",
name="option3_i18n",
field=models.JSONField(default=list, verbose_name="Option 3"),
),
migrations.AddField(
model_name="question",
name="option4_i18n",
field=models.JSONField(default=list, verbose_name="Option 4"),
),
migrations.RunPython(forwards, migrations.RunPython.noop),
migrations.RemoveField(model_name="quiz", name="title"),
migrations.RemoveField(model_name="quiz", name="description"),
migrations.RemoveField(model_name="question", name="question"),
migrations.RemoveField(model_name="question", name="option1"),
migrations.RemoveField(model_name="question", name="option2"),
migrations.RemoveField(model_name="question", name="option3"),
migrations.RemoveField(model_name="question", name="option4"),
migrations.RenameField(model_name="quiz", old_name="title_i18n", new_name="title"),
migrations.RenameField(model_name="quiz", old_name="description_i18n", new_name="description"),
migrations.RenameField(model_name="question", old_name="question_i18n", new_name="question"),
migrations.RenameField(model_name="question", old_name="option1_i18n", new_name="option1"),
migrations.RenameField(model_name="question", old_name="option2_i18n", new_name="option2"),
migrations.RenameField(model_name="question", old_name="option3_i18n", new_name="option3"),
migrations.RenameField(model_name="question", old_name="option4_i18n", new_name="option4"),
]

24
apps/quiz/models/quiz.py

@ -1,6 +1,7 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.account.models import User
from apps.course.models.course import extract_text_from_json, format_multilingual_field
@ -8,8 +9,8 @@ class Quiz(models.Model):
lesson = models.ForeignKey("course.CourseLesson", verbose_name=_('Lesson'), related_name='quizzes', on_delete=models.CASCADE)
course = models.ForeignKey("course.Course", verbose_name=_('Course'), related_name='quizzes', on_delete=models.CASCADE , null=True, blank=True)
title = models.CharField(max_length=255, verbose_name=_('Title'), help_text=_("Quiz Title"))
description = models.CharField(max_length=55, blank=True, null=True, verbose_name=_("Description"))
title = models.JSONField(default=list, verbose_name=_('Title'), help_text=_("Quiz Title"))
description = models.JSONField(default=list, blank=True, verbose_name=_("Description"))
each_question_timing = models.PositiveIntegerField(verbose_name=_("Each Question Timing"))
status = models.BooleanField(default=True, verbose_name=_("Status"))
@ -29,6 +30,8 @@ class Quiz(models.Model):
# Automatically set the course based on the selected lesson
if self.lesson_id and hasattr(self.lesson, 'course'):
self.course = self.lesson.course
self.title = format_multilingual_field(self.title)
self.description = format_multilingual_field(self.description)
super().save(*args, **kwargs)
@ -41,11 +44,11 @@ class Question(models.Model):
]
quiz = models.ForeignKey(Quiz, verbose_name=_('Quiz'), on_delete=models.CASCADE, related_name='questions')
question = models.CharField(max_length=255, verbose_name=_('Question'))
option1 = models.CharField(max_length=255, verbose_name=_('Option 1'))
option2 = models.CharField(max_length=255, verbose_name=_('Option 2'))
option3 = models.CharField(max_length=255, verbose_name=_('Option 3'))
option4 = models.CharField(max_length=255, verbose_name=_('Option 4'))
question = models.JSONField(default=list, verbose_name=_('Question'))
option1 = models.JSONField(default=list, verbose_name=_('Option 1'))
option2 = models.JSONField(default=list, verbose_name=_('Option 2'))
option3 = models.JSONField(default=list, verbose_name=_('Option 3'))
option4 = models.JSONField(default=list, verbose_name=_('Option 4'))
correct_answer = models.PositiveSmallIntegerField(choices=CHOICES, verbose_name=_('Correct Answer'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created At'))
priority = models.IntegerField(null=True, blank=True, verbose_name=_('Priority'))
@ -57,7 +60,7 @@ class Question(models.Model):
ordering = ("priority", "id",)
def __str__(self):
return self.question
return extract_text_from_json(self.question)
def __repr__(self):
return f"Question(id={self.id})"
@ -66,6 +69,11 @@ class Question(models.Model):
if self.priority is None:
max_priority = Question.objects.filter(quiz=self.quiz).aggregate(models.Max('priority'))['priority__max']
self.priority = (max_priority or 0) + 1
self.question = format_multilingual_field(self.question)
self.option1 = format_multilingual_field(self.option1)
self.option2 = format_multilingual_field(self.option2)
self.option3 = format_multilingual_field(self.option3)
self.option4 = format_multilingual_field(self.option4)
super().save(*args, **kwargs)

46
apps/quiz/serializers/admin.py

@ -40,13 +40,17 @@ class AdminQuizListSerializer(serializers.ModelSerializer):
participants_count = serializers.SerializerMethodField()
course_title = serializers.SerializerMethodField()
lesson_title = serializers.SerializerMethodField()
title_i18n = serializers.JSONField(source='title', read_only=True)
description_i18n = serializers.JSONField(source='description', read_only=True)
class Meta:
model = Quiz
fields = [
'id',
'title',
'title_i18n',
'description',
'description_i18n',
'each_question_timing',
'status',
'course',
@ -76,19 +80,30 @@ class AdminQuizListSerializer(serializers.ModelSerializer):
raw_title = obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None)
return get_localized_field(lang, raw_title)
def to_representation(self, instance):
ret = super().to_representation(instance)
lang = get_request_lang(self)
ret['title'] = get_localized_field(lang, instance.title)
ret['description'] = get_localized_field(lang, instance.description)
return ret
class AdminQuizDetailSerializer(serializers.ModelSerializer):
questions_count = serializers.SerializerMethodField()
participants_count = serializers.SerializerMethodField()
course_title = serializers.SerializerMethodField()
lesson_title = serializers.SerializerMethodField()
title_i18n = serializers.JSONField(source='title', read_only=True)
description_i18n = serializers.JSONField(source='description', read_only=True)
class Meta:
model = Quiz
fields = [
'id',
'title',
'title_i18n',
'description',
'description_i18n',
'each_question_timing',
'status',
'course',
@ -119,22 +134,53 @@ class AdminQuizDetailSerializer(serializers.ModelSerializer):
raw_title = obj.lesson.title or (obj.lesson.lesson.title if obj.lesson.lesson else None)
return get_localized_field(lang, raw_title)
def to_representation(self, instance):
ret = super().to_representation(instance)
lang = get_request_lang(self)
ret['title'] = get_localized_field(lang, instance.title)
ret['description'] = get_localized_field(lang, instance.description)
return ret
class AdminQuestionSerializer(serializers.ModelSerializer):
question_i18n = serializers.JSONField(source='question', read_only=True)
option1_i18n = serializers.JSONField(source='option1', read_only=True)
option2_i18n = serializers.JSONField(source='option2', read_only=True)
option3_i18n = serializers.JSONField(source='option3', read_only=True)
option4_i18n = serializers.JSONField(source='option4', read_only=True)
class Meta:
model = Question
fields = [
'id',
'quiz',
'question',
'question_i18n',
'option1',
'option1_i18n',
'option2',
'option2_i18n',
'option3',
'option3_i18n',
'option4',
'option4_i18n',
'correct_answer',
'priority'
]
def _get_localized_value(self, value):
lang = get_request_lang(self)
return get_localized_field(lang, value)
def to_representation(self, instance):
ret = super().to_representation(instance)
ret['question'] = self._get_localized_value(instance.question)
ret['option1'] = self._get_localized_value(instance.option1)
ret['option2'] = self._get_localized_value(instance.option2)
ret['option3'] = self._get_localized_value(instance.option3)
ret['option4'] = self._get_localized_value(instance.option4)
return ret
class AdminParticipantAnswerSerializer(serializers.ModelSerializer):
class Meta:

35
apps/quiz/serializers/quiz.py

@ -1,6 +1,7 @@
from rest_framework import serializers
from apps.course.access import user_has_course_access
from apps.course.models.course import get_localized_field
from apps.quiz.models import Question, Quiz, QuizParticipant
@ -12,11 +13,23 @@ from apps.quiz.models import Question, Quiz, QuizParticipant
class QuizListSerializer(serializers.ModelSerializer):
is_complated = serializers.SerializerMethodField()
permission = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
description = serializers.SerializerMethodField()
class Meta:
model = Quiz
fields = ['id', 'title', 'description', 'permission', 'each_question_timing', 'is_complated']
def get_title(self, obj):
request = self.context.get('request')
lang = getattr(request, 'LANGUAGE_CODE', None) or 'en'
return get_localized_field(lang, obj.title)
def get_description(self, obj):
request = self.context.get('request')
lang = getattr(request, 'LANGUAGE_CODE', None) or 'en'
return get_localized_field(lang, obj.description)
def get_permission(self, obj):
request = self.context.get('request')
if not request or not request.user.is_authenticated:
@ -48,12 +61,20 @@ class QuizListSerializer(serializers.ModelSerializer):
class QuestionSerializer(serializers.ModelSerializer):
options = serializers.SerializerMethodField()
question = serializers.SerializerMethodField()
def get_question(self, obj):
request = self.context.get('request')
lang = getattr(request, 'LANGUAGE_CODE', None) or 'en'
return get_localized_field(lang, obj.question)
def get_options(self, obj) -> list:
request = self.context.get('request')
lang = getattr(request, 'LANGUAGE_CODE', None) or 'en'
return [
{
'id': i,
'title': getattr(obj, f"option{i}")
'title': get_localized_field(lang, getattr(obj, f"option{i}"))
} for i in range(1, 5)
]
@ -66,11 +87,23 @@ class QuizSerializer(serializers.ModelSerializer):
lesson = serializers.PrimaryKeyRelatedField(read_only=True)
questions = QuestionSerializer(many=True)
permission = serializers.SerializerMethodField()
title = serializers.SerializerMethodField()
description = serializers.SerializerMethodField()
class Meta:
model = Quiz
fields = ['id', 'permission', 'lesson', 'title', 'description', 'each_question_timing', 'questions']
def get_title(self, obj):
request = self.context.get('request')
lang = getattr(request, 'LANGUAGE_CODE', None) or 'en'
return get_localized_field(lang, obj.title)
def get_description(self, obj):
request = self.context.get('request')
lang = getattr(request, 'LANGUAGE_CODE', None) or 'en'
return get_localized_field(lang, obj.description)
def get_permission(self, obj):
request = self.context.get('request')
if not request or not request.user.is_authenticated:

21
apps/quiz/views/admin.py

@ -68,6 +68,21 @@ def find_matching_lesson_ids(search_term: str):
return matching_ids
def find_matching_quiz_ids(search_term: str):
normalized = (search_term or "").strip().lower()
if not normalized:
return []
matching_ids = []
for quiz in Quiz.objects.all().only("id", "title", "description"):
title_text = extract_text_from_json(quiz.title).lower()
description_text = extract_text_from_json(quiz.description).lower()
if normalized in title_text or normalized in description_text:
matching_ids.append(quiz.id)
return matching_ids
class AdminQuizViewSet(ModelViewSet):
"""
Admin CRUD ViewSet for Quiz management.
@ -96,15 +111,15 @@ class AdminQuizViewSet(ModelViewSet):
has_search = True
matching_course_ids = find_matching_course_ids(search_query)
matching_lesson_ids = find_matching_lesson_ids(search_query)
matching_quiz_ids = find_matching_quiz_ids(search_query)
from django.db.models import Case, When, Value, IntegerField
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(description__icontains=search_query) |
Q(id__in=matching_quiz_ids) |
Q(course_id__in=matching_course_ids) |
Q(lesson_id__in=matching_lesson_ids)
).annotate(
search_relevance=Case(
When(title__icontains=search_query, then=Value(2)),
When(id__in=matching_quiz_ids, then=Value(2)),
default=Value(1),
output_field=IntegerField()
)

Loading…
Cancel
Save