Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Task control panel #62

Merged
merged 1 commit into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Top

- Add permissions to dicom_web views (see TODOs there)
- task urls without job
- Just warn when one only one series of the study could not be transferred (only error when all series could not be transferred)
- Allow to trigger toasts from HTMX responses using HX-Trigger # see core.js of RADIS
- Fix toasts that newest is always on top (also in RADIS)

Expand Down Expand Up @@ -118,6 +120,9 @@

## Maybe

- Show failed tasks in results of batch query and batch transfer
- Allow to find multiple Patients for the same PatientName + PatientBirthDate in batch query
-- Currently we don't allow this, but this can happen when a patient has multiple PatientIDs in the same PACS (e.g. has external images)
- exclude test folders from autorelad in ServerCommand (maybe a custom filter is needed)
- Switch from Daphne to Uvicorn (maybe it has faster restart times during development)
- Switch from Celery to Huey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% load dicom_task_status_css_class from core_extras %}
{% load crispy from crispy_forms_tags %}
{% load render_table from django_tables2 %}
{% load task_control_panel from batch_query_extras %}
{% block heading %}
<div class="d-flex justify-content-between align-items-start">
<h4 class="mb-3">Batch Query Task</h4>
Expand All @@ -16,4 +17,5 @@ <h5 text-nowrap>Results</h5>
<div class="col col-md-auto">{% crispy page_size_select %}</div>
</div>
{% render_table table %}
{% task_control_panel %}
{% endblock content %}
22 changes: 16 additions & 6 deletions adit/batch_query/templatetags/batch_query_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,22 @@
@register.inclusion_tag("core/_job_detail_control_panel.html", takes_context=True)
def job_control_panel(context: dict[str, Any]) -> dict[str, Any]:
return {
"delete_url": "batch_query_job_delete",
"verify_url": "batch_query_job_verify",
"cancel_url": "batch_query_job_cancel",
"resume_url": "batch_query_job_resume",
"retry_url": "batch_query_job_retry",
"restart_url": "batch_query_job_restart",
"job_delete_url": "batch_query_job_delete",
"job_verify_url": "batch_query_job_verify",
"job_cancel_url": "batch_query_job_cancel",
"job_resume_url": "batch_query_job_resume",
"job_retry_url": "batch_query_job_retry",
"job_restart_url": "batch_query_job_restart",
"user": context["user"],
"job": context["job"],
}


@register.inclusion_tag("core/_task_detail_control_panel.html", takes_context=True)
def task_control_panel(context: dict[str, Any]) -> dict[str, Any]:
return {
"task_delete_url": "batch_query_task_delete",
"task_reset_url": "batch_query_task_reset",
"user": context["user"],
"task": context["task"],
}
12 changes: 12 additions & 0 deletions adit/batch_query/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
BatchQueryJobVerifyView,
BatchQueryResultDownloadView,
BatchQueryResultListView,
BatchQueryTaskDeleteView,
BatchQueryTaskDetailView,
BatchQueryTaskResetView,
BatchQueryUpdatePreferencesView,
)

Expand Down Expand Up @@ -88,4 +90,14 @@
BatchQueryTaskDetailView.as_view(),
name="batch_query_task_detail",
),
path(
"tasks/<int:pk>/delete/",
BatchQueryTaskDeleteView.as_view(),
name="batch_query_task_delete",
),
path(
"tasks/<int:pk>/reset/",
BatchQueryTaskResetView.as_view(),
name="batch_query_task_reset",
),
]
13 changes: 13 additions & 0 deletions adit/batch_query/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,11 @@
DicomJobResumeView,
DicomJobRetryView,
DicomJobVerifyView,
DicomTaskDeleteView,
DicomTaskDetailView,
DicomTaskResetView,
)
from adit.selective_transfer.mixins import SelectiveTransferLockedMixin

from .filters import BatchQueryJobFilter, BatchQueryResultFilter, BatchQueryTaskFilter
from .forms import BatchQueryJobForm
Expand Down Expand Up @@ -147,6 +150,16 @@ def get_table_data(self):
return task.results.all()


class BatchQueryTaskDeleteView(SelectiveTransferLockedMixin, DicomTaskDeleteView):
model = BatchQueryTask


class BatchQueryTaskResetView(SelectiveTransferLockedMixin, DicomTaskResetView):
model = BatchQueryTask
default_priority = settings.BATCH_QUERY_DEFAULT_PRIORITY
urgent_priority = settings.BATCH_QUERY_URGENT_PRIORITY


