from django.contrib import messages from django.contrib.auth.decorators import login_required 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, UpdateView, ) from accounts.decorators import lecturer_required from .forms import ( EssayForm, MCQuestionForm, MCQuestionFormSet, QuestionForm, 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_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): form.instance.course = get_object_or_404(Course, slug=self.kwargs["slug"]) with transaction.atomic(): self.object = form.save() 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_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): with transaction.atomic(): self.object = form.save() return redirect("quiz_index", self.kwargs["slug"]) @login_required @lecturer_required def quiz_delete(request, slug, pk): quiz = get_object_or_404(Quiz, pk=pk) quiz.delete() 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().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.method == "POST": context["formset"] = MCQuestionFormSet(self.request.POST) else: context["formset"] = MCQuestionFormSet() return context def form_valid(self, form): context = self.get_context_data() formset = context["formset"] if formset.is_valid(): with transaction.atomic(): # Save the MCQuestion instance without committing to the database yet self.object = form.save(commit=False) self.object.save() # Retrieve the Quiz instance quiz = get_object_or_404(Quiz, id=self.kwargs["quiz_id"]) # set the many-to-many relationship self.object.quiz.add(quiz) # Save the formset (choices for the question) formset.instance = self.object formset.save() if "another" in self.request.POST: return redirect( "mc_create", slug=self.kwargs["slug"], quiz_id=self.kwargs["quiz_id"], ) return redirect("quiz_index", slug=self.kwargs["slug"]) else: return self.form_invalid(form) # ######################################################## # Quiz Progress and Marking Views # ######################################################## @method_decorator([login_required], name="dispatch") class QuizUserProgressView(TemplateView): template_name = "quiz/progress.html" def get_context_data(self, **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"] = context["exams"].count() return context @method_decorator([login_required, lecturer_required], name="dispatch") class QuizMarkingList(ListView): model = Sitting template_name = "quiz/quiz_marking_list.html" def get_queryset(self): 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 ) 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(DetailView): model = Sitting template_name = "quiz/quiz_marking_detail.html" def post(self, request, *args, **kwargs): sitting = self.get_object() 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(question) return self.get(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["questions"] = self.object.get_questions(with_answers=True) return context # ######################################################## # Quiz Taking View # ######################################################## @method_decorator([login_required], name="dispatch") class QuizTake(FormView): form_class = QuestionForm 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"]) 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 not self.sitting: messages.info( request, "You have already completed this quiz. Only one attempt is permitted.", ) return redirect("quiz_index", slug=self.course.slug) return super().dispatch(request, *args, **kwargs) def get_form_kwargs(self): kwargs = super().get_form_kwargs() self.question = self.sitting.get_first_question() self.progress = self.sitting.progress() kwargs["question"] = self.question return kwargs 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 not self.sitting.get_first_question(): return self.final_result_user() 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: 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 not self.quiz.answers_at_end: self.previous = { "previous_answer": guess, "previous_outcome": is_correct, "previous_question": self.question, "answers": self.question.get_choices(), "question_type": {self.question.__class__.__name__: True}, } else: self.previous = {} 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": 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": getattr(self, "previous", {}), } if self.quiz.answers_at_end: results["questions"] = self.sitting.get_questions(with_answers=True) results["incorrect_questions"] = self.sitting.get_incorrect_questions if ( not self.quiz.exam_paper or self.request.user.is_superuser or self.request.user.is_lecturer ): self.sitting.delete() return render(self.request, self.result_template_name, results)