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:
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
@@ -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 }}">
<i class="bi bi-eye"></i> View
</button>
<div class="series-card-inline-actions">
@@ -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">
<i class="bi bi-layout-split"></i> 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);
});
@@ -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)
except Exception as exc:
logger.exception("Error during uploads import")
logging.getLogger(__name__).exception("Error during uploads import")
return HttpResponse(
f'<div class="alert alert-danger mb-0">Import failed: {escape(str(exc))}</div>',
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 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,
},
)
+26 -3
View File
@@ -52,6 +52,10 @@
</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-body">
<form method="get" class="row g-2">
@@ -70,6 +74,18 @@
{% endfor %}
</select>
</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="form-check mt-1">
<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 class="col-12 col-md-auto">
{% 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 %}
<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 %}
</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">
<a href="{{ raw_url }}" class="btn btn-outline-dark" target="_blank" rel="noopener">View raw file</a>
</div>
</form>
<form id="clear-log-form" method="post" class="d-none">
{% csrf_token %}
<input type="hidden" name="action" value="clear">
</form>
</div>
</div>
@@ -153,7 +176,7 @@
{% endif %}
{% if noise_suppressed and not include_noise %}
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 %}
</div>
{% else %}