continue working on case collections

This commit is contained in:
Ross
2023-07-31 10:25:23 +01:00
parent dd300afd63
commit f268f040e2
15 changed files with 343 additions and 49 deletions
+17
View File
@@ -3,6 +3,7 @@ from django.contrib.admin import widgets
from django.forms import ( from django.forms import (
BaseInlineFormSet, BaseInlineFormSet,
Form, Form,
HiddenInput,
ModelForm, ModelForm,
ModelMultipleChoiceField, ModelMultipleChoiceField,
ModelChoiceField, ModelChoiceField,
@@ -22,6 +23,7 @@ from atlas.models import (
CidReportAnswer, CidReportAnswer,
Differential, Differential,
Finding, Finding,
SelfReview,
Series, Series,
SeriesImage, SeriesImage,
SeriesFinding, SeriesFinding,
@@ -571,3 +573,18 @@ class AddCollectionToCaseForm(Form):
raise ValidationError("Invalid collection") raise ValidationError("Invalid collection")
return collection_set return collection_set
class SelfReviewForm(ModelForm):
class Meta:
model = SelfReview
fields = ["user_exam", "case", "comments", "findings", "interpretation"]
widgets = {"user_exam": HiddenInput(), "case": HiddenInput()}
def __init__(self, *args, **kwargs):
user_exam_id = kwargs.pop("user_exam_id")
case_id = kwargs.pop("case_id")
ModelForm.__init__(self, *args, **kwargs)
super(SelfReviewForm, self).__init__(*args, **kwargs)
+29
View File
@@ -0,0 +1,29 @@
# Generated by Django 4.1.4 on 2023-07-24 11:42
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('generic', '0008_delete_selfreview'),
('atlas', '0006_casecollection_candidates_only_and_more'),
]
operations = [
migrations.CreateModel(
name='SelfReview',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('comments', models.TextField(blank=True, null=True)),
('findings', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(5), django.core.validators.MinValueValidator(1)])),
('interpretation', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(5), django.core.validators.MinValueValidator(1)])),
('review_date', models.DateTimeField(auto_now_add=True)),
('review_update_date', models.DateTimeField(auto_now=True)),
('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='atlas.case')),
('user_exam', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='generic.ciduserexam')),
],
),
]
+45
View File
@@ -578,3 +578,48 @@ class CidReportAnswer(BaseReportAnswer):
class UserReportAnswer(BaseReportAnswer): class UserReportAnswer(BaseReportAnswer):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class SelfReview(models.Model):
"""Holds data regarding a users self review / reflection"""
user_exam = models.ForeignKey(CidUserExam, on_delete=models.CASCADE)
case = models.ForeignKey(Case, on_delete=models.CASCADE)
comments = models.TextField(null=True, blank=True)
findings = models.IntegerField(
null=True, blank=True, validators=[MaxValueValidator(5), MinValueValidator(1)]
)
interpretation = models.IntegerField(
null=True, blank=True, validators=[MaxValueValidator(5), MinValueValidator(1)]
)
review_date = models.DateTimeField(auto_now_add=True)
review_update_date = models.DateTimeField(auto_now=True)
def get_absolute_url(self):
return reverse(
"atlas:collection_take_overview_user", kwargs={"pk": self.user_exam.exam.pk}
)
def get_display_block(self):
html = """<div class='self-feedback-block'>
Comments: <span class="comment">{comments}</span><br/>
Findings: <span class="findings">{findings}</span><br/>
Interpretation: <span class="interpretation">{interpretation}</span><br/>
<span class="date">Date: {date}</span><br/>
</div>
"""
return format_html(
html,
comments=self.comments,
findings=self.findings,
interpretation=self.interpretation,
date=self.review_date,
)
@@ -8,7 +8,7 @@
{% endif %} {% endif %}
{% if collection.publish_results or cid_user_exam.completed %} {% if completed %}
<span class="stamp-white">REVIEW</span> <span class="stamp-white">REVIEW</span>
{% endif %} {% endif %}
@@ -78,16 +78,30 @@
<form method="POST" class="post-form">{% csrf_token %} <form method="POST" class="post-form">{% csrf_token %}
{% if collection.collection_type == "REP" %} {% if collection.collection_type == "REP" %}
<div class="form-contents"> <div class="form-contents">
<fieldset {% if collection.publish_results or cid_user_exam.completed %}disabled="disabled"{% endif %}> <fieldset {% if completed %}disabled="disabled"{% endif %}>
{{form}} {{form}}
</fieldset> </fieldset>
</div> </div>
<p> <p>
{% if collection.publish_results %} {% if completed %}
<h4>Answer score: {{answer.score}}</h4> <h4>Answer score: {{answer.score}}</h4>
Answer feedback: {{answer.feedback|safe}} Answer feedback: {{answer.feedback|safe}}
<br/>
{% if self_review %}
<h4>Self Feedback</h4>
{% for review in self_review %}
{{review.get_display_block}}
{% endfor %}
{% endif %}
{% endif %} {% endif %}
</p> </p>
{% endif %} {% endif %}
{% if previous %} {% if previous %}
<button type="submit" name="previous" class="save btn btn-default">Previous</button> <button type="submit" name="previous" class="save btn btn-default">Previous</button>
@@ -103,6 +117,10 @@
<br /> <br />
<button type="submit" name="finish" class="save btn btn-default">Overview</button> <button type="submit" name="finish" class="save btn btn-default">Overview</button>
<button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button> <button type="submit" id="goto-button" value="test" name="goto" class="hide">goto</button>
{% if collection.publish_results or cid_user_exam.completed %}
<a href="{% url 'atlas:add_self_review' cid_user_exam.id case.id %}">Add self review</a>
{% endif %}
</form> </form>
<style> <style>
label { label {
@@ -8,7 +8,7 @@
<div class="floating-header"> <div class="floating-header">
<a href="{% url 'atlas:exam_update' collection.id %}" title="Edit the Collection">Edit</a> <a href="{% url 'atlas:exam_update' collection.id %}" title="Edit the Collection">Edit</a>
\ <a href="{% url 'atlas:exam_deleted' collection.id %}" title="Delete the Collection">Delete</a> \ <a href="{% url 'atlas:exam_deleted' collection.id %}" title="Delete the Collection">Delete</a>
{% comment %} \ <a href="{% url 'atlas:collection_clone' collection.id %}" title="Clone the Collection">Clone</a> {% endcomment %} \ <a href="{% url 'atlas:exam_clone' collection.id %}" title="Clone the Collection">Clone</a>
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
\ <a href="{% url 'admin:atlas_casecollection_change' collection.id %}" title="Edit the Collection using the admin interface">Admin Edit</a> \ <a href="{% url 'admin:atlas_casecollection_change' collection.id %}" title="Edit the Collection using the admin interface">Admin Edit</a>
{% endif %} {% endif %}
@@ -3,10 +3,26 @@
{% block content %} {% block content %}
<h2>Collection: {{exam.name}}</h2> <h2>Collection: {{exam.name}}</h2>
<ul> {% comment %} <ul>
{% for case in collection.cases.all %} {% for case in collection.cases.all %}
<li><a href="{% url 'atlas:collection_case_view_review' pk=collection.id case_number=forloop.counter0 %}">Case {{forloop.counter}}</a></li> <li><a href="{% url 'atlas:collection_case_view_take_user' pk=collection.id case_number=forloop.counter0 %}">Case {{forloop.counter}}</a></li>
{% endfor %} {% endfor %}
</ul> </ul> {% endcomment %}
User: {{request.user}}<br/>
{% if cid_exam %}
Started: {{cid_exam.start_time}} <br/>
{% if cid_exam.completed %}
Ended: {{cid_exam.end_time}} <br/>
{% endif %}
{% endif %}
<a href="{% url 'atlas:collection_case_view_take_user' pk=collection.pk case_number=0 %}">
{% if cid_exam.completed %}
<button>Review</button>
{% else %}
<button>Start</button>
{% endif %}
</a>
{% endblock %} {% endblock %}
@@ -16,7 +16,7 @@
<div class="sba-finish-list"> <div class="sba-finish-list">
<ul> <ul>
{% for question, answer in question_answer_tuples %} {% for question, answer in question_answer_tuples %}
<li> <span {% if not answer %}class="unanswered"{% endif %}> <li> <span {% if not collection.review_only and not answer %}class="unanswered"{% endif %}>
<a href=" <a href="
{% if cid is not None %} {% if cid is not None %}
{% url 'atlas:collection_case_view_take' pk=collection.id case_number=forloop.counter0 cid=cid passcode=passcode %} {% url 'atlas:collection_case_view_take' pk=collection.id case_number=forloop.counter0 cid=cid passcode=passcode %}
@@ -27,11 +27,13 @@
Case: {{forloop.counter}} Case: {{forloop.counter}}
</a> </a>
<br/> <br/>
{% if not collection.review_only %}
{% if not answer %} {% if not answer %}
No answer No answer
{% else %} {% else %}
{{answer.answer}} {{answer.answer}}
{% endif %} {% endif %}
{% endif %}
</span> </span>
</li> </li>
{% endfor %} {% endfor %}
@@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<h2>Add Self Review</h2>
<div class="alert alert-info" role="alert">
Adding self feedback for {{user_exam.exam}}/{{case}}
</div>
<form action="" method="post">
{% csrf_token %}
{{ form.non_field_errors }}
{{ form.as_p }}
{% comment %} <div class="fieldWrapper">
{{ form.content_type.errors }}
{{ form.content_type }}
</div>
<div class="fieldWrapper">
{{ form.object_id.errors }}
{{ form.object_id }}
</div>
<div class="fieldWrapper">
{{ form.note_type.errors }}
<label for="{{ form.note_type.id_for_label }}">Note type</label>
{{ form.note_type }}
</div>
<div class="fieldWrapper">
{{ form.note.errors }}
<label for="{{ form.note.id_for_label }}">Additional information</label><br/>
{{ form.note }}
</div> {% endcomment %}
<input type="submit" value="Submit">
</form>
{% endblock %}
+6
View File
@@ -14,6 +14,11 @@ urlpatterns = [
RedirectView.as_view(pattern_name="atlas:case_create", permanent=False), RedirectView.as_view(pattern_name="atlas:case_create", permanent=False),
name="create", name="create",
), ),
path(
"add_self_review/<int:user_exam_id>/<int:case_id>",
views.AddSelfReview.as_view(),
name="add_self_review",
),
path("author/<int:pk>/", views.author_detail, name="author_detail"), path("author/<int:pk>/", views.author_detail, name="author_detail"),
path("author/", views.author_list, name="author_list"), path("author/", views.author_list, name="author_list"),
path("case/", views.CaseView.as_view(), name="case_view"), path("case/", views.CaseView.as_view(), name="case_view"),
@@ -23,6 +28,7 @@ urlpatterns = [
views.CaseCollectionCreate.as_view(), views.CaseCollectionCreate.as_view(),
name="exam_create", name="exam_create",
), ),
path("collection/<int:exam_id>/clone", views.CollectionClone.as_view(), name="exam_clone"),
path( path(
"collection/<int:pk>/delete", "collection/<int:pk>/delete",
views.CaseCollectionDelete.as_view(), views.CaseCollectionDelete.as_view(),
+78 -9
View File
@@ -27,7 +27,7 @@ from django.urls import reverse_lazy, reverse
from django.http import Http404, JsonResponse from django.http import Http404, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from generic.models import CidUser from generic.models import CidUser, CidUserExam
from .forms import ( from .forms import (
AddCollectionToCaseForm, AddCollectionToCaseForm,
@@ -38,6 +38,7 @@ from .forms import (
CidReportAnswerMarkForm, CidReportAnswerMarkForm,
ConditionForm, ConditionForm,
FindingForm, FindingForm,
SelfReviewForm,
SeriesForm, SeriesForm,
SeriesImageFormSet, SeriesImageFormSet,
SeriesFormSet, SeriesFormSet,
@@ -54,6 +55,7 @@ from .models import (
Condition, Condition,
PathologicalProcess, PathologicalProcess,
Presentation, Presentation,
SelfReview,
Series, Series,
Examination, Examination,
Finding, Finding,
@@ -1114,7 +1116,7 @@ def collection_take_start(request, pk):
valid_user = (collection.check_logged_in_user(request),) valid_user = (collection.check_logged_in_user(request),)
cid_exam = None cid_exam = None
if valid_user: if collection.collection_type == "REV" or valid_user:
cid_exam = collection.get_cid_user_exams(user_user=request.user).first() cid_exam = collection.get_cid_user_exams(user_user=request.user).first()
template_variables = { template_variables = {
@@ -1372,14 +1374,13 @@ def collection_case_view_take(
collection = get_object_or_404(CaseCollection, pk=pk) collection = get_object_or_404(CaseCollection, pk=pk)
form = None form = None
answer: CidReportAnswer | UserReportAnswer answer: None | CidReportAnswer | UserReportAnswer = None
collection.check_user_can_take(cid, passcode, request) collection.check_user_can_take(cid, passcode, request)
cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related() cases = collection.cases.all().order_by("casedetail__sort_order").prefetch_related()
case = cases[case_number] case = cases[case_number]
if not collection.review_only():
cid_user_exam = collection.get_or_create_cid_user_exam( cid_user_exam = collection.get_or_create_cid_user_exam(
cid=cid, user_user=request.user cid=cid, user_user=request.user
) )
@@ -1388,6 +1389,9 @@ def collection_case_view_take(
cid_user_exam.start_time = timezone.now() cid_user_exam.start_time = timezone.now()
cid_user_exam.save() cid_user_exam.save()
if not collection.review_only():
if collection.collection_type == "REP": if collection.collection_type == "REP":
case_detail = CaseDetail.objects.get(case=case, collection=collection) case_detail = CaseDetail.objects.get(case=case, collection=collection)
@@ -1429,10 +1433,6 @@ def collection_case_view_take(
redirect_url = "atlas:collection_case_view_take_user" redirect_url = "atlas:collection_case_view_take_user"
overview_url = "atlas:collection_take_overview_user" overview_url = "atlas:collection_take_overview_user"
else:
kwargs = {"pk": pk}
redirect_url = "atlas:collection_case_view_take"
if "next" in request.POST: if "next" in request.POST:
return redirect(redirect_url, case_number=case_number + 1, **kwargs) return redirect(redirect_url, case_number=case_number + 1, **kwargs)
elif "previous" in request.POST: elif "previous" in request.POST:
@@ -1457,18 +1457,23 @@ def collection_case_view_take(
# Set what details a user is / is not able to view when taking the case # Set what details a user is / is not able to view when taking the case
# this can be configured via the collection edit menu # this can be configured via the collection edit menu
if collection.publish_results: if collection.publish_results or cid_user_exam.completed:
completed = True
show_title = collection.show_title_post show_title = collection.show_title_post
show_history = collection.show_history_post show_history = collection.show_history_post
show_description = collection.show_description_post show_description = collection.show_description_post
show_discussion = collection.show_discussion_post show_discussion = collection.show_discussion_post
show_report = collection.show_report_post show_report = collection.show_report_post
self_review = cid_user_exam.selfreview_set.filter(case=case)
else: else:
completed = False
show_title = collection.show_title_pre show_title = collection.show_title_pre
show_history = collection.show_history_pre show_history = collection.show_history_pre
show_description = collection.show_description_pre show_description = collection.show_description_pre
show_discussion = collection.show_discussion_pre show_discussion = collection.show_discussion_pre
show_report = collection.show_report_pre show_report = collection.show_report_pre
self_review = []
return render( return render(
request, request,
@@ -1490,6 +1495,8 @@ def collection_case_view_take(
"show_discussion": show_discussion, "show_discussion": show_discussion,
"show_report": show_report, "show_report": show_report,
"cid_user_exam": cid_user_exam, "cid_user_exam": cid_user_exam,
"completed": completed,
"self_review": self_review,
}, },
) )
@@ -1735,3 +1742,65 @@ def collection_scores_cid(request, pk):
GenericExamViews = ExamViews( GenericExamViews = ExamViews(
CaseCollection, Case, None, CidReportAnswer, "atlas", "casecollection" CaseCollection, Case, None, CidReportAnswer, "atlas", "casecollection"
) )
class CollectionCloneMixin:
def get_initial(self):
old_object = get_object_or_404(self.model, pk=self.kwargs["exam_id"])
initial_data = model_to_dict(old_object, exclude=["id"])
# We manually transfer the forign keys / m2m relationships
questions = old_object.cases.all().values_list("id", flat=True)
authors = old_object.author.all().values_list("id", flat=True)
self.cases = list(questions)
self.author = list(authors)
return initial_data
def form_valid(self, form):
object = form.save()
# Reapply these otherwise they get lost?
object.cases.set(self.cases)
object.author.set(self.author)
object.save()
return HttpResponseRedirect(object.get_absolute_url())
class CollectionClone(CollectionCloneMixin, CaseCollectionCreate):
"""Clone exam view"""
class AddSelfReview(CreateView):
model = SelfReview
form_class = SelfReviewForm
# fields = ("author", "note_type", "note")
def get_form_kwargs(self):
kwargs = super(AddSelfReview, self).get_form_kwargs()
# update the kwargs for the form init method with yours
kwargs.update(self.kwargs) # self.kwargs contains all url conf params
#kwargs.update({"user": self.request.user})
return kwargs
def get_initial(self):
user_exam_id = self.kwargs["user_exam_id"]
case_id = self.kwargs["case_id"]
self.user_exam = get_object_or_404(CidUserExam, pk=user_exam_id)
self.case = get_object_or_404(Case, pk=case_id)
return {"user_exam": user_exam_id, "case": case_id}
def get_context_data(self, **kwargs):
# This can also be accessed via view.**** in the template
context = super(AddSelfReview, self).get_context_data(**kwargs)
context["user_exam"] = self.user_exam
context["case"] = self.case
return context
# def form_valid(self, form):
# model = form.save(commit=False)
#
# model.save()
#
# response = super().form_valid(form)
#
# return response
+29
View File
@@ -0,0 +1,29 @@
# Generated by Django 4.1.4 on 2023-07-24 11:15
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('atlas', '0006_casecollection_candidates_only_and_more'),
('generic', '0006_alter_ciduserexam_completed'),
]
operations = [
migrations.CreateModel(
name='SelfReview',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('comments', models.TextField(blank=True, null=True)),
('findings', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(5), django.core.validators.MinValueValidator(1)])),
('interpretation', models.IntegerField(blank=True, null=True, validators=[django.core.validators.MaxValueValidator(5), django.core.validators.MinValueValidator(1)])),
('review_date', models.DateTimeField(auto_now_add=True)),
('review_update_date', models.DateTimeField(auto_now=True)),
('case', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='atlas.case')),
('user_exam', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='generic.ciduserexam')),
],
),
]
@@ -0,0 +1,16 @@
# Generated by Django 4.1.4 on 2023-07-24 11:42
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('generic', '0007_selfreview'),
]
operations = [
migrations.DeleteModel(
name='SelfReview',
),
]
+2
View File
@@ -6,6 +6,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import models from django.db import models
from django.http import Http404, HttpRequest from django.http import Http404, HttpRequest
from django.utils import timezone from django.utils import timezone
from django.core.validators import MaxValueValidator, MinValueValidator
from smtplib import SMTPException from smtplib import SMTPException
from django.urls import reverse from django.urls import reverse
@@ -1315,3 +1316,4 @@ def create_user_profile(sender, instance, created, **kwargs):
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs): def save_user_profile(sender, instance, **kwargs):
instance.userprofile.save() instance.userprofile.save()
+9
View File
@@ -1000,3 +1000,12 @@ h1, h2, h3, h4, h5, h6 {
text-shadow: 0 0 2px white; text-shadow: 0 0 2px white;
box-shadow: 0 0 2px white; box-shadow: 0 0 2px white;
} }
.self-feedback-block {
border: gray 1px dashed;
}
.self-feedback-block.data {
opacity: 60%;
font-size: small;
}