tidy up the marker changes
This commit is contained in:
@@ -195,6 +195,8 @@ class ExamMarkerFormMixin(ModelForm):
|
||||
queryset=User.objects.all(),
|
||||
widget=FilteredSelectMultiple(verbose_name="Markers", is_stacked=False),
|
||||
)
|
||||
self.fields["markers"].required = False
|
||||
|
||||
|
||||
|
||||
class ExaminationForm(ModelForm):
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
{% endif %}
|
||||
|
||||
|
||||
This exam has {{question_number}} questions.
|
||||
<span title="Time per question: {% widthratio exam.time_limit question_number 1 %} seconds">Time limit: {{exam.time_limit}} seconds [{{exam.get_time_limit}}]
|
||||
This exam has {{question_number}} questions.
|
||||
<span title="Time per question: {% widthratio exam.time_limit question_number 1 %} seconds">Time limit: {{exam.time_limit}} seconds [{{exam.get_time_limit}}]
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</span>.
|
||||
Exam mode: {{ exam.exam_mode }}<br />
|
||||
@@ -37,22 +37,33 @@ Exam mode: {{ exam.exam_mode }}<br />
|
||||
Cid candidates: <a href="{{exam.get_cid_edit_url}}">{{candidate_count.0}}</a>, User candidates: <a href="{{exam.get_user_edit_url}}">{{candidate_count.1}}</a><br />
|
||||
{% endif %}
|
||||
|
||||
<details>
|
||||
<summary>Answer management</summary>
|
||||
Markers:
|
||||
{% for marker in exam.markers.all %}
|
||||
{{marker}}{% if not forloop.last %},{% endif %}
|
||||
{% empty %}
|
||||
<span title="Only exam authors will be able to mark.">No additional markers.</span>
|
||||
{% endfor %}
|
||||
<br/>
|
||||
|
||||
<div class="alert alert-danger">
|
||||
<p>Manage answers for this collection. </p>
|
||||
|
||||
Please note these are permanant and cannot be undone.
|
||||
</div>
|
||||
{% if can_edit %}
|
||||
<details>
|
||||
<summary>Answer management</summary>
|
||||
|
||||
<button title="This will clear all user answers and attempts"
|
||||
hx-post="{% url exam.get_app_name|add:':exam_reset_answers' exam.pk %}"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to reset all answers? This action cannot be undone."
|
||||
>Reset all answers</button>
|
||||
|
||||
</details>
|
||||
<div class="alert alert-danger">
|
||||
<p>Manage answers for this collection. </p>
|
||||
|
||||
Please note these are permanant and cannot be undone.
|
||||
</div>
|
||||
|
||||
<button title="This will clear all user answers and attempts"
|
||||
hx-post="{% url exam.get_app_name|add:':exam_reset_answers' exam.pk %}"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Are you sure you want to reset all answers? This action cannot be undone."
|
||||
>Reset all answers</button>
|
||||
|
||||
</details>
|
||||
{% endif %}
|
||||
|
||||
Open access: {{ exam.open_access }}<br />
|
||||
|
||||
@@ -60,16 +71,24 @@ Open access: {{ exam.open_access }}<br />
|
||||
Start date: {{ exam.start_date }}
|
||||
{% endif %}
|
||||
{% if exam.end_date %}
|
||||
/ End date: {{ exam.end_date }}
|
||||
/ End date: {{ exam.end_date }}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<div class="parent-help" title="Click to enable / disable the exam">
|
||||
Exam active: <input type="checkbox" id="exam-active-switch" {% if exam.active %}checked{% endif %} data-posturl="{% url exam.get_app_name|add:':exam_toggle_active' pk=exam.pk %}"> <span class="help-text">[When checked the exam will be available to take in the test system]</span>
|
||||
Exam active: <input type="checkbox"
|
||||
{% if not can_edit %}
|
||||
disabled
|
||||
{% endif %}
|
||||
id="exam-active-switch" {% if exam.active %}checked{% endif %} data-posturl="{% url exam.get_app_name|add:':exam_toggle_active' pk=exam.pk %}"> <span class="help-text">[When checked the exam will be available to take in the test system]</span>
|
||||
</div>
|
||||
{% if exam.exam_mode %}
|
||||
<div class="parent-help" title="Click to enable / disable the exam results">
|
||||
Publish results: <input type="checkbox" id="exam-publish-results-switch" data-posturl="{% url exam.get_app_name|add:':exam_toggle_results_published' pk=exam.pk %}"
|
||||
Publish results: <input type="checkbox"
|
||||
{% if not can_edit %}
|
||||
disabled
|
||||
{% endif %}
|
||||
id="exam-publish-results-switch" data-posturl="{% url exam.get_app_name|add:':exam_toggle_results_published' pk=exam.pk %}"
|
||||
{% if exam.publish_results %}checked{% endif %}> <span class="help-text">[When checked the exam results will be available to users on this site]</span>
|
||||
</div>
|
||||
{% if exam.get_app_name == "anatomy" or exam.get_app_name == "rapids" or exam.get_app_name == "longs" %}
|
||||
|
||||
+39
-15
@@ -427,11 +427,14 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
self.ExtraExamFilter = ExtraExamFilter
|
||||
|
||||
# TODO: these may be better implemented as decorators
|
||||
def check_user_access(self, user: User, exam_id: int = None):
|
||||
def check_user_access(self, user: User, exam_id: int = None, marker=False):
|
||||
"""Check if a user should be able to access a view
|
||||
|
||||
Args:
|
||||
user ([user]): user object
|
||||
exam_id ([int], optional): exam id. Defaults to None.
|
||||
|
||||
marker ([boolean], optional): Allow access if the user is a marker. Defaults to False. Requires that exam_id is set.
|
||||
|
||||
Returns:
|
||||
[boolean]: True if the user has access
|
||||
@@ -440,11 +443,18 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
|
||||
if marker and exam_id is None:
|
||||
# raise error if exam_id is not provided
|
||||
raise ValueError("exam_id must be provided if marker checking for a marker")
|
||||
|
||||
if exam_id is not None:
|
||||
exam = get_object_or_404(self.Exam, pk=exam_id)
|
||||
exam: ExamBase = get_object_or_404(self.Exam, pk=exam_id)
|
||||
if (exam.open_access and exam.active) or user in exam.get_author_objects():
|
||||
return True
|
||||
|
||||
if marker and user in exam.markers.all():
|
||||
return True
|
||||
|
||||
if exam.authors_only:
|
||||
return False
|
||||
|
||||
@@ -487,6 +497,9 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_user_marker_access(self, user: User, exam_id: int = None):
|
||||
return self.check_user_access(user, exam_id, marker=True)
|
||||
|
||||
def check_user_edit_access(self, user, exam_id=None):
|
||||
"""Check if a user should be able to access a view
|
||||
|
||||
@@ -541,7 +554,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
and not user.groups.filter(name="casecollection_checker").exists()
|
||||
):
|
||||
return False
|
||||
return True
|
||||
return False
|
||||
|
||||
@method_decorator(login_required)
|
||||
def exam_toggle_archived(self, request, pk):
|
||||
@@ -830,7 +843,8 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
exam = get_object_or_404(self.Exam.objects.prefetch_related("author"), pk=pk)
|
||||
|
||||
if request.user not in exam.author.all():
|
||||
if not self.check_user_access(request.user, pk):
|
||||
# We also let markers view the exam
|
||||
if not self.check_user_access(request.user, pk, marker=True):
|
||||
raise PermissionDenied
|
||||
|
||||
can_edit = (
|
||||
@@ -866,15 +880,22 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
)
|
||||
elif self.app_name == "longs":
|
||||
questions = (
|
||||
exam.exam_questions.select_related()
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"series",
|
||||
queryset=LongSeries.objects.select_related(
|
||||
"modality", "examination", "plane", "contrast"
|
||||
).prefetch_related(Prefetch("images")),
|
||||
),
|
||||
"author"
|
||||
exam.exam_questions.prefetch_related(
|
||||
#Prefetch(
|
||||
# "series",
|
||||
# queryset=LongSeries.objects.select_related(
|
||||
# "modality", "examination", "plane", "contrast"
|
||||
# ).prefetch_related(Prefetch("images")),
|
||||
#),
|
||||
"author",
|
||||
#"examquestiondetail",
|
||||
#"longsseries",
|
||||
"series",
|
||||
#"seriesdetail",
|
||||
"series__images",
|
||||
"series__plane",
|
||||
"series__examination"
|
||||
|
||||
# to_attr="prefetched_primary_answer"
|
||||
# queryset=self.Answer.objects.filter(),
|
||||
# "series",
|
||||
@@ -1311,7 +1332,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
raise Http404("Packet not in exam mode")
|
||||
|
||||
if request.user not in exam.author.all():
|
||||
if not self.check_user_access(request.user, pk):
|
||||
if not self.check_user_marker_access(request.user, pk):
|
||||
raise PermissionDenied
|
||||
|
||||
if self.app_name == "anatomy":
|
||||
@@ -1443,7 +1464,7 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
exam: ExamBase = get_object_or_404(self.Exam, pk=pk)
|
||||
|
||||
if request.user not in exam.author.all():
|
||||
if not self.check_user_access(request.user, pk):
|
||||
if not self.check_user_access(request.user, pk, marker=True):
|
||||
raise PermissionDenied
|
||||
|
||||
question = exam.get_questions()[sk]
|
||||
@@ -2329,6 +2350,9 @@ class ExamViews(View, LoginRequiredMixin):
|
||||
def exam_stats(self, request, exam_id):
|
||||
exam: ExamBase = get_object_or_404(self.Exam, pk=exam_id)
|
||||
|
||||
if not self.check_user_edit_access(request.user, exam_id):
|
||||
raise PermissionDenied()
|
||||
|
||||
if not exam.exam_mode:
|
||||
raise Http404("Packet not in exam mode")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user