Support running tasks

This commit is contained in:
shamoon 2025-02-25 11:08:21 -08:00
parent 1c7c703e5f
commit acbb18034a
14 changed files with 455 additions and 200 deletions

View File

@ -1736,7 +1736,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">83</context>
<context context-type="linenumber">87</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -3543,7 +3543,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">79</context>
<context context-type="linenumber">83</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -4101,15 +4101,15 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">165</context>
<context context-type="linenumber">175</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">189</context>
<context context-type="linenumber">209</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">213</context>
<context context-type="linenumber">243</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/toast/toast.component.html</context>
@ -4396,7 +4396,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">125</context>
<context context-type="linenumber">129</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
@ -4992,11 +4992,18 @@
<context context-type="linenumber">72</context>
</context-group>
</trans-unit>
<trans-unit id="235571817610183244" datatype="html">
<source>Web UI</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">76</context>
</context-group>
</trans-unit>
<trans-unit id="3553216189604488439" datatype="html">
<source>Modified</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">87</context>
<context context-type="linenumber">91</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context>
@ -5007,70 +5014,70 @@
<source>Custom Field</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">91</context>
<context context-type="linenumber">95</context>
</context-group>
</trans-unit>
<trans-unit id="8696908693776094667" datatype="html">
<source>Consumption Started</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">98</context>
<context context-type="linenumber">102</context>
</context-group>
</trans-unit>
<trans-unit id="7858311467093621703" datatype="html">
<source>Document Added</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">102</context>
<context context-type="linenumber">106</context>
</context-group>
</trans-unit>
<trans-unit id="7955486237346046731" datatype="html">
<source>Document Updated</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">106</context>
<context context-type="linenumber">110</context>
</context-group>
</trans-unit>
<trans-unit id="9172233176401579786" datatype="html">
<source>Scheduled</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">110</context>
<context context-type="linenumber">114</context>
</context-group>
</trans-unit>
<trans-unit id="5502398334173581061" datatype="html">
<source>Assignment</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">117</context>
<context context-type="linenumber">121</context>
</context-group>
</trans-unit>
<trans-unit id="6234812824772766804" datatype="html">
<source>Removal</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">121</context>
<context context-type="linenumber">125</context>
</context-group>
</trans-unit>
<trans-unit id="4206419737792796794" datatype="html">
<source>Webhook</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">129</context>
<context context-type="linenumber">133</context>
</context-group>
</trans-unit>
<trans-unit id="3138206142174978019" datatype="html">
<source>Create new workflow</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">225</context>
<context context-type="linenumber">229</context>
</context-group>
</trans-unit>
<trans-unit id="5996779210524133604" datatype="html">
<source>Edit workflow</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
<context context-type="linenumber">229</context>
<context context-type="linenumber">233</context>
</context-group>
</trans-unit>
<trans-unit id="6381578200008167206" datatype="html">
@ -5560,7 +5567,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">231</context>
<context context-type="linenumber">261</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
@ -6014,39 +6021,54 @@
<context context-type="linenumber">146</context>
</context-group>
</trans-unit>
<trans-unit id="9127131074422113272" datatype="html">
<source>Run Task</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">166</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">200</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">234</context>
</context-group>
</trans-unit>
<trans-unit id="4089509911694721896" datatype="html">
<source>Last Updated</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">163</context>
<context context-type="linenumber">173</context>
</context-group>
</trans-unit>
<trans-unit id="46628344485199198" datatype="html">
<source>Classifier</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">168</context>
<context context-type="linenumber">178</context>
</context-group>
</trans-unit>
<trans-unit id="6096684179126491743" datatype="html">
<source>Last Trained</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">187</context>
<context context-type="linenumber">207</context>
</context-group>
</trans-unit>
<trans-unit id="6427836860962380759" datatype="html">
<source>Sanity Checker</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">192</context>
<context context-type="linenumber">212</context>
</context-group>
</trans-unit>
<trans-unit id="6578747070254776938" datatype="html">
<source>Last Run</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
<context context-type="linenumber">211</context>
<context context-type="linenumber">241</context>
</context-group>
</trans-unit>
<trans-unit id="6732151329960766506" datatype="html">
@ -9692,28 +9714,28 @@
<source>Connecting...</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/upload-documents.service.ts</context>
<context context-type="linenumber">42</context>
<context context-type="linenumber">43</context>
</context-group>
</trans-unit>
<trans-unit id="1245343823699368872" datatype="html">
<source>Uploading...</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/upload-documents.service.ts</context>
<context context-type="linenumber">54</context>
<context context-type="linenumber">55</context>
</context-group>
</trans-unit>
<trans-unit id="7446520539098045935" datatype="html">
<source>Upload complete, waiting...</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/upload-documents.service.ts</context>
<context context-type="linenumber">57</context>
<context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="1405142710727603568" datatype="html">
<source>HTTP error: <x id="PH" equiv-text="error.status"/> <x id="PH_1" equiv-text="error.statusText"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/upload-documents.service.ts</context>
<context context-type="linenumber">70</context>
<context context-type="linenumber">71</context>
</context-group>
</trans-unit>
<trans-unit id="2119857572761283468" datatype="html">

