start working on case_study bulk editiing and add relative dates to the model
This commit is contained in:
+4
-2
@@ -746,7 +746,8 @@ class CaseForm(ModelForm):
|
|||||||
"open_access",
|
"open_access",
|
||||||
"previous_case",
|
"previous_case",
|
||||||
"diagnostic_certainty",
|
"diagnostic_certainty",
|
||||||
"cimar_uuid",
|
#cimar_uuid",
|
||||||
|
"study_date",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -779,7 +780,8 @@ class CaseForm(ModelForm):
|
|||||||
"open_access",
|
"open_access",
|
||||||
"previous_case",
|
"previous_case",
|
||||||
"diagnostic_certainty",
|
"diagnostic_certainty",
|
||||||
"cimar_uuid",
|
#cimar_uuid",
|
||||||
|
"study_date",
|
||||||
]
|
]
|
||||||
# fields = ['question', 'findings', 'subspecialty', 'references']
|
# fields = ['question', 'findings', 'subspecialty', 'references']
|
||||||
widgets = {
|
widgets = {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.1 on 2026-06-01 09:01
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('atlas', '0104_series_source_series_instance_uid'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='case',
|
||||||
|
name='study_date',
|
||||||
|
field=models.DateField(blank=True, help_text='Optional manually overridden study date for linked case series.', null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -840,6 +840,12 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
|
|
||||||
cimar_uuid = models.CharField(max_length=255, null=True, blank=True)
|
cimar_uuid = models.CharField(max_length=255, null=True, blank=True)
|
||||||
|
|
||||||
|
study_date = models.DateField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Optional manually overridden study date for linked case series.",
|
||||||
|
)
|
||||||
|
|
||||||
def get_ordered_series_details(self):
|
def get_ordered_series_details(self):
|
||||||
cached = getattr(self, "_ordered_series_details_cache", None)
|
cached = getattr(self, "_ordered_series_details_cache", None)
|
||||||
if cached is not None:
|
if cached is not None:
|
||||||
@@ -873,6 +879,41 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
self._ordered_series_cache = ordered
|
self._ordered_series_cache = ordered
|
||||||
return ordered
|
return ordered
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_dicom_date(value):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(str(value), "%Y%m%d").date()
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_effective_study_date(self):
|
||||||
|
if self.study_date:
|
||||||
|
return self.study_date
|
||||||
|
|
||||||
|
for series in self.get_ordered_series():
|
||||||
|
image = series.images.filter(removed=False).first()
|
||||||
|
if not image:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ds = image.get_dicom_data()
|
||||||
|
except Exception:
|
||||||
|
ds = None
|
||||||
|
if not ds:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raw_date = (
|
||||||
|
getattr(ds, "StudyDate", None)
|
||||||
|
or getattr(ds, "AcquisitionDate", None)
|
||||||
|
or getattr(ds, "SeriesDate", None)
|
||||||
|
)
|
||||||
|
parsed = self._parse_dicom_date(raw_date)
|
||||||
|
if parsed:
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def get_app_name(self):
|
def get_app_name(self):
|
||||||
return "atlas"
|
return "atlas"
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,84 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if can_edit_chain %}
|
||||||
|
<div class="card border-secondary-subtle shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="h5 mb-1">Study Dates</h2>
|
||||||
|
<p class="text-muted mb-0">Edit each case date directly, or use the copy action to apply one case date to the rest of the linked chain.</p>
|
||||||
|
</div>
|
||||||
|
{% if anchor_date %}
|
||||||
|
<div class="small text-muted">Relative offsets are measured from {{ anchor_date|date:"Y-m-d" }}.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-hover align-middle mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Case</th>
|
||||||
|
<th scope="col" class="text-nowrap">Study date</th>
|
||||||
|
<th scope="col" class="text-nowrap">Relative</th>
|
||||||
|
<th scope="col" class="text-nowrap">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in linked_case_rows %}
|
||||||
|
<tr class="{% if row.is_current %}table-primary{% endif %}">
|
||||||
|
<td>
|
||||||
|
<div class="fw-semibold">{{ row.case.title|default:"Untitled case" }}</div>
|
||||||
|
<div class="small text-muted">Case {{ row.case.pk }}{% if row.is_current %} · current{% endif %}</div>
|
||||||
|
</td>
|
||||||
|
<td style="min-width: 14rem;">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
class="form-control form-control-sm"
|
||||||
|
name="study_date_{{ row.case.pk }}"
|
||||||
|
value="{% if row.study_date %}{{ row.study_date|date:'Y-m-d' }}{% endif %}"
|
||||||
|
{% if not row.can_edit %}disabled{% endif %}
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{% if row.relative_days is not None %}
|
||||||
|
{% if row.relative_days == 0 %}
|
||||||
|
Day 0
|
||||||
|
{% elif row.relative_days > 0 %}
|
||||||
|
+{{ row.relative_days }} day{{ row.relative_days|pluralize }}
|
||||||
|
{% else %}
|
||||||
|
{{ row.relative_days }} day{{ row.relative_days|pluralize }}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">No anchor</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-sm btn-outline-info"
|
||||||
|
name="apply_to_all_from"
|
||||||
|
value="{{ row.case.pk }}"
|
||||||
|
{% if not row.can_edit %}disabled{% endif %}
|
||||||
|
>
|
||||||
|
Copy to others
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap justify-content-end gap-2 mt-3">
|
||||||
|
<button type="submit" class="btn btn-primary">Save study dates</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card border-secondary-subtle shadow-sm h-100">
|
<div class="card border-secondary-subtle shadow-sm h-100">
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm-end small text-muted">
|
<div class="text-sm-end small text-muted">
|
||||||
{% if linked_case.published_date %}
|
{% if linked_case.get_effective_study_date %}
|
||||||
|
<div>Study date {{ linked_case.get_effective_study_date|date:"Y-m-d" }}</div>
|
||||||
|
{% elif linked_case.published_date %}
|
||||||
<div>Published {{ linked_case.published_date|date:"Y-m-d" }}</div>
|
<div>Published {{ linked_case.published_date|date:"Y-m-d" }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div>Created {{ linked_case.created_date|date:"Y-m-d" }}</div>
|
<div>Created {{ linked_case.created_date|date:"Y-m-d" }}</div>
|
||||||
|
|||||||
+172
-6
@@ -1,15 +1,181 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import pytest
|
|
||||||
|
|
||||||
# from django.contrib.auth.models import User
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
import pytest
|
||||||
|
from atlas.models import CaseCollection, Finding, Condition
|
||||||
|
#from atlas.views import GenericExamViews
|
||||||
|
from pytest_django.asserts import assertRedirects, assertContains, assertQuerySetEqual
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
from rich.pretty import pprint
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
#@pytest.mark.django_db
|
||||||
|
def test_create_condition(db):
|
||||||
|
abnormality = Condition.objects.create(name="Abnorm")
|
||||||
|
assert abnormality.name == "Abnorm"
|
||||||
|
|
||||||
from django.contrib.auth.models import Group
|
|
||||||
|
def test_condition_tree_structure(db):
|
||||||
|
parent = Condition.objects.create(name="parent")
|
||||||
|
|
||||||
|
child1 = Condition.objects.create(name="child1")
|
||||||
|
child2 = Condition.objects.create(name="child2")
|
||||||
|
|
||||||
|
parent.child.set([child1, child2])
|
||||||
|
|
||||||
|
child1_1 = Condition.objects.create(name="child1_1")
|
||||||
|
child1_2 = Condition.objects.create(name="child1_2")
|
||||||
|
|
||||||
|
child1.child.set([child1_1, child1_2])
|
||||||
|
|
||||||
|
assert len(parent.get_children()) == 2
|
||||||
|
assert len(child1.get_children()) == 2
|
||||||
|
|
||||||
|
assert len(child1.get_parents()) == 1
|
||||||
|
assert parent in child1.get_parents()
|
||||||
|
|
||||||
|
assert len(child2.get_parents()) == 1
|
||||||
|
assert parent in child2.get_parents()
|
||||||
|
|
||||||
|
|
||||||
|
#@pytest.fixture
|
||||||
|
#def create_basic_user(db, django_user_model):
|
||||||
|
# return django_user_model.objects.create_superuser("basicuser", "ross@xkjq.uk", "password")
|
||||||
|
#
|
||||||
|
#@pytest.fixture
|
||||||
|
#def create_exam(db):
|
||||||
|
# return Exam.objects.create(name="test exam", exam_mode=True)
|
||||||
|
#
|
||||||
|
#def test_exam_creation(create_exam):
|
||||||
|
# exams = Exam.objects.filter(name="test exam")
|
||||||
|
# assert exams.exists()
|
||||||
|
#
|
||||||
|
# exam = exams.first()
|
||||||
|
#
|
||||||
|
# assert exam.time_limit == 35 * 60
|
||||||
|
#
|
||||||
|
#def test_exam_list(rf, admin_user, create_exam):
|
||||||
|
# request = rf.get('/rapids/exam/')
|
||||||
|
# # Remember that when using RequestFactory, the request does not pass
|
||||||
|
# # through middleware. If your view expects fields such as request.user to be
|
||||||
|
# # set, you need to set them explicitly.
|
||||||
|
# # The following line sets request.user to an admin user.
|
||||||
|
# request.user = admin_user
|
||||||
|
# response = GenericExamViews.exam_list(request)
|
||||||
|
# #print(response)
|
||||||
|
# assert response.status_code == 200
|
||||||
|
#
|
||||||
|
#def test_exam_active_admin(rf, admin_user, create_exam, client):
|
||||||
|
# request = rf.get(reverse('rapids:active_exams'))
|
||||||
|
# request.user = admin_user
|
||||||
|
# response = GenericExamViews.active_exams(request)
|
||||||
|
# print(response.content)
|
||||||
|
# assert response.status_code == 200
|
||||||
|
#
|
||||||
|
#def test_exam_active_basic_user(create_basic_user, create_exam, client):
|
||||||
|
# client.login(username="basicuser", password="password")
|
||||||
|
# response = client.get(reverse('rapids:active_exams'))
|
||||||
|
# print(response.content)
|
||||||
|
# assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_create_case(db, create_case):
|
||||||
|
case = create_case
|
||||||
|
assert case.title == "Sample Case"
|
||||||
|
|
||||||
|
def test_create_series(db, create_series):
|
||||||
|
series = create_series
|
||||||
|
assert series
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_collection(db, create_case_collection, create_case, make_case, client, create_user):
|
||||||
|
collection: CaseCollection = create_case_collection
|
||||||
|
|
||||||
|
# create some cases
|
||||||
|
case1 = create_case
|
||||||
|
case2 = make_case()
|
||||||
|
case3 = make_case(title="Case 3")
|
||||||
|
case4 = make_case(title="Case 4")
|
||||||
|
|
||||||
|
collection.cases.set([case1, case2, case3, case4])
|
||||||
|
|
||||||
|
case5 = make_case(title="Case 5")
|
||||||
|
|
||||||
|
assert collection.cases.all().count() == 4
|
||||||
|
|
||||||
|
collection.add_case(case5)
|
||||||
|
|
||||||
|
assert collection.cases.all().count() == 5
|
||||||
|
|
||||||
|
assert collection.get_case_by_index(0) == case1
|
||||||
|
assert collection.get_case_by_index(1) == case2
|
||||||
|
assert collection.get_case_by_index(2) == case3
|
||||||
|
assert collection.get_case_by_index(3) == case4
|
||||||
|
assert collection.get_case_by_index(4) == case5
|
||||||
|
|
||||||
|
assert collection.get_absolute_url() == reverse("atlas:collection_detail", kwargs={"pk": collection.pk})
|
||||||
|
|
||||||
|
assert collection.get_take_url() == reverse("atlas:collection_take_start", kwargs={"pk": collection.pk})
|
||||||
|
|
||||||
|
start_order = collection.get_cases()
|
||||||
|
|
||||||
|
new_order = [
|
||||||
|
case5.pk,
|
||||||
|
case1.pk,
|
||||||
|
case2.pk,
|
||||||
|
case3.pk,
|
||||||
|
case4.pk
|
||||||
|
]
|
||||||
|
response: HttpResponse = client.post(reverse("atlas:exam_json_edit", kwargs={"pk":collection.pk} ), json=new_order, follow=True)
|
||||||
|
# Check that sorting the exam works
|
||||||
|
|
||||||
|
# First attempt is not logged in
|
||||||
|
assertRedirects(response, f"{reverse('login')}?next=%2Fatlas%2Fcollection%2F1%2Fjson_edit")
|
||||||
|
|
||||||
|
client.force_login(create_user)
|
||||||
|
|
||||||
|
# Random user cannot modify order
|
||||||
|
response: HttpResponse = client.post(reverse("atlas:exam_json_edit", kwargs={"pk":collection.pk} ), json=new_order, follow=True)
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
assertQuerySetEqual(start_order , collection.get_cases())
|
||||||
|
|
||||||
|
# Once the user has been made an author, they can modify the order
|
||||||
|
collection.add_author(create_user)
|
||||||
|
response: HttpResponse = client.post(reverse("atlas:exam_json_edit", kwargs={"pk":collection.pk} ), data={"set_exam_order" : json.dumps(new_order)}, follow=True)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
assert collection.get_cases().count() == 5
|
||||||
|
assert collection.get_case_by_index(0) == case5
|
||||||
|
assert collection.get_case_by_index(1) == case1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_linked_cases_overview_can_copy_study_date_to_chain(client, create_user, make_case):
|
||||||
|
user = create_user
|
||||||
|
case1 = make_case(title="Case 1")
|
||||||
|
case2 = make_case(title="Case 2")
|
||||||
|
case2.previous_case = case1
|
||||||
|
case2.save(update_fields=["previous_case"])
|
||||||
|
|
||||||
|
case1.author.add(user)
|
||||||
|
case2.author.add(user)
|
||||||
|
|
||||||
|
client.force_login(user)
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
reverse("atlas:linked_cases_overview", kwargs={"case_id": case1.pk}),
|
||||||
|
data={
|
||||||
|
f"study_date_{case1.pk}": "2026-01-10",
|
||||||
|
f"study_date_{case2.pk}": "2026-01-12",
|
||||||
|
"apply_to_all_from": str(case1.pk),
|
||||||
|
},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
case1.refresh_from_db()
|
||||||
|
case2.refresh_from_db()
|
||||||
|
|
||||||
|
assert str(case1.study_date) == "2026-01-10"
|
||||||
|
assert str(case2.study_date) == "2026-01-10"
|
||||||
|
|||||||
+89
-7
@@ -4,6 +4,7 @@ import csv
|
|||||||
import random
|
import random
|
||||||
import copy
|
import copy
|
||||||
import io
|
import io
|
||||||
|
import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
@@ -168,6 +169,31 @@ from .filters import (
|
|||||||
NormalCaseFilter,
|
NormalCaseFilter,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_study_date(value):
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
parsed_value = str(value)
|
||||||
|
for date_format in ("%Y%m%d", "%Y-%m-%d"):
|
||||||
|
try:
|
||||||
|
return datetime.datetime.strptime(parsed_value, date_format).date()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_study_date_from_dicoms(dicoms):
|
||||||
|
for dicom in dicoms.iterator():
|
||||||
|
tags = dicom.basic_dicom_tags or {}
|
||||||
|
for key in ("StudyDate", "AcquisitionDate", "SeriesDate"):
|
||||||
|
study_date = _parse_study_date(tags.get(key))
|
||||||
|
if study_date:
|
||||||
|
return study_date
|
||||||
|
return None
|
||||||
|
|
||||||
from .tasks import push_case_to_cimar_task, series_downsample_task, series_reconstruct_task
|
from .tasks import push_case_to_cimar_task, series_downsample_task, series_reconstruct_task
|
||||||
try:
|
try:
|
||||||
from django_tasks import TaskResultStatus
|
from django_tasks import TaskResultStatus
|
||||||
@@ -3637,14 +3663,15 @@ def uploads_import_case_series_htmx(request):
|
|||||||
for field_name in case_field_names:
|
for field_name in case_field_names:
|
||||||
setattr(case, field_name, getattr(base_case, field_name))
|
setattr(case, field_name, getattr(base_case, field_name))
|
||||||
case.previous_case = previous_case
|
case.previous_case = previous_case
|
||||||
|
study_series_uids = study_groups[study_uid]
|
||||||
|
study_dicoms = dicoms.filter(series_instance_uid__in=study_series_uids)
|
||||||
|
case.study_date = _extract_study_date_from_dicoms(study_dicoms)
|
||||||
case.save()
|
case.save()
|
||||||
case.author.add(request.user)
|
case.author.add(request.user)
|
||||||
|
|
||||||
for field_name in m2m_field_names:
|
for field_name in m2m_field_names:
|
||||||
getattr(case, field_name).set(form.cleaned_data.get(field_name, []))
|
getattr(case, field_name).set(form.cleaned_data.get(field_name, []))
|
||||||
|
|
||||||
study_series_uids = study_groups[study_uid]
|
|
||||||
study_dicoms = dicoms.filter(series_instance_uid__in=study_series_uids)
|
|
||||||
imported_rows = import_dicoms_helper(request, case_id=case.pk, dicoms=study_dicoms)
|
imported_rows = import_dicoms_helper(request, case_id=case.pk, dicoms=study_dicoms)
|
||||||
created_cases.append(case)
|
created_cases.append(case)
|
||||||
import_results.append((case, study_uid, imported_rows))
|
import_results.append((case, study_uid, imported_rows))
|
||||||
@@ -9520,20 +9547,17 @@ def linked_cases_overview(request, case_id):
|
|||||||
"""
|
"""
|
||||||
Display the linked-case chain around a case, including prior and follow-up cases.
|
Display the linked-case chain around a case, including prior and follow-up cases.
|
||||||
"""
|
"""
|
||||||
# Get the current case
|
|
||||||
case = get_object_or_404(Case, pk=case_id)
|
case = get_object_or_404(Case, pk=case_id)
|
||||||
|
|
||||||
# Traverse backward to get all previous cases
|
|
||||||
previous_cases = []
|
previous_cases = []
|
||||||
try:
|
try:
|
||||||
current_case = case.previous_case
|
current_case = case.previous_case
|
||||||
while current_case:
|
while current_case:
|
||||||
previous_cases.insert(0, current_case) # Insert at the beginning to maintain order
|
previous_cases.insert(0, current_case)
|
||||||
current_case = current_case.previous_case
|
current_case = current_case.previous_case
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Traverse forward to get all next cases
|
|
||||||
next_cases = []
|
next_cases = []
|
||||||
try:
|
try:
|
||||||
current_case = case.next_case
|
current_case = case.next_case
|
||||||
@@ -9547,7 +9571,62 @@ def linked_cases_overview(request, case_id):
|
|||||||
direct_previous = previous_cases[-1] if previous_cases else None
|
direct_previous = previous_cases[-1] if previous_cases else None
|
||||||
direct_next = next_cases[0] if next_cases else None
|
direct_next = next_cases[0] if next_cases else None
|
||||||
|
|
||||||
# Render the template with the linked cases
|
if request.method == "POST":
|
||||||
|
if not case.check_user_can_edit(request.user):
|
||||||
|
raise PermissionDenied("You do not have permission to edit this case chain.")
|
||||||
|
|
||||||
|
apply_to_all_from = request.POST.get("apply_to_all_from")
|
||||||
|
updated_cases = 0
|
||||||
|
|
||||||
|
if apply_to_all_from:
|
||||||
|
source_case = next(
|
||||||
|
(linked_case for linked_case in chain_cases if str(linked_case.pk) == apply_to_all_from),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if source_case and source_case.check_user_can_edit(request.user):
|
||||||
|
source_date = _parse_study_date(request.POST.get(f"study_date_{source_case.pk}"))
|
||||||
|
for linked_case in chain_cases:
|
||||||
|
if not linked_case.check_user_can_edit(request.user):
|
||||||
|
continue
|
||||||
|
if linked_case.study_date != source_date:
|
||||||
|
linked_case.study_date = source_date
|
||||||
|
linked_case.save(update_fields=["study_date"])
|
||||||
|
updated_cases += 1
|
||||||
|
messages.success(request, f"Applied the study date to {updated_cases} case(s).")
|
||||||
|
else:
|
||||||
|
for linked_case in chain_cases:
|
||||||
|
if not linked_case.check_user_can_edit(request.user):
|
||||||
|
continue
|
||||||
|
raw_value = request.POST.get(f"study_date_{linked_case.pk}", "").strip()
|
||||||
|
new_date = _parse_study_date(raw_value)
|
||||||
|
if linked_case.study_date != new_date:
|
||||||
|
linked_case.study_date = new_date
|
||||||
|
linked_case.save(update_fields=["study_date"])
|
||||||
|
updated_cases += 1
|
||||||
|
messages.success(request, f"Updated {updated_cases} linked case date(s).")
|
||||||
|
|
||||||
|
return redirect("atlas:linked_cases_overview", case_id=case.pk)
|
||||||
|
|
||||||
|
linked_case_rows = []
|
||||||
|
anchor_date = next(
|
||||||
|
(linked_case.get_effective_study_date() for linked_case in chain_cases if linked_case.get_effective_study_date()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
for linked_case in chain_cases:
|
||||||
|
study_date = linked_case.get_effective_study_date()
|
||||||
|
relative_days = None
|
||||||
|
if anchor_date and study_date:
|
||||||
|
relative_days = (study_date - anchor_date).days
|
||||||
|
linked_case_rows.append(
|
||||||
|
{
|
||||||
|
"case": linked_case,
|
||||||
|
"study_date": study_date,
|
||||||
|
"relative_days": relative_days,
|
||||||
|
"can_edit": linked_case.check_user_can_edit(request.user),
|
||||||
|
"is_current": linked_case.pk == case.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"atlas/linked_cases_overview.html",
|
"atlas/linked_cases_overview.html",
|
||||||
@@ -9556,6 +9635,9 @@ def linked_cases_overview(request, case_id):
|
|||||||
"previous_cases": previous_cases,
|
"previous_cases": previous_cases,
|
||||||
"next_cases": next_cases,
|
"next_cases": next_cases,
|
||||||
"chain_cases": chain_cases,
|
"chain_cases": chain_cases,
|
||||||
|
"linked_case_rows": linked_case_rows,
|
||||||
|
"anchor_date": anchor_date,
|
||||||
|
"can_edit_chain": any(row["can_edit"] for row in linked_case_rows),
|
||||||
"chain_total": len(chain_cases),
|
"chain_total": len(chain_cases),
|
||||||
"current_index": len(previous_cases) + 1,
|
"current_index": len(previous_cases) + 1,
|
||||||
"direct_previous": direct_previous,
|
"direct_previous": direct_previous,
|
||||||
|
|||||||
+179
-179
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user