mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-03 00:17:16 +00:00
feat(tasks): replace PaperlessTask model with structured redesign
Drop the old string-based PaperlessTask table and recreate it with Status/TaskType/TriggerSource enums, JSONField result storage, and duration tracking fields. Update all call sites to use the new API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
dc06b679d3
commit
ef4e3d31ef
+128
-56
@@ -3,7 +3,6 @@ from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import pathvalidate
|
||||
from celery import states
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import User
|
||||
@@ -663,97 +662,170 @@ class UiSettings(models.Model):
|
||||
|
||||
|
||||
class PaperlessTask(ModelWithOwner):
|
||||
ALL_STATES = sorted(states.ALL_STATES)
|
||||
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
||||
"""
|
||||
Tracks background task execution for user visibility and debugging.
|
||||
|
||||
State transitions:
|
||||
PENDING -> STARTED -> SUCCESS
|
||||
PENDING -> STARTED -> FAILURE
|
||||
PENDING -> REVOKED (if cancelled before starting)
|
||||
"""
|
||||
|
||||
class Status(models.TextChoices):
|
||||
PENDING = "pending", _("Pending")
|
||||
STARTED = "started", _("Started")
|
||||
SUCCESS = "success", _("Success")
|
||||
FAILURE = "failure", _("Failure")
|
||||
REVOKED = "revoked", _("Revoked")
|
||||
|
||||
class TaskType(models.TextChoices):
|
||||
AUTO = ("auto_task", _("Auto Task"))
|
||||
SCHEDULED_TASK = ("scheduled_task", _("Scheduled Task"))
|
||||
MANUAL_TASK = ("manual_task", _("Manual Task"))
|
||||
CONSUME_FILE = "consume_file", _("Consume File")
|
||||
TRAIN_CLASSIFIER = "train_classifier", _("Train Classifier")
|
||||
SANITY_CHECK = "sanity_check", _("Sanity Check")
|
||||
INDEX_OPTIMIZE = "index_optimize", _("Index Optimize")
|
||||
INDEX_REBUILD = "index_rebuild", _("Index Rebuild")
|
||||
MAIL_FETCH = "mail_fetch", _("Mail Fetch")
|
||||
LLM_INDEX = "llm_index", _("LLM Index")
|
||||
|
||||
class TaskName(models.TextChoices):
|
||||
CONSUME_FILE = ("consume_file", _("Consume File"))
|
||||
TRAIN_CLASSIFIER = ("train_classifier", _("Train Classifier"))
|
||||
CHECK_SANITY = ("check_sanity", _("Check Sanity"))
|
||||
INDEX_OPTIMIZE = ("index_optimize", _("Index Optimize"))
|
||||
LLMINDEX_UPDATE = ("llmindex_update", _("LLM Index Update"))
|
||||
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)
|
||||
MANUAL = "manual", _("Manual") # User explicitly ran via /api/tasks/run/
|
||||
|
||||
# Identification
|
||||
task_id = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
verbose_name=_("Task ID"),
|
||||
help_text=_("Celery ID for the Task that was run"),
|
||||
help_text=_("Celery task ID"),
|
||||
)
|
||||
|
||||
acknowledged = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Acknowledged"),
|
||||
help_text=_("If the task is acknowledged via the frontend or API"),
|
||||
task_type = models.CharField(
|
||||
max_length=50,
|
||||
choices=TaskType.choices,
|
||||
verbose_name=_("Task Type"),
|
||||
help_text=_("The kind of work being performed"),
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
task_file_name = models.CharField(
|
||||
null=True,
|
||||
max_length=255,
|
||||
verbose_name=_("Task Filename"),
|
||||
help_text=_("Name of the file which the Task was run for"),
|
||||
)
|
||||
|
||||
task_name = models.CharField(
|
||||
null=True,
|
||||
max_length=255,
|
||||
choices=TaskName.choices,
|
||||
verbose_name=_("Task Name"),
|
||||
help_text=_("Name of the task that was run"),
|
||||
trigger_source = models.CharField(
|
||||
max_length=50,
|
||||
choices=TriggerSource.choices,
|
||||
verbose_name=_("Trigger Source"),
|
||||
help_text=_("What initiated this task"),
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
# State tracking
|
||||
status = models.CharField(
|
||||
max_length=30,
|
||||
default=states.PENDING,
|
||||
choices=TASK_STATE_CHOICES,
|
||||
verbose_name=_("Task State"),
|
||||
help_text=_("Current state of the task being run"),
|
||||
choices=Status.choices,
|
||||
default=Status.PENDING,
|
||||
verbose_name=_("Status"),
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
date_created = models.DateTimeField(
|
||||
null=True,
|
||||
default=timezone.now,
|
||||
verbose_name=_("Created DateTime"),
|
||||
help_text=_("Datetime field when the task result was created in UTC"),
|
||||
verbose_name=_("Created"),
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
date_started = models.DateTimeField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("Started DateTime"),
|
||||
help_text=_("Datetime field when the task was started in UTC"),
|
||||
blank=True,
|
||||
verbose_name=_("Started"),
|
||||
)
|
||||
|
||||
date_done = models.DateTimeField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("Completed DateTime"),
|
||||
help_text=_("Datetime field when the task was completed in UTC"),
|
||||
blank=True,
|
||||
verbose_name=_("Completed"),
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
result = models.TextField(
|
||||
# Duration fields -- populated by task_postrun signal handler
|
||||
duration_seconds = models.FloatField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("Result Data"),
|
||||
help_text=_(
|
||||
"The data returned by the task",
|
||||
),
|
||||
blank=True,
|
||||
verbose_name=_("Duration (seconds)"),
|
||||
help_text=_("Elapsed time from start to completion"),
|
||||
)
|
||||
|
||||
type = models.CharField(
|
||||
max_length=30,
|
||||
choices=TaskType.choices,
|
||||
default=TaskType.AUTO,
|
||||
verbose_name=_("Task Type"),
|
||||
help_text=_("The type of task that was run"),
|
||||
wait_time_seconds = models.FloatField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Wait Time (seconds)"),
|
||||
help_text=_("Time from task creation to worker pickup"),
|
||||
)
|
||||
|
||||
# Input/Output data
|
||||
input_data = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
verbose_name=_("Input Data"),
|
||||
help_text=_("Structured input parameters for the task"),
|
||||
)
|
||||
|
||||
result_data = models.JSONField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Result Data"),
|
||||
help_text=_("Structured result data from task execution"),
|
||||
)
|
||||
|
||||
result_message = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Result Message"),
|
||||
help_text=_("Human-readable result message"),
|
||||
)
|
||||
|
||||
# Acknowledgment
|
||||
acknowledged = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Acknowledged"),
|
||||
db_index=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Task")
|
||||
verbose_name_plural = _("Tasks")
|
||||
ordering = ["-date_created"]
|
||||
indexes = [
|
||||
models.Index(fields=["status", "date_created"]),
|
||||
models.Index(fields=["task_type", "status"]),
|
||||
models.Index(fields=["owner", "acknowledged", "date_created"]),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Task {self.task_id}"
|
||||
return f"{self.get_task_type_display()} [{self.task_id[:8]}]"
|
||||
|
||||
@property
|
||||
def is_complete(self) -> bool:
|
||||
return self.status in (
|
||||
self.Status.SUCCESS,
|
||||
self.Status.FAILURE,
|
||||
self.Status.REVOKED,
|
||||
)
|
||||
|
||||
@property
|
||||
def related_document_ids(self) -> list[int]:
|
||||
if not self.result_data:
|
||||
return []
|
||||
if doc_id := self.result_data.get("document_id"):
|
||||
return [doc_id]
|
||||
if dup_id := self.result_data.get("duplicate_of"):
|
||||
return [dup_id]
|
||||
return []
|
||||
|
||||
|
||||
class Note(SoftDeleteModel):
|
||||
|
||||
Reference in New Issue
Block a user