diff --git a/atlas/forms.py b/atlas/forms.py index 0bf69ada..accf5850 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -180,6 +180,60 @@ class ConditionForm(ModelForm): pass +class ConditionRelationshipForm(Form): + parents = ModelMultipleChoiceField( + required=False, + queryset=Condition.objects.none(), + widget=autocomplete.ModelSelect2Multiple(url="atlas:condition-autocomplete"), + ) + children = ModelMultipleChoiceField( + required=False, + queryset=Condition.objects.none(), + widget=autocomplete.ModelSelect2Multiple(url="atlas:condition-autocomplete"), + ) + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop("instance", None) + super().__init__(*args, **kwargs) + + condition_qs = Condition.objects.all().order_by("name") + if self.instance is not None and getattr(self.instance, "pk", None): + condition_qs = condition_qs.exclude(pk=self.instance.pk) + + self.fields["parents"].queryset = condition_qs + self.fields["children"].queryset = condition_qs + + self.helper = FormHelper() + self.helper.form_method = "post" + self.helper.form_tag = False + self.helper.layout = Layout( + Fieldset( + "Condition relationships", + "parents", + "children", + ) + ) + + def save(self, commit=True): + if self.instance is None: + raise ValueError("ConditionRelationshipForm requires an instance") + + parents = list(self.cleaned_data.get("parents", [])) + children = list(self.cleaned_data.get("children", [])) + + self.instance.parent.set(parents) + + current_children = list(self.instance.child.all()) + for child in current_children: + if child not in children: + self.instance.child.remove(child) + for child in children: + if child not in current_children: + self.instance.child.add(child) + + return self.instance + + class CaseCollectionForm(ModelForm): class Meta: model = CaseCollection @@ -211,6 +265,19 @@ class CaseCollectionForm(ModelForm): label=label, ) + if "prerequisites" in self.fields: + authored_collections = CaseCollection.objects.filter(author=self.user).distinct().order_by("name") + if getattr(self.instance, "pk", None): + authored_collections = authored_collections.exclude(pk=self.instance.pk) + self.fields["prerequisites"].queryset = authored_collections + self.fields["prerequisites"].widget = autocomplete.ModelSelect2Multiple( + url="atlas:casecollection-prerequisite-autocomplete", + attrs={ + "data-placeholder": "Search authored collections...", + "data-minimum-input-length": 0, + }, + ) + # Identify show_ fields show_fields = [f for f in self.fields if f.startswith("show_")] show_pre_fields = [f for f in show_fields if f.endswith("_pre")] @@ -630,8 +697,9 @@ class CaseForm(ModelForm): # This is populated if we create the case from an exam/collection self.collection = None - if kwargs["initial"] is not None and "exams" in kwargs["initial"]: - collections = kwargs["initial"].pop("exams") + initial = kwargs.setdefault("initial", {}) + if initial is not None and "exams" in initial: + collections = initial.pop("exams") logger.debug(collections) self.collection = get_object_or_404(CaseCollection, pk=collections[0]) diff --git a/atlas/models.py b/atlas/models.py index 7a2d46c2..c5c30958 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -138,7 +138,10 @@ def _series_declared_groups_cache_key(series_id): def invalidate_series_declared_groups_cache(series_id): if not series_id: return - cache.delete(_series_declared_groups_cache_key(series_id)) + try: + cache.delete(_series_declared_groups_cache_key(series_id)) + except Exception: + pass def _build_declared_series_groups(series, series_images_with_urls): @@ -155,7 +158,10 @@ def _build_declared_series_groups(series, series_images_with_urls): for image_obj, image_url in series_images_with_urls ], } - cached = cache.get(cache_key) + try: + cached = cache.get(cache_key) + except Exception: + cached = None if isinstance(cached, dict): if cached.get("signature") == signature: return cached.get("groups", []) @@ -257,7 +263,10 @@ def _build_declared_series_groups(series, series_images_with_urls): "groupValues": {}, } ] - cache.set(cache_key, {"signature": signature, "groups": result}, timeout=60 * 60) + try: + cache.set(cache_key, {"signature": signature, "groups": result}, timeout=60 * 60) + except Exception: + pass return result grouped = {} @@ -313,7 +322,10 @@ def _build_declared_series_groups(series, series_images_with_urls): } ) - cache.set(cache_key, {"signature": signature, "groups": ordered_groups}, timeout=60 * 60) + try: + cache.set(cache_key, {"signature": signature, "groups": ordered_groups}, timeout=60 * 60) + except Exception: + pass return ordered_groups diff --git a/atlas/tasks.py b/atlas/tasks.py index a605469f..fe4c9769 100644 --- a/atlas/tasks.py +++ b/atlas/tasks.py @@ -173,6 +173,10 @@ def series_downsample_task(context, series_id, user_id, downsample_pct): series.source_series_instance_uid = source_series_uid series.series_instance_uid = replacement_series_uid series.save(update_fields=["modified", "source_series_instance_uid", "series_instance_uid"]) + try: + series.get_total_image_size(refresh=True) + except Exception as exc: + logger.warning("Failed to refresh total_image_size for series {}: {}", series.pk, exc) cache.set( progress_key, diff --git a/atlas/templates/atlas/case_admin_overview.html b/atlas/templates/atlas/case_admin_overview.html new file mode 100644 index 00000000..1eb5cbc3 --- /dev/null +++ b/atlas/templates/atlas/case_admin_overview.html @@ -0,0 +1,112 @@ +{% extends 'atlas/base.html' %} + +{% block content %} +
+
+

Case Admin Overview

+ Back to case list +
+ +
+
+
+
+
Total cases
+
{{ totals.total_cases }}
+
+
+
+
+
+
+
Authors with cases
+
{{ totals.total_authors }}
+
+
+
+
+
+
+
Open-access cases
+
{{ totals.open_access_cases }}
+
+
+
+
+
+
+
Archived cases
+
{{ totals.archived_cases }}
+
+
+
+
+ +
+
+
+
Cases per user
+
+
+ + + + + + + + + {% for row in author_rows %} + + + + + {% empty %} + + + + {% endfor %} + +
UserCase count
+ {% if row.get_full_name %}{{ row.get_full_name }}{% else %}{{ row.username }}{% endif %} +
{{ row.username }}
+
{{ row.case_count }}
No case authors found.
+
+
+
+
+ +
+
+
+ Recently added cases + +
+
+ Click Load to fetch recently added cases. +
+
+ +
+
+ Largest cases + +
+
+ Click Load to fetch the largest cases by image footprint. +
+
+
+
+
+{% endblock %} diff --git a/atlas/templates/atlas/condition_detail.html b/atlas/templates/atlas/condition_detail.html index 102fa4e0..4154fc7d 100755 --- a/atlas/templates/atlas/condition_detail.html +++ b/atlas/templates/atlas/condition_detail.html @@ -54,6 +54,13 @@ {% endfor %} + {% if can_merge %} +
Edit relationships
+
+ {% include 'atlas/partials/_condition_relationships.html' %} +
+ {% endif %} +
RCR condition
{% if condition.rcr_curriculum %}Yes{% else %}No{% endif %}
diff --git a/atlas/templates/atlas/partials/_case_admin_largest_cases.html b/atlas/templates/atlas/partials/_case_admin_largest_cases.html new file mode 100644 index 00000000..e829b944 --- /dev/null +++ b/atlas/templates/atlas/partials/_case_admin_largest_cases.html @@ -0,0 +1,27 @@ +{% if largest_rows %} +
+ + + + + + + + + + {% for row in largest_rows %} + + + + + + {% endfor %} + +
CaseSeriesTotal size
+ {{ row.case.title }} +
#{{ row.case.pk }}
+
{{ row.series_count }}{{ row.human_total }}
+
+{% else %} +
No case-size data available.
+{% endif %} diff --git a/atlas/templates/atlas/partials/_case_admin_recent_cases.html b/atlas/templates/atlas/partials/_case_admin_recent_cases.html new file mode 100644 index 00000000..8ad8e11f --- /dev/null +++ b/atlas/templates/atlas/partials/_case_admin_recent_cases.html @@ -0,0 +1,33 @@ +{% if recent_cases %} +
+ + + + + + + + + + {% for case in recent_cases %} + + + + + + {% endfor %} + +
CaseCreatedAuthors
+ {{ case.title }} +
#{{ case.pk }}
+
{{ case.created_date|date:"Y-m-d H:i" }} + {% for author in case.author.all %} + {{ author.username }} + {% empty %} + None + {% endfor %} +
+
+{% else %} +
No recent cases found.
+{% endif %} diff --git a/atlas/templates/atlas/partials/_condition_relationships.html b/atlas/templates/atlas/partials/_condition_relationships.html new file mode 100644 index 00000000..2fbbcbe1 --- /dev/null +++ b/atlas/templates/atlas/partials/_condition_relationships.html @@ -0,0 +1,13 @@ +
+ {% csrf_token %} + +
+ + {{ relationship_form.parents }} +
+
+ + {{ relationship_form.children }} +
+ +
diff --git a/atlas/templates/atlas/partials/_series_item.html b/atlas/templates/atlas/partials/_series_item.html index 45c8551f..a1531acd 100644 --- a/atlas/templates/atlas/partials/_series_item.html +++ b/atlas/templates/atlas/partials/_series_item.html @@ -1,6 +1,6 @@
  • - +
    {{ tags.SeriesDescription|default:series }} {% if series_is_partial %} diff --git a/atlas/templates/atlas/partials/case_search_widget.html b/atlas/templates/atlas/partials/case_search_widget.html index 30c96ede..2041d7d3 100644 --- a/atlas/templates/atlas/partials/case_search_widget.html +++ b/atlas/templates/atlas/partials/case_search_widget.html @@ -66,12 +66,13 @@ var item = e.target.closest('.list-group-item'); if (!item || !targetEl.contains(item)) return; - // Explicit select action button should always select the row. - if (e.target.closest('.case-select-btn')) { + var selectBtn = e.target.closest('.case-select-btn'); + if (selectBtn) { e.preventDefault(); } else { - // Ignore other interactive elements inside the item (links, buttons, forms, inputs) - if (e.target.closest('a,button,form,input')) return; + // Ignore all non-select interactions so rows are passive unless + // explicit selection controls are present. + return; } var casePk = item.getAttribute('data-case-pk'); diff --git a/atlas/templates/atlas/series_viewer.html b/atlas/templates/atlas/series_viewer.html index e2c6aad9..4690a483 100755 --- a/atlas/templates/atlas/series_viewer.html +++ b/atlas/templates/atlas/series_viewer.html @@ -216,10 +216,17 @@
    Actions
    Anonymise dicoms - Order by slice location - Order by instance number - Order by SeriesInstanceUID - Order by uploaded filename +
    diff --git a/atlas/templates/atlas/user_uploads.html b/atlas/templates/atlas/user_uploads.html index 9ff8f2fa..b9eb7a3f 100644 --- a/atlas/templates/atlas/user_uploads.html +++ b/atlas/templates/atlas/user_uploads.html @@ -1,5 +1,6 @@ {% extends 'atlas/base.html' %} {% load case_widgets %} +{% load crispy_forms_tags %} {% block content %}

    Uploaded dicoms

    @@ -173,6 +174,7 @@ class="btn btn-primary" >Import {% if case %}into case{% endif %} +
    @@ -231,6 +233,30 @@
    + +