diff --git a/atlas/forms.py b/atlas/forms.py index 0898438c..f3b567d2 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -1062,6 +1062,14 @@ class AddCollectionToCaseForm(Form): class SelfReviewForm(ModelForm): + SCORE_CHOICES = [ + (1, "1 - Poor"), + (2, "2 - Limited"), + (3, "3 - Adequate"), + (4, "4 - Good"), + (5, "5 - Excellent"), + ] + class Meta: model = SelfReview fields = ["user_exam", "case", "comments", "findings", "interpretation"] @@ -1076,6 +1084,44 @@ class SelfReviewForm(ModelForm): ModelForm.__init__(self, *args, **kwargs) super(SelfReviewForm, self).__init__(*args, **kwargs) + # Render scoring fields as explicit 1-5 selector choices for cleaner UX. + self.fields["findings"] = forms.TypedChoiceField( + choices=self.SCORE_CHOICES, + coerce=int, + empty_value=None, + required=False, + widget=RadioSelect, + label="Findings", + help_text="How well did you identify key findings?", + ) + self.fields["interpretation"] = forms.TypedChoiceField( + choices=self.SCORE_CHOICES, + coerce=int, + empty_value=None, + required=False, + widget=RadioSelect, + label="Interpretation", + help_text="How strong was your overall interpretation?", + ) + + self.fields["comments"].widget.attrs.update( + { + "rows": 5, + "placeholder": "What went well? What would you do differently next time?", + } + ) + + self.helper = FormHelper() + self.helper.form_method = "post" + self.helper.form_tag = False + self.helper.layout = Layout( + Field("user_exam"), + Field("case"), + Field("comments"), + Field("findings"), + Field("interpretation"), + ) + class UncategorisedDicomForm(ModelForm): class Meta: diff --git a/atlas/templates/atlas/collection_case_view_take.html b/atlas/templates/atlas/collection_case_view_take.html index c30e7e0d..b21dcdcc 100644 --- a/atlas/templates/atlas/collection_case_view_take.html +++ b/atlas/templates/atlas/collection_case_view_take.html @@ -214,9 +214,23 @@ {% if collection.self_review %}
-

- -

+
+
+ + Add Self Review + + + +
+
{% if self_review %}

Self Feedback

@@ -263,9 +277,23 @@ {% if question_completed %}
{% if collection.self_review %} -

- -

+
+
+ + Add Self Review + + + +
+
{% if self_review %}

Self Feedback

