fix many issues

This commit is contained in:
Ross
2026-06-03 20:38:29 +01:00
parent 46518a45c1
commit 2e854c43b6
7 changed files with 166 additions and 12 deletions
+10 -5
View File
@@ -1379,7 +1379,7 @@ def _populate_case_study_date_on_series_image_save(sender, instance, **kwargs):
if not instance.series_id: if not instance.series_id:
return 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") raw_date = tags.get("StudyDate") or tags.get("AcquisitionDate") or tags.get("SeriesDate")
parsed = Case._parse_dicom_date(raw_date) parsed = Case._parse_dicom_date(raw_date)
if not parsed: if not parsed:
@@ -1520,6 +1520,14 @@ class Series(SeriesBase):
self.pk, self.get_examination_full(), self.description, case_id 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): def get_absolute_url(self):
return reverse("atlas:series_detail", kwargs={"pk": self.pk}) return reverse("atlas:series_detail", kwargs={"pk": self.pk})
@@ -1558,10 +1566,7 @@ class Series(SeriesBase):
# else: # else:
# instances.append(image.get_image_dicom_json(image_index)) # instances.append(image.get_image_dicom_json(image_index))
description = f"{self.examination} ({self.plane})" series_json["SeriesDescription"] = self.get_viewer_stack_description()
if self.contrast and self.contrast is not None:
description = f"{description} / {self.contrast}"
series_json["SeriesDescription"] = description
series_json["instances"] = instances series_json["instances"] = instances
return series_json return series_json
@@ -6,7 +6,7 @@
id="series-block-{{ series.pk }}" id="series-block-{{ series.pk }}"
data-series-id="{{series.pk}}" data-series-id="{{series.pk}}"
data-series-label="Series {{ forloop.counter }}" 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-image-ids='{{ series.get_image_url_array_and_count.0 }}'
data-series-detail-url="{% url 'atlas:series_detail' pk=series.pk %}" data-series-detail-url="{% url 'atlas:series_detail' pk=series.pk %}"
data-series-popup-url="/atlas/series/{{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" class="btn btn-link btn-sm text-muted series-open-primary-btn"
title="View series in viewer" title="View series in viewer"
data-series-id="{{series.pk}}" data-series-id="{{series.pk}}"
data-series-name="{{ series|escape }}"> data-series-name="{{ series.get_viewer_stack_description|escape }}">
<i class="bi bi-eye"></i> View <i class="bi bi-eye"></i> View
</button> </button>
<div class="series-card-inline-actions"> <div class="series-card-inline-actions">
@@ -57,7 +57,7 @@
class="btn btn-outline-info btn-sm series-open-new-viewport-btn d-none" class="btn btn-outline-info btn-sm series-open-new-viewport-btn d-none"
title="View series in a new pane" title="View series in a new pane"
data-series-id="{{series.pk}}" data-series-id="{{series.pk}}"
data-series-name="{{ series|escape }}" data-series-name="{{ series.get_viewer_stack_description|escape }}"
tabindex="-1" tabindex="-1"
aria-hidden="true"> aria-hidden="true">
<i class="bi bi-layout-split"></i> View in new pane <i class="bi bi-layout-split"></i> View in new pane
@@ -1357,7 +1357,9 @@
} }
document.querySelectorAll(".select-series-btn").forEach(function(btn) { 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"); const parent = btn.closest(".series-block");
toggleSeriesSelection(parent); toggleSeriesSelection(parent);
}); });
@@ -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)
+1
View File
@@ -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) imported = import_dicoms_helper(request, case_id=case_id, dicoms=dicoms)
except Exception as exc: except Exception as exc:
logger.exception("Error during uploads import") logger.exception("Error during uploads import")
logging.getLogger(__name__).exception("Error during uploads import")
return HttpResponse( return HttpResponse(
f'<div class="alert alert-danger mb-0">Import failed: {escape(str(exc))}</div>', f'<div class="alert alert-danger mb-0">Import failed: {escape(str(exc))}</div>',
status=500, status=500,
+37
View File
@@ -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") == ""
+66
View File
@@ -3,6 +3,7 @@ from typing import Any, Optional
import os import os
import re import re
from io import StringIO from io import StringIO
from datetime import datetime
from django.conf import settings from django.conf import settings
from django_tables2 import SingleTableMixin from django_tables2 import SingleTableMixin
@@ -306,6 +307,53 @@ def logs_view(request):
show_all = request.GET.get("all") == "1" show_all = request.GET.get("all") == "1"
raw_mode = request.GET.get("raw") == "1" raw_mode = request.GET.get("raw") == "1"
include_noise = request.GET.get("include_noise") == "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 raw_mode:
if not os.path.exists(log_file_path): 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: if search_lower and search_lower not in haystack:
continue 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) filtered_entries.append(entry)
total = len(filtered_entries) total = len(filtered_entries)
@@ -459,6 +520,11 @@ def logs_view(request):
"raw_url": f"{reverse('logs_view')}?raw=1", "raw_url": f"{reverse('logs_view')}?raw=1",
"include_noise": include_noise, "include_noise": include_noise,
"noise_suppressed": noise_suppressed, "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,
}, },
) )
+26 -3
View File
@@ -52,6 +52,10 @@
</div> </div>
</div> </div>
{% if action_message %}
<div class="alert alert-info">{{ action_message }}</div>
{% endif %}
<div class="card border-0 shadow-sm mb-4"> <div class="card border-0 shadow-sm mb-4">
<div class="card-body"> <div class="card-body">
<form method="get" class="row g-2"> <form method="get" class="row g-2">
@@ -70,6 +74,18 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-12 col-md-2">
<input type="date" name="date_from" class="form-control" value="{{ date_from }}" aria-label="Date from">
</div>
<div class="col-12 col-md-2">
<input type="date" name="date_to" class="form-control" value="{{ date_to }}" aria-label="Date to">
</div>
<div class="col-12 col-md-1">
<input type="time" name="time_from" class="form-control" value="{{ time_from }}" aria-label="Time from">
</div>
<div class="col-12 col-md-1">
<input type="time" name="time_to" class="form-control" value="{{ time_to }}" aria-label="Time to">
</div>
<div class="col-12 col-md-2 d-flex align-items-center"> <div class="col-12 col-md-2 d-flex align-items-center">
<div class="form-check mt-1"> <div class="form-check mt-1">
<input class="form-check-input" type="checkbox" value="1" id="include_noise" name="include_noise" {% if include_noise %}checked{% endif %}> <input class="form-check-input" type="checkbox" value="1" id="include_noise" name="include_noise" {% if include_noise %}checked{% endif %}>
@@ -86,15 +102,22 @@
</div> </div>
<div class="col-12 col-md-auto"> <div class="col-12 col-md-auto">
{% if show_all %} {% if show_all %}
<a href="{% url 'logs_view' %}?q={{ search_query|urlencode }}&level={{ level_filter|urlencode }}{% if include_noise %}&include_noise=1{% endif %}" class="btn btn-outline-secondary">Show recent only</a> <a href="{% url 'logs_view' %}?q={{ search_query|urlencode }}&level={{ level_filter|urlencode }}{% if date_from %}&date_from={{ date_from|urlencode }}{% endif %}{% if date_to %}&date_to={{ date_to|urlencode }}{% endif %}{% if time_from %}&time_from={{ time_from|urlencode }}{% endif %}{% if time_to %}&time_to={{ time_to|urlencode }}{% endif %}{% if include_noise %}&include_noise=1{% endif %}" class="btn btn-outline-secondary">Show recent only</a>
{% else %} {% else %}
<a href="{% url 'logs_view' %}?q={{ search_query|urlencode }}&level={{ level_filter|urlencode }}&all=1{% if include_noise %}&include_noise=1{% endif %}" class="btn btn-outline-info">View all logs</a> <a href="{% url 'logs_view' %}?q={{ search_query|urlencode }}&level={{ level_filter|urlencode }}&all=1{% if date_from %}&date_from={{ date_from|urlencode }}{% endif %}{% if date_to %}&date_to={{ date_to|urlencode }}{% endif %}{% if time_from %}&time_from={{ time_from|urlencode }}{% endif %}{% if time_to %}&time_to={{ time_to|urlencode }}{% endif %}{% if include_noise %}&include_noise=1{% endif %}" class="btn btn-outline-info">View all logs</a>
{% endif %} {% endif %}
</div> </div>
<div class="col-12 col-md-auto">
<button type="submit" form="clear-log-form" class="btn btn-outline-danger" onclick="return confirm('Clear the log file?');">Reset log file</button>
</div>
<div class="col-12 col-md-auto"> <div class="col-12 col-md-auto">
<a href="{{ raw_url }}" class="btn btn-outline-dark" target="_blank" rel="noopener">View raw file</a> <a href="{{ raw_url }}" class="btn btn-outline-dark" target="_blank" rel="noopener">View raw file</a>
</div> </div>
</form> </form>
<form id="clear-log-form" method="post" class="d-none">
{% csrf_token %}
<input type="hidden" name="action" value="clear">
</form>
</div> </div>
</div> </div>
@@ -153,7 +176,7 @@
{% endif %} {% endif %}
{% if noise_suppressed and not include_noise %} {% if noise_suppressed and not include_noise %}
Hidden {{ noise_suppressed }} noisy template debug entr{{ noise_suppressed|pluralize:"y,ies" }}. Hidden {{ noise_suppressed }} noisy template debug entr{{ noise_suppressed|pluralize:"y,ies" }}.
<a href="{% url 'logs_view' %}?q={{ search_query|urlencode }}&level={{ level_filter|urlencode }}{% if show_all %}&all=1{% endif %}&include_noise=1" class="alert-link">Show them</a>. <a href="{% url 'logs_view' %}?q={{ search_query|urlencode }}&level={{ level_filter|urlencode }}{% if show_all %}&all=1{% endif %}{% if date_from %}&date_from={{ date_from|urlencode }}{% endif %}{% if date_to %}&date_to={{ date_to|urlencode }}{% endif %}{% if time_from %}&time_from={{ time_from|urlencode }}{% endif %}{% if time_to %}&time_to={{ time_to|urlencode }}{% endif %}&include_noise=1" class="alert-link">Show them</a>.
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}