View File

@ -144,7 +144,7 @@
<div class="card-body">
<dl class="card-text">
<dt i18n>Search Index</dt>
<dd>
<dd class="d-flex align-items-center">
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave">
{{status.tasks.index_status}}
@if (status.tasks.index_status === 'OK') {
@ -157,6 +157,16 @@
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
}
</div>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskName.IndexOptimize)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<div class="badge cursor-pointer bg-info btn-outline-secondary ms-2" (click)="runTask(PaperlessTaskName.IndexOptimize)">
<i-bs name="play-fill"></i-bs>&nbsp;
<ng-container i18n>Run Task</ng-container>
</div>
}
}
</dd>
<ng-template #indexStatus>
@if (status.tasks.index_status === 'OK') {
@ -166,7 +176,7 @@
}
</ng-template>
<dt i18n>Classifier</dt>
<dd>
<dd class="d-flex align-items-center">
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave">
{{status.tasks.classifier_status}}
@if (status.tasks.classifier_status === 'OK') {
@ -181,6 +191,16 @@
[class.text-warning]="status.tasks.classifier_status === SystemStatusItemStatus.WARNING"></i-bs>
}
</div>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskName.TrainClassifier)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<div class="badge cursor-pointer bg-info btn-outline-secondary ms-2" (click)="runTask(PaperlessTaskName.TrainClassifier)">
<i-bs name="play-fill"></i-bs>&nbsp;
<ng-container i18n>Run Task</ng-container>
</div>
}
}
</dd>
<ng-template #classifierStatus>
@if (status.tasks.classifier_status === 'OK') {
@ -190,7 +210,7 @@
}
</ng-template>
<dt i18n>Sanity Checker</dt>
<dd>
<dd class="d-flex align-items-center">
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="sanityCheckerStatus" triggers="mouseenter:mouseleave">
{{status.tasks.sanity_check_status}}
@if (status.tasks.sanity_check_status === 'OK') {
@ -205,6 +225,16 @@
[class.text-warning]="status.tasks.sanity_check_status === SystemStatusItemStatus.WARNING"></i-bs>
}
</div>
@if (currentUserIsSuperUser) {
@if (isRunning(PaperlessTaskName.SanityCheck)) {
<div class="spinner-border spinner-border-sm ms-2" role="status"></div>
} @else {
<div class="badge cursor-pointer bg-info btn-outline-secondary ms-2" (click)="runTask(PaperlessTaskName.SanityCheck)">
<i-bs name="play-fill"></i-bs>&nbsp;
<ng-container i18n>Run Task</ng-container>
</div>
}
}
</dd>
<ng-template #sanityCheckerStatus>
@if (status.tasks.sanity_check_status === 'OK') {

View File

@ -7,12 +7,17 @@ import {
NgbProgressbarModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { PaperlessTaskName } from 'src/app/data/paperless-task'
import {
SystemStatus,
SystemStatusItemStatus,
} from 'src/app/data/system-status'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SystemStatusService } from 'src/app/services/system-status.service'
import { TasksService } from 'src/app/services/tasks.service'
import { ToastService } from 'src/app/services/toast.service'
@Component({
selector: 'pngx-system-status-dialog',
@ -30,13 +35,24 @@ import { FileSizePipe } from 'src/app/pipes/file-size.pipe'
})
export class SystemStatusDialogComponent {
public SystemStatusItemStatus = SystemStatusItemStatus
public PaperlessTaskName = PaperlessTaskName
public status: SystemStatus
public copied: boolean = false
private runningTasks: Set<PaperlessTaskName> = new Set()
get currentUserIsSuperUser(): boolean {
return this.permissionsService.isSuperUser()
}
constructor(
public activeModal: NgbActiveModal,
private clipboard: Clipboard
private clipboard: Clipboard,
private statusService: SystemStatusService,
private tasksService: TasksService,
private toastService: ToastService,
private permissionsService: PermissionsService
) {}
public close() {
@ -56,4 +72,30 @@ export class SystemStatusDialogComponent {
const now = new Date()
return now.getTime() - date.getTime() > hours * 60 * 60 * 1000
}
public isRunning(taskName: PaperlessTaskName): boolean {
return this.runningTasks.has(taskName)
}
public runTask(taskName: PaperlessTaskName) {
this.runningTasks.add(taskName)
this.toastService.showInfo(`Task ${taskName} started`)
this.tasksService.run(taskName).subscribe({
next: () => {
this.runningTasks.delete(taskName)
this.statusService.get().subscribe({
next: (status) => {
this.status = status
},
})
},
error: (err) => {
this.runningTasks.delete(taskName)
this.toastService.showError(
`Failed to start task ${taskName}, see the logs for more details`,
err
)
},
})
}
}

View File

@ -10,6 +10,7 @@ export enum PaperlessTaskName {
ConsumeFile = 'consume_file',
TrainClassifier = 'train_classifier',
SanityCheck = 'check_sanity',
IndexOptimize = 'index_optimize',
}
export enum PaperlessTaskStatus {

View File

@ -132,4 +132,19 @@ describe('TasksService', () => {
expect(tasksService.queuedFileTasks).toHaveLength(1)
expect(tasksService.startedFileTasks).toHaveLength(1)
})
it('supports running tasks', () => {
tasksService.run(PaperlessTaskName.SanityCheck).subscribe((res) => {
expect(res).toEqual({
result: 'success',
})
})
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}tasks/run/`
)
expect(req.request.method).toEqual('POST')
req.flush({
result: 'success',
})
})
})

View File

@ -1,6 +1,6 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Subject } from 'rxjs'
import { Observable, Subject } from 'rxjs'
import { first, takeUntil } from 'rxjs/operators'
import {
PaperlessTask,
@ -14,6 +14,7 @@ import { environment } from 'src/environments/environment'
})
export class TasksService {
private baseUrl: string = environment.apiBaseUrl
private endpoint: string = 'tasks'
public loading: boolean
@ -55,7 +56,7 @@ export class TasksService {
this.http
.get<PaperlessTask[]>(
`${this.baseUrl}tasks/?task_name=consume_file&acknowledged=false`
`${this.baseUrl}${this.endpoint}/?task_name=consume_file&acknowledged=false`
)
.pipe(takeUntil(this.unsubscribeNotifer), first())
.subscribe((r) => {
@ -80,4 +81,13 @@ export class TasksService {
public cancelPending(): void {
this.unsubscribeNotifer.next(true)
}
public run(taskName: PaperlessTaskName): Observable<any> {
return this.http.post<any>(
`${environment.apiBaseUrl}${this.endpoint}/run/`,
{
task_name: taskName,
}
)
}
}

View File

@ -107,6 +107,7 @@ import {
personFillLock,
personLock,
personSquare,
playFill,
plus,
plusCircle,
questionCircle,
@ -311,6 +312,7 @@ const icons = {
personFillLock,
personLock,
personSquare,
playFill,
plus,
plusCircle,
questionCircle,

View File

@ -50,6 +50,7 @@ class Migration(migrations.Migration):
("consume_file", "Consume File"),
("train_classifier", "Train Classifier"),
("check_sanity", "Check Sanity"),
("index_optimize", "Index Optimize"),
],
help_text="Name of the task that was run",
max_length=255,

View File

@ -659,6 +659,7 @@ class PaperlessTask(ModelWithOwner):
CONSUME_FILE = ("consume_file", _("Consume File"))
TRAIN_CLASSIFIER = ("train_classifier", _("Train Classifier"))
CHECK_SANITY = ("check_sanity", _("Check Sanity"))
INDEX_OPTIMIZE = ("index_optimize", _("Index Optimize"))
task_id = models.CharField(
max_length=255,

View File

@ -1742,6 +1742,14 @@ class TasksViewSerializer(OwnedObjectSerializer):
return result
class RunTaskViewSerializer(serializers.Serializer):
task_name = serializers.ChoiceField(
choices=PaperlessTask.TaskName.choices,
label="Task Name",
write_only=True,
)
class AcknowledgeTasksViewSerializer(serializers.Serializer):
tasks = serializers.ListField(
required=True,

View File

@ -201,13 +201,16 @@ def consume_file(
@shared_task
def sanity_check():
messages = sanity_checker.check_sanity()
def sanity_check(*, scheduled=True, raise_on_error=True):
messages = sanity_checker.check_sanity(scheduled=scheduled)
messages.log_messages()
if messages.has_error:
raise SanityCheckFailedException("Sanity check failed with errors. See log.")
message = "Sanity check exited with errors. See log."
if raise_on_error:
raise SanityCheckFailedException(message)
return message
elif messages.has_warning:
return "Sanity check exited with warnings. See log."
elif len(messages) > 0:

View File

@ -1,4 +1,5 @@
import uuid
from unittest import mock
import celery
from django.contrib.auth.models import Permission
@ -309,3 +310,57 @@ class TestTasks(DirectoriesMixin, APITestCase):
returned_data = response.data[0]
self.assertEqual(returned_data["related_document"], "1234")
@mock.patch("documents.tasks.train_classifier")
def test_run_train_classifier_task(self, mock_train_classifier):
"""
GIVEN:
- A superuser
WHEN:
- API call is made to run the train classifier task
THEN:
- The task is run
"""
mock_train_classifier.return_value = "Task started"
response = self.client.post(
self.ENDPOINT + "run/",
{"task_name": PaperlessTask.TaskName.TRAIN_CLASSIFIER},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {"result": "Task started"})
mock_train_classifier.assert_called_once_with(scheduled=False)
# mock error
mock_train_classifier.reset_mock()
mock_train_classifier.side_effect = Exception("Error")
response = self.client.post(
self.ENDPOINT + "run/",
{"task_name": PaperlessTask.TaskName.TRAIN_CLASSIFIER},
)
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
mock_train_classifier.assert_called_once_with(scheduled=False)
@mock.patch("documents.tasks.sanity_check")
def test_run_task_requires_superuser(self, mock_check_sanity):
"""
GIVEN:
- A regular user
WHEN:
- API call is made to run a task
THEN:
- The task is not run
"""
regular_user = User.objects.create_user(username="test")
regular_user.user_permissions.add(*Permission.objects.all())
self.client.logout()
self.client.force_authenticate(user=regular_user)
response = self.client.post(
self.ENDPOINT + "run/",
{"task_name": PaperlessTask.TaskName.CHECK_SANITY},
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
mock_check_sanity.assert_not_called()

View File

@ -38,6 +38,7 @@ from django.http import HttpResponse
from django.http import HttpResponseBadRequest
from django.http import HttpResponseForbidden
from django.http import HttpResponseRedirect
from django.http import HttpResponseServerError
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.utils.decorators import method_decorator
@ -145,6 +146,7 @@ from documents.serialisers import DocumentListSerializer
from documents.serialisers import DocumentSerializer
from documents.serialisers import DocumentTypeSerializer
from documents.serialisers import PostDocumentSerializer
from documents.serialisers import RunTaskViewSerializer
from documents.serialisers import SavedViewSerializer
from documents.serialisers import SearchResultSerializer
from documents.serialisers import ShareLinkSerializer
@ -161,6 +163,9 @@ from documents.serialisers import WorkflowTriggerSerializer
from documents.signals import document_updated
from documents.tasks import consume_file
from documents.tasks import empty_trash
from documents.tasks import index_optimize
from documents.tasks import sanity_check
from documents.tasks import train_classifier
from documents.templating.filepath import validate_filepath_template_and_render
from paperless import version
from paperless.celery import app as celery_app
@ -2233,6 +2238,18 @@ class TasksViewSet(ReadOnlyModelViewSet):
)
filterset_class = PaperlessTaskFilterSet
TASK_AND_ARGS_BY_NAME = {
PaperlessTask.TaskName.INDEX_OPTIMIZE: (index_optimize, {}),
PaperlessTask.TaskName.TRAIN_CLASSIFIER: (
train_classifier,
{"scheduled": False},
),
PaperlessTask.TaskName.CHECK_SANITY: (
sanity_check,
{"scheduled": False, "raise_on_error": False},
),
}
def get_queryset(self):
queryset = PaperlessTask.objects.all().order_by("-date_created")
task_id = self.request.query_params.get("task_id")
@ -2257,6 +2274,25 @@ class TasksViewSet(ReadOnlyModelViewSet):
except Exception:
return HttpResponseBadRequest()
@action(methods=["post"], detail=False)
def run(self, request):
serializer = RunTaskViewSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
task_name = serializer.validated_data.get("task_name")
if not request.user.is_superuser:
return HttpResponseForbidden("Insufficient permissions")
try:
task_func, task_args = self.TASK_AND_ARGS_BY_NAME[task_name]
result = task_func(**task_args)
return Response({"result": result})
except Exception as e:
logger.warning(f"An error occurred running task: {e!s}")
return HttpResponseServerError(
"Error running task, check logs for more detail.",
)
class ShareLinkViewSet(ModelViewSet, PassUserMixin):
model = ShareLink

View File

@ -2,7 +2,7 @@ msgid ""
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-02-14 15:45-0800\n"
"POT-Creation-Date: 2025-02-25 11:07-0800\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
@ -57,31 +57,31 @@ msgstr ""
msgid "Custom field not found"
msgstr ""
#: documents/models.py:41 documents/models.py:829
#: documents/models.py:41 documents/models.py:830
msgid "owner"
msgstr ""
#: documents/models.py:58 documents/models.py:1040
#: documents/models.py:58 documents/models.py:1041
msgid "None"
msgstr ""
#: documents/models.py:59 documents/models.py:1041
#: documents/models.py:59 documents/models.py:1042
msgid "Any word"
msgstr ""
#: documents/models.py:60 documents/models.py:1042
#: documents/models.py:60 documents/models.py:1043
msgid "All words"
msgstr ""
#: documents/models.py:61 documents/models.py:1043
#: documents/models.py:61 documents/models.py:1044
msgid "Exact match"
msgstr ""
#: documents/models.py:62 documents/models.py:1044
#: documents/models.py:62 documents/models.py:1045
msgid "Regular expression"
msgstr ""
#: documents/models.py:63 documents/models.py:1045
#: documents/models.py:63 documents/models.py:1046
msgid "Fuzzy word"
msgstr ""
@ -89,20 +89,20 @@ msgstr ""
msgid "Automatic"
msgstr ""
#: documents/models.py:67 documents/models.py:433 documents/models.py:1521
#: documents/models.py:67 documents/models.py:433 documents/models.py:1526
#: paperless_mail/models.py:23 paperless_mail/models.py:143
msgid "name"
msgstr ""
#: documents/models.py:69 documents/models.py:1108
#: documents/models.py:69 documents/models.py:1110
msgid "match"
msgstr ""
#: documents/models.py:72 documents/models.py:1111
#: documents/models.py:72 documents/models.py:1113
msgid "matching algorithm"
msgstr ""
#: documents/models.py:77 documents/models.py:1116
#: documents/models.py:77 documents/models.py:1118
msgid "is insensitive"
msgstr ""
@ -168,7 +168,7 @@ msgstr ""
msgid "title"
msgstr ""
#: documents/models.py:175 documents/models.py:743
#: documents/models.py:175 documents/models.py:744
msgid "content"
msgstr ""
@ -206,8 +206,8 @@ msgstr ""
msgid "The number of pages of the document."
msgstr ""
#: documents/models.py:221 documents/models.py:401 documents/models.py:749
#: documents/models.py:787 documents/models.py:858 documents/models.py:916
#: documents/models.py:221 documents/models.py:401 documents/models.py:750
#: documents/models.py:788 documents/models.py:859 documents/models.py:917
msgid "created"
msgstr ""
@ -255,8 +255,8 @@ msgstr ""
msgid "The position of this document in your physical document archive."
msgstr ""
#: documents/models.py:295 documents/models.py:760 documents/models.py:814
#: documents/models.py:1564
#: documents/models.py:295 documents/models.py:761 documents/models.py:815
#: documents/models.py:1569
msgid "document"
msgstr ""
@ -320,11 +320,11 @@ msgstr ""
msgid "Title"
msgstr ""
#: documents/models.py:420 documents/models.py:1060
#: documents/models.py:420 documents/models.py:1062
msgid "Created"
msgstr ""
#: documents/models.py:421 documents/models.py:1059
#: documents/models.py:421 documents/models.py:1061
msgid "Added"
msgstr ""
@ -632,589 +632,597 @@ msgstr ""
msgid "Check Sanity"
msgstr ""
#: documents/models.py:666
msgid "Task ID"
#: documents/models.py:662
msgid "Index Optimize"
msgstr ""
#: documents/models.py:667
msgid "Task ID"
msgstr ""
#: documents/models.py:668
msgid "Celery ID for the Task that was run"
msgstr ""
#: documents/models.py:672
#: documents/models.py:673
msgid "Acknowledged"
msgstr ""
#: documents/models.py:673
#: documents/models.py:674
msgid "If the task is acknowledged via the frontend or API"
msgstr ""
#: documents/models.py:679
#: documents/models.py:680
msgid "Task Filename"
msgstr ""
#: documents/models.py:680
#: documents/models.py:681
msgid "Name of the file which the Task was run for"
msgstr ""
#: documents/models.py:687
#: documents/models.py:688
msgid "Task Name"
msgstr ""
#: documents/models.py:688
#: documents/models.py:689
msgid "Name of the task that was run"
msgstr ""
#: documents/models.py:695
#: documents/models.py:696
msgid "Task State"
msgstr ""
#: documents/models.py:696
#: documents/models.py:697
msgid "Current state of the task being run"
msgstr ""
#: documents/models.py:702
#: documents/models.py:703
msgid "Created DateTime"
msgstr ""
#: documents/models.py:703
#: documents/models.py:704
msgid "Datetime field when the task result was created in UTC"
msgstr ""
#: documents/models.py:709
#: documents/models.py:710
msgid "Started DateTime"
msgstr ""
#: documents/models.py:710
#: documents/models.py:711
msgid "Datetime field when the task was started in UTC"
msgstr ""
#: documents/models.py:716
#: documents/models.py:717
msgid "Completed DateTime"
msgstr ""
#: documents/models.py:717
#: documents/models.py:718
msgid "Datetime field when the task was completed in UTC"
msgstr ""
#: documents/models.py:723
#: documents/models.py:724
msgid "Result Data"
msgstr ""
#: documents/models.py:725
#: documents/models.py:726
msgid "The data returned by the task"
msgstr ""
#: documents/models.py:733
#: documents/models.py:734
msgid "Task Type"
msgstr ""
#: documents/models.py:734
#: documents/models.py:735
msgid "The type of task that was run"
msgstr ""
#: documents/models.py:745
#: documents/models.py:746
msgid "Note for the document"
msgstr ""
#: documents/models.py:769
#: documents/models.py:770
msgid "user"
msgstr ""
#: documents/models.py:774
#: documents/models.py:775
msgid "note"
msgstr ""
#: documents/models.py:775
#: documents/models.py:776
msgid "notes"
msgstr ""
#: documents/models.py:783
#: documents/models.py:784
msgid "Archive"
msgstr ""
#: documents/models.py:784
#: documents/models.py:785
msgid "Original"
msgstr ""
#: documents/models.py:795 paperless_mail/models.py:75
#: documents/models.py:796 paperless_mail/models.py:75
msgid "expiration"
msgstr ""
#: documents/models.py:802
#: documents/models.py:803
msgid "slug"
msgstr ""
#: documents/models.py:834
#: documents/models.py:835
msgid "share link"
msgstr ""
#: documents/models.py:835
#: documents/models.py:836
msgid "share links"
msgstr ""
#: documents/models.py:847
#: documents/models.py:848
msgid "String"
msgstr ""
#: documents/models.py:848
#: documents/models.py:849
msgid "URL"
msgstr ""
#: documents/models.py:849
#: documents/models.py:850
msgid "Date"
msgstr ""
#: documents/models.py:850
#: documents/models.py:851
msgid "Boolean"
msgstr ""
#: documents/models.py:851
#: documents/models.py:852
msgid "Integer"
msgstr ""
#: documents/models.py:852
#: documents/models.py:853
msgid "Float"
msgstr ""
#: documents/models.py:853
#: documents/models.py:854
msgid "Monetary"
msgstr ""
#: documents/models.py:854
#: documents/models.py:855
msgid "Document Link"
msgstr ""
#: documents/models.py:855
#: documents/models.py:856
msgid "Select"
msgstr ""
#: documents/models.py:867
#: documents/models.py:868
msgid "data type"
msgstr ""
#: documents/models.py:874
#: documents/models.py:875
msgid "extra data"
msgstr ""
#: documents/models.py:878
#: documents/models.py:879
msgid "Extra data for the custom field, such as select options"
msgstr ""
#: documents/models.py:884
#: documents/models.py:885
msgid "custom field"
msgstr ""
#: documents/models.py:885
#: documents/models.py:886
msgid "custom fields"
msgstr ""
#: documents/models.py:982
#: documents/models.py:983
msgid "custom field instance"
msgstr ""
#: documents/models.py:983
#: documents/models.py:984
msgid "custom field instances"
msgstr ""
#: documents/models.py:1048
#: documents/models.py:1049
msgid "Consumption Started"
msgstr ""
#: documents/models.py:1049
#: documents/models.py:1050
msgid "Document Added"
msgstr ""
#: documents/models.py:1050
#: documents/models.py:1051
msgid "Document Updated"
msgstr ""
#: documents/models.py:1051
#: documents/models.py:1052
msgid "Scheduled"
msgstr ""
#: documents/models.py:1054
#: documents/models.py:1055
msgid "Consume Folder"
msgstr ""
#: documents/models.py:1055
#: documents/models.py:1056
msgid "Api Upload"
msgstr ""
#: documents/models.py:1056
#: documents/models.py:1057
msgid "Mail Fetch"
msgstr ""
#: documents/models.py:1061
#: documents/models.py:1058
msgid "Web UI"
msgstr ""
#: documents/models.py:1063
msgid "Modified"
msgstr ""
#: documents/models.py:1062
#: documents/models.py:1064
msgid "Custom Field"
msgstr ""
#: documents/models.py:1065
#: documents/models.py:1067
msgid "Workflow Trigger Type"
msgstr ""
#: documents/models.py:1077
#: documents/models.py:1079
msgid "filter path"
msgstr ""
#: documents/models.py:1082
#: documents/models.py:1084
msgid ""
"Only consume documents with a path that matches this if specified. Wildcards "
"specified as * are allowed. Case insensitive."
msgstr ""
#: documents/models.py:1089
#: documents/models.py:1091
msgid "filter filename"
msgstr ""
#: documents/models.py:1094 paperless_mail/models.py:200
#: documents/models.py:1096 paperless_mail/models.py:200
msgid ""
"Only consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr ""
#: documents/models.py:1105
#: documents/models.py:1107
msgid "filter documents from this mail rule"
msgstr ""
#: documents/models.py:1121
#: documents/models.py:1123
msgid "has these tag(s)"
msgstr ""
#: documents/models.py:1129
#: documents/models.py:1131
msgid "has this document type"
msgstr ""
#: documents/models.py:1137
#: documents/models.py:1139
msgid "has this correspondent"
msgstr ""
#: documents/models.py:1141
#: documents/models.py:1143
msgid "schedule offset days"
msgstr ""
#: documents/models.py:1144
#: documents/models.py:1146
msgid "The number of days to offset the schedule trigger by."
msgstr ""
#: documents/models.py:1149
#: documents/models.py:1151
msgid "schedule is recurring"
msgstr ""
#: documents/models.py:1152
#: documents/models.py:1154
msgid "If the schedule should be recurring."
msgstr ""
#: documents/models.py:1157
#: documents/models.py:1159
msgid "schedule recurring delay in days"
msgstr ""
#: documents/models.py:1161
#: documents/models.py:1163
msgid "The number of days between recurring schedule triggers."
msgstr ""
#: documents/models.py:1166
#: documents/models.py:1168
msgid "schedule date field"
msgstr ""
#: documents/models.py:1171
#: documents/models.py:1173
msgid "The field to check for a schedule trigger."
msgstr ""
#: documents/models.py:1180
#: documents/models.py:1182
msgid "schedule date custom field"
msgstr ""
#: documents/models.py:1184
#: documents/models.py:1186
msgid "workflow trigger"
msgstr ""
#: documents/models.py:1185
#: documents/models.py:1187
msgid "workflow triggers"
msgstr ""
#: documents/models.py:1193
#: documents/models.py:1195
msgid "email subject"
msgstr ""
#: documents/models.py:1197
#: documents/models.py:1199
msgid ""
"The subject of the email, can include some placeholders, see documentation."
msgstr ""
#: documents/models.py:1203
#: documents/models.py:1205
msgid "email body"
msgstr ""
#: documents/models.py:1206
#: documents/models.py:1208
msgid ""
"The body (message) of the email, can include some placeholders, see "
"documentation."
msgstr ""
#: documents/models.py:1212
#: documents/models.py:1214
msgid "emails to"
msgstr ""
#: documents/models.py:1215
#: documents/models.py:1217
msgid "The destination email addresses, comma separated."
msgstr ""
#: documents/models.py:1221
#: documents/models.py:1223
msgid "include document in email"
msgstr ""
#: documents/models.py:1230
#: documents/models.py:1234
msgid "webhook url"
msgstr ""
#: documents/models.py:1232
#: documents/models.py:1237
msgid "The destination URL for the notification."
msgstr ""
#: documents/models.py:1237
#: documents/models.py:1242
msgid "use parameters"
msgstr ""
#: documents/models.py:1242
#: documents/models.py:1247
msgid "send as JSON"
msgstr ""
#: documents/models.py:1246
#: documents/models.py:1251
msgid "webhook parameters"
msgstr ""
#: documents/models.py:1249
#: documents/models.py:1254
msgid "The parameters to send with the webhook URL if body not used."
msgstr ""
#: documents/models.py:1253
#: documents/models.py:1258
msgid "webhook body"
msgstr ""
#: documents/models.py:1256
#: documents/models.py:1261
msgid "The body to send with the webhook URL if parameters not used."
msgstr ""
#: documents/models.py:1260
#: documents/models.py:1265
msgid "webhook headers"
msgstr ""
#: documents/models.py:1263
#: documents/models.py:1268
msgid "The headers to send with the webhook URL."
msgstr ""
#: documents/models.py:1268
#: documents/models.py:1273
msgid "include document in webhook"
msgstr ""
#: documents/models.py:1279
#: documents/models.py:1284
msgid "Assignment"
msgstr ""
#: documents/models.py:1283
#: documents/models.py:1288
msgid "Removal"
msgstr ""
#: documents/models.py:1287 documents/templates/account/password_reset.html:15
#: documents/models.py:1292 documents/templates/account/password_reset.html:15
msgid "Email"
msgstr ""
#: documents/models.py:1291
#: documents/models.py:1296
msgid "Webhook"
msgstr ""
#: documents/models.py:1295
#: documents/models.py:1300
msgid "Workflow Action Type"
msgstr ""
#: documents/models.py:1301
#: documents/models.py:1306
msgid "assign title"
msgstr ""
#: documents/models.py:1306
#: documents/models.py:1311
msgid ""
"Assign a document title, can include some placeholders, see documentation."
msgstr ""
#: documents/models.py:1315 paperless_mail/models.py:274
#: documents/models.py:1320 paperless_mail/models.py:274
msgid "assign this tag"
msgstr ""
#: documents/models.py:1324 paperless_mail/models.py:282
#: documents/models.py:1329 paperless_mail/models.py:282
msgid "assign this document type"
msgstr ""
#: documents/models.py:1333 paperless_mail/models.py:296
#: documents/models.py:1338 paperless_mail/models.py:296
msgid "assign this correspondent"
msgstr ""
#: documents/models.py:1342
#: documents/models.py:1347
msgid "assign this storage path"
msgstr ""
#: documents/models.py:1351
#: documents/models.py:1356
msgid "assign this owner"
msgstr ""
#: documents/models.py:1358
#: documents/models.py:1363
msgid "grant view permissions to these users"
msgstr ""
#: documents/models.py:1365
#: documents/models.py:1370
msgid "grant view permissions to these groups"
msgstr ""
#: documents/models.py:1372
#: documents/models.py:1377
msgid "grant change permissions to these users"
msgstr ""
#: documents/models.py:1379
#: documents/models.py:1384
msgid "grant change permissions to these groups"
msgstr ""
#: documents/models.py:1386
#: documents/models.py:1391
msgid "assign these custom fields"
msgstr ""
#: documents/models.py:1393
#: documents/models.py:1398
msgid "remove these tag(s)"
msgstr ""
#: documents/models.py:1398
#: documents/models.py:1403
msgid "remove all tags"
msgstr ""
#: documents/models.py:1405
#: documents/models.py:1410
msgid "remove these document type(s)"
msgstr ""
#: documents/models.py:1410
#: documents/models.py:1415
msgid "remove all document types"
msgstr ""
#: documents/models.py:1417
#: documents/models.py:1422
msgid "remove these correspondent(s)"
msgstr ""
#: documents/models.py:1422
#: documents/models.py:1427
msgid "remove all correspondents"
msgstr ""
#: documents/models.py:1429
#: documents/models.py:1434
msgid "remove these storage path(s)"
msgstr ""
#: documents/models.py:1434
#: documents/models.py:1439
msgid "remove all storage paths"
msgstr ""
#: documents/models.py:1441
#: documents/models.py:1446
msgid "remove these owner(s)"
msgstr ""
#: documents/models.py:1446
#: documents/models.py:1451
msgid "remove all owners"
msgstr ""
#: documents/models.py:1453
#: documents/models.py:1458
msgid "remove view permissions for these users"
msgstr ""
#: documents/models.py:1460
#: documents/models.py:1465
msgid "remove view permissions for these groups"
msgstr ""
#: documents/models.py:1467
#: documents/models.py:1472
msgid "remove change permissions for these users"
msgstr ""
#: documents/models.py:1474
#: documents/models.py:1479
msgid "remove change permissions for these groups"
msgstr ""
#: documents/models.py:1479
#: documents/models.py:1484
msgid "remove all permissions"
msgstr ""
#: documents/models.py:1486
#: documents/models.py:1491
msgid "remove these custom fields"
msgstr ""
#: documents/models.py:1491
#: documents/models.py:1496
msgid "remove all custom fields"
msgstr ""
#: documents/models.py:1500
#: documents/models.py:1505
msgid "email"
msgstr ""
#: documents/models.py:1509
#: documents/models.py:1514
msgid "webhook"
msgstr ""
#: documents/models.py:1513
#: documents/models.py:1518
msgid "workflow action"
msgstr ""
#: documents/models.py:1514
#: documents/models.py:1519
msgid "workflow actions"
msgstr ""
#: documents/models.py:1523 paperless_mail/models.py:145
#: documents/models.py:1528 paperless_mail/models.py:145
msgid "order"
msgstr ""
#: documents/models.py:1529
#: documents/models.py:1534
msgid "triggers"
msgstr ""
#: documents/models.py:1536
#: documents/models.py:1541
msgid "actions"
msgstr ""
#: documents/models.py:1539 paperless_mail/models.py:154
#: documents/models.py:1544 paperless_mail/models.py:154
msgid "enabled"
msgstr ""
#: documents/models.py:1550
#: documents/models.py:1555
msgid "workflow"
msgstr ""
#: documents/models.py:1554
#: documents/models.py:1559
msgid "workflow trigger type"
msgstr ""
#: documents/models.py:1568
#: documents/models.py:1573
msgid "date run"
msgstr ""
#: documents/models.py:1574
#: documents/models.py:1579
msgid "workflow run"
msgstr ""
#: documents/models.py:1575
#: documents/models.py:1580
msgid "workflow runs"
msgstr ""
#: documents/serialisers.py:127
#: documents/serialisers.py:128
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
#: documents/serialisers.py:553
#: documents/serialisers.py:554
msgid "Invalid color."
msgstr ""
#: documents/serialisers.py:1554
#: documents/serialisers.py:1570
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
#: documents/serialisers.py:1643
#: documents/serialisers.py:1659
msgid "Invalid variable detected."
msgstr ""
@ -1434,6 +1442,27 @@ msgstr ""
msgid "As a final step, please complete the following form:"
msgstr ""
#: documents/validators.py:24
#, python-brace-format
msgid "Unable to parse URI {value}, missing scheme"
msgstr ""
#: documents/validators.py:29
#, python-brace-format
msgid "Unable to parse URI {value}, missing net location or path"
msgstr ""
#: documents/validators.py:36
msgid ""
"URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '."
"join(allowed_schemes)}"
msgstr ""
#: documents/validators.py:45
#, python-brace-format
msgid "Unable to parse URI {value}"
msgstr ""
#: paperless/apps.py:10
msgid "Paperless"
msgstr ""
@ -1718,7 +1747,7 @@ msgstr ""
msgid "Chinese Traditional"
msgstr ""
#: paperless/urls.py:364
#: paperless/urls.py:369
msgid "Paperless-ngx administration"
msgstr ""