Browse Source

update

master
alireza 1 year ago
parent
commit
0376e44003
  1. 2
      .env.prod
  2. 18
      apps/chat/migrations/0004_roommessage_unread_messages_count.py
  3. 1
      apps/chat/models.py
  4. 3
      apps/course/admin/course.py
  5. 28
      apps/course/doc.py
  6. 18
      apps/course/migrations/0005_participant_unread_messages_count.py
  7. 1
      apps/course/models/participant.py
  8. 15
      apps/course/serializers/lesson.py
  9. 14
      apps/course/views/lesson.py
  10. 3
      apps/quiz/admin.py
  11. 5
      apps/quiz/admin/__init__.py
  12. 61
      apps/quiz/admin/participant.py
  13. 63
      apps/quiz/admin/question.py
  14. 54
      apps/quiz/admin/quiz.py
  15. 132
      apps/quiz/admin/user_rank_quiz.py
  16. 2
      apps/quiz/apps.py
  17. 131
      apps/quiz/doc.py
  18. 102
      apps/quiz/migrations/0001_initial.py
  19. 18
      apps/quiz/migrations/0002_quiz_description.py
  20. 2
      apps/quiz/models/__init__.py
  21. 6
      apps/quiz/models/participant.py
  22. 14
      apps/quiz/models/quiz.py
  23. 2
      apps/quiz/serializers/__init__.py
  24. 45
      apps/quiz/serializers/participant.py
  25. 59
      apps/quiz/serializers/quiz.py
  26. 13
      apps/quiz/urls.py
  27. 2
      apps/quiz/views/__init__.py
  28. 22
      apps/quiz/views/participant.py
  29. 33
      apps/quiz/views/quiz.py
  30. 1
      config/settings/base.py
  31. 1
      config/urls.py

2
.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 DJANGO_SETTINGS_MODULE=config.settings.production

18
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),
),
]

1
apps/chat/models.py

@ -46,6 +46,7 @@ class RoomMessage(models.Model):
default=RoomTypeChoices.GROUP, default=RoomTypeChoices.GROUP,
verbose_name="Room Type" verbose_name="Room Type"
) )
unread_messages_count = models.IntegerField(default=0)
def __str__(self): def __str__(self):
if self.room_type == self.RoomTypeChoices.GROUP: if self.room_type == self.RoomTypeChoices.GROUP:

3
apps/course/admin/course.py

@ -42,7 +42,8 @@ class CourseAdmin(AjaxDatatable):
search_fields = ('title', 'description') search_fields = ('title', 'description')
exclude = ('slug', ) exclude = ('slug', )
# def has_change_permission(self, request, obj=None):
# return False
# @admin.display(description='Add Student') # @admin.display(description='Add Student')
# def _add_student(self, obj): # def _add_student(self, obj):

28
apps/course/doc.py

@ -51,6 +51,11 @@ def doc_courses_lesson():
(مقدار is_complated مشخص میکند آیا کاربر این درس را گذرانده است (مقدار is_complated مشخص میکند آیا کاربر این درس را گذرانده است
ممکن است درس دارای کوعیز باشد که باید در زیر آ» مانند طرح نمایش داده شود ممکن است درس دارای کوعیز باشد که باید در زیر آ» مانند طرح نمایش داده شود
) )
دارای ابجکت کوعیز که لیستی از کوعیز های مربوط به یک درس را نمایش میدهد
بایستی مانند طرح در زیر درس قرار داده شود
و دارای مقدار permission
است که مشخص میکند ایا این کاربر کوعیز را از قبل شرکت کرده است
--- ---
``` ```
@ -81,12 +86,27 @@ def doc_courses_lesson():
"content_file": null, "content_file": null,
"video_link": "https://example.com/videos/variables_intro.mp4", "video_link": "https://example.com/videos/variables_intro.mp4",
"is_complated": true, "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
} }
] ]
``` ```
""" """

18
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),
),
]

1
apps/course/models/participant.py

@ -18,6 +18,7 @@ class Participant(models.Model):
related_name='participants' related_name='participants'
) )
joined_date = models.DateTimeField(auto_now_add=True) joined_date = models.DateTimeField(auto_now_add=True)
unread_messages_count = models.IntegerField(default=0)
class Meta: class Meta:
unique_together = ('student', 'course') unique_together = ('student', 'course')

15
apps/course/serializers/lesson.py

@ -1,8 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from apps.course.models import Lesson, Participant, LessonCompletion 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): class LessonSerializer(serializers.ModelSerializer):
is_complated = serializers.SerializerMethodField() is_complated = serializers.SerializerMethodField()
quiz = serializers.SerializerMethodField()
quizs = serializers.SerializerMethodField()
class Meta: class Meta:
model = Lesson 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): def get_is_complated(self, obj):
request = self.context.get('request') request = self.context.get('request')
@ -35,5 +33,8 @@ class LessonSerializer(serializers.ModelSerializer):
).exists() ).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

