fix many issues
This commit is contained in:
+10
-5
@@ -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)
|
||||
@@ -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,
|
||||
|
||||
@@ -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") == ""
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
Reference in New Issue
Block a user