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. 3
      apps/chat/models.py
  4. 3
      apps/course/admin/course.py
  5. 30
      apps/course/doc.py
  6. 18
      apps/course/migrations/0005_participant_unread_messages_count.py
  7. 3
      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

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

3
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'}"

3
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):

30
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
}
]
```
"""

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

3
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')

15
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

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

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):
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')
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()

14
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'

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.course.apps.CourseConfig',
'apps.chat.apps.ChatConfig',
'apps.quiz.apps.QuizConfig',
]
THIRD_PARTY_APPS = [

1
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()),
]

Loading…
Cancel
Save