14
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.utils import swagger_auto_schema
from drf_yasg import openapi from drf_yasg import openapi
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from rest_framework import status
from apps.course.serializers import ( from apps.course.serializers import (
LessonSerializer LessonSerializer
) )
from apps.course.models import Course, Lesson from apps.course.models import Course, Lesson
from apps.course.doc import * from apps.course.doc import *
from utils.exceptions import AppAPIException
@ -18,11 +18,17 @@ class LessonListView(ListAPIView):
serializer_class = LessonSerializer serializer_class = LessonSerializer
queryset = Lesson.objects.filter(is_active=True) queryset = Lesson.objects.filter(is_active=True)
# doc_courses_lesson
@swagger_auto_schema( @swagger_auto_schema(
operation_description=doc_courses_lesson(), operation_description=doc_courses_lesson(),
) )
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
course_slug = self.kwargs.get('slug') 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') return self.queryset.filter(course=course).order_by('priority','id')

3
apps/quiz/admin.py

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

5
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 *

61
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]

63
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',)
}
)
)

54
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("<span class='badge badge-primary'>Active</span>")
return mark_safe("<span class='badge badge-warning'>Inactive</span>")
@admin.display(description='Questions', ordering='questions_count')
def _questions(self, obj):
return mark_safe(f"<a href='/admin/quiz/question/?quiz={obj.id}'>Questions: {obj.questions_count}</a>")
def get_queryset(self, request):
return super().get_queryset(request).annotate(
questions_count=Count('questions')
)

132
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('<a href="/en/admin/account/clientuser/{}/change/">{}</a>', 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'

2
apps/quiz/apps.py

@ -3,4 +3,4 @@ from django.apps import AppConfig
class QuizConfig(AppConfig): class QuizConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'quiz'
name = 'apps.quiz'

131
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>/`
## پارامترها
- `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
}
]
}
"""

102
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',),
},
),
]

18
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'),
),
]

2
apps/quiz/models/__init__.py

@ -0,0 +1,2 @@
from .quiz import *
from .participant import *

6
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') 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') user = models.ForeignKey('account.User', on_delete=models.CASCADE, verbose_name='user', related_name='uquizzes')
started_at = models.DateTimeField(verbose_name='started at') started_at = models.DateTimeField(verbose_name='started at')
@ -30,7 +30,7 @@ class Participant(models.Model):
@staticmethod @staticmethod
def get_user_ranks(quiz_id): 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( rank=Window(
expression=Rank(), expression=Rank(),
order_by=F('total_score').desc() order_by=F('total_score').desc()
@ -47,7 +47,7 @@ class ParticipantAnswer(models.Model):
(4, 'Option 4'), (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) question = models.ForeignKey("quiz.Question", on_delete=models.CASCADE)
option_num = models.PositiveSmallIntegerField(choices=CHOICES, verbose_name='selected option') option_num = models.PositiveSmallIntegerField(choices=CHOICES, verbose_name='selected option')
at_time = models.DateTimeField() at_time = models.DateTimeField()

14
apps/quiz/models/quiz.py

@ -1,9 +1,13 @@
from django.db import models from django.db import models
from django.utils.translation import gettext_lazy as _
from apps.account.models import User
class Quiz(models.Model): 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() each_question_timing = models.PositiveIntegerField()
status = models.BooleanField(default=True) status = models.BooleanField(default=True)
@ -51,3 +55,11 @@ class Question(models.Model):
def __repr__(self): def __repr__(self):
return f"Question(id={self.id})" return f"Question(id={self.id})"
class QuizRankUser(User):
class Meta:
proxy = True
verbose_name = 'Rank Quiz'
verbose_name_plural = 'Rank Quizzes'

2
apps/quiz/serializers/__init__.py

@ -0,0 +1,2 @@
from .quiz import *
from .participant import *

45
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

59
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

13
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('<int:lesson_id>/', views.QuizDetailAPIView.as_view()),
]

2
apps/quiz/views/__init__.py

@ -0,0 +1,2 @@
from .quiz import *
from .participant import *

22
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)

33
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()

1
config/settings/base.py

@ -44,6 +44,7 @@ LOCAL_APPS = [
'apps.api.apps.ApiConfig', 'apps.api.apps.ApiConfig',
'apps.course.apps.CourseConfig', 'apps.course.apps.CourseConfig',
'apps.chat.apps.ChatConfig', 'apps.chat.apps.ChatConfig',
'apps.quiz.apps.QuizConfig',
] ]
THIRD_PARTY_APPS = [ THIRD_PARTY_APPS = [

1
config/urls.py

@ -36,6 +36,7 @@ api_patterns = [
path('account/', include('apps.account.urls')), path('account/', include('apps.account.urls')),
path('courses/', include('apps.course.urls')), path('courses/', include('apps.course.urls')),
path('quiz/', include('apps.quiz.urls')),
path('upload-tmp-media/', UploadTmpMedia.as_view()), path('upload-tmp-media/', UploadTmpMedia.as_view()),
] ]

Loading…
Cancel
Save