Browse Source

refactor(quiz): Adjust quiz lesson relation

- Changed Quiz.lesson to CourseLesson
- Updated serializers and views
- Modified admin to restrict access
- Added management command to clear data
- Updated admin UI
master
mortezaei 9 months ago
parent
commit
8807bd61bd
  1. 6
      apps/course/serializers/lesson.py
  2. 2
      apps/course/views/lesson.py
  3. 23
      apps/quiz/admin/quiz.py
  4. 0
      apps/quiz/management/__init__.py
  5. 0
      apps/quiz/management/commands/__init__.py
  6. 78
      apps/quiz/management/commands/clear_quiz_data.py
  7. 20
      apps/quiz/migrations/0002_change_quiz_lesson_to_courselesson.py
  8. 3
      apps/quiz/models/quiz.py
  9. 13
      apps/quiz/serializers/quiz.py
  10. 28
      config/settings/base.py

6
apps/course/serializers/lesson.py

@ -63,9 +63,9 @@ class CourseLessonSerializer(serializers.ModelSerializer):
).exists()
def get_quizs(self, obj):
# Assuming the related_name for quizzes is now on CourseLesson
# print(f'--> type:{type(obj)} obj:{obj.lesson.quizzes.all()}')
quizzes = obj.lesson.quizzes.all() if hasattr(obj.lesson, 'quizzes') else []
# 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
return None

2
apps/course/views/lesson.py

@ -37,7 +37,7 @@ class LessonListView(ListAPIView):
'lesson'
).prefetch_related(
'completions',
'lesson__quizzes'
'quizzes'
).filter(
course=course,
is_active=True

23
apps/quiz/admin/quiz.py

@ -6,7 +6,7 @@ from django.urls import reverse
from unfold.admin import ModelAdmin
from unfold.decorators import display
from apps.course.models import Lesson
from apps.course.models import CourseLesson
from apps.quiz.models import Quiz
from apps.quiz.admin.question import QuestionAdminInline
from utils.admin import project_admin_site
@ -40,12 +40,21 @@ class QuizAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
if obj is None:
form.base_fields['lesson'].queryset = (
Lesson.objects.all() if request.user.is_staff
else Lesson.objects.filter(course__professor=request.user)
)
form.base_fields['lesson'].widget.can_add_related = False
# محدود کردن انتخاب lesson بر اساس سطح دسترسی کاربر
if (request.user.is_staff or
request.user.has_role('admin') or
request.user.has_role('super_admin')):
# اولویت اول: staff یا admin - دسترسی کامل
form.base_fields['lesson'].queryset = CourseLesson.objects.all()
elif request.user.has_role('professor'):
# اولویت دوم: professor - فقط CourseLesson های دوره‌های خود
form.base_fields['lesson'].queryset = CourseLesson.objects.filter(course__professor=request.user)
else:
# سایر کاربران - عدم دسترسی
form.base_fields['lesson'].queryset = CourseLesson.objects.none()
form.base_fields['lesson'].widget.can_add_related = False
return form

0
apps/quiz/management/__init__.py

0
apps/quiz/management/commands/__init__.py

78
apps/quiz/management/commands/clear_quiz_data.py

@ -0,0 +1,78 @@
from django.core.management.base import BaseCommand
from django.db import transaction
from apps.quiz.models import Quiz, Question, QuizParticipant, ParticipantAnswer
class Command(BaseCommand):
help = 'Clear all quiz-related data from the database'
def add_arguments(self, parser):
parser.add_argument(
'--confirm',
action='store_true',
help='Confirm that you want to delete all quiz data',
)
def handle(self, *args, **options):
if not options['confirm']:
self.stdout.write(
self.style.WARNING(
'This command will delete ALL quiz-related data from the database!\n'
'This includes:\n'
'- All Quizzes\n'
'- All Questions\n'
'- All Quiz Participants\n'
'- All Participant Answers\n\n'
'Use --confirm flag to proceed with deletion.\n'
'Example: python manage.py clear_quiz_data --confirm'
)
)
return
try:
with transaction.atomic():
# Count records before deletion
participant_answers_count = ParticipantAnswer.objects.count()
quiz_participants_count = QuizParticipant.objects.count()
questions_count = Question.objects.count()
quizzes_count = Quiz.objects.count()
self.stdout.write(
f'Found {participant_answers_count} participant answers, '
f'{quiz_participants_count} quiz participants, '
f'{questions_count} questions, and '
f'{quizzes_count} quizzes.'
)
# Delete in order to respect foreign key constraints
# ParticipantAnswer -> QuizParticipant -> Quiz
# Question -> Quiz
self.stdout.write('Deleting participant answers...')
ParticipantAnswer.objects.all().delete()
self.stdout.write('Deleting quiz participants...')
QuizParticipant.objects.all().delete()
self.stdout.write('Deleting questions...')
Question.objects.all().delete()
self.stdout.write('Deleting quizzes...')
Quiz.objects.all().delete()
self.stdout.write(
self.style.SUCCESS(
f'Successfully deleted all quiz data:\n'
f'- {participant_answers_count} participant answers\n'
f'- {quiz_participants_count} quiz participants\n'
f'- {questions_count} questions\n'
f'- {quizzes_count} quizzes'
)
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'Error occurred while clearing quiz data: {str(e)}')
)
raise

