from django.forms.models import model_to_dict from django.shortcuts import render, get_object_or_404, redirect from django.views.decorators.csrf import csrf_exempt from django import forms # 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.core.exceptions import FieldError, PermissionDenied from django.contrib.auth.mixins import LoginRequiredMixin from django.views.generic.detail import DetailView 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, JsonResponse from django.http import HttpResponseRedirect, HttpResponse from dal import autocomplete from django.conf import settings from django.utils.html import format_html_join from django.utils.safestring import mark_safe from .forms import ( AnatomyAnswerForm, AnatomyQuestionAnswerForm, AnswerFormSet, AnswerUpdateFormSet, BodyPartForm, ExamAuthorForm, ExamGroupsForm, ExamMarkerForm, ExaminationForm, MarkAnatomyQuestionForm, AnatomyQuestionForm, ExamForm, ) from .models import ( AnatomyQuestion, BodyPart, UserAnswer, # Examination, Structure, Exam, Answer, # HalfMarkAnswers, # IncorrectAnswers, ) from generic.models import CidUser, Examination from generic.views import AuthorRequiredMixin, ExamCloneMixin, ExamCreateBase, ExamDeleteBase, ExamGroupsUpdateBase, ExamUpdateBase, ExamViews, GenericViewBase, UpdateQuestionMixin from reversion.views import RevisionMixin from .decorators import user_is_author_or_anatomy_checker from .tables import AnatomyQuestionTable, AnatomyUserAnswerTable from .filters import AnatomyQuestionFilter, AnatomyUserAnswerFilter from django_tables2 import SingleTableView, SingleTableMixin from django_filters.views import FilterView from collections import defaultdict import os import json import statistics import plotly.express as px from helpers.images import image_as_base64 from django.template.defaulttags import register from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin from rest_framework import viewsets, permissions from .serializers import ExamSerializer class AuthorOrCheckerRequiredMixin(object): def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) if self.request.user.groups.filter(name="anatomy_checker").exists(): return obj if self.request.user not in obj.author.all(): raise PermissionDenied() # or Http404 return obj @register.filter def get_item(dictionary, key, default=None): if dictionary is None: return default return dictionary.get(key) def user_is_admin(user): if user: if user.pk == 1: return True return False def question_list(request): questions = AnatomyQuestion.objects.all() if not request.user.groups.filter(name="anatomy_checker").exists(): questions = questions.filter(open_access=False) # raise PermissionDenied() return render(request, "anatomy/question_list.html", {"questions": questions}) @login_required def answer_question(request, pk): question = get_object_or_404(AnatomyQuestion, pk=pk) answer = question.user_answers.filter( user=request.user ).first() # .filter(user=User) if request.method == "POST": if answer: form = AnatomyAnswerForm(request.POST, instance=answer) else: form = AnatomyAnswerForm(request.POST) if form.is_valid(): answer = form.save(commit=False) answer.user = request.user answer.question = question # answer.published_date = timezone.now() answer.save() return redirect("question_detail", pk=pk) else: form = AnatomyAnswerForm(instance=answer) return render( request, "anatomy/answer_question.html", {"form": form, "question": question} ) # @login_required # def exam_take(request, pk, sk): # """ # Allows taking of the exam on the django server (when logged in) # # No longer used (deprecated in favour of using RTS) # """ # exam = get_object_or_404(Exam, pk=pk) # # questions = exam.exam_questions.all() # # try: # question = questions[sk] # except IndexError: # raise Http404("Exam question does not exist") # # n = sk # # question_details = { # "total": len(questions), # "current": n + 1, # } # # # Get data for flagged # # answered_questions = UserAnswer.objects.filter(user=request.user).values_list("question__pk", flat=True) # user_answer_data = UserAnswer.objects.filter(user=request.user).values_list( # "question__pk", "answer", "flagged" # ) # # flagged_questions = UserAnswer.objects.filter(user=request.user, flagged=True).values_list("question__pk", flat=True) # # flagged_questions I= UserAnswer.filter(user=request.user, ).values_list("question__pk", flat=True) # # answered_questions = set() # flagged_questions = set() # for ans_pk, answer, flagged in user_answer_data: # if len(answer.strip()) > 0: # answered_questions.add(ans_pk) # if flagged: # flagged_questions.add(ans_pk) # # # u = question.user_answers # answer = question.user_answers.filter( # user=request.user # ).first() # .filter(user=User) # if request.method == "POST": # if answer: # form = AnatomyAnswerForm(request.POST, instance=answer) # else: # form = AnatomyAnswerForm(request.POST) # if form.is_valid(): # answer = form.save(commit=False) # answer.user = request.user # answer.question = question # # answer.published_date = timezone.now() # answer.save() # if "next" in request.POST: # return redirect("anatomy:exam_take", pk=pk, sk=n + 1) # elif "previous" in request.POST: # return redirect("anatomy:exam_take", pk=pk, sk=n - 1) # else: # form = AnatomyAnswerForm(instance=answer) # return render( # request, # "anatomy/exam.html", # { # "exam": exam, # "form": form, # "question": question, # "question_details": question_details, # "questions": questions, # "flagged_questions": flagged_questions, # "answered_questions": answered_questions, # "answer": answer, # }, # ) # def flag_question(request): # pk = request.GET.get("pk", None) # ans = UserAnswer.objects.filter(user=request.user, question__pk=pk).first() # ans.flagged = not ans.flagged # ans.save() # data = {"flagged": ans.flagged} # return JsonResponse(data) @login_required def mark_all(request, exam_pk, sk): return mark(request, exam_pk, sk, unmarked_exam_answers_only=False) @login_required def mark_review(request, exam_pk, sk): return mark(request, exam_pk, sk, unmarked_exam_answers_only=False, review=True) # @user_passes_test(user_is_admin, login_url="/accounts/login") @login_required def mark(request, exam_pk, sk, unmarked_exam_answers_only=True, review=False): exam = get_object_or_404(Exam, pk=exam_pk) if not GenericExamViews.check_user_marker_access(request.user, exam_pk): raise PermissionDenied if not exam.exam_mode: raise Http404("Packet not in exam mode") mark_url = "anatomy:mark" if review: mark_url = "anatomy:mark_review" elif not unmarked_exam_answers_only: mark_url = "anatomy:mark_all" questions = exam.get_questions().prefetch_related("answers") n = sk question_details = { "total": len(questions), "current": n + 1, } try: question: AnatomyQuestion = questions[sk] except IndexError: raise Http404("Exam question does not exist") if request.method == "POST": form = MarkAnatomyQuestionForm(request.POST) # ******************************* # TODO: convert to JSON request # ******************************* if form.is_valid(): # If skip button is pressed skip the question without marking if "skip" in request.POST: return redirect(mark_url, exam_pk=exam_pk, sk=n + 1) cd = form.cleaned_data marked_answers = json.loads(cd.get("marked_answers")) # This should probably use json (or something else...) # ajax..... to_run = ( ("correct", Answer.MarkOptions.CORRECT), ("half-correct", Answer.MarkOptions.HALF_MARK), ("incorrect", Answer.MarkOptions.INCORRECT), ) for status_string, status in to_run: for ans in marked_answers[status_string]: ans = ans.strip() if ans == "": continue a = Answer.objects.filter( answer__iexact=ans, question_id=question.pk ).first() if a is None: a = Answer() a.question_id = question.pk a.answer = ans a.status = status a.save() if "next" in request.POST: return redirect(mark_url, exam_pk=exam_pk, sk=n + 1) elif "previous" in request.POST: return redirect(mark_url, exam_pk=exam_pk, sk=n - 1) # Reset user answers (relies on the functional javascript) unmarked_user_answers = set() else: form = MarkAnatomyQuestionForm() # correct_answers = question.answers.filter(status=Answer.MarkOptions.CORRECT) # half_mark_answers = question.answers.filter(status=Answer.MarkOptions.HALF_MARK) # incorrect_answers = question.answers.filter(status=Answer.MarkOptions.INCORRECT) correct_answers = [] half_mark_answers = [] incorrect_answers = [] review_user_answers = [] unmarked_answers_bool = False unmarked_user_answers = [] if review: unmarked_user_answers = question.get_user_answers( exam_pk=exam_pk, include_normal=False ) elif unmarked_exam_answers_only: unmarked_user_answers = question.get_unmarked_user_answers(exam_pk=exam_pk) else: unmarked_user_answers = question.get_unmarked_user_answers() if not unmarked_exam_answers_only: correct_answers = question.answers.filter(status=Answer.MarkOptions.CORRECT) half_mark_answers = question.answers.filter(status=Answer.MarkOptions.HALF_MARK) incorrect_answers = question.answers.filter(status=Answer.MarkOptions.INCORRECT) answer_suggest_incorrect: list = [] if review: for ans in unmarked_user_answers: # duplicated from user answer model.... marked_ans = question.answers.filter(answer_compare__iexact=ans).first() mark = "unmarked" if marked_ans is not None: if marked_ans.status == Answer.MarkOptions.CORRECT: mark = "correct" elif marked_ans.status == Answer.MarkOptions.HALF_MARK: mark = "half-correct" elif marked_ans.status == Answer.MarkOptions.INCORRECT: mark = "incorrect" if mark == "unmarked": unmarked_answers_bool = True review_user_answers.append((ans, mark)) else: for ans in unmarked_user_answers: for suggest_incorrect in question.answer_suggest_incorrect: if suggest_incorrect in ans: answer_suggest_incorrect.append(ans) break return render( request, "anatomy/mark.html", { "exam": exam, "form": form, "review": review, "question": question, "question_number": n, "question_details": question_details, "unmarked_answers_bool": unmarked_answers_bool, "user_answers": unmarked_user_answers, "review_user_answers": review_user_answers, "correct_answers": correct_answers, "half_mark_answers": half_mark_answers, "incorrect_answers": incorrect_answers, "unmarked_exam_answers_only": unmarked_exam_answers_only, "answer_suggest_incorrect": answer_suggest_incorrect, }, ) #@login_required #def exam_scores_refresh(request, pk): # exam = get_object_or_404(Exam, pk=pk) # # if not exam.exam_mode: # raise Http404("Packet not in exam mode") # # questions = exam.exam_questions.all() # Don't worry about order here # # cids = UserAnswer.objects.filter(question__in=questions, exam__id=pk) # # # Force a score update # for c in cids: # c.get_answer_score(cached=False) # # return render( # request, # "anatomy/base.html", # { # "simple_content": "Answer scores updated", # }, # ) def exam_scores_cid_user(request, pk, cid, passcode): exam = get_object_or_404(Exam, pk=pk) if not exam.exam_mode: raise Http404("Packet not in exam mode") if not exam.check_cid_user(cid, passcode, request.user): raise Http404("Error accessing exam") questions = exam.get_questions().prefetch_related("answers") answers_and_marks = [] answers_marks = [] answers = [] view_all_results = False if request.user.groups.filter(name="view_all_results").exists(): view_all_results = True for q in questions: # Get user answer user_answer = q.cid_user_answers.filter(cid=cid, exam__id=pk).first() if not user_answer: # skip if no answer answers_marks.append(0) answers.append("") answer_score = 0 ans = "Not answered" else: ans = user_answer.answer answer_score = user_answer.get_answer_score() correct_answer = q.get_primary_answer() if not exam.publish_results and not view_all_results: correct_answer = "*****" answer_score = 0 answers.append(ans) answers_marks.append(answer_score) answers_and_marks.append((ans, answer_score, correct_answer)) if "unmarked" in answers_marks: answered = [i for i in answers_marks if type(i) == int] total_score = sum(answered) unmarked_number = len(answers_marks) - len(answered) total_score = "{} ({} unmarked)".format(total_score, unmarked_number) else: total_score = sum(answers_marks) max_score = len(questions) * 2 return render( request, "anatomy/exam_scores_user.html", { "exam": exam, "cid": cid, "passcode": passcode, "questions": questions, "answers": answers, "answers_marks": answers_marks, "total_score": total_score, "max_score": max_score, "answers_and_marks": answers_and_marks, "view_all_results": view_all_results, }, ) class AnatomyQuestionCreateBase(RevisionMixin, LoginRequiredMixin, CreateView): model = AnatomyQuestion form_class = AnatomyQuestionForm def get_form_kwargs(self): kwargs = super(AnatomyQuestionCreateBase, self).get_form_kwargs() kwargs.update({"user": self.request.user}) return kwargs def get_context_data(self, **kwargs): context = super(AnatomyQuestionCreateBase, self).get_context_data(**kwargs) if "object" in context: context["question"] = context["object"] 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) context = self.get_context_data(form=form) formset = context["answer_formset"] if formset.is_valid(): response = super().form_valid(form) formset.instance = self.object formset.save() # 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("anatomy:question_clone", pk=self.object.pk) else: return super().form_invalid(form) def get_success_url(self) -> str: return redirect("anatomy:question_detail", pk=self.object.pk) class AnatomyQuestionCreate(AnatomyQuestionCreateBase): # 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 # # There has to be a better way... # try: # s = (i.pk for i in self.request.user.AnatomyQuestion_default.site.all()) # self.initial.update({'site': s}) # except AttributeError: # pass # return self.initial # # fields = '__all__' # #fields = [ 'condition' ] # #initial = {'date_of_death': '05/01/2018'} # exclude = [ 'created_date', 'published_date' ] # self.object = form.save(commit=False) # self.object.save() # form.instance.author.add(self.request.user.id) # return super().form_valid(form) class AnatomyQuestionAnswerUpdate(RevisionMixin, AuthorOrCheckerRequiredMixin, UpdateView): model = AnatomyQuestion form_class = AnatomyQuestionAnswerForm template_name = "anatomy/anatomyquestionanswer_form.html" def get_context_data(self, **kwargs): context = super(AnatomyQuestionAnswerUpdate, self).get_context_data(**kwargs) if self.request.POST: context["answer_formset"] = AnswerUpdateFormSet( self.request.POST, self.request.FILES, instance=self.object ) context["answer_formset"].full_clean() else: context["answer_formset"] = AnswerUpdateFormSet(instance=self.object) context["question"] = context["object"] return context 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) formset = context["answer_formset"] # logger.debug(formset.is_valid()) if formset.is_valid(): response = super().form_valid(form) formset.instance = self.object formset.save() return response else: return super().form_invalid(form) # class AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, class AnatomyQuestionUpdate(UpdateQuestionMixin): model = AnatomyQuestion form_class = AnatomyQuestionForm 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) #formset = context["answer_formset"] return super().form_valid(form) ## logger.debug(formset.is_valid()) #if formset.is_valid(): # response = super().form_valid(form) # formset.instance = self.object # formset.save() # # restore exam orders # #for exam in self.object.exams.all(): # # if exam in exam_orders and self.object in exam_orders[exam]: # # print(exam_orders[exam]) # # exam.exam_questions.set(exam_orders[exam]) # # exam.save() # return response #else: # return super().form_invalid(form) class QuestionClone(AnatomyQuestionCreateBase): """Clones a existing question""" def get_initial(self): # print(self.request) old_object = get_object_or_404(AnatomyQuestion, pk=self.kwargs["pk"]) if self.request.user not in old_object.get_author_objects() and not self.request.user.is_superuser: raise PermissionDenied() # or Http404 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) # TODO: clone answers? return initial_data @login_required def create_body_part(request): form = BodyPartForm(request.POST or None) if form.is_valid(): instance = form.save() return HttpResponse( '' % (instance.pk, instance) ) return render( request, "anatomy/create_simple.html", {"form": form, "name": "BodyPart"} ) @csrf_exempt def get_body_part_id(request): if request.accepts("application/json")(): body_part_name = request.GET["body_part_name"] body_part_id = BodyPart.objects.get(name=body_part_name).id data = { "body_part_id": body_part_id, } return HttpResponse(json.dumps(data), content_type="application/json") return HttpResponse("/") @login_required def create_examination(request): form = ExaminationForm(request.POST or None) if form.is_valid(): instance = form.save() return HttpResponse( '' % (instance.pk, instance) ) return render( request, "anatomy/create_simple.html", {"form": form, "name": "Examination"} ) @csrf_exempt def get_examination_id(request): if request.accepts("application/json")(): examination_name = request.GET["examination_name"] examination_id = Examination.objects.get(name=examination_name).id data = { "examination_id": examination_id, } return HttpResponse(json.dumps(data), content_type="application/json") return HttpResponse("/") @csrf_exempt def get_structure_id(request): if request.accepts("application/json")(): structure_name = request.GET["structure_name"] structure_id = Structure.objects.get(name=structure_name).id data = { "structure_id": structure_id, } return HttpResponse(json.dumps(data), content_type="application/json") return HttpResponse("/") class AnatomyQuestionView(LoginRequiredMixin, SingleTableMixin, FilterView): model = AnatomyQuestion table_class = AnatomyQuestionTable template_name = "question_table_view.html" filterset_class = AnatomyQuestionFilter def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["app_name"] = "anatomy" return context # def get_queryset(self): # qs = super().get_queryset() # qs = qs.select_related('modality', 'question_type') # return qs @user_is_author_or_anatomy_checker def question_save_annotation(request, pk): if request.method == "POST": question = get_object_or_404(AnatomyQuestion, pk=pk) question.image_annotations = request.POST.get("annotation") print(question.image_annotations) question.save() data = {"status": "success"} return JsonResponse(data, status=200) else: data = {"status": "error"} return JsonResponse(data, status=400) GenericExamViews = ExamViews( Exam, AnatomyQuestion, Answer, UserAnswer, "anatomy", "anatomy" ) class UserAnswerView(SuperuserRequiredMixin, DetailView): model = UserAnswer class UserAnswerTableView(SuperuserRequiredMixin, SingleTableMixin, FilterView): model = UserAnswer table_class = AnatomyUserAnswerTable template_name = "anatomy/user_answer_question_view.html" filterset_class = AnatomyUserAnswerFilter class UserAnswerDelete(SuperuserRequiredMixin, DeleteView): model = UserAnswer template_name = "user_answer_delete.html" success_url = reverse_lazy("anatomy:user_answer_table_view") class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView): model = AnatomyQuestion success_url = reverse_lazy("anatomy:question_list") class ExamCreate(ExamCreateBase): model = Exam form_class = ExamForm class ExamUpdate(ExamUpdateBase, AuthorOrCheckerRequiredMixin): model = Exam form_class = ExamForm #def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context["can_edit"] = self.object.check_user_can_edit(self.request.user) # return context 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="anatomy_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 GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam) class ExamClone(ExamCloneMixin, ExamCreate): """Clone exam view""" def confirm_answer(request, answer_id: int): if request.htmx: answer = get_object_or_404(Answer, pk=answer_id) # Check if the user has permission to confirm the answer if not answer.can_edit(request.user): return HttpResponse("Invalid permissions", status=403) answer.clean() answer.proposed = False answer.save() return HttpResponse("Answer accepted") raise PermissionDenied() def delete_answer(request, answer_id: int): if request.htmx: answer = get_object_or_404(Answer, pk=answer_id) # Check if the user has permission to confirm the answer if not answer.can_edit(request.user): return HttpResponse("Invalid permissions", status=403) answer.delete() return HttpResponse("Answer deleted") raise PermissionDenied() def question_add_exam(request, question_id: int): if request.htmx: question = get_object_or_404(AnatomyQuestion, pk=question_id) if not question.can_edit(request.user): return HttpResponse("Invalid permissions", status=403) if request.POST: exam_id = request.POST.get("exam_id", None) if exam_id is None: return HttpResponse("No exam id provided", status=400) exam = get_object_or_404(Exam, pk=exam_id) if request.POST.get("remove", False) == "true": question.exams.remove(exam) return HttpResponse("Question removed from exam.") else: question.exams.add(exam) return HttpResponse("Question added to exam.") else: # Return a list of exams that we can add to the question exams = Exam.objects.filter(author=request.user) | Exam.objects.filter(open_access=True) exams = exams.difference(question.exams.all()) if not exams: return HttpResponse("No exams available to add") html = "Exams to add to question:
" for exam in exams: html = html + (f"
" ) html = html + "
Exams to remove from question:
" for exam in question.exams.all(): html = html + (f"
" ) html = html + "" return HttpResponse(mark_safe(html)) raise PermissionDenied()