further improve anatomy questions
This commit is contained in:
@@ -329,6 +329,12 @@ class Answer(models.Model):
|
|||||||
self.answer = self.answer.strip()
|
self.answer = self.answer.strip()
|
||||||
self.answer_compare = get_answer_compare(self.answer)
|
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):
|
# def get_compare_string(self):
|
||||||
# s = self.answer.lower().strip()
|
# s = self.answer.lower().strip()
|
||||||
# s = s.translate(str.maketrans('', '', string.punctuation))
|
# s = s.translate(str.maketrans('', '', string.punctuation))
|
||||||
|
|||||||
@@ -20,25 +20,40 @@
|
|||||||
</div>
|
</div>
|
||||||
<h2>Question type: {{question.question_type}}</h2>
|
<h2>Question type: {{question.question_type}}</h2>
|
||||||
<h3>Primary answer: {{ question.get_primary_answer }}</h3>
|
<h3>Primary answer: {{ question.get_primary_answer }}</h3>
|
||||||
|
<div>
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary title="Click to view the question answers">
|
||||||
Answers:
|
Answers:
|
||||||
</summary>
|
</summary>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>Answer</th><th>Score</th></tr>
|
<tr><th>Answer</th><th>Score</th></tr>
|
||||||
{% for answer in question.answers.all|dictsortreversed:"status" %}
|
{% for answer in question.answers.all|dictsortreversed:"status" %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
|
||||||
<span {% if answer.proposed %}class="proposed-answer" data-aid="{{answer.pk}}" data-question-type="anatomy"
|
|
||||||
{% endif %}>
|
{% endif %}>
|
||||||
|
<span >
|
||||||
{{ answer }}
|
{{ answer }}
|
||||||
</span>
|
</span>
|
||||||
<td>
|
<td>
|
||||||
<td>{{answer.status}}</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>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</table>
|
</table>
|
||||||
</details>
|
</details>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Answer help: {{ question.answer_help|safe }}
|
Answer help: {{ question.answer_help|safe }}
|
||||||
</div>
|
</div>
|
||||||
@@ -52,6 +67,10 @@
|
|||||||
Exams: {% for exam in question.exams.all %}
|
Exams: {% for exam in question.exams.all %}
|
||||||
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">{{ exam }}</a>
|
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}">{{ exam }}</a>
|
||||||
{% endfor %}
|
{% 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>
|
||||||
<div>
|
<div>
|
||||||
Modality: {{ question.modality }}
|
Modality: {{ question.modality }}
|
||||||
@@ -149,3 +168,16 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% 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(),
|
views.QuestionDelete.as_view(),
|
||||||
name="question_delete",
|
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", views.mark, name="mark"),
|
||||||
path("exam/<int:exam_pk>/<int:sk>/mark/all", views.mark_all, name="mark_all"),
|
path("exam/<int:exam_pk>/<int:sk>/mark/all", views.mark_all, name="mark_all"),
|
||||||
path(
|
path(
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ from django.http import HttpResponseRedirect, HttpResponse
|
|||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.utils.html import format_html_join
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
@@ -851,3 +853,81 @@ GenericViews = GenericViewBase("anatomy", AnatomyQuestion, UserAnswer, Exam)
|
|||||||
class ExamClone(ExamCloneMixin, ExamCreate):
|
class ExamClone(ExamCloneMixin, ExamCreate):
|
||||||
"""Clone exam view"""
|
"""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.decorators import user_is_cid_user_manager
|
||||||
from generic.filters import CidUserFilter, ExaminationFilter, SupervisorFilter
|
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
|
from generic.mixins import CheckCanEditMixin, SuperuserRequiredMixin
|
||||||
|
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -79,6 +84,7 @@ from .models import (
|
|||||||
ExamCollection,
|
ExamCollection,
|
||||||
ExamUserStatus,
|
ExamUserStatus,
|
||||||
Examination,
|
Examination,
|
||||||
|
QuestionBase,
|
||||||
QuestionNote,
|
QuestionNote,
|
||||||
Supervisor,
|
Supervisor,
|
||||||
UserGrades,
|
UserGrades,
|
||||||
@@ -87,15 +93,31 @@ from .models import (
|
|||||||
get_next_cid,
|
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 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 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 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 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 physics.models import Exam as PhysicsExam
|
||||||
|
|
||||||
from django.db.models import Case, When
|
from django.db.models import Case, When
|
||||||
@@ -112,13 +134,13 @@ import plotly.express as px
|
|||||||
from django.db.models import Prefetch
|
from django.db.models import Prefetch
|
||||||
|
|
||||||
|
|
||||||
|
class RedirectMixin:
|
||||||
class RedirectMixin():
|
|
||||||
def get_success_url(self) -> str:
|
def get_success_url(self) -> str:
|
||||||
if "redirect" in self.request.GET:
|
if "redirect" in self.request.GET:
|
||||||
return self.request.GET["redirect"]
|
return self.request.GET["redirect"]
|
||||||
return super().get_success_url()
|
return super().get_success_url()
|
||||||
|
|
||||||
|
|
||||||
class AuthorRequiredMixin(object):
|
class AuthorRequiredMixin(object):
|
||||||
def get_object(self, *args, **kwargs):
|
def get_object(self, *args, **kwargs):
|
||||||
obj = super().get_object(*args, **kwargs)
|
obj = super().get_object(*args, **kwargs)
|
||||||
@@ -141,6 +163,7 @@ class CidManagerRequiredMixin(UserPassesTestMixin):
|
|||||||
# return obj
|
# return obj
|
||||||
# raise PermissionDenied() # or Http404
|
# raise PermissionDenied() # or Http404
|
||||||
|
|
||||||
|
|
||||||
def get_exam_model_from_app_name(app_name: str) -> ExamBase:
|
def get_exam_model_from_app_name(app_name: str) -> ExamBase:
|
||||||
EXAM_MAP = {
|
EXAM_MAP = {
|
||||||
"physics": PhysicsExam,
|
"physics": PhysicsExam,
|
||||||
@@ -687,13 +710,21 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
def exam_list(self, request, all=False):
|
def exam_list(self, request, all=False):
|
||||||
if not self.check_user_access(request.user):
|
if not self.check_user_access(request.user):
|
||||||
# raise PermissionDenied
|
# raise PermissionDenied
|
||||||
exam_list = self.Exam.objects.filter(author__id=request.user.id, exam_mode=True).order_by(
|
exam_list = self.Exam.objects.filter(
|
||||||
"name"
|
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:
|
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:
|
if not all:
|
||||||
exam_list = exam_list.filter(archive=False).order_by("name")
|
exam_list = exam_list.filter(archive=False).order_by("name")
|
||||||
@@ -904,7 +935,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
queryset=self.Answer.objects.filter(
|
queryset=self.Answer.objects.filter(
|
||||||
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
||||||
),
|
),
|
||||||
to_attr="prefetched_primary_answer"
|
to_attr="prefetched_primary_answer",
|
||||||
# queryset=self.Answer.objects.filter(),
|
# queryset=self.Answer.objects.filter(),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -926,8 +957,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# "seriesdetail",
|
# "seriesdetail",
|
||||||
"series__images",
|
"series__images",
|
||||||
"series__plane",
|
"series__plane",
|
||||||
"series__examination"
|
"series__examination",
|
||||||
|
|
||||||
# to_attr="prefetched_primary_answer"
|
# to_attr="prefetched_primary_answer"
|
||||||
# queryset=self.Answer.objects.filter(),
|
# queryset=self.Answer.objects.filter(),
|
||||||
# "series",
|
# "series",
|
||||||
@@ -948,7 +978,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
queryset=self.Answer.objects.filter(
|
queryset=self.Answer.objects.filter(
|
||||||
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
||||||
),
|
),
|
||||||
to_attr="prefetched_primary_answer"
|
to_attr="prefetched_primary_answer",
|
||||||
# queryset=self.Answer.objects.filter(),
|
# queryset=self.Answer.objects.filter(),
|
||||||
),
|
),
|
||||||
"images",
|
"images",
|
||||||
@@ -966,12 +996,17 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# )
|
# )
|
||||||
elif self.app_name == "physics":
|
elif self.app_name == "physics":
|
||||||
questions = (
|
questions = (
|
||||||
exam.exam_questions.select_related("category").all()
|
exam.exam_questions.select_related("category")
|
||||||
|
.all()
|
||||||
.order_by("examquestiondetail__sort_order")
|
.order_by("examquestiondetail__sort_order")
|
||||||
# .prefetch_related("images", "abnormality", "region", "examination")
|
# .prefetch_related("images", "abnormality", "region", "examination")
|
||||||
)
|
)
|
||||||
else:
|
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)
|
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")
|
# 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)
|
# TODO decide if / how this should be flagged to a user (updating teh exam order will fix)
|
||||||
|
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"{}/exam_overview.html".format(self.app_name),
|
"{}/exam_overview.html".format(self.app_name),
|
||||||
@@ -1049,7 +1083,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
raise PermissionDenied
|
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)
|
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")
|
||||||
@@ -1080,7 +1116,6 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
# if request.user not in exam.author.all():
|
# if request.user not in exam.author.all():
|
||||||
# raise PermissionDenied
|
# raise PermissionDenied
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|
||||||
# context = {
|
# context = {
|
||||||
@@ -1208,7 +1243,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if cid is not None:
|
if cid is not None:
|
||||||
statuses = exam.exam_user_status.filter(cid_user_exam__cid_user__cid=cid)
|
statuses = exam.exam_user_status.filter(cid_user_exam__cid_user__cid=cid)
|
||||||
elif user_id is not None:
|
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:
|
else:
|
||||||
statuses = exam.exam_user_status.all()
|
statuses = exam.exam_user_status.all()
|
||||||
|
|
||||||
@@ -1332,7 +1369,8 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
)
|
)
|
||||||
case "longs":
|
case "longs":
|
||||||
question_detail = LongsExamQuestionDetail.objects.get(
|
question_detail = LongsExamQuestionDetail.objects.get(
|
||||||
question=question, exam=exam)
|
question=question, exam=exam
|
||||||
|
)
|
||||||
case "physics":
|
case "physics":
|
||||||
question_detail = PhysicsExamQuestionDetail.objects.get(
|
question_detail = PhysicsExamQuestionDetail.objects.get(
|
||||||
question=question, exam=exam
|
question=question, exam=exam
|
||||||
@@ -1408,7 +1446,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
queryset=self.Answer.objects.filter(
|
queryset=self.Answer.objects.filter(
|
||||||
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
proposed=False, status=self.Answer.MarkOptions.CORRECT
|
||||||
),
|
),
|
||||||
to_attr="prefetched_primary_answer"
|
to_attr="prefetched_primary_answer",
|
||||||
# queryset=self.Answer.objects.filter(),
|
# queryset=self.Answer.objects.filter(),
|
||||||
),
|
),
|
||||||
# Prefetch(
|
# Prefetch(
|
||||||
@@ -1490,7 +1528,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@method_decorator(login_required)
|
@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)
|
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
|
||||||
|
|
||||||
if c_or_u == "u":
|
if c_or_u == "u":
|
||||||
@@ -1801,7 +1841,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
cid_user=c, start_time=t
|
cid_user=c, start_time=t
|
||||||
)
|
)
|
||||||
exam.exam_user_status.create(
|
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()
|
cid_user_exam.end_time = timezone.now()
|
||||||
@@ -1831,7 +1873,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
user_id = None
|
user_id = None
|
||||||
else:
|
else:
|
||||||
user_id = request.user.pk
|
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")
|
raise Http404("No available exam")
|
||||||
|
|
||||||
# exam_json_cache = cache.get("{}_exam_json_{}".format(self.app_name, pk))
|
# 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":
|
if self.app_name == "longs":
|
||||||
feedback = user_answer.candidate_feedback
|
feedback = user_answer.candidate_feedback
|
||||||
|
|
||||||
|
|
||||||
match self.app_name:
|
match self.app_name:
|
||||||
case "sbas":
|
case "sbas":
|
||||||
correct_answer = q.get_correct_answer()
|
correct_answer = q.get_correct_answer()
|
||||||
@@ -2084,7 +2127,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
answers_marks.append(answer_score)
|
answers_marks.append(answer_score)
|
||||||
|
|
||||||
print(q.get_questions(), ans, answer_score, correct_answer)
|
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))
|
answers_and_marks.append((q, merged_ans))
|
||||||
case _:
|
case _:
|
||||||
correct_answer = q.get_primary_answer()
|
correct_answer = q.get_primary_answer()
|
||||||
@@ -2096,7 +2141,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
answers_marks.append(answer_score)
|
answers_marks.append(answer_score)
|
||||||
|
|
||||||
if self.app_name == "longs":
|
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:
|
else:
|
||||||
answers_and_marks.append((ans, answer_score, correct_answer))
|
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]
|
answered = [i for i in answers_marks if type(i) == int]
|
||||||
total_score = sum(answered)
|
total_score = sum(answered)
|
||||||
unmarked_number = len(answers_marks) - len(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:
|
else:
|
||||||
total_score = sum(answers_marks)
|
total_score = sum(answers_marks)
|
||||||
|
|
||||||
@@ -2259,9 +2308,6 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
zipped_ans_scores = zip(ans, answer_score)
|
zipped_ans_scores = zip(ans, answer_score)
|
||||||
by_question[q][cid] = zipped_ans_scores
|
by_question[q][cid] = zipped_ans_scores
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
user_scores = {}
|
user_scores = {}
|
||||||
user_scores_normalised = {}
|
user_scores_normalised = {}
|
||||||
user_answer_count = {}
|
user_answer_count = {}
|
||||||
@@ -2278,7 +2324,6 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
by_question[question][user] = ("Not answered", 3.0)
|
by_question[question][user] = ("Not answered", 3.0)
|
||||||
user_answers_marks[user].append(3.0)
|
user_answers_marks[user].append(3.0)
|
||||||
|
|
||||||
|
|
||||||
if self.app_name in ("rapids", "anatomy", "sbas", "longs"):
|
if self.app_name in ("rapids", "anatomy", "sbas", "longs"):
|
||||||
user_scores[user] = sum(
|
user_scores[user] = sum(
|
||||||
[i for i in user_answers_marks[user] if i != "unmarked"]
|
[i for i in user_answers_marks[user] if i != "unmarked"]
|
||||||
@@ -2400,9 +2445,9 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.app_name == "rapids":
|
if self.app_name == "rapids":
|
||||||
template_variables[
|
template_variables["user_answers_callstates"] = (
|
||||||
"user_answers_callstates"
|
user_answers_callstates_counted
|
||||||
] = user_answers_callstates_counted
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -2419,9 +2464,7 @@ class ExamViews(View, LoginRequiredMixin):
|
|||||||
if not exam.exam_mode:
|
if not exam.exam_mode:
|
||||||
raise Http404("Packet not in exam mode")
|
raise Http404("Packet not in exam mode")
|
||||||
|
|
||||||
template_variables = {
|
template_variables = {"exam": exam}
|
||||||
"exam": exam
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
@@ -2476,7 +2519,7 @@ class GenericViewBase:
|
|||||||
|
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
def question_detail(self, request, pk):
|
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 not question.open_access:
|
||||||
if (
|
if (
|
||||||
@@ -2499,7 +2542,12 @@ class GenericViewBase:
|
|||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
f"{self.app_name}/question_detail.html",
|
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
|
# TODO: improve permissions on these
|
||||||
@@ -2514,15 +2562,22 @@ class GenericViewBase:
|
|||||||
question = get_object_or_404(self.question_object, pk=pk)
|
question = get_object_or_404(self.question_object, pk=pk)
|
||||||
|
|
||||||
if answer_compare is None:
|
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:
|
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(
|
return render(
|
||||||
request,
|
request,
|
||||||
f"{self.app_name}/question_user_answers.html",
|
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)
|
@method_decorator(login_required)
|
||||||
@@ -2548,12 +2603,11 @@ class GenericViewBase:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ExamCloneMixin():
|
class ExamCloneMixin:
|
||||||
def get_template_names(self) -> list[str]:
|
def get_template_names(self) -> list[str]:
|
||||||
return "exam_clone_form.html"
|
return "exam_clone_form.html"
|
||||||
# return super().get_template_names()
|
# return super().get_template_names()
|
||||||
|
|
||||||
|
|
||||||
def get_initial(self):
|
def get_initial(self):
|
||||||
old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"])
|
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 Examination.objects.none()
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|
||||||
|
|
||||||
class SupervisorAutocomplete(autocomplete.Select2QuerySetView):
|
class SupervisorAutocomplete(autocomplete.Select2QuerySetView):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
# TODO: we should probably filter this to only
|
# TODO: we should probably filter this to only
|
||||||
@@ -2746,6 +2802,7 @@ def cid_group_view_all(request):
|
|||||||
{"groups": groups, "view_all": True},
|
{"groups": groups, "view_all": True},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def user_group_add_candidates_to_exams(request, group_id):
|
def user_group_add_candidates_to_exams(request, group_id):
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
@@ -2766,11 +2823,10 @@ def user_group_add_candidates_to_exams(request, group_id):
|
|||||||
return HttpResponse(f"Candidates added")
|
return HttpResponse(f"Candidates added")
|
||||||
# exam.save()
|
# exam.save()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
raise PermissionDenied() # or Http404
|
raise PermissionDenied() # or Http404
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def user_group_view_detail(request, group_id):
|
def user_group_view_detail(request, group_id):
|
||||||
group = get_object_or_404(UserUserGroup, pk=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)
|
user_models = User.objects.filter(id__in=selected_users)
|
||||||
return user_models
|
return user_models
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def user_not_trainee(request, user_id):
|
def user_not_trainee(request, user_id):
|
||||||
user = get_object_or_404(User, pk=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.peninsula_trainee = False
|
||||||
user.userprofile.save()
|
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
|
@user_is_cid_user_manager
|
||||||
def users_bulk_edit(request):
|
def users_bulk_edit(request):
|
||||||
@@ -2863,8 +2923,7 @@ def users_bulk_edit(request):
|
|||||||
# TODO: work out how to actually use hyperscript
|
# TODO: work out how to actually use hyperscript
|
||||||
html = (
|
html = (
|
||||||
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-post="{reverse('generic:users_bulk_edit')}"
|
||||||
hx-include="[name='selection']"
|
hx-include="[name='selection']"
|
||||||
hx-confirm="This will remove grades of all selected users, are you sure you wish to continue?"
|
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):
|
def create_cid_email(request):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def cid_details(request, cid: int):
|
def cid_details(request, cid: int):
|
||||||
print(cid)
|
print(cid)
|
||||||
@@ -3077,6 +3137,7 @@ def cid_details(request, cid: int):
|
|||||||
|
|
||||||
raise PermissionDenied() # or Http404
|
raise PermissionDenied() # or Http404
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def manage_cid_users(request):
|
def manage_cid_users(request):
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@@ -3308,11 +3369,13 @@ class UserUserGroupUpdate(RevisionMixin, CidManagerRequiredMixin, UpdateView):
|
|||||||
context["group_type"] = "user"
|
context["group_type"] = "user"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView):
|
class UserUserGroupDelete(RevisionMixin, CidManagerRequiredMixin, DeleteView):
|
||||||
model = UserUserGroup
|
model = UserUserGroup
|
||||||
template_name = "confirm_delete.html"
|
template_name = "confirm_delete.html"
|
||||||
success_url = reverse_lazy("generic:user_group_view")
|
success_url = reverse_lazy("generic:user_group_view")
|
||||||
|
|
||||||
|
|
||||||
class UserGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
class UserGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
||||||
model = UserUserGroup
|
model = UserUserGroup
|
||||||
form_class = UserGroupExamForm
|
form_class = UserGroupExamForm
|
||||||
@@ -3322,6 +3385,7 @@ class UserGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
|||||||
context["group_type"] = "user"
|
context["group_type"] = "user"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class CidGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
class CidGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
||||||
model = CidUserGroup
|
model = CidUserGroup
|
||||||
form_class = CidGroupExamForm
|
form_class = CidGroupExamForm
|
||||||
@@ -3331,6 +3395,7 @@ class CidGroupExamUpdate(CidManagerRequiredMixin, UpdateView):
|
|||||||
context["group_type"] = "cid"
|
context["group_type"] = "cid"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
class ExamCreateBase(RevisionMixin, LoginRequiredMixin, CreateView):
|
||||||
template_name = "exam_create_form.html"
|
template_name = "exam_create_form.html"
|
||||||
|
|
||||||
@@ -3391,24 +3456,28 @@ def exam_inactive(request, context):
|
|||||||
#
|
#
|
||||||
# success_url = reverse_lazy("accounts_list")
|
# success_url = reverse_lazy("accounts_list")
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@user_is_cid_user_manager
|
||||||
def trainees(request, grade: None | str = None):
|
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:
|
if grade is not None:
|
||||||
trainees = trainees.filter(grade__name=grade)
|
trainees = trainees.filter(grade__name=grade)
|
||||||
|
|
||||||
context = {
|
context = {"trainees": trainees, "grade": grade}
|
||||||
"trainees" : trainees,
|
|
||||||
"grade": grade
|
|
||||||
}
|
|
||||||
|
|
||||||
return render(request, "generic/trainees.html", context)
|
return render(request, "generic/trainees.html", context)
|
||||||
|
|
||||||
|
|
||||||
def create_trainee(request, context=None):
|
def create_trainee(request, context=None):
|
||||||
return create_user(request, context, trainee=True)
|
return create_user(request, context, trainee=True)
|
||||||
|
|
||||||
|
|
||||||
@user_is_cid_user_manager
|
@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:
|
if trainee:
|
||||||
@@ -3503,6 +3572,7 @@ class SupervisorCreate(CidManagerRequiredMixin, CreateView):
|
|||||||
# class SupervisorList(CidManagerRequiredMixin, ListView):
|
# class SupervisorList(CidManagerRequiredMixin, ListView):
|
||||||
# model = Supervisor
|
# model = Supervisor
|
||||||
|
|
||||||
|
|
||||||
class SupervisorList(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
class SupervisorList(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
||||||
model = Supervisor
|
model = Supervisor
|
||||||
table_class = SupervisorTable
|
table_class = SupervisorTable
|
||||||
@@ -3510,20 +3580,25 @@ class SupervisorList(CidManagerRequiredMixin, SingleTableMixin, FilterView):
|
|||||||
|
|
||||||
filterset_class = SupervisorFilter
|
filterset_class = SupervisorFilter
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionList(ListView):
|
class ExamCollectionList(ListView):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionDetail(DetailView, AuthorRequiredMixin):
|
class ExamCollectionDetail(DetailView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionEdit(UpdateView, AuthorRequiredMixin):
|
class ExamCollectionEdit(UpdateView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
form_class = ExamCollectionForm
|
form_class = ExamCollectionForm
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
|
class ExamCollectionCreate(CreateView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
form_class = ExamCollectionForm
|
form_class = ExamCollectionForm
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
template_name = "generic/examcollection_clone_form.html"
|
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"])
|
old_object = get_object_or_404(self.model, pk=self.kwargs["pk"])
|
||||||
self.initial_object = old_object
|
self.initial_object = old_object
|
||||||
|
|
||||||
|
|
||||||
initial_data = model_to_dict(old_object, exclude=["id", "date"])
|
initial_data = model_to_dict(old_object, exclude=["id", "date"])
|
||||||
|
|
||||||
## We manually transfer the forign keys / m2m relationships
|
## We manually transfer the forign keys / m2m relationships
|
||||||
@@ -3571,11 +3645,13 @@ class ExamCollectionClone(CreateView, AuthorRequiredMixin):
|
|||||||
object.save()
|
object.save()
|
||||||
return HttpResponseRedirect(object.get_absolute_url())
|
return HttpResponseRedirect(object.get_absolute_url())
|
||||||
|
|
||||||
|
|
||||||
class ExamCollectionDelete(DeleteView, AuthorRequiredMixin):
|
class ExamCollectionDelete(DeleteView, AuthorRequiredMixin):
|
||||||
model = ExamCollection
|
model = ExamCollection
|
||||||
template_name = "confirm_delete.html"
|
template_name = "confirm_delete.html"
|
||||||
success_url = reverse_lazy("generic:examcollection_list")
|
success_url = reverse_lazy("generic:examcollection_list")
|
||||||
|
|
||||||
|
|
||||||
class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView):
|
class ExaminationView(SuperuserRequiredMixin, SingleTableMixin, FilterView):
|
||||||
model = Examination
|
model = Examination
|
||||||
table_class = ExaminationTable
|
table_class = ExaminationTable
|
||||||
@@ -3597,7 +3673,8 @@ class ExaminationUpdate(UpdateView, SuperuserRequiredMixin):
|
|||||||
|
|
||||||
class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
||||||
"""Download all images from an image series"""
|
"""Download all images from an image series"""
|
||||||
http_method_names = ['get']
|
|
||||||
|
http_method_names = ["get"]
|
||||||
series_object = None
|
series_object = None
|
||||||
|
|
||||||
def get_files(self):
|
def get_files(self):
|
||||||
@@ -3611,7 +3688,9 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
|||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
temp_file = ContentFile(b"", name=self.get_archive_name(request))
|
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()
|
files = self.get_files()
|
||||||
for file_ in files:
|
for file_ in files:
|
||||||
path = os.path.split(file_.name)[-1]
|
path = os.path.split(file_.name)[-1]
|
||||||
@@ -3620,24 +3699,35 @@ class SeriesImagesZipViewBase(SuperuserRequiredMixin, View):
|
|||||||
file_size = temp_file.tell()
|
file_size = temp_file.tell()
|
||||||
temp_file.seek(0)
|
temp_file.seek(0)
|
||||||
|
|
||||||
response = HttpResponse(temp_file, content_type='application/zip')
|
response = HttpResponse(temp_file, content_type="application/zip")
|
||||||
response['Content-Disposition'] = 'attachment; filename=%s' % self.get_archive_name(request)
|
response["Content-Disposition"] = (
|
||||||
response['Content-Length'] = file_size
|
"attachment; filename=%s" % self.get_archive_name(request)
|
||||||
|
)
|
||||||
|
response["Content-Length"] = file_size
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ExamGroupsUpdateBase(
|
class ExamGroupsUpdateBase(
|
||||||
RevisionMixin, CheckCanEditMixin, LoginRequiredMixin, AuthorRequiredMixin, UpdateView
|
RevisionMixin,
|
||||||
|
CheckCanEditMixin,
|
||||||
|
LoginRequiredMixin,
|
||||||
|
AuthorRequiredMixin,
|
||||||
|
UpdateView,
|
||||||
):
|
):
|
||||||
template_name = "generic/exam_groups_edit.html"
|
template_name = "generic/exam_groups_edit.html"
|
||||||
|
|
||||||
def get_success_url(self) -> str:
|
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):
|
def get_form_kwargs(self):
|
||||||
kwargs = super(ExamGroupsUpdateBase, self).get_form_kwargs()
|
kwargs = super(ExamGroupsUpdateBase, self).get_form_kwargs()
|
||||||
kwargs.update({"user": self.request.user})
|
kwargs.update({"user": self.request.user})
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
|
|
||||||
class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
|
class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
|
||||||
|
|
||||||
def get_form_kwargs(self):
|
def get_form_kwargs(self):
|
||||||
@@ -3661,4 +3751,3 @@ class UpdateQuestionMixin(RedirectMixin, RevisionMixin, UpdateView):
|
|||||||
if self.request.user in obj.get_author_objects():
|
if self.request.user in obj.get_author_objects():
|
||||||
return context
|
return context
|
||||||
raise PermissionDenied() # or Http404
|
raise PermissionDenied() # or Http404
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user