class BatchQueryResultListView(
BatchQueryLockedMixin,
LoginRequiredMixin,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{% extends "batch_transfer/batch_transfer_layout.html" %}
{% load task_control_panel from batch_transfer_extras %}
{% block heading %}
<h4 class="mb-3">Batch Transfer Task</h4>
{% endblock heading %}
{% block content %}
{% include "core/_transfer_task_detail.html" %}
{% task_control_panel %}
{% endblock content %}
22 changes: 16 additions & 6 deletions adit/batch_transfer/templatetags/batch_transfer_extras.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,27 @@
@register.inclusion_tag("core/_job_detail_control_panel.html", takes_context=True)
def job_control_panel(context: dict[str, Any]) -> dict[str, Any]:
return {
"delete_url": "batch_transfer_job_delete",
"verify_url": "batch_transfer_job_verify",
"cancel_url": "batch_transfer_job_cancel",
"resume_url": "batch_transfer_job_resume",
"retry_url": "batch_transfer_job_retry",
"restart_url": "batch_transfer_job_restart",
"job_delete_url": "batch_transfer_job_delete",
"job_verify_url": "batch_transfer_job_verify",
"job_cancel_url": "batch_transfer_job_cancel",
"job_resume_url": "batch_transfer_job_resume",
"job_retry_url": "batch_transfer_job_retry",
"job_restart_url": "batch_transfer_job_restart",
"user": context["user"],
"job": context["job"],
}


@register.inclusion_tag("core/_task_detail_control_panel.html", takes_context=True)
def task_control_panel(context: dict[str, Any]) -> dict[str, Any]:
return {
"task_delete_url": "batch_transfer_task_delete",
"task_reset_url": "batch_transfer_task_reset",
"user": context["user"],
"task": context["task"],
}


@register.filter
def task_status_badge_class(status: TransferTask.Status) -> str:
css_classes = {
Expand Down
12 changes: 12 additions & 0 deletions adit/batch_transfer/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
BatchTransferJobResumeView,
BatchTransferJobRetryView,
BatchTransferJobVerifyView,
BatchTransferTaskDeleteView,
BatchTransferTaskDetailView,
BatchTransferTaskResetView,
BatchTransferUpdatePreferencesView,
)

Expand Down Expand Up @@ -81,4 +83,14 @@
BatchTransferTaskDetailView.as_view(),
name="batch_transfer_task_detail",
),
path(
"tasks/<int:pk>/delete/",
BatchTransferTaskDeleteView.as_view(),
name="batch_transfer_task_delete",
),
path(
"tasks/<int:pk>/reset/",
BatchTransferTaskResetView.as_view(),
name="batch_transfer_task_reset",
),
]
12 changes: 12 additions & 0 deletions adit/batch_transfer/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
DicomJobResumeView,
DicomJobRetryView,
DicomJobVerifyView,
DicomTaskDeleteView,
DicomTaskDetailView,
DicomTaskResetView,
TransferJobListView,
)

Expand Down Expand Up @@ -126,3 +128,13 @@ class BatchTransferTaskDetailView(BatchTransferLockedMixin, DicomTaskDetailView)
model = BatchTransferTask
job_url_name = "batch_transfer_job_detail"
template_name = "batch_transfer/batch_transfer_task_detail.html"


class BatchTransferTaskDeleteView(BatchTransferLockedMixin, DicomTaskDeleteView):
model = BatchTransferTask


class BatchTransferTaskResetView(BatchTransferLockedMixin, DicomTaskResetView):
model = BatchTransferTask
default_priority = settings.BATCH_TRANSFER_DEFAULT_PRIORITY
urgent_priority = settings.BATCH_TRANSFER_URGENT_PRIORITY
35 changes: 27 additions & 8 deletions adit/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models.constraints import UniqueConstraint
from django.db.models.query import QuerySet

from adit.accounts.models import User

Expand Down Expand Up @@ -229,12 +230,17 @@ class Meta:
def __str__(self) -> str:
return f"{self.__class__.__name__} [ID {self.id}]"

def reset_tasks(self, only_failed=False):
def get_absolute_url(self) -> str:
# Gets overridden in subclasses
...

def reset_tasks(self, only_failed=False) -> None:
if only_failed:
dicom_tasks = self.tasks.filter(status=DicomTask.Status.FAILURE)
else:
dicom_tasks = self.tasks.all()

# See also core.views.DicomTaskResetView.post
dicom_tasks.update(
status=DicomJob.Status.PENDING,
retries=0,
Expand All @@ -245,31 +251,31 @@ def reset_tasks(self, only_failed=False):
)

@property
def is_deletable(self):
def is_deletable(self) -> bool:
non_pending_tasks = self.tasks.exclude(status=DicomTask.Status.PENDING)
return (
self.status in [self.Status.UNVERIFIED, self.Status.PENDING]
and non_pending_tasks.count() == 0
)

