diff --git a/atlas/forms.py b/atlas/forms.py index 169dc941..25658235 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -953,7 +953,13 @@ CaseCollectionCaseFormSet = inlineformset_factory( CaseCollection, Case.casecollection_set.through, form=CaseCollectionCaseForm, - exclude=["question_schema", "question_answers"], + exclude=[ + "question_schema", + "question_answers", + "series_visibility_config", + "learner_comment", + "learner_review_comment", + ], can_delete=True, extra=0, max_num=50, @@ -1370,6 +1376,8 @@ class CaseDetailForm(ModelForm): fields = [ "redact_history", "override_history", + "learner_comment", + "learner_review_comment", "question_time_limit_override", "answer_entry_grace_period_override", ] @@ -1443,6 +1451,11 @@ class CaseDetailForm(ModelForm): HTML(history_block), Field("override_history"), Field("redact_history"), + Fieldset( + "Learner Comments", + Field("learner_comment"), + Field("learner_review_comment"), + ), Fieldset( "Case Timing Overrides", Field("question_time_limit_override"), diff --git a/atlas/migrations/0106_casedetail_learner_comment_and_more.py b/atlas/migrations/0106_casedetail_learner_comment_and_more.py new file mode 100644 index 00000000..0df75f40 --- /dev/null +++ b/atlas/migrations/0106_casedetail_learner_comment_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 6.0.1 on 2026-06-01 11:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('atlas', '0105_case_study_date'), + ] + + operations = [ + migrations.AddField( + model_name='casedetail', + name='learner_comment', + field=models.TextField(blank=True, default='', help_text='Comment shown to the learner while taking the case and on review.'), + ), + migrations.AddField( + model_name='casedetail', + name='learner_review_comment', + field=models.TextField(blank=True, default='', help_text='Comment shown to the learner only on review.'), + ), + migrations.AddField( + model_name='casedetail', + name='series_visibility_config', + field=models.JSONField(blank=True, default=dict, help_text='Per-series visibility map for this case in this collection.'), + ), + ] diff --git a/atlas/models.py b/atlas/models.py index 19f6290d..7b5b86d7 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -2217,6 +2217,29 @@ class CaseDetail(models.Model): help_text="Optional per-case override for additional answer-only time in seconds after case view lock.", ) + class SeriesVisibility(models.TextChoices): + ALWAYS = "AL", _("Show all the time") + REVIEW = "RE", _("Show on review") + HIDDEN = "NO", _("Do not show") + + series_visibility_config = models.JSONField( + default=dict, + blank=True, + help_text="Per-series visibility map for this case in this collection.", + ) + + learner_comment = models.TextField( + blank=True, + default="", + help_text="Comment shown to the learner while taking the case and on review.", + ) + + learner_review_comment = models.TextField( + blank=True, + default="", + help_text="Comment shown to the learner only on review.", + ) + class Meta: ordering = ("sort_order",) @@ -2270,23 +2293,72 @@ class CaseDetail(models.Model): return self.override_history return self.case.history or "No history provided" - def get_case_series_nested(self, include_priors=True): - case_series_images = self.case.get_series_images_nested(as_json=False) + def get_series_visibility_map(self) -> dict[str, str]: + raw_map = self.series_visibility_config or {} + if not isinstance(raw_map, dict): + return {} + + valid_values = { + self.SeriesVisibility.ALWAYS, + self.SeriesVisibility.REVIEW, + self.SeriesVisibility.HIDDEN, + } + cleaned = {} + for key, value in raw_map.items(): + key_str = str(key) + value_str = str(value) + if value_str in valid_values: + cleaned[key_str] = value_str + return cleaned + + def get_series_visibility_for(self, series, review_mode: bool = False) -> str: + visibility = self.get_series_visibility_map().get( + str(series.pk), self.SeriesVisibility.ALWAYS + ) + if visibility == self.SeriesVisibility.REVIEW and not review_mode: + return self.SeriesVisibility.HIDDEN + return visibility + + def get_visible_case_series(self, review_mode: bool = False): + visible = [] + for series in self.case.get_ordered_series(): + visibility = self.get_series_visibility_for(series, review_mode=review_mode) + if visibility != self.SeriesVisibility.HIDDEN: + visible.append(series) + return visible + + def get_case_series_nested(self, include_priors=True, review_mode: bool = False): + case_series_images = [] + for series in self.get_visible_case_series(review_mode=review_mode): + case_series_images.append([ + image.image.url + for image in series.images.filter(removed=False) + if image.image + ]) if include_priors: logger.debug(f"Checking for prior cases for case {self.case}") logger.debug(f"Found {self.case.prior_case.count()} prior cases for case {self.case}") for prior in self.caseprior_set.all(): + if prior.prior_visibility == CasePrior.PriorVisibility.NONE: + continue + if prior.prior_visibility == CasePrior.PriorVisibility.REVIEW and not review_mode: + continue logger.debug(f"Adding prior case {prior.prior_case} to case {self.case}") case_series_images.extend(prior.prior_case.get_series_images_nested(as_json=False)) return json.dumps(case_series_images) - def get_case_named_stacks(self, include_priors=True): + def get_case_named_stacks(self, include_priors=True, review_mode: bool = False): def build_stacks_for(case_obj, prefix=None): stacks = [] - for series in case_obj.get_ordered_series(): + if case_obj == self.case: + series_iterable = self.get_visible_case_series(review_mode=review_mode) + else: + series_iterable = case_obj.get_ordered_series() + + for series in series_iterable: series_images = list(series.get_images()) series_images_with_urls = [(img, f"{REMOTE_URL}{img.image.url}") for img in series_images] images = [url for _, url in series_images_with_urls] @@ -2316,6 +2388,10 @@ class CaseDetail(models.Model): # include priors as separate entries if include_priors: for prior in self.caseprior_set.all(): + if prior.prior_visibility == CasePrior.PriorVisibility.NONE: + continue + if prior.prior_visibility == CasePrior.PriorVisibility.REVIEW and not review_mode: + continue prior_case = prior.prior_case results.append( { diff --git a/atlas/templates/atlas/collection_case_details.html b/atlas/templates/atlas/collection_case_details.html index 8e7789c9..6da318e9 100644 --- a/atlas/templates/atlas/collection_case_details.html +++ b/atlas/templates/atlas/collection_case_details.html @@ -14,13 +14,54 @@
{% csrf_token %} {% crispy form form.helper %} + +
+
+ Series Visibility +
+
+

