feat(tasks): Implement task action functionality in task overview page
This commit is contained in:
+100
-41
@@ -199,19 +199,8 @@ def series_reconstruct_task(
|
|||||||
native_col_spacing = geom["col_spacing"]
|
native_col_spacing = geom["col_spacing"]
|
||||||
native_z_spacing = geom["native_z_spacing"]
|
native_z_spacing = geom["native_z_spacing"]
|
||||||
|
|
||||||
target_spacing = float(slice_spacing_val) if slice_spacing_val is not None else native_z_spacing
|
requested_slice_spacing = float(slice_spacing_val) if slice_spacing_val is not None else None
|
||||||
slab_thickness = float(slice_thickness_val) if slice_thickness_val is not None else target_spacing
|
requested_slab_thickness = float(slice_thickness_val) if slice_thickness_val is not None else None
|
||||||
|
|
||||||
volume_for_recon, recon_centers_mm = atlas_views._aggregate_volume_along_z(
|
|
||||||
volume,
|
|
||||||
source_positions_mm,
|
|
||||||
float(target_spacing),
|
|
||||||
float(slab_thickness),
|
|
||||||
recon_thickness_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
if volume_for_recon.shape[0] < 1:
|
|
||||||
raise ValueError("No reconstruction slices were generated")
|
|
||||||
|
|
||||||
# Determine source acquisition plane from normal vector.
|
# Determine source acquisition plane from normal vector.
|
||||||
dominant_normal_axis = int(np.argmax(np.abs(normal_dir)))
|
dominant_normal_axis = int(np.argmax(np.abs(normal_dir)))
|
||||||
@@ -219,9 +208,14 @@ def series_reconstruct_task(
|
|||||||
|
|
||||||
# Reorient the reconstructed volume into patient axes order [z, y, x] so
|
# Reorient the reconstructed volume into patient axes order [z, y, x] so
|
||||||
# generated plane names are anatomically correct regardless of source plane.
|
# generated plane names are anatomically correct regardless of source plane.
|
||||||
source_axis_dirs = [normal_dir, row_dir, col_dir]
|
# NOTE: DICOM stores ImageOrientationPatient as:
|
||||||
source_axis_spacings = [float(target_spacing), float(native_row_spacing), float(native_col_spacing)]
|
# - first triplet: direction across columns (x-axis in pixel grid)
|
||||||
source_axis_sizes = [int(volume_for_recon.shape[0]), int(volume_for_recon.shape[1]), int(volume_for_recon.shape[2])]
|
# - second triplet: direction across rows (y-axis in pixel grid)
|
||||||
|
# In _extract_recon_geometry names are historically row_dir/col_dir, so we
|
||||||
|
# map axis spacing/indexing explicitly to avoid directional mixups.
|
||||||
|
source_axis_dirs = [normal_dir, col_dir, row_dir]
|
||||||
|
source_axis_spacings = [float(native_z_spacing), float(native_row_spacing), float(native_col_spacing)]
|
||||||
|
source_axis_sizes = [int(volume.shape[0]), int(volume.shape[1]), int(volume.shape[2])]
|
||||||
|
|
||||||
source_for_patient_axis = {}
|
source_for_patient_axis = {}
|
||||||
sign_for_patient_axis = {}
|
sign_for_patient_axis = {}
|
||||||
@@ -243,7 +237,7 @@ def series_reconstruct_task(
|
|||||||
spacing_y = source_axis_spacings[src_y]
|
spacing_y = source_axis_spacings[src_y]
|
||||||
spacing_z = source_axis_spacings[src_z]
|
spacing_z = source_axis_spacings[src_z]
|
||||||
|
|
||||||
vol_zyx = np.transpose(volume_for_recon, (src_z, src_y, src_x))
|
vol_zyx = np.transpose(volume, (src_z, src_y, src_x))
|
||||||
|
|
||||||
if sign_for_patient_axis[2] < 0:
|
if sign_for_patient_axis[2] < 0:
|
||||||
vol_zyx = np.flip(vol_zyx, axis=0)
|
vol_zyx = np.flip(vol_zyx, axis=0)
|
||||||
@@ -255,9 +249,9 @@ def series_reconstruct_task(
|
|||||||
def source_indices_from_patient_zyx(iz, iy, ix):
|
def source_indices_from_patient_zyx(iz, iy, ix):
|
||||||
src_indices = [0, 0, 0]
|
src_indices = [0, 0, 0]
|
||||||
|
|
||||||
z_index = int(iz)
|
z_index = float(iz)
|
||||||
y_index = int(iy)
|
y_index = float(iy)
|
||||||
x_index = int(ix)
|
x_index = float(ix)
|
||||||
|
|
||||||
if sign_for_patient_axis[2] < 0:
|
if sign_for_patient_axis[2] < 0:
|
||||||
z_index = source_axis_sizes[src_z] - 1 - z_index
|
z_index = source_axis_sizes[src_z] - 1 - z_index
|
||||||
@@ -275,41 +269,106 @@ def series_reconstruct_task(
|
|||||||
src_k, src_r, src_c = source_indices_from_patient_zyx(iz, iy, ix)
|
src_k, src_r, src_c = source_indices_from_patient_zyx(iz, iy, ix)
|
||||||
pos = (
|
pos = (
|
||||||
np.asarray(origin_ipp, dtype=float)
|
np.asarray(origin_ipp, dtype=float)
|
||||||
+ (np.asarray(normal_dir, dtype=float) * (float(src_k) * float(target_spacing)))
|
+ (np.asarray(normal_dir, dtype=float) * (float(src_k) * float(native_z_spacing)))
|
||||||
+ (np.asarray(row_dir, dtype=float) * (float(src_r) * float(native_row_spacing)))
|
+ (np.asarray(col_dir, dtype=float) * (float(src_r) * float(native_row_spacing)))
|
||||||
+ (np.asarray(col_dir, dtype=float) * (float(src_c) * float(native_col_spacing)))
|
+ (np.asarray(row_dir, dtype=float) * (float(src_c) * float(native_col_spacing)))
|
||||||
)
|
)
|
||||||
return [float(pos[0]), float(pos[1]), float(pos[2])]
|
return [float(pos[0]), float(pos[1]), float(pos[2])]
|
||||||
|
|
||||||
|
spacing_x = source_axis_spacings[src_x]
|
||||||
|
spacing_y = source_axis_spacings[src_y]
|
||||||
|
spacing_z = source_axis_spacings[src_z]
|
||||||
|
|
||||||
|
def _reduce_slab(slab):
|
||||||
|
if recon_thickness_mode == "max":
|
||||||
|
out = np.max(slab, axis=0)
|
||||||
|
elif recon_thickness_mode == "min":
|
||||||
|
out = np.min(slab, axis=0)
|
||||||
|
else:
|
||||||
|
out = np.mean(slab, axis=0)
|
||||||
|
|
||||||
|
if np.issubdtype(volume.dtype, np.integer):
|
||||||
|
info = np.iinfo(volume.dtype)
|
||||||
|
out = np.clip(np.rint(out), info.min, info.max).astype(volume.dtype)
|
||||||
|
else:
|
||||||
|
out = out.astype(volume.dtype, copy=False)
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _slice_indices_for_axis(axis_length, axis_spacing, output_spacing, slab_thickness):
|
||||||
|
if output_spacing <= 0:
|
||||||
|
raise ValueError("Slice spacing must be greater than 0")
|
||||||
|
if slab_thickness <= 0:
|
||||||
|
raise ValueError("Slice thickness must be greater than 0")
|
||||||
|
|
||||||
|
axis_positions_mm = np.arange(axis_length, dtype=float) * float(axis_spacing)
|
||||||
|
end_pos = axis_positions_mm[-1] if axis_positions_mm.size else 0.0
|
||||||
|
centers_mm = np.arange(0.0, end_pos + (output_spacing * 0.5), output_spacing, dtype=float)
|
||||||
|
if centers_mm.size == 0:
|
||||||
|
centers_mm = np.array([0.0], dtype=float)
|
||||||
|
|
||||||
|
half = slab_thickness / 2.0
|
||||||
|
slabs = []
|
||||||
|
centers_idx = []
|
||||||
|
for center_mm in centers_mm:
|
||||||
|
mask = np.abs(axis_positions_mm - center_mm) <= (half + 1e-6)
|
||||||
|
if not mask.any():
|
||||||
|
nearest = int(np.argmin(np.abs(axis_positions_mm - center_mm)))
|
||||||
|
idxs = np.array([nearest], dtype=int)
|
||||||
|
else:
|
||||||
|
idxs = np.where(mask)[0].astype(int)
|
||||||
|
|
||||||
|
slabs.append(idxs)
|
||||||
|
centers_idx.append(float(center_mm / axis_spacing) if axis_spacing > 0 else float(idxs[0]))
|
||||||
|
|
||||||
|
return slabs, centers_idx
|
||||||
|
|
||||||
plane_builders = {}
|
plane_builders = {}
|
||||||
for plane in recon_planes:
|
for plane in recon_planes:
|
||||||
plane_norm = plane.lower()
|
plane_norm = plane.lower()
|
||||||
if plane_norm == "axial":
|
if plane_norm == "axial":
|
||||||
|
plane_spacing = float(requested_slice_spacing) if requested_slice_spacing is not None else float(spacing_z)
|
||||||
|
plane_thickness = float(requested_slab_thickness) if requested_slab_thickness is not None else float(plane_spacing)
|
||||||
|
slab_indices, slab_centers_idx = _slice_indices_for_axis(
|
||||||
|
int(vol_zyx.shape[0]), float(spacing_z), plane_spacing, plane_thickness
|
||||||
|
)
|
||||||
plane_builders[plane_norm] = {
|
plane_builders[plane_norm] = {
|
||||||
"count": int(vol_zyx.shape[0]),
|
"count": int(len(slab_indices)),
|
||||||
"slice_fn": lambda idx: vol_zyx[idx, :, :],
|
"slice_fn": lambda idx, slabs=slab_indices: _reduce_slab(vol_zyx[slabs[idx], :, :]),
|
||||||
"pixel_spacing_out": [float(spacing_y), float(spacing_x)],
|
"pixel_spacing_out": [float(spacing_y), float(spacing_x)],
|
||||||
"spacing_between_slices_out": float(spacing_z),
|
"spacing_between_slices_out": float(plane_spacing),
|
||||||
"image_orientation_out": [0.0, 1.0, 0.0, 1.0, 0.0, 0.0],
|
"slice_thickness_out": float(plane_thickness),
|
||||||
"position_fn": lambda idx: position_from_patient_zyx(idx, 0, 0),
|
"image_orientation_out": [1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||||
|
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(centers[idx], 0.0, 0.0),
|
||||||
}
|
}
|
||||||
elif plane_norm == "coronal":
|
elif plane_norm == "coronal":
|
||||||
|
plane_spacing = float(requested_slice_spacing) if requested_slice_spacing is not None else float(spacing_y)
|
||||||
|
plane_thickness = float(requested_slab_thickness) if requested_slab_thickness is not None else float(plane_spacing)
|
||||||
|
slab_indices, slab_centers_idx = _slice_indices_for_axis(
|
||||||
|
int(vol_zyx.shape[1]), float(spacing_y), plane_spacing, plane_thickness
|
||||||
|
)
|
||||||
plane_builders[plane_norm] = {
|
plane_builders[plane_norm] = {
|
||||||
"count": int(vol_zyx.shape[1]),
|
"count": int(len(slab_indices)),
|
||||||
"slice_fn": lambda idx: vol_zyx[:, idx, :],
|
"slice_fn": lambda idx, slabs=slab_indices: _reduce_slab(vol_zyx[:, slabs[idx], :]),
|
||||||
"pixel_spacing_out": [float(spacing_z), float(spacing_x)],
|
"pixel_spacing_out": [float(spacing_z), float(spacing_x)],
|
||||||
"spacing_between_slices_out": float(spacing_y),
|
"spacing_between_slices_out": float(plane_spacing),
|
||||||
"image_orientation_out": [0.0, 0.0, 1.0, 1.0, 0.0, 0.0],
|
"slice_thickness_out": float(plane_thickness),
|
||||||
"position_fn": lambda idx: position_from_patient_zyx(0, idx, 0),
|
"image_orientation_out": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(0.0, centers[idx], 0.0),
|
||||||
}
|
}
|
||||||
elif plane_norm == "sagittal":
|
elif plane_norm == "sagittal":
|
||||||
|
plane_spacing = float(requested_slice_spacing) if requested_slice_spacing is not None else float(spacing_x)
|
||||||
|
plane_thickness = float(requested_slab_thickness) if requested_slab_thickness is not None else float(plane_spacing)
|
||||||
|
slab_indices, slab_centers_idx = _slice_indices_for_axis(
|
||||||
|
int(vol_zyx.shape[2]), float(spacing_x), plane_spacing, plane_thickness
|
||||||
|
)
|
||||||
plane_builders[plane_norm] = {
|
plane_builders[plane_norm] = {
|
||||||
"count": int(vol_zyx.shape[2]),
|
"count": int(len(slab_indices)),
|
||||||
"slice_fn": lambda idx: vol_zyx[:, :, idx],
|
"slice_fn": lambda idx, slabs=slab_indices: _reduce_slab(vol_zyx[:, :, slabs[idx]]),
|
||||||
"pixel_spacing_out": [float(spacing_z), float(spacing_y)],
|
"pixel_spacing_out": [float(spacing_z), float(spacing_y)],
|
||||||
"spacing_between_slices_out": float(spacing_x),
|
"spacing_between_slices_out": float(plane_spacing),
|
||||||
"image_orientation_out": [0.0, 0.0, 1.0, 0.0, 1.0, 0.0],
|
"slice_thickness_out": float(plane_thickness),
|
||||||
"position_fn": lambda idx: position_from_patient_zyx(0, 0, idx),
|
"image_orientation_out": [0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
"position_fn": lambda idx, centers=slab_centers_idx: position_from_patient_zyx(0.0, 0.0, centers[idx]),
|
||||||
}
|
}
|
||||||
|
|
||||||
if not plane_builders:
|
if not plane_builders:
|
||||||
@@ -344,7 +403,7 @@ def series_reconstruct_task(
|
|||||||
ds_new.PixelSpacing = [float(cfg["pixel_spacing_out"][0]), float(cfg["pixel_spacing_out"][1])]
|
ds_new.PixelSpacing = [float(cfg["pixel_spacing_out"][0]), float(cfg["pixel_spacing_out"][1])]
|
||||||
ds_new.ImageOrientationPatient = cfg["image_orientation_out"]
|
ds_new.ImageOrientationPatient = cfg["image_orientation_out"]
|
||||||
ds_new.ImagePositionPatient = cfg["position_fn"](idx)
|
ds_new.ImagePositionPatient = cfg["position_fn"](idx)
|
||||||
ds_new.SliceThickness = float(slab_thickness)
|
ds_new.SliceThickness = float(cfg["slice_thickness_out"])
|
||||||
ds_new.SpacingBetweenSlices = float(cfg["spacing_between_slices_out"])
|
ds_new.SpacingBetweenSlices = float(cfg["spacing_between_slices_out"])
|
||||||
|
|
||||||
out_io = io.BytesIO()
|
out_io = io.BytesIO()
|
||||||
@@ -401,8 +460,8 @@ def series_reconstruct_task(
|
|||||||
return {
|
return {
|
||||||
"series_id": series.pk,
|
"series_id": series.pk,
|
||||||
"created_series": created_series,
|
"created_series": created_series,
|
||||||
"target_spacing": float(target_spacing),
|
"target_spacing": float(requested_slice_spacing) if requested_slice_spacing is not None else None,
|
||||||
"slab_thickness": float(slab_thickness),
|
"slab_thickness": float(requested_slab_thickness) if requested_slab_thickness is not None else None,
|
||||||
"mode": recon_thickness_mode,
|
"mode": recon_thickness_mode,
|
||||||
"source_plane": source_plane,
|
"source_plane": source_plane,
|
||||||
}
|
}
|
||||||
@@ -51,10 +51,28 @@ Atlas Task Overview
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-responsive">
|
<form method="post" class="mb-3">
|
||||||
<table class="table table-sm table-striped align-middle">
|
{% csrf_token %}
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2 mb-2">
|
||||||
|
<select class="form-select form-select-sm" name="task_action" style="max-width: 320px;">
|
||||||
|
<option value="">Choose task action...</option>
|
||||||
|
<option value="reset_failed">Reset FAILED to READY</option>
|
||||||
|
<option value="reset_selected">Reset selected to READY (skip RUNNING)</option>
|
||||||
|
<option value="delete_completed">Delete selected COMPLETED</option>
|
||||||
|
<option value="delete_failed">Delete selected FAILED</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">Apply to selected</button>
|
||||||
|
<div class="form-check ms-2">
|
||||||
|
<input class="form-check-input" type="checkbox" id="select-all-tasks">
|
||||||
|
<label class="form-check-label small" for="select-all-tasks">Select all</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-striped align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th style="width: 36px;"></th>
|
||||||
<th>Task</th>
|
<th>Task</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Generated Series</th>
|
<th>Generated Series</th>
|
||||||
@@ -64,11 +82,15 @@ Atlas Task Overview
|
|||||||
<th>Finished</th>
|
<th>Finished</th>
|
||||||
<th>Worker</th>
|
<th>Worker</th>
|
||||||
<th>Error</th>
|
<th>Error</th>
|
||||||
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for task in tasks %}
|
{% for task in tasks %}
|
||||||
<tr>
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input class="form-check-input task-select" type="checkbox" name="task_ids" value="{{ task.id }}">
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="small fw-semibold">{{ task.task_path }}</div>
|
<div class="small fw-semibold">{{ task.task_path }}</div>
|
||||||
<div class="small text-muted">{{ task.id }}</div>
|
<div class="small text-muted">{{ task.id }}</div>
|
||||||
@@ -99,14 +121,58 @@ Atlas Task Overview
|
|||||||
<td class="small">{{ task.finished_at|date:"Y-m-d H:i:s" }}</td>
|
<td class="small">{{ task.finished_at|date:"Y-m-d H:i:s" }}</td>
|
||||||
<td class="small">{{ task.worker_ids }}</td>
|
<td class="small">{{ task.worker_ids }}</td>
|
||||||
<td class="small text-danger">{{ task.exception_class_path|default:"" }}</td>
|
<td class="small text-danger">{{ task.exception_class_path|default:"" }}</td>
|
||||||
|
<td class="small">
|
||||||
|
{% if task.traceback %}
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-danger btn-sm"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#traceback-{{ task.id }}"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="traceback-{{ task.id }}"
|
||||||
|
>
|
||||||
|
View full traceback
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% if task.traceback %}
|
||||||
|
<tr class="collapse" id="traceback-{{ task.id }}">
|
||||||
|
<td></td>
|
||||||
|
<td colspan="10">
|
||||||
|
<div class="border rounded bg-dark-subtle p-2">
|
||||||
|
<div class="small fw-semibold mb-1">Full traceback</div>
|
||||||
|
<pre class="small mb-0" style="white-space: pre-wrap;">{{ task.traceback }}</pre>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" class="text-muted">No tasks found for this filter.</td>
|
<td colspan="11" class="text-muted">No tasks found for this filter.</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const selectAll = document.getElementById('select-all-tasks');
|
||||||
|
const rows = Array.from(document.querySelectorAll('.task-select'));
|
||||||
|
if (!selectAll || !rows.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll.addEventListener('change', function () {
|
||||||
|
rows.forEach((cb) => {
|
||||||
|
cb.checked = selectAll.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@@ -819,6 +819,54 @@ def task_overview(request):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
action = (request.POST.get("task_action") or "").strip()
|
||||||
|
selected_ids = [task_id for task_id in request.POST.getlist("task_ids") if task_id]
|
||||||
|
|
||||||
|
if not selected_ids:
|
||||||
|
messages.warning(request, "Select at least one task to run an action.")
|
||||||
|
return redirect(f"{reverse('atlas:task_overview')}?status={(request.GET.get('status') or 'all').strip().lower()}")
|
||||||
|
|
||||||
|
selected_qs = DBTaskResult.objects.filter(id__in=selected_ids)
|
||||||
|
|
||||||
|
if action == "reset_failed":
|
||||||
|
updated = selected_qs.filter(status="FAILED").update(
|
||||||
|
status="READY",
|
||||||
|
started_at=None,
|
||||||
|
finished_at=None,
|
||||||
|
worker_ids="",
|
||||||
|
exception_class_path="",
|
||||||
|
traceback="",
|
||||||
|
return_value=None,
|
||||||
|
)
|
||||||
|
messages.success(request, f"Reset {updated} failed task(s) to READY.")
|
||||||
|
elif action == "reset_selected":
|
||||||
|
blocked = selected_qs.filter(status="RUNNING").count()
|
||||||
|
updated = selected_qs.exclude(status="RUNNING").update(
|
||||||
|
status="READY",
|
||||||
|
started_at=None,
|
||||||
|
finished_at=None,
|
||||||
|
worker_ids="",
|
||||||
|
exception_class_path="",
|
||||||
|
traceback="",
|
||||||
|
return_value=None,
|
||||||
|
)
|
||||||
|
msg = f"Reset {updated} task(s) to READY."
|
||||||
|
if blocked:
|
||||||
|
msg += f" Skipped {blocked} running task(s)."
|
||||||
|
messages.success(request, msg)
|
||||||
|
elif action == "delete_completed":
|
||||||
|
deleted, _ = selected_qs.filter(status="SUCCESSFUL").delete()
|
||||||
|
messages.success(request, f"Deleted {deleted} completed task record(s).")
|
||||||
|
elif action == "delete_failed":
|
||||||
|
deleted, _ = selected_qs.filter(status="FAILED").delete()
|
||||||
|
messages.success(request, f"Deleted {deleted} failed task record(s).")
|
||||||
|
else:
|
||||||
|
messages.error(request, "Invalid task action requested.")
|
||||||
|
|
||||||
|
status_q = (request.GET.get("status") or "all").strip().lower()
|
||||||
|
return redirect(f"{reverse('atlas:task_overview')}?status={status_q}")
|
||||||
|
|
||||||
selected_status = (request.GET.get("status") or "all").strip().lower()
|
selected_status = (request.GET.get("status") or "all").strip().lower()
|
||||||
status_map = {
|
status_map = {
|
||||||
"active": ["READY", "RUNNING"],
|
"active": ["READY", "RUNNING"],
|
||||||
|
|||||||
Reference in New Issue
Block a user