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 PermissionDenied 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, JsonResponse from django.http import HttpResponseRedirect, HttpResponse from .forms import ( AnatomyAnswerForm, AnswerFormSet, AnswerUpdateFormSet, BodyPartForm, ExaminationForm, MarkAnatomyQuestionForm, AnatomyQuestionForm, StructureForm, ) from .models import ( AnatomyQuestion, BodyPart, CidUserAnswer, #Examination, Structure, Exam, Answer, # HalfMarkAnswers, # IncorrectAnswers, ) from generic.models import Examination from generic.views import ExamViews 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 SuperuserRequiredMixin 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): 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 question_detail(request, pk): question = get_object_or_404(AnatomyQuestion, pk=pk) if not question.open_access and not request.user.groups.filter(name="anatomy_checker").exists() : raise PermissionDenied() return render(request, "anatomy/question_detail.html", {"question": question}) @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) def loadJsonAnswer(answer): # Backend eid is just the object pk number # in the frontend this is appended with the type # e.g. anatomy/1 eid = int(answer["eid"].split("/")[1]) # As access is not restricted make sure the data appears valid if (not isinstance(answer["cid"], int)) or ( not isinstance(eid, int) or (not isinstance(answer["ans"], str)) ): return JsonResponse({"success": False, "error": "cid or eid or answers not defined"}) # The model should catch invalid data but this should be less intensive max_int = 999999999999999999999 if answer["cid"] > max_int or eid > max_int: return JsonResponse({"success": False, "error": "invalid cid"}) exam = get_object_or_404(Exam, pk=eid) if not exam.active: return JsonResponse( {"success": False, "error": "No active exam: {}".format(eid)} ) exiting_answers = CidUserAnswer.objects.filter( question__id=answer["qid"], exam__id=eid, cid=answer["cid"] ) if not exiting_answers: ans = CidUserAnswer(answer=answer["ans"], cid=answer["cid"]) ans.question_id = answer["qid"] ans.exam_id = eid ans.full_clean() ans.save() else: # Update an existing answer # should never be more than one (famous last words) ans = exiting_answers[0] ans.answer = answer["ans"] ans.full_clean() ans.save() return True #@csrf_exempt #def postExamAnswers(request): # if request.is_ajax and request.method == "POST": # # n = 0 # # horrible but it works # #for k in request.POST.dict(): # # for answer in json.loads(request.POST.get("answers")): # ret = loadJsonAnswer(answer) # # if ret is not True: # return ret # n = n + 1 # # # print(UserAnswer.objects.filter(exam__id=q["eid"])) # # print(request.urlencode()) # # return JsonResponse({"success": True, "question_count": n}) # return JsonResponse({"success": False, "error": "Invalid data"}) # # # return render(request, "anatomy/exam.html", {"exam" : exam, "question" : question}) # @user_passes_test(user_is_admin, login_url="/accounts/login") @login_required def mark(request, pk, sk): exam = get_object_or_404(Exam, pk=pk) questions = exam.exam_questions.all() n = sk question_details = { "total": len(questions), "current": n + 1, } try: question = questions[sk] except IndexError: raise Http404("Exam question does not exist") answers_dict = {} for ans in question.answers.all(): answers_dict[ans.answer_compare] = ans marked_answers_set = set(answers_dict.keys()) # correct_answers = [i.answer.lower() for i in question.answers.all()] # half_correct_answers = [ # i.answer.lower() for i in question.half_mark_answers.all() # ] # incorrect_answers = [ # i.answer.lower() for i in question.incorrect_answers.all() # ] unmarked_user_answers = question.GetUnmarkedAnswers() 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("anatomy:mark", pk=pk, sk=n + 1) cd = form.cleaned_data # correct = cd.get("correct") # half_correct = cd.get("half_correct") # incorrect = cd.get("incorrect") marked_answers = json.loads(cd.get("marked_answers")) ## This is mildly dangerous as we delete all answers # question.answers.all().delete() # question.half_mark_answers.all().delete() # question.incorrect_answers.all().delete() # This should probably use json (or something else...) # ajax..... for ans in marked_answers["correct"]: 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 = Answer.MarkOptions.CORRECT a.save() # for ans in half_correct.split("--//--"): for ans in marked_answers["half-correct"]: 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 = Answer.MarkOptions.HALF_MARK a.save() for ans in marked_answers["incorrect"]: 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 = Answer.MarkOptions.INCORRECT a.save() # 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:mark", pk=pk, sk=n + 1) elif "previous" in request.POST: return redirect("anatomy:mark", pk=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) return render( request, "anatomy/mark.html", { "exam": exam, "form": form, "question": question, "question_details": question_details, "user_answers": unmarked_user_answers, "correct_answers": correct_answers, "half_mark_answers": half_mark_answers, "incorrect_answers": incorrect_answers, }, ) @login_required def exam_scores_cid(request, pk): exam = get_object_or_404(Exam, pk=pk) questions = exam.exam_questions.all() cids = ( CidUserAnswer.objects.filter(question__in=questions) .order_by("cid") .values_list("cid", flat=True) .distinct() ) user_answers_and_marks = defaultdict(list) user_answers_marks = defaultdict(list) user_answers = defaultdict(list) user_names = {} by_question = defaultdict(list) unmarked = set() # Loop through all candidates for cid in cids: # Convoluted (probably...) user_names[cid] = cid for q in questions: # Get user answer s = q.cid_user_answers.filter(cid=cid).first() if not s: # skip if no answer user_answers_marks[cid].append(0) user_answers[cid].append("") by_question[q].append(("", 0)) continue ans = s.answer answer_score = s.get_answer_score() if answer_score == "unmarked": index = exam.get_question_index(q) unmarked.add(index) user_answers[cid].append(ans) user_answers_marks[cid].append(answer_score) user_answers_and_marks[cid].append((ans, answer_score)) by_question[q].append((ans, answer_score)) user_scores = {} for user in user_answers_marks: user_scores[user] = sum( [i for i in user_answers_marks[user] if i != "unmarked"] ) user_scores_list = list(user_scores.values()) if len(user_scores_list) < 1: mean = 0 median = 0 mode = 0 fig_html = "" else: mean = statistics.mean(user_scores_list) median = statistics.median(user_scores_list) try: mode = statistics.mode(user_scores_list) except statistics.StatisticsError: mode = "No unique mode" df = user_scores_list fig = px.histogram( df, x=0, title="{}: distribution of scores".format(exam), labels={"0": "Score"}, height=400, width=600, ) fig_html = fig.to_html() max_score = len(questions) * 2 return render( request, "anatomy/exam_scores.html", { "cids": cids, "exam": exam, "unmarked": unmarked, "questions": questions, "by_question": by_question, "user_answers": dict(user_answers), "user_answers_marks": dict(user_answers_marks), "user_scores": user_scores, "user_scores_list": user_scores_list, "user_names": user_names, "user_answers_and_marks": user_answers_and_marks, "max_score": max_score, "mean": mean, "median": median, "mode": mode, "plot": fig_html, }, ) def exam_scores_cid_user(request, pk, sk): exam = get_object_or_404(Exam, pk=pk) # TODO:Need some kind of test for cid cid = sk questions = exam.exam_questions.all() answers_and_marks = [] answers_marks = [] answers = [] for q in questions: # Get user answer user_answer = q.cid_user_answers.filter(cid=cid).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.GetPrimaryAnswer() if not exam.publish_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, "questions": questions, "answers": answers, "answers_marks": answers_marks, "total_score": total_score, "max_score": max_score, "answers_and_marks": answers_and_marks, }, ) # @login_required # def exam_scores(request, pk): # exam = get_object_or_404(Exam, pk=pk) # questions = exam.exam_questions.all() # users = (CidUserAnswer.objects.filter( # question__in=questions).values_list("user").distinct()) # user_answers_and_marks = defaultdict(list) # user_answers_marks = defaultdict(list) # user_answers = defaultdict(list) # user_names = {} # by_question = defaultdict(list) # for u in users: ## Convoluted (probably...) # user_names[u] = User.objects.filter(pk=u[0]).first().get_username() # for q in questions: # user_answer = q.user_answers.filter(user__in=u).first() # if not user_answer: # user_answers_marks[u].append(0) # user_answers[u].append("") # continue # ans = user_answer.answer # answer_score = user_answer.get_answer_score() # user_answers[u].append(ans) # user_answers_marks[u].append(answer_score) # user_answers_and_marks[u].append((ans, answer_score)) # by_question[q].append(ans) # user_scores = {} # for user in user_answers_marks: # user_scores[user] = sum(user_answers_marks[user]) # total = len(questions) # return render( # request, # "anatomy/exam_scores.html", # { # "exam": exam, # "questions": questions, # "by_question": by_question, # "user_answers": dict(user_answers), # "user_answers_marks": dict(user_answers_marks), # "user_scores": user_scores, # "user_names": user_names, # "user_answers_and_marks": user_answers_and_marks, # }, # ) # def cid_scores(request, pk): # #exam = get_object_or_404(Exam, pk=pk) # # # TODO:Need some kind of test for cid # cid = pk # # #questions = exam.exam_questions.all() # answers = CidUserAnswer.objects.filter( # cid=cid) # # # if not answers: # raise Http404("cid not found") # # exam_ids = answers.values_list("exam").distinct() # # exams = Exam.objects.filter(id__in=exam_ids) # # # return render( # request, # "anatomy/cid_scores.html", # { # "exams": exams, # "cid": cid, # }, # ) class AnatomyQuestionCreateBase(LoginRequiredMixin, CreateView): model = AnatomyQuestion form_class = AnatomyQuestionForm def get_context_data(self, **kwargs): context = super(AnatomyQuestionCreateBase, 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) 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) @login_required def question_clone(request, pk): new_item = get_object_or_404(AnatomyQuestion, pk=pk) new_item.pk = None # autogen a new pk (item_id) # new_item.name = "Copy of " + new_item.name #need to change uniques form = AnatomyQuestion(request.POST or None, instance=new_item) formset = AnswerFormSet() if form.is_valid(): form.instance.author.add(request.user.id) # logger.debug(formset.is_valid()) if formset.is_valid(): response = super().form_valid(form) formset.instance = obj formset.save() return response else: return super().form_invalid(form) context = { "form": form, "answer_formset": formset # other context } return render(request, "anatomy/anatomyquestion_form.html", context) # @login_required class AnatomyQuestionCreate(AnatomyQuestionCreateBase): # initial = {'laterality': AnatomyQuestion.NONE} def get_initial(self): pass # # 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 AnatomyQuestionUpdate(LoginRequiredMixin, AuthorOrCheckerRequiredMixin, class AnatomyQuestionUpdate(LoginRequiredMixin, UpdateView): model = AnatomyQuestion form_class = AnatomyQuestionForm def get_context_data(self, **kwargs): context = super(AnatomyQuestionUpdate, 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) return context def form_valid(self, form): # save exam orders (there must be a better way to do this) exam_orders = {} for exam in self.object.exams.all(): exam_orders[exam] = list(exam.exam_questions.all()) print(exam_orders[exam]) 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() # 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 rapid""" # fields = '__all__' # #fields = [ 'condition' ] # #initial = {'date_of_death': '05/01/2018'} # exclude = [ 'created_date', 'published_date' ] def get_initial(self): # print(self.request) old_object = get_object_or_404(AnatomyQuestion, pk=self.kwargs["pk"]) initial_data = model_to_dict(old_object, exclude=["id"]) exams = old_object.exams.all().values_list("id", flat=True) initial_data["exams"] = list(exams) 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.is_ajax(): 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.is_ajax(): 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("/") @login_required def create_structure(request): form = StructureForm(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": "Structure"} ) @csrf_exempt def get_structure_id(request): if request.is_ajax(): 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 = "anatomy/anatomy_question_view.html" filterset_class = AnatomyQuestionFilter class AnatomyUserAnswerView(LoginRequiredMixin, SingleTableMixin, FilterView): model = CidUserAnswer table_class = AnatomyUserAnswerTable template_name = "anatomy/anatomy_question_view.html" filterset_class = AnatomyUserAnswerFilter def question_save_annotation(request, pk): if request.is_ajax() and 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) AnatomyExamViews = ExamViews(Exam, AnatomyQuestion, "anatomy", "anatomy", loadJsonAnswer) class UserAnswerDelete(SuperuserRequiredMixin, DeleteView): model = CidUserAnswer success_url = reverse_lazy("anatomy:anatomy_user_answer_view") class QuestionDelete(AuthorOrCheckerRequiredMixin, DeleteView): model = AnatomyQuestion success_url = reverse_lazy("anatomy:anatomy_question_view")