mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-17 17:34:55 +00:00
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:
co-authored by
Claude Sonnet 4.6
parent
e37ecc2b43
commit
0bc9227d6f
@@ -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):
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user