Files
penracourses/anatomy/views.py
T

1100 lines
36 KiB
Python

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,
SuggestIncorrectAnswerForm,
)
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 django.db.models import Count
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_view(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_view.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
@login_required
def exam_review_question_responses(request, pk: int, q_index: int):
"""Return the responses partial for a given anatomy exam question (HTMX-friendly)."""
exam = get_object_or_404(Exam, pk=pk)
questions = exam.get_questions()
try:
question = questions[q_index]
except Exception:
raise Http404("Question not found in exam")
try:
exam_user_answers_qs = question.cid_user_answers.filter(exam=exam).select_related("user")
except Exception:
exam_user_answers_qs = question.cid_user_answers.all()
# Allow an HTMX-driven reveal of respondent names. Default: anonymised.
reveal = request.GET.get("reveal", "0")
reveal_respondents = str(reveal).lower() in ("1", "true", "yes", "on")
# Build a small list that includes a sensible display name for each respondent.
# If the answer is from a regular user use their full name/username. If it's a CID
# candidate try to resolve the CidUser and show CID + name if available. If neither
# is present fall back to empty/anonymous.
exam_user_answers = []
for ua in exam_user_answers_qs:
# default display
display = None
is_cid = False
try:
if getattr(ua, "user", None):
user = ua.user
# prefer full name where available
full = getattr(user, "get_full_name", None)
if full:
name = user.get_full_name() or user.username
else:
name = getattr(user, "username", str(user))
display = name
elif getattr(ua, "cid", None):
cid_val = ua.cid
cu = CidUser.objects.filter(cid=cid_val).first()
if cu is not None and cu.name:
display = f"CID{cid_val} ({cu.name})"
else:
display = f"CID{cid_val}"
is_cid = True
except Exception:
display = None
exam_user_answers.append({"ua": ua, "display_name_raw": display, "is_cid": is_cid})
context = {
"exam": exam,
"question": question,
"q_index": q_index,
"app_name": "anatomy",
"exam_user_answers": exam_user_answers,
"reveal_respondents": reveal_respondents,
}
return render(request, "anatomy/partials/exam_review_question_responses_fragment.html", context)
@login_required
def exam_review_question_summary(request, pk: int, q_index: int):
"""Return aggregated response summary partial for anatomy question (HTMX-friendly)."""
exam = get_object_or_404(Exam, pk=pk)
questions = exam.get_questions()
try:
question = questions[q_index]
except Exception:
raise Http404("Question not found in exam")
ua_qs = question.cid_user_answers.filter(exam=exam)
total_responses = ua_qs.count()
# counts by score values ('2','1','0','')
score_counts = {}
for s, c in ua_qs.values_list("score").annotate(count=Count("id")) if False else []:
pass
# simpler: compute via values and normalise unmarked -> 'empty'
score_counts = {str(row["score"]) if row["score"] is not None else "empty": row["count"] for row in ua_qs.values("score").annotate(count=Count("id"))}
# top submitted answers (by answer_compare) with dominant score per answer
# First, get counts grouped by answer_compare and score
grouped = ua_qs.values("answer_compare", "score").annotate(count=Count("id"))
# Aggregate totals and score breakdown per answer_compare
answer_map = {}
for row in grouped:
ans = row["answer_compare"]
sc = str(row["score"]) if row["score"] is not None else "empty"
cnt = row["count"]
if ans not in answer_map:
answer_map[ans] = {"total": 0, "score_counts": {}}
answer_map[ans]["total"] += cnt
answer_map[ans]["score_counts"][sc] = answer_map[ans]["score_counts"].get(sc, 0) + cnt
# Build list sorted by total count desc, and choose a dominant score per answer
top_answers = []
for ans, meta in answer_map.items():
# pick dominant score by highest count; tie-break prefer higher numeric score
score_counts = meta["score_counts"]
# sort by (count desc, score desc) where 'empty' maps to -1
def score_key(item):
s, c = item
s_val = -1 if s == "empty" else int(s)
return (c, s_val)
dominant = max(score_counts.items(), key=score_key)[0]
top_answers.append({"answer_compare": ans, "count": meta["total"], "dominant_score": dominant})
top_answers.sort(key=lambda x: x["count"], reverse=True)
top_answers = top_answers[:10]
correct_count = None
correct_pct = None
# compute correct based on matching marked answers if available
if total_responses:
# count scores equal to '2' as correct
correct_count = score_counts.get("2", 0)
correct_pct = round(100.0 * correct_count / total_responses, 1) if total_responses else None
# compute percentage distribution for score buckets for the summary bar chart
score_keys = ["2", "1", "0", "empty"]
score_pcts = {}
if total_responses:
for k in score_keys:
score_pcts[k] = round(100.0 * score_counts.get(k, 0) / total_responses, 1)
else:
for k in score_keys:
score_pcts[k] = 0.0
context = {
"exam": exam,
"question": question,
"q_index": q_index,
"app_name": "anatomy",
"exam_response_total": total_responses,
"exam_response_score_counts": score_counts,
"exam_response_score_pcts": score_pcts,
"exam_response_correct_count": correct_count,
"exam_response_correct_pct": correct_pct,
"top_answers": top_answers,
}
return render(request, "anatomy/partials/exam_review_question_summary_fragment.html", context)
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
words_present: set = set()
words_present_in_correct: set = set()
for ans in correct_answers:
answer_words = set(ans.get_constituent_words())
words_present.update(answer_words)
words_present_in_correct.update(answer_words)
for ans in half_mark_answers:
answer_words = set(ans.get_constituent_words())
words_present.update(answer_words)
words_present_in_correct.update(answer_words)
for ans in incorrect_answers:
answer_words = set(ans.get_constituent_words())
words_present.update(answer_words)
words_suggest_incorrect = words_present - words_present_in_correct - set(question.answer_suggest_incorrect)
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,
"words_suggest_incorrect": words_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()
response = super().form_valid(form)
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)
# 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)
def get_success_url(self) -> str:
return reverse("anatomy:question_detail", kwargs={"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)
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(
'<script>opener.closePopup(window, "%s", "%s", "#id_body_part");</script>'
% (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(
'<script>opener.closePopup(window, "%s", "%s", "#id_examination");</script>'
% (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_suggest_incorrect_answers(request, pk):
question = get_object_or_404(AnatomyQuestion, pk=pk)
if request.method == "POST":
form = SuggestIncorrectAnswerForm(request.POST, instance=question)
if form.is_valid():
obj = form.save()
obj.save()
return render(request, "anatomy/question_detail.html#suggest-incorrect-form", {"question": question})
return HttpResponse("Invalid")
else:
form = SuggestIncorrectAnswerForm(instance=question)
return render(request, "anatomy/suggest_incorrect.html", {"form": form, "question": question})
return HttpResponse("Error", status=400)
@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")
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_view")
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 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)
exams = exams.difference(question.exams.all())
if not exams:
return HttpResponse("No exams available to add")
html = "Exams to add to question: <br>"
for exam in exams:
html = html + (f"<span id='htmx-exam-list'><form><input name='exam_id' value='{exam.id}' type='hidden'><button hx-post=\"{request.path}\""
f">{exam.name}: {exam.id}</button></form>"
)
html = html + "<br>Exams to remove from question: <br>"
for exam in question.exams.all():
html = html + (f"<span id='htmx-exam-list'><form><input name='exam_id' value='{exam.id}' type='hidden'><input name='remove' value='true' type='hidden'><button hx-post=\"{request.path}\""
f"hx-confirm='Are you sure you want to remove this exam from the question?'"
f">{exam.name}: {exam.id}</button></form>"
)
html = html + "<button _='on click remove #htmx-exam-list'>Cancel</button>"
return HttpResponse(mark_safe(html))
raise PermissionDenied()