refactor(tasks): simplify task tracking code after redesign

- Extract COMPLETE_STATUSES as a class constant on PaperlessTask,
  eliminating the repeated status tuple across models.py, views.py (3×),
  and filters.py
- Extract _CELERY_STATE_TO_STATUS as a module-level constant instead of
  rebuilding the dict on every task_postrun
- Extract _V9_TYPE_TO_TRIGGER_SOURCE and _RUNNABLE_TASKS as class
  constants on TasksViewSet instead of rebuilding on every request
- Extract _TRIGGER_SOURCE_TO_V9_TYPE as a class constant on
  TaskSerializerV9 instead of rebuilding per serialized object
- Extract _get_consume_args helper to deduplicate identical arg
  extraction logic in _extract_input_data, _determine_trigger_source,
  and _extract_owner_id
- Move inline imports (re, traceback) and Avg to module level
- Fix _DOCUMENT_SOURCE_TO_TRIGGER type annotation key type to
  DocumentSource instead of Any
- Remove redundant truthiness checks in SystemStatusView branches
  already guarded by an is-None check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
stumpylog
2026-04-15 18:34:34 -07:00
co-authored by Claude Sonnet 4.6
parent e37ecc2b43
commit 0bc9227d6f
5 changed files with 63 additions and 91 deletions
+2 -7
View File
@@ -906,14 +906,9 @@ class PaperlessTaskFilterSet(FilterSet):
fields = ["task_type", "trigger_source", "status", "acknowledged", "owner"]
def filter_is_complete(self, queryset, name, value):
complete = [
PaperlessTask.Status.SUCCESS,
PaperlessTask.Status.FAILURE,
PaperlessTask.Status.REVOKED,
]
if value:
return queryset.filter(status__in=complete)
return queryset.exclude(status__in=complete)
return queryset.filter(status__in=PaperlessTask.COMPLETE_STATUSES)
return queryset.exclude(status__in=PaperlessTask.COMPLETE_STATUSES)
class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
+8 -9
View File
@@ -687,16 +687,19 @@ class PaperlessTask(ModelWithOwner):
MAIL_FETCH = "mail_fetch", _("Mail Fetch")
LLM_INDEX = "llm_index", _("LLM Index")
COMPLETE_STATUSES = (
Status.SUCCESS,
Status.FAILURE,
Status.REVOKED,
)
class TriggerSource(models.TextChoices):
SCHEDULED = "scheduled", _("Scheduled") # Celery beat
WEB_UI = "web_ui", _("Web UI") # Document uploaded via web
API_UPLOAD = "api_upload", _("API Upload") # Document uploaded via API
FOLDER_CONSUME = "folder_consume", _("Folder Consume") # Consume folder
EMAIL_CONSUME = "email_consume", _("Email Consume") # Email attachment
SYSTEM = (
"system",
_("System"),
) # Auto-triggered by system (self-heal, config side-effect)
SYSTEM = "system", _("System") # Auto-triggered (self-heal, config side-effect)
MANUAL = "manual", _("Manual") # User explicitly ran via /api/tasks/run/
# Identification
@@ -811,11 +814,7 @@ class PaperlessTask(ModelWithOwner):
@property
def is_complete(self) -> bool:
return self.status in (
self.Status.SUCCESS,
self.Status.FAILURE,
self.Status.REVOKED,
)
return self.status in self.COMPLETE_STATUSES
@property
def related_document_ids(self) -> list[int]:
+6 -6
View File
@@ -2529,13 +2529,13 @@ class TaskSerializerV9(serializers.ModelSerializer):
return None
return obj.input_data.get("filename")
_TRIGGER_SOURCE_TO_V9_TYPE = {
PaperlessTask.TriggerSource.SCHEDULED: "SCHEDULED_TASK",
PaperlessTask.TriggerSource.SYSTEM: "AUTO_TASK",
}
def get_type(self, obj: PaperlessTask) -> str:
# Old type values: AUTO_TASK, SCHEDULED_TASK, MANUAL_TASK
source_to_old_type = {
PaperlessTask.TriggerSource.SCHEDULED: "SCHEDULED_TASK",
PaperlessTask.TriggerSource.SYSTEM: "AUTO_TASK",
}
return source_to_old_type.get(obj.trigger_source, "MANUAL_TASK")
return self._TRIGGER_SOURCE_TO_V9_TYPE.get(obj.trigger_source, "MANUAL_TASK")
def get_related_document(self, obj: PaperlessTask) -> int | None:
ids = obj.related_document_ids
+23 -15
View File
@@ -2,7 +2,9 @@ from __future__ import annotations
import hashlib
import logging
import re as _re
import shutil
import traceback as _tb
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
@@ -1009,7 +1011,13 @@ TRACKED_TASKS: dict[str, PaperlessTask.TaskType] = {
"paperless_mail.tasks.process_mail_accounts": PaperlessTask.TaskType.MAIL_FETCH,
}
_DOCUMENT_SOURCE_TO_TRIGGER: dict[Any, PaperlessTask.TriggerSource] = {
_CELERY_STATE_TO_STATUS: dict[str, PaperlessTask.Status] = {
"SUCCESS": PaperlessTask.Status.SUCCESS,
"FAILURE": PaperlessTask.Status.FAILURE,
"REVOKED": PaperlessTask.Status.REVOKED,
}
_DOCUMENT_SOURCE_TO_TRIGGER: dict[DocumentSource, PaperlessTask.TriggerSource] = {
DocumentSource.ConsumeFolder: PaperlessTask.TriggerSource.FOLDER_CONSUME,
DocumentSource.ApiUpload: PaperlessTask.TriggerSource.API_UPLOAD,
DocumentSource.MailFetch: PaperlessTask.TriggerSource.EMAIL_CONSUME,
@@ -1017,14 +1025,23 @@ _DOCUMENT_SOURCE_TO_TRIGGER: dict[Any, PaperlessTask.TriggerSource] = {
}
def _get_consume_args(
args: tuple,
task_kwargs: dict,
) -> tuple[Any | None, Any | None]:
"""Extract (input_doc, overrides) from consume_file task arguments."""
input_doc = args[0] if args else task_kwargs.get("input_doc")
overrides = args[1] if len(args) >= 2 else task_kwargs.get("overrides")
return input_doc, overrides
def _extract_input_data(
task_type: PaperlessTask.TaskType,
args: tuple,
task_kwargs: dict,
) -> dict:
if task_type == PaperlessTask.TaskType.CONSUME_FILE:
input_doc = args[0] if args else task_kwargs.get("input_doc")
overrides = args[1] if len(args) >= 2 else task_kwargs.get("overrides")
input_doc, overrides = _get_consume_args(args, task_kwargs)
if input_doc is None:
return {}
data: dict = {
@@ -1066,7 +1083,7 @@ def _determine_trigger_source(
return PaperlessTask.TriggerSource.SYSTEM
if task_type == PaperlessTask.TaskType.CONSUME_FILE:
input_doc = args[0] if args else task_kwargs.get("input_doc")
input_doc, _ = _get_consume_args(args, task_kwargs)
if input_doc is not None:
return _DOCUMENT_SOURCE_TO_TRIGGER.get(
input_doc.source,
@@ -1083,15 +1100,13 @@ def _extract_owner_id(
) -> int | None:
if task_type != PaperlessTask.TaskType.CONSUME_FILE:
return None
overrides = args[1] if len(args) >= 2 else task_kwargs.get("overrides")
_, overrides = _get_consume_args(args, task_kwargs)
if overrides and hasattr(overrides, "owner_id"):
return overrides.owner_id
return None
def _parse_legacy_result(result: str) -> dict | None:
import re as _re
if match := _re.search(r"New document id (\d+) created", result):
return {"document_id": int(match.group(1))}
if match := _re.search(r"It is a duplicate of .* \(#(\d+)\)", result):
@@ -1187,12 +1202,7 @@ def task_postrun_handler(
try:
close_old_connections()
status_map = {
"SUCCESS": PaperlessTask.Status.SUCCESS,
"FAILURE": PaperlessTask.Status.FAILURE,
"REVOKED": PaperlessTask.Status.REVOKED,
}
new_status = status_map.get(state, PaperlessTask.Status.FAILURE)
new_status = _CELERY_STATE_TO_STATUS.get(state, PaperlessTask.Status.FAILURE)
result_data: dict | None = None
result_message: str | None = None
@@ -1252,8 +1262,6 @@ def task_failure_handler(
"error_message": str(exception) if exception else "Unknown error",
}
if traceback:
import traceback as _tb
tb_str = "".join(_tb.format_tb(traceback))
result_data["traceback"] = tb_str[:5000]
+24 -54
View File
@@ -29,6 +29,7 @@ from django.core.cache import cache
from django.db import connections
from django.db.migrations.loader import MigrationLoader
from django.db.migrations.recorder import MigrationRecorder
from django.db.models import Avg
from django.db.models import Case
from django.db.models import Count
from django.db.models import F
@@ -3787,6 +3788,20 @@ class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
]
ordering = ["-date_created"]
# v9 backwards compat: maps old "type" query param values to new TriggerSource
_V9_TYPE_TO_TRIGGER_SOURCE = {
"AUTO_TASK": PaperlessTask.TriggerSource.SYSTEM,
"SCHEDULED_TASK": PaperlessTask.TriggerSource.SCHEDULED,
"MANUAL_TASK": PaperlessTask.TriggerSource.MANUAL,
}
_RUNNABLE_TASKS = {
PaperlessTask.TaskType.INDEX_OPTIMIZE: (index_optimize, {}),
PaperlessTask.TaskType.TRAIN_CLASSIFIER: (train_classifier, {}),
PaperlessTask.TaskType.SANITY_CHECK: (sanity_check, {"raise_on_error": False}),
PaperlessTask.TaskType.LLM_INDEX: (llmindex_index, {"rebuild": False}),
}
def get_serializer_class(self):
# v9: use backwards-compatible serializer with old field names
if self.request.version and int(self.request.version) < 10:
@@ -3802,13 +3817,7 @@ class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
queryset = queryset.filter(task_type=task_name)
task_type_old = self.request.query_params.get("type")
if task_type_old is not None:
# Old type values: AUTO_TASK -> SYSTEM, SCHEDULED_TASK -> SCHEDULED, MANUAL_TASK -> MANUAL
old_to_new = {
"AUTO_TASK": PaperlessTask.TriggerSource.SYSTEM,
"SCHEDULED_TASK": PaperlessTask.TriggerSource.SCHEDULED,
"MANUAL_TASK": PaperlessTask.TriggerSource.MANUAL,
}
new_source = old_to_new.get(task_type_old)
new_source = self._V9_TYPE_TO_TRIGGER_SOURCE.get(task_type_old)
if new_source:
queryset = queryset.filter(trigger_source=new_source)
# v10+: direct task_id param for backwards compat
@@ -3841,11 +3850,7 @@ class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
self.get_queryset()
.filter(
acknowledged=False,
status__in=[
PaperlessTask.Status.SUCCESS,
PaperlessTask.Status.FAILURE,
PaperlessTask.Status.REVOKED,
],
status__in=PaperlessTask.COMPLETE_STATUSES,
)
.update(acknowledged=True)
)
@@ -3854,11 +3859,6 @@ class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
@action(methods=["get"], detail=False)
def summary(self, request):
"""Aggregated task statistics per task_type over the last N days (default 30)."""
from django.db.models import Avg
from django.db.models import Count
from django.db.models import Max
from django.db.models import Q
days = int(request.query_params.get("days", 30))
cutoff = timezone.now() - timedelta(days=days)
queryset = self.get_queryset().filter(date_created__gte=cutoff)
@@ -3909,27 +3909,14 @@ class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
serializer.is_valid(raise_exception=True)
task_type = serializer.validated_data.get("task_type")
task_func_map = {
PaperlessTask.TaskType.INDEX_OPTIMIZE: (index_optimize, {}),
PaperlessTask.TaskType.TRAIN_CLASSIFIER: (train_classifier, {}),
PaperlessTask.TaskType.SANITY_CHECK: (
sanity_check,
{"raise_on_error": False},
),
PaperlessTask.TaskType.LLM_INDEX: (
llmindex_index,
{"rebuild": False},
),
}
if task_type not in task_func_map:
if task_type not in self._RUNNABLE_TASKS:
return Response(
{"error": f"Task type '{task_type}' cannot be manually triggered"},
status=status.HTTP_400_BAD_REQUEST,
)
try:
task_func, task_kwargs = task_func_map[task_type]
task_func, task_kwargs = self._RUNNABLE_TASKS[task_type]
async_result = task_func.apply_async(
kwargs=task_kwargs,
headers={"trigger_source": "manual"},
@@ -4569,11 +4556,7 @@ class SystemStatusView(PassUserMixin):
last_trained_task = (
PaperlessTask.objects.filter(
task_type=PaperlessTask.TaskType.TRAIN_CLASSIFIER,
status__in=[
PaperlessTask.Status.SUCCESS,
PaperlessTask.Status.FAILURE,
PaperlessTask.Status.REVOKED,
], # ignore running tasks
status__in=PaperlessTask.COMPLETE_STATUSES, # ignore running tasks
)
.order_by("-date_done")
.first()
@@ -4583,10 +4566,7 @@ class SystemStatusView(PassUserMixin):
if last_trained_task is None:
classifier_status = "WARNING"
classifier_error = "No classifier training tasks found"
elif (
last_trained_task
and last_trained_task.status != PaperlessTask.Status.SUCCESS
):
elif last_trained_task.status != PaperlessTask.Status.SUCCESS:
classifier_status = "ERROR"
classifier_error = last_trained_task.result_message
classifier_last_trained = (
@@ -4596,11 +4576,7 @@ class SystemStatusView(PassUserMixin):
last_sanity_check = (
PaperlessTask.objects.filter(
task_type=PaperlessTask.TaskType.SANITY_CHECK,
status__in=[
PaperlessTask.Status.SUCCESS,
PaperlessTask.Status.FAILURE,
PaperlessTask.Status.REVOKED,
], # ignore running tasks
status__in=PaperlessTask.COMPLETE_STATUSES, # ignore running tasks
)
.order_by("-date_done")
.first()
@@ -4610,10 +4586,7 @@ class SystemStatusView(PassUserMixin):
if last_sanity_check is None:
sanity_check_status = "WARNING"
sanity_check_error = "No sanity check tasks found"
elif (
last_sanity_check
and last_sanity_check.status != PaperlessTask.Status.SUCCESS
):
elif last_sanity_check.status != PaperlessTask.Status.SUCCESS:
sanity_check_status = "ERROR"
sanity_check_error = last_sanity_check.result_message
sanity_check_last_run = (
@@ -4638,10 +4611,7 @@ class SystemStatusView(PassUserMixin):
if last_llmindex_update is None:
llmindex_status = "WARNING"
llmindex_error = "No LLM index update tasks found"
elif (
last_llmindex_update
and last_llmindex_update.status == PaperlessTask.Status.FAILURE
):
elif last_llmindex_update.status == PaperlessTask.Status.FAILURE:
llmindex_status = "ERROR"
llmindex_error = last_llmindex_update.result_message
llmindex_last_modified = (