further improve anatomy questions

This commit is contained in:
Ross
2024-10-07 14:00:45 +01:00
parent 92663bc620
commit 3f5f39c41c
5 changed files with 344 additions and 130 deletions
+6
View File
@@ -329,6 +329,12 @@ class Answer(models.Model):
self.answer = self.answer.strip()
self.answer_compare = get_answer_compare(self.answer)
def can_edit(self, user):
"""Check if user can edit the answer
This just mirrors the result from the attached question"""
return self.question.can_edit(user)
# def get_compare_string(self):
# s = self.answer.lower().strip()
# s = s.translate(str.maketrans('', '', string.punctuation))
+52 -20
View File
@@ -20,25 +20,40 @@
</div>
<h2>Question type: {{question.question_type}}</h2>
<h3>Primary answer: {{ question.get_primary_answer }}</h3>
<details>
<summary>
Answers:
</summary>
<table>
<tr><th>Answer</th><th>Score</th></tr>
{% for answer in question.answers.all|dictsortreversed:"status" %}
<tr>
<td>
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
{% endif %}>
{{ answer }}
</span>
<td>
<td>{{answer.status}}</td>
</tr>
{% endfor %}
</table>
</details>
<div>
<details>
<summary title="Click to view the question answers">
Answers:
</summary>
<table>
<tr><th>Answer</th><th>Score</th></tr>
{% for answer in question.answers.all|dictsortreversed:"status" %}
<tr>
<td {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
{% endif %}>
<span >
{{ answer }}
</span>
<td>
<td>{{answer.status}}</td>
{% if answer.proposed %}
<td>
<button class="btn btn-sm accept-button" data-aid={{answer.id}}
hx-get="{% url 'anatomy:confirm_answer' answer.id %}"
title="Click to accept the proposed answer">Accept</button>
<button class="btn btn-sm delete-button" data-aid={{answer.id}}
hx-get="{% url 'anatomy:delete_answer' answer.id %}"
title="Click to delete the proposed answer"
>Delete</button>
</td>
{% endif %}
</tr>
{% endfor %}
</table>
</details>
</div>
<div>
Answer help: {{ question.answer_help|safe }}
</div>
@@ -52,6 +67,10 @@
Exams: {% for exam in question.exams.all %}
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">{{ exam }}</a>
{% endfor %}
<button class="btn btn-sm" hx-get="{% url 'anatomy:question_add_exam' question_id=question.pk %}"
hx-target="#exam-list"
hx-swap="innerHTML">Edit exam(s)</button>
<span id="exam-list"></span>
</div>
<div>
Modality: {{ question.modality }}
@@ -148,4 +167,17 @@
}
</script>
{% endblock %}
{% endblock %}
{% block css %}
<style>
.proposed-answer::before {
content: "\F505";
font-family: bootstrap-icons;
}
.question div {
margin-bottom: 10px;
}
</style>
{% endblock css %}
+7
View File
@@ -47,6 +47,13 @@ urlpatterns = [
views.QuestionDelete.as_view(),
name="question_delete",
),
path(
"question/<int:question_id>/add_exam",
views.question_add_exam,
name="question_add_exam",
),
path("answer/<int:answer_id>/confirm", views.confirm_answer, name="confirm_answer"),
path("answer/<int:answer_id>/delete", views.delete_answer, name="delete_answer"),
path("exam/<int:exam_pk>/<int:sk>/mark", views.mark, name="mark"),
path("exam/<int:exam_pk>/<int:sk>/mark/all", views.mark_all, name="mark_all"),
path(
+80
View File
@@ -27,6 +27,8 @@ 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 (
@@ -851,3 +853,81 @@ 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: <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()
+199 -110
View File
@@ -48,7 +48,12 @@ from atlas.models import CaseCollection, CaseDetail
from generic.decorators import user_is_cid_user_manager
from generic.filters import CidUserFilter, ExaminationFilter, SupervisorFilter
from generic.tables import CidUserExamTable, CidUserTable, ExaminationTable, SupervisorTable
from generic.tables import (
CidUserExamTable,
CidUserTable,
ExaminationTable,
SupervisorTable,
)
from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
import zipfile
@@ -79,6 +84,7 @@ from .models import (
ExamCollection,
ExamUserStatus,
Examination,
QuestionBase,
QuestionNote,
Supervisor,
UserGrades,
@@ -87,15 +93,31 @@ from .models import (
get_next_cid,
)
from rapids.models import Rapid as RapidQuestion, ExamQuestionDetail as RapidsExamQuestionDetail
from rapids.models import (
Rapid as RapidQuestion,
ExamQuestionDetail as RapidsExamQuestionDetail,
)
from rapids.models import Exam as RapidsExam
from longs.models import Long as LongQuestion, LongSeries, ExamQuestionDetail as LongsExamQuestionDetail
from longs.models import (
Long as LongQuestion,
LongSeries,
ExamQuestionDetail as LongsExamQuestionDetail,
)
from longs.models import Exam as LongsExam
from anatomy.models import AnatomyQuestion as AnatomyQuestion, ExamQuestionDetail as AnatomyExamQuestionDetail
from anatomy.models import (
AnatomyQuestion as AnatomyQuestion,
ExamQuestionDetail as AnatomyExamQuestionDetail,
)
from anatomy.models import Exam as AnatomyExam
from sbas.models import Question as SbasQuestion, ExamQuestionDetail as SbasExamQuestionDetail
from sbas.models import (
Question as SbasQuestion,
ExamQuestionDetail as SbasExamQuestionDetail,
)
from sbas.models import Exam as SbasExam
from physics.models import Question as PhysicsQuestion, ExamQuestionDetail as PhysicsExamQuestionDetail
from physics.models import (
Question as PhysicsQuestion,
ExamQuestionDetail as PhysicsExamQuestionDetail,
)
from physics.models import Exam as PhysicsExam
from django.db.models import Case, When
@@ -112,13 +134,13 @@ import plotly.express as px
from django.db.models import Prefetch
class RedirectMixin():
class RedirectMixin:
def get_success_url(self) -> str:
if "redirect" in self.request.GET:
return self.request.GET["redirect"]
return super().get_success_url()
class AuthorRequiredMixin(object):
def get_object(self, *args, **kwargs):
obj = super().get_object(*args, **kwargs)
@@ -141,6 +163,7 @@ class CidManagerRequiredMixin(UserPassesTestMixin):
# return obj
# raise PermissionDenied() # or Http404
def get_exam_model_from_app_name(app_name: str) -> ExamBase:
EXAM_MAP = {
"physics": PhysicsExam,
@@ -471,7 +494,7 @@ class ExamViews(View, LoginRequiredMixin):
if (exam.open_access and exam.active) or user in exam.get_author_objects():
return True
if marker and user in exam.markers.all():
if marker and user in exam.markers.all():
return True
if exam.authors_only:
@@ -535,7 +558,7 @@ class ExamViews(View, LoginRequiredMixin):
if exam_id is not None:
exam = get_object_or_404(self.Exam, pk=exam_id)
# Remove open_access check for the momement
#if exam.open_access or user in exam.get_author_objects():
# if exam.open_access or user in exam.get_author_objects():
# return True
if user in exam.get_author_objects():
return True
@@ -687,13 +710,21 @@ class ExamViews(View, LoginRequiredMixin):
def exam_list(self, request, all=False):
if not self.check_user_access(request.user):
# raise PermissionDenied
exam_list = self.Exam.objects.filter(author__id=request.user.id, exam_mode=True).order_by(
"name"
)
exam_list = self.Exam.objects.filter(
author__id=request.user.id, exam_mode=True
).order_by("name")
exam_list = exam_list | self.Exam.objects.filter(markers__id=request.user.id, exam_mode=True).order_by("name")
exam_list = exam_list | self.Exam.objects.filter(
markers__id=request.user.id, exam_mode=True
).order_by("name")
else:
exam_list = self.Exam.objects.prefetch_related("valid_user_users", "valid_cid_users").filter(exam_mode=True).order_by("name")
exam_list = (
self.Exam.objects.prefetch_related(
"valid_user_users", "valid_cid_users"
)
.filter(exam_mode=True)
.order_by("name")
)
if not all:
exam_list = exam_list.filter(archive=False).order_by("name")
@@ -743,7 +774,7 @@ class ExamViews(View, LoginRequiredMixin):
status = {}
for user in users:
#user = User.objects.get(pk=u)
# user = User.objects.get(pk=u)
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
if user_exam.results_emailed_status:
status[user] = json.loads(user_exam.results_emailed_status)
@@ -796,7 +827,7 @@ class ExamViews(View, LoginRequiredMixin):
email_results = {}
for user in users:
#user = User.objects.get(pk=u)
# user = User.objects.get(pk=u)
user_exam = exam.get_or_create_cid_user_exam(user_user=user)
if unsent_only and user_exam.results_emailed_status:
@@ -904,7 +935,7 @@ class ExamViews(View, LoginRequiredMixin):
queryset=self.Answer.objects.filter(
proposed=False, status=self.Answer.MarkOptions.CORRECT
),
to_attr="prefetched_primary_answer"
to_attr="prefetched_primary_answer",
# queryset=self.Answer.objects.filter(),
),
)
@@ -913,21 +944,20 @@ class ExamViews(View, LoginRequiredMixin):
elif self.app_name == "longs":
questions = (
exam.exam_questions.prefetch_related(
#Prefetch(
# Prefetch(
# "series",
# queryset=LongSeries.objects.select_related(
# "modality", "examination", "plane", "contrast"
# ).prefetch_related(Prefetch("images")),
#),
# ),
"author",
#"examquestiondetail",
#"longsseries",
# "examquestiondetail",
# "longsseries",
"series",
#"seriesdetail",
# "seriesdetail",
"series__images",
"series__plane",
"series__examination"
"series__examination",
# to_attr="prefetched_primary_answer"
# queryset=self.Answer.objects.filter(),
# "series",
@@ -948,7 +978,7 @@ class ExamViews(View, LoginRequiredMixin):
queryset=self.Answer.objects.filter(
proposed=False, status=self.Answer.MarkOptions.CORRECT
),
to_attr="prefetched_primary_answer"
to_attr="prefetched_primary_answer",
# queryset=self.Answer.objects.filter(),
),
"images",
@@ -966,25 +996,29 @@ class ExamViews(View, LoginRequiredMixin):
# )
elif self.app_name == "physics":
questions = (
exam.exam_questions.select_related("category").all()
exam.exam_questions.select_related("category")
.all()
.order_by("examquestiondetail__sort_order")
# .prefetch_related("images", "abnormality", "region", "examination")
)
else:
questions = exam.exam_questions.select_related().all().order_by("examquestiondetail__sort_order")
questions = (
exam.exam_questions.select_related()
.all()
.order_by("examquestiondetail__sort_order")
)
question_number = len(questions)
cid_candidates = exam.valid_cid_users.count()
users_candidates = exam.valid_user_users.count()
#question_orders = [q.examquestiondetail.sort_order for q in questions]
# question_orders = [q.examquestiondetail.sort_order for q in questions]
# Exam order can be missing when, this can be checked with
# exam.exam_questions.select_related().all().order_by("examquestiondetail__sort_order").values_list("examquestiondetail__sort_order")
# TODO decide if / how this should be flagged to a user (updating teh exam order will fix)
return render(
request,
"{}/exam_overview.html".format(self.app_name),
@@ -1049,10 +1083,12 @@ class ExamViews(View, LoginRequiredMixin):
if not request.user.is_superuser:
raise PermissionDenied
cid_users = exam.valid_cid_users.all()#.order_by("cid")#.prefetch_related("*")
cid_users = (
exam.valid_cid_users.all()
) # .order_by("cid")#.prefetch_related("*")
cid_user_count = len(cid_users)
user_users = exam.valid_user_users.all()#.order_by("username")
user_users = exam.valid_user_users.all() # .order_by("username")
user_user_count = len(user_users)
user_exam_data = exam.cid_users.all().prefetch_related("cid_user", "user_user")
@@ -1072,7 +1108,7 @@ class ExamViews(View, LoginRequiredMixin):
return render(request, "exam_cids.html", context)
#def exam_groups_edit(self, request, exam_id):
# def exam_groups_edit(self, request, exam_id):
# exam = get_object_or_404(self.Exam, pk=exam_id)
# if not request.user.groups.filter(name="cid_user_manager").exists():
@@ -1080,8 +1116,7 @@ class ExamViews(View, LoginRequiredMixin):
# if request.user not in exam.author.all():
# raise PermissionDenied
#
#
# context = {
# "exam": exam,
@@ -1208,7 +1243,9 @@ class ExamViews(View, LoginRequiredMixin):
if cid is not None:
statuses = exam.exam_user_status.filter(cid_user_exam__cid_user__cid=cid)
elif user_id is not None:
statuses = exam.exam_user_status.filter(cid_user_exam__user_user__id=user_id)
statuses = exam.exam_user_status.filter(
cid_user_exam__user_user__id=user_id
)
else:
statuses = exam.exam_user_status.all()
@@ -1332,7 +1369,8 @@ class ExamViews(View, LoginRequiredMixin):
)
case "longs":
question_detail = LongsExamQuestionDetail.objects.get(
question=question, exam=exam)
question=question, exam=exam
)
case "physics":
question_detail = PhysicsExamQuestionDetail.objects.get(
question=question, exam=exam
@@ -1341,7 +1379,7 @@ class ExamViews(View, LoginRequiredMixin):
question_detail = SbasExamQuestionDetail.objects.get(
question=question, exam=exam
)
case _:
case _:
data = {"status": "error"}
return JsonResponse(data, status=200)
question_detail.sort_order = n
@@ -1408,7 +1446,7 @@ class ExamViews(View, LoginRequiredMixin):
queryset=self.Answer.objects.filter(
proposed=False, status=self.Answer.MarkOptions.CORRECT
),
to_attr="prefetched_primary_answer"
to_attr="prefetched_primary_answer",
# queryset=self.Answer.objects.filter(),
),
# Prefetch(
@@ -1490,15 +1528,17 @@ class ExamViews(View, LoginRequiredMixin):
)
@method_decorator(login_required)
def exam_question_user_answer(self, request, pk: int, sk: int, c_or_u: str, user_or_cid: str):
def exam_question_user_answer(
self, request, pk: int, sk: int, c_or_u: str, user_or_cid: str
):
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
if c_or_u == "u":
user = get_object_or_404(User,pk=user_or_cid)
user = get_object_or_404(User, pk=user_or_cid)
answer = exam.get_question_user_user_answer(sk, user)
elif c_or_u == "c":
#cid_user = CidUser.objects.filter(cid=user_or_cid)
answer =exam.get_question_cid_user_answer(sk, user_or_cid)
# cid_user = CidUser.objects.filter(cid=user_or_cid)
answer = exam.get_question_cid_user_answer(sk, user_or_cid)
else:
raise Http404
@@ -1510,7 +1550,7 @@ class ExamViews(View, LoginRequiredMixin):
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
cid_user = CidUser.objects.filter(cid=cid)
answer =exam.get_question_cid_user_answer(sk, cid_user)
answer = exam.get_question_cid_user_answer(sk, cid_user)
print(answer)
@@ -1522,12 +1562,12 @@ class ExamViews(View, LoginRequiredMixin):
if not self.check_user_access(request.user, pk, marker=True):
raise PermissionDenied
#question = exam.get_questions()[sk]
# question = exam.get_questions()[sk]
question = exam.get_question_by_index(sk)
exam_length = len(exam.exam_questions.all())
#pos = exam.get_question_index(question) + 1
# pos = exam.get_question_index(question) + 1
pos = sk + 1
previous = -1
@@ -1801,7 +1841,9 @@ class ExamViews(View, LoginRequiredMixin):
cid_user=c, start_time=t
)
exam.exam_user_status.create(
cid_user_exam=cid_user_exam, status="submitted", extra="manual submission"
cid_user_exam=cid_user_exam,
status="submitted",
extra="manual submission",
)
cid_user_exam.end_time = timezone.now()
@@ -1831,7 +1873,9 @@ class ExamViews(View, LoginRequiredMixin):
user_id = None
else:
user_id = request.user.pk
if exam.exam_mode and not exam.check_cid_user(cid, passcode, request.user, user_id):
if exam.exam_mode and not exam.check_cid_user(
cid, passcode, request.user, user_id
):
raise Http404("No available exam")
# exam_json_cache = cache.get("{}_exam_json_{}".format(self.app_name, pk))
@@ -2010,7 +2054,7 @@ class ExamViews(View, LoginRequiredMixin):
if not exam.check_cid_user(cid, passcode, request.user):
if not self.check_user_access(request.user, pk):
#raise PermissionDenied
# raise PermissionDenied
raise Http404("Error accessing exam")
if user is not None:
@@ -2060,7 +2104,6 @@ class ExamViews(View, LoginRequiredMixin):
if self.app_name == "longs":
feedback = user_answer.candidate_feedback
match self.app_name:
case "sbas":
correct_answer = q.get_correct_answer()
@@ -2084,7 +2127,9 @@ class ExamViews(View, LoginRequiredMixin):
answers_marks.append(answer_score)
print(q.get_questions(), ans, answer_score, correct_answer)
merged_ans = zip(q.get_questions(), ans, answer_score, correct_answer)
merged_ans = zip(
q.get_questions(), ans, answer_score, correct_answer
)
answers_and_marks.append((q, merged_ans))
case _:
correct_answer = q.get_primary_answer()
@@ -2096,7 +2141,9 @@ class ExamViews(View, LoginRequiredMixin):
answers_marks.append(answer_score)
if self.app_name == "longs":
answers_and_marks.append((ans, answer_score, correct_answer, feedback))
answers_and_marks.append(
(ans, answer_score, correct_answer, feedback)
)
else:
answers_and_marks.append((ans, answer_score, correct_answer))
@@ -2110,7 +2157,9 @@ class ExamViews(View, LoginRequiredMixin):
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)
total_score = "{} ({} unmarked)".format(
total_score, unmarked_number
)
else:
total_score = sum(answers_marks)
@@ -2172,7 +2221,7 @@ class ExamViews(View, LoginRequiredMixin):
# user_answers_and_marks = defaultdict(list)
user_answers_marks = defaultdict(list)
#user_answers = defaultdict(list)
# user_answers = defaultdict(list)
# user_names = {}
# cid_passcodes = {}
@@ -2246,7 +2295,7 @@ class ExamViews(View, LoginRequiredMixin):
if answer_score == "unmarked":
index = exam.get_question_index(q)
unmarked.add(index)
#user_answers[cid].append(ans)
# user_answers[cid].append(ans)
user_answers_marks[cid].append(answer_score)
if self.app_name == "rapids":
@@ -2259,9 +2308,6 @@ class ExamViews(View, LoginRequiredMixin):
zipped_ans_scores = zip(ans, answer_score)
by_question[q][cid] = zipped_ans_scores
user_scores = {}
user_scores_normalised = {}
user_answer_count = {}
@@ -2274,11 +2320,10 @@ class ExamViews(View, LoginRequiredMixin):
# If either question or user do not exist we give a default
# not answered == score of 3
if user not in by_question[question]:
#print("NOT in")
# print("NOT in")
by_question[question][user] = ("Not answered", 3.0)
user_answers_marks[user].append(3.0)
if self.app_name in ("rapids", "anatomy", "sbas", "longs"):
user_scores[user] = sum(
[i for i in user_answers_marks[user] if i != "unmarked"]
@@ -2313,7 +2358,7 @@ class ExamViews(View, LoginRequiredMixin):
mode = 0
fig_html = ""
else:
#Exclude very low scores from statistics
# Exclude very low scores from statistics
lower_score_bound = max_score / 5
bounded_scores = [i for i in user_scores_list if i > lower_score_bound]
if bounded_scores:
@@ -2400,9 +2445,9 @@ class ExamViews(View, LoginRequiredMixin):
}
if self.app_name == "rapids":
template_variables[
"user_answers_callstates"
] = user_answers_callstates_counted
template_variables["user_answers_callstates"] = (
user_answers_callstates_counted
)
return render(
request,
@@ -2419,9 +2464,7 @@ class ExamViews(View, LoginRequiredMixin):
if not exam.exam_mode:
raise Http404("Packet not in exam mode")
template_variables = {
"exam": exam
}
template_variables = {"exam": exam}
return render(
request,
@@ -2476,7 +2519,7 @@ class GenericViewBase:
@method_decorator(login_required)
def question_detail(self, request, pk):
question = get_object_or_404(self.question_object, pk=pk)
question: QuestionBase = get_object_or_404(self.question_object, pk=pk)
if not question.open_access:
if (
@@ -2499,7 +2542,12 @@ class GenericViewBase:
return render(
request,
f"{self.app_name}/question_detail.html",
{"question": question, "view_feedback": view_feedback, "remote_url": settings.REMOTE_URL},
{
"question": question,
"view_feedback": view_feedback,
"remote_url": settings.REMOTE_URL,
"can_edit": question.can_edit(request.user),
},
)
# TODO: improve permissions on these
@@ -2514,15 +2562,22 @@ class GenericViewBase:
question = get_object_or_404(self.question_object, pk=pk)
if answer_compare is None:
answers = self.cid_user_answer_object.objects.filter(question__id=question.pk).prefetch_related("question", "exam", "user")
answers = self.cid_user_answer_object.objects.filter(
question__id=question.pk
).prefetch_related("question", "exam", "user")
else:
answers = self.cid_user_answer_object.objects.filter(question__id=question.pk, answer_compare=answer_compare).prefetch_related("question", "exam", "user")
answers = self.cid_user_answer_object.objects.filter(
question__id=question.pk, answer_compare=answer_compare
).prefetch_related("question", "exam", "user")
return render(
request,
f"{self.app_name}/question_user_answers.html",
{"question": question, "answers": answers, "answer_compare": answer_compare},
{
"question": question,
"answers": answers,
"answer_compare": answer_compare,
},
)
@method_decorator(login_required)
@@ -2548,11 +2603,10 @@ class GenericViewBase:
)
class ExamCloneMixin():
class ExamCloneMixin:
def get_template_names(self) -> list[str]:
return "exam_clone_form.html"
#return super().get_template_names()
# return super().get_template_names()
def get_initial(self):
old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"])
@@ -2617,6 +2671,8 @@ class ExaminationAutocomplete(autocomplete.Select2QuerySetView):
return Examination.objects.none()
return qs
class SupervisorAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
# TODO: we should probably filter this to only
@@ -2746,6 +2802,7 @@ def cid_group_view_all(request):
{"groups": groups, "view_all": True},
)
@user_is_cid_user_manager
def user_group_add_candidates_to_exams(request, group_id):
if request.htmx:
@@ -2764,13 +2821,12 @@ def user_group_add_candidates_to_exams(request, group_id):
else:
exam.valid_user_users.add(*group.users.all())
return HttpResponse(f"Candidates added")
#exam.save()
# exam.save()
raise PermissionDenied() # or Http404
pass
@user_is_cid_user_manager
def user_group_view_detail(request, group_id):
group = get_object_or_404(UserUserGroup, pk=group_id)
@@ -2817,6 +2873,7 @@ def get_user_selection_from_request(request):
user_models = User.objects.filter(id__in=selected_users)
return user_models
@user_is_cid_user_manager
def user_not_trainee(request, user_id):
user = get_object_or_404(User, pk=user_id)
@@ -2824,7 +2881,10 @@ def user_not_trainee(request, user_id):
user.userprofile.peninsula_trainee = False
user.userprofile.save()
return HttpResponse(f"{user.username} is no longer a trainee", content_type="text/plain")
return HttpResponse(
f"{user.username} is no longer a trainee", content_type="text/plain"
)
@user_is_cid_user_manager
def users_bulk_edit(request):
@@ -2863,8 +2923,7 @@ def users_bulk_edit(request):
# TODO: work out how to actually use hyperscript
html = (
html
+
f"""<button class='change-grade-button' id='add-grade--remove'
+ f"""<button class='change-grade-button' id='add-grade--remove'
hx-post="{reverse('generic:users_bulk_edit')}"
hx-include="[name='selection']"
hx-confirm="This will remove grades of all selected users, are you sure you wish to continue?"
@@ -3065,6 +3124,7 @@ def candidate_email_results_resend(request, cid, resend=True):
def create_cid_email(request):
pass
@user_is_cid_user_manager
def cid_details(request, cid: int):
print(cid)
@@ -3077,6 +3137,7 @@ def cid_details(request, cid: int):
raise PermissionDenied() # or Http404
@user_is_cid_user_manager
def manage_cid_users(request):
if request.method == "POST":
@@ -3308,11 +3369,13 @@ class UserUserGroupUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView):
context["group_type"] = "user"
return context
class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView):
model = UserUserGroup
template_name = "confirm_delete.html"
success_url = reverse_lazy("generic:user_group_view")
class UserGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
model = UserUserGroup
form_class = UserGroupExamForm
@@ -3322,6 +3385,7 @@ class UserGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
context["group_type"] = "user"
return context
class CidGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
model = CidUserGroup
form_class = CidGroupExamForm
@@ -3331,6 +3395,7 @@ class CidGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
context["group_type"] = "cid"
return context
class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
template_name = "exam_create_form.html"
@@ -3391,26 +3456,30 @@ def exam_inactive(request, context):
#
# success_url = reverse_lazy("accounts_list")
@user_is_cid_user_manager
def trainees(request, grade: None|str=None):
trainees = UserProfile.objects.filter(peninsula_trainee=True, user__is_active=True).order_by("grade__name", Lower("user__last_name")).prefetch_related("supervisor", "grade", "user")
@user_is_cid_user_manager
def trainees(request, grade: None | str = None):
trainees = (
UserProfile.objects.filter(peninsula_trainee=True, user__is_active=True)
.order_by("grade__name", Lower("user__last_name"))
.prefetch_related("supervisor", "grade", "user")
)
if grade is not None:
trainees = trainees.filter(grade__name=grade)
context = {
"trainees" : trainees,
"grade": grade
}
context = {"trainees": trainees, "grade": grade}
return render(request, "generic/trainees.html", context)
def create_trainee(request, context=None):
return create_user(request, context, trainee=True)
@user_is_cid_user_manager
def create_user(request, context=None, trainee: bool=False):
def create_user(request, context=None, trainee: bool = False):
if trainee:
form_template = "generic/trainee_creation_form.html"
else:
@@ -3500,9 +3569,10 @@ class SupervisorCreate(CidManagerRequiredMixin, CreateView):
# success_url = reverse_lazy("generic:supervisor_detail")
#class SupervisorList(CidManagerRequiredMixin, ListView):
# class SupervisorList(CidManagerRequiredMixin, ListView):
# model = Supervisor
class SupervisorList(CidManagerRequiredMixin, SingleTableMixin, FilterView):
model = Supervisor
table_class = SupervisorTable
@@ -3510,20 +3580,25 @@ class SupervisorList(CidManagerRequiredMixin, SingleTableMixin, FilterView):
filterset_class = SupervisorFilter
class ExamCollectionList(ListView):
model = ExamCollection
class ExamCollectionDetail(DetailView, AuthorRequiredMixin):
model = ExamCollection
class ExamCollectionEdit(UpdateView, AuthorRequiredMixin):
model = ExamCollection
form_class = ExamCollectionForm
class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
model = ExamCollection
form_class = ExamCollectionForm
class ExamCollectionClone(CreateView, AuthorRequiredMixin):
model = ExamCollection
template_name = "generic/examcollection_clone_form.html"
@@ -3533,23 +3608,22 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin):
old_object = get_object_or_404(self.model, pk=self.kwargs["pk"])
self.initial_object = old_object
initial_data = model_to_dict(old_object, exclude=["id", "date"])
## We manually transfer the forign keys / m2m relationships
#questions = old_object.exam_questions.all().values_list("id", flat=True)
# questions = old_object.exam_questions.all().values_list("id", flat=True)
authors = old_object.author.all().values_list("id", flat=True)
## clear the associated groups
#initial_data["valid_user_users"] = []
#initial_data["valid_cid_users"] = []
#initial_data["cid_user_groups"] = []
#initial_data["user_user_groups"] = []
# initial_data["valid_user_users"] = []
# initial_data["valid_cid_users"] = []
# initial_data["cid_user_groups"] = []
# initial_data["user_user_groups"] = []
#initial_data["active"] = False
#initial_data["publish_results"] = False
# initial_data["active"] = False
# initial_data["publish_results"] = False
#self.exam_questions = list(questions)
# self.exam_questions = list(questions)
self.author = list(authors)
return initial_data
@@ -3557,7 +3631,7 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin):
def form_valid(self, form):
object = form.save()
# Reapply these otherwise they get lost?
#object.exam_questions.set(self.exam_questions)
# object.exam_questions.set(self.exam_questions)
GROUP_TYPES = (
("Rapid Exams", "rapids_exams", RapidsExam),
@@ -3571,11 +3645,13 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin):
object.save()
return HttpResponseRedirect(object.get_absolute_url())
class ExamCollectionDelete(DeleteView, AuthorRequiredMixin):
model = ExamCollection
template_name = "confirm_delete.html"
success_url = reverse_lazy("generic:examcollection_list")
class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView):
model = Examination
table_class = ExaminationTable
@@ -3597,7 +3673,8 @@ class ExaminationUpdate(UpdateView, SuperuserRequiredMixin):
class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
"""Download all images from an image series"""
http_method_names = ['get']
http_method_names = ["get"]
series_object = None
def get_files(self):
@@ -3606,12 +3683,14 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
return [i.image.file for i in series.get_images()]
def get_archive_name(self, request):
#series = Series.objects.get(pk=self.kwargs["pk"])
# series = Series.objects.get(pk=self.kwargs["pk"])
return f"Series {self.kwargs['pk']}.zip"
def get(self, request, *args, **kwargs):
temp_file = ContentFile(b"", name=self.get_archive_name(request))
with zipfile.ZipFile(temp_file, mode='w', compression=zipfile.ZIP_DEFLATED) as zip_file:
with zipfile.ZipFile(
temp_file, mode="w", compression=zipfile.ZIP_DEFLATED
) as zip_file:
files = self.get_files()
for file_ in files:
path = os.path.split(file_.name)[-1]
@@ -3620,24 +3699,35 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
file_size = temp_file.tell()
temp_file.seek(0)
response = HttpResponse(temp_file, content_type='application/zip')
response['Content-Disposition'] = 'attachment; filename=%s' % self.get_archive_name(request)
response['Content-Length'] = file_size
response = HttpResponse(temp_file, content_type="application/zip")
response["Content-Disposition"] = (
"attachment; filename=%s" % self.get_archive_name(request)
)
response["Content-Length"] = file_size
return response
class ExamGroupsUpdateBase(
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
RevisionMixin,
CheckCanEditMixin,
LoginRequiredMixin,
AuthorRequiredMixin,
UpdateView,
):
template_name = "generic/exam_groups_edit.html"
def get_success_url(self) -> str:
return reverse_lazy(f"{self.object.get_app_name()}:exam_cids", kwargs={"exam_id": self.object.pk})
return reverse_lazy(
f"{self.object.get_app_name()}:exam_cids",
kwargs={"exam_id": self.object.pk},
)
def get_form_kwargs(self):
kwargs = super(ExamGroupsUpdateBase, self).get_form_kwargs()
kwargs.update({"user": self.request.user})
return kwargs
class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
def get_form_kwargs(self):
@@ -3661,4 +3751,3 @@ class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
if self.request.user in obj.get_author_objects():
return context
raise PermissionDenied() # or Http404