@@ -306,6 +334,17 @@
+ + + {% endblock %} @@ -590,6 +667,89 @@ } }); + window.openSelfReviewPopup = function (url) { + var features = 'width=980,height=840,resizable=yes,scrollbars=yes'; + window.open(url, 'selfReviewPopup', features); + }; + + window.selfReviewCurrentEmbedUrl = null; + + window.bindSelfReviewEmbedForm = function () { + var root = document.getElementById('selfReviewSidebarBody'); + if (!root) return; + + var form = root.querySelector('form.self-review-embed-form'); + if (!form) return; + + if (form.dataset.bound === '1') return; + form.dataset.bound = '1'; + + form.addEventListener('submit', async function (event) { + event.preventDefault(); + + var formData = new FormData(form); + var csrftokenInput = form.querySelector('input[name="csrfmiddlewaretoken"]'); + var csrftoken = csrftokenInput ? csrftokenInput.value : ''; + + try { + var resp = await fetch(window.selfReviewCurrentEmbedUrl || window.location.href, { + method: 'POST', + body: formData, + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRFToken': csrftoken, + }, + credentials: 'same-origin', + }); + + if (resp.redirected) { + root.innerHTML = '
Self review saved. You can continue reviewing this case.
'; + setTimeout(function () { window.closeSelfReviewSidebar(); }, 700); + return; + } + + var html = await resp.text(); + root.innerHTML = html; + window.bindSelfReviewEmbedForm(); + } catch (e) { + var msg = root.querySelector('.self-review-embed-message'); + if (msg) { + msg.innerHTML = '
Unable to save self review. Please try again.
'; + } + } + }); + }; + + window.closeSelfReviewSidebar = function () { + var sidebar = document.getElementById('selfReviewSidebar'); + if (sidebar) { + sidebar.classList.remove('open'); + } + }; + + window.openSelfReviewSidebar = async function (url) { + var sidebar = document.getElementById('selfReviewSidebar'); + var body = document.getElementById('selfReviewSidebarBody'); + if (!sidebar || !body) { + window.location.href = url; + return; + } + + var embedUrl = url + (url.indexOf('?') === -1 ? '?embed=1' : '&embed=1'); + window.selfReviewCurrentEmbedUrl = embedUrl; + body.innerHTML = '
Loading self-review form...
'; + sidebar.classList.add('open'); + + try { + var resp = await fetch(embedUrl, { credentials: 'same-origin' }); + var html = await resp.text(); + body.innerHTML = html; + window.bindSelfReviewEmbedForm(); + } catch (e) { + body.innerHTML = '
Could not load self-review sidebar content. Use Popup as fallback.
'; + } + }; + {% endblock js %} diff --git a/atlas/templates/atlas/partials/_selfreview_form_embed.html b/atlas/templates/atlas/partials/_selfreview_form_embed.html new file mode 100644 index 00000000..e52d76a1 --- /dev/null +++ b/atlas/templates/atlas/partials/_selfreview_form_embed.html @@ -0,0 +1,83 @@ +{% load crispy_forms_tags %} + +
+
+
+

Self Review

+ {{ user_exam.exam }} +
+
+

+ Record a quick reflection while reviewing this case. +

+
+ Case: {{ case }} +
+ +
+ {% csrf_token %} + {{ form.non_field_errors }} + {{ form.user_exam }} + {{ form.case }} + +
+ {{ form.comments|as_crispy_field }} +
+ +
+ +
+ {% for value, label in form.fields.findings.choices %} + + + {% endfor %} +
+ {% if form.findings.help_text %} +
{{ form.findings.help_text }}
+ {% endif %} + {% if form.findings.errors %} +
{{ form.findings.errors|striptags }}
+ {% endif %} +
+ +
+ +
+ {% for value, label in form.fields.interpretation.choices %} + + + {% endfor %} +
+ {% if form.interpretation.help_text %} +
{{ form.interpretation.help_text }}
+ {% endif %} + {% if form.interpretation.errors %} +
{{ form.interpretation.errors|striptags }}
+ {% endif %} +
+ +
+ + {% if review %} + Delete + {% endif %} +
+
+
+
+
+
diff --git a/atlas/templates/atlas/selfreview_form.html b/atlas/templates/atlas/selfreview_form.html index 5d1f48c9..ef1a1ba9 100644 --- a/atlas/templates/atlas/selfreview_form.html +++ b/atlas/templates/atlas/selfreview_form.html @@ -1,38 +1,88 @@ {% extends "base.html" %} +{% load crispy_forms_tags %} {% block content %} -

Add Self Review

- - This form allows you to record your own feedback for the question / case. This can take the form of a note or a ranking (1-5) of your findings and interpretation. -
- {% csrf_token %} - {{ form.non_field_errors }} +
+
+
+

Self Review

+ {{ user_exam.exam }} +
+
+

+ Reflect on this case and score your own performance. These scores are personal and intended to support learning. +

+
+ Case: {{ case }} +
- {{ form }} -{% comment %}
- {{ form.content_type.errors }} - {{ form.content_type }} + + {% csrf_token %} + {{ form.non_field_errors }} + {{ form.user_exam }} + {{ form.case }} + +
+ {{ form.comments|as_crispy_field }} +
+ +
+ +
+ {% for value, label in form.fields.findings.choices %} + + + {% endfor %} +
+ {% if form.findings.help_text %} +
{{ form.findings.help_text }}
+ {% endif %} + {% if form.findings.errors %} +
{{ form.findings.errors|striptags }}
+ {% endif %} +
+ +
+ +
+ {% for value, label in form.fields.interpretation.choices %} + + + {% endfor %} +
+ {% if form.interpretation.help_text %} +
{{ form.interpretation.help_text }}
+ {% endif %} + {% if form.interpretation.errors %} +
{{ form.interpretation.errors|striptags }}
+ {% endif %} +
+ +
+ + Open Case + {% if review %} + Delete Review + {% endif %} +
+ +
-
- {{ form.object_id.errors }} - {{ form.object_id }} -
-
- {{ form.note_type.errors }} - - {{ form.note_type }} -
-
- {{ form.note.errors }} -
- {{ form.note }} -
{% endcomment %} - - - {% if review %}
{% endif %} +
{% endblock %} diff --git a/atlas/views.py b/atlas/views.py index 6ce4e3f1..9af89cd1 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -5613,6 +5613,11 @@ class CollectionClone(CollectionCloneMixin, CaseCollectionCreate): class SelfReviewDelete(DeleteView): + """Delete a user's self-review entry for a specific case attempt. + + The view resolves ``user_exam``, ``case`` and ``review`` from URL params and, + on success, returns the learner to the case take page. + """ model = SelfReview template_name = "confirm_delete.html" # success_url = reverse_lazy("atlas:finding_view") @@ -5640,9 +5645,18 @@ class SelfReviewDelete(DeleteView): class SelfReviewUpdate(UpdateView): + """Edit an existing self-review entry for a case attempt. + + Uses ``SelfReviewForm`` and redirects back to the case take page after save. + """ model = SelfReview form_class = SelfReviewForm + def get_template_names(self): + if self.request.GET.get("embed") == "1": + return ["atlas/partials/_selfreview_form_embed.html"] + return ["atlas/selfreview_form.html"] + def get_initial(self): user_exam_id = self.kwargs["user_exam_id"] case_id = self.kwargs["case_id"] @@ -5659,6 +5673,7 @@ class SelfReviewUpdate(UpdateView): context["user_exam"] = self.user_exam context["case"] = self.case context["review"] = self.review + context["embed"] = self.request.GET.get("embed") == "1" return context def get_success_url(self) -> str: @@ -5677,9 +5692,19 @@ class SelfReviewUpdate(UpdateView): class AddSelfReview(CreateView): + """Create a new self-review entry linked to a user exam and case. + + The form captures free-text comments and 1-5 self-assessment scores for + findings and interpretation, then returns the learner to the case take page. + """ model = SelfReview form_class = SelfReviewForm + def get_template_names(self): + if self.request.GET.get("embed") == "1": + return ["atlas/partials/_selfreview_form_embed.html"] + return ["atlas/selfreview_form.html"] + # fields = ("author", "note_type", "note") def get_form_kwargs(self): @@ -5702,6 +5727,7 @@ class AddSelfReview(CreateView): context = super(AddSelfReview, self).get_context_data(**kwargs) context["user_exam"] = self.user_exam context["case"] = self.case + context["embed"] = self.request.GET.get("embed") == "1" return context def get_success_url(self) -> str: