From 2e854c43b6b58deacb5ba2c10cfa3fee27b3b80c Mon Sep 17 00:00:00 2001 From: Ross Date: Wed, 3 Jun 2026 20:38:29 +0100 Subject: [PATCH] fix many issues --- atlas/models.py | 15 +++-- atlas/templates/atlas/case_display_block.html | 10 +-- atlas/tests/test_series_viewer_description.py | 20 ++++++ atlas/views.py | 1 + rad/tests/test_logs_view.py | 37 +++++++++++ rad/views.py | 66 +++++++++++++++++++ templates/logs_viewer.html | 29 +++++++- 7 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 atlas/tests/test_series_viewer_description.py create mode 100644 rad/tests/test_logs_view.py diff --git a/atlas/models.py b/atlas/models.py index 7b5b86d7..3c60c57d 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -1379,7 +1379,7 @@ def _populate_case_study_date_on_series_image_save(sender, instance, **kwargs): if not instance.series_id: return - tags = instance.basic_dicom_tags or {} + tags = getattr(instance, "basic_dicom_tags", None) or {} raw_date = tags.get("StudyDate") or tags.get("AcquisitionDate") or tags.get("SeriesDate") parsed = Case._parse_dicom_date(raw_date) if not parsed: @@ -1520,6 +1520,14 @@ class Series(SeriesBase): self.pk, self.get_examination_full(), self.description, case_id ) + def get_viewer_stack_description(self) -> str: + description = self.description or str(self.examination or "Series") + if self.plane: + description = f"{description} ({self.plane})" + if self.contrast: + description = f"{description} / {self.contrast}" + return description + def get_absolute_url(self): return reverse("atlas:series_detail", kwargs={"pk": self.pk}) @@ -1558,10 +1566,7 @@ class Series(SeriesBase): # else: # instances.append(image.get_image_dicom_json(image_index)) - description = f"{self.examination} ({self.plane})" - if self.contrast and self.contrast is not None: - description = f"{description} / {self.contrast}" - series_json["SeriesDescription"] = description + series_json["SeriesDescription"] = self.get_viewer_stack_description() series_json["instances"] = instances return series_json diff --git a/atlas/templates/atlas/case_display_block.html b/atlas/templates/atlas/case_display_block.html index 405b73f0..e147946a 100755 --- a/atlas/templates/atlas/case_display_block.html +++ b/atlas/templates/atlas/case_display_block.html @@ -6,7 +6,7 @@ id="series-block-{{ series.pk }}" data-series-id="{{series.pk}}" data-series-label="Series {{ forloop.counter }}" - data-series-name="{{ series|escape }}" + data-series-name="{{ series.get_viewer_stack_description|escape }}" data-image-ids='{{ series.get_image_url_array_and_count.0 }}' data-series-detail-url="{% url 'atlas:series_detail' pk=series.pk %}" data-series-popup-url="/atlas/series/{{series.pk}}" @@ -43,7 +43,7 @@ class="btn btn-link btn-sm text-muted series-open-primary-btn" title="View series in viewer" data-series-id="{{series.pk}}" - data-series-name="{{ series|escape }}"> + data-series-name="{{ series.get_viewer_stack_description|escape }}"> View
@@ -57,7 +57,7 @@ class="btn btn-outline-info btn-sm series-open-new-viewport-btn d-none" title="View series in a new pane" data-series-id="{{series.pk}}" - data-series-name="{{ series|escape }}" + data-series-name="{{ series.get_viewer_stack_description|escape }}" tabindex="-1" aria-hidden="true"> View in new pane @@ -1357,7 +1357,9 @@ } document.querySelectorAll(".select-series-btn").forEach(function(btn) { - btn.addEventListener("click", function() { + btn.addEventListener("click", function(event) { + event.preventDefault(); + event.stopPropagation(); const parent = btn.closest(".series-block"); toggleSeriesSelection(parent); }); diff --git a/atlas/tests/test_series_viewer_description.py b/atlas/tests/test_series_viewer_description.py new file mode 100644 index 00000000..d86caea0 --- /dev/null +++ b/atlas/tests/test_series_viewer_description.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import pytest + +from atlas.models import SeriesImage, _populate_case_study_date_on_series_image_save + + +@pytest.mark.django_db +def test_series_viewer_stack_description_includes_plane_and_contrast(create_series): + series = create_series + + assert series.get_viewer_stack_description() == "Test Examination (Test Plane) / Test Contrast" + assert series.get_series_dicom_json()["SeriesDescription"] == "Test Examination (Test Plane) / Test Contrast" + + +@pytest.mark.django_db +def test_series_image_save_hook_handles_missing_basic_dicom_tags(create_series): + image = SeriesImage(series=create_series) + + _populate_case_study_date_on_series_image_save(SeriesImage, image) \ No newline at end of file diff --git a/atlas/views.py b/atlas/views.py index e0bc70f9..a013d187 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -3576,6 +3576,7 @@ def uploads_import_htmx(request, case_id: int | None = None): imported = import_dicoms_helper(request, case_id=case_id, dicoms=dicoms) except Exception as exc: logger.exception("Error during uploads import") + logging.getLogger(__name__).exception("Error during uploads import") return HttpResponse( f'
Import failed: {escape(str(exc))}
', status=500, diff --git a/rad/tests/test_logs_view.py b/rad/tests/test_logs_view.py new file mode 100644 index 00000000..821609f9 --- /dev/null +++ b/rad/tests/test_logs_view.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import pytest +from django.urls import reverse + + +@pytest.mark.django_db +def test_logs_view_filters_by_date_and_can_reset_log_file(client, admin_user, monkeypatch, tmp_path): + client.force_login(admin_user) + + log_file = tmp_path / "log.txt" + log_file.write_text( + "[03/Jun/2026 10:00:00] INFO [atlas.views:10] first entry\n" + "[03/Jun/2026 11:00:00] ERROR [atlas.views:11] second entry\n", + encoding="utf-8", + ) + monkeypatch.setattr("rad.views.settings.BASE_DIR", str(tmp_path)) + + response = client.get( + reverse("logs_view"), + { + "date_from": "2026-06-03", + "date_to": "2026-06-03", + "time_from": "10:30", + "time_to": "11:30", + }, + ) + + assert response.status_code == 200 + assert b"second entry" in response.content + assert b"first entry" not in response.content + + response = client.post(reverse("logs_view"), {"action": "clear"}) + + assert response.status_code == 200 + assert b"Log file cleared." in response.content + assert log_file.read_text(encoding="utf-8") == "" \ No newline at end of file diff --git a/rad/views.py b/rad/views.py index 5edb635a..04770b80 100644 --- a/rad/views.py +++ b/rad/views.py @@ -3,6 +3,7 @@ from typing import Any, Optional import os import re from io import StringIO +from datetime import datetime from django.conf import settings from django_tables2 import SingleTableMixin @@ -306,6 +307,53 @@ def logs_view(request): show_all = request.GET.get("all") == "1" raw_mode = request.GET.get("raw") == "1" include_noise = request.GET.get("include_noise") == "1" + date_from_raw = request.GET.get("date_from", "").strip() + date_to_raw = request.GET.get("date_to", "").strip() + time_from_raw = request.GET.get("time_from", "").strip() + time_to_raw = request.GET.get("time_to", "").strip() + action_message = "" + + if request.method == "POST" and request.POST.get("action") == "clear": + try: + os.makedirs(os.path.dirname(log_file_path), exist_ok=True) + with open(log_file_path, "w", encoding="utf-8"): + pass + action_message = "Log file cleared." + except Exception as exc: + action_message = f"Failed to clear log file: {exc}" + + def _parse_log_timestamp(timestamp_text: str) -> datetime | None: + if not timestamp_text: + return None + for timestamp_format in ("%d/%b/%Y %H:%M:%S", "%Y-%m-%d %H:%M:%S"): + try: + return datetime.strptime(timestamp_text, timestamp_format) + except ValueError: + continue + return None + + def _parse_date_input(value: str): + if not value: + return None + try: + return datetime.strptime(value, "%Y-%m-%d").date() + except ValueError: + return None + + def _parse_time_input(value: str): + if not value: + return None + for time_format in ("%H:%M", "%H:%M:%S"): + try: + return datetime.strptime(value, time_format).time() + except ValueError: + continue + return None + + date_from = _parse_date_input(date_from_raw) + date_to = _parse_date_input(date_to_raw) + time_from = _parse_time_input(time_from_raw) + time_to = _parse_time_input(time_to_raw) if raw_mode: if not os.path.exists(log_file_path): @@ -438,6 +486,19 @@ def logs_view(request): if search_lower and search_lower not in haystack: continue + entry_timestamp = _parse_log_timestamp(entry.get("timestamp", "")) + if date_from or date_to or time_from or time_to: + if entry_timestamp is None: + continue + if date_from and entry_timestamp.date() < date_from: + continue + if date_to and entry_timestamp.date() > date_to: + continue + if time_from and entry_timestamp.time() < time_from: + continue + if time_to and entry_timestamp.time() > time_to: + continue + filtered_entries.append(entry) total = len(filtered_entries) @@ -459,6 +520,11 @@ def logs_view(request): "raw_url": f"{reverse('logs_view')}?raw=1", "include_noise": include_noise, "noise_suppressed": noise_suppressed, + "date_from": date_from_raw, + "date_to": date_to_raw, + "time_from": time_from_raw, + "time_to": time_to_raw, + "action_message": action_message, }, ) diff --git a/templates/logs_viewer.html b/templates/logs_viewer.html index 98fd4ba6..9627e796 100644 --- a/templates/logs_viewer.html +++ b/templates/logs_viewer.html @@ -52,6 +52,10 @@
+ {% if action_message %} +
{{ action_message }}
+ {% endif %} +
@@ -70,6 +74,18 @@ {% endfor %}
+
+ +
+
+ +
+
+ +
+
+ +
@@ -86,15 +102,22 @@
{% if show_all %} - Show recent only + Show recent only {% else %} - View all logs + View all logs {% endif %}
+
+ +
+
+ {% csrf_token %} + +
@@ -153,7 +176,7 @@ {% endif %} {% if noise_suppressed and not include_noise %} Hidden {{ noise_suppressed }} noisy template debug entr{{ noise_suppressed|pluralize:"y,ies" }}. - Show them. + Show them. {% endif %} {% else %}