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