reoorganise atlas modules
This commit is contained in:
+38
-1
@@ -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
@@ -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
@@ -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
@@ -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}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user