Add django-tasks and django-tasks-db to requirements
- Updated requirements.txt to include django-tasks and django-tasks-db for task management.
This commit is contained in:
@@ -2,50 +2,68 @@
|
||||
|
||||
{% partialdef case-series %}
|
||||
{% for series in case.get_ordered_series %}
|
||||
<span class="series-block series-block-interactive" id="series-block-{{ series.pk }}" data-series-id="{{series.pk}}">
|
||||
<span>
|
||||
<span class="series-block-series-number">Series {{ forloop.counter }}:</span><br>
|
||||
<a href="{% url 'atlas:series_detail' pk=series.pk %}" class="series-block-title-link">
|
||||
{{series.get_block}}
|
||||
</a>
|
||||
<br>
|
||||
<div class="series-block-actions mt-2 small">
|
||||
<article class="series-block series-block-interactive"
|
||||
id="series-block-{{ series.pk }}"
|
||||
data-series-id="{{series.pk}}"
|
||||
data-series-label="Series {{ forloop.counter }}"
|
||||
data-series-name="{{ series|escape }}"
|
||||
data-series-detail-url="{% url 'atlas:series_detail' pk=series.pk %}"
|
||||
data-series-popup-url="/atlas/series/{{series.pk}}">
|
||||
<input type="checkbox" class="hide" name="series-ids" value="{{series.pk}}">
|
||||
<div class="series-card-shell">
|
||||
<button type="button"
|
||||
class="series-card-main"
|
||||
data-series-action="details"
|
||||
data-series-id="{{series.pk}}"
|
||||
aria-label="Open series actions for Series {{ forloop.counter }}">
|
||||
<div class="series-card-thumb-wrap">
|
||||
{% with preview_image=series.images.first %}
|
||||
{% if preview_image and preview_image.image %}
|
||||
<img src="{{ preview_image.image.url }}"
|
||||
alt="Series {{ forloop.counter }} thumbnail"
|
||||
class="series-card-thumb">
|
||||
{% else %}
|
||||
<div class="series-card-thumb series-card-thumb-placeholder">
|
||||
<i class="bi bi-image"></i>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="series-card-content">
|
||||
<div class="series-card-heading">
|
||||
<span class="series-card-series-number">Series {{ forloop.counter }}</span>
|
||||
<span class="series-card-image-count">{{ series.images.count }} image{{ series.images.count|pluralize }}</span>
|
||||
</div>
|
||||
<div class="series-card-description">{{ series.description|default:"No description" }}</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="series-card-controls">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-xs btn-sm select-series-btn"
|
||||
class="btn btn-outline-primary btn-sm select-series-btn"
|
||||
style="display: none;"
|
||||
data-series-id="{{series.pk}}">
|
||||
Select
|
||||
</button>
|
||||
<input type="checkbox" class="hide" name="series-ids" value="{{series.pk}}">
|
||||
<button type="button"
|
||||
class="btn btn-outline-info btn-xs btn-sm series-open-primary-btn"
|
||||
class="btn btn-outline-info btn-sm series-open-primary-btn"
|
||||
title="Open series in primary viewport"
|
||||
data-series-id="{{series.pk}}"
|
||||
data-series-name="{{ series|escape }}">
|
||||
<i class="bi bi-eye"></i> Open
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-outline-info btn-xs btn-sm series-open-new-viewport-btn"
|
||||
class="btn btn-outline-info btn-sm series-open-new-viewport-btn d-none"
|
||||
title="Add a viewport and open series"
|
||||
data-series-id="{{series.pk}}"
|
||||
data-series-name="{{ series|escape }}">
|
||||
data-series-name="{{ series|escape }}"
|
||||
tabindex="-1"
|
||||
aria-hidden="true">
|
||||
<i class="bi bi-layout-split"></i> Open + viewport
|
||||
</button>
|
||||
<a href="{% url 'atlas:series_detail' pk=series.pk %}"
|
||||
class="btn btn-outline-secondary btn-xs btn-sm"
|
||||
title="Open series page">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Page
|
||||
</a>
|
||||
<span class="series-block-popup-link ms-1">
|
||||
<a href="#"
|
||||
class="btn btn-outline-secondary btn-xs btn-sm"
|
||||
onclick="return window.create_popup_window('/atlas/series/{{series.pk}}', 'Series')">
|
||||
<i class="bi bi-window"></i> Popup
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% endpartialdef %}
|
||||
|
||||
@@ -484,27 +502,26 @@
|
||||
data-named-stacks='{{case.get_case_named_stacks}}'
|
||||
></div>
|
||||
</details>
|
||||
<div class="pre-whitespace multi-image-block"><b>Series:</b>
|
||||
<form>
|
||||
<div class="d-flex flex-wrap gap-2 align-items-start" id="case-series-row">
|
||||
{% partial case-series %}
|
||||
</div>
|
||||
</form>
|
||||
<div class="d-flex flex-column gap-2 align-items-start">
|
||||
<details class="series-panel multi-image-block" id="case-series-panel" open>
|
||||
<summary class="series-panel-summary">
|
||||
<div>
|
||||
<span class="series-panel-title">Series</span>
|
||||
<span class="series-panel-subtitle">Compact cards by default. Open a card for more actions.</span>
|
||||
</div>
|
||||
<span class="series-panel-badge">{{ case.get_ordered_series|length }} total</span>
|
||||
</summary>
|
||||
<div class="series-panel-body">
|
||||
<div class="series-toolbar">
|
||||
<button type="button"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
class="btn btn-outline-primary btn-sm series-toolbar-btn"
|
||||
onclick="window.location.href='{% url 'atlas:user_uploads_case' case_id=case.pk %}'">
|
||||
Import new uploads
|
||||
</button>
|
||||
</div>
|
||||
<details class="series-actions" id="series-actions">
|
||||
<summary>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" title="Click to manage series" onclick="this.closest('details').open = !this.closest('details').open;">
|
||||
Manage Series
|
||||
</button>
|
||||
</summary>
|
||||
<div class="series-actions-body">
|
||||
<details class="series-actions" id="series-actions">
|
||||
<summary class="series-actions-summary">
|
||||
<span class="btn btn-outline-secondary btn-sm series-toolbar-btn">Manage series</span>
|
||||
</summary>
|
||||
<div class="series-actions-body">
|
||||
<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">
|
||||
@@ -550,6 +567,16 @@
|
||||
>
|
||||
Remove selected series from case
|
||||
</button>
|
||||
<button hx-post="{% url 'atlas:series_bulk_delete' %}"
|
||||
title="delete selected series"
|
||||
hx-include="[name='series-ids']:checked"
|
||||
hx-target="#series-action-results"
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Delete the selected series from the database? This cannot be undone."
|
||||
class="btn btn-sm btn-danger text-start"
|
||||
type="button">
|
||||
Delete selected series
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-secondary text-start"
|
||||
data-bs-toggle="modal"
|
||||
@@ -633,13 +660,19 @@
|
||||
<div class="text-muted small mt-2">Case move options are unavailable in this view.</div>
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
<div class="alert alert-info mt-2" id="series-action-alert" style="display:none;">
|
||||
<span id="series-action-results" ></span>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<form>
|
||||
<div class="series-card-grid" id="case-series-row">
|
||||
{% partial case-series %}
|
||||
</div>
|
||||
<div class="alert alert-info mt-2" id="series-action-alert" style="display:none;">
|
||||
<span id="series-action-results" ></span>
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="container">
|
||||
|
||||
@@ -955,6 +988,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="seriesActionsModal" tabindex="-1" aria-labelledby="seriesActionsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<h5 class="modal-title" id="seriesActionsModalLabel">Series actions</h5>
|
||||
<div class="small text-muted" id="seriesActionsModalSubheading"></div>
|
||||
</div>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body d-flex flex-column gap-2">
|
||||
<button type="button" class="btn btn-outline-info" id="series-modal-open-primary-btn">
|
||||
<i class="bi bi-eye"></i> Open in viewer
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-info" id="series-modal-open-new-viewport-btn">
|
||||
<i class="bi bi-layout-split"></i> Open with new viewport
|
||||
</button>
|
||||
<a href="#" class="btn btn-outline-secondary" id="series-modal-page-link">
|
||||
<i class="bi bi-box-arrow-up-right"></i> Open series page
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary" id="series-modal-popup-btn">
|
||||
<i class="bi bi-window"></i> Open popup window
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" id="series-modal-select-btn" style="display:none;">
|
||||
Select for manage series
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// From series_viewer.html
|
||||
function loadAnnotationAndViewportOnElement(annotation, viewport, dicom_element, current_image_id_index) {
|
||||
@@ -1079,12 +1143,77 @@
|
||||
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");
|
||||
const seriesActionsModalEl = document.getElementById("seriesActionsModal");
|
||||
const seriesActionsModal = seriesActionsModalEl ? new bootstrap.Modal(seriesActionsModalEl) : null;
|
||||
const seriesModalSubheading = document.getElementById("seriesActionsModalSubheading");
|
||||
const seriesModalOpenPrimaryBtn = document.getElementById("series-modal-open-primary-btn");
|
||||
const seriesModalOpenNewViewportBtn = document.getElementById("series-modal-open-new-viewport-btn");
|
||||
const seriesModalPageLink = document.getElementById("series-modal-page-link");
|
||||
const seriesModalPopupBtn = document.getElementById("series-modal-popup-btn");
|
||||
const seriesModalSelectBtn = document.getElementById("series-modal-select-btn");
|
||||
let activeSeriesCard = null;
|
||||
|
||||
function toggleCheckboxes() {
|
||||
const isOpen = detailsElement && detailsElement.hasAttribute("open");
|
||||
buttons.forEach((button) => {
|
||||
button.style.display = isOpen ? "inline-block" : "none";
|
||||
});
|
||||
if (seriesModalSelectBtn) {
|
||||
seriesModalSelectBtn.style.display = isOpen ? "inline-block" : "none";
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSeriesSelection(card) {
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
const checkbox = card.querySelector("input[type='checkbox'][name='series-ids']");
|
||||
if (!checkbox) {
|
||||
return;
|
||||
}
|
||||
checkbox.checked = !checkbox.checked;
|
||||
syncSeriesSelectionUi();
|
||||
}
|
||||
|
||||
function openSeriesActionsModal(card) {
|
||||
if (!card || !seriesActionsModal) {
|
||||
return;
|
||||
}
|
||||
|
||||
activeSeriesCard = card;
|
||||
|
||||
const seriesId = card.dataset.seriesId;
|
||||
const seriesLabel = card.dataset.seriesLabel || "Series";
|
||||
const seriesName = card.dataset.seriesName || seriesLabel;
|
||||
const detailUrl = card.dataset.seriesDetailUrl;
|
||||
const popupUrl = card.dataset.seriesPopupUrl;
|
||||
|
||||
if (seriesModalSubheading) {
|
||||
seriesModalSubheading.textContent = `${seriesLabel} · ${seriesName}`;
|
||||
}
|
||||
if (seriesModalPageLink) {
|
||||
seriesModalPageLink.href = detailUrl || "#";
|
||||
}
|
||||
if (seriesModalPopupBtn) {
|
||||
seriesModalPopupBtn.onclick = function() {
|
||||
return window.create_popup_window(popupUrl, 'Series');
|
||||
};
|
||||
}
|
||||
if (seriesModalOpenPrimaryBtn) {
|
||||
seriesModalOpenPrimaryBtn.dataset.seriesId = seriesId;
|
||||
seriesModalOpenPrimaryBtn.dataset.seriesName = seriesName;
|
||||
}
|
||||
if (seriesModalOpenNewViewportBtn) {
|
||||
seriesModalOpenNewViewportBtn.dataset.seriesId = seriesId;
|
||||
seriesModalOpenNewViewportBtn.dataset.seriesName = seriesName;
|
||||
}
|
||||
if (seriesModalSelectBtn) {
|
||||
const checkbox = card.querySelector("input[type='checkbox'][name='series-ids']");
|
||||
const isSelected = checkbox ? checkbox.checked : false;
|
||||
seriesModalSelectBtn.textContent = isSelected ? "Deselect from manage series" : "Select for manage series";
|
||||
}
|
||||
|
||||
seriesActionsModal.show();
|
||||
}
|
||||
|
||||
function syncSeriesSelectionUi() {
|
||||
@@ -1119,12 +1248,48 @@
|
||||
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();
|
||||
toggleSeriesSelection(parent);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-series-action='details']").forEach(function(btn) {
|
||||
btn.addEventListener("click", function() {
|
||||
openSeriesActionsModal(btn.closest(".series-block"));
|
||||
});
|
||||
});
|
||||
|
||||
if (seriesModalSelectBtn) {
|
||||
seriesModalSelectBtn.addEventListener("click", function() {
|
||||
toggleSeriesSelection(activeSeriesCard);
|
||||
const checkbox = activeSeriesCard ? activeSeriesCard.querySelector("input[type='checkbox'][name='series-ids']") : null;
|
||||
seriesModalSelectBtn.textContent = checkbox && checkbox.checked ? "Deselect from manage series" : "Select for manage series";
|
||||
});
|
||||
}
|
||||
|
||||
if (seriesModalOpenPrimaryBtn) {
|
||||
seriesModalOpenPrimaryBtn.addEventListener("click", function() {
|
||||
const matchingButton = document.querySelector(`.series-open-primary-btn[data-series-id='${this.dataset.seriesId}']`);
|
||||
if (matchingButton) {
|
||||
matchingButton.click();
|
||||
if (seriesActionsModal) {
|
||||
seriesActionsModal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (seriesModalOpenNewViewportBtn) {
|
||||
seriesModalOpenNewViewportBtn.addEventListener("click", function() {
|
||||
const matchingButton = document.querySelector(`.series-open-new-viewport-btn[data-series-id='${this.dataset.seriesId}']`);
|
||||
if (matchingButton) {
|
||||
matchingButton.click();
|
||||
if (seriesActionsModal) {
|
||||
seriesActionsModal.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
seriesCheckboxes.forEach((checkbox) => {
|
||||
checkbox.addEventListener("change", syncSeriesSelectionUi);
|
||||
});
|
||||
@@ -1566,24 +1731,187 @@
|
||||
#series-reorder-list .drag-over {
|
||||
border-top: 2px solid #0d6efd;
|
||||
}
|
||||
.series-panel {
|
||||
border: 1px solid rgba(13,110,253,0.2);
|
||||
border-radius: 1rem;
|
||||
background: linear-gradient(180deg, rgba(13,110,253,0.05), rgba(255,255,255,0.02));
|
||||
padding: .25rem;
|
||||
margin-top: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.series-panel-summary {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.series-panel-summary::-webkit-details-marker { display: none; }
|
||||
.series-panel-title {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .01em;
|
||||
}
|
||||
.series-panel-subtitle {
|
||||
display: block;
|
||||
color: var(--bs-secondary-color, #6c757d);
|
||||
font-size: .85rem;
|
||||
margin-top: .15rem;
|
||||
}
|
||||
.series-panel-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 5.5rem;
|
||||
padding: .35rem .75rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(13,110,253,0.12);
|
||||
color: #9ec5fe;
|
||||
font-size: .8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.series-panel-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 0 1.1rem 1.1rem;
|
||||
}
|
||||
.series-toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(12rem, 18rem) minmax(0, 1fr);
|
||||
gap: .75rem;
|
||||
align-items: start;
|
||||
}
|
||||
.series-toolbar-btn {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
.series-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: .85rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
.series-block {
|
||||
min-width: 0;
|
||||
}
|
||||
.series-card-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .65rem;
|
||||
height: 100%;
|
||||
padding: .85rem;
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
border-radius: .9rem;
|
||||
background: rgba(15,23,42,0.22);
|
||||
box-shadow: 0 10px 24px rgba(0,0,0,0.08);
|
||||
}
|
||||
.series-card-main {
|
||||
display: grid;
|
||||
grid-template-columns: 84px minmax(0, 1fr);
|
||||
gap: .85rem;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
}
|
||||
.series-card-main:hover .series-card-description,
|
||||
.series-card-main:focus-visible .series-card-description {
|
||||
color: var(--bs-emphasis-color, #fff);
|
||||
}
|
||||
.series-card-main:focus-visible {
|
||||
outline: 2px solid rgba(13,110,253,0.55);
|
||||
outline-offset: 4px;
|
||||
border-radius: .6rem;
|
||||
}
|
||||
.series-card-thumb-wrap {
|
||||
width: 84px;
|
||||
}
|
||||
.series-card-thumb {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
object-fit: cover;
|
||||
border-radius: .75rem;
|
||||
border: 1px solid rgba(255,255,255,0.12);
|
||||
display: block;
|
||||
background: rgba(255,255,255,0.04);
|
||||
}
|
||||
.series-card-thumb-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bs-secondary-color, #6c757d);
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
.series-card-content {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .35rem;
|
||||
}
|
||||
.series-card-heading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: .45rem .6rem;
|
||||
}
|
||||
.series-card-series-number {
|
||||
font-weight: 700;
|
||||
}
|
||||
.series-card-image-count {
|
||||
font-size: .8rem;
|
||||
color: var(--bs-secondary-color, #6c757d);
|
||||
}
|
||||
.series-card-description {
|
||||
font-size: .95rem;
|
||||
line-height: 1.35;
|
||||
color: rgba(255,255,255,0.88);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-height: 2.55em;
|
||||
}
|
||||
.series-card-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .5rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.series-card-controls .btn {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.series-block.highlight-series .series-card-shell,
|
||||
.series-block:hover .series-card-shell {
|
||||
border-color: rgba(13,110,253,0.45);
|
||||
box-shadow: 0 14px 28px rgba(13,110,253,0.12);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.series-actions[open] {
|
||||
border: 2px solid #0d6efd;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
margin-top: 8px;
|
||||
border-radius: .85rem;
|
||||
padding: .85rem;
|
||||
}
|
||||
.series-actions {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0; /* allow flex children to shrink without overflow */
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .5rem;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
border-radius: .375rem;
|
||||
border-radius: .85rem;
|
||||
padding: .5rem;
|
||||
background: rgba(255,255,255,0.02);
|
||||
margin-top: .5rem;
|
||||
}
|
||||
.series-actions-summary { list-style: none; }
|
||||
.series-actions-summary::-webkit-details-marker { display:none; }
|
||||
.series-actions-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1605,12 +1933,36 @@
|
||||
.series-move-panel .case-details {
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.series-actions summary { list-style: none; }
|
||||
.series-actions summary::-webkit-details-marker { display:none; }
|
||||
.series-actions .btn {
|
||||
vertical-align: middle;
|
||||
text-align: left;
|
||||
}
|
||||
@media (max-width: 991.98px) {
|
||||
.series-toolbar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@media (max-width: 575.98px) {
|
||||
.series-panel-summary {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.series-panel-badge {
|
||||
min-width: 0;
|
||||
}
|
||||
.series-card-main {
|
||||
grid-template-columns: 72px minmax(0, 1fr);
|
||||
}
|
||||
.series-card-thumb-wrap,
|
||||
.series-card-thumb {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
.series-card-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.series-actions { background: rgba(255,255,255,0.02); border-color: rgba(255,255,255,0.04); }
|
||||
}
|
||||
|
||||
+40
-16
@@ -9014,21 +9014,28 @@ def remove_case_from_collection(request, case_pk, collection_pk):
|
||||
return HttpResponse(f"Case removed from collection: {collection.name}")
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def remove_selected_series_from_case(request, case_pk):
|
||||
ids = request.POST.getlist('series-ids')
|
||||
case = Case.objects.get(pk=case_pk)
|
||||
ids = request.POST.getlist('series-ids') or request.POST.getlist('series_ids')
|
||||
case = get_object_or_404(Case, pk=case_pk)
|
||||
|
||||
if not case.check_user_can_edit(request.user):
|
||||
return HttpResponse("You do not have permission to edit this case.", status=403)
|
||||
|
||||
|
||||
if not ids:
|
||||
return HttpResponse("No series selected for removal.")
|
||||
return HttpResponse("No series selected for removal.", status=400)
|
||||
|
||||
linked_series_ids = list(
|
||||
Series.objects.filter(pk__in=ids, case=case).values_list("pk", flat=True).distinct()
|
||||
)
|
||||
if not linked_series_ids:
|
||||
return HttpResponse("No valid series selected for removal.", status=400)
|
||||
|
||||
# Remove the association (for a through model)
|
||||
SeriesDetail.objects.filter(case=case, series_id__in=ids).delete()
|
||||
return HttpResponse(f"Series removed from case.")
|
||||
SeriesDetail.objects.filter(case=case, series_id__in=linked_series_ids).delete()
|
||||
return HttpResponse(f"Removed {len(linked_series_ids)} series from this case.")
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -9225,18 +9232,35 @@ def seriesfinding_migration_status(request):
|
||||
"findings_status": findings_status
|
||||
})
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def series_bulk_delete(request):
|
||||
if request.method == "POST":
|
||||
ids = request.POST.getlist("series_ids")
|
||||
# Only delete series the user has permission to delete
|
||||
series_to_delete = Series.objects.filter(pk__in=ids)
|
||||
deleted_ids = []
|
||||
for series in series_to_delete:
|
||||
if series.check_user_can_edit(request.user):
|
||||
series.delete()
|
||||
deleted_ids.append(series.pk)
|
||||
# Optionally, you could add a message here about which were deleted
|
||||
return redirect('atlas:series_view')
|
||||
ids = request.POST.getlist("series_ids") or request.POST.getlist("series-ids")
|
||||
if not ids:
|
||||
response = HttpResponse("No series selected to delete.", status=400)
|
||||
return response if request.htmx else redirect("atlas:series_view")
|
||||
|
||||
series_to_delete = list(Series.objects.filter(pk__in=ids).distinct())
|
||||
if not series_to_delete:
|
||||
response = HttpResponse("No valid series selected to delete.", status=400)
|
||||
return response if request.htmx else redirect("atlas:series_view")
|
||||
|
||||
forbidden_series = [series.pk for series in series_to_delete if not series.check_user_can_edit(request.user)]
|
||||
if forbidden_series:
|
||||
response = HttpResponse(
|
||||
f"You do not have permission to delete {len(forbidden_series)} selected series.",
|
||||
status=403,
|
||||
)
|
||||
return response if request.htmx else redirect("atlas:series_view")
|
||||
|
||||
deleted_count = len(series_to_delete)
|
||||
for series in series_to_delete:
|
||||
series.delete()
|
||||
|
||||
response = HttpResponse(f"Deleted {deleted_count} series.")
|
||||
if request.htmx:
|
||||
return response
|
||||
return redirect(request.META.get("HTTP_REFERER") or reverse("atlas:series_view"))
|
||||
|
||||
@user_is_collection_author_or_atlas_editor
|
||||
def collection_case_displaysetup(request, collection_id, case_number=None, case_id=None):
|
||||
|
||||
+183
-183
File diff suppressed because one or more lines are too long
+3
-1
@@ -61,4 +61,6 @@ pygments
|
||||
celery[redis]
|
||||
redis
|
||||
pywatchman
|
||||
numpy==2.4.0
|
||||
numpy==2.4.0
|
||||
django-tasks
|
||||
django-tasks-db
|
||||
Reference in New Issue
Block a user