further improve anatomy questions
This commit is contained in:
@@ -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))
|
||||
|
||||
@@ -20,25 +20,40 @@
|
||||
</div>
|
||||
<h2>Question type: {{question.question_type}}</h2>
|
||||
<h3>Primary answer: {{ question.get_primary_answer }}</h3>
|
||||
<div>
|
||||
<details>
|
||||
<summary>
|
||||
<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>
|
||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
|
||||
<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 }}
|
||||
@@ -149,3 +168,16 @@
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<style>
|
||||
.proposed-answer::before {
|
||||
content: "\F505";
|
||||
font-family: bootstrap-icons;
|
||||
}
|
||||
|
||||
.question div {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
{% endblock css %}
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
+158
-69
@@ -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,
|
||||
@@ -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")
|
||||
@@ -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(),
|
||||
),
|
||||
)
|
||||
@@ -926,8 +957,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
# "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,12 +996,17 @@ 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)
|
||||
|
||||
@@ -984,7 +1019,6 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
# 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,7 +1083,9 @@ 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")
|
||||
@@ -1080,7 +1116,6 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
# if request.user not in exam.author.all():
|
||||
# raise PermissionDenied
|
||||
|
||||
|
||||
#
|
||||
|
||||
# context = {
|
||||
@@ -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
|
||||
@@ -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,7 +1528,9 @@ 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":
|
||||
@@ -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))
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {}
|
||||
@@ -2278,7 +2324,6 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
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"]
|
||||
@@ -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,12 +2603,11 @@ class GenericViewBase:
|
||||
)
|
||||
|
||||
|
||||
class ExamCloneMixin():
|
||||
class ExamCloneMixin:
|
||||
def get_template_names(self) -> list[str]:
|
||||
return "exam_clone_form.html"
|
||||
# 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:
|
||||
@@ -2766,11 +2823,10 @@ def user_group_add_candidates_to_exams(request, group_id):
|
||||
return HttpResponse(f"Candidates added")
|
||||
# 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,24 +3456,28 @@ 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")
|
||||
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):
|
||||
if trainee:
|
||||
@@ -3503,6 +3572,7 @@ class SupervisorCreate(CidManagerRequiredMixin, CreateView):
|
||||
# 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,7 +3608,6 @@ 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
|
||||
@@ -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):
|
||||
@@ -3611,7 +3688,9 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user