from reversion.views import RevisionMixin from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin from django.views.generic.detail import DetailView from generic.models import CidUser from sbas.forms import UserAnswerForm, ExamAuthorForm, ExamForm from django.shortcuts import render, get_object_or_404, redirect from django.views.decorators.csrf import csrf_exempt from django import forms from django.utils import timezone from django.forms.models import model_to_dict # from django.contrib.auth.models import User from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import User from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic import ListView from django.db.models.functions import Lower from django.core.cache import cache from django.urls import reverse_lazy, reverse from django.http import Http404, HttpResponseBadRequest, JsonResponse from django.http import HttpResponseRedirect, HttpResponse from .models import Question, Category, Exam, UserAnswer from django_tables2 import SingleTableView, SingleTableMixin from django_filters.views import FilterView from collections import defaultdict import os import base64 import mimetypes import json import statistics import plotly.express as px from generic.views import ( AuthorRequiredMixin, ExamCloneMixin, ExamCreateBase, ExamDeleteBase, ExamGroupsUpdateBase, ExamUpdateBase, ExamViews, GenericViewBase, UpdateQuestionMixin, exam_inactive, ) from rest_framework import viewsets, permissions from rest_framework.pagination import PageNumberPagination from django.core.exceptions import PermissionDenied from .tables import QuestionTable, UserAnswerTable from .filters import QuestionFilter, UserAnswerFilter from .forms import ( ExamGroupsForm, ExamMarkerForm, QuestionForm, ExamForm, ) class AuthorOrCheckerRequiredMixin(object): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) if self.request.user.groups.filter(name="sbas_checker").exists(): return obj if self.request.user not in obj.author.all(): raise PermissionDenied() # or Http404 return obj def question_list(request): questions = Question.objects.all() return render(request, "sbas/question_list.html", {"questions": questions}) @login_required def question_detail(request, pk): question = get_object_or_404(Question, pk=pk) return render(request, "sbas/question_detail.html", {"question": question}) def active_exams(request): exams = Exam.objects.all() print(exams) active_exams = [] for exam in exams: if exam.active: active_exams.append(exam) return render(request, "sbas/available_exam_list.html", {"exams": active_exams}) def exam_start(request, pk): exam = get_object_or_404(Exam, pk=pk) if not exam.active: return exam_inactive(request, context={"exam": exam}) return render( request, "sbas/exam_start.html", {"exam": exam, "valid_user": exam.check_logged_in_user(request)}, ) def exam_take_overview(request, pk, cid=None, passcode=None): exam = get_object_or_404(Exam, pk=pk) if not exam.active: return exam_inactive(request, context={"exam": exam}) exam.check_user_can_take(cid, passcode, request.user) questions = exam.get_questions() if cid is not None: answers = UserAnswer.objects.filter(cid=cid, exam=exam) else: answers = UserAnswer.objects.filter(user=request.user, exam=exam) answer_question_map = {} for ans in answers: answer_question_map[ans.question] = ans question_answer_tuples = [] answer_count = 0 for q in questions: if q in answer_question_map and answer_question_map[q].answer: question_answer_tuples.append((q, answer_question_map[q])) answer_count += 1 else: question_answer_tuples.append((q, None)) cid_user_exam = exam.get_or_create_cid_user_exam(cid=cid, user_user=request.user) return render( request, "sbas/exam_take_overview.html", { "exam": exam, "cid": cid, "question_answer_tuples": question_answer_tuples, "answer_count": answer_count, "exam_length": len(questions), "passcode": passcode, "cid_user_exam": cid_user_exam, }, ) def exam_take(request, pk: int, sk: int, cid: int = None, passcode: str = None): exam = get_object_or_404(Exam, pk=pk) if not exam.active: return exam_inactive(request, context={"exam": exam}) exam.check_user_can_take(cid, passcode, request.user) cid_user_exam = exam.get_or_create_cid_user_exam(cid = cid, user_user=request.user) question = exam.get_questions()[sk] exam_length = len(exam.exam_questions.all()) pos = exam.get_question_index(question) if cid is not None: answer = question.cid_user_answers.filter(cid=cid, exam=exam).first() else: answer = question.cid_user_answers.filter(user=request.user, exam=exam).first() previous = -1 if sk > 0: previous = sk - 1 next = sk + 1 if sk == exam_length - 1: next = False if request.method == "POST": if answer: form = UserAnswerForm(request.POST, instance=answer) else: form = UserAnswerForm(request.POST) if form.is_valid(): answer = form.save(commit=False) if cid is not None: answer.cid = cid else: answer.user = request.user answer.question = question answer.exam = exam # answer.published_date = timezone.now() answer.save() cid_user_exam.end_time = timezone.now() cid_user_exam.save() if cid is not None: kwargs = {"pk": pk, "cid": cid, "passcode": passcode} take_url = "sbas:exam_take" finish_url = "sbas:exam_take_overview" else: kwargs = {"pk": pk} take_url = "sbas:exam_take_user" finish_url = "sbas:exam_take_overview_user" if "next" in request.POST: if not next: return HttpResponseBadRequest("Invalid request") return redirect(take_url, sk=pos + 1, **kwargs) elif "previous" in request.POST: return redirect(take_url, sk=pos - 1, **kwargs) elif "finish" in request.POST: return redirect(finish_url, **kwargs) elif "goto" in request.POST: return redirect(take_url, sk=int(request.POST.get("goto")), **kwargs) else: form = UserAnswerForm(instance=answer) saved_answer = False if answer is not None: saved_answer = answer.answer return render( request, "sbas/exam_take.html", { "form": form, "cid": cid, "exam": exam, "question": question, "next": next, "previous": previous, "exam_length": exam_length, "pos": pos, "saved_answer": saved_answer, "passcode": passcode, "cid_user_exam": cid_user_exam, }, ) GenericExamViews = ExamViews(Exam, Question, None, UserAnswer, "sbas", "sbas") class QuestionView(LoginRequiredMixin, SingleTableMixin, FilterView): model = Question table_class = QuestionTable template_name = "question_table_view.html" filterset_class = QuestionFilter def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["app_name"] = "sbas" return context class QuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView): model = Question form_class = QuestionForm def get_form_kwargs(self): kwargs = super(QuestionCreateBase, self).get_form_kwargs() kwargs.update({"user": self.request.user}) return kwargs def get_context_data(self, **kwargs): context = super(QuestionCreateBase, self).get_context_data(**kwargs) # if self.request.POST: # context["answer_formset"] = AnswerFormSet( # self.request.POST, self.request.FILES # ) # else: # context["answer_formset"] = AnswerFormSet() return context def form_valid(self, form): self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) response = super().form_valid(form) # If the normal submit button is pressed we save as normal if "submit" in self.request.POST: return response # else we redirect to the clone url else: return redirect("sbas:question_clone", pk=self.object.pk) class QuestionCreate(QuestionCreateBase): # initial = {'laterality': AnatomyQuestion.NONE} def get_initial(self): if "pk" in self.kwargs: initial = super().get_initial() exam = get_object_or_404(Exam, pk=self.kwargs["pk"]) initial["exams"] = [exam.id] return initial class QuestionUpdate(UpdateQuestionMixin): model = Question form_class = QuestionForm def form_valid(self, form): self.object = form.save(commit=False) self.object.save() form.instance.author.add(self.request.user.id) context = self.get_context_data(form=form) response = super().form_valid(form) return response class QuestionClone(QuestionCreateBase): """Clones a existing question""" def get_initial(self): # print(self.request) old_object = get_object_or_404(Question, pk=self.kwargs["pk"]) initial_data = model_to_dict(old_object, exclude=["id"]) # We don't want to clone the exam details # exams = old_object.exams.all().values_list("id", flat=True) # initial_data["exams"] = list(exams) return initial_data class UserAnswerView(LoginRequiredMixin, DetailView): model = UserAnswer class UserAnswerTableView(LoginRequiredMixin, SingleTableMixin, FilterView): model = UserAnswer table_class = UserAnswerTable template_name = "sbas/user_answer_question_view.html" filterset_class = UserAnswerFilter class UserAnswerDelete(SuperuserRequiredMixin, DeleteView): model = UserAnswer template_name = "user_answer_delete.html" success_url = reverse_lazy("sbas:user_answer_table_view") # @user_passes_test(lambda u: u.is_superuser) # def user_answer_delete_multiple(request): # print(request.POST["answer_ids"]) # answer_ids = json.loads(request.POST["answer_ids"]) # # # We could probably delete them all at once.... # for id in answer_ids: # UserAnswer.objects.get(pk=id).delete() # return JsonResponse({"status": "success"}) GenericViews = GenericViewBase("sbas", Question, UserAnswer, Exam) class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView): model = Question success_url = reverse_lazy("sbas:question_list") class ExamCreate(ExamCreateBase): model = Exam form_class = ExamForm class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): model = Exam form_class = ExamForm class ExamDelete(AuthorOrCheckerRequiredMixin, ExamDeleteBase): model = Exam # class ExamViewSet(RevisionMixin, viewsets.ModelViewSet): # # queryset = Exam.objects.all().order_by('name') # serializer_class = ExamSerializer # permission_classes = [permissions.IsAuthenticated] # # def get_queryset(self): # """ # This view should return a list exams available to a user. # """ # user = self.request.user # if user.groups.filter(name="sbas_checker").exists(): # return Exam.objects.all() # # return Exam.objects.filter(author__id=user.id) class ExamAuthorUpdate( RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView ): model = Exam form_class = ExamAuthorForm template_name = "author_form.html" class ExamMarkersUpdate( RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView ): model = Exam form_class = ExamMarkerForm template_name = "markers_form.html" class ExamGroupsUpdate(ExamGroupsUpdateBase): model = Exam form_class = ExamGroupsForm class ExamClone(ExamCloneMixin, ExamCreate): """Clone exam view""" def exam_clone2(request, exam_id): exam = get_object_or_404(Exam, pk=exam_id) new_exam = exam.clone_model() return redirect("sbas:exam_update", pk=new_exam.id)