mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-03 23:22:42 -06:00
Merge branch 'dev' into feature-date-parse-plugin
This commit is contained in:
@@ -562,8 +562,8 @@ you may want to adjust these settings to prevent abuse.
|
|||||||
|
|
||||||
#### Workflow placeholders
|
#### Workflow placeholders
|
||||||
|
|
||||||
Titles can be assigned by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
Titles and webhook payloads can be generated by workflows using [Jinja templates](https://jinja.palletsprojects.com/en/3.1.x/templates/).
|
||||||
This allows for complex logic to be used to generate the title, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
|
This allows for complex logic to be used, including [logical structures](https://jinja.palletsprojects.com/en/3.1.x/templates/#list-of-control-structures)
|
||||||
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11).
|
and [filters](https://jinja.palletsprojects.com/en/3.1.x/templates/#id11).
|
||||||
The template is provided as a string.
|
The template is provided as a string.
|
||||||
|
|
||||||
@@ -586,7 +586,7 @@ applied. You can use the following placeholders in the template with any trigger
|
|||||||
- `{{added_time}}`: added time in HH:MM format
|
- `{{added_time}}`: added time in HH:MM format
|
||||||
- `{{original_filename}}`: original file name without extension
|
- `{{original_filename}}`: original file name without extension
|
||||||
- `{{filename}}`: current file name without extension
|
- `{{filename}}`: current file name without extension
|
||||||
- `{{doc_title}}`: current document title
|
- `{{doc_title}}`: current document title (cannot be used in title assignment)
|
||||||
|
|
||||||
The following placeholders are only available for "added" or "updated" triggers
|
The following placeholders are only available for "added" or "updated" triggers
|
||||||
|
|
||||||
|
|||||||
@@ -5359,6 +5359,27 @@
|
|||||||
<context context-type="linenumber">429</context>
|
<context context-type="linenumber">429</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="32686762098259088" datatype="html">
|
||||||
|
<source> One password per line. The workflow will try them in order until one succeeds. </source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">436,438</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3853121441237751087" datatype="html">
|
||||||
|
<source>Passwords</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">441</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3653669613103848563" datatype="html">
|
||||||
|
<source>Passwords are stored in plain text. Use with caution.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">445</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="4626030417479279989" datatype="html">
|
<trans-unit id="4626030417479279989" datatype="html">
|
||||||
<source>Consume Folder</source>
|
<source>Consume Folder</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@@ -5454,109 +5475,116 @@
|
|||||||
<context context-type="linenumber">140</context>
|
<context context-type="linenumber">140</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="4824906895380506720" datatype="html">
|
||||||
|
<source>Password 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">144</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="4522609911791833187" datatype="html">
|
<trans-unit id="4522609911791833187" datatype="html">
|
||||||
<source>Has any of these tags</source>
|
<source>Has any of these tags</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">209</context>
|
<context context-type="linenumber">213</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4166903555074156852" datatype="html">
|
<trans-unit id="4166903555074156852" datatype="html">
|
||||||
<source>Has all of these tags</source>
|
<source>Has all of these tags</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">216</context>
|
<context context-type="linenumber">220</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6624363795312783141" datatype="html">
|
<trans-unit id="6624363795312783141" datatype="html">
|
||||||
<source>Does not have these tags</source>
|
<source>Does not have these tags</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">223</context>
|
<context context-type="linenumber">227</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7168528512669831184" datatype="html">
|
<trans-unit id="7168528512669831184" datatype="html">
|
||||||
<source>Has any of these correspondents</source>
|
<source>Has any of these correspondents</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">230</context>
|
<context context-type="linenumber">234</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5281365940563983618" datatype="html">
|
<trans-unit id="5281365940563983618" datatype="html">
|
||||||
<source>Has correspondent</source>
|
<source>Has correspondent</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">238</context>
|
<context context-type="linenumber">242</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6884498632428600393" datatype="html">
|
<trans-unit id="6884498632428600393" datatype="html">
|
||||||
<source>Does not have correspondents</source>
|
<source>Does not have correspondents</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">246</context>
|
<context context-type="linenumber">250</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4806713133917046341" datatype="html">
|
<trans-unit id="4806713133917046341" datatype="html">
|
||||||
<source>Has document type</source>
|
<source>Has document type</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">254</context>
|
<context context-type="linenumber">258</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8801397520369995032" datatype="html">
|
<trans-unit id="8801397520369995032" datatype="html">
|
||||||
<source>Has any of these document types</source>
|
<source>Has any of these document types</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">262</context>
|
<context context-type="linenumber">266</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1507843981661822403" datatype="html">
|
<trans-unit id="1507843981661822403" datatype="html">
|
||||||
<source>Does not have document types</source>
|
<source>Does not have document types</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">270</context>
|
<context context-type="linenumber">274</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4277260190522078330" datatype="html">
|
<trans-unit id="4277260190522078330" datatype="html">
|
||||||
<source>Has storage path</source>
|
<source>Has storage path</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">278</context>
|
<context context-type="linenumber">282</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8858580062214623097" datatype="html">
|
<trans-unit id="8858580062214623097" datatype="html">
|
||||||
<source>Has any of these storage paths</source>
|
<source>Has any of these storage paths</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">286</context>
|
<context context-type="linenumber">290</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6070943364927280151" datatype="html">
|
<trans-unit id="6070943364927280151" datatype="html">
|
||||||
<source>Does not have storage paths</source>
|
<source>Does not have storage paths</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">294</context>
|
<context context-type="linenumber">298</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6250799006816371860" datatype="html">
|
<trans-unit id="6250799006816371860" datatype="html">
|
||||||
<source>Matches custom field query</source>
|
<source>Matches custom field query</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">302</context>
|
<context context-type="linenumber">306</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3138206142174978019" datatype="html">
|
<trans-unit id="3138206142174978019" datatype="html">
|
||||||
<source>Create new workflow</source>
|
<source>Create new workflow</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">531</context>
|
<context context-type="linenumber">535</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5996779210524133604" datatype="html">
|
<trans-unit id="5996779210524133604" datatype="html">
|
||||||
<source>Edit workflow</source>
|
<source>Edit workflow</source>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">535</context>
|
<context context-type="linenumber">539</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5457837313196342910" datatype="html">
|
<trans-unit id="5457837313196342910" datatype="html">
|
||||||
|
|||||||
@@ -430,6 +430,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@case (WorkflowActionType.PasswordRemoval) {
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<p class="small" i18n>
|
||||||
|
One password per line. The workflow will try them in order until one succeeds.
|
||||||
|
</p>
|
||||||
|
<pngx-input-textarea
|
||||||
|
i18n-title
|
||||||
|
title="Passwords"
|
||||||
|
formControlName="passwords"
|
||||||
|
rows="4"
|
||||||
|
[error]="error?.actions?.[i]?.passwords"
|
||||||
|
hint="Passwords are stored in plain text. Use with caution."
|
||||||
|
i18n-hint
|
||||||
|
></pngx-input-textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
|||||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import {
|
import {
|
||||||
|
FormArray,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormGroup,
|
FormGroup,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@@ -994,4 +995,32 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
component.removeSelectedCustomField(3, formGroup)
|
component.removeSelectedCustomField(3, formGroup)
|
||||||
expect(formGroup.get('assign_custom_fields').value).toEqual([])
|
expect(formGroup.get('assign_custom_fields').value).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should handle parsing of passwords from array to string and back on save', () => {
|
||||||
|
const passwordAction: WorkflowAction = {
|
||||||
|
id: 1,
|
||||||
|
type: WorkflowActionType.PasswordRemoval,
|
||||||
|
passwords: ['pass1', 'pass2'],
|
||||||
|
}
|
||||||
|
component.object = {
|
||||||
|
name: 'Workflow with Passwords',
|
||||||
|
id: 1,
|
||||||
|
order: 1,
|
||||||
|
enabled: true,
|
||||||
|
triggers: [],
|
||||||
|
actions: [passwordAction],
|
||||||
|
}
|
||||||
|
component.ngOnInit()
|
||||||
|
|
||||||
|
const formActions = component.objectForm.get('actions') as FormArray
|
||||||
|
expect(formActions.value[0].passwords).toBe('pass1\npass2')
|
||||||
|
formActions.at(0).get('passwords').setValue('pass1\npass2\npass3')
|
||||||
|
component.save()
|
||||||
|
|
||||||
|
expect(component.objectForm.get('actions').value[0].passwords).toEqual([
|
||||||
|
'pass1',
|
||||||
|
'pass2',
|
||||||
|
'pass3',
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -139,6 +139,10 @@ export const WORKFLOW_ACTION_OPTIONS = [
|
|||||||
id: WorkflowActionType.Webhook,
|
id: WorkflowActionType.Webhook,
|
||||||
name: $localize`Webhook`,
|
name: $localize`Webhook`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: WorkflowActionType.PasswordRemoval,
|
||||||
|
name: $localize`Password removal`,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export enum TriggerFilterType {
|
export enum TriggerFilterType {
|
||||||
@@ -1202,11 +1206,25 @@ export class WorkflowEditDialogComponent
|
|||||||
headers: new FormControl(action.webhook?.headers),
|
headers: new FormControl(action.webhook?.headers),
|
||||||
include_document: new FormControl(!!action.webhook?.include_document),
|
include_document: new FormControl(!!action.webhook?.include_document),
|
||||||
}),
|
}),
|
||||||
|
passwords: new FormControl(
|
||||||
|
this.formatPasswords(action.passwords ?? [])
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
{ emitEvent }
|
{ emitEvent }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatPasswords(passwords: string[] = []): string {
|
||||||
|
return passwords.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
private parsePasswords(value: string = ''): string[] {
|
||||||
|
return value
|
||||||
|
.split(/[\n,]+/)
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter((entry) => entry.length > 0)
|
||||||
|
}
|
||||||
|
|
||||||
private updateAllTriggerActionFields(emitEvent: boolean = false) {
|
private updateAllTriggerActionFields(emitEvent: boolean = false) {
|
||||||
this.triggerFields.clear({ emitEvent: false })
|
this.triggerFields.clear({ emitEvent: false })
|
||||||
this.object?.triggers.forEach((trigger) => {
|
this.object?.triggers.forEach((trigger) => {
|
||||||
@@ -1331,6 +1349,7 @@ export class WorkflowEditDialogComponent
|
|||||||
headers: null,
|
headers: null,
|
||||||
include_document: false,
|
include_document: false,
|
||||||
},
|
},
|
||||||
|
passwords: [],
|
||||||
}
|
}
|
||||||
this.object.actions.push(action)
|
this.object.actions.push(action)
|
||||||
this.createActionField(action)
|
this.createActionField(action)
|
||||||
@@ -1367,6 +1386,7 @@ export class WorkflowEditDialogComponent
|
|||||||
if (action.type !== WorkflowActionType.Email) {
|
if (action.type !== WorkflowActionType.Email) {
|
||||||
action.email = null
|
action.email = null
|
||||||
}
|
}
|
||||||
|
action.passwords = this.parsePasswords(action.passwords as any)
|
||||||
})
|
})
|
||||||
super.save()
|
super.save()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export enum WorkflowActionType {
|
|||||||
Removal = 2,
|
Removal = 2,
|
||||||
Email = 3,
|
Email = 3,
|
||||||
Webhook = 4,
|
Webhook = 4,
|
||||||
|
PasswordRemoval = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowActionEmail extends ObjectWithId {
|
export interface WorkflowActionEmail extends ObjectWithId {
|
||||||
@@ -97,4 +98,6 @@ export interface WorkflowAction extends ObjectWithId {
|
|||||||
email?: WorkflowActionEmail
|
email?: WorkflowActionEmail
|
||||||
|
|
||||||
webhook?: WorkflowActionWebhook
|
webhook?: WorkflowActionWebhook
|
||||||
|
|
||||||
|
passwords?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2025-12-29 03:56
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "0008_sharelinkbundle"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="passwords",
|
||||||
|
field=models.JSONField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Passwords to try when removing PDF protection. Separate with commas or new lines.",
|
||||||
|
null=True,
|
||||||
|
verbose_name="passwords",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="type",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
choices=[
|
||||||
|
(1, "Assignment"),
|
||||||
|
(2, "Removal"),
|
||||||
|
(3, "Email"),
|
||||||
|
(4, "Webhook"),
|
||||||
|
(5, "Password removal"),
|
||||||
|
],
|
||||||
|
default=1,
|
||||||
|
verbose_name="Workflow Action Type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1405,6 +1405,10 @@ class WorkflowAction(models.Model):
|
|||||||
4,
|
4,
|
||||||
_("Webhook"),
|
_("Webhook"),
|
||||||
)
|
)
|
||||||
|
PASSWORD_REMOVAL = (
|
||||||
|
5,
|
||||||
|
_("Password removal"),
|
||||||
|
)
|
||||||
|
|
||||||
type = models.PositiveIntegerField(
|
type = models.PositiveIntegerField(
|
||||||
_("Workflow Action Type"),
|
_("Workflow Action Type"),
|
||||||
@@ -1634,6 +1638,15 @@ class WorkflowAction(models.Model):
|
|||||||
verbose_name=_("webhook"),
|
verbose_name=_("webhook"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
passwords = models.JSONField(
|
||||||
|
_("passwords"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_(
|
||||||
|
"Passwords to try when removing PDF protection. Separate with commas or new lines.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("workflow action")
|
verbose_name = _("workflow action")
|
||||||
verbose_name_plural = _("workflow actions")
|
verbose_name_plural = _("workflow actions")
|
||||||
|
|||||||
@@ -2627,6 +2627,7 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
|||||||
"remove_change_groups",
|
"remove_change_groups",
|
||||||
"email",
|
"email",
|
||||||
"webhook",
|
"webhook",
|
||||||
|
"passwords",
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
@@ -2683,6 +2684,23 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
|||||||
"Webhook data is required for webhook actions",
|
"Webhook data is required for webhook actions",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
"type" in attrs
|
||||||
|
and attrs["type"] == WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL
|
||||||
|
):
|
||||||
|
passwords = attrs.get("passwords")
|
||||||
|
# ensure passwords is a non-empty list of non-empty strings
|
||||||
|
if (
|
||||||
|
passwords is None
|
||||||
|
or not isinstance(passwords, list)
|
||||||
|
or len(passwords) == 0
|
||||||
|
or any(not isinstance(pw, str) for pw in passwords)
|
||||||
|
or any(len(pw.strip()) == 0 for pw in passwords)
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError(
|
||||||
|
"Passwords are required for password removal actions",
|
||||||
|
)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ from documents.permissions import get_objects_for_user_owner_aware
|
|||||||
from documents.templating.utils import convert_format_str_to_template_format
|
from documents.templating.utils import convert_format_str_to_template_format
|
||||||
from documents.workflows.actions import build_workflow_action_context
|
from documents.workflows.actions import build_workflow_action_context
|
||||||
from documents.workflows.actions import execute_email_action
|
from documents.workflows.actions import execute_email_action
|
||||||
|
from documents.workflows.actions import execute_password_removal_action
|
||||||
from documents.workflows.actions import execute_webhook_action
|
from documents.workflows.actions import execute_webhook_action
|
||||||
from documents.workflows.mutations import apply_assignment_to_document
|
from documents.workflows.mutations import apply_assignment_to_document
|
||||||
from documents.workflows.mutations import apply_assignment_to_overrides
|
from documents.workflows.mutations import apply_assignment_to_overrides
|
||||||
@@ -831,6 +832,8 @@ def run_workflows(
|
|||||||
logging_group,
|
logging_group,
|
||||||
original_file,
|
original_file,
|
||||||
)
|
)
|
||||||
|
elif action.type == WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL:
|
||||||
|
execute_password_removal_action(action, document, logging_group)
|
||||||
|
|
||||||
if not use_overrides:
|
if not use_overrides:
|
||||||
# limit title to 128 characters
|
# limit title to 128 characters
|
||||||
|
|||||||
@@ -838,3 +838,61 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.action.refresh_from_db()
|
self.action.refresh_from_db()
|
||||||
self.assertEqual(self.action.assign_title, "Patched Title")
|
self.assertEqual(self.action.assign_title, "Patched Title")
|
||||||
|
|
||||||
|
def test_password_action_passwords_field(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Nothing
|
||||||
|
WHEN:
|
||||||
|
- A workflow password removal action is created with passwords set
|
||||||
|
THEN:
|
||||||
|
- The passwords field is correctly stored and retrieved
|
||||||
|
"""
|
||||||
|
passwords = ["password1", "password2", "password3"]
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/workflow_actions/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||||
|
"passwords": passwords,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
self.assertEqual(response.data["passwords"], passwords)
|
||||||
|
|
||||||
|
def test_password_action_invalid_passwords_field(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Nothing
|
||||||
|
WHEN:
|
||||||
|
- A workflow password removal action is created with invalid passwords field
|
||||||
|
THEN:
|
||||||
|
- The required validation error is raised
|
||||||
|
"""
|
||||||
|
for payload in [
|
||||||
|
{"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL},
|
||||||
|
{
|
||||||
|
"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||||
|
"passwords": "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||||
|
"passwords": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||||
|
"passwords": ["", "password2"],
|
||||||
|
},
|
||||||
|
]:
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/workflow_actions/",
|
||||||
|
json.dumps(payload),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(
|
||||||
|
"Passwords are required",
|
||||||
|
str(response.data["non_field_errors"][0]),
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import datetime
|
|||||||
import json
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
|
import tempfile
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -60,6 +61,7 @@ from documents.tests.utils import DirectoriesMixin
|
|||||||
from documents.tests.utils import DummyProgressManager
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
|
from documents.workflows.actions import execute_password_removal_action
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
|
|
||||||
@@ -3722,6 +3724,196 @@ class TestWorkflows(
|
|||||||
|
|
||||||
mock_post.assert_called_once()
|
mock_post.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.remove_password")
|
||||||
|
def test_password_removal_action_attempts_multiple_passwords(
|
||||||
|
self,
|
||||||
|
mock_remove_password,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow password removal action
|
||||||
|
- Multiple passwords provided
|
||||||
|
WHEN:
|
||||||
|
- Document updated triggering the workflow
|
||||||
|
THEN:
|
||||||
|
- Password removal is attempted until one succeeds
|
||||||
|
"""
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="Protected",
|
||||||
|
checksum="pw-checksum",
|
||||||
|
)
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||||
|
passwords="wrong, right\n extra ",
|
||||||
|
)
|
||||||
|
workflow = Workflow.objects.create(name="Password workflow")
|
||||||
|
workflow.triggers.add(trigger)
|
||||||
|
workflow.actions.add(action)
|
||||||
|
|
||||||
|
mock_remove_password.side_effect = [
|
||||||
|
ValueError("wrong password"),
|
||||||
|
"OK",
|
||||||
|
]
|
||||||
|
|
||||||
|
run_workflows(trigger.type, doc)
|
||||||
|
|
||||||
|
assert mock_remove_password.call_count == 2
|
||||||
|
mock_remove_password.assert_has_calls(
|
||||||
|
[
|
||||||
|
mock.call(
|
||||||
|
[doc.id],
|
||||||
|
password="wrong",
|
||||||
|
update_document=True,
|
||||||
|
user=doc.owner,
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
[doc.id],
|
||||||
|
password="right",
|
||||||
|
update_document=True,
|
||||||
|
user=doc.owner,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.remove_password")
|
||||||
|
def test_password_removal_action_fails_without_correct_password(
|
||||||
|
self,
|
||||||
|
mock_remove_password,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow password removal action
|
||||||
|
- No correct password provided
|
||||||
|
WHEN:
|
||||||
|
- Document updated triggering the workflow
|
||||||
|
THEN:
|
||||||
|
- Password removal is attempted for all passwords and fails
|
||||||
|
"""
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="Protected",
|
||||||
|
checksum="pw-checksum-2",
|
||||||
|
)
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||||
|
passwords=" \n , ",
|
||||||
|
)
|
||||||
|
workflow = Workflow.objects.create(name="Password workflow missing passwords")
|
||||||
|
workflow.triggers.add(trigger)
|
||||||
|
workflow.actions.add(action)
|
||||||
|
|
||||||
|
run_workflows(trigger.type, doc)
|
||||||
|
|
||||||
|
mock_remove_password.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.remove_password")
|
||||||
|
def test_password_removal_action_skips_without_passwords(
|
||||||
|
self,
|
||||||
|
mock_remove_password,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow password removal action with no passwords
|
||||||
|
WHEN:
|
||||||
|
- Workflow is run
|
||||||
|
THEN:
|
||||||
|
- Password removal is not attempted
|
||||||
|
"""
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="Protected",
|
||||||
|
checksum="pw-checksum-2",
|
||||||
|
)
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||||
|
passwords="",
|
||||||
|
)
|
||||||
|
workflow = Workflow.objects.create(name="Password workflow missing passwords")
|
||||||
|
workflow.triggers.add(trigger)
|
||||||
|
workflow.actions.add(action)
|
||||||
|
|
||||||
|
run_workflows(trigger.type, doc)
|
||||||
|
|
||||||
|
mock_remove_password.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.remove_password")
|
||||||
|
def test_password_removal_consumable_document_deferred(
|
||||||
|
self,
|
||||||
|
mock_remove_password,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow password removal action
|
||||||
|
- Simulated consumption trigger (a ConsumableDocument is used)
|
||||||
|
WHEN:
|
||||||
|
- Document consumption is finished
|
||||||
|
THEN:
|
||||||
|
- Password removal is attempted
|
||||||
|
"""
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.PASSWORD_REMOVAL,
|
||||||
|
passwords="first, second",
|
||||||
|
)
|
||||||
|
|
||||||
|
temp_dir = Path(tempfile.mkdtemp())
|
||||||
|
original_file = temp_dir / "file.pdf"
|
||||||
|
original_file.write_bytes(b"pdf content")
|
||||||
|
consumable = ConsumableDocument(
|
||||||
|
source=DocumentSource.ApiUpload,
|
||||||
|
original_file=original_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
execute_password_removal_action(action, consumable, logging_group=None)
|
||||||
|
|
||||||
|
mock_remove_password.assert_not_called()
|
||||||
|
|
||||||
|
mock_remove_password.side_effect = [
|
||||||
|
ValueError("bad password"),
|
||||||
|
"OK",
|
||||||
|
]
|
||||||
|
|
||||||
|
doc = Document.objects.create(
|
||||||
|
checksum="pw-checksum-consumed",
|
||||||
|
title="Protected",
|
||||||
|
)
|
||||||
|
|
||||||
|
document_consumption_finished.send(
|
||||||
|
sender=self.__class__,
|
||||||
|
document=doc,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_remove_password.call_count == 2
|
||||||
|
mock_remove_password.assert_has_calls(
|
||||||
|
[
|
||||||
|
mock.call(
|
||||||
|
[doc.id],
|
||||||
|
password="first",
|
||||||
|
update_document=True,
|
||||||
|
user=doc.owner,
|
||||||
|
),
|
||||||
|
mock.call(
|
||||||
|
[doc.id],
|
||||||
|
password="second",
|
||||||
|
update_document=True,
|
||||||
|
user=doc.owner,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# ensure handler disconnected after first run
|
||||||
|
document_consumption_finished.send(
|
||||||
|
sender=self.__class__,
|
||||||
|
document=doc,
|
||||||
|
)
|
||||||
|
assert mock_remove_password.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookSend:
|
class TestWebhookSend:
|
||||||
def test_send_webhook_data_or_json(
|
def test_send_webhook_data_or_json(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -14,6 +15,7 @@ from documents.models import Document
|
|||||||
from documents.models import DocumentType
|
from documents.models import DocumentType
|
||||||
from documents.models import WorkflowAction
|
from documents.models import WorkflowAction
|
||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
|
from documents.signals import document_consumption_finished
|
||||||
from documents.templating.workflows import parse_w_workflow_placeholders
|
from documents.templating.workflows import parse_w_workflow_placeholders
|
||||||
from documents.workflows.webhooks import send_webhook
|
from documents.workflows.webhooks import send_webhook
|
||||||
|
|
||||||
@@ -265,3 +267,74 @@ def execute_webhook_action(
|
|||||||
f"Error occurred sending webhook: {e}",
|
f"Error occurred sending webhook: {e}",
|
||||||
extra={"group": logging_group},
|
extra={"group": logging_group},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def execute_password_removal_action(
|
||||||
|
action: WorkflowAction,
|
||||||
|
document: Document | ConsumableDocument,
|
||||||
|
logging_group,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Try to remove a password from a document using the configured list.
|
||||||
|
"""
|
||||||
|
passwords = action.passwords
|
||||||
|
if not passwords:
|
||||||
|
logger.warning(
|
||||||
|
"Password removal action %s has no passwords configured",
|
||||||
|
action.pk,
|
||||||
|
extra={"group": logging_group},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
passwords = [
|
||||||
|
password.strip()
|
||||||
|
for password in re.split(r"[,\n]", passwords)
|
||||||
|
if password.strip()
|
||||||
|
]
|
||||||
|
|
||||||
|
if isinstance(document, ConsumableDocument):
|
||||||
|
# hook the consumption-finished signal to attempt password removal later
|
||||||
|
def handler(sender, **kwargs):
|
||||||
|
consumed_document: Document = kwargs.get("document")
|
||||||
|
if consumed_document is not None:
|
||||||
|
execute_password_removal_action(
|
||||||
|
action,
|
||||||
|
consumed_document,
|
||||||
|
logging_group,
|
||||||
|
)
|
||||||
|
document_consumption_finished.disconnect(handler)
|
||||||
|
|
||||||
|
document_consumption_finished.connect(handler, weak=False)
|
||||||
|
return
|
||||||
|
|
||||||
|
# import here to avoid circular dependency
|
||||||
|
from documents.bulk_edit import remove_password
|
||||||
|
|
||||||
|
for password in passwords:
|
||||||
|
try:
|
||||||
|
remove_password(
|
||||||
|
[document.id],
|
||||||
|
password=password,
|
||||||
|
update_document=True,
|
||||||
|
user=document.owner,
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Removed password from document %s using workflow action %s",
|
||||||
|
document.pk,
|
||||||
|
action.pk,
|
||||||
|
extra={"group": logging_group},
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(
|
||||||
|
"Password removal failed for document %s with supplied password: %s",
|
||||||
|
document.pk,
|
||||||
|
e,
|
||||||
|
extra={"group": logging_group},
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
"Password removal failed for document %s after trying all provided passwords",
|
||||||
|
document.pk,
|
||||||
|
extra={"group": logging_group},
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-01-31 17:12+0000\n"
|
"POT-Creation-Date: 2026-02-03 17:32+0000\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@@ -89,7 +89,7 @@ msgstr ""
|
|||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:66 documents/models.py:444 documents/models.py:1646
|
#: documents/models.py:66 documents/models.py:444 documents/models.py:1659
|
||||||
#: paperless_mail/models.py:23 paperless_mail/models.py:143
|
#: paperless_mail/models.py:23 paperless_mail/models.py:143
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -252,7 +252,7 @@ msgid "The position of this document in your physical document archive."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:313 documents/models.py:688 documents/models.py:742
|
#: documents/models.py:313 documents/models.py:688 documents/models.py:742
|
||||||
#: documents/models.py:1689
|
#: documents/models.py:1702
|
||||||
msgid "document"
|
msgid "document"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1089,183 +1089,197 @@ msgid "Webhook"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1410
|
#: documents/models.py:1410
|
||||||
|
msgid "Password removal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:1414
|
||||||
msgid "Workflow Action Type"
|
msgid "Workflow Action Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1415 documents/models.py:1648
|
#: documents/models.py:1419 documents/models.py:1661
|
||||||
#: paperless_mail/models.py:145
|
#: paperless_mail/models.py:145
|
||||||
msgid "order"
|
msgid "order"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1418
|
#: documents/models.py:1422
|
||||||
msgid "assign title"
|
msgid "assign title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1422
|
#: documents/models.py:1426
|
||||||
msgid "Assign a document title, must be a Jinja2 template, see documentation."
|
msgid "Assign a document title, must be a Jinja2 template, see documentation."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1430 paperless_mail/models.py:274
|
#: documents/models.py:1434 paperless_mail/models.py:274
|
||||||
msgid "assign this tag"
|
msgid "assign this tag"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1439 paperless_mail/models.py:282
|
#: documents/models.py:1443 paperless_mail/models.py:282
|
||||||
msgid "assign this document type"
|
msgid "assign this document type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1448 paperless_mail/models.py:296
|
#: documents/models.py:1452 paperless_mail/models.py:296
|
||||||
msgid "assign this correspondent"
|
msgid "assign this correspondent"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1457
|
#: documents/models.py:1461
|
||||||
msgid "assign this storage path"
|
msgid "assign this storage path"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1466
|
#: documents/models.py:1470
|
||||||
msgid "assign this owner"
|
msgid "assign this owner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1473
|
#: documents/models.py:1477
|
||||||
msgid "grant view permissions to these users"
|
msgid "grant view permissions to these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1480
|
#: documents/models.py:1484
|
||||||
msgid "grant view permissions to these groups"
|
msgid "grant view permissions to these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1487
|
#: documents/models.py:1491
|
||||||
msgid "grant change permissions to these users"
|
msgid "grant change permissions to these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1494
|
#: documents/models.py:1498
|
||||||
msgid "grant change permissions to these groups"
|
msgid "grant change permissions to these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1501
|
#: documents/models.py:1505
|
||||||
msgid "assign these custom fields"
|
msgid "assign these custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1505
|
#: documents/models.py:1509
|
||||||
msgid "custom field values"
|
msgid "custom field values"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1509
|
#: documents/models.py:1513
|
||||||
msgid "Optional values to assign to the custom fields."
|
msgid "Optional values to assign to the custom fields."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1518
|
#: documents/models.py:1522
|
||||||
msgid "remove these tag(s)"
|
msgid "remove these tag(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1523
|
#: documents/models.py:1527
|
||||||
msgid "remove all tags"
|
msgid "remove all tags"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1530
|
#: documents/models.py:1534
|
||||||
msgid "remove these document type(s)"
|
msgid "remove these document type(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1535
|
#: documents/models.py:1539
|
||||||
msgid "remove all document types"
|
msgid "remove all document types"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1542
|
#: documents/models.py:1546
|
||||||
msgid "remove these correspondent(s)"
|
msgid "remove these correspondent(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1547
|
#: documents/models.py:1551
|
||||||
msgid "remove all correspondents"
|
msgid "remove all correspondents"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1554
|
#: documents/models.py:1558
|
||||||
msgid "remove these storage path(s)"
|
msgid "remove these storage path(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1559
|
#: documents/models.py:1563
|
||||||
msgid "remove all storage paths"
|
msgid "remove all storage paths"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1566
|
#: documents/models.py:1570
|
||||||
msgid "remove these owner(s)"
|
msgid "remove these owner(s)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1571
|
#: documents/models.py:1575
|
||||||
msgid "remove all owners"
|
msgid "remove all owners"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1578
|
#: documents/models.py:1582
|
||||||
msgid "remove view permissions for these users"
|
msgid "remove view permissions for these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1585
|
#: documents/models.py:1589
|
||||||
msgid "remove view permissions for these groups"
|
msgid "remove view permissions for these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1592
|
#: documents/models.py:1596
|
||||||
msgid "remove change permissions for these users"
|
msgid "remove change permissions for these users"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1599
|
#: documents/models.py:1603
|
||||||
msgid "remove change permissions for these groups"
|
msgid "remove change permissions for these groups"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1604
|
#: documents/models.py:1608
|
||||||
msgid "remove all permissions"
|
msgid "remove all permissions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1611
|
#: documents/models.py:1615
|
||||||
msgid "remove these custom fields"
|
msgid "remove these custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1616
|
#: documents/models.py:1620
|
||||||
msgid "remove all custom fields"
|
msgid "remove all custom fields"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1625
|
#: documents/models.py:1629
|
||||||
msgid "email"
|
msgid "email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1634
|
#: documents/models.py:1638
|
||||||
msgid "webhook"
|
msgid "webhook"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1638
|
#: documents/models.py:1642
|
||||||
|
msgid "passwords"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:1646
|
||||||
|
msgid ""
|
||||||
|
"Passwords to try when removing PDF protection. Separate with commas or new "
|
||||||
|
"lines."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/models.py:1651
|
||||||
msgid "workflow action"
|
msgid "workflow action"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1639
|
#: documents/models.py:1652
|
||||||
msgid "workflow actions"
|
msgid "workflow actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1654
|
#: documents/models.py:1667
|
||||||
msgid "triggers"
|
msgid "triggers"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1661
|
#: documents/models.py:1674
|
||||||
msgid "actions"
|
msgid "actions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1664 paperless_mail/models.py:154
|
#: documents/models.py:1677 paperless_mail/models.py:154
|
||||||
msgid "enabled"
|
msgid "enabled"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1675
|
#: documents/models.py:1688
|
||||||
msgid "workflow"
|
msgid "workflow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1679
|
#: documents/models.py:1692
|
||||||
msgid "workflow trigger type"
|
msgid "workflow trigger type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1693
|
#: documents/models.py:1706
|
||||||
msgid "date run"
|
msgid "date run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1699
|
#: documents/models.py:1712
|
||||||
msgid "workflow run"
|
msgid "workflow run"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/models.py:1700
|
#: documents/models.py:1713
|
||||||
msgid "workflow runs"
|
msgid "workflow runs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user