@property
def is_verified(self):
def is_verified(self) -> bool:
return self.status != self.Status.UNVERIFIED

@property
def is_cancelable(self):
def is_cancelable(self) -> bool:
return self.status in [self.Status.PENDING, self.Status.IN_PROGRESS]

@property
def is_resumable(self):
def is_resumable(self) -> bool:
return self.status == self.Status.CANCELED

@property
def is_retriable(self):
def is_retriable(self) -> bool:
return self.status == self.Status.FAILURE

@property
def is_restartable(self):
def is_restartable(self) -> bool:
return self.status in [
self.Status.CANCELED,
self.Status.SUCCESS,
Expand All @@ -278,7 +284,7 @@ def is_restartable(self):
]

@property
def processed_tasks(self):
def processed_tasks(self) -> QuerySet["DicomTask"]:
non_processed = (
DicomTask.Status.PENDING,
DicomTask.Status.IN_PROGRESS,
Expand Down Expand Up @@ -372,6 +378,19 @@ def __str__(self) -> str:
def queued(self) -> QueuedTask | None:
return self._queued.first()

@property
def is_deletable(self) -> bool:
return self.status == self.Status.PENDING

@property
def is_resettable(self) -> bool:
return self.status in [
self.Status.CANCELED,
self.Status.SUCCESS,
self.Status.WARNING,
self.Status.FAILURE,
]


class TransferTask(DicomTask):
class Meta(DicomTask.Meta):
Expand Down
14 changes: 7 additions & 7 deletions adit/core/templates/core/_job_detail_control_panel.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div class="d-flex">
{% if user.is_staff and not job.is_verified %}
<form class="me-3"
action="{% url verify_url job.pk %}"
action="{% url job_verify_url job.pk %}"
method="post"
onSubmit="return confirm('Are you sure you want to verify this job?');">
{% csrf_token %}
Expand All @@ -14,7 +14,7 @@
{% endif %}
{% if job.is_deletable %}
<form class="me-3"
action="{% url delete_url job.pk %}"
action="{% url job_delete_url job.pk %}"
method="post"
onSubmit="return confirm('Are you sure you want to delete this job?');">
{% csrf_token %}
Expand All @@ -26,7 +26,7 @@
{% endif %}
{% if job.is_cancelable %}
<form class="me-3"
action="{% url cancel_url job.pk %}"
action="{% url job_cancel_url job.pk %}"
method="post"
onSubmit="return confirm('Are you sure you want to cancel this job?');">
{% csrf_token %}
Expand All @@ -38,7 +38,7 @@
{% endif %}
{% if job.is_resumable %}
<form class="me-3"
action="{% url resume_url job.pk %}"
action="{% url job_resume_url job.pk %}"
method="post"
onSubmit="return confirm('Are you sure you want to resume this job?');">
{% csrf_token %}
Expand All @@ -50,7 +50,7 @@
{% endif %}
{% if job.is_retriable %}
<form class="me-3"
action="{% url retry_url job.pk %}"
action="{% url job_retry_url job.pk %}"
method="post"
onSubmit="return confirm('Are you sure you want to retry the failed tasks?');">
{% csrf_token %}
Expand All @@ -62,11 +62,11 @@
{% endif %}
{% if user.is_staff and job.is_restartable %}
<form class="me-3"
action="{% url restart_url job.pk %}"
action="{% url job_restart_url job.pk %}"
method="post"
onSubmit="return confirm('Are you sure you want to restart the entire job?');">
{% csrf_token %}
<button type="submit" class="btn btn-danger">
<button type="submit" class="btn btn-warning">
{% bootstrap_icon "arrow-repeat" %}
Restart Entire Job
</button>
Expand Down
27 changes: 27 additions & 0 deletions adit/core/templates/core/_task_detail_control_panel.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{% load bootstrap_icon from core_extras %}
<div class="d-flex">
{% if task.is_deletable %}
<form class="me-3"
action="{% url task_delete_url task.pk %}"
method="post"
onSubmit="return confirm('Are you sure you want to delete this task?');">
{% csrf_token %}
<button type="submit" class="btn btn-danger">
{% bootstrap_icon "trash" %}
Delete Task
</button>
</form>
{% endif %}
{% if task.is_resettable %}
<form class="me-3"
action="{% url task_reset_url task.pk %}"
method="post"
onSubmit="return confirm('Are you sure you want to reset this task?');">
{% csrf_token %}
<button type="submit" class="btn btn-warning">
{% bootstrap_icon "arrow-counterclockwise" %}
Reset Task
</button>
</form>
{% endif %}
</div>
Loading