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 @@
{% 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 %} +Relation: {{ relation }}
++ Study date: + {% if case.study_date %} + {{ case.study_date }} + {% else %} + Unknown + {% endif %} +