diff --git a/apps/course/serializers/lesson.py b/apps/course/serializers/lesson.py index f545f16..5d95553 100644 --- a/apps/course/serializers/lesson.py +++ b/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 \ No newline at end of file diff --git a/apps/course/views/lesson.py b/apps/course/views/lesson.py index 17f8aa4..94be924 100644 --- a/apps/course/views/lesson.py +++ b/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 diff --git a/apps/quiz/admin/quiz.py b/apps/quiz/admin/quiz.py index 1956e12..95d7cd5 100644 --- a/apps/quiz/admin/quiz.py +++ b/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 diff --git a/apps/quiz/management/__init__.py b/apps/quiz/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/quiz/management/commands/__init__.py b/apps/quiz/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/quiz/management/commands/clear_quiz_data.py b/apps/quiz/management/commands/clear_quiz_data.py new file mode 100644 index 0000000..c3a0192 --- /dev/null +++ b/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 \ No newline at end of file diff --git a/apps/quiz/migrations/0002_change_quiz_lesson_to_courselesson.py b/apps/quiz/migrations/0002_change_quiz_lesson_to_courselesson.py new file mode 100644 index 0000000..fe81dbe --- /dev/null +++ b/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'), + ), + ] diff --git a/apps/quiz/models/quiz.py b/apps/quiz/models/quiz.py index 26bda6a..1be1190 100644 --- a/apps/quiz/models/quiz.py +++ b/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() diff --git a/apps/quiz/serializers/quiz.py b/apps/quiz/serializers/quiz.py index 99e2aeb..6156e64 100644 --- a/apps/quiz/serializers/quiz.py +++ b/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 diff --git a/config/settings/base.py b/config/settings/base.py index 9761a72..9fc0f3b 100755 --- a/config/settings/base.py +++ b/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,