31 changed files with 849 additions and 28 deletions
-
2.env.prod
-
18apps/chat/migrations/0004_roommessage_unread_messages_count.py
-
3apps/chat/models.py
-
3apps/course/admin/course.py
-
30apps/course/doc.py
-
18apps/course/migrations/0005_participant_unread_messages_count.py
-
3apps/course/models/participant.py
-
15apps/course/serializers/lesson.py
-
14apps/course/views/lesson.py
-
3apps/quiz/admin.py
-
5apps/quiz/admin/__init__.py
-
61apps/quiz/admin/participant.py
-
63apps/quiz/admin/question.py
-
54apps/quiz/admin/quiz.py
-
132apps/quiz/admin/user_rank_quiz.py
-
2apps/quiz/apps.py
-
131apps/quiz/doc.py
-
102apps/quiz/migrations/0001_initial.py
-
18apps/quiz/migrations/0002_quiz_description.py
-
2apps/quiz/models/__init__.py
-
6apps/quiz/models/participant.py
-
14apps/quiz/models/quiz.py
-
2apps/quiz/serializers/__init__.py
-
45apps/quiz/serializers/participant.py
-
59apps/quiz/serializers/quiz.py
-
13apps/quiz/urls.py
-
2apps/quiz/views/__init__.py
-
22apps/quiz/views/participant.py
-
33apps/quiz/views/quiz.py
-
1config/settings/base.py
-
1config/urls.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), |
|||
), |
|||
] |
|||
@ -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,3 +0,0 @@ |
|||
from django.contrib import admin |
|||
|
|||
# Register your models here. |
|||
@ -0,0 +1,5 @@ |
|||
from .quiz import * |
|||
from .question import * |
|||
from .participant import * |
|||
# from .prize import * |
|||
# from .user_rank_quiz import * |
|||
@ -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] |
|||
|
|||
|
|||
|
|||
|
|||
|
|||
|
|||
@ -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',) |
|||
} |
|||
) |
|||
) |
|||
@ -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') |
|||
) |
|||
@ -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' |
|||
@ -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 |
|||
} |
|||
] |
|||
} |
|||
""" |
|||
@ -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',), |
|||
}, |
|||
), |
|||
] |
|||
@ -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'), |
|||
), |
|||
] |
|||
@ -0,0 +1,2 @@ |
|||
from .quiz import * |
|||
from .participant import * |
|||
@ -0,0 +1,2 @@ |
|||
from .quiz import * |
|||
from .participant import * |
|||
@ -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 |
|||
@ -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 |
|||
|
|||
|
|||
@ -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()), |
|||
|
|||
] |
|||
@ -0,0 +1,2 @@ |
|||
from .quiz import * |
|||
from .participant import * |
|||
@ -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) |
|||
@ -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() |
|||
|
|||
|
|||
|
|||
|
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue