feat: Add functionality to move selected series between cases with a new form and UI enhancements
This commit is contained in:
@@ -117,6 +117,26 @@ class CaseSelect(Select):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MoveSelectedSeriesForm(Form):
|
||||||
|
destination_case = ModelChoiceField(
|
||||||
|
queryset=Case.objects.none(),
|
||||||
|
widget=CaseSelect(attrs={"class": "form-select form-select-sm"}),
|
||||||
|
label="Move selected series to case",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
user = kwargs.pop("user", None)
|
||||||
|
source_case = kwargs.pop("source_case", None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
available_cases = get_cases_available_to_user(user)
|
||||||
|
if source_case is not None:
|
||||||
|
available_cases = available_cases.exclude(pk=source_case.pk)
|
||||||
|
|
||||||
|
self.fields["destination_case"].queryset = available_cases.order_by("pk")
|
||||||
|
|
||||||
|
|
||||||
class ConditionForm(ModelForm):
|
class ConditionForm(ModelForm):
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Condition
|
model = Condition
|
||||||
@@ -268,6 +288,7 @@ class CaseCollectionForm(ModelForm):
|
|||||||
"exam_mode",
|
"exam_mode",
|
||||||
"self_review",
|
"self_review",
|
||||||
"feedback_once_collection_complete",
|
"feedback_once_collection_complete",
|
||||||
|
"case_query_messaging_enabled",
|
||||||
"viewer_mode",
|
"viewer_mode",
|
||||||
"question_time_limit",
|
"question_time_limit",
|
||||||
"prerequisites",
|
"prerequisites",
|
||||||
|
|||||||
@@ -417,8 +417,16 @@
|
|||||||
Manage Series
|
Manage Series
|
||||||
</button>
|
</button>
|
||||||
</summary>
|
</summary>
|
||||||
<a href="{% url 'atlas:series_id_create' pk=case.pk %}">Create and add new series</a><br />
|
<div class="series-actions-body">
|
||||||
<button class="btn btn-sm btn-outline-secondary mb-1" hx-get="{% url 'atlas:case_order_dicom' pk=case.pk %}"
|
<a class="btn btn-sm btn-outline-secondary text-start" href="{% url 'atlas:series_id_create' pk=case.pk %}">Create and add new series</a>
|
||||||
|
|
||||||
|
<div class="series-selection-toolbar d-flex flex-wrap gap-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" id="series-select-all-btn">Select all</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" id="series-deselect-all-btn">Deselect all</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="series-actions-grid d-flex flex-column gap-2">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary text-start" hx-get="{% url 'atlas:case_order_dicom' pk=case.pk %}"
|
||||||
title="order dicom by slice location"
|
title="order dicom by slice location"
|
||||||
hx-target="#series-action-results"
|
hx-target="#series-action-results"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
@@ -426,7 +434,7 @@
|
|||||||
>
|
>
|
||||||
Order dicoms by slice location
|
Order dicoms by slice location
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-primary mb-1" hx-post="{% url 'atlas:combine_series' %}"
|
<button class="btn btn-sm btn-outline-primary text-start" hx-post="{% url 'atlas:combine_series' %}"
|
||||||
title="merge series"
|
title="merge series"
|
||||||
hx-include="[name='series-ids']"
|
hx-include="[name='series-ids']"
|
||||||
hx-target="#series-action-results"
|
hx-target="#series-action-results"
|
||||||
@@ -435,8 +443,8 @@
|
|||||||
>
|
>
|
||||||
Merge selected series
|
Merge selected series
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-sm btn-outline-secondary mb-1" hx-post="{% url 'atlas:use_dates_as_descriptions' case.pk %}"
|
<button class="btn btn-sm btn-outline-secondary text-start" hx-post="{% url 'atlas:use_dates_as_descriptions' case.pk %}"
|
||||||
title="merge series"
|
title="use dicom dates as series descriptions"
|
||||||
hx-include="[name='series-ids']"
|
hx-include="[name='series-ids']"
|
||||||
hx-target="#series-action-results"
|
hx-target="#series-action-results"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
@@ -450,17 +458,39 @@
|
|||||||
hx-target="#series-action-results"
|
hx-target="#series-action-results"
|
||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-confirm="Are you sure you want to remove the selected series from this case? They will not be deleted from the database."
|
hx-confirm="Are you sure you want to remove the selected series from this case? They will not be deleted from the database."
|
||||||
class="btn btn-warning mt-2"
|
class="btn btn-sm btn-warning text-start"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Remove selected series from case
|
Remove selected series from case
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-secondary mt-2"
|
class="btn btn-sm btn-secondary text-start"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#reorderSeriesModal">
|
data-bs-target="#reorderSeriesModal">
|
||||||
Reorder series
|
Reorder series
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="series-move-panel border rounded p-2">
|
||||||
|
<div class="small fw-semibold mb-2">Move selected series to another case</div>
|
||||||
|
{% if move_series_form %}
|
||||||
|
<form id="move-selected-series-form" class="d-flex flex-column gap-2">
|
||||||
|
{{ move_series_form.destination_case }}
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-primary text-start"
|
||||||
|
hx-post="{% url 'atlas:move_selected_series_to_case' case.pk %}"
|
||||||
|
hx-include="[name='series-ids']:checked, #move-selected-series-form [name='destination_case']"
|
||||||
|
hx-target="#series-action-results"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-confirm="Move selected series to the chosen destination case?">
|
||||||
|
Move selected series
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-muted small">Case move options are unavailable in this view.</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="alert alert-info mt-2" id="series-action-alert" style="display:none;">
|
<div class="alert alert-info mt-2" id="series-action-alert" style="display:none;">
|
||||||
<span id="series-action-results" ></span>
|
<span id="series-action-results" ></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -901,32 +931,80 @@
|
|||||||
|
|
||||||
const detailsElement = document.getElementById("series-actions");
|
const detailsElement = document.getElementById("series-actions");
|
||||||
const buttons = document.querySelectorAll(".select-series-btn");
|
const buttons = document.querySelectorAll(".select-series-btn");
|
||||||
|
const seriesCheckboxes = document.querySelectorAll("input[type='checkbox'][name='series-ids']");
|
||||||
|
const selectAllBtn = document.getElementById("series-select-all-btn");
|
||||||
|
const deselectAllBtn = document.getElementById("series-deselect-all-btn");
|
||||||
|
|
||||||
function toggleCheckboxes() {
|
function toggleCheckboxes() {
|
||||||
const isOpen = detailsElement.hasAttribute("open");
|
const isOpen = detailsElement && detailsElement.hasAttribute("open");
|
||||||
buttons.forEach(button => {
|
buttons.forEach((button) => {
|
||||||
button.style.display = isOpen ? "inline-block" : "none";
|
button.style.display = isOpen ? "inline-block" : "none";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncSeriesSelectionUi() {
|
||||||
document.querySelectorAll(".select-series-btn").forEach(function(btn) {
|
document.querySelectorAll(".select-series-btn").forEach(function(btn) {
|
||||||
btn.addEventListener("click", function() {
|
|
||||||
const parent = btn.closest(".series-block");
|
const parent = btn.closest(".series-block");
|
||||||
|
if (!parent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const checkbox = parent.querySelector("input[type='checkbox'][name='series-ids']");
|
const checkbox = parent.querySelector("input[type='checkbox'][name='series-ids']");
|
||||||
checkbox.checked = !checkbox.checked;
|
if (!checkbox) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
btn.classList.toggle("btn-primary", checkbox.checked);
|
btn.classList.toggle("btn-primary", checkbox.checked);
|
||||||
btn.classList.toggle("btn-outline-primary", !checkbox.checked);
|
btn.classList.toggle("btn-outline-primary", !checkbox.checked);
|
||||||
btn.textContent = checkbox.checked ? "Selected" : "Select";
|
btn.textContent = checkbox.checked ? "Selected" : "Select";
|
||||||
// Highlight the series-block when selected
|
|
||||||
if (checkbox.checked) {
|
if (checkbox.checked) {
|
||||||
parent.classList.add("highlight-series");
|
parent.classList.add("highlight-series");
|
||||||
} else {
|
} else {
|
||||||
parent.classList.remove("highlight-series");
|
parent.classList.remove("highlight-series");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedCount = Array.from(seriesCheckboxes).filter((checkbox) => checkbox.checked).length;
|
||||||
|
if (selectAllBtn) {
|
||||||
|
selectAllBtn.disabled = selectedCount === seriesCheckboxes.length && seriesCheckboxes.length > 0;
|
||||||
|
}
|
||||||
|
if (deselectAllBtn) {
|
||||||
|
deselectAllBtn.disabled = selectedCount === 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll(".select-series-btn").forEach(function(btn) {
|
||||||
|
btn.addEventListener("click", function() {
|
||||||
|
const parent = btn.closest(".series-block");
|
||||||
|
const checkbox = parent.querySelector("input[type='checkbox'][name='series-ids']");
|
||||||
|
checkbox.checked = !checkbox.checked;
|
||||||
|
syncSeriesSelectionUi();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
seriesCheckboxes.forEach((checkbox) => {
|
||||||
|
checkbox.addEventListener("change", syncSeriesSelectionUi);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectAllBtn) {
|
||||||
|
selectAllBtn.addEventListener("click", function() {
|
||||||
|
seriesCheckboxes.forEach((checkbox) => {
|
||||||
|
checkbox.checked = true;
|
||||||
|
});
|
||||||
|
syncSeriesSelectionUi();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deselectAllBtn) {
|
||||||
|
deselectAllBtn.addEventListener("click", function() {
|
||||||
|
seriesCheckboxes.forEach((checkbox) => {
|
||||||
|
checkbox.checked = false;
|
||||||
|
});
|
||||||
|
syncSeriesSelectionUi();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Initial state
|
// Initial state
|
||||||
toggleCheckboxes();
|
toggleCheckboxes();
|
||||||
|
syncSeriesSelectionUi();
|
||||||
|
|
||||||
|
|
||||||
// Show the alert when a result is received
|
// Show the alert when a result is received
|
||||||
@@ -942,7 +1020,9 @@
|
|||||||
observer.observe(resultsSpan, { childList: true, subtree: true });
|
observer.observe(resultsSpan, { childList: true, subtree: true });
|
||||||
|
|
||||||
// Listen for toggle events on the <details> element
|
// Listen for toggle events on the <details> element
|
||||||
|
if (detailsElement) {
|
||||||
detailsElement.addEventListener("toggle", toggleCheckboxes);
|
detailsElement.addEventListener("toggle", toggleCheckboxes);
|
||||||
|
}
|
||||||
|
|
||||||
const modal = new bootstrap.Modal(document.getElementById("findingModal"));
|
const modal = new bootstrap.Modal(document.getElementById("findingModal"));
|
||||||
const modalBody = document.getElementById("findingModalBody");
|
const modalBody = document.getElementById("findingModalBody");
|
||||||
@@ -1231,12 +1311,31 @@
|
|||||||
background: rgba(255,255,255,0.02);
|
background: rgba(255,255,255,0.02);
|
||||||
margin-top: .5rem;
|
margin-top: .5rem;
|
||||||
}
|
}
|
||||||
|
.series-actions-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5rem;
|
||||||
|
}
|
||||||
|
.series-selection-toolbar {
|
||||||
|
border-top: 1px solid rgba(0,0,0,0.08);
|
||||||
|
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||||||
|
padding: .5rem 0;
|
||||||
|
}
|
||||||
|
.series-actions-grid .btn,
|
||||||
|
.series-actions-body > a.btn,
|
||||||
|
.series-move-panel .btn {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.series-move-panel {
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
}
|
||||||
|
.series-move-panel .case-details {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
.series-actions summary { list-style: none; }
|
.series-actions summary { list-style: none; }
|
||||||
.series-actions summary::-webkit-details-marker { display:none; }
|
.series-actions summary::-webkit-details-marker { display:none; }
|
||||||
.series-actions .btn { vertical-align: middle; }
|
.series-actions .btn { vertical-align: middle; }
|
||||||
.series-actions > a, .series-actions > button { width: 100%; text-align: left; }
|
.series-actions .btn { text-align: left; }
|
||||||
.series-actions > button.btn.btn-warning,
|
|
||||||
.series-actions > button.btn.btn-secondary { width: auto; align-self: flex-start; }
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.series-actions { background: rgba(255,255,255,0.02); border-color: rgba(255,255,255,0.04); }
|
.series-actions { background: rgba(255,255,255,0.02); border-color: rgba(255,255,255,0.04); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -460,6 +460,11 @@ urlpatterns = [
|
|||||||
views.remove_selected_series_from_case,
|
views.remove_selected_series_from_case,
|
||||||
name="remove_selected_series_from_case",
|
name="remove_selected_series_from_case",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"case/<int:case_pk>/series_move/",
|
||||||
|
views.move_selected_series_to_case,
|
||||||
|
name="move_selected_series_to_case",
|
||||||
|
),
|
||||||
path('case/<int:case_pk>/reorder_series/', views.reorder_series, name='reorder_series'),
|
path('case/<int:case_pk>/reorder_series/', views.reorder_series, name='reorder_series'),
|
||||||
path(
|
path(
|
||||||
"series/<int:pk>/dicom_json",
|
"series/<int:pk>/dicom_json",
|
||||||
|
|||||||
+77
-2
@@ -12,7 +12,7 @@ from django.utils import timezone
|
|||||||
from django.views.decorators.http import require_POST
|
from django.views.decorators.http import require_POST
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Max
|
||||||
from django.template.defaultfilters import filesizeformat
|
from django.template.defaultfilters import filesizeformat
|
||||||
|
|
||||||
# from django.contrib.auth.models import User
|
# from django.contrib.auth.models import User
|
||||||
@@ -66,6 +66,7 @@ from .forms import (
|
|||||||
CaseCollectionUpdateCaseForm,
|
CaseCollectionUpdateCaseForm,
|
||||||
CaseDetailForm,
|
CaseDetailForm,
|
||||||
CaseQuestionForm,
|
CaseQuestionForm,
|
||||||
|
MoveSelectedSeriesForm,
|
||||||
CaseDisplaySetForm,
|
CaseDisplaySetForm,
|
||||||
CaseForm,
|
CaseForm,
|
||||||
CaseResourceFormSet,
|
CaseResourceFormSet,
|
||||||
@@ -829,6 +830,7 @@ def get_case_for_case_detail(pk: int) -> Case:
|
|||||||
def case_detail(request, pk):
|
def case_detail(request, pk):
|
||||||
case = get_case_for_case_detail(pk)
|
case = get_case_for_case_detail(pk)
|
||||||
can_edit = case.check_user_can_edit(request.user)
|
can_edit = case.check_user_can_edit(request.user)
|
||||||
|
move_series_form = MoveSelectedSeriesForm(user=request.user, source_case=case)
|
||||||
|
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
@@ -838,6 +840,7 @@ def case_detail(request, pk):
|
|||||||
"case": case,
|
"case": case,
|
||||||
"cimar_sid": request.user.userprofile.cimar_sid,
|
"cimar_sid": request.user.userprofile.cimar_sid,
|
||||||
"can_edit": can_edit,
|
"can_edit": can_edit,
|
||||||
|
"move_series_form": move_series_form,
|
||||||
# Provide helper lists for resource search filters
|
# Provide helper lists for resource search filters
|
||||||
"site_queryset": Site.objects.all(),
|
"site_queryset": Site.objects.all(),
|
||||||
"subspecialty_queryset": Subspecialty.objects.all(),
|
"subspecialty_queryset": Subspecialty.objects.all(),
|
||||||
@@ -4156,7 +4159,10 @@ def collection_history(request, exam_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def _get_case_review_stats_for_exam(
|
def _get_case_review_stats_for_exam(
|
||||||
*, cid_user_exam: CidUserExam, casedetails: list[CaseDetail] | QuerySet[CaseDetail]
|
*,
|
||||||
|
cid_user_exam: CidUserExam,
|
||||||
|
casedetails: list[CaseDetail] | QuerySet[CaseDetail],
|
||||||
|
messaging_enabled: bool = True,
|
||||||
) -> tuple[dict[int, dict[str, int | bool]], int, int]:
|
) -> tuple[dict[int, dict[str, int | bool]], int, int]:
|
||||||
case_ids = [casedetail.case_id for casedetail in casedetails]
|
case_ids = [casedetail.case_id for casedetail in casedetails]
|
||||||
stats_by_case = {
|
stats_by_case = {
|
||||||
@@ -4173,6 +4179,9 @@ def _get_case_review_stats_for_exam(
|
|||||||
for case_id in case_ids
|
for case_id in case_ids
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if not messaging_enabled:
|
||||||
|
return stats_by_case, 0, 0
|
||||||
|
|
||||||
message_rows = (
|
message_rows = (
|
||||||
CaseReviewMessage.objects.filter(cid_user_exam=cid_user_exam, case_id__in=case_ids)
|
CaseReviewMessage.objects.filter(cid_user_exam=cid_user_exam, case_id__in=case_ids)
|
||||||
.values("case_id")
|
.values("case_id")
|
||||||
@@ -4237,6 +4246,7 @@ def _build_collection_feedback_rows(
|
|||||||
stats_by_case, outstanding_case_count, total_outstanding_feedback = _get_case_review_stats_for_exam(
|
stats_by_case, outstanding_case_count, total_outstanding_feedback = _get_case_review_stats_for_exam(
|
||||||
cid_user_exam=cid_user_exam,
|
cid_user_exam=cid_user_exam,
|
||||||
casedetails=casedetails,
|
casedetails=casedetails,
|
||||||
|
messaging_enabled=_collection_case_query_messaging_enabled(collection),
|
||||||
)
|
)
|
||||||
|
|
||||||
rows = []
|
rows = []
|
||||||
@@ -4695,6 +4705,15 @@ def _user_can_moderate_collection(collection: CaseCollection, user: User) -> boo
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _collection_case_query_messaging_enabled(collection: CaseCollection) -> bool:
|
||||||
|
return bool(getattr(collection, "case_query_messaging_enabled", True))
|
||||||
|
|
||||||
|
|
||||||
|
def _enforce_collection_case_query_messaging_enabled(collection: CaseCollection):
|
||||||
|
if not _collection_case_query_messaging_enabled(collection):
|
||||||
|
raise Http404("Case query and messaging are disabled for this collection")
|
||||||
|
|
||||||
|
|
||||||
def _get_case_review_thread_context(
|
def _get_case_review_thread_context(
|
||||||
*,
|
*,
|
||||||
collection: CaseCollection,
|
collection: CaseCollection,
|
||||||
@@ -7153,6 +7172,62 @@ def remove_selected_series_from_case(request, case_pk):
|
|||||||
SeriesDetail.objects.filter(case=case, series_id__in=ids).delete()
|
SeriesDetail.objects.filter(case=case, series_id__in=ids).delete()
|
||||||
return HttpResponse(f"Series removed from case.")
|
return HttpResponse(f"Series removed from case.")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_POST
|
||||||
|
def move_selected_series_to_case(request, case_pk):
|
||||||
|
source_case = get_object_or_404(Case, pk=case_pk)
|
||||||
|
|
||||||
|
if not source_case.check_user_can_edit(request.user):
|
||||||
|
return HttpResponse("You do not have permission to edit this case.", status=403)
|
||||||
|
|
||||||
|
form = MoveSelectedSeriesForm(request.POST, user=request.user, source_case=source_case)
|
||||||
|
if not form.is_valid():
|
||||||
|
return HttpResponse("Please choose a destination case.", status=400)
|
||||||
|
|
||||||
|
target_case = form.cleaned_data["destination_case"]
|
||||||
|
if not target_case.check_user_can_edit(request.user):
|
||||||
|
return HttpResponse("You do not have permission to edit the destination case.", status=403)
|
||||||
|
|
||||||
|
ids = request.POST.getlist("series-ids")
|
||||||
|
if not ids:
|
||||||
|
return HttpResponse("No series selected to move.")
|
||||||
|
|
||||||
|
series_to_move = list(
|
||||||
|
Series.objects.filter(pk__in=ids, case=source_case).distinct()
|
||||||
|
)
|
||||||
|
if not series_to_move:
|
||||||
|
return HttpResponse("No valid series selected to move.", status=400)
|
||||||
|
|
||||||
|
for series in series_to_move:
|
||||||
|
if not series.check_user_can_edit(request.user):
|
||||||
|
return HttpResponse(
|
||||||
|
"You do not have permission to edit one or more selected series.",
|
||||||
|
status=403,
|
||||||
|
)
|
||||||
|
|
||||||
|
next_sort_order = (
|
||||||
|
SeriesDetail.objects.filter(case=target_case).aggregate(max_sort=Max("sort_order"))["max_sort"] or -1
|
||||||
|
) + 1
|
||||||
|
|
||||||
|
added_to_target = 0
|
||||||
|
for series in series_to_move:
|
||||||
|
link_exists = SeriesDetail.objects.filter(case=target_case, series=series).exists()
|
||||||
|
if not link_exists:
|
||||||
|
SeriesDetail.objects.create(
|
||||||
|
case=target_case,
|
||||||
|
series=series,
|
||||||
|
sort_order=next_sort_order,
|
||||||
|
)
|
||||||
|
next_sort_order += 1
|
||||||
|
added_to_target += 1
|
||||||
|
|
||||||
|
SeriesDetail.objects.filter(case=source_case, series=series).delete()
|
||||||
|
|
||||||
|
return HttpResponse(
|
||||||
|
f"Moved {len(series_to_move)} series to case {target_case.pk}. Added {added_to_target} new links in destination case."
|
||||||
|
)
|
||||||
|
|
||||||
@require_POST
|
@require_POST
|
||||||
def reorder_series(request, case_pk):
|
def reorder_series(request, case_pk):
|
||||||
case = get_object_or_404(Case, pk=case_pk)
|
case = get_object_or_404(Case, pk=case_pk)
|
||||||
|
|||||||
Reference in New Issue
Block a user