20
apps/quiz/migrations/0002_change_quiz_lesson_to_courselesson.py

@ -0,0 +1,20 @@
# Generated by Django 5.1.8 on 2025-08-12 22:14
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('course', '0006_participant_is_active'),
('quiz', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='quiz',
name='lesson',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='course.courselesson', verbose_name='lesson'),
),
]

3
apps/quiz/models/quiz.py

@ -5,7 +5,8 @@ from apps.account.models import User
class Quiz(models.Model):
lesson = models.ForeignKey("course.Lesson", verbose_name=_('lesson'), related_name='quizzes', on_delete=models.CASCADE)
lesson = models.ForeignKey("course.CourseLesson", verbose_name=_('lesson'), related_name='quizzes', on_delete=models.CASCADE)
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")
each_question_timing = models.PositiveIntegerField()

13
apps/quiz/serializers/quiz.py

@ -1,7 +1,7 @@
from rest_framework import serializers
from apps.quiz.models import Question, Quiz, QuizParticipant
from apps.course.models import Lesson, Participant
from apps.course.models import Participant
@ -24,9 +24,8 @@ class QuizListSerializer(serializers.ModelSerializer):
# Check if the user has participated in this quiz
user = request.user
# Get the course through CourseLesson model
from apps.course.models.lesson import CourseLesson
course_lesson = CourseLesson.objects.filter(lesson=obj.lesson).first()
# obj.lesson is now CourseLesson directly
course_lesson = obj.lesson
if not course_lesson:
return False
@ -86,16 +85,14 @@ class QuizSerializer(serializers.ModelSerializer):
# Check if the user has participated in this quiz
user = request.user
# Get the course through CourseLesson model
from apps.course.models.lesson import CourseLesson
course_lesson = CourseLesson.objects.filter(lesson=obj.lesson).first()
# obj.lesson is now CourseLesson directly
course_lesson = obj.lesson
if not course_lesson:
return False
course = course_lesson.course
# Check if user is a participant in the course
from apps.course.models import Participant
if not Participant.objects.filter(student=user, course=course).exists():
return False

28
config/settings/base.py

@ -461,8 +461,7 @@ UNFOLD = {
"course.course",
"course.courselesson",
"course.courseglossary",
"course.courseattachment",
"quiz.quiz",
"course.courseattachment",
],
"items": [
{
@ -489,13 +488,7 @@ UNFOLD = {
"link": reverse_lazy("admin:course_courseglossary_changelist"),
"active": lambda request: request.path.startswith(str(reverse_lazy("admin:course_courseglossary_changelist"))),
},
{
"title": _("Quizzes"),
"icon": "quiz",
"link": reverse_lazy("admin:quiz_quiz_changelist"),
"active": lambda request: request.path.startswith(str(reverse_lazy("admin:quiz_quiz_changelist"))),
},
],
},
],
@ -609,6 +602,23 @@ UNFOLD = {
},
]
},
{
"title": _("Quizzes"),
"collapsible": True,
"separator": True,
"items": [
{
"title": _("Quizzes"),
"icon": "quiz",
"link": reverse_lazy("admin:quiz_quiz_changelist"),
},
{
"title": _("Quiz Participants"),
"icon": "group",
"link": reverse_lazy("admin:quiz_quizparticipant_changelist"),
},
]
},
{
"title": _("Transactions"),
"collapsible": True,

Loading…
Cancel
Save