improve the linked cases editor
This commit is contained in:
+51
-4
@@ -888,14 +888,22 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_effective_study_date(self):
|
def _get_series_study_date(self):
|
||||||
if self.study_date:
|
|
||||||
return self.study_date
|
|
||||||
|
|
||||||
for series in self.get_ordered_series():
|
for series in self.get_ordered_series():
|
||||||
image = series.images.filter(removed=False).first()
|
image = series.images.filter(removed=False).first()
|
||||||
if not image:
|
if not image:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
tags = getattr(image, "basic_dicom_tags", None) or {}
|
||||||
|
raw_date = (
|
||||||
|
tags.get("StudyDate")
|
||||||
|
or tags.get("AcquisitionDate")
|
||||||
|
or tags.get("SeriesDate")
|
||||||
|
)
|
||||||
|
parsed = self._parse_dicom_date(raw_date)
|
||||||
|
if parsed:
|
||||||
|
return parsed
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ds = image.get_dicom_data()
|
ds = image.get_dicom_data()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -914,6 +922,20 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def get_effective_study_date(self):
|
||||||
|
if self.study_date:
|
||||||
|
return self.study_date
|
||||||
|
|
||||||
|
return self._get_series_study_date()
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if self.pk and not self.study_date:
|
||||||
|
derived_study_date = self._get_series_study_date()
|
||||||
|
if derived_study_date:
|
||||||
|
self.study_date = derived_study_date
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
def get_app_name(self):
|
def get_app_name(self):
|
||||||
return "atlas"
|
return "atlas"
|
||||||
|
|
||||||
@@ -1352,6 +1374,20 @@ class SeriesImage(SeriesImageBase):
|
|||||||
# }}
|
# }}
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=SeriesImage)
|
||||||
|
def _populate_case_study_date_on_series_image_save(sender, instance, **kwargs):
|
||||||
|
if not instance.series_id:
|
||||||
|
return
|
||||||
|
|
||||||
|
tags = instance.basic_dicom_tags or {}
|
||||||
|
raw_date = tags.get("StudyDate") or tags.get("AcquisitionDate") or tags.get("SeriesDate")
|
||||||
|
parsed = Case._parse_dicom_date(raw_date)
|
||||||
|
if not parsed:
|
||||||
|
return
|
||||||
|
|
||||||
|
Case.objects.filter(series=instance.series, study_date__isnull=True).update(study_date=parsed)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_save, sender=SeriesImage)
|
@receiver(pre_save, sender=SeriesImage)
|
||||||
def _capture_previous_series_for_series_image(sender, instance, **kwargs):
|
def _capture_previous_series_for_series_image(sender, instance, **kwargs):
|
||||||
if not getattr(instance, "pk", None):
|
if not getattr(instance, "pk", None):
|
||||||
@@ -2084,6 +2120,17 @@ class SeriesDetail(models.Model):
|
|||||||
class Meta:
|
class Meta:
|
||||||
ordering = ("sort_order",)
|
ordering = ("sort_order",)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=SeriesDetail)
|
||||||
|
def _populate_case_study_date_on_series_link(sender, instance, created, **kwargs):
|
||||||
|
case = instance.case
|
||||||
|
if case.study_date:
|
||||||
|
return
|
||||||
|
|
||||||
|
derived_study_date = case._get_series_study_date()
|
||||||
|
if derived_study_date:
|
||||||
|
Case.objects.filter(pk=case.pk, study_date__isnull=True).update(study_date=derived_study_date)
|
||||||
|
|
||||||
class CaseDisplaySet(models.Model, AuthorMixin):
|
class CaseDisplaySet(models.Model, AuthorMixin):
|
||||||
"""
|
"""
|
||||||
This is analogous to a SeriesFinding but for a Case (it has
|
This is analogous to a SeriesFinding but for a Case (it has
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends "atlas/base.html" %}
|
{% extends "atlas/base.html" %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container py-4">
|
<div class="container py-4">
|
||||||
@@ -15,6 +16,12 @@
|
|||||||
{% if direct_next %}
|
{% if direct_next %}
|
||||||
<a class="btn btn-outline-secondary" href="{% url 'atlas:linked_cases_overview' direct_next.pk %}">Next in chain</a>
|
<a class="btn btn-outline-secondary" href="{% url 'atlas:linked_cases_overview' direct_next.pk %}">Next in chain</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if can_edit_chain and not show_case_editor %}
|
||||||
|
<a class="btn btn-outline-info" href="{% url 'atlas:linked_cases_overview' current_case.pk %}?show_editor=1">Open case series editor</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if show_case_editor %}
|
||||||
|
<a class="btn btn-outline-secondary" href="{% url 'atlas:linked_cases_overview' current_case.pk %}">Hide case series editor</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,6 +40,7 @@
|
|||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="form_action" value="study_dates">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-dark table-hover align-middle mb-0">
|
<table class="table table-dark table-hover align-middle mb-0">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -96,6 +104,77 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if show_case_editor and case_editor_formset %}
|
||||||
|
<div class="card border-info-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">Case Series Editor</h2>
|
||||||
|
<p class="text-muted mb-0">Edit full case details across this linked chain in one pass, then optionally copy a single field from one case to every other case.</p>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">
|
||||||
|
Editable: {{ editable_case_count }}
|
||||||
|
{% if locked_case_count %} · Read-only: {{ locked_case_count }}{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="form_action" value="case_series_editor">
|
||||||
|
{{ case_editor_formset.management_form }}
|
||||||
|
|
||||||
|
<div class="row g-2 align-items-end mb-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label form-label-sm mb-1">Copy field from case</label>
|
||||||
|
<select class="form-select form-select-sm" name="apply_from_case">
|
||||||
|
<option value="">No field propagation</option>
|
||||||
|
{% for row in case_editor_rows %}
|
||||||
|
<option value="{{ row.case.pk }}" {% if case_editor_selected_source == row.case.pk|stringformat:'s' %}selected{% endif %}>
|
||||||
|
{{ row.case.pk }} · {{ row.case.title|default:"Untitled case" }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label class="form-label form-label-sm mb-1">Field to copy to all linked cases</label>
|
||||||
|
<select class="form-select form-select-sm" name="apply_field">
|
||||||
|
<option value="">Choose a field</option>
|
||||||
|
{% for value, label in case_editor_apply_field_choices %}
|
||||||
|
<option value="{{ value }}" {% if case_editor_selected_field == value %}selected{% endif %}>{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="small text-muted">Apply runs after saving any edits you make below.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="accordion" id="linked-case-editor-accordion">
|
||||||
|
{% for row in case_editor_rows %}
|
||||||
|
<div class="accordion-item mb-2 border-secondary-subtle">
|
||||||
|
<h2 class="accordion-header" id="editor-heading-{{ row.case.pk }}">
|
||||||
|
<button class="accordion-button {% if not row.is_current %}collapsed{% endif %}" type="button" data-bs-toggle="collapse" data-bs-target="#editor-body-{{ row.case.pk }}" aria-expanded="{% if row.is_current %}true{% else %}false{% endif %}" aria-controls="editor-body-{{ row.case.pk }}">
|
||||||
|
<span class="fw-semibold">Case {{ row.case.pk }} · {{ row.case.title|default:"Untitled case" }}</span>
|
||||||
|
{% if row.is_current %}<span class="badge text-bg-primary ms-2">Current</span>{% endif %}
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="editor-body-{{ row.case.pk }}" class="accordion-collapse collapse {% if row.is_current %}show{% endif %}" aria-labelledby="editor-heading-{{ row.case.pk }}" data-bs-parent="#linked-case-editor-accordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
{{ row.form|crispy }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-end mt-3">
|
||||||
|
<button type="submit" class="btn btn-info">Save case series editor changes</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">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
import pytest
|
import pytest
|
||||||
from atlas.models import CaseCollection, Finding, Condition
|
from atlas.models import Case, CaseCollection, Finding, Condition
|
||||||
#from atlas.views import GenericExamViews
|
#from atlas.views import GenericExamViews
|
||||||
from pytest_django.asserts import assertRedirects, assertContains, assertQuerySetEqual
|
from pytest_django.asserts import assertRedirects, assertContains, assertQuerySetEqual
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
@@ -179,3 +180,56 @@ def test_linked_cases_overview_can_copy_study_date_to_chain(client, create_user,
|
|||||||
|
|
||||||
assert str(case1.study_date) == "2026-01-10"
|
assert str(case1.study_date) == "2026-01-10"
|
||||||
assert str(case2.study_date) == "2026-01-10"
|
assert str(case2.study_date) == "2026-01-10"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_case_save_auto_populates_study_date_when_missing(monkeypatch, make_case):
|
||||||
|
expected_date = datetime.date(2026, 2, 14)
|
||||||
|
monkeypatch.setattr(Case, "_get_series_study_date", lambda self: expected_date)
|
||||||
|
|
||||||
|
case = make_case(title="Autodate Case")
|
||||||
|
case.study_date = None
|
||||||
|
case.save(update_fields=["study_date"])
|
||||||
|
case.refresh_from_db()
|
||||||
|
|
||||||
|
assert case.study_date == expected_date
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_case_save_preserves_manual_study_date(monkeypatch, make_case):
|
||||||
|
manual_date = datetime.date(2026, 3, 1)
|
||||||
|
monkeypatch.setattr(Case, "_get_series_study_date", lambda self: datetime.date(2026, 4, 1))
|
||||||
|
|
||||||
|
case = make_case(title="Manual Date Case")
|
||||||
|
case.study_date = manual_date
|
||||||
|
case.save(update_fields=["study_date"])
|
||||||
|
case.refresh_from_db()
|
||||||
|
|
||||||
|
assert case.study_date == manual_date
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_linked_cases_editor_is_hidden_until_requested(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_default = client.get(
|
||||||
|
reverse("atlas:linked_cases_overview", kwargs={"case_id": case1.pk})
|
||||||
|
)
|
||||||
|
assert response_default.status_code == 200
|
||||||
|
assertContains(response_default, "Open case series editor")
|
||||||
|
assert "Case Series Editor" not in response_default.content.decode("utf-8")
|
||||||
|
|
||||||
|
response_with_editor = client.get(
|
||||||
|
reverse("atlas:linked_cases_overview", kwargs={"case_id": case1.pk}) + "?show_editor=1"
|
||||||
|
)
|
||||||
|
assert response_with_editor.status_code == 200
|
||||||
|
assertContains(response_with_editor, "Case Series Editor")
|
||||||
|
|||||||
+130
-1
@@ -59,6 +59,7 @@ from django.http import Http404, JsonResponse
|
|||||||
from django.http import HttpResponseRedirect, HttpResponse
|
from django.http import HttpResponseRedirect, HttpResponse
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.forms.widgets import HiddenInput
|
from django.forms.widgets import HiddenInput
|
||||||
|
from django.forms import modelformset_factory
|
||||||
import zipfile
|
import zipfile
|
||||||
import os
|
import os
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
@@ -9545,7 +9546,12 @@ def reorder_series(request, case_pk):
|
|||||||
|
|
||||||
def linked_cases_overview(request, case_id):
|
def linked_cases_overview(request, case_id):
|
||||||
"""
|
"""
|
||||||
Display the linked-case chain around a case, including prior and follow-up cases.
|
Display and manage a linked-case chain.
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- Read-only overview of previous/current/next linked cases.
|
||||||
|
- Study-date editing for relative timeline management.
|
||||||
|
- On-demand bulk case-series editor for updating full case fields across the chain.
|
||||||
"""
|
"""
|
||||||
case = get_object_or_404(Case, pk=case_id)
|
case = get_object_or_404(Case, pk=case_id)
|
||||||
|
|
||||||
@@ -9568,10 +9574,121 @@ def linked_cases_overview(request, case_id):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
chain_cases = previous_cases + [case] + next_cases
|
chain_cases = previous_cases + [case] + next_cases
|
||||||
|
chain_case_ids = [linked_case.pk for linked_case in chain_cases]
|
||||||
|
editable_chain_case_ids = [
|
||||||
|
linked_case.pk
|
||||||
|
for linked_case in chain_cases
|
||||||
|
if linked_case.check_user_can_edit(request.user)
|
||||||
|
]
|
||||||
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
|
||||||
|
|
||||||
|
case_editor_prefix = "case_series_editor"
|
||||||
|
show_case_editor = request.GET.get("show_editor") == "1"
|
||||||
|
case_editor_selected_field = ""
|
||||||
|
case_editor_selected_source = ""
|
||||||
|
|
||||||
|
case_editor_apply_fields = [
|
||||||
|
field_name
|
||||||
|
for field_name in CaseForm.Meta.fields
|
||||||
|
if field_name != "previous_case"
|
||||||
|
]
|
||||||
|
case_editor_apply_field_choices = [
|
||||||
|
(
|
||||||
|
field_name,
|
||||||
|
str(Case._meta.get_field(field_name).verbose_name).replace("_", " ").title(),
|
||||||
|
)
|
||||||
|
for field_name in case_editor_apply_fields
|
||||||
|
]
|
||||||
|
|
||||||
|
case_editor_formset_factory = modelformset_factory(Case, form=CaseForm, extra=0)
|
||||||
|
|
||||||
|
def build_case_editor_formset(data=None):
|
||||||
|
queryset = (
|
||||||
|
Case.objects.filter(pk__in=editable_chain_case_ids)
|
||||||
|
.prefetch_related(
|
||||||
|
"subspecialty",
|
||||||
|
"condition",
|
||||||
|
"presentation",
|
||||||
|
"pathological_process",
|
||||||
|
"procedures",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
formset = case_editor_formset_factory(
|
||||||
|
data=data,
|
||||||
|
queryset=queryset,
|
||||||
|
prefix=case_editor_prefix,
|
||||||
|
form_kwargs={"user": request.user},
|
||||||
|
)
|
||||||
|
forms_by_pk = {form.instance.pk: form for form in formset.forms}
|
||||||
|
ordered_rows = []
|
||||||
|
for linked_case in chain_cases:
|
||||||
|
form = forms_by_pk.get(linked_case.pk)
|
||||||
|
if form is None:
|
||||||
|
continue
|
||||||
|
ordered_rows.append(
|
||||||
|
{
|
||||||
|
"case": linked_case,
|
||||||
|
"form": form,
|
||||||
|
"is_current": linked_case.pk == case.pk,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return formset, ordered_rows
|
||||||
|
|
||||||
|
case_editor_formset = None
|
||||||
|
case_editor_rows = []
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
form_action = request.POST.get("form_action", "study_dates")
|
||||||
|
|
||||||
|
if form_action == "case_series_editor":
|
||||||
|
if not editable_chain_case_ids:
|
||||||
|
raise PermissionDenied("You do not have permission to edit this case chain.")
|
||||||
|
|
||||||
|
show_case_editor = True
|
||||||
|
case_editor_selected_field = request.POST.get("apply_field", "")
|
||||||
|
case_editor_selected_source = request.POST.get("apply_from_case", "")
|
||||||
|
case_editor_formset, case_editor_rows = build_case_editor_formset(request.POST)
|
||||||
|
|
||||||
|
if case_editor_formset.is_valid():
|
||||||
|
case_editor_formset.save()
|
||||||
|
|
||||||
|
applied_count = 0
|
||||||
|
if case_editor_selected_field and case_editor_selected_source:
|
||||||
|
if case_editor_selected_field not in case_editor_apply_fields:
|
||||||
|
messages.error(request, "Select a valid field to apply across the linked series.")
|
||||||
|
else:
|
||||||
|
source_case = Case.objects.filter(
|
||||||
|
pk=case_editor_selected_source,
|
||||||
|
pk__in=editable_chain_case_ids,
|
||||||
|
).first()
|
||||||
|
if source_case is None:
|
||||||
|
messages.error(request, "Select a valid source case for field propagation.")
|
||||||
|
else:
|
||||||
|
target_cases = Case.objects.filter(pk__in=editable_chain_case_ids).exclude(pk=source_case.pk)
|
||||||
|
case_field = Case._meta.get_field(case_editor_selected_field)
|
||||||
|
if case_field.many_to_many:
|
||||||
|
source_values = list(getattr(source_case, case_editor_selected_field).all())
|
||||||
|
for target_case in target_cases:
|
||||||
|
getattr(target_case, case_editor_selected_field).set(source_values)
|
||||||
|
applied_count += 1
|
||||||
|
else:
|
||||||
|
source_value = getattr(source_case, case_editor_selected_field)
|
||||||
|
for target_case in target_cases:
|
||||||
|
if getattr(target_case, case_editor_selected_field) != source_value:
|
||||||
|
setattr(target_case, case_editor_selected_field, source_value)
|
||||||
|
target_case.save(update_fields=[case_editor_selected_field])
|
||||||
|
applied_count += 1
|
||||||
|
|
||||||
|
if applied_count:
|
||||||
|
messages.success(request, f"Applied {case_editor_selected_field} to {applied_count} linked case(s).")
|
||||||
|
|
||||||
|
messages.success(request, "Saved linked case editor changes.")
|
||||||
|
return redirect(f"{reverse('atlas:linked_cases_overview', kwargs={'case_id': case.pk})}?show_editor=1")
|
||||||
|
|
||||||
|
messages.error(request, "Please correct the errors in the linked case editor form.")
|
||||||
|
|
||||||
|
else:
|
||||||
if not case.check_user_can_edit(request.user):
|
if not case.check_user_can_edit(request.user):
|
||||||
raise PermissionDenied("You do not have permission to edit this case chain.")
|
raise PermissionDenied("You do not have permission to edit this case chain.")
|
||||||
|
|
||||||
@@ -9607,6 +9724,9 @@ def linked_cases_overview(request, case_id):
|
|||||||
|
|
||||||
return redirect("atlas:linked_cases_overview", case_id=case.pk)
|
return redirect("atlas:linked_cases_overview", case_id=case.pk)
|
||||||
|
|
||||||
|
if show_case_editor and editable_chain_case_ids:
|
||||||
|
case_editor_formset, case_editor_rows = build_case_editor_formset()
|
||||||
|
|
||||||
linked_case_rows = []
|
linked_case_rows = []
|
||||||
anchor_date = next(
|
anchor_date = next(
|
||||||
(linked_case.get_effective_study_date() for linked_case in chain_cases if linked_case.get_effective_study_date()),
|
(linked_case.get_effective_study_date() for linked_case in chain_cases if linked_case.get_effective_study_date()),
|
||||||
@@ -9642,6 +9762,15 @@ def linked_cases_overview(request, case_id):
|
|||||||
"current_index": len(previous_cases) + 1,
|
"current_index": len(previous_cases) + 1,
|
||||||
"direct_previous": direct_previous,
|
"direct_previous": direct_previous,
|
||||||
"direct_next": direct_next,
|
"direct_next": direct_next,
|
||||||
|
"show_case_editor": show_case_editor,
|
||||||
|
"case_editor_formset": case_editor_formset,
|
||||||
|
"case_editor_rows": case_editor_rows,
|
||||||
|
"case_editor_apply_field_choices": case_editor_apply_field_choices,
|
||||||
|
"case_editor_selected_field": case_editor_selected_field,
|
||||||
|
"case_editor_selected_source": case_editor_selected_source,
|
||||||
|
"case_editor_prefix": case_editor_prefix,
|
||||||
|
"editable_case_count": len(editable_chain_case_ids),
|
||||||
|
"locked_case_count": len(chain_case_ids) - len(editable_chain_case_ids),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user