reoorganise atlas modules

This commit is contained in:
Ross
2023-07-24 09:19:08 +01:00
parent 9a5088c03a
commit e6469e70f0
10 changed files with 275 additions and 114 deletions
+38 -1
View File
@@ -1,6 +1,8 @@
import json
import os
import pathlib
from django.http import Http404
from rad.settings import REMOTE_URL
from django.db.models.fields.files import ImageField
from django.db.models.fields.related import ForeignKey
@@ -506,11 +508,46 @@ class CaseCollection(ExamCollectionGenericBase):
return reverse("atlas:collection_detail", kwargs={"pk": self.pk})
def get_take_url(self):
return reverse("atlas:collection_take", kwargs={"pk": self.pk})
return reverse("atlas:collection_take_start", kwargs={"pk": self.pk})
def __str__(self) -> str:
return self.name
def review_only(self) -> bool:
"""Returns True if a users cannot submit responses
Returns:
bool: _description_
"""
if self.collection_type == self.COLLECTION_TYPE_CHOICES.REVIEW:
return True
return False
def check_user_can_take(self, cid, passcode, request):
"""
Helper to check if a user is allowed to access a collection
Args:
cid (_type_): _description_
passcode (_type_): _description_
request (_type_): _description_
Raises:
Http404: If user does not have access
"""
if not self.active:
raise Http404("Exam not found")
#if self.collection_is_review():
# return True
#if not self.self_review:
if not self.check_cid_user(cid, passcode, request):
raise Http404("Error accessing exam")
class CaseDetail(models.Model):
case = models.ForeignKey(Case, on_delete=models.CASCADE)
@@ -7,6 +7,12 @@
: {{case.title}}
{% endif %}
{% if collection.publish_results or cid_user_exam.completed %}
review
{% endif %}
</h2>
{% if show_description and case.description %}
@@ -72,7 +78,7 @@
<form method="POST" class="post-form">{% csrf_token %}
{% if collection.collection_type == "REP" %}
<div class="form-contents">
<fieldset {% if collection.publish_results %}disabled="disabled"{% endif %}>
<fieldset {% if collection.publish_results or cid_user_exam.completed %}disabled="disabled"{% endif %}>
{{form}}
</fieldset>
</div>
@@ -6,6 +6,7 @@
Publish results: {{collection.publish_results}}<br />
Active: {{collection.active}}<br />
Collection Type: {{collection.collection_type}}<br />
Self review: {{collection.self_review}}<br />
</div>
<h3>Cases</h3>
<ul>
@@ -1,53 +0,0 @@
{% extends 'atlas/base.html' %}
{% block content %}
<h2>{{collection.name}}
<h2>Start: {{exam.name}}</h2>
{% if request.user.is_authenticated and valid_user %}
<a href="{% url 'atlas:collection_case_view_take_user' pk=collection.pk case_number=0 %}">Start</a>
{% else %}
Enter your CID and passcode in the below boxes.<br />
<p><input id="cid-box" type="text" value="Candidate ID"></p>
<p><input id="passcode-box" type="text" value="Passcode"></p>
<button>Start</button>
<script type="text/javascript">
$(document).ready(function () {
window.location.search.substr(1).split("&").forEach((item) =>
{
s = item.split("=");
if (s[0] == "cid") {
$("#cid-box").val(s[1]);
}
if (s[0] == "passcode") {
$("#passcode-box").val(s[1]);
}
});
$("#cid-box, #passcode-box").keypress(function(e) {
// Enter pressed?
console.log(e)
if(e.which == 10 || e.which == 13) {
$("button").click();
}
});
$("button").click(() => {
let cid = $("#cid-box").val();
let passcode = $("#passcode-box").val();
if (Number.isInteger(parseInt(cid))) {
window.location.replace("{% url 'atlas:collection_case_view_take' pk=collection.pk case_number=0 cid='0000000' passcode='ZZZZZZ' %}".replace("0000000", cid).replace("ZZZZZZ", passcode));
} else {
alert("Please enter a valid Candidate ID (CID).")
}
});
});
</script>
{% endif %}
{% endblock %}
@@ -5,7 +5,12 @@
CID: {{cid}}
{% endif %}
<h2>Collection: {{collection.name}}</h2>
<h2>Collection: {{collection.name}}
{% if collection.publish_results or cid_user_exam.completed %}
review
{% endif %}
</h2>
<div><p>Questions</p></div>
{{answer_count}} out of {{collection_length}} cases answered. Click to go to case.
@@ -31,7 +36,25 @@
Start time: {{cid_user_exam.start_time}}
<p>Completed: {{cid_user_exam.completed}}</p>
{% if collection.self_review %}
<p>Completed: <span id="completed-state">{{cid_user_exam.completed}}</span></p>
{% if not cid_user_exam.completed %}
<form hx-post=""
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'
hx-delete
hx-confirm="Finish session?"
hx-target="#completed-state"
_="on htmx:afterOnLoad remove me"
>
<button type="submit" name="finish" value="finish">Finish</button>
</form>
{% endif %}
{% endif %}
{% endblock %}
@@ -0,0 +1,61 @@
{% extends 'atlas/base.html' %}
{% block content %}
<h2>Start: {{collection.name}}</h2>
{% if request.user.is_authenticated and valid_user %}
User: {{request.user}}<br/>
{% if cid_exam %}
Started: {{cid_exam.start_time}} <br/>
{% endif %}
<a href="{% url 'atlas:collection_case_view_take_user' pk=collection.pk case_number=0 %}">
<button>Start</button>
</a>
{% else %}
Enter your CID and passcode in the below boxes.<br />
<p><input id="cid-box" type="text" value="Candidate ID"></p>
<p><input id="passcode-box" type="text" value="Passcode"></p>
<button>Start</button>
<script type="text/javascript">
$(document).ready(function () {
window.location.search.substr(1).split("&").forEach((item) =>
{
s = item.split("=");
if (s[0] == "cid") {
$("#cid-box").val(s[1]);
}
if (s[0] == "passcode") {
$("#passcode-box").val(s[1]);
}
});
$("#cid-box, #passcode-box").keypress(function(e) {
// Enter pressed?
console.log(e)
if(e.which == 10 || e.which == 13) {
$("button").click();
}
});
$("button").click(() => {
let cid = $("#cid-box").val();
let passcode = $("#passcode-box").val();
if (Number.isInteger(parseInt(cid))) {
window.location.replace("{% url 'atlas:collection_case_view_take' pk=collection.pk case_number=0 cid='0000000' passcode='ZZZZZZ' %}".replace("0000000", cid).replace("ZZZZZZ", passcode));
} else {
alert("Please enter a valid Candidate ID (CID).")
}
});
});
</script>
{% endif %}
{% endblock %}
+5 -6
View File
@@ -34,7 +34,11 @@ urlpatterns = [
name="collection_update",
),
path("collection/<int:pk>", views.collection_detail, name="collection_detail"),
path("collection/<int:pk>/take", views.collection_take, name="collection_take"),
path(
"collection/<int:pk>/take",
views.collection_take_start,
name="collection_take_start",
),
path(
"collection/<int:exam_id>/cids",
views.GenericExamViews.exam_cids,
@@ -95,11 +99,6 @@ urlpatterns = [
views.collection_case_view_take_user,
name="collection_case_view_take_user",
),
path(
"collection/<int:pk>/<int:case_number>/review",
views.collection_case_view_review,
name="collection_case_view_review",
),
path(
"collection/<int:pk>/json_edit",
views.GenericExamViews.exam_json_edit,
+118 -48
View File
@@ -1096,19 +1096,39 @@ def collection_detail(request, pk):
)
def collection_take(request, pk):
def collection_take_start(request, pk):
"""The starting page for taking / reviewing a case collection
Args:
request (_type_): _description_
pk (_type_): _description_
Returns:
_type_: _description_
"""
collection = get_object_or_404(CaseCollection, pk=pk)
valid_user = (collection.check_logged_in_user(request),)
cid_exam = None
if valid_user:
cid_exam = collection.get_cid_user_exams(user_user=request.user).first()
template_variables = {
"collection": collection,
"valid_user": collection.check_logged_in_user(request),
"valid_user": valid_user,
"cid_exam": cid_exam,
}
match collection.collection_type:
case "REP":
return render(request, "atlas/collection_take.html", template_variables)
return render(
request, "atlas/collection_take_start.html", template_variables
)
case "REV":
return render(request, "atlas/collection_review.html", template_variables)
return render(
request, "atlas/collection_review_start.html", template_variables
)
@user_is_collection_author_or_atlas_editor
@@ -1253,13 +1273,36 @@ def collection_take_overview_user(request, pk):
def collection_take_overview(
request, pk: int, cid: int | None = None, passcode: str | None = None
):
"""
Summary page that shows which questions a user has attempted
(with / without answers)
Args:
request (_type_): _description_
pk (int): _description_
cid (int | None, optional): _description_. Defaults to None.
passcode (str | None, optional): _description_. Defaults to None.
Returns:
_type_: _description_
"""
collection = get_object_or_404(CaseCollection, pk=pk)
if not collection.active and not collection.self_review:
raise Http404("Exam not found")
collection.check_user_can_take(cid, passcode, request)
if not collection.check_cid_user(cid, passcode, request):
raise Http404("Error accessing exam")
cid_user_exam = collection.get_or_create_cid_user_exam(
cid=cid, user_user=request.user
)
if request.htmx:
if "finish" in request.POST:
cid_user_exam.completed = True
cid_user_exam.save()
return HttpResponse("True")
else:
raise Http404()
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
@@ -1285,10 +1328,6 @@ def collection_take_overview(
else:
question_answer_tuples.append((q, None))
cid_user_exam = collection.get_or_create_cid_user_exam(
cid=cid, user_user=request.user
)
return render(
request,
"atlas/collection_take_overview.html",
@@ -1304,10 +1343,6 @@ def collection_take_overview(
)
def collection_case_view_review(request, pk: int, case_number: int):
return collection_case_view_take(request, pk, case_number, None, None)
def collection_case_view_take_user(request, pk: int, case_number: int):
return collection_case_view_take(request, pk, case_number)
@@ -1315,54 +1350,72 @@ def collection_case_view_take_user(request, pk: int, case_number: int):
def collection_case_view_take(
request, pk: int, case_number: int, cid=None, passcode=None
):
"""
Displays the page from which the user takes a case in the collection
Args:
request (_type_): _description_
pk (int): _description_
case_number (int): _description_
cid (_type_, optional): _description_. Defaults to None.
passcode (_type_, optional): _description_. Defaults to None.
Raises:
Http404: If user does not have access
Returns:
_type_: _description_
"""
collection = get_object_or_404(CaseCollection, pk=pk)
form = None
answer: CidReportAnswer | UserReportAnswer
if collection.collection_type != collection.COLLECTION_TYPE_CHOICES.REVIEW:
if not collection.active and not collection.self_review:
raise Http404("Exam not found")
if cid is not None and not collection.check_cid_user(cid, passcode, request):
raise Http404("Error accessing exam")
cid_user_exam = collection.get_or_create_cid_user_exam(
cid=cid, user_user=request.user
)
collection.check_user_can_take(cid, passcode, request)
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
case = cases[case_number]
if collection.collection_type == "REP":
if not collection.review_only():
cid_user_exam = collection.get_or_create_cid_user_exam(
cid=cid, user_user=request.user
)
case_detail = CaseDetail.objects.get(case=case, collection=collection)
if cid is not None:
answer = case_detail.cidreportanswer_set.filter(cid=cid).first()
ReportAnswerForm = CidReportAnswerForm
else:
answer = case_detail.userreportanswer_set.filter(user=request.user).first()
ReportAnswerForm = UserReportAnswerForm
if cid_user_exam.start_time is None:
cid_user_exam.start_time = timezone.now()
cid_user_exam.save()
if collection.collection_type == "REP":
case_detail = CaseDetail.objects.get(case=case, collection=collection)
if cid is not None:
answer = case_detail.cidreportanswer_set.filter(cid=cid).first()
ReportAnswerForm = CidReportAnswerForm
else:
answer = case_detail.userreportanswer_set.filter(
user=request.user
).first()
ReportAnswerForm = UserReportAnswerForm
if request.method == "POST":
if collection.collection_type == "REP":
if not collection.publish_results:
if answer:
form = ReportAnswerForm(request.POST, instance=answer)
else:
form = ReportAnswerForm(request.POST)
if form.is_valid():
answer = form.save(commit=False)
answer.set_cid_or_user(cid=cid, user=request.user)
answer.question = case_detail
# answer.published_date = timezone.now()
answer.save()
cid_user_exam.end_time = timezone.now()
cid_user_exam.save()
if not cid_user_exam.completed:
if form.is_valid():
answer = form.save(commit=False)
answer.set_cid_or_user(cid=cid, user=request.user)
answer.question = case_detail
# answer.published_date = timezone.now()
answer.save()
cid_user_exam.end_time = timezone.now()
cid_user_exam.save()
if cid is not None:
kwargs = {"pk": pk, "cid": cid, "passcode": passcode}
@@ -1375,7 +1428,7 @@ def collection_case_view_take(
else:
kwargs = {"pk": pk}
redirect_url = "atlas:collection_case_view_review"
redirect_url = "atlas:collection_case_view_take"
if "next" in request.POST:
return redirect(redirect_url, case_number=case_number + 1, **kwargs)
@@ -1399,6 +1452,8 @@ def collection_case_view_take(
previous = case_number > 0
next = case_number < (len(cases) - 1)
# Set what details a user is / is not able to view when taking the case
# this can be configured via the collection edit menu
if collection.publish_results:
show_title = collection.show_title_post
show_history = collection.show_history_post
@@ -1431,23 +1486,38 @@ def collection_case_view_take(
"show_description": show_description,
"show_discussion": show_discussion,
"show_report": show_report,
"cid_user_exam": cid_user_exam,
},
)
@user_is_collection_author_or_atlas_editor
def collection_case_view(request, pk, case_number):
"""Edit/management overview of the case (as part of the collection)
Args:
request (_type_): _description_
pk (_type_): _description_
case_number (_type_): _description_
Raises:
Http404: _description_
Returns:
_type_: _description_
"""
collection = get_object_or_404(CaseCollection, pk=pk)
form = None
answer = None
if collection.collection_type != collection.COLLECTION_TYPE_CHOICES.REVIEW:
if not collection.active and not collection.self_review:
raise Http404("Exam not found")
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
case = cases[case_number]
try:
case = cases[case_number]
except IndexError: # Catch an invalid case_number
s = f"Invalid case number: {case_number}"
raise Http404(s)
series_list = case.series.all().prefetch_related("images", "examination", "plane")
+20 -3
View File
@@ -503,7 +503,9 @@ class ExamCollectionGenericBase(models.Model):
new.save()
return new
def get_cid_user_exams(self, cid_user=None, user_user=None):
def get_cid_user_exams(
self, cid_user: "CidUser" = None, user_user: User = None
) -> "CidUserExam":
content_type = ContentType.objects.get_for_model(self)
if cid_user is None and user_user is None:
return CidUserExam.objects.filter(
@@ -1071,11 +1073,26 @@ class CidUserExam(models.Model):
def __str__(self) -> str:
if self.cid_user is None:
user = self.user_user.username
try:
user = self.user_user.username
except AttributeError:
user = "None"
else:
user = self.cid_user.cid
if self.start_time is None:
start_time = ""
else:
start_time = f"{ self.start_time:%Y-%m-%d %H:%M }"
if self.end_time is None:
end_time = ""
else:
end_time = f"{ self.end_time:%Y-%m-%d %H:%M }"
return (
f"{user}: {self.start_time:%Y-%m-%d %H:%M} {self.end_time:%Y-%m-%d %H:%M}"
f"{user}: {start_time} {end_time}"
)