diff --git a/atlas/models.py b/atlas/models.py
index 280856f3..9b08d44c 100644
--- a/atlas/models.py
+++ b/atlas/models.py
@@ -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)
diff --git a/atlas/templates/atlas/collection_case_view_take.html b/atlas/templates/atlas/collection_case_view_take.html
index 32e19b54..71b7e5e6 100644
--- a/atlas/templates/atlas/collection_case_view_take.html
+++ b/atlas/templates/atlas/collection_case_view_take.html
@@ -7,6 +7,12 @@
: {{case.title}}
{% endif %}
+
+ {% if collection.publish_results or cid_user_exam.completed %}
+ review
+ {% endif %}
+
+
{% if show_description and case.description %}
@@ -72,7 +78,7 @@
+ {% endif %}
+
+ {% endif %}
+
{% endblock %}
diff --git a/atlas/templates/atlas/collection_take_start.html b/atlas/templates/atlas/collection_take_start.html
new file mode 100644
index 00000000..78497790
--- /dev/null
+++ b/atlas/templates/atlas/collection_take_start.html
@@ -0,0 +1,61 @@
+{% extends 'atlas/base.html' %}
+
+{% block content %}
+ Start: {{collection.name}}
+
+ {% if request.user.is_authenticated and valid_user %}
+ User: {{request.user}}
+
+ {% if cid_exam %}
+ Started: {{cid_exam.start_time}}
+ {% endif %}
+
+
+
+
+
+ {% else %}
+ Enter your CID and passcode in the below boxes.
+
+
+
+
+
+
+ {% endif %}
+
+{% endblock %}
diff --git a/atlas/urls.py b/atlas/urls.py
index d7f7d01a..97895b66 100755
--- a/atlas/urls.py
+++ b/atlas/urls.py
@@ -34,7 +34,11 @@ urlpatterns = [
name="collection_update",
),
path("collection/", views.collection_detail, name="collection_detail"),
- path("collection//take", views.collection_take, name="collection_take"),
+ path(
+ "collection//take",
+ views.collection_take_start,
+ name="collection_take_start",
+ ),
path(
"collection//cids",
views.GenericExamViews.exam_cids,
@@ -95,11 +99,6 @@ urlpatterns = [
views.collection_case_view_take_user,
name="collection_case_view_take_user",
),
- path(
- "collection///review",
- views.collection_case_view_review,
- name="collection_case_view_review",
- ),
path(
"collection//json_edit",
views.GenericExamViews.exam_json_edit,
diff --git a/atlas/views.py b/atlas/views.py
index e14a60f4..1c2f14f7 100755
--- a/atlas/views.py
+++ b/atlas/views.py
@@ -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")
diff --git a/generic/models.py b/generic/models.py
index 0a028a49..4a832679 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -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}"
)