Choose how each series is shown for this case in this collection.

+ {% if series_visibility_rows %} +
+ + + + + + + + + {% for row in series_visibility_rows %} + + + + + {% endfor %} + +
SeriesVisibility
+
Series {{ forloop.counter }}
+
{{ row.series }}
+
+ +
+
+ {% else %} +
No series linked to this case.
+ {% endif %} +
+
+
{% endblock %} {% block js %} - - + + {% endblock %} \ No newline at end of file diff --git a/atlas/templates/atlas/collection_case_view_take.html b/atlas/templates/atlas/collection_case_view_take.html index 48a88eb3..1db533b4 100644 --- a/atlas/templates/atlas/collection_case_view_take.html +++ b/atlas/templates/atlas/collection_case_view_take.html @@ -101,6 +101,18 @@ {% endif %} + {% if casedetail.learner_comment %} +
+ Collection note: {{ casedetail.learner_comment|linebreaks }} +
+ {% endif %} + + {% if question_completed and casedetail.learner_review_comment %} +
+ Review note: {{ casedetail.learner_review_comment|linebreaks }} +
+ {% endif %} + {% if collection.show_ohif_viewer or collection.show_ohif_viewer_link or collection.show_built_in_viewer %}
{% if collection.show_built_in_viewer %} @@ -118,7 +130,7 @@
diff --git a/atlas/templates/atlas/collection_case_view_take_answers.html b/atlas/templates/atlas/collection_case_view_take_answers.html index 236a38cc..704d62cf 100644 --- a/atlas/templates/atlas/collection_case_view_take_answers.html +++ b/atlas/templates/atlas/collection_case_view_take_answers.html @@ -4,6 +4,16 @@ {% block content %}

Case: {{ case.title }}

+ {% if casedetail.learner_comment %} +
+ Collection note: {{ casedetail.learner_comment|linebreaks }} +
+ {% endif %} + {% if casedetail.learner_review_comment %} +
+ Review note: {{ casedetail.learner_review_comment|linebreaks }} +
+ {% endif %} {{ casedetail.question_schema }} diff --git a/atlas/templates/atlas/collection_viva_local.html b/atlas/templates/atlas/collection_viva_local.html index bbcc34f1..2bf0e1fe 100644 --- a/atlas/templates/atlas/collection_viva_local.html +++ b/atlas/templates/atlas/collection_viva_local.html @@ -56,7 +56,7 @@
Series
- {% for series in case.prefetched_series %} + {% for series in case.visible_prefetched_series %}
@@ -375,6 +375,11 @@ $('#open-viewer-local').click(function() { $("#open-viewer-local").removeClass("flash-button"); viewerConnected = false; + try { + localStorage.removeItem('rad_local_viewer_popout_state'); + } catch (e) { + console.warn('Unable to clear previous viewer state', e); + } const opened = openSecondaryWindow("{% url 'atlas:collection_viewer_local_progressive' %}"); if (!opened) { showUserMessage('Unable to open viewer window. Please allow pop-ups for this site and try again.', 'danger'); diff --git a/atlas/templates/atlas/partials/_prior_card.html b/atlas/templates/atlas/partials/_prior_card.html index 78d44bf1..82faef5f 100644 --- a/atlas/templates/atlas/partials/_prior_card.html +++ b/atlas/templates/atlas/partials/_prior_card.html @@ -16,6 +16,14 @@

Relation: {{ relation }}

+

+ Study date: + {% if case.study_date %} + {{ case.study_date }} + {% else %} + Unknown + {% endif %} +

{{case.get_series_blocks|safe}}
@@ -37,6 +45,12 @@
+
+
+ + +
+