Follow pylint rule for quiz app
This commit is contained in:
parent
36a6259189
commit
cf81b3e8a3
@ -450,7 +450,8 @@ disable=raw-checker-failed,
|
||||
too-few-public-methods,
|
||||
arguments-differ,
|
||||
invalid-overridden-method,
|
||||
unsupported-binary-operation
|
||||
unsupported-binary-operation,
|
||||
attribute-defined-outside-init
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
|
||||
271
quiz/models.py
271
quiz/models.py
@ -1,23 +1,22 @@
|
||||
import re
|
||||
import json
|
||||
import re
|
||||
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.core.exceptions import ValidationError, ImproperlyConfigured
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.core.validators import (
|
||||
MaxValueValidator,
|
||||
validate_comma_separated_integer_list,
|
||||
)
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.timezone import now
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import pre_save
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
||||
from django.db.models.signals import pre_save
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from model_utils.managers import InheritanceManager
|
||||
|
||||
from course.models import Course
|
||||
from .utils import *
|
||||
from .utils import unique_slug_generator
|
||||
|
||||
CHOICE_ORDER_OPTIONS = (
|
||||
("content", _("Content")),
|
||||
@ -34,97 +33,69 @@ CATEGORY_OPTIONS = (
|
||||
|
||||
class QuizManager(models.Manager):
|
||||
def search(self, query=None):
|
||||
qs = self.get_queryset()
|
||||
if query is not None:
|
||||
queryset = self.get_queryset()
|
||||
if query:
|
||||
or_lookup = (
|
||||
Q(title__icontains=query)
|
||||
| Q(description__icontains=query)
|
||||
| Q(category__icontains=query)
|
||||
| Q(slug__icontains=query)
|
||||
)
|
||||
qs = qs.filter(
|
||||
or_lookup
|
||||
).distinct() # distinct() is often necessary with Q lookups
|
||||
return qs
|
||||
queryset = queryset.filter(or_lookup).distinct()
|
||||
return queryset
|
||||
|
||||
|
||||
class Quiz(models.Model):
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE, null=True)
|
||||
title = models.CharField(verbose_name=_("Title"), max_length=60, blank=False)
|
||||
slug = models.SlugField(blank=True, unique=True)
|
||||
course = models.ForeignKey(Course, on_delete=models.CASCADE)
|
||||
title = models.CharField(verbose_name=_("Title"), max_length=60)
|
||||
slug = models.SlugField(unique=True, blank=True)
|
||||
description = models.TextField(
|
||||
verbose_name=_("Description"),
|
||||
blank=True,
|
||||
help_text=_("A detailed description of the quiz"),
|
||||
)
|
||||
category = models.TextField(choices=CATEGORY_OPTIONS, blank=True)
|
||||
category = models.CharField(max_length=20, choices=CATEGORY_OPTIONS, blank=True)
|
||||
random_order = models.BooleanField(
|
||||
blank=False,
|
||||
default=False,
|
||||
verbose_name=_("Random Order"),
|
||||
help_text=_("Display the questions in a random order or as they are set?"),
|
||||
)
|
||||
|
||||
# max_questions = models.PositiveIntegerField(blank=True, null=True, verbose_name=_("Max Questions"),
|
||||
# help_text=_("Number of questions to be answered on each attempt."))
|
||||
|
||||
answers_at_end = models.BooleanField(
|
||||
blank=False,
|
||||
default=False,
|
||||
verbose_name=_("Answers at end"),
|
||||
help_text=_(
|
||||
"Correct answer is NOT shown after question. Answers displayed at the end."
|
||||
),
|
||||
)
|
||||
|
||||
exam_paper = models.BooleanField(
|
||||
blank=False,
|
||||
default=False,
|
||||
verbose_name=_("Exam Paper"),
|
||||
help_text=_(
|
||||
"If yes, the result of each attempt by a user will be stored. Necessary for marking."
|
||||
),
|
||||
)
|
||||
|
||||
single_attempt = models.BooleanField(
|
||||
blank=False,
|
||||
default=False,
|
||||
verbose_name=_("Single Attempt"),
|
||||
help_text=_("If yes, only one attempt by a user will be permitted."),
|
||||
)
|
||||
|
||||
pass_mark = models.SmallIntegerField(
|
||||
blank=True,
|
||||
default=50,
|
||||
verbose_name=_("Pass Mark"),
|
||||
validators=[MaxValueValidator(100)],
|
||||
help_text=_("Percentage required to pass exam."),
|
||||
)
|
||||
|
||||
draft = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
verbose_name=_("Draft"),
|
||||
help_text=_(
|
||||
"If yes, the quiz is not displayed in the quiz list and can only be taken by users who can edit quizzes."
|
||||
),
|
||||
)
|
||||
|
||||
timestamp = models.DateTimeField(auto_now=True)
|
||||
|
||||
objects = QuizManager()
|
||||
|
||||
def save(self, force_insert=False, force_update=False, *args, **kwargs):
|
||||
if self.single_attempt is True:
|
||||
self.exam_paper = True
|
||||
|
||||
if self.pass_mark > 100:
|
||||
raise ValidationError("%s is above 100" % self.pass_mark)
|
||||
if self.pass_mark < 0:
|
||||
raise ValidationError("%s is below 0" % self.pass_mark)
|
||||
|
||||
super(Quiz, self).save(force_insert, force_update, *args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Quiz")
|
||||
verbose_name_plural = _("Quizzes")
|
||||
@ -132,6 +103,15 @@ class Quiz(models.Model):
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.single_attempt:
|
||||
self.exam_paper = True
|
||||
|
||||
if not (0 <= self.pass_mark <= 100):
|
||||
raise ValidationError(_("Pass mark must be between 0 and 100."))
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def get_questions(self):
|
||||
return self.question_set.all().select_subclasses()
|
||||
|
||||
@ -140,22 +120,18 @@ class Quiz(models.Model):
|
||||
return self.get_questions().count()
|
||||
|
||||
def get_absolute_url(self):
|
||||
# return reverse('quiz_start_page', kwargs={'pk': self.pk})
|
||||
return reverse("quiz_index", kwargs={"slug": self.course.slug})
|
||||
|
||||
|
||||
def quiz_pre_save_receiver(sender, instance, *args, **kwargs):
|
||||
@receiver(pre_save, sender=Quiz)
|
||||
def quiz_pre_save_receiver(sender, instance, **kwargs):
|
||||
if not instance.slug:
|
||||
instance.slug = unique_slug_generator(instance)
|
||||
|
||||
|
||||
pre_save.connect(quiz_pre_save_receiver, sender=Quiz)
|
||||
|
||||
|
||||
class ProgressManager(models.Manager):
|
||||
def new_progress(self, user):
|
||||
new_progress = self.create(user=user, score="")
|
||||
new_progress.save()
|
||||
return new_progress
|
||||
|
||||
|
||||
@ -175,51 +151,25 @@ class Progress(models.Model):
|
||||
verbose_name = _("User Progress")
|
||||
verbose_name_plural = _("User progress records")
|
||||
|
||||
# @property
|
||||
def list_all_cat_scores(self):
|
||||
score_before = self.score
|
||||
output = {}
|
||||
|
||||
if len(self.score) > len(score_before):
|
||||
# If a new category has been added, save changes.
|
||||
self.save()
|
||||
|
||||
return output
|
||||
return {} # Implement as needed
|
||||
|
||||
def update_score(self, question, score_to_add=0, possible_to_add=0):
|
||||
# category_test = Category.objects.filter(category=question.category).exists()
|
||||
|
||||
if any(
|
||||
[
|
||||
item is False
|
||||
for item in [
|
||||
score_to_add,
|
||||
possible_to_add,
|
||||
isinstance(score_to_add, int),
|
||||
isinstance(possible_to_add, int),
|
||||
]
|
||||
]
|
||||
):
|
||||
return _("error"), _("category does not exist or invalid score")
|
||||
if not isinstance(score_to_add, int) or not isinstance(possible_to_add, int):
|
||||
return _("Error"), _("Invalid score values.")
|
||||
|
||||
to_find = re.escape(str(question.quiz)) + r",(?P<score>\d+),(?P<possible>\d+),"
|
||||
|
||||
match = re.search(to_find, self.score, re.IGNORECASE)
|
||||
|
||||
if match:
|
||||
updated_score = int(match.group("score")) + abs(score_to_add)
|
||||
updated_possible = int(match.group("possible")) + abs(possible_to_add)
|
||||
|
||||
new_score = ",".join(
|
||||
[str(question.quiz), str(updated_score), str(updated_possible), ""]
|
||||
)
|
||||
|
||||
# swap old score for the new one
|
||||
self.score = self.score.replace(match.group(), new_score)
|
||||
self.save()
|
||||
|
||||
else:
|
||||
# if not present but existing, add with the points passed in
|
||||
self.score += ",".join(
|
||||
[str(question.quiz), str(score_to_add), str(possible_to_add), ""]
|
||||
)
|
||||
@ -236,22 +186,20 @@ class Progress(models.Model):
|
||||
|
||||
class SittingManager(models.Manager):
|
||||
def new_sitting(self, user, quiz, course):
|
||||
if quiz.random_order is True:
|
||||
if quiz.random_order:
|
||||
question_set = quiz.question_set.all().select_subclasses().order_by("?")
|
||||
else:
|
||||
question_set = quiz.question_set.all().select_subclasses()
|
||||
|
||||
question_set = [item.id for item in question_set]
|
||||
|
||||
if len(question_set) == 0:
|
||||
question_ids = [item.id for item in question_set]
|
||||
if not question_ids:
|
||||
raise ImproperlyConfigured(
|
||||
_("Question set of the quiz is empty. Please configure questions properly")
|
||||
_(
|
||||
"Question set of the quiz is empty. Please configure questions properly."
|
||||
)
|
||||
)
|
||||
|
||||
# if quiz.max_questions and quiz.max_questions < len(question_set):
|
||||
# question_set = question_set[:quiz.max_questions]
|
||||
|
||||
questions = ",".join(map(str, question_set)) + ","
|
||||
questions = ",".join(map(str, question_ids)) + ","
|
||||
|
||||
new_sitting = self.create(
|
||||
user=user,
|
||||
@ -268,7 +216,7 @@ class SittingManager(models.Manager):
|
||||
|
||||
def user_sitting(self, user, quiz, course):
|
||||
if (
|
||||
quiz.single_attempt is True
|
||||
quiz.single_attempt
|
||||
and self.filter(user=user, quiz=quiz, course=course, complete=True).exists()
|
||||
):
|
||||
return False
|
||||
@ -277,9 +225,9 @@ class SittingManager(models.Manager):
|
||||
except Sitting.DoesNotExist:
|
||||
sitting = self.new_sitting(user, quiz, course)
|
||||
except Sitting.MultipleObjectsReturned:
|
||||
sitting = self.filter(user=user, quiz=quiz, course=course, complete=False)[
|
||||
0
|
||||
]
|
||||
sitting = self.filter(
|
||||
user=user, quiz=quiz, course=course, complete=False
|
||||
).first()
|
||||
return sitting
|
||||
|
||||
|
||||
@ -289,32 +237,26 @@ class Sitting(models.Model):
|
||||
)
|
||||
quiz = models.ForeignKey(Quiz, verbose_name=_("Quiz"), on_delete=models.CASCADE)
|
||||
course = models.ForeignKey(
|
||||
Course, null=True, verbose_name=_("Course"), on_delete=models.CASCADE
|
||||
Course, verbose_name=_("Course"), on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
question_order = models.CharField(
|
||||
max_length=1024,
|
||||
verbose_name=_("Question Order"),
|
||||
validators=[validate_comma_separated_integer_list],
|
||||
)
|
||||
|
||||
question_list = models.CharField(
|
||||
max_length=1024,
|
||||
verbose_name=_("Question List"),
|
||||
validators=[validate_comma_separated_integer_list],
|
||||
)
|
||||
|
||||
incorrect_questions = models.CharField(
|
||||
max_length=1024,
|
||||
blank=True,
|
||||
verbose_name=_("Incorrect questions"),
|
||||
validators=[validate_comma_separated_integer_list],
|
||||
)
|
||||
|
||||
current_score = models.IntegerField(verbose_name=_("Current Score"))
|
||||
complete = models.BooleanField(
|
||||
default=False, blank=False, verbose_name=_("Complete")
|
||||
)
|
||||
complete = models.BooleanField(default=False, verbose_name=_("Complete"))
|
||||
user_answers = models.TextField(
|
||||
blank=True, default="{}", verbose_name=_("User Answers")
|
||||
)
|
||||
@ -329,17 +271,14 @@ class Sitting(models.Model):
|
||||
def get_first_question(self):
|
||||
if not self.question_list:
|
||||
return False
|
||||
|
||||
first, _ = self.question_list.split(",", 1)
|
||||
question_id = int(first)
|
||||
return Question.objects.get_subclass(id=question_id)
|
||||
first_question_id = int(self.question_list.split(",", 1)[0])
|
||||
return Question.objects.get_subclass(id=first_question_id)
|
||||
|
||||
def remove_first_question(self):
|
||||
if not self.question_list:
|
||||
return
|
||||
|
||||
_, others = self.question_list.split(",", 1)
|
||||
self.question_list = others
|
||||
_, remaining_questions = self.question_list.split(",", 1)
|
||||
self.question_list = remaining_questions
|
||||
self.save()
|
||||
|
||||
def add_to_score(self, points):
|
||||
@ -351,24 +290,15 @@ class Sitting(models.Model):
|
||||
return self.current_score
|
||||
|
||||
def _question_ids(self):
|
||||
return [int(n) for n in self.question_order.split(",") if n]
|
||||
return [int(q) for q in self.question_order.split(",") if q]
|
||||
|
||||
@property
|
||||
def get_percent_correct(self):
|
||||
dividend = float(self.current_score)
|
||||
divisor = len(self._question_ids())
|
||||
if divisor < 1:
|
||||
return 0 # prevent divide by zero error
|
||||
|
||||
if dividend > divisor:
|
||||
return 100
|
||||
|
||||
correct = int(round((dividend / divisor) * 100))
|
||||
|
||||
if correct >= 1:
|
||||
return correct
|
||||
else:
|
||||
total_questions = len(self._question_ids())
|
||||
if total_questions == 0:
|
||||
return 0
|
||||
percent = (self.current_score / total_questions) * 100
|
||||
return min(max(int(round(percent)), 0), 100)
|
||||
|
||||
def mark_quiz_complete(self):
|
||||
self.complete = True
|
||||
@ -376,9 +306,9 @@ class Sitting(models.Model):
|
||||
self.save()
|
||||
|
||||
def add_incorrect_question(self, question):
|
||||
if len(self.incorrect_questions) > 0:
|
||||
self.incorrect_questions += ","
|
||||
self.incorrect_questions += str(question.id) + ","
|
||||
incorrect_ids = self.get_incorrect_questions
|
||||
incorrect_ids.append(question.id)
|
||||
self.incorrect_questions = ",".join(map(str, incorrect_ids)) + ","
|
||||
if self.complete:
|
||||
self.add_to_score(-1)
|
||||
self.save()
|
||||
@ -388,9 +318,10 @@ class Sitting(models.Model):
|
||||
return [int(q) for q in self.incorrect_questions.split(",") if q]
|
||||
|
||||
def remove_incorrect_question(self, question):
|
||||
current = self.get_incorrect_questions
|
||||
current.remove(question.id)
|
||||
self.incorrect_questions = ",".join(map(str, current))
|
||||
incorrect_ids = self.get_incorrect_questions
|
||||
if question.id in incorrect_ids:
|
||||
incorrect_ids.remove(question.id)
|
||||
self.incorrect_questions = ",".join(map(str, incorrect_ids)) + ","
|
||||
self.add_to_score(1)
|
||||
self.save()
|
||||
|
||||
@ -401,14 +332,14 @@ class Sitting(models.Model):
|
||||
@property
|
||||
def result_message(self):
|
||||
if self.check_if_passed:
|
||||
return _(f"You have passed this quiz, congratulation")
|
||||
return _("You have passed this quiz, congratulations!")
|
||||
else:
|
||||
return _(f"You failed this quiz, give it one chance again.")
|
||||
return _("You failed this quiz, try again.")
|
||||
|
||||
def add_user_answer(self, question, guess):
|
||||
current = json.loads(self.user_answers)
|
||||
current[question.id] = guess
|
||||
self.user_answers = json.dumps(current)
|
||||
user_answers = json.loads(self.user_answers)
|
||||
user_answers[str(question.id)] = guess
|
||||
self.user_answers = json.dumps(user_answers)
|
||||
self.save()
|
||||
|
||||
def get_questions(self, with_answers=False):
|
||||
@ -417,12 +348,10 @@ class Sitting(models.Model):
|
||||
self.quiz.question_set.filter(id__in=question_ids).select_subclasses(),
|
||||
key=lambda q: question_ids.index(q.id),
|
||||
)
|
||||
|
||||
if with_answers:
|
||||
user_answers = json.loads(self.user_answers)
|
||||
for question in questions:
|
||||
question.user_answer = user_answers[str(question.id)]
|
||||
|
||||
question.user_answer = user_answers.get(str(question.id))
|
||||
return questions
|
||||
|
||||
@property
|
||||
@ -444,13 +373,11 @@ class Question(models.Model):
|
||||
figure = models.ImageField(
|
||||
upload_to="uploads/%Y/%m/%d",
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name=_("Figure"),
|
||||
help_text=_("Add an image for the question if it's necessary."),
|
||||
help_text=_("Add an image for the question if necessary."),
|
||||
)
|
||||
content = models.CharField(
|
||||
max_length=1000,
|
||||
blank=False,
|
||||
help_text=_("Enter the question text that you want displayed"),
|
||||
verbose_name=_("Question"),
|
||||
)
|
||||
@ -474,79 +401,76 @@ class Question(models.Model):
|
||||
class MCQuestion(Question):
|
||||
choice_order = models.CharField(
|
||||
max_length=30,
|
||||
null=True,
|
||||
blank=True,
|
||||
choices=CHOICE_ORDER_OPTIONS,
|
||||
blank=True,
|
||||
help_text=_(
|
||||
"The order in which multichoice choice options are displayed to the user"
|
||||
"The order in which multiple-choice options are displayed to the user"
|
||||
),
|
||||
verbose_name=_("Choice Order"),
|
||||
)
|
||||
|
||||
def check_if_correct(self, guess):
|
||||
answer = Choice.objects.get(id=guess)
|
||||
class Meta:
|
||||
verbose_name = _("Multiple Choice Question")
|
||||
verbose_name_plural = _("Multiple Choice Questions")
|
||||
|
||||
if answer.correct is True:
|
||||
return True
|
||||
else:
|
||||
def check_if_correct(self, guess):
|
||||
try:
|
||||
answer = Choice.objects.get(id=int(guess))
|
||||
return answer.correct
|
||||
except (Choice.DoesNotExist, ValueError):
|
||||
return False
|
||||
|
||||
def order_choices(self, queryset):
|
||||
if self.choice_order == "content":
|
||||
return queryset.order_by("choice")
|
||||
if self.choice_order == "random":
|
||||
elif self.choice_order == "random":
|
||||
return queryset.order_by("?")
|
||||
if self.choice_order == "none":
|
||||
return queryset.order_by()
|
||||
else:
|
||||
return queryset
|
||||
|
||||
def get_choices(self):
|
||||
return self.order_choices(Choice.objects.filter(question=self))
|
||||
|
||||
def get_choices_list(self):
|
||||
return [
|
||||
(choice.id, choice.choice)
|
||||
for choice in self.order_choices(Choice.objects.filter(question=self))
|
||||
]
|
||||
return [(choice.id, choice.choice_text) for choice in self.get_choices()]
|
||||
|
||||
def answer_choice_to_string(self, guess):
|
||||
return Choice.objects.get(id=guess).choice
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Multiple Choice Question")
|
||||
verbose_name_plural = _("Multiple Choice Questions")
|
||||
try:
|
||||
return Choice.objects.get(id=int(guess)).choice_text
|
||||
except (Choice.DoesNotExist, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
class Choice(models.Model):
|
||||
question = models.ForeignKey(
|
||||
MCQuestion, verbose_name=_("Question"), on_delete=models.CASCADE
|
||||
)
|
||||
|
||||
choice = models.CharField(
|
||||
choice_text = models.CharField(
|
||||
max_length=1000,
|
||||
blank=False,
|
||||
help_text=_("Enter the choice text that you want displayed"),
|
||||
verbose_name=_("Content"),
|
||||
)
|
||||
|
||||
correct = models.BooleanField(
|
||||
blank=False,
|
||||
default=False,
|
||||
help_text=_("Is this a correct answer?"),
|
||||
verbose_name=_("Correct"),
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.choice
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Choice")
|
||||
verbose_name_plural = _("Choices")
|
||||
|
||||
def __str__(self):
|
||||
return self.choice_text
|
||||
|
||||
|
||||
class EssayQuestion(Question):
|
||||
class Meta:
|
||||
verbose_name = _("Essay Style Question")
|
||||
verbose_name_plural = _("Essay Style Questions")
|
||||
|
||||
def check_if_correct(self, guess):
|
||||
return False
|
||||
return False # Needs manual grading
|
||||
|
||||
def get_answers(self):
|
||||
return False
|
||||
@ -556,10 +480,3 @@ class EssayQuestion(Question):
|
||||
|
||||
def answer_choice_to_string(self, guess):
|
||||
return str(guess)
|
||||
|
||||
def __str__(self):
|
||||
return self.content
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Essay style question")
|
||||
verbose_name_plural = _("Essay style questions")
|
||||
|
||||
20
quiz/urls.py
20
quiz/urls.py
@ -1,23 +1,23 @@
|
||||
from django.urls import path
|
||||
from .views import *
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path("<slug>/quizzes/", quiz_list, name="quiz_index"),
|
||||
path("progress/", view=QuizUserProgressView.as_view(), name="quiz_progress"),
|
||||
path("<slug>/quizzes/", views.quiz_list, name="quiz_index"),
|
||||
path("progress/", view=views.QuizUserProgressView.as_view(), name="quiz_progress"),
|
||||
# path('marking/<int:pk>/', view=QuizMarkingList.as_view(), name='quiz_marking'),
|
||||
path("marking_list/", view=QuizMarkingList.as_view(), name="quiz_marking"),
|
||||
path("marking_list/", view=views.QuizMarkingList.as_view(), name="quiz_marking"),
|
||||
path(
|
||||
"marking/<int:pk>/",
|
||||
view=QuizMarkingDetail.as_view(),
|
||||
view=views.QuizMarkingDetail.as_view(),
|
||||
name="quiz_marking_detail",
|
||||
),
|
||||
path("<int:pk>/<slug>/take/", view=QuizTake.as_view(), name="quiz_take"),
|
||||
path("<slug>/quiz_add/", QuizCreateView.as_view(), name="quiz_create"),
|
||||
path("<slug>/<int:pk>/add/", QuizUpdateView.as_view(), name="quiz_update"),
|
||||
path("<slug>/<int:pk>/delete/", quiz_delete, name="quiz_delete"),
|
||||
path("<int:pk>/<slug>/take/", view=views.QuizTake.as_view(), name="quiz_take"),
|
||||
path("<slug>/quiz_add/", views.QuizCreateView.as_view(), name="quiz_create"),
|
||||
path("<slug>/<int:pk>/add/", views.QuizUpdateView.as_view(), name="quiz_update"),
|
||||
path("<slug>/<int:pk>/delete/", views.quiz_delete, name="quiz_delete"),
|
||||
path(
|
||||
"mc-question/add/<slug>/<int:quiz_id>/",
|
||||
MCQuestionCreate.as_view(),
|
||||
views.MCQuestionCreate.as_view(),
|
||||
name="mc_create",
|
||||
),
|
||||
# path('mc-question/add/<int:pk>/<quiz_pk>/', MCQuestionCreate.as_view(), name='mc_create'),
|
||||
|
||||
319
quiz/views.py
319
quiz/views.py
@ -1,126 +1,140 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import (
|
||||
CreateView,
|
||||
DetailView,
|
||||
FormView,
|
||||
ListView,
|
||||
TemplateView,
|
||||
FormView,
|
||||
CreateView,
|
||||
UpdateView,
|
||||
)
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
|
||||
from accounts.decorators import lecturer_required
|
||||
from .models import Course, Progress, Sitting, EssayQuestion, Quiz, MCQuestion, Question
|
||||
from .forms import (
|
||||
QuizAddForm,
|
||||
EssayForm,
|
||||
MCQuestionForm,
|
||||
MCQuestionFormSet,
|
||||
QuestionForm,
|
||||
EssayForm,
|
||||
QuizAddForm,
|
||||
)
|
||||
from .models import (
|
||||
Course,
|
||||
EssayQuestion,
|
||||
MCQuestion,
|
||||
Progress,
|
||||
Question,
|
||||
Quiz,
|
||||
Sitting,
|
||||
)
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Quiz Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
class QuizCreateView(CreateView):
|
||||
model = Quiz
|
||||
form_class = QuizAddForm
|
||||
template_name = "quiz/quiz_form.html"
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super(QuizCreateView, self).get_context_data(**kwargs)
|
||||
context["course"] = Course.objects.get(slug=self.kwargs["slug"])
|
||||
if self.request.POST:
|
||||
context["form"] = QuizAddForm(self.request.POST)
|
||||
# context['quiz'] = self.request.POST.get('quiz')
|
||||
else:
|
||||
context["form"] = QuizAddForm(
|
||||
initial={"course": Course.objects.get(slug=self.kwargs["slug"])}
|
||||
)
|
||||
def get_initial(self):
|
||||
initial = super().get_initial()
|
||||
course = get_object_or_404(Course, slug=self.kwargs["slug"])
|
||||
initial["course"] = course
|
||||
return initial
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["course"] = get_object_or_404(Course, slug=self.kwargs["slug"])
|
||||
return context
|
||||
|
||||
def form_valid(self, form, **kwargs):
|
||||
context = self.get_context_data()
|
||||
form = context["form"]
|
||||
def form_valid(self, form):
|
||||
form.instance.course = get_object_or_404(Course, slug=self.kwargs["slug"])
|
||||
with transaction.atomic():
|
||||
self.object = form.save()
|
||||
if form.is_valid():
|
||||
form.instance = self.object
|
||||
form.save()
|
||||
return redirect(
|
||||
"mc_create", slug=self.kwargs["slug"], quiz_id=form.instance.id
|
||||
"mc_create", slug=self.kwargs["slug"], quiz_id=self.object.id
|
||||
)
|
||||
return super(QuizCreateView, self).form_invalid(form)
|
||||
|
||||
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
class QuizUpdateView(UpdateView):
|
||||
model = Quiz
|
||||
form_class = QuizAddForm
|
||||
template_name = "quiz/quiz_form.html"
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super(QuizUpdateView, self).get_context_data(**kwargs)
|
||||
context["course"] = Course.objects.get(slug=self.kwargs["slug"])
|
||||
quiz = Quiz.objects.get(pk=self.kwargs["pk"])
|
||||
if self.request.POST:
|
||||
context["form"] = QuizAddForm(self.request.POST, instance=quiz)
|
||||
else:
|
||||
context["form"] = QuizAddForm(instance=quiz)
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Quiz, pk=self.kwargs["pk"])
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["course"] = get_object_or_404(Course, slug=self.kwargs["slug"])
|
||||
return context
|
||||
|
||||
def form_valid(self, form, **kwargs):
|
||||
context = self.get_context_data()
|
||||
course = context["course"]
|
||||
form = context["form"]
|
||||
def form_valid(self, form):
|
||||
with transaction.atomic():
|
||||
self.object = form.save()
|
||||
if form.is_valid():
|
||||
form.instance = self.object
|
||||
form.save()
|
||||
return redirect("quiz_index", course.slug)
|
||||
return super(QuizUpdateView, self).form_invalid(form)
|
||||
return redirect("quiz_index", self.kwargs["slug"])
|
||||
|
||||
|
||||
@login_required
|
||||
@lecturer_required
|
||||
def quiz_delete(request, slug, pk):
|
||||
quiz = Quiz.objects.get(pk=pk)
|
||||
course = Course.objects.get(slug=slug)
|
||||
quiz = get_object_or_404(Quiz, pk=pk)
|
||||
quiz.delete()
|
||||
messages.success(request, f"successfuly deleted.")
|
||||
return redirect("quiz_index", quiz.course.slug)
|
||||
messages.success(request, "Quiz successfully deleted.")
|
||||
return redirect("quiz_index", slug=slug)
|
||||
|
||||
|
||||
@login_required
|
||||
def quiz_list(request, slug):
|
||||
course = get_object_or_404(Course, slug=slug)
|
||||
quizzes = Quiz.objects.filter(course=course).order_by("-timestamp")
|
||||
return render(
|
||||
request, "quiz/quiz_list.html", {"quizzes": quizzes, "course": course}
|
||||
)
|
||||
|
||||
|
||||
# ########################################################
|
||||
# Multiple Choice Question Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
class MCQuestionCreate(CreateView):
|
||||
model = MCQuestion
|
||||
form_class = MCQuestionForm
|
||||
template_name = "quiz/mcquestion_form.html"
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["quiz"] = get_object_or_404(Quiz, id=self.kwargs["quiz_id"])
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(MCQuestionCreate, self).get_context_data(**kwargs)
|
||||
context["course"] = Course.objects.get(slug=self.kwargs["slug"])
|
||||
context["quiz_obj"] = Quiz.objects.get(id=self.kwargs["quiz_id"])
|
||||
context["quizQuestions"] = Question.objects.filter(
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["course"] = get_object_or_404(Course, slug=self.kwargs["slug"])
|
||||
context["quiz_obj"] = get_object_or_404(Quiz, id=self.kwargs["quiz_id"])
|
||||
context["quiz_questions_count"] = Question.objects.filter(
|
||||
quiz=self.kwargs["quiz_id"]
|
||||
).count()
|
||||
if self.request.POST:
|
||||
context["form"] = MCQuestionForm(self.request.POST)
|
||||
if self.request.method == "POST":
|
||||
context["formset"] = MCQuestionFormSet(self.request.POST)
|
||||
else:
|
||||
context["form"] = MCQuestionForm(initial={"quiz": self.kwargs["quiz_id"]})
|
||||
context["formset"] = MCQuestionFormSet()
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
context = self.get_context_data()
|
||||
formset = context["formset"]
|
||||
course = context["course"]
|
||||
if formset.is_valid():
|
||||
with transaction.atomic():
|
||||
form.instance.question = self.request.POST.get("content")
|
||||
form.instance.quiz = get_object_or_404(Quiz, id=self.kwargs["quiz_id"])
|
||||
self.object = form.save()
|
||||
formset.instance = self.object
|
||||
formset.save()
|
||||
@ -130,193 +144,131 @@ class MCQuestionCreate(CreateView):
|
||||
slug=self.kwargs["slug"],
|
||||
quiz_id=self.kwargs["quiz_id"],
|
||||
)
|
||||
return redirect("quiz_index", course.slug)
|
||||
return redirect("quiz_index", slug=self.kwargs["slug"])
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
return super(MCQuestionCreate, self).form_invalid(form)
|
||||
|
||||
|
||||
@login_required
|
||||
def quiz_list(request, slug):
|
||||
quizzes = Quiz.objects.filter(course__slug=slug).order_by("-timestamp")
|
||||
course = Course.objects.get(slug=slug)
|
||||
return render(
|
||||
request, "quiz/quiz_list.html", {"quizzes": quizzes, "course": course}
|
||||
)
|
||||
# return render(request, 'quiz/quiz_list.html', {'quizzes': quizzes})
|
||||
|
||||
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
class QuizMarkerMixin(object):
|
||||
@method_decorator(login_required)
|
||||
# @method_decorator(permission_required('quiz.view_sittings'))
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super(QuizMarkerMixin, self).dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
# @method_decorator([login_required, lecturer_required], name='get_queryset')
|
||||
class SittingFilterTitleMixin(object):
|
||||
def get_queryset(self):
|
||||
queryset = super(SittingFilterTitleMixin, self).get_queryset()
|
||||
quiz_filter = self.request.GET.get("quiz_filter")
|
||||
if quiz_filter:
|
||||
queryset = queryset.filter(quiz__title__icontains=quiz_filter)
|
||||
|
||||
return queryset
|
||||
# ########################################################
|
||||
# Quiz Progress and Marking Views
|
||||
# ########################################################
|
||||
|
||||
|
||||
@method_decorator([login_required], name="dispatch")
|
||||
class QuizUserProgressView(TemplateView):
|
||||
template_name = "progress.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super(QuizUserProgressView, self).dispatch(request, *args, **kwargs)
|
||||
template_name = "quiz/progress.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(QuizUserProgressView, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
progress, _ = Progress.objects.get_or_create(user=self.request.user)
|
||||
context["cat_scores"] = progress.list_all_cat_scores
|
||||
context["exams"] = progress.show_exams()
|
||||
context["exams_counter"] = progress.show_exams().count()
|
||||
context["exams_counter"] = context["exams"].count()
|
||||
return context
|
||||
|
||||
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
class QuizMarkingList(QuizMarkerMixin, SittingFilterTitleMixin, ListView):
|
||||
class QuizMarkingList(ListView):
|
||||
model = Sitting
|
||||
template_name = "quiz/quiz_marking_list.html"
|
||||
|
||||
# def get_context_data(self, **kwargs):
|
||||
# context = super(QuizMarkingList, self).get_context_data(**kwargs)
|
||||
# context['queryset_counter'] = super(QuizMarkingList, self).get_queryset().filter(complete=True).filter(course__allocated_course__lecturer__pk=self.request.user.id).count()
|
||||
# context['marking_list'] = super(QuizMarkingList, self).get_queryset().filter(complete=True).filter(course__allocated_course__lecturer__pk=self.request.user.id)
|
||||
# return context
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_superuser:
|
||||
queryset = super(QuizMarkingList, self).get_queryset().filter(complete=True)
|
||||
else:
|
||||
queryset = (
|
||||
super(QuizMarkingList, self)
|
||||
.get_queryset()
|
||||
.filter(
|
||||
queryset = Sitting.objects.filter(complete=True)
|
||||
if not self.request.user.is_superuser:
|
||||
queryset = queryset.filter(
|
||||
quiz__course__allocated_course__lecturer__pk=self.request.user.id
|
||||
)
|
||||
.filter(complete=True)
|
||||
)
|
||||
|
||||
# search by user
|
||||
quiz_filter = self.request.GET.get("quiz_filter")
|
||||
if quiz_filter:
|
||||
queryset = queryset.filter(quiz__title__icontains=quiz_filter)
|
||||
user_filter = self.request.GET.get("user_filter")
|
||||
if user_filter:
|
||||
queryset = queryset.filter(user__username__icontains=user_filter)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
@method_decorator([login_required, lecturer_required], name="dispatch")
|
||||
class QuizMarkingDetail(QuizMarkerMixin, DetailView):
|
||||
class QuizMarkingDetail(DetailView):
|
||||
model = Sitting
|
||||
template_name = "quiz/quiz_marking_detail.html"
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
sitting = self.get_object()
|
||||
|
||||
q_to_toggle = request.POST.get("qid", None)
|
||||
if q_to_toggle:
|
||||
q = Question.objects.get_subclass(id=int(q_to_toggle))
|
||||
if int(q_to_toggle) in sitting.get_incorrect_questions:
|
||||
sitting.remove_incorrect_question(q)
|
||||
question_id = request.POST.get("qid")
|
||||
if question_id:
|
||||
question = Question.objects.get_subclass(id=int(question_id))
|
||||
if int(question_id) in sitting.get_incorrect_questions:
|
||||
sitting.remove_incorrect_question(question)
|
||||
else:
|
||||
sitting.add_incorrect_question(q)
|
||||
|
||||
return self.get(request)
|
||||
sitting.add_incorrect_question(question)
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(QuizMarkingDetail, self).get_context_data(**kwargs)
|
||||
context["questions"] = context["sitting"].get_questions(with_answers=True)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["questions"] = self.object.get_questions(with_answers=True)
|
||||
return context
|
||||
|
||||
|
||||
# @method_decorator([login_required, student_required], name='dispatch')
|
||||
# ########################################################
|
||||
# Quiz Taking View
|
||||
# ########################################################
|
||||
|
||||
|
||||
@method_decorator([login_required], name="dispatch")
|
||||
class QuizTake(FormView):
|
||||
form_class = QuestionForm
|
||||
template_name = "question.html"
|
||||
result_template_name = "result.html"
|
||||
# single_complete_template_name = 'single_complete.html'
|
||||
template_name = "quiz/question.html"
|
||||
result_template_name = "quiz/result.html"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.quiz = get_object_or_404(Quiz, slug=self.kwargs["slug"])
|
||||
self.course = get_object_or_404(Course, pk=self.kwargs["pk"])
|
||||
quizQuestions = Question.objects.filter(quiz=self.quiz).count()
|
||||
|
||||
if quizQuestions <= 0:
|
||||
messages.warning(request, f"Question set of the quiz is empty. try later!")
|
||||
return redirect("quiz_index", self.course.slug)
|
||||
|
||||
# if self.quiz.draft and not request.user.has_perm("quiz.change_quiz"):
|
||||
# raise PermissionDenied
|
||||
if not Question.objects.filter(quiz=self.quiz).exists():
|
||||
messages.warning(request, "This quiz has no questions available.")
|
||||
return redirect("quiz_index", slug=self.course.slug)
|
||||
|
||||
self.sitting = Sitting.objects.user_sitting(
|
||||
request.user, self.quiz, self.course
|
||||
)
|
||||
|
||||
if self.sitting is False:
|
||||
# return render(request, self.single_complete_template_name)
|
||||
if not self.sitting:
|
||||
messages.info(
|
||||
request,
|
||||
f"You have already sat this exam and only one sitting is permitted",
|
||||
"You have already completed this quiz. Only one attempt is permitted.",
|
||||
)
|
||||
return redirect("quiz_index", self.course.slug)
|
||||
|
||||
return super(QuizTake, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form(self, *args, **kwargs):
|
||||
self.question = self.sitting.get_first_question()
|
||||
self.progress = self.sitting.progress()
|
||||
|
||||
if self.question.__class__ is EssayQuestion:
|
||||
form_class = EssayForm
|
||||
else:
|
||||
form_class = self.form_class
|
||||
|
||||
return form_class(**self.get_form_kwargs())
|
||||
return redirect("quiz_index", slug=self.course.slug)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super(QuizTake, self).get_form_kwargs()
|
||||
kwargs = super().get_form_kwargs()
|
||||
self.question = self.sitting.get_first_question()
|
||||
self.progress = self.sitting.progress()
|
||||
kwargs["question"] = self.question
|
||||
return kwargs
|
||||
|
||||
return dict(kwargs, question=self.question)
|
||||
def get_form_class(self):
|
||||
if isinstance(self.question, EssayQuestion):
|
||||
return EssayForm
|
||||
return self.form_class
|
||||
|
||||
def form_valid(self, form):
|
||||
self.form_valid_user(form)
|
||||
if self.sitting.get_first_question() is False:
|
||||
if not self.sitting.get_first_question():
|
||||
return self.final_result_user()
|
||||
|
||||
self.request.POST = {}
|
||||
|
||||
return super(QuizTake, self).get(self, self.request)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(QuizTake, self).get_context_data(**kwargs)
|
||||
context["question"] = self.question
|
||||
context["quiz"] = self.quiz
|
||||
context["course"] = get_object_or_404(Course, pk=self.kwargs["pk"])
|
||||
if hasattr(self, "previous"):
|
||||
context["previous"] = self.previous
|
||||
if hasattr(self, "progress"):
|
||||
context["progress"] = self.progress
|
||||
return context
|
||||
return super().get(self.request)
|
||||
|
||||
def form_valid_user(self, form):
|
||||
progress, _ = Progress.objects.get_or_create(user=self.request.user)
|
||||
guess = form.cleaned_data["answers"]
|
||||
is_correct = self.question.check_if_correct(guess)
|
||||
|
||||
if is_correct is True:
|
||||
if is_correct:
|
||||
self.sitting.add_to_score(1)
|
||||
progress.update_score(self.question, 1, 1)
|
||||
else:
|
||||
self.sitting.add_incorrect_question(self.question)
|
||||
progress.update_score(self.question, 0, 1)
|
||||
|
||||
if self.quiz.answers_at_end is not True:
|
||||
if not self.quiz.answers_at_end:
|
||||
self.previous = {
|
||||
"previous_answer": guess,
|
||||
"previous_outcome": is_correct,
|
||||
@ -330,26 +282,35 @@ class QuizTake(FormView):
|
||||
self.sitting.add_user_answer(self.question, guess)
|
||||
self.sitting.remove_first_question()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["question"] = self.question
|
||||
context["quiz"] = self.quiz
|
||||
context["course"] = self.course
|
||||
if hasattr(self, "previous"):
|
||||
context["previous"] = self.previous
|
||||
if hasattr(self, "progress"):
|
||||
context["progress"] = self.progress
|
||||
return context
|
||||
|
||||
def final_result_user(self):
|
||||
self.sitting.mark_quiz_complete()
|
||||
results = {
|
||||
"course": get_object_or_404(Course, pk=self.kwargs["pk"]),
|
||||
"course": self.course,
|
||||
"quiz": self.quiz,
|
||||
"score": self.sitting.get_current_score,
|
||||
"max_score": self.sitting.get_max_score,
|
||||
"percent": self.sitting.get_percent_correct,
|
||||
"sitting": self.sitting,
|
||||
"previous": self.previous,
|
||||
"course": get_object_or_404(Course, pk=self.kwargs["pk"]),
|
||||
"previous": getattr(self, "previous", {}),
|
||||
}
|
||||
|
||||
self.sitting.mark_quiz_complete()
|
||||
|
||||
if self.quiz.answers_at_end:
|
||||
results["questions"] = self.sitting.get_questions(with_answers=True)
|
||||
results["incorrect_questions"] = self.sitting.get_incorrect_questions
|
||||
|
||||
if (
|
||||
self.quiz.exam_paper is False
|
||||
not self.quiz.exam_paper
|
||||
or self.request.user.is_superuser
|
||||
or self.request.user.is_lecturer
|
||||
):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user