diff --git a/.env.prod b/.env.prod
index 5ee9c9b..034e084 100644
--- a/.env.prod
+++ b/.env.prod
@@ -1,4 +1,4 @@
-DJANGO_ALLOWED_HOSTS=127.0.0.1,imam-javad.nwhco.ir,www.imam-javad.nwhco.ir,*.nwhco.ir,188.40.92.124,88.99.212.243
+DJANGO_ALLOWED_HOSTS=127.0.0.1,imamjavad.nwhco.ir,www.imamjavad.nwhco.ir,*.nwhco.ir,188.40.92.124,88.99.212.243
DJANGO_SETTINGS_MODULE=config.settings.production
diff --git a/apps/chat/migrations/0004_roommessage_unread_messages_count.py b/apps/chat/migrations/0004_roommessage_unread_messages_count.py
new file mode 100644
index 0000000..5811ba2
--- /dev/null
+++ b/apps/chat/migrations/0004_roommessage_unread_messages_count.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2024-11-26 01:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('chat', '0003_auto_20241125_1737'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='roommessage',
+ name='unread_messages_count',
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/apps/chat/models.py b/apps/chat/models.py
index ecd8d89..d5263dc 100644
--- a/apps/chat/models.py
+++ b/apps/chat/models.py
@@ -46,7 +46,8 @@ class RoomMessage(models.Model):
default=RoomTypeChoices.GROUP,
verbose_name="Room Type"
)
-
+ unread_messages_count = models.IntegerField(default=0)
+
def __str__(self):
if self.room_type == self.RoomTypeChoices.GROUP:
return f"Group Room: {self.course.title if self.course else 'N/A'}"
diff --git a/apps/course/admin/course.py b/apps/course/admin/course.py
index b75ba99..f987c6f 100644
--- a/apps/course/admin/course.py
+++ b/apps/course/admin/course.py
@@ -42,7 +42,8 @@ class CourseAdmin(AjaxDatatable):
search_fields = ('title', 'description')
exclude = ('slug', )
-
+ # def has_change_permission(self, request, obj=None):
+ # return False
# @admin.display(description='Add Student')
# def _add_student(self, obj):
diff --git a/apps/course/doc.py b/apps/course/doc.py
index d7b2e39..55e929d 100644
--- a/apps/course/doc.py
+++ b/apps/course/doc.py
@@ -51,6 +51,11 @@ def doc_courses_lesson():
(مقدار is_complated مشخص میکند آیا کاربر این درس را گذرانده است
ممکن است درس دارای کوعیز باشد که باید در زیر آ» مانند طرح نمایش داده شود
)
+
+دارای ابجکت کوعیز که لیستی از کوعیز های مربوط به یک درس را نمایش میدهد
+بایستی مانند طرح در زیر درس قرار داده شود
+و دارای مقدار permission
+است که مشخص میکند ایا این کاربر کوعیز را از قبل شرکت کرده است
---
```
@@ -81,12 +86,27 @@ def doc_courses_lesson():
"content_file": null,
"video_link": "https://example.com/videos/variables_intro.mp4",
"is_complated": true,
- "quiz": {
- "title": "",
- "description": "",
- "is_complated": "",
- }
+ "quizs": [
+ {
+ "id": 1,
+ "title": "Тестовые курсы",
+ "description": "урок 1-2",
+ "permission": true,
+ "each_question_timing": 30
+ }
+ ]
+ },
+ {
+ "id": 1,
+ "title": "Introduction to Variables",
+ "duration": 30,
+ "content_type": "link",
+ "content_file": null,
+ "video_link": "https://example.com/videos/variables_intro.mp4",
+ "is_complated": true,
+ "quizs": null
}
+
]
```
"""
diff --git a/apps/course/migrations/0005_participant_unread_messages_count.py b/apps/course/migrations/0005_participant_unread_messages_count.py
new file mode 100644
index 0000000..acf281f
--- /dev/null
+++ b/apps/course/migrations/0005_participant_unread_messages_count.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2024-11-26 01:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('course', '0004_auto_20241122_1913'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='participant',
+ name='unread_messages_count',
+ field=models.IntegerField(default=0),
+ ),
+ ]
diff --git a/apps/course/models/participant.py b/apps/course/models/participant.py
index 8dbd266..2e99a4e 100644
--- a/apps/course/models/participant.py
+++ b/apps/course/models/participant.py
@@ -18,7 +18,8 @@ class Participant(models.Model):
related_name='participants'
)
joined_date = models.DateTimeField(auto_now_add=True)
-
+ unread_messages_count = models.IntegerField(default=0)
+
class Meta:
unique_together = ('student', 'course')
\ No newline at end of file
diff --git a/apps/course/serializers/lesson.py b/apps/course/serializers/lesson.py
index 19dc90b..43c6f6b 100644
--- a/apps/course/serializers/lesson.py
+++ b/apps/course/serializers/lesson.py
@@ -1,8 +1,6 @@
-
-
from rest_framework import serializers
from apps.course.models import Lesson, Participant, LessonCompletion
-
+from apps.quiz.serializers import QuizListSerializer
@@ -10,11 +8,11 @@ from apps.course.models import Lesson, Participant, LessonCompletion
class LessonSerializer(serializers.ModelSerializer):
is_complated = serializers.SerializerMethodField()
- quiz = serializers.SerializerMethodField()
+ quizs = serializers.SerializerMethodField()
class Meta:
model = Lesson
- fields = ['id', 'title', 'priority', 'is_active', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quiz']
+ fields = ['id', 'title', 'priority', 'is_active', 'duration', 'content_type', 'content_file', 'video_link', 'is_complated', 'quizs']
def get_is_complated(self, obj):
request = self.context.get('request')
@@ -35,5 +33,8 @@ class LessonSerializer(serializers.ModelSerializer):
).exists()
- def get_quiz(self, obj):
- return {}
+ def get_quizs(self, obj):
+ quizzes = obj.quizzes.all() # استفاده از related_name 'quizzes' برای دسترسی به کوییزهای درس
+ if quizzes.exists():
+ return QuizListSerializer(quizzes, many=True, context=self.context).data
+ return None
diff --git a/apps/course/views/lesson.py b/apps/course/views/lesson.py
index 831d1cd..cec6a12 100644
--- a/apps/course/views/lesson.py
+++ b/apps/course/views/lesson.py
@@ -3,14 +3,14 @@ from rest_framework.generics import ListAPIView, RetrieveAPIView
from drf_yasg.utils import swagger_auto_schema
from drf_yasg import openapi
from django.shortcuts import get_object_or_404
-
+from rest_framework import status
from apps.course.serializers import (
LessonSerializer
)
from apps.course.models import Course, Lesson
from apps.course.doc import *
-
+from utils.exceptions import AppAPIException
@@ -18,11 +18,17 @@ class LessonListView(ListAPIView):
serializer_class = LessonSerializer
queryset = Lesson.objects.filter(is_active=True)
-# doc_courses_lesson
@swagger_auto_schema(
operation_description=doc_courses_lesson(),
)
+ def get(self, request, *args, **kwargs):
+ return super().get(request, *args, **kwargs)
+
def get_queryset(self):
course_slug = self.kwargs.get('slug')
- course = get_object_or_404(Course, slug=course_slug)
+ course = get_object_or_404(Course, )
+ course = Course.objects.filter(slug=course_slug).first()
+ if not course:
+ raise AppAPIException({"message": "course not found"}, status_code=status.HTTP_404_NOT_FOUND)
+
return self.queryset.filter(course=course).order_by('priority','id')
diff --git a/apps/quiz/admin.py b/apps/quiz/admin.py
deleted file mode 100644
index 8c38f3f..0000000
--- a/apps/quiz/admin.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.contrib import admin
-
-# Register your models here.
diff --git a/apps/quiz/admin/__init__.py b/apps/quiz/admin/__init__.py
new file mode 100644
index 0000000..42092ad
--- /dev/null
+++ b/apps/quiz/admin/__init__.py
@@ -0,0 +1,5 @@
+from .quiz import *
+from .question import *
+from .participant import *
+# from .prize import *
+# from .user_rank_quiz import *
\ No newline at end of file
diff --git a/apps/quiz/admin/participant.py b/apps/quiz/admin/participant.py
new file mode 100644
index 0000000..8775a8f
--- /dev/null
+++ b/apps/quiz/admin/participant.py
@@ -0,0 +1,61 @@
+from ajaxdatatable.admin import AjaxDatatable
+from django.contrib import admin
+from django.db.models import F, Q
+from django.contrib.admin import SimpleListFilter
+from django.utils.translation import gettext_lazy as _
+from apps.quiz.models import QuizParticipant, ParticipantAnswer
+from apps.account.models import User
+import datetime
+
+
+class ParticipantAnswerInline(admin.StackedInline):
+ model = ParticipantAnswer
+
+ readonly_fields = (
+ '_correct_answer', 'question', 'at_time', 'answer_timing',
+ )
+
+ def _correct_answer(self, obj):
+ return obj.correct_answer
+
+ def has_add_permission(self, request, obj):
+ return False
+
+ def has_delete_permission(self, request, obj=None):
+ return False
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).annotate(correct_answer=F('question__correct_answer'))
+
+
+
+
+class UserEmailFilter(SimpleListFilter):
+ title = _('User Email')
+ parameter_name = 'user_email'
+
+ def lookups(self, request, model_admin):
+ users = User.objects.all()
+ return [(user.email, user.email) for user in users]
+
+ def queryset(self, request, queryset):
+ if self.value():
+ email = self.value().replace('%40', '@')
+ return queryset.filter(user__email=email)
+ return queryset
+
+
+@admin.register(QuizParticipant)
+class ParticipantAdmin(AjaxDatatable):
+ inlines = [ParticipantAnswerInline]
+ search_fields = ['user__username', 'user__fullname']
+ list_display = ['quiz', 'user', 'started_at', 'ended_at', 'total_timing', 'question_score', 'timing_score',
+ 'total_score']
+ latest_by = 'started_at'
+ list_filter = ['started_at', 'ended_at', 'quiz__status', UserEmailFilter]
+
+
+
+
+
+
diff --git a/apps/quiz/admin/question.py b/apps/quiz/admin/question.py
new file mode 100644
index 0000000..a46e063
--- /dev/null
+++ b/apps/quiz/admin/question.py
@@ -0,0 +1,63 @@
+from ajaxdatatable.admin import AjaxDatatable
+from django import forms
+from django.contrib import admin
+
+from apps.quiz.models import Question
+
+
+class QuestionAdminForm(forms.ModelForm):
+ class Meta:
+ model = Question
+ exclude = ()
+ widgets = {
+ 'correct_answer': forms.RadioSelect,
+ 'question': forms.Textarea
+ }
+
+
+# @admin.register(Question)
+# class QuestionAdmin(AjaxDatatable):
+# list_display = ('question', 'correct_answer', 'quiz', 'priority')
+# form = QuestionAdminForm
+# ordering = ("priority", "id",)
+# fieldsets = (
+# (
+# '', {
+# 'fields': (
+# 'question',
+# ('option1', 'option2'),
+# ('option3', 'option4'),
+# 'correct_answer',
+# )
+# },
+# ),
+# (
+# '', {
+# 'fields': ('priority',)
+# }
+# )
+# )
+
+class QuestionAdminInline(admin.StackedInline):
+ model = Question
+ list_display = ('question', 'correct_answer', 'quiz', 'priority')
+ form = QuestionAdminForm
+ ordering = ("priority", "id",)
+ extra = 0
+ fieldsets = (
+ (
+ '', {
+ 'fields': (
+ 'question',
+ ('option1', 'option2'),
+ ('option3', 'option4'),
+ 'correct_answer',
+ )
+ },
+ ),
+ (
+ '', {
+ 'fields': ('priority',)
+ }
+ )
+ )
diff --git a/apps/quiz/admin/quiz.py b/apps/quiz/admin/quiz.py
new file mode 100644
index 0000000..9520416
--- /dev/null
+++ b/apps/quiz/admin/quiz.py
@@ -0,0 +1,54 @@
+from ajaxdatatable.admin import AjaxDatatable
+from django.contrib import admin
+from django.db.models import Count
+from django.utils.safestring import mark_safe
+from django.utils.html import format_html
+from django.urls import reverse, path
+
+from apps.course.models import Lesson
+from apps.quiz.models import Quiz
+from .question import QuestionAdminInline
+
+
+
+
+@admin.register(Quiz)
+class QuizAdmin(AjaxDatatable):
+ search_fields = ['title', 'lesson__title']
+ list_display = ['title', 'description','lesson','each_question_timing', '_status', '_questions',]
+ autocomplete_fields = ['lesson',]
+ list_filter = ['each_question_timing',]
+ inlines = [QuestionAdminInline,]
+
+
+ def get_queryset(self, request):
+ queryset = super().get_queryset(request)
+ if request.user.groups.filter(name="Professor Group").exists():
+ return queryset.filter(lesson__course__professor=request.user)
+
+ return queryset
+
+ 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
+
+ return form
+
+
+ @admin.display(description='Status', ordering='status')
+ def _status(self, obj):
+ if obj.status:
+ return mark_safe("Active")
+
+ return mark_safe("Inactive")
+
+ @admin.display(description='Questions', ordering='questions_count')
+ def _questions(self, obj):
+ return mark_safe(f"Questions: {obj.questions_count}")
+
+ def get_queryset(self, request):
+ return super().get_queryset(request).annotate(
+ questions_count=Count('questions')
+ )
diff --git a/apps/quiz/admin/user_rank_quiz.py b/apps/quiz/admin/user_rank_quiz.py
new file mode 100644
index 0000000..e6b988d
--- /dev/null
+++ b/apps/quiz/admin/user_rank_quiz.py
@@ -0,0 +1,132 @@
+# import calendar
+# from django.utils import timezone
+
+# from ajaxdatatable.admin import AjaxDatatable
+# from django.contrib import admin
+# from django.utils.translation import gettext_lazy as _
+# from django.contrib.admin import SimpleListFilter
+# from django.db.models.functions import Rank, Coalesce
+# from django.db.models import Sum, F, Window, CharField
+# from django.utils.html import format_html
+# from apps.quiz.models import Quiz, QuizRankUser, Participant, QuizCategory
+# from apps.account.models import User
+
+
+# class QuizFilter(SimpleListFilter):
+# title = _('Quiz')
+# parameter_name = 'quiz'
+
+# def lookups(self, request, model_admin):
+# quizzes = Quiz.objects.all()
+# return [(quiz.id, quiz.video.title) for quiz in quizzes]
+
+# def queryset(self, request, queryset):
+# if self.value():
+# return queryset.filter(uquizzes__quiz__id=self.value())
+# return queryset
+
+# class QuizCategoryFilter(SimpleListFilter):
+# title = _('Quiz Category')
+# parameter_name = 'quiz_category'
+
+# def lookups(self, request, model_admin):
+# categories = QuizCategory.objects.all()
+# return [(category.id, category.name) for category in categories]
+
+# def queryset(self, request, queryset):
+# if self.value():
+# return queryset.filter(uquizzes__quiz__category__id=self.value())
+# return queryset
+
+# class MonthFilter(SimpleListFilter):
+# title = _('Month')
+# parameter_name = 'month'
+
+# def lookups(self, request, model_admin):
+# return [(str(i), calendar.month_name[i]) for i in range(1, 13)]
+
+# def queryset(self, request, queryset):
+# if self.value():
+# month = int(self.value())
+# year = timezone.now().year
+# return queryset.filter(uquizzes__started_at__year=year, uquizzes__started_at__month=month)
+# return queryset
+
+
+# @admin.register(QuizRankUser)
+# class QuizRankUserAdmin(AjaxDatatable):
+# list_display = ('username_link', 'get_total_score', 'get_rank')
+# list_filter = (QuizFilter, QuizCategoryFilter, MonthFilter)
+# readonly_fields = ('date_joined', 'last_login')
+
+
+# def get_queryset(self, request):
+# queryset = super().get_queryset(request)
+
+# quiz_id = request.GET.get('quiz')
+# category_id = request.GET.get('quiz_category')
+# month = request.GET.get('month')
+
+# filters = {}
+# if quiz_id:
+# filters['uquizzes__quiz_id'] = quiz_id
+# if category_id:
+# filters['uquizzes__quiz__category_id'] = category_id
+# if month:
+# month = int(month)
+# year = timezone.now().year
+# filters['uquizzes__started_at__year'] = year
+# filters['uquizzes__started_at__month'] = month
+
+# if filters:
+# queryset = queryset.filter(**filters)
+
+# users_scores = Participant.objects.filter(**{k.replace('uquizzes__', ''): v for k, v in filters.items()}).select_related('user').values(
+# username=Coalesce(F('user__username'), F('user__email'), output_field=CharField())
+# ).annotate(
+# score=Sum('total_score')
+# ).order_by('-score')
+
+# # Add rank to each user using window function
+# users_scores = users_scores.annotate(
+# rank=Window(
+# expression=Rank(),
+# order_by=F('score').desc()
+# )
+# ).order_by("rank")
+
+# user_scores_dict = {user['username']: user for user in users_scores}
+# for user in queryset:
+# user.score = user_scores_dict.get(user.username, {}).get('score', 0)
+# user.rank = user_scores_dict.get(user.username, {}).get('rank', 'N/A')
+# self.queryset = queryset
+# return queryset
+
+# def has_view_permission(self, request, obj=None):
+# return True
+
+# def has_change_permission(self, request, obj=None):
+# return False
+
+# def has_add_permission(self, request):
+# return False
+
+# def has_delete_permission(self, request, obj=None):
+# return False
+
+# def username_link(self, obj):
+# return format_html('{}', obj.id, obj.username)
+# username_link.short_description = 'Username'
+# username_link.admin_order_field = 'username'
+
+# def get_total_score(self, obj):
+# for user in self.queryset:
+# if user.id == obj.id:
+# return user.score
+# get_total_score.short_description = 'Total Score'
+
+# def get_rank(self, obj):
+# for user in self.queryset:
+# if user.id == obj.id:
+# return user.rank
+# get_rank.short_description = 'Rank'
diff --git a/apps/quiz/apps.py b/apps/quiz/apps.py
index 3dc8afe..519127d 100644
--- a/apps/quiz/apps.py
+++ b/apps/quiz/apps.py
@@ -3,4 +3,4 @@ from django.apps import AppConfig
class QuizConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
- name = 'quiz'
+ name = 'apps.quiz'
diff --git a/apps/quiz/doc.py b/apps/quiz/doc.py
new file mode 100644
index 0000000..7099d7e
--- /dev/null
+++ b/apps/quiz/doc.py
@@ -0,0 +1,131 @@
+def doc_quiz_submit():
+ return """
+# 📝 ارسال پاسخهای کوییز
+
+این API برای ثبت شرکت کاربر در کوییز و ارسال پاسخهای مربوطه استفاده میشود. زمانی که کاربر در کوییز شرکت میکند، باید به همراه پاسخهای خود، زمان پاسخدهی و اطلاعات دیگر را ارسال نماید. در این API، کاربر نمیتواند دوباره در همان کوییز شرکت کند.
+
+---
+
+## 📄 توضیحات مقادیر پاسخ
+
+| کلید | نوع داده | توضیحات |
+|------------------------|-----------------|---------------------------------------------------------|
+| `quiz` | Integer | شناسه کوییز که کاربر در آن شرکت کرده است. |
+| `started_at` | DateTime | زمان شروع کوییز. |
+| `ended_at` | DateTime | زمان پایان کوییز. |
+| `total_timing` | Integer | مدت زمان کلی که کاربر برای پاسخدهی به کوییز صرف کرده است.|
+| `question_score` | Integer | امتیاز بهدستآمده توسط کاربر در پاسخ به سوالات کوییز. |
+| `timing_score` | Integer | امتیاز بهدستآمده توسط کاربر بر اساس زمان پاسخدهی. |
+| `total_score` | Integer | امتیاز کلی کاربر در کوییز (ترکیب امتیاز سوالات و زمان).|
+| `answers` | Array | لیستی از پاسخهای کاربر به سوالات. |
+| `answers.question` | Integer | شناسه سوالی که کاربر به آن پاسخ داده است. |
+| `answers.option_num` | Integer | شماره گزینهای که کاربر انتخاب کرده است. |
+| `answers.at_time` | DateTime | زمانی که کاربر پاسخ به سوال را ارسال کرده است. |
+| `answers.answer_timing`| Integer | مدت زمان پاسخدهی به سوال (در ثانیه). |
+
+---
+## errors:
+# کاربر از قبل کوعیز را شرکت کرده است
+```json
+{
+ "status": "error",
+ "code": "validation_error",
+ "status_code": 400,
+ "message": "There were validation errors.",
+ "errors": [
+ {
+ "field": "quiz",
+ "message": "you have already participated in the quiz"
+ }
+ ]
+}
+```
+
+---
+## پاسخ موفق (201 Created)
+
+در صورتی که ثبتنام موفقیتآمیز باشد و پاسخها ذخیره شوند، یک شیء JSON مشابه با نمونه زیر برگشت داده میشود:
+
+### پاسخ:
+```json
+{
+ "quiz": 1,
+ "started_at": "2024-11-29T12:00:00Z",
+ "ended_at": "2024-11-29T12:30:00Z",
+ "total_timing": 1800,
+ "question_score": 80,
+ "timing_score": 10,
+ "total_score": 90,
+ "answers": [
+ {
+ "question": 1,
+ "option_num": 3,
+ "at_time": "2024-11-29T12:05:00Z",
+ "answer_timing": 30
+ },
+ {
+ "question": 2,
+ "option_num": 1,
+ "at_time": "2024-11-29T12:15:00Z",
+ "answer_timing": 45
+ }
+ ]
+}
+"""
+
+def doc_quiz_detail():
+ return """
+# 📋 Quiz Detail API
+
+با ایدی درس میتواند وارد یک کوعیز شوید
+این api
+سوالات کوعیز و جزعیاتش را برمیگرداند
+
+
+## URL
+`GET /path//`
+
+## پارامترها
+
+- `lesson_id`: شناسه درس برای دریافت کوییز مرتبط.
+
+## پاسخ موفق (200 OK)
+
+در صورتی که درس دارای کوییز باشد، یک شیء JSON با اطلاعات کوییز برگشت داده میشود.
+
+### پاسخ:
+
+```json
+{
+ "id": 1,
+ "permission": true,
+ "lesson": 101,
+ "title": "Quiz on Python Basics",
+ "description": "A quiz on the basics of Python programming.",
+ "each_question_timing": 30,
+ "questions": [
+ {
+ "id": 1,
+ "question": "What is the output of print(2 + 3)?",
+ "options": [
+ {"id": 1, "title": "5"},
+ {"id": 2, "title": "6"},
+ {"id": 3, "title": "7"},
+ {"id": 4, "title": "8"}
+ ],
+ "correct_answer": 1
+ },
+ {
+ "id": 2,
+ "question": "What is the result of 2 * 3?",
+ "options": [
+ {"id": 1, "title": "6"},
+ {"id": 2, "title": "5"},
+ {"id": 3, "title": "7"},
+ {"id": 4, "title": "8"}
+ ],
+ "correct_answer": 1
+ }
+ ]
+}
+"""
diff --git a/apps/quiz/migrations/0001_initial.py b/apps/quiz/migrations/0001_initial.py
new file mode 100644
index 0000000..44ea814
--- /dev/null
+++ b/apps/quiz/migrations/0001_initial.py
@@ -0,0 +1,102 @@
+# Generated by Django 3.2.4 on 2024-11-29 11:00
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('course', '0005_participant_unread_messages_count'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('account', '0004_user_skill'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Quiz',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(help_text='Quiz Title', max_length=255, verbose_name='title')),
+ ('each_question_timing', models.PositiveIntegerField()),
+ ('status', models.BooleanField(default=True)),
+ ('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='quizzes', to='course.lesson', verbose_name='lesson')),
+ ],
+ options={
+ 'verbose_name': 'Quiz',
+ 'verbose_name_plural': 'Quizzes',
+ 'ordering': ('-id',),
+ },
+ ),
+ migrations.CreateModel(
+ name='QuizRankUser',
+ fields=[
+ ],
+ options={
+ 'verbose_name': 'Rank Quiz',
+ 'verbose_name_plural': 'Rank Quizzes',
+ 'proxy': True,
+ 'indexes': [],
+ 'constraints': [],
+ },
+ bases=('account.user',),
+ ),
+ migrations.CreateModel(
+ name='QuizParticipant',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('started_at', models.DateTimeField(verbose_name='started at')),
+ ('ended_at', models.DateTimeField(verbose_name='ended at')),
+ ('total_timing', models.PositiveIntegerField(help_text='Seconds take to finish the quiz')),
+ ('question_score', models.PositiveIntegerField()),
+ ('timing_score', models.PositiveIntegerField()),
+ ('total_score', models.PositiveIntegerField()),
+ ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='participants', to='quiz.quiz')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='uquizzes', to=settings.AUTH_USER_MODEL, verbose_name='user')),
+ ],
+ options={
+ 'verbose_name': 'Participant',
+ 'verbose_name_plural': 'Participants',
+ 'ordering': ('-id',),
+ },
+ ),
+ migrations.CreateModel(
+ name='Question',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('question', models.CharField(max_length=255)),
+ ('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')),
+ ('correct_answer', models.PositiveSmallIntegerField(choices=[(1, 'Option 1'), (2, 'Option 2'), (3, 'Option 3'), (4, 'Option 4')])),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
+ ('priority', models.IntegerField(blank=True, null=True)),
+ ('quiz', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to='quiz.quiz', verbose_name='quiz')),
+ ],
+ options={
+ 'verbose_name': 'Question',
+ 'verbose_name_plural': 'Questions',
+ 'ordering': ('-priority', '-id'),
+ },
+ ),
+ migrations.CreateModel(
+ name='ParticipantAnswer',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('option_num', models.PositiveSmallIntegerField(choices=[(1, 'Option 1'), (2, 'Option 2'), (3, 'Option 3'), (4, 'Option 4')], verbose_name='selected option')),
+ ('at_time', models.DateTimeField()),
+ ('answer_timing', models.PositiveSmallIntegerField(default=0, verbose_name='seconds take to answer')),
+ ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='quiz.quizparticipant')),
+ ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='quiz.question')),
+ ],
+ options={
+ 'verbose_name': 'User Quiz Answer',
+ 'verbose_name_plural': 'User Quiz Answers',
+ 'ordering': ('-id',),
+ },
+ ),
+ ]
diff --git a/apps/quiz/migrations/0002_quiz_description.py b/apps/quiz/migrations/0002_quiz_description.py
new file mode 100644
index 0000000..a7b0f71
--- /dev/null
+++ b/apps/quiz/migrations/0002_quiz_description.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.2.4 on 2024-11-29 11:58
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('quiz', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='quiz',
+ name='description',
+ field=models.CharField(blank=True, max_length=55, null=True, verbose_name='Description'),
+ ),
+ ]
diff --git a/apps/quiz/models/__init__.py b/apps/quiz/models/__init__.py
index e69de29..16a10f1 100644
--- a/apps/quiz/models/__init__.py
+++ b/apps/quiz/models/__init__.py
@@ -0,0 +1,2 @@
+from .quiz import *
+from .participant import *
\ No newline at end of file
diff --git a/apps/quiz/models/participant.py b/apps/quiz/models/participant.py
index 8454bb5..305af43 100644
--- a/apps/quiz/models/participant.py
+++ b/apps/quiz/models/participant.py
@@ -5,7 +5,7 @@ from apps.account.models import User
-class Participant(models.Model):
+class QuizParticipant(models.Model):
quiz = models.ForeignKey('quiz.Quiz', on_delete=models.CASCADE, related_name='participants')
user = models.ForeignKey('account.User', on_delete=models.CASCADE, verbose_name='user', related_name='uquizzes')
started_at = models.DateTimeField(verbose_name='started at')
@@ -30,7 +30,7 @@ class Participant(models.Model):
@staticmethod
def get_user_ranks(quiz_id):
- return Participant.objects.filter(quiz_id=quiz_id).annotate(
+ return QuizParticipant.objects.filter(quiz_id=quiz_id).annotate(
rank=Window(
expression=Rank(),
order_by=F('total_score').desc()
@@ -47,7 +47,7 @@ class ParticipantAnswer(models.Model):
(4, 'Option 4'),
]
- participant = models.ForeignKey(Participant, on_delete=models.CASCADE, related_name='answers')
+ participant = models.ForeignKey(QuizParticipant, on_delete=models.CASCADE, related_name='answers')
question = models.ForeignKey("quiz.Question", on_delete=models.CASCADE)
option_num = models.PositiveSmallIntegerField(choices=CHOICES, verbose_name='selected option')
at_time = models.DateTimeField()
diff --git a/apps/quiz/models/quiz.py b/apps/quiz/models/quiz.py
index ec9972c..26bda6a 100644
--- a/apps/quiz/models/quiz.py
+++ b/apps/quiz/models/quiz.py
@@ -1,9 +1,13 @@
from django.db import models
+from django.utils.translation import gettext_lazy as _
+from apps.account.models import User
class Quiz(models.Model):
- course = models.ForeignKey("course.Course", verbose_name='course', related_name='quizzes', on_delete=models.CASCADE)
+ lesson = models.ForeignKey("course.Lesson", 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()
status = models.BooleanField(default=True)
@@ -51,3 +55,11 @@ class Question(models.Model):
def __repr__(self):
return f"Question(id={self.id})"
+
+
+class QuizRankUser(User):
+ class Meta:
+ proxy = True
+ verbose_name = 'Rank Quiz'
+ verbose_name_plural = 'Rank Quizzes'
+
diff --git a/apps/quiz/serializers/__init__.py b/apps/quiz/serializers/__init__.py
new file mode 100644
index 0000000..16a10f1
--- /dev/null
+++ b/apps/quiz/serializers/__init__.py
@@ -0,0 +1,2 @@
+from .quiz import *
+from .participant import *
\ No newline at end of file
diff --git a/apps/quiz/serializers/participant.py b/apps/quiz/serializers/participant.py
new file mode 100644
index 0000000..2ec1280
--- /dev/null
+++ b/apps/quiz/serializers/participant.py
@@ -0,0 +1,45 @@
+from rest_framework import serializers
+
+from apps.quiz.models import QuizParticipant, ParticipantAnswer
+
+
+class ParticipantAnswerSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ParticipantAnswer
+ fields = ['question', 'option_num', 'at_time', 'answer_timing']
+
+
+
+class QuizParticipantSerializer(serializers.ModelSerializer):
+ user = serializers.HiddenField(default=serializers.CurrentUserDefault())
+ answers = ParticipantAnswerSerializer(many=True)
+
+ def validate_quiz(self, obj):
+ if QuizParticipant.objects.filter(quiz=obj, user=self.context['request'].user).exists():
+ raise serializers.ValidationError('you have already participated in the quiz')
+
+ return obj
+
+ class Meta:
+ model = QuizParticipant
+ fields = [
+ 'quiz', 'user', 'started_at', 'ended_at', 'total_timing',
+ 'question_score', 'timing_score', 'total_score',
+ 'answers',
+ ]
+
+ def create(self, validated_data):
+ answers = validated_data.pop('answers', [])
+ obj = super().create(validated_data)
+ answers_objs = []
+ for ans in answers:
+ answers_objs.append(
+ ParticipantAnswer(
+ participant=obj,
+ **ans,
+ )
+ )
+
+ ParticipantAnswer.objects.bulk_create(answers_objs)
+
+ return obj
diff --git a/apps/quiz/serializers/quiz.py b/apps/quiz/serializers/quiz.py
new file mode 100644
index 0000000..08c6c55
--- /dev/null
+++ b/apps/quiz/serializers/quiz.py
@@ -0,0 +1,59 @@
+from rest_framework import serializers
+
+from apps.quiz.models import Question, Quiz, QuizParticipant
+from apps.course.models import Lesson
+
+
+
+
+
+
+
+class QuizListSerializer(serializers.ModelSerializer):
+ permission = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Quiz
+ fields = ['id', 'title', 'description', 'permission', 'each_question_timing',]
+
+ def get_permission(self, obj):
+ # Check if the user has participated in this quiz
+ user = self.context['request'].user
+ participated = QuizParticipant.objects.filter(user=user, quiz=obj).exists()
+ return not participated
+
+
+
+
+class QuestionSerializer(serializers.ModelSerializer):
+ options = serializers.SerializerMethodField()
+
+ def get_options(self, obj) -> list:
+ return [
+ {
+ 'id': i,
+ 'title': getattr(obj, f"option{i}")
+ } 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)
+ questions = QuestionSerializer(many=True)
+ permission = serializers.SerializerMethodField()
+
+ class Meta:
+ model = Quiz
+ fields = ['id', 'permission', 'lesson', 'title', 'description', 'each_question_timing', 'questions']
+
+ def get_permission(self, obj):
+ # Check if the user has participated in this quiz
+ user = self.context['request'].user
+ participated = QuizParticipant.objects.filter(user=user, quiz=obj).exists()
+ return not participated
+
+
diff --git a/apps/quiz/urls.py b/apps/quiz/urls.py
new file mode 100644
index 0000000..1245fea
--- /dev/null
+++ b/apps/quiz/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path
+
+from . import views
+
+urlpatterns = [
+ # path('prizes/', PrizeListAPIView.as_view()),
+ # path('ranked-list/', RankedListAPIView.as_view()),
+ # path('self-rank/', SelfRankAPIView.as_view()),
+ # path('my-quizzes/', UserQuizScores.as_view()),
+ path('submit-quiz/', views.QuizParticipantCreateAPIView.as_view()),
+ path('/', views.QuizDetailAPIView.as_view()),
+
+]
diff --git a/apps/quiz/views/__init__.py b/apps/quiz/views/__init__.py
new file mode 100644
index 0000000..16a10f1
--- /dev/null
+++ b/apps/quiz/views/__init__.py
@@ -0,0 +1,2 @@
+from .quiz import *
+from .participant import *
\ No newline at end of file
diff --git a/apps/quiz/views/participant.py b/apps/quiz/views/participant.py
new file mode 100644
index 0000000..2a0504d
--- /dev/null
+++ b/apps/quiz/views/participant.py
@@ -0,0 +1,22 @@
+from django.db.models import Value
+from rest_framework.generics import RetrieveAPIView, CreateAPIView
+from rest_framework.permissions import IsAuthenticated
+
+from drf_yasg.utils import swagger_auto_schema
+from drf_yasg import openapi
+
+from apps.quiz.serializers import QuizParticipantSerializer
+from apps.quiz.doc import *
+
+
+
+class QuizParticipantCreateAPIView(CreateAPIView):
+ serializer_class = QuizParticipantSerializer
+ permission_classes = [IsAuthenticated]
+
+
+ @swagger_auto_schema(
+ operation_description=doc_quiz_submit(),
+ )
+ def post(self, request, *args, **kwargs):
+ return super().post(request, *args, **kwargs)
diff --git a/apps/quiz/views/quiz.py b/apps/quiz/views/quiz.py
new file mode 100644
index 0000000..507d25c
--- /dev/null
+++ b/apps/quiz/views/quiz.py
@@ -0,0 +1,33 @@
+from django.db.models import Value
+from rest_framework.generics import RetrieveAPIView
+from rest_framework.permissions import IsAuthenticated
+
+from drf_yasg.utils import swagger_auto_schema
+from drf_yasg import openapi
+from apps.quiz.models import Quiz
+from apps.quiz.serializers.quiz import QuizSerializer
+from apps.quiz.doc import *
+
+
+
+class QuizDetailAPIView(RetrieveAPIView):
+ serializer_class = QuizSerializer
+ permission_classes = [IsAuthenticated]
+
+
+ @swagger_auto_schema(
+ operation_description=doc_quiz_detail(),
+ )
+ def get(self, request, *args, **kwargs):
+ return super().get(request, *args, **kwargs)
+
+ def get_object(self):
+ return Quiz.objects.filter(
+ lesson__id=self.kwargs['lesson_id'],
+ ).annotate(
+ lesson__has_quiz=Value(True)
+ ).select_related('lesson').first()
+
+
+
+
diff --git a/config/settings/base.py b/config/settings/base.py
index 99005bc..8df4188 100644
--- a/config/settings/base.py
+++ b/config/settings/base.py
@@ -44,6 +44,7 @@ LOCAL_APPS = [
'apps.api.apps.ApiConfig',
'apps.course.apps.CourseConfig',
'apps.chat.apps.ChatConfig',
+ 'apps.quiz.apps.QuizConfig',
]
THIRD_PARTY_APPS = [
diff --git a/config/urls.py b/config/urls.py
index 0eaccb1..34860d6 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -36,6 +36,7 @@ api_patterns = [
path('account/', include('apps.account.urls')),
path('courses/', include('apps.course.urls')),
+ path('quiz/', include('apps.quiz.urls')),
path('upload-tmp-media/', UploadTmpMedia.as_view()),
]