add basic (user) history for collections

This commit is contained in:
Ross
2024-09-23 11:49:36 +01:00
parent 5f23134fa6
commit edc9b29233
8 changed files with 222 additions and 21 deletions
+15
View File
@@ -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")
@@ -1,5 +1,6 @@
{% if request.user.is_authenticated %}
<br/>Collection: {{collection.name}}-> <a href="{% url 'atlas:collection_detail' pk=collection.pk %}">Overview</a> /
<a href="{% url 'atlas:collection_history' collection.pk %}">History</a> /
<a href="{% url 'atlas:collection_mark_overview' collection.pk %}">Mark</a> /
<a href="{% url 'atlas:collection_scores_cid' collection.pk %}">Scores</a> /
<a href="{% url 'atlas:exam_cids' collection.pk %}">Candidates</a> /
@@ -0,0 +1,20 @@
{% extends 'atlas/exams.html' %}
{% block content %}
<h2>{{collection.name}}</h2>
<h3>Users</h3>
<ul>
{% for userexam in userexams %}
<li>
<b><a href="{% url 'atlas:collection_history_user' collection.pk userexam.user_user.pk %}">{{userexam.get_user_name}}</a><b><br/>
Completed: {{userexam.completed}}<br/>
Started: {{userexam.start_time}}, Ended: {{userexam.end_time}}<br/>
</li>
{% endfor %}
</ul>
{% endblock %}
@@ -0,0 +1,87 @@
{% extends 'atlas/exams.html' %}
{% block content %}
<h2>Collection: {{collection.name}}</h2>
<h3>User: {{user}}</h3>
<ul>
{% for casedetail, user_answer in user_answers %}
<li class="case">
<h4>{{forloop.counter}} / Case: {{casedetail.case.title}}</h4>
{% if not user_answer %}
<span class="case-not-answered">Case not answered.</span>
{% else %}
<div class="answer-block">
{% for value, user_answer, correct_answer, answer_is_correct, automark in user_answer.get_correct_json_answers %}
{% if not user_answer %}
Not answered
{% else %}
<div class="{% if answer_is_correct %}
correct
{% else %}
incorrect
{% endif %}
{% if automark %}
automark
{% endif %}
">
<h5>{{value.title}}</h5>
{{value.description}}
<div
>
Answer : {{user_answer}}
{% if not answer_is_correct %}
<br/>Correct answer: {{correct_answer}}
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
{% endif %}
</li>
{% endfor %}
</ul>
{% endblock %}
{% block css %}
{{ block.super }}
<style>
.correct {
}
.correct h5::after {
content: '✓';
color: green;
}
.incorrect h5::after {
content: '✗';
color: orange;
}
.incorrect.automark h5::after {
content: '✗';
color: red;
}
.answer-block>div {
padding-bottom: 10px;
}
.case:has(.case-not-answered) {
opacity: 0.5;
}
</style>
{% endblock %}
+1 -1
View File
@@ -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}}
+11
View File
@@ -85,6 +85,17 @@ urlpatterns = [
views.GenericExamViews.exam_cids,
name="exam_cids",
),
path(
"collection/<int:exam_id>/history",
views.collection_history,
name="collection_history",
),
path(
"collection/<int:exam_id>/history/<int:user_pk>/user",
views.collection_history_user,
name="collection_history_user",
),
path(
"collection/<int:pk>/user_status",
views.GenericExamViews.exam_user_status,
+80 -17
View File
@@ -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")
raise Http404("Invalid request")
+7 -3
View File
@@ -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)