diff --git a/atlas/models.py b/atlas/models.py
index f20a08cf..9b0d54d8 100644
--- a/atlas/models.py
+++ b/atlas/models.py
@@ -555,6 +555,14 @@ class SeriesImage(SeriesImageBase):
help_text="Reference to the object that has replaced this one.",
)
+ def get_file_size(self):
+ try:
+ return self.image.size
+
+ # We catch this for when the image does not exist
+ except FileNotFoundError:
+ return 0
+
def get_dicom_data(self):
try:
with pydicom.dcmread(self.image) as d:
@@ -1066,6 +1074,13 @@ class CaseDetail(models.Model):
"""Returns the correct question answers as a json string"""
return json.dumps(self.question_answers)
+ def get_user_answers(self, user):
+ """Returns the users answers as a json string"""
+ try:
+ return UserReportAnswer.objects.get(question=self, user=user)
+ except UserReportAnswer.DoesNotExist:
+ return None
+
class CasePrior(models.Model):
case_detail = models.ForeignKey(CaseDetail, on_delete=models.CASCADE)
prior_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name="prior_case")
diff --git a/atlas/templates/atlas/collection_headers.html b/atlas/templates/atlas/collection_headers.html
index d3d443ed..890e00f1 100644
--- a/atlas/templates/atlas/collection_headers.html
+++ b/atlas/templates/atlas/collection_headers.html
@@ -1,5 +1,6 @@
{% if request.user.is_authenticated %}
Collection: {{collection.name}}-> Overview /
+ History /
Mark /
Scores /
Candidates /
diff --git a/atlas/templates/atlas/collection_history.html b/atlas/templates/atlas/collection_history.html
new file mode 100644
index 00000000..91abe2a0
--- /dev/null
+++ b/atlas/templates/atlas/collection_history.html
@@ -0,0 +1,20 @@
+{% extends 'atlas/exams.html' %}
+
+{% block content %}
+
{{collection.name}}
+
+ Users
+
+
+ {% for userexam in userexams %}
+ -
+ {{userexam.get_user_name}}
+ Completed: {{userexam.completed}}
+ Started: {{userexam.start_time}}, Ended: {{userexam.end_time}}
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/atlas/templates/atlas/collection_history_user.html b/atlas/templates/atlas/collection_history_user.html
new file mode 100644
index 00000000..df23dd55
--- /dev/null
+++ b/atlas/templates/atlas/collection_history_user.html
@@ -0,0 +1,87 @@
+{% extends 'atlas/exams.html' %}
+
+{% block content %}
+ Collection: {{collection.name}}
+
+ User: {{user}}
+
+
+ {% for casedetail, user_answer in user_answers %}
+ -
+
+
{{forloop.counter}} / Case: {{casedetail.case.title}}
+ {% if not user_answer %}
+ Case not answered.
+ {% else %}
+
+ {% for value, user_answer, correct_answer, answer_is_correct, automark in user_answer.get_correct_json_answers %}
+ {% if not user_answer %}
+ Not answered
+ {% else %}
+
+
{{value.title}}
+ {{value.description}}
+
+ Answer : {{user_answer}}
+
+ {% if not answer_is_correct %}
+
Correct answer: {{correct_answer}}
+ {% endif %}
+
+
+
+ {% endif %}
+
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
+
+{% block css %}
+ {{ block.super }}
+
+{% endblock %}
\ No newline at end of file
diff --git a/atlas/templates/atlas/series_viewer.html b/atlas/templates/atlas/series_viewer.html
index 357a6f31..ab0a3b2b 100755
--- a/atlas/templates/atlas/series_viewer.html
+++ b/atlas/templates/atlas/series_viewer.html
@@ -102,7 +102,7 @@
{% if image.image %}
- [{{image.image.size|filesizeformat}}]
+ [{{image.get_file_size|filesizeformat}}]
{% endif %}
{{image.image_md5_hash}} ({{image.is_dicom}}) {{image.image_blake3_hash}}
diff --git a/atlas/urls.py b/atlas/urls.py
index 34fe22a8..46c51482 100755
--- a/atlas/urls.py
+++ b/atlas/urls.py
@@ -85,6 +85,17 @@ urlpatterns = [
views.GenericExamViews.exam_cids,
name="exam_cids",
),
+ path(
+ "collection//history",
+ views.collection_history,
+ name="collection_history",
+ ),
+ path(
+ "collection//history//user",
+ views.collection_history_user,
+ name="collection_history_user",
+ ),
+
path(
"collection//user_status",
views.GenericExamViews.exam_user_status,
diff --git a/atlas/views.py b/atlas/views.py
index 3cfe7893..9295e109 100755
--- a/atlas/views.py
+++ b/atlas/views.py
@@ -246,6 +246,7 @@ def question_schema_detail(request, pk: int):
{"question_schema": question_schema, "example_form": example_form},
)
+
@login_required
# @user_is_atlas_editor
def question_schema_schemas(request):
@@ -259,6 +260,7 @@ def question_schema_schemas(request):
},
)
+
@login_required
# @user_is_atlas_editor
def condition_detail(request, pk):
@@ -857,14 +859,17 @@ class QuestionSchemaCreate(LoginRequiredMixin, CreateView):
model = QuestionSchema
form_class = QuestionSchemaForm
+
class QuestionSchemaUpdate(LoginRequiredMixin, UpdateView):
model = QuestionSchema
form_class = QuestionSchemaForm
+
class QuestionSchemaDelete(AuthorOrCheckerRequiredMixin, DeleteView):
model = QuestionSchema
template_name = "confirm_delete.html"
+
class FindingUpdate(RevisionMixin, LoginRequiredMixin, UpdateView):
model = Finding
form_class = FindingForm
@@ -1272,7 +1277,6 @@ def series_order_dicom(request, pk):
@login_required
def image_diff(request):
if request.htmx:
-
image_ids = request.POST.getlist("image-id")
if len(image_ids) == 2:
@@ -1596,6 +1600,58 @@ def collection_detail(request, pk):
)
+@user_is_collection_author_or_atlas_editor
+def collection_history(request, exam_id: int):
+ """An overview page that shows who has attempted / finised the collection"""
+ collection = get_object_or_404(CaseCollection, pk=exam_id)
+
+ casedetails = CaseDetail.objects.filter(collection=collection).order_by(
+ "sort_order"
+ )
+
+ userexams = collection.get_cid_user_exams()
+
+ print(userexams)
+
+ return render(
+ request,
+ "atlas/collection_history.html",
+ {
+ "collection": collection,
+ "casesdetails": casedetails,
+ "can_edit": True,
+ "userexams": userexams,
+ },
+ )
+
+
+@user_is_collection_author_or_atlas_editor
+def collection_history_user(request, exam_id: int, user_pk: int):
+ collection = get_object_or_404(CaseCollection, pk=exam_id)
+
+ user = get_object_or_404(User, pk=user_pk)
+
+ casedetails = CaseDetail.objects.filter(
+ collection=collection,
+ ).order_by("sort_order")
+
+ print(user)
+ print(casedetails)
+
+ user_answers = [(casedetail, casedetail.get_user_answers(user)) for casedetail in casedetails]
+
+ return render(
+ request,
+ "atlas/collection_history_user.html",
+ {
+ "collection": collection,
+ "user": user,
+ "casedetails": casedetails,
+ "user_answers": user_answers,
+ },
+ )
+
+
def collection_take_start(request, pk):
"""The starting page for taking / reviewing a case collection
@@ -1630,23 +1686,29 @@ def collection_take_start(request, pk):
request, "atlas/collection_review_start.html", template_variables
)
+
@user_is_collection_author_or_atlas_editor
def collection_case_priors(request, exam_id, case_id):
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
-
if request.htmx:
if "remove" in request.POST:
- p = CasePrior.objects.get(case_detail=case_detail, prior_case=request.POST["remove"])
+ p = CasePrior.objects.get(
+ case_detail=case_detail, prior_case=request.POST["remove"]
+ )
p.delete()
return HttpResponse(f"Case removed")
elif "prior_case_id" in request.POST:
prior_case = Case.objects.get(pk=request.POST["prior_case_id"])
- p, created = CasePrior.objects.get_or_create(case_detail=case_detail, prior_case=prior_case)
- p.relation_text=request.POST["relation"]
+ p, created = CasePrior.objects.get_or_create(
+ case_detail=case_detail, prior_case=prior_case
+ )
+ p.relation_text = request.POST["relation"]
if not p.relation_text:
- return HttpResponse("You need to enter text to describe the relationship between the cases")
+ return HttpResponse(
+ "You need to enter text to describe the relationship between the cases"
+ )
p.save()
@@ -1674,7 +1736,6 @@ def collection_case_priors(request, exam_id, case_id):
else:
available_priors.append((case, False, ""))
-
form = PriorCaseForm()
case_number, case_count = collection.get_index_of_case(
@@ -1684,14 +1745,13 @@ def collection_case_priors(request, exam_id, case_id):
previous = collection.get_previous_case(case_detail.case)
next = collection.get_next_case(case_detail.case)
-
return render(
request,
"atlas/collection_case_priors.html",
{
"case_detail": case_detail,
"form": form,
- #"example_form": example_form,
+ # "example_form": example_form,
"collection": collection,
"case": case_detail.case,
"previous": previous,
@@ -1703,6 +1763,7 @@ def collection_case_priors(request, exam_id, case_id):
},
)
+
@user_is_collection_author_or_atlas_editor
def collection_case_details(request, exam_id, case_id):
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
@@ -1713,7 +1774,6 @@ def collection_case_details(request, exam_id, case_id):
raise Http404("Collection not in question mode")
if request.method == "POST":
-
# Called if the user saves the correct answer form
if request.POST.get("submit") == "answer":
# answers = request.POST
@@ -1986,7 +2046,9 @@ def collection_take_overview(
cid_user_exam.save()
collection.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",
)
return HttpResponse("True")
@@ -2012,7 +2074,9 @@ def collection_take_overview(
answer_count = 0
for cd in case_details:
self_review = cid_user_exam.selfreview_set.filter(case=cd.case)
- if cd in answer_question_map and (answer_question_map[cd].answer or answer_question_map[cd].json_answer):
+ if cd in answer_question_map and (
+ answer_question_map[cd].answer or answer_question_map[cd].json_answer
+ ):
question_answer_tuples.append((cd, answer_question_map[cd], self_review))
answer_count += 1
else:
@@ -2192,8 +2256,6 @@ def collection_case_view_take(
for series in prior.prior_case.get_series():
series_to_load.append((series, True, prior.relation_text))
-
-
previous = case_number > 0
next = case_number < (case_count - 1)
@@ -2317,6 +2379,7 @@ def use_dates_as_descriptions(request, pk):
return HttpResponse("Done")
+
def collection_case_dicom_json(request, exam_id, case_id):
case_detail = CaseDetail.objects.get(case=case_id, collection=exam_id)
@@ -2715,6 +2778,7 @@ class CaseCollectionAuthorUpdate(RevisionMixin, AuthorRequiredMixin, UpdateView)
context["collection"] = context["object"]
return context
+
class ExamGroupsUpdate(ExamGroupsUpdateBase):
model = CaseCollection
form_class = ExamGroupsForm
@@ -2819,6 +2883,7 @@ def collection_question_schemas(request, exam_id: int):
},
)
+
def collection_reset_answers(request, exam_id: int):
if request.htmx:
collection = get_object_or_404(CaseCollection, pk=exam_id)
@@ -2834,14 +2899,12 @@ def collection_reset_answers(request, exam_id: int):
user_answers = case.userreportanswer_set.all()
user_answers.delete()
-
# User statuses
collection.exam_user_status.all().delete()
# CidUserExams
collection.cid_users.all().delete()
return HttpResponse("Success")
-
else:
- raise Http404("Invalid request")
\ No newline at end of file
+ raise Http404("Invalid request")
diff --git a/generic/models.py b/generic/models.py
index 8098247b..edbbb17e 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -531,7 +531,7 @@ class SeriesBase(models.Model):
size = 0
for i in images:
- size += i.image.size
+ size += i.get_file_size()
return size
@@ -997,7 +997,6 @@ class ExamBase(ExamOrCollectionGenericBase):
return User.objects.filter(pk__in=user_ids)
-
def generate_user_report(self, user):
exam_text = [f"Candidate {user.first_name} [{user.email}]"]
@@ -1478,6 +1477,12 @@ class CidUserExam(models.Model):
return f"{self.exam} / {user}: {start_time} {end_time}"
+ def get_user_name(self) -> str:
+ if self.cid_user is None:
+ return self.user_user.username
+ else:
+ return "CID" + str(self.cid_user.cid)
+
CID_GROUP_EXAMS = (
("SBAs", "sba_cid_user_groups"),
@@ -1504,7 +1509,6 @@ class BaseUserGroup(models.Model):
class CidUserGroup(BaseUserGroup):
-
def __str__(self) -> str:
return str(self.name)