From acbb18034a00a38c96482a0b97364ecfa1dbc797 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 25 Feb 2025 11:08:21 -0800 Subject: [PATCH] Support running tasks --- src-ui/messages.xlf | 76 ++-- .../system-status-dialog.component.html | 36 +- .../system-status-dialog.component.ts | 44 ++- src-ui/src/app/data/paperless-task.ts | 1 + src-ui/src/app/services/tasks.service.spec.ts | 15 + src-ui/src/app/services/tasks.service.ts | 14 +- src-ui/src/main.ts | 2 + ..._alter_paperlesstask_task_name_and_more.py | 1 + src/documents/models.py | 1 + src/documents/serialisers.py | 8 + src/documents/tasks.py | 9 +- src/documents/tests/test_api_tasks.py | 55 +++ src/documents/views.py | 36 ++ src/locale/en_US/LC_MESSAGES/django.po | 357 ++++++++++-------- 14 files changed, 455 insertions(+), 200 deletions(-) diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf index fe15461a7..6b908d900 100644 --- a/src-ui/messages.xlf +++ b/src-ui/messages.xlf @@ -1736,7 +1736,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 83 + 87 src/app/components/document-list/document-list.component.html @@ -3543,7 +3543,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 79 + 83 src/app/components/document-list/document-list.component.html @@ -4101,15 +4101,15 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html - 165 + 175 src/app/components/common/system-status-dialog/system-status-dialog.component.html - 189 + 209 src/app/components/common/system-status-dialog/system-status-dialog.component.html - 213 + 243 src/app/components/common/toast/toast.component.html @@ -4396,7 +4396,7 @@ src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 125 + 129 src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html @@ -4992,11 +4992,18 @@ 72 + + Web UI + + src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts + 76 + + Modified src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 87 + 91 src/app/data/document.ts @@ -5007,70 +5014,70 @@ Custom Field src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 91 + 95 Consumption Started src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 98 + 102 Document Added src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 102 + 106 Document Updated src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 106 + 110 Scheduled src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 110 + 114 Assignment src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 117 + 121 Removal src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 121 + 125 Webhook src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 129 + 133 Create new workflow src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 225 + 229 Edit workflow src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts - 229 + 233 @@ -5560,7 +5567,7 @@ src/app/components/common/system-status-dialog/system-status-dialog.component.html - 231 + 261 src/app/components/manage/mail/mail.component.html @@ -6014,39 +6021,54 @@ 146 + + Run Task + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 166 + + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 200 + + + src/app/components/common/system-status-dialog/system-status-dialog.component.html + 234 + + Last Updated src/app/components/common/system-status-dialog/system-status-dialog.component.html - 163 + 173 Classifier src/app/components/common/system-status-dialog/system-status-dialog.component.html - 168 + 178 Last Trained src/app/components/common/system-status-dialog/system-status-dialog.component.html - 187 + 207 Sanity Checker src/app/components/common/system-status-dialog/system-status-dialog.component.html - 192 + 212 Last Run src/app/components/common/system-status-dialog/system-status-dialog.component.html - 211 + 241 @@ -9692,28 +9714,28 @@ Connecting... src/app/services/upload-documents.service.ts - 42 + 43 Uploading... src/app/services/upload-documents.service.ts - 54 + 55 Upload complete, waiting... src/app/services/upload-documents.service.ts - 57 + 58 HTTP error: src/app/services/upload-documents.service.ts - 70 + 71 diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html index c87f80f43..df3b6940d 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.html @@ -144,7 +144,7 @@
Search Index
-
+
{{status.tasks.index_status}} @if (status.tasks.index_status === 'OK') { @@ -157,6 +157,16 @@ }
+ @if (currentUserIsSuperUser) { + @if (isRunning(PaperlessTaskName.IndexOptimize)) { +
+ } @else { +
+   + Run Task +
+ } + }
@if (status.tasks.index_status === 'OK') { @@ -166,7 +176,7 @@ }
Classifier
-
+
{{status.tasks.classifier_status}} @if (status.tasks.classifier_status === 'OK') { @@ -181,6 +191,16 @@ [class.text-warning]="status.tasks.classifier_status === SystemStatusItemStatus.WARNING"> }
+ @if (currentUserIsSuperUser) { + @if (isRunning(PaperlessTaskName.TrainClassifier)) { +
+ } @else { +
+   + Run Task +
+ } + }
@if (status.tasks.classifier_status === 'OK') { @@ -190,7 +210,7 @@ }
Sanity Checker
-
+
{{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"> }
+ @if (currentUserIsSuperUser) { + @if (isRunning(PaperlessTaskName.SanityCheck)) { +
+ } @else { +
+   + Run Task +
+ } + }
@if (status.tasks.sanity_check_status === 'OK') { diff --git a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts index 710d01dcf..d1e1e5745 100644 --- a/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts +++ b/src-ui/src/app/components/common/system-status-dialog/system-status-dialog.component.ts @@ -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 = 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 + ) + }, + }) + } } diff --git a/src-ui/src/app/data/paperless-task.ts b/src-ui/src/app/data/paperless-task.ts index d788dcbd4..1bec277eb 100644 --- a/src-ui/src/app/data/paperless-task.ts +++ b/src-ui/src/app/data/paperless-task.ts @@ -10,6 +10,7 @@ export enum PaperlessTaskName { ConsumeFile = 'consume_file', TrainClassifier = 'train_classifier', SanityCheck = 'check_sanity', + IndexOptimize = 'index_optimize', } export enum PaperlessTaskStatus { diff --git a/src-ui/src/app/services/tasks.service.spec.ts b/src-ui/src/app/services/tasks.service.spec.ts index 940690453..0d4c8ee01 100644 --- a/src-ui/src/app/services/tasks.service.spec.ts +++ b/src-ui/src/app/services/tasks.service.spec.ts @@ -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', + }) + }) }) diff --git a/src-ui/src/app/services/tasks.service.ts b/src-ui/src/app/services/tasks.service.ts index 967d4e8ff..3ecfffe38 100644 --- a/src-ui/src/app/services/tasks.service.ts +++ b/src-ui/src/app/services/tasks.service.ts @@ -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( - `${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 { + return this.http.post( + `${environment.apiBaseUrl}${this.endpoint}/run/`, + { + task_name: taskName, + } + ) + } } diff --git a/src-ui/src/main.ts b/src-ui/src/main.ts index a9d446891..612e883d8 100644 --- a/src-ui/src/main.ts +++ b/src-ui/src/main.ts @@ -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, diff --git a/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py b/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py index 506817ead..aeedbd6a0 100644 --- a/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py +++ b/src/documents/migrations/1063_paperlesstask_type_alter_paperlesstask_task_name_and_more.py @@ -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, diff --git a/src/documents/models.py b/src/documents/models.py index 3f7d459a9..42e473438 100644 --- a/src/documents/models.py +++ b/src/documents/models.py @@ -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, diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 251eb8797..e94f4234a 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -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, diff --git a/src/documents/tasks.py b/src/documents/tasks.py index e3c579743..8a504d28d 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -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: diff --git a/src/documents/tests/test_api_tasks.py b/src/documents/tests/test_api_tasks.py index 3d4839ff5..57b56a2ef 100644 --- a/src/documents/tests/test_api_tasks.py +++ b/src/documents/tests/test_api_tasks.py @@ -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() diff --git a/src/documents/views.py b/src/documents/views.py index 92aea1fe4..99cb9d76e 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -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 diff --git a/src/locale/en_US/LC_MESSAGES/django.po b/src/locale/en_US/LC_MESSAGES/django.po index 3ac865367..5dad5273b 100644 --- a/src/locale/en_US/LC_MESSAGES/django.po +++ b/src/locale/en_US/LC_MESSAGES/django.po @@ -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 ""