From cf81b3e8a329e069f5529390f4ac0f5807931b28 Mon Sep 17 00:00:00 2001 From: Adil Mohak Date: Sat, 5 Oct 2024 01:17:28 +0300 Subject: [PATCH] Follow pylint rule for quiz app --- .pylintrc | 3 +- quiz/models.py | 277 +++++++++++++++-------------------------- quiz/urls.py | 20 +-- quiz/views.py | 325 ++++++++++++++++++++++--------------------------- 4 files changed, 252 insertions(+), 373 deletions(-) diff --git a/.pylintrc b/.pylintrc index 7f22e63..99d3a19 100644 --- a/.pylintrc +++ b/.pylintrc @@ -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 diff --git a/quiz/models.py b/quiz/models.py index 0423d17..2600b94 100644 --- a/quiz/models.py +++ b/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\d+),(?P\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,11 +318,12 @@ 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)) - self.add_to_score(1) - self.save() + 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() @property def check_if_passed(self): @@ -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() - return queryset + 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") diff --git a/quiz/urls.py b/quiz/urls.py index 6c73580..4e42e7e 100644 --- a/quiz/urls.py +++ b/quiz/urls.py @@ -1,23 +1,23 @@ from django.urls import path -from .views import * +from . import views urlpatterns = [ - path("/quizzes/", quiz_list, name="quiz_index"), - path("progress/", view=QuizUserProgressView.as_view(), name="quiz_progress"), + path("/quizzes/", views.quiz_list, name="quiz_index"), + path("progress/", view=views.QuizUserProgressView.as_view(), name="quiz_progress"), # path('marking//', 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//", - view=QuizMarkingDetail.as_view(), + view=views.QuizMarkingDetail.as_view(), name="quiz_marking_detail", ), - path("//take/", view=QuizTake.as_view(), name="quiz_take"), - path("/quiz_add/", QuizCreateView.as_view(), name="quiz_create"), - path("//add/", QuizUpdateView.as_view(), name="quiz_update"), - path("//delete/", quiz_delete, name="quiz_delete"), + path("//take/", view=views.QuizTake.as_view(), name="quiz_take"), + path("/quiz_add/", views.QuizCreateView.as_view(), name="quiz_create"), + path("//add/", views.QuizUpdateView.as_view(), name="quiz_update"), + path("//delete/", views.quiz_delete, name="quiz_delete"), path( "mc-question/add///", - MCQuestionCreate.as_view(), + views.MCQuestionCreate.as_view(), name="mc_create", ), # path('mc-question/add///', MCQuestionCreate.as_view(), name='mc_create'), diff --git a/quiz/views.py b/quiz/views.py index c4d75e3..bb4c380 100644 --- a/quiz/views.py +++ b/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 - ) - return super(QuizCreateView, self).form_invalid(form) + return redirect( + "mc_create", slug=self.kwargs["slug"], quiz_id=self.object.id + ) @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( - quiz__course__allocated_course__lecturer__pk=self.request.user.id - ) - .filter(complete=True) + 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 ) - - # 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 ):