### Workflow Triggers
+#### Types
+
Currently, there are three events that correspond to workflow trigger 'types':
1. **Consumption Started**: _before_ a document is consumed, so events can include filters by source (mail, consumption
### Workflow Actions
-There are currently two types of workflow actions, "Assignment", which can assign:
+#### Types
+
+The following workflow action types are available:
+
+##### Assignment
-- Title, see [title placeholders](usage.md#title-placeholders) below
+"Assignment" actions can assign:
+
+- Title, see [workflow placeholders](usage.md#workflow-placeholders) below
- Tags, correspondent, document type and storage path
- Document owner
- View and / or edit permissions to users or groups
- Custom fields. Note that no value for the field will be set
-and "Removal" actions, which can remove either all of or specific sets of the following:
+##### Removal
+
+"Removal" actions can remove either all of or specific sets of the following:
- Tags, correspondents, document types or storage paths
- Document owner
- View and / or edit permissions
- Custom fields
-#### Title placeholders
+##### Email
+
+"Email" actions can send documents via email. This action requires a mail server to be [configured](configuration.md#email-sending). You can specify:
+
+- The recipient email address(es) separated by commas
+- The subject and body of the email, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below
+- Whether to include the document as an attachment
+
+##### Webhook
+
+"Webhook" actions send a POST request to a specified URL. You can specify:
+
+- The URL to send the request to
+- The request body as text or as key-value pairs, which can include placeholders, see [placeholders](usage.md#workflow-placeholders) below.
+- The request headers as key-value pairs
+
+#### Workflow placeholders
-Workflow titles can include placeholders but the available options differ depending on the type of
-workflow trigger. This is because at the time of consumption (when the title is to be set), no automatic tags etc. have been
+Some workflow text can include placeholders but the available options differ depending on the type of
+workflow trigger. This is because at the time of consumption (when the text is to be set), no automatic tags etc. have been
applied. You can use the following placeholders with any trigger type:
- `{correspondent}`: assigned correspondent name
- `{created_month_name_short}`: created month short name
- `{created_day}`: created day
- `{created_time}`: created time in HH:MM format
+- `{doc_url}`: URL to the document in the web UI. Requires the `PAPERLESS_URL` setting to be set.
### Workflow permissions
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
- <context context-type="linenumber">72</context>
+ <context context-type="linenumber">67</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
- <context context-type="linenumber">81</context>
+ <context context-type="linenumber">76</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<source><x id="INTERPOLATION" equiv-text="{{ getDaysRemaining(document) }}"/> days</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
- <context context-type="linenumber">63</context>
+ <context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="6770769801335635194" datatype="html">
<source>Restore</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
- <context context-type="linenumber">71</context>
+ <context context-type="linenumber">66</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
- <context context-type="linenumber">78</context>
+ <context context-type="linenumber">73</context>
</context-group>
</trans-unit>
<trans-unit id="2308646316372333720" datatype="html">
<source>{VAR_PLURAL, plural, =1 {One document in trash} other {<x id="INTERPOLATION"/> total documents in trash}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
- <context context-type="linenumber">94</context>
+ <context context-type="linenumber">89</context>
</context-group>
</trans-unit>
<trans-unit id="9021887951960049161" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">847</context>
+ <context context-type="linenumber">846</context>
</context-group>
</trans-unit>
<trans-unit id="7266264608936522311" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">871</context>
+ <context context-type="linenumber">870</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1171</context>
+ <context context-type="linenumber">1169</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1210</context>
+ <context context-type="linenumber">1207</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1251</context>
+ <context context-type="linenumber">1248</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">63</context>
+ <context context-type="linenumber">68</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
- <context context-type="linenumber">135</context>
+ <context context-type="linenumber">140</context>
</context-group>
</trans-unit>
<trans-unit id="searchResults.noResults" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">824</context>
+ <context context-type="linenumber">823</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<source>Delete original document after successful split</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component.html</context>
- <context context-type="linenumber">51</context>
+ <context context-type="linenumber">49</context>
</context-group>
</trans-unit>
<trans-unit id="2509141182388535183" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">60</context>
+ <context context-type="linenumber">62</context>
</context-group>
</trans-unit>
<trans-unit id="9195188695728229921" datatype="html">
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
<context context-type="linenumber">14</context>
</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">101</context>
+ </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/profile-edit-dialog/profile-edit-dialog.component.html</context>
<context context-type="linenumber">10</context>
<context context-type="linenumber">301</context>
</context-group>
</trans-unit>
+ <trans-unit id="8987736563240025468" datatype="html">
+ <source>Email subject</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">329</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="8239445959209739142" datatype="html">
+ <source>Email body</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">330</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="1222152280703048012" datatype="html">
+ <source>Email recipients</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">331</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="7916910101279824329" datatype="html">
+ <source>Attach document</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">332</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="5028001922785731600" datatype="html">
+ <source>Webhook url</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">340</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="7491983459027245019" datatype="html">
+ <source>Use parameters for webhook body</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">341</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="6806149889743731985" datatype="html">
+ <source>Webhook params</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">343</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="7089924379374330" datatype="html">
+ <source>Webhook body</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">345</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="3829826512656746316" datatype="html">
+ <source>Webhook headers</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">347</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="2114525789021600887" datatype="html">
+ <source>Include document</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">348</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="4626030417479279989" datatype="html">
<source>Consume Folder</source>
<context-group purpose="location">
<context context-type="linenumber">97</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">105</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">172</context>
+ <context context-type="linenumber">180</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">176</context>
+ <context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit id="6381578200008167206" datatype="html">
<source>Create</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
- <context context-type="linenumber">58</context>
+ <context context-type="linenumber">50</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/share-links-dropdown/share-links-dropdown.component.html</context>
<source>Apply</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
- <context context-type="linenumber">64</context>
+ <context context-type="linenumber">56</context>
</context-group>
</trans-unit>
<trans-unit id="7780041345210191160" datatype="html">
<source>Click again to exclude items.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
- <context context-type="linenumber">71</context>
+ <context context-type="linenumber">63</context>
</context-group>
</trans-unit>
<trans-unit id="7593728289020204896" datatype="html">
<source>Not assigned</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
- <context context-type="linenumber">370</context>
+ <context context-type="linenumber">351</context>
</context-group>
<note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
</trans-unit>
<source>Open <x id="PH" equiv-text="this.title"/> filter</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
- <context context-type="linenumber">486</context>
+ <context context-type="linenumber">463</context>
</context-group>
</trans-unit>
<trans-unit id="7005745151564974365" datatype="html">
<context context-type="linenumber">29</context>
</context-group>
</trans-unit>
+ <trans-unit id="3249513483374643425" datatype="html">
+ <source>Add</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/input/entries/entries.component.html</context>
+ <context context-type="linenumber">8</context>
+ </context-group>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
+ <context context-type="linenumber">17</context>
+ </context-group>
+ </trans-unit>
<trans-unit id="6932865105766151309" datatype="html">
<source>Upload</source>
<context-group purpose="location">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.ts</context>
- <context context-type="linenumber">79</context>
+ <context context-type="linenumber">86</context>
</context-group>
</trans-unit>
<trans-unit id="2504502765849142619" datatype="html">
<context context-type="linenumber">45</context>
</context-group>
</trans-unit>
- <trans-unit id="3249513483374643425" datatype="html">
- <source>Add</source>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
- <context context-type="linenumber">17</context>
- </context-group>
- </trans-unit>
<trans-unit id="1230154438678955604" datatype="html">
<source>Change</source>
<context-group purpose="location">
<source>Error loading preview</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/preview-popup/preview-popup.component.html</context>
- <context context-type="linenumber">10</context>
- </context-group>
- </trans-unit>
- <trans-unit id="3601402187462260332" datatype="html">
- <source>Open preview</source>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/components/common/preview-popup/preview-popup.component.ts</context>
- <context context-type="linenumber">37</context>
+ <context context-type="linenumber">4</context>
</context-group>
</trans-unit>
<trans-unit id="2984628903434675339" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">74</context>
+ <context context-type="linenumber">79</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
- <context context-type="linenumber">328</context>
+ <context context-type="linenumber">323</context>
</context-group>
</trans-unit>
<trans-unit id="157572966557284263" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">80</context>
+ <context context-type="linenumber">85</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
- <context context-type="linenumber">335</context>
+ <context context-type="linenumber">330</context>
</context-group>
</trans-unit>
<trans-unit id="8911158217491828773" datatype="html">
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1228</context>
+ <context context-type="linenumber">1225</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/guards/dirty-saved-view.guard.ts</context>
<source>Document saved successfully.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">738</context>
+ <context context-type="linenumber">737</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">752</context>
+ <context context-type="linenumber">751</context>
</context-group>
</trans-unit>
<trans-unit id="448882439049417053" datatype="html">
<source>Error saving document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">756</context>
+ <context context-type="linenumber">755</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">797</context>
+ <context context-type="linenumber">796</context>
</context-group>
</trans-unit>
<trans-unit id="8410796510716511826" datatype="html">
<source>Do you really want to move the document "<x id="PH" equiv-text="this.document.title"/>" to the trash?</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">825</context>
+ <context context-type="linenumber">824</context>
</context-group>
</trans-unit>
<trans-unit id="282586936710748252" datatype="html">
<source>Documents can be restored prior to permanent deletion.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">826</context>
+ <context context-type="linenumber">825</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<source>Move to trash</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">828</context>
+ <context context-type="linenumber">827</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<source>Reprocess confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">867</context>
+ <context context-type="linenumber">866</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<source>This operation will permanently recreate the archive file for this document.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">868</context>
+ <context context-type="linenumber">867</context>
</context-group>
</trans-unit>
<trans-unit id="302054111564709516" datatype="html">
<source>The archive file will be re-generated with the current settings.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">869</context>
+ <context context-type="linenumber">868</context>
</context-group>
</trans-unit>
<trans-unit id="1192507664585066165" datatype="html">
<source>Reprocess operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">879</context>
+ <context context-type="linenumber">878</context>
</context-group>
</trans-unit>
<trans-unit id="4409560272830824468" datatype="html">
<source>Error executing operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">890</context>
+ <context context-type="linenumber">889</context>
</context-group>
</trans-unit>
<trans-unit id="4458954481601077369" datatype="html">
<source>Page Fit</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">963</context>
+ <context context-type="linenumber">962</context>
</context-group>
</trans-unit>
<trans-unit id="1217563727923422413" datatype="html">
<source>Split confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1169</context>
+ <context context-type="linenumber">1167</context>
</context-group>
</trans-unit>
<trans-unit id="2805304563009985503" datatype="html">
<source>This operation will split the selected document(s) into new documents.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1170</context>
+ <context context-type="linenumber">1168</context>
</context-group>
</trans-unit>
<trans-unit id="4158171846914923744" datatype="html">
<source>Split operation will begin in the background.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1186</context>
+ <context context-type="linenumber">1184</context>
</context-group>
</trans-unit>
<trans-unit id="3235014591864339926" datatype="html">
<source>Error executing split operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1195</context>
+ <context context-type="linenumber">1193</context>
</context-group>
</trans-unit>
<trans-unit id="6555329262222566158" datatype="html">
<source>Rotate confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1208</context>
+ <context context-type="linenumber">1205</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
- <context context-type="linenumber">788</context>
+ <context context-type="linenumber">787</context>
</context-group>
</trans-unit>
<trans-unit id="857641176955257111" datatype="html">
<source>This operation will permanently rotate the original version of the current document.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1209</context>
+ <context context-type="linenumber">1206</context>
</context-group>
</trans-unit>
<trans-unit id="4069543875319587651" datatype="html">
<source>Rotation will begin in the background. Close and re-open the document after the operation has completed to see the changes.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1225</context>
+ <context context-type="linenumber">1222</context>
</context-group>
</trans-unit>
<trans-unit id="2962674215361798818" datatype="html">
<source>Error executing rotate operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1237</context>
+ <context context-type="linenumber">1234</context>
</context-group>
</trans-unit>
<trans-unit id="3539261415918606512" datatype="html">
<source>Delete pages confirm</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1249</context>
+ <context context-type="linenumber">1246</context>
</context-group>
</trans-unit>
<trans-unit id="5854352498125813866" datatype="html">
<source>This operation will permanently delete the selected pages from the original document.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1250</context>
+ <context context-type="linenumber">1247</context>
</context-group>
</trans-unit>
<trans-unit id="8617528702531167646" datatype="html">
<source>Delete pages operation will begin in the background. Close and re-open or reload this document after the operation has completed to see the changes.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1265</context>
+ <context context-type="linenumber">1262</context>
</context-group>
</trans-unit>
<trans-unit id="1249139200486584973" datatype="html">
<source>Error executing delete pages operation</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
- <context context-type="linenumber">1274</context>
+ <context context-type="linenumber">1271</context>
</context-group>
</trans-unit>
<trans-unit id="4958946940233632319" datatype="html">
</trans-unit>
<trans-unit id="6390006284731990222" datatype="html">
<source>This operation will permanently rotate the original version of <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source>
+ <context-group purpose="location">
+ <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
+ <context context-type="linenumber">788</context>
+ </context-group>
+ </trans-unit>
+ <trans-unit id="4233432423256408453" datatype="html">
+ <source>This will alter the original copy.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">789</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
- <context context-type="linenumber">304</context>
+ <context context-type="linenumber">299</context>
</context-group>
</trans-unit>
<trans-unit id="106713086593101376" datatype="html">
<source>View notes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">69</context>
+ <context context-type="linenumber">74</context>
</context-group>
</trans-unit>
<trans-unit id="3727324658595204357" datatype="html">
<source>Created: <x id="INTERPOLATION" equiv-text="{{ document.created_date | customDate }}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">93,94</context>
+ <context context-type="linenumber">98,99</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<source>Added: <x id="INTERPOLATION" equiv-text="{{ document.added | customDate }}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">94,95</context>
+ <context context-type="linenumber">99,100</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<source>Modified: <x id="INTERPOLATION" equiv-text="{{ document.modified | customDate }}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">95,96</context>
+ <context context-type="linenumber">100,101</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<source>{VAR_PLURAL, plural, =1 {1 page} other {<x id="INTERPOLATION"/> pages}}</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">112</context>
+ <context context-type="linenumber">117</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<source>Shared</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">122</context>
+ <context context-type="linenumber">127</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-small/document-card-small.component.html</context>
<source>Score:</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
- <context context-type="linenumber">127</context>
+ <context context-type="linenumber">132</context>
</context-group>
</trans-unit>
<trans-unit id="3661756380991326939" datatype="html">
<source>Edit document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
- <context context-type="linenumber">296</context>
- </context-group>
- </trans-unit>
- <trans-unit id="3420321797707163677" datatype="html">
- <source>Preview document</source>
- <context-group purpose="location">
- <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
- <context context-type="linenumber">297</context>
+ <context context-type="linenumber">295</context>
</context-group>
</trans-unit>
<trans-unit id="2807800733729323332" datatype="html">
<source>Yes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
- <context context-type="linenumber">356</context>
+ <context context-type="linenumber">351</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>
<source>No</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
- <context context-type="linenumber">356</context>
+ <context context-type="linenumber">351</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>
<source>English (US)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">51</context>
+ <context context-type="linenumber">46</context>
</context-group>
</trans-unit>
<trans-unit id="7318555235181361185" datatype="html">
<source>Afrikaans</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">57</context>
+ <context context-type="linenumber">52</context>
</context-group>
</trans-unit>
<trans-unit id="6269202464699193298" datatype="html">
<source>Arabic</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">63</context>
+ <context context-type="linenumber">58</context>
</context-group>
</trans-unit>
<trans-unit id="3098941349689899577" datatype="html">
<source>Belarusian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">69</context>
+ <context context-type="linenumber">64</context>
</context-group>
</trans-unit>
<trans-unit id="6821856961727142928" datatype="html">
<source>Bulgarian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">75</context>
+ <context context-type="linenumber">70</context>
</context-group>
</trans-unit>
<trans-unit id="1001043467371963032" datatype="html">
<source>Catalan</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">81</context>
+ <context context-type="linenumber">76</context>
</context-group>
</trans-unit>
<trans-unit id="2719780722934172508" datatype="html">
<source>Czech</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">87</context>
+ <context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="2924289692679201020" datatype="html">
<source>Danish</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">93</context>
+ <context context-type="linenumber">88</context>
</context-group>
</trans-unit>
<trans-unit id="1858110241312746425" datatype="html">
<source>German</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">99</context>
+ <context context-type="linenumber">94</context>
</context-group>
</trans-unit>
<trans-unit id="7067741492320440272" datatype="html">
<source>Greek</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">105</context>
+ <context context-type="linenumber">100</context>
</context-group>
</trans-unit>
<trans-unit id="6987083569809053351" datatype="html">
<source>English (GB)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">111</context>
+ <context context-type="linenumber">106</context>
</context-group>
</trans-unit>
<trans-unit id="5190825892106392539" datatype="html">
<source>Spanish</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">117</context>
+ <context context-type="linenumber">112</context>
</context-group>
</trans-unit>
<trans-unit id="861663369293303028" datatype="html">
<source>Finnish</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">123</context>
+ <context context-type="linenumber">118</context>
</context-group>
</trans-unit>
<trans-unit id="7633754075223722162" datatype="html">
<source>French</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">129</context>
+ <context context-type="linenumber">124</context>
</context-group>
</trans-unit>
<trans-unit id="7891809788881004730" datatype="html">
<source>Hungarian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">135</context>
+ <context context-type="linenumber">130</context>
</context-group>
</trans-unit>
<trans-unit id="2935232983274991580" datatype="html">
<source>Italian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">141</context>
+ <context context-type="linenumber">136</context>
</context-group>
</trans-unit>
<trans-unit id="6924606686202701860" datatype="html">
<source>Japanese</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">147</context>
+ <context context-type="linenumber">142</context>
</context-group>
</trans-unit>
<trans-unit id="6145439649200570157" datatype="html">
<source>Korean</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">153</context>
+ <context context-type="linenumber">148</context>
</context-group>
</trans-unit>
<trans-unit id="1334425850005897370" datatype="html">
<source>Luxembourgish</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">159</context>
+ <context context-type="linenumber">154</context>
</context-group>
</trans-unit>
<trans-unit id="3071065188816255493" datatype="html">
<source>Dutch</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">165</context>
+ <context context-type="linenumber">160</context>
</context-group>
</trans-unit>
<trans-unit id="8069284467804715623" datatype="html">
<source>Norwegian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">171</context>
+ <context context-type="linenumber">166</context>
</context-group>
</trans-unit>
<trans-unit id="792060551707690640" datatype="html">
<source>Polish</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">177</context>
+ <context context-type="linenumber">172</context>
</context-group>
</trans-unit>
<trans-unit id="9184513005098760425" datatype="html">
<source>Portuguese (Brazil)</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">183</context>
+ <context context-type="linenumber">178</context>
</context-group>
</trans-unit>
<trans-unit id="153799456510623899" datatype="html">
<source>Portuguese</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">189</context>
+ <context context-type="linenumber">184</context>
</context-group>
</trans-unit>
<trans-unit id="8118856427047826368" datatype="html">
<source>Romanian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">195</context>
+ <context context-type="linenumber">190</context>
</context-group>
</trans-unit>
<trans-unit id="7137419789978325708" datatype="html">
<source>Russian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">201</context>
+ <context context-type="linenumber">196</context>
</context-group>
</trans-unit>
<trans-unit id="9102963095355753902" datatype="html">
<source>Slovak</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">207</context>
+ <context context-type="linenumber">202</context>
</context-group>
</trans-unit>
<trans-unit id="4287008301409320881" datatype="html">
<source>Slovenian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">213</context>
+ <context context-type="linenumber">208</context>
</context-group>
</trans-unit>
<trans-unit id="8608389829607915090" datatype="html">
<source>Serbian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">219</context>
+ <context context-type="linenumber">214</context>
</context-group>
</trans-unit>
<trans-unit id="499386805970351976" datatype="html">
<source>Swedish</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">225</context>
+ <context context-type="linenumber">220</context>
</context-group>
</trans-unit>
<trans-unit id="5682359291233237791" datatype="html">
<source>Turkish</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">231</context>
+ <context context-type="linenumber">226</context>
</context-group>
</trans-unit>
<trans-unit id="3578644052206125685" datatype="html">
<source>Ukrainian</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">237</context>
+ <context context-type="linenumber">232</context>
</context-group>
</trans-unit>
<trans-unit id="4689443708886954687" datatype="html">
<source>Chinese Simplified</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">243</context>
+ <context context-type="linenumber">238</context>
</context-group>
</trans-unit>
<trans-unit id="4912706592792948707" datatype="html">
<source>ISO 8601</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">251</context>
+ <context context-type="linenumber">246</context>
</context-group>
</trans-unit>
<trans-unit id="313643372755303297" datatype="html">
<source>Successfully completed one-time migratration of settings to the database!</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">584</context>
+ <context context-type="linenumber">574</context>
</context-group>
</trans-unit>
<trans-unit id="5558341108007064934" datatype="html">
<source>Unable to migrate settings to the database, please try saving manually.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">585</context>
+ <context context-type="linenumber">575</context>
</context-group>
</trans-unit>
<trans-unit id="1168781785897678748" datatype="html">
<source>You can restart the tour from the settings page.</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
- <context context-type="linenumber">655</context>
+ <context context-type="linenumber">645</context>
</context-group>
</trans-unit>
<trans-unit id="3852289441366561594" datatype="html">
import { HotkeyDialogComponent } from './components/common/hotkey-dialog/hotkey-dialog.component'
import { DeletePagesConfirmDialogComponent } from './components/common/confirm-dialog/delete-pages-confirm-dialog/delete-pages-confirm-dialog.component'
import { TrashComponent } from './components/admin/trash/trash.component'
+import { EntriesComponent } from './components/common/input/entries/entries.component'
import {
airplane,
archive,
HotkeyDialogComponent,
DeletePagesConfirmDialogComponent,
TrashComponent,
+ EntriesComponent,
],
bootstrap: [AppComponent],
imports: [
</div>
</div>
}
+ @case (WorkflowActionType.Email) {
+ <div class="row" [formGroup]="formGroup.get('email')">
+ <input type="hidden" formControlName="id" />
+ <div class="col">
+ <pngx-input-text i18n-title title="Email subject" formControlName="subject" [error]="error?.actions?.[i]?.email?.subject"></pngx-input-text>
+ <pngx-input-textarea i18n-title title="Email body" formControlName="body" [error]="error?.actions?.[i]?.email?.body"></pngx-input-textarea>
+ <pngx-input-text i18n-title title="Email recipients" formControlName="to" [error]="error?.actions?.[i]?.email?.to"></pngx-input-text>
+ <pngx-input-switch i18n-title title="Attach document" formControlName="include_document"></pngx-input-switch>
+ </div>
+ </div>
+ }
+ @case (WorkflowActionType.Webhook) {
+ <div class="row" [formGroup]="formGroup.get('webhook')">
+ <input type="hidden" formControlName="id" />
+ <div class="col">
+ <pngx-input-text i18n-title title="Webhook url" formControlName="url" [error]="error?.actions?.[i]?.url"></pngx-input-text>
+ <pngx-input-switch i18n-title title="Use parameters for webhook body" formControlName="use_params"></pngx-input-switch>
+ @if (formGroup.get('webhook').value['use_params']) {
+ <pngx-input-entries i18n-title title="Webhook params" formControlName="params" [error]="error?.actions?.[i]?.params"></pngx-input-entries>
+ } @else {
+ <pngx-input-textarea i18n-title title="Webhook body" formControlName="body" [error]="error?.actions?.[i]?.body"></pngx-input-textarea>
+ }
+ <pngx-input-entries i18n-title title="Webhook headers" formControlName="headers" [error]="error?.actions?.[i]?.headers"></pngx-input-entries>
+ <pngx-input-switch i18n-title title="Include document" formControlName="include_document"></pngx-input-switch>
+ </div>
+ </div>
+ }
}
</div>
</ng-template>
component.actionFields.at(0).get('remove_change_groups').disabled
).toBeFalsy()
})
+
+ it('should prune empty nested objects on save', () => {
+ component.object = workflow
+ component.addTrigger()
+ component.addAction()
+ expect(component.objectForm.get('actions').value[0].email).not.toBeNull()
+ expect(component.objectForm.get('actions').value[0].webhook).not.toBeNull()
+ component.save()
+ expect(component.objectForm.get('actions').value[0].email).toBeNull()
+ expect(component.objectForm.get('actions').value[0].webhook).toBeNull()
+ })
})
id: WorkflowActionType.Removal,
name: $localize`Removal`,
},
+ {
+ id: WorkflowActionType.Email,
+ name: $localize`Email`,
+ },
+ {
+ id: WorkflowActionType.Webhook,
+ name: $localize`Webhook`,
+ },
]
const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
remove_all_custom_fields: new FormControl(
action.remove_all_custom_fields
),
+ email: new FormGroup({
+ id: new FormControl(action.email?.id),
+ subject: new FormControl(action.email?.subject),
+ body: new FormControl(action.email?.body),
+ to: new FormControl(action.email?.to),
+ include_document: new FormControl(!!action.email?.include_document),
+ }),
+ webhook: new FormGroup({
+ id: new FormControl(action.webhook?.id),
+ url: new FormControl(action.webhook?.url),
+ use_params: new FormControl(action.webhook?.use_params),
+ params: new FormControl(action.webhook?.params),
+ body: new FormControl(action.webhook?.body),
+ headers: new FormControl(action.webhook?.headers),
+ include_document: new FormControl(!!action.webhook?.include_document),
+ }),
}),
{ emitEvent }
)
remove_all_permissions: false,
remove_custom_fields: [],
remove_all_custom_fields: false,
+ email: {
+ id: null,
+ subject: null,
+ body: null,
+ to: null,
+ include_document: false,
+ },
+ webhook: {
+ id: null,
+ url: null,
+ use_params: true,
+ params: null,
+ body: null,
+ headers: null,
+ include_document: false,
+ },
}
this.object.actions.push(action)
this.createActionField(action)
c.get('id').setValue(null, { emitEvent: false })
)
}
+
+ save(): void {
+ this.objectForm
+ .get('actions')
+ .value.forEach((action: WorkflowAction, i) => {
+ if (action.type !== WorkflowActionType.Webhook) {
+ action.webhook = null
+ }
+ if (action.type !== WorkflowActionType.Email) {
+ action.email = null
+ }
+ })
+ super.save()
+ }
}
--- /dev/null
+<div class="mb-3" [class.pb-3]="error">
+ <div class="row">
+ <div class="d-flex align-items-center mb-2">
+ @if (title) {
+ <label class="form-label mb-0" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
+ }
+ <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="addEntry()">
+ <i-bs name="plus-circle"></i-bs> <ng-container i18n>Add</ng-container>
+ </button>
+ </div>
+ <div class="position-relative">
+ @for (entry of entries; let i = $index; track entry[0]) {
+ <div class="input-group mb-3">
+ <input type="text" class="form-control" [(ngModel)]="entry[0]" (change)="inputChange()" [disabled]="disabled" autocomplete="off">
+ <input type="text" class="form-control" [(ngModel)]="entry[1]" (change)="inputChange()" [disabled]="disabled" autocomplete="off">
+ <button type="button" class="btn btn-outline-secondary" (click)="removeEntry(i)">
+ <i-bs class="text-danger" name="trash"></i-bs>
+ </button>
+ </div>
+ }
+ @if (hint) {
+ <small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
+ }
+ <div class="invalid-feedback position-absolute top-100">
+ {{error}}
+ </div>
+ </div>
+ </div>
+</div>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+import {
+ FormsModule,
+ NG_VALUE_ACCESSOR,
+ ReactiveFormsModule,
+} from '@angular/forms'
+import { EntriesComponent } from './entries.component'
+import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
+
+describe('EntriesComponent', () => {
+ let component: EntriesComponent
+ let fixture: ComponentFixture<EntriesComponent>
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [EntriesComponent],
+ imports: [
+ FormsModule,
+ ReactiveFormsModule,
+ NgxBootstrapIconsModule.pick(allIcons),
+ ],
+ }).compileComponents()
+ })
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(EntriesComponent)
+ component = fixture.componentInstance
+ fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
+ fixture.detectChanges()
+ })
+
+ it('should add an entry', () => {
+ component.addEntry()
+ expect(component.entries.length).toBe(1)
+ expect(component.entries[0]).toEqual(['', ''])
+ })
+
+ it('should remove an entry', () => {
+ component.addEntry()
+ component.addEntry()
+ expect(component.entries.length).toBe(2)
+ component.removeEntry(0)
+ expect(component.entries.length).toBe(1)
+ })
+
+ it('should write value correctly', () => {
+ const newValue = { key1: 'value1', key2: 'value2' }
+ component.writeValue(newValue)
+ expect(component.entries).toEqual(Object.entries(newValue))
+ component.writeValue(null)
+ expect(component.entries).toEqual([])
+ })
+
+ it('should correctly generate the value on input change', () => {
+ const onChangeSpy = jest.spyOn(component, 'onChange')
+ component.entries = [
+ ['key1', 'value1'],
+ ['key2', ''],
+ ['', ''],
+ ]
+ component.inputChange()
+ // Only the first two entries should be included
+ expect(onChangeSpy).toHaveBeenCalledWith({ key1: 'value1', key2: '' })
+ })
+})
--- /dev/null
+import { Component, forwardRef } from '@angular/core'
+import { AbstractInputComponent } from '../abstract-input'
+import { NG_VALUE_ACCESSOR } from '@angular/forms'
+
+@Component({
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => EntriesComponent),
+ multi: true,
+ },
+ ],
+ selector: 'pngx-input-entries',
+ templateUrl: './entries.component.html',
+ styleUrl: './entries.component.scss',
+})
+export class EntriesComponent extends AbstractInputComponent<object> {
+ entries = []
+
+ constructor() {
+ super()
+ }
+
+ inputChange(): void {
+ // Remove empty keys
+ this.onChange(
+ Object.fromEntries(this.entries.filter(([key]) => key?.length))
+ )
+ }
+
+ writeValue(newValue: any): void {
+ if (!newValue) {
+ newValue = {}
+ }
+ this.entries = Object.entries(newValue)
+ this.value = newValue
+ }
+
+ addEntry(): void {
+ this.entries.push(['', ''])
+ this.inputChange()
+ }
+
+ removeEntry(index: number): void {
+ this.entries.splice(index, 1)
+ this.inputChange()
+ }
+}
export enum WorkflowActionType {
Assignment = 1,
Removal = 2,
+ Email = 3,
+ Webhook = 4,
}
+
+export interface WorkflowActionEmail extends ObjectWithId {
+ subject?: string
+
+ body?: string
+
+ to?: string
+
+ include_document?: boolean
+}
+
+export interface WorkflowActionWebhook extends ObjectWithId {
+ url?: string
+
+ use_params?: boolean
+
+ params?: object
+
+ body?: string
+
+ headers?: object
+
+ include_document?: boolean
+}
+
export interface WorkflowAction extends ObjectWithId {
type: WorkflowActionType
remove_custom_fields?: number[] // [CustomField.id]
remove_all_custom_fields?: boolean
+
+ email?: WorkflowActionEmail
+
+ webhook?: WorkflowActionWebhook
}
from documents.signals import document_consumption_finished
from documents.signals import document_consumption_started
from documents.signals.handlers import run_workflows
-from documents.templating.title import parse_doc_title_w_placeholders
+from documents.templating.workflows import parse_w_workflow_placeholders
from documents.utils import copy_basic_file_stats
from documents.utils import copy_file_with_basic_stats
from documents.utils import run_subprocess
else None
)
- return parse_doc_title_w_placeholders(
+ return parse_w_workflow_placeholders(
title,
correspondent_name,
doc_type_name,
)
return {
- "EMAIL_ENABLED": django_settings.EMAIL_HOST != "localhost"
- or django_settings.EMAIL_HOST_USER != "",
+ "EMAIL_ENABLED": django_settings.EMAIL_ENABLED,
"DISABLE_REGULAR_LOGIN": django_settings.DISABLE_REGULAR_LOGIN,
"REDIRECT_LOGIN_TO_SSO": django_settings.REDIRECT_LOGIN_TO_SSO,
"ACCOUNT_ALLOW_SIGNUPS": django_settings.ACCOUNT_ALLOW_SIGNUPS,
--- /dev/null
+# Generated by Django 5.1.3 on 2024-11-26 04:07
+
+import django.db.models.deletion
+from django.db import migrations
+from django.db import models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="WorkflowActionEmail",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "subject",
+ models.CharField(
+ help_text="The subject of the email, can include some placeholders, see documentation.",
+ max_length=256,
+ verbose_name="email subject",
+ ),
+ ),
+ (
+ "body",
+ models.TextField(
+ help_text="The body (message) of the email, can include some placeholders, see documentation.",
+ verbose_name="email body",
+ ),
+ ),
+ (
+ "to",
+ models.TextField(
+ help_text="The destination email addresses, comma separated.",
+ verbose_name="emails to",
+ ),
+ ),
+ (
+ "include_document",
+ models.BooleanField(
+ default=False,
+ verbose_name="include document in email",
+ ),
+ ),
+ ],
+ ),
+ migrations.CreateModel(
+ name="WorkflowActionWebhook",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "url",
+ models.URLField(
+ help_text="The destination URL for the notification.",
+ verbose_name="webhook url",
+ ),
+ ),
+ (
+ "use_params",
+ models.BooleanField(default=True, verbose_name="use parameters"),
+ ),
+ (
+ "params",
+ models.JSONField(
+ blank=True,
+ help_text="The parameters to send with the webhook URL if body not used.",
+ null=True,
+ verbose_name="webhook parameters",
+ ),
+ ),
+ (
+ "body",
+ models.TextField(
+ blank=True,
+ help_text="The body to send with the webhook URL if parameters not used.",
+ null=True,
+ verbose_name="webhook body",
+ ),
+ ),
+ (
+ "headers",
+ models.JSONField(
+ blank=True,
+ help_text="The headers to send with the webhook URL.",
+ null=True,
+ verbose_name="webhook headers",
+ ),
+ ),
+ (
+ "include_document",
+ models.BooleanField(
+ default=False,
+ verbose_name="include document in webhook",
+ ),
+ ),
+ ],
+ ),
+ migrations.AlterField(
+ model_name="workflowaction",
+ name="type",
+ field=models.PositiveIntegerField(
+ choices=[
+ (1, "Assignment"),
+ (2, "Removal"),
+ (3, "Email"),
+ (4, "Webhook"),
+ ],
+ default=1,
+ verbose_name="Workflow Action Type",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="email",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="action",
+ to="documents.workflowactionemail",
+ verbose_name="email",
+ ),
+ ),
+ migrations.AddField(
+ model_name="workflowaction",
+ name="webhook",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="action",
+ to="documents.workflowactionwebhook",
+ verbose_name="webhook",
+ ),
+ ),
+ ]
class Migration(migrations.Migration):
dependencies = [
- ("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"),
+ ("documents", "1059_workflowactionemail_workflowactionwebhook_and_more"),
]
operations = [
return f"WorkflowTrigger {self.pk}"
+class WorkflowActionEmail(models.Model):
+ subject = models.CharField(
+ _("email subject"),
+ max_length=256,
+ null=False,
+ help_text=_(
+ "The subject of the email, can include some placeholders, "
+ "see documentation.",
+ ),
+ )
+
+ body = models.TextField(
+ _("email body"),
+ null=False,
+ help_text=_(
+ "The body (message) of the email, can include some placeholders, "
+ "see documentation.",
+ ),
+ )
+
+ to = models.TextField(
+ _("emails to"),
+ null=False,
+ help_text=_(
+ "The destination email addresses, comma separated.",
+ ),
+ )
+
+ include_document = models.BooleanField(
+ default=False,
+ verbose_name=_("include document in email"),
+ )
+
+ def __str__(self):
+ return f"Workflow Email Action {self.pk}"
+
+
+class WorkflowActionWebhook(models.Model):
+ url = models.URLField(
+ _("webhook url"),
+ null=False,
+ help_text=_("The destination URL for the notification."),
+ )
+
+ use_params = models.BooleanField(
+ default=True,
+ verbose_name=_("use parameters"),
+ )
+
+ params = models.JSONField(
+ _("webhook parameters"),
+ null=True,
+ blank=True,
+ help_text=_("The parameters to send with the webhook URL if body not used."),
+ )
+
+ body = models.TextField(
+ _("webhook body"),
+ null=True,
+ blank=True,
+ help_text=_("The body to send with the webhook URL if parameters not used."),
+ )
+
+ headers = models.JSONField(
+ _("webhook headers"),
+ null=True,
+ blank=True,
+ help_text=_("The headers to send with the webhook URL."),
+ )
+
+ include_document = models.BooleanField(
+ default=False,
+ verbose_name=_("include document in webhook"),
+ )
+
+ def __str__(self):
+ return f"Workflow Webhook Action {self.pk}"
+
+
class WorkflowAction(models.Model):
class WorkflowActionType(models.IntegerChoices):
ASSIGNMENT = (
2,
_("Removal"),
)
+ EMAIL = (
+ 3,
+ _("Email"),
+ )
+ WEBHOOK = (
+ 4,
+ _("Webhook"),
+ )
type = models.PositiveIntegerField(
_("Workflow Action Type"),
verbose_name=_("remove all custom fields"),
)
+ email = models.ForeignKey(
+ WorkflowActionEmail,
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name="action",
+ verbose_name=_("email"),
+ )
+
+ webhook = models.ForeignKey(
+ WorkflowActionWebhook,
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name="action",
+ verbose_name=_("webhook"),
+ )
+
class Meta:
verbose_name = _("workflow action")
verbose_name_plural = _("workflow actions")
from documents.models import UiSettings
from documents.models import Workflow
from documents.models import WorkflowAction
+from documents.models import WorkflowActionEmail
+from documents.models import WorkflowActionWebhook
from documents.models import WorkflowTrigger
from documents.parsers import is_mime_type_supported
from documents.permissions import get_groups_with_only_permission
return attrs
+class WorkflowActionEmailSerializer(serializers.ModelSerializer):
+ id = serializers.IntegerField(allow_null=True, required=False)
+
+ class Meta:
+ model = WorkflowActionEmail
+ fields = [
+ "id",
+ "subject",
+ "body",
+ "to",
+ "include_document",
+ ]
+
+
+class WorkflowActionWebhookSerializer(serializers.ModelSerializer):
+ id = serializers.IntegerField(allow_null=True, required=False)
+
+ class Meta:
+ model = WorkflowActionWebhook
+ fields = [
+ "id",
+ "url",
+ "use_params",
+ "params",
+ "body",
+ "headers",
+ "include_document",
+ ]
+
+
class WorkflowActionSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False, allow_null=True)
assign_correspondent = CorrespondentField(allow_null=True, required=False)
assign_tags = TagsField(many=True, allow_null=True, required=False)
assign_document_type = DocumentTypeField(allow_null=True, required=False)
assign_storage_path = StoragePathField(allow_null=True, required=False)
+ email = WorkflowActionEmailSerializer(allow_null=True, required=False)
+ webhook = WorkflowActionWebhookSerializer(allow_null=True, required=False)
class Meta:
model = WorkflowAction
"remove_view_groups",
"remove_change_users",
"remove_change_groups",
+ "email",
+ "webhook",
]
def validate(self, attrs):
{"assign_title": f'Invalid f-string detected: "{e.args[0]}"'},
)
+ if (
+ "type" in attrs
+ and attrs["type"] == WorkflowAction.WorkflowActionType.EMAIL
+ and "email" not in attrs
+ ):
+ raise serializers.ValidationError(
+ "Email data is required for email actions",
+ )
+
+ if (
+ "type" in attrs
+ and attrs["type"] == WorkflowAction.WorkflowActionType.WEBHOOK
+ and "webhook" not in attrs
+ ):
+ raise serializers.ValidationError(
+ "Webhook data is required for webhook actions",
+ )
+
return attrs
remove_change_users = action.pop("remove_change_users", None)
remove_change_groups = action.pop("remove_change_groups", None)
+ email_data = action.pop("email", None)
+ webhook_data = action.pop("webhook", None)
+
action_instance, _ = WorkflowAction.objects.update_or_create(
id=action.get("id"),
defaults=action,
)
+ if email_data is not None:
+ serializer = WorkflowActionEmailSerializer(data=email_data)
+ serializer.is_valid(raise_exception=True)
+ email, _ = WorkflowActionEmail.objects.update_or_create(
+ id=email_data.get("id"),
+ defaults=serializer.validated_data,
+ )
+ action_instance.email = email
+ action_instance.save()
+
+ if webhook_data is not None:
+ serializer = WorkflowActionWebhookSerializer(data=webhook_data)
+ serializer.is_valid(raise_exception=True)
+ webhook, _ = WorkflowActionWebhook.objects.update_or_create(
+ id=webhook_data.get("id"),
+ defaults=serializer.validated_data,
+ )
+ action_instance.webhook = webhook
+ action_instance.save()
+
if assign_tags is not None:
action_instance.assign_tags.set(assign_tags)
if assign_view_users is not None:
if action.workflows.all().count() == 0:
action.delete()
+ WorkflowActionEmail.objects.filter(action=None).delete()
+ WorkflowActionWebhook.objects.filter(action=None).delete()
+
def create(self, validated_data) -> Workflow:
if "triggers" in validated_data:
triggers = validated_data.pop("triggers")
import os
import shutil
+import httpx
+from celery import shared_task
from celery import states
from celery.signals import before_task_publish
from celery.signals import task_failure
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
+from django.core.mail import EmailMessage
from django.db import DatabaseError
from django.db import close_old_connections
from django.db import models
from documents.models import WorkflowTrigger
from documents.permissions import get_objects_for_user_owner_aware
from documents.permissions import set_permissions_for_object
-from documents.templating.title import parse_doc_title_w_placeholders
+from documents.templating.workflows import parse_w_workflow_placeholders
logger = logging.getLogger("paperless.handlers")
)
+@shared_task(
+ retry_backoff=True,
+ autoretry_for=(httpx.HTTPStatusError,),
+ max_retries=3,
+ throws=(httpx.HTTPError,),
+)
+def send_webhook(url, data, headers, files):
+ try:
+ httpx.post(
+ url,
+ data=data,
+ files=files,
+ headers=headers,
+ ).raise_for_status()
+ logger.info(
+ f"Webhook sent to {url}",
+ )
+ except Exception as e:
+ logger.error(
+ f"Failed attempt sending webhook to {url}: {e}",
+ )
+ raise e
+
+
def run_workflows(
trigger_type: WorkflowTrigger.WorkflowTriggerType,
document: Document | ConsumableDocument,
if action.assign_title:
if not use_overrides:
try:
- document.title = parse_doc_title_w_placeholders(
+ document.title = parse_w_workflow_placeholders(
action.assign_title,
document.correspondent.name if document.correspondent else "",
document.document_type.name if document.document_type else "",
):
overrides.custom_field_ids.remove(field.pk)
+ def email_action():
+ if not settings.EMAIL_ENABLED:
+ logger.error(
+ "Email backend has not been configured, cannot send email notifications",
+ extra={"group": logging_group},
+ )
+ return
+
+ title = (
+ document.title
+ if isinstance(document, Document)
+ else str(document.original_file)
+ )
+ doc_url = None
+ if isinstance(document, Document):
+ doc_url = f"{settings.PAPERLESS_URL}/documents/{document.pk}/"
+ correspondent = document.correspondent.name if document.correspondent else ""
+ document_type = document.document_type.name if document.document_type else ""
+ owner_username = document.owner.username if document.owner else ""
+ filename = document.original_filename or ""
+ added = timezone.localtime(document.added)
+ created = timezone.localtime(document.created)
+ subject = parse_w_workflow_placeholders(
+ action.email.subject,
+ correspondent,
+ document_type,
+ owner_username,
+ added,
+ filename,
+ created,
+ title,
+ doc_url,
+ )
+ body = parse_w_workflow_placeholders(
+ action.email.body,
+ correspondent,
+ document_type,
+ owner_username,
+ added,
+ filename,
+ created,
+ title,
+ doc_url,
+ )
+ try:
+ email = EmailMessage(
+ subject=subject,
+ body=body,
+ to=action.email.to.split(","),
+ )
+ if action.email.include_document:
+ email.attach_file(document.source_path)
+ n_messages = email.send()
+ logger.debug(
+ f"Sent {n_messages} notification email(s) to {action.email.to}",
+ extra={"group": logging_group},
+ )
+ except Exception as e:
+ logger.exception(
+ f"Error occurred sending notification email: {e}",
+ extra={"group": logging_group},
+ )
+
+ def webhook_action():
+ title = (
+ document.title
+ if isinstance(document, Document)
+ else str(document.original_file)
+ )
+ doc_url = None
+ if isinstance(document, Document):
+ doc_url = f"{settings.PAPERLESS_URL}/documents/{document.pk}/"
+ correspondent = document.correspondent.name if document.correspondent else ""
+ document_type = document.document_type.name if document.document_type else ""
+ owner_username = document.owner.username if document.owner else ""
+ filename = document.original_filename or ""
+ added = timezone.localtime(document.added)
+ created = timezone.localtime(document.created)
+
+ try:
+ data = {}
+ if action.webhook.use_params:
+ try:
+ for key, value in action.webhook.params.items():
+ data[key] = parse_w_workflow_placeholders(
+ value,
+ correspondent,
+ document_type,
+ owner_username,
+ added,
+ filename,
+ created,
+ title,
+ doc_url,
+ )
+ except Exception as e:
+ logger.error(
+ f"Error occurred parsing webhook params: {e}",
+ extra={"group": logging_group},
+ )
+ else:
+ data = parse_w_workflow_placeholders(
+ action.webhook.body,
+ correspondent,
+ document_type,
+ owner_username,
+ added,
+ filename,
+ created,
+ title,
+ doc_url,
+ )
+ headers = {}
+ if action.webhook.headers:
+ try:
+ headers = {
+ str(k): str(v) for k, v in action.webhook.headers.items()
+ }
+ except Exception as e:
+ logger.error(
+ f"Error occurred parsing webhook headers: {e}",
+ extra={"group": logging_group},
+ )
+ files = None
+ if action.webhook.include_document:
+ with open(document.source_path, "rb") as f:
+ files = {
+ "file": (document.original_filename, f, document.mime_type),
+ }
+ send_webhook.delay(
+ url=action.webhook.url,
+ data=data,
+ headers=headers,
+ files=files,
+ )
+ logger.debug(
+ f"Webhook to {action.webhook.url} queued",
+ extra={"group": logging_group},
+ )
+ except Exception as e:
+ logger.exception(
+ f"Error occurred sending webhook: {e}",
+ extra={"group": logging_group},
+ )
+
use_overrides = overrides is not None
messages = []
assignment_action()
elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
removal_action()
+ elif action.type == WorkflowAction.WorkflowActionType.EMAIL:
+ email_action()
+ elif action.type == WorkflowAction.WorkflowActionType.WEBHOOK:
+ webhook_action()
if not use_overrides:
# save first before setting tags
from pathlib import Path
-def parse_doc_title_w_placeholders(
- title: str,
+def parse_w_workflow_placeholders(
+ text: str,
correspondent_name: str,
doc_type_name: str,
owner_username: str,
local_added: datetime,
original_filename: str,
created: datetime | None = None,
+ doc_title: str | None = None,
+ doc_url: str | None = None,
) -> str:
"""
Available title placeholders for Workflows depend on what has already been assigned,
"created_time": created.strftime("%H:%M"),
},
)
- return title.format(**formatting).strip()
+ if doc_title is not None:
+ formatting.update({"doc_title": doc_title})
+ if doc_url is not None:
+ formatting.update({"doc_url": doc_url})
+ return text.format(**formatting).strip()
self.assertNotEqual(workflow.triggers.first().id, self.trigger.id)
self.assertEqual(WorkflowAction.objects.all().count(), 1)
self.assertNotEqual(workflow.actions.first().id, self.action.id)
+
+ def test_email_action_validation(self):
+ """
+ GIVEN:
+ - API request to create a workflow with an email action
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP response
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Workflow 2",
+ "order": 1,
+ "triggers": [
+ {
+ "type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "*",
+ },
+ ],
+ "actions": [
+ {
+ "type": WorkflowAction.WorkflowActionType.EMAIL,
+ },
+ ],
+ },
+ ),
+ content_type="application/json",
+ )
+ # Notification action requires to, subject and body
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Workflow 2",
+ "order": 1,
+ "triggers": [
+ {
+ "type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "*",
+ },
+ ],
+ "actions": [
+ {
+ "type": WorkflowAction.WorkflowActionType.EMAIL,
+ "email": {
+ "subject": "Subject",
+ "body": "Body",
+ },
+ },
+ ],
+ },
+ ),
+ content_type="application/json",
+ )
+ # Notification action requires destination emails or url
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Workflow 2",
+ "order": 1,
+ "triggers": [
+ {
+ "type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "*",
+ },
+ ],
+ "actions": [
+ {
+ "type": WorkflowAction.WorkflowActionType.EMAIL,
+ "email": {
+ "subject": "Subject",
+ "body": "Body",
+ "to": "me@example.com",
+ "include_document": False,
+ },
+ },
+ ],
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+
+ def test_webhook_action_validation(self):
+ """
+ GIVEN:
+ - API request to create a workflow with a notification action
+ WHEN:
+ - API is called
+ THEN:
+ - Correct HTTP response
+ """
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Workflow 2",
+ "order": 1,
+ "triggers": [
+ {
+ "type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "*",
+ },
+ ],
+ "actions": [
+ {
+ "type": WorkflowAction.WorkflowActionType.WEBHOOK,
+ },
+ ],
+ },
+ ),
+ content_type="application/json",
+ )
+ # Notification action requires url
+ self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+ response = self.client.post(
+ self.ENDPOINT,
+ json.dumps(
+ {
+ "name": "Workflow 2",
+ "order": 1,
+ "triggers": [
+ {
+ "type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
+ "sources": [DocumentSource.ApiUpload],
+ "filter_filename": "*",
+ },
+ ],
+ "actions": [
+ {
+ "type": WorkflowAction.WorkflowActionType.WEBHOOK,
+ "webhook": {
+ "url": "https://example.com",
+ "include_document": False,
+ },
+ },
+ ],
+ },
+ ),
+ content_type="application/json",
+ )
+ self.assertEqual(response.status_code, status.HTTP_201_CREATED)
class TestMigrateCustomFieldSelects(TestMigrations):
- migrate_from = "1058_workflowtrigger_schedule_date_custom_field_and_more"
- migrate_to = "1059_alter_customfieldinstance_value_select"
+ migrate_from = "1059_workflowactionemail_workflowactionwebhook_and_more"
+ migrate_to = "1060_alter_customfieldinstance_value_select"
def setUpBeforeMigration(self, apps):
CustomField = apps.get_model("documents.CustomField")
class TestMigrationCustomFieldSelectsReverse(TestMigrations):
- migrate_from = "1059_alter_customfieldinstance_value_select"
- migrate_to = "1058_workflowtrigger_schedule_date_custom_field_and_more"
+ migrate_from = "1060_alter_customfieldinstance_value_select"
+ migrate_to = "1059_workflowactionemail_workflowactionwebhook_and_more"
def setUpBeforeMigration(self, apps):
CustomField = apps.get_model("documents.CustomField")
import shutil
from datetime import timedelta
-from pathlib import Path
from typing import TYPE_CHECKING
from unittest import mock
from django.contrib.auth.models import Group
from django.contrib.auth.models import User
+from django.test import override_settings
from django.utils import timezone
from guardian.shortcuts import assign_perm
from guardian.shortcuts import get_groups_with_perms
from guardian.shortcuts import get_users_with_perms
+from httpx import HTTPStatusError
from rest_framework.test import APITestCase
+from documents.signals.handlers import run_workflows
+from documents.signals.handlers import send_webhook
+
if TYPE_CHECKING:
from django.db.models import QuerySet
+
from documents import tasks
from documents.data_models import ConsumableDocument
from documents.data_models import DocumentSource
from documents.models import Tag
from documents.models import Workflow
from documents.models import WorkflowAction
+from documents.models import WorkflowActionEmail
+from documents.models import WorkflowActionWebhook
from documents.models import WorkflowRun
from documents.models import WorkflowTrigger
from documents.signals import document_consumption_finished
from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import DummyProgressManager
from documents.tests.utils import FileSystemAssertsMixin
+from documents.tests.utils import SampleDirMixin
from paperless_mail.models import MailAccount
from paperless_mail.models import MailRule
-class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
- SAMPLE_DIR = Path(__file__).parent / "samples"
-
+class TestWorkflows(
+ DirectoriesMixin,
+ FileSystemAssertsMixin,
+ SampleDirMixin,
+ APITestCase,
+):
def setUp(self) -> None:
self.c = Correspondent.objects.create(name="Correspondent Name")
self.c2 = Correspondent.objects.create(name="Correspondent Name 2")
self.assertEqual(doc.owner, self.user2)
self.assertEqual(doc.tags.all().count(), 1)
self.assertIn(self.t2, doc.tags.all())
+
+ @override_settings(
+ PAPERLESS_EMAIL_HOST="localhost",
+ EMAIL_ENABLED=True,
+ PAPERLESS_URL="http://localhost:8000",
+ )
+ @mock.patch("httpx.post")
+ @mock.patch("django.core.mail.message.EmailMessage.send")
+ def test_workflow_email_action(self, mock_email_send, mock_post):
+ """
+ GIVEN:
+ - Document updated workflow with email action
+ WHEN:
+ - Document that matches is updated
+ THEN:
+ - email is sent
+ """
+ mock_post.return_value = mock.Mock(
+ status_code=200,
+ json=mock.Mock(return_value={"status": "ok"}),
+ )
+ mock_email_send.return_value = 1
+
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ email_action = WorkflowActionEmail.objects.create(
+ subject="Test Notification: {doc_title}",
+ body="Test message: {doc_url}",
+ to="user@example.com",
+ include_document=False,
+ )
+ self.assertEqual(str(email_action), f"Workflow Email Action {email_action.id}")
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.EMAIL,
+ email=email_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="sample.pdf",
+ )
+
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ mock_email_send.assert_called_once()
+
+ @override_settings(
+ PAPERLESS_EMAIL_HOST="localhost",
+ EMAIL_ENABLED=True,
+ PAPERLESS_URL="http://localhost:8000",
+ )
+ @mock.patch("httpx.post")
+ @mock.patch("django.core.mail.message.EmailMessage.send")
+ def test_workflow_email_include_file(self, mock_email_send, mock_post):
+ """
+ GIVEN:
+ - Document updated workflow with email action
+ - Include document is set to True
+ WHEN:
+ - Document that matches is updated
+ THEN:
+ - Notification includes document file
+ """
+
+ # move the file
+ test_file = shutil.copy(
+ self.SAMPLE_DIR / "simple.pdf",
+ self.dirs.scratch_dir / "simple.pdf",
+ )
+
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ email_action = WorkflowActionEmail.objects.create(
+ subject="Test Notification: {doc_title}",
+ body="Test message: {doc_url}",
+ to="me@example.com",
+ include_document=True,
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.EMAIL,
+ email=email_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ filename=test_file,
+ )
+
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ mock_email_send.assert_called_once()
+
+ @override_settings(
+ EMAIL_ENABLED=False,
+ )
+ def test_workflow_email_action_no_email_setup(self):
+ """
+ GIVEN:
+ - Document updated workflow with email action
+ - Email is not enabled
+ WHEN:
+ - Document that matches is updated
+ THEN:
+ - Error is logged
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ email_action = WorkflowActionEmail.objects.create(
+ subject="Test Notification: {doc_title}",
+ body="Test message: {doc_url}",
+ to="me@example.com",
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.EMAIL,
+ email=email_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="sample.pdf",
+ )
+
+ with self.assertLogs("paperless.handlers", level="ERROR") as cm:
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ expected_str = "Email backend has not been configured"
+ self.assertIn(expected_str, cm.output[0])
+
+ @override_settings(
+ EMAIL_ENABLED=True,
+ PAPERLESS_URL="http://localhost:8000",
+ )
+ @mock.patch("django.core.mail.message.EmailMessage.send")
+ def test_workflow_email_action_fail(self, mock_email_send):
+ """
+ GIVEN:
+ - Document updated workflow with email action
+ WHEN:
+ - Document that matches is updated
+ - An error occurs during email send
+ THEN:
+ - Error is logged
+ """
+ mock_email_send.side_effect = Exception("Error occurred sending email")
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ email_action = WorkflowActionEmail.objects.create(
+ subject="Test Notification: {doc_title}",
+ body="Test message: {doc_url}",
+ to="me@example.com",
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.EMAIL,
+ email=email_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="sample.pdf",
+ )
+
+ with self.assertLogs("paperless.handlers", level="ERROR") as cm:
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ expected_str = "Error occurred sending email"
+ self.assertIn(expected_str, cm.output[0])
+
+ @override_settings(
+ PAPERLESS_EMAIL_HOST="localhost",
+ EMAIL_ENABLED=True,
+ PAPERLESS_URL="http://localhost:8000",
+ )
+ @mock.patch("documents.signals.handlers.send_webhook.delay")
+ def test_workflow_webhook_action_body(self, mock_post):
+ """
+ GIVEN:
+ - Document updated workflow with webhook action which uses body
+ WHEN:
+ - Document that matches is updated
+ THEN:
+ - Webhook is sent with body
+ """
+ mock_post.return_value = mock.Mock(
+ status_code=200,
+ json=mock.Mock(return_value={"status": "ok"}),
+ )
+
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ webhook_action = WorkflowActionWebhook.objects.create(
+ use_params=False,
+ body="Test message: {doc_url}",
+ url="http://paperless-ngx.com",
+ include_document=False,
+ )
+ self.assertEqual(
+ str(webhook_action),
+ f"Workflow Webhook Action {webhook_action.id}",
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.WEBHOOK,
+ webhook=webhook_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="sample.pdf",
+ )
+
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ mock_post.assert_called_once_with(
+ url="http://paperless-ngx.com",
+ data=f"Test message: http://localhost:8000/documents/{doc.id}/",
+ headers={},
+ files=None,
+ )
+
+ @override_settings(
+ PAPERLESS_EMAIL_HOST="localhost",
+ EMAIL_ENABLED=True,
+ PAPERLESS_URL="http://localhost:8000",
+ )
+ @mock.patch("documents.signals.handlers.send_webhook.delay")
+ def test_workflow_webhook_action_w_files(self, mock_post):
+ """
+ GIVEN:
+ - Document updated workflow with webhook action which includes document
+ WHEN:
+ - Document that matches is updated
+ THEN:
+ - Webhook is sent with file
+ """
+ mock_post.return_value = mock.Mock(
+ status_code=200,
+ json=mock.Mock(return_value={"status": "ok"}),
+ )
+
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ webhook_action = WorkflowActionWebhook.objects.create(
+ use_params=False,
+ body="Test message: {doc_url}",
+ url="http://paperless-ngx.com",
+ include_document=True,
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.WEBHOOK,
+ webhook=webhook_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ test_file = shutil.copy(
+ self.SAMPLE_DIR / "simple.pdf",
+ self.dirs.scratch_dir / "simple.pdf",
+ )
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="simple.pdf",
+ filename=test_file,
+ mime_type="application/pdf",
+ )
+
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ mock_post.assert_called_once_with(
+ url="http://paperless-ngx.com",
+ data=f"Test message: http://localhost:8000/documents/{doc.id}/",
+ headers={},
+ files={"file": ("simple.pdf", mock.ANY, "application/pdf")},
+ )
+
+ @override_settings(
+ PAPERLESS_EMAIL_HOST="localhost",
+ EMAIL_ENABLED=True,
+ PAPERLESS_URL="http://localhost:8000",
+ )
+ def test_workflow_webhook_action_fail(self):
+ """
+ GIVEN:
+ - Document updated workflow with webhook action
+ WHEN:
+ - Document that matches is updated
+ - An error occurs during webhook
+ THEN:
+ - Error is logged
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ webhook_action = WorkflowActionWebhook.objects.create(
+ use_params=True,
+ params={
+ "title": "Test webhook: {doc_title}",
+ "body": "Test message: {doc_url}",
+ },
+ url="http://paperless-ngx.com",
+ include_document=True,
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.WEBHOOK,
+ webhook=webhook_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="sample.pdf",
+ )
+
+ # fails because no file
+ with self.assertLogs("paperless.handlers", level="ERROR") as cm:
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ expected_str = "Error occurred sending webhook"
+ self.assertIn(expected_str, cm.output[0])
+
+ def test_workflow_webhook_action_url_invalid_params_headers(self):
+ """
+ GIVEN:
+ - Document updated workflow with webhook action
+ - Invalid params and headers JSON
+ WHEN:
+ - Document that matches is updated
+ THEN:
+ - Error is logged
+ """
+ trigger = WorkflowTrigger.objects.create(
+ type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
+ )
+ webhook_action = WorkflowActionWebhook.objects.create(
+ url="http://paperless-ngx.com",
+ use_params=True,
+ params="invalid",
+ headers="invalid",
+ )
+ action = WorkflowAction.objects.create(
+ type=WorkflowAction.WorkflowActionType.WEBHOOK,
+ webhook=webhook_action,
+ )
+ w = Workflow.objects.create(
+ name="Workflow 1",
+ order=0,
+ )
+ w.triggers.add(trigger)
+ w.actions.add(action)
+ w.save()
+
+ doc = Document.objects.create(
+ title="sample test",
+ correspondent=self.c,
+ original_filename="sample.pdf",
+ )
+
+ with self.assertLogs("paperless.handlers", level="ERROR") as cm:
+ run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
+
+ expected_str = "Error occurred parsing webhook params"
+ self.assertIn(expected_str, cm.output[0])
+ expected_str = "Error occurred parsing webhook headers"
+ self.assertIn(expected_str, cm.output[1])
+
+ @mock.patch("httpx.post")
+ def test_workflow_webhook_send_webhook_task(self, mock_post):
+ mock_post.return_value = mock.Mock(
+ status_code=200,
+ json=mock.Mock(return_value={"status": "ok"}),
+ raise_for_status=mock.Mock(),
+ )
+
+ with self.assertLogs("paperless.handlers") as cm:
+ send_webhook(
+ url="http://paperless-ngx.com",
+ data="Test message",
+ headers={},
+ files=None,
+ )
+
+ mock_post.assert_called_once_with(
+ "http://paperless-ngx.com",
+ data="Test message",
+ headers={},
+ files=None,
+ )
+
+ expected_str = "Webhook sent to http://paperless-ngx.com"
+ self.assertIn(expected_str, cm.output[0])
+
+ @mock.patch("httpx.post")
+ def test_workflow_webhook_send_webhook_retry(self, mock_http):
+ mock_http.return_value.raise_for_status = mock.Mock(
+ side_effect=HTTPStatusError(
+ "Error",
+ request=mock.Mock(),
+ response=mock.Mock(),
+ ),
+ )
+
+ with self.assertLogs("paperless.handlers") as cm:
+ with self.assertRaises(HTTPStatusError):
+ send_webhook(
+ url="http://paperless-ngx.com",
+ data="Test message",
+ headers={},
+ files=None,
+ )
+
+ self.assertEqual(mock_http.call_count, 1)
+
+ expected_str = (
+ "Failed attempt sending webhook to http://paperless-ngx.com"
+ )
+ self.assertIn(expected_str, cm.output[0])
msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-10-19 23:22-0700\n"
+"POT-Creation-Date: 2024-11-25 21:28-0800\n"
"PO-Revision-Date: 2022-02-17 04:17\n"
"Last-Translator: \n"
"Language-Team: English\n"
msgid "Maximum number of query conditions exceeded."
msgstr ""
-#: documents/filters.py:455
+#: documents/filters.py:463
msgid "{name!r} is not a valid custom field."
msgstr ""
-#: documents/filters.py:492
+#: documents/filters.py:500
msgid "{data_type} does not support query expr {expr!r}."
msgstr ""
-#: documents/filters.py:600
+#: documents/filters.py:608
msgid "Maximum nesting depth exceeded."
msgstr ""
-#: documents/models.py:41 documents/models.py:802
+#: documents/models.py:41 documents/models.py:801
msgid "owner"
msgstr ""
-#: documents/models.py:58 documents/models.py:1009
+#: documents/models.py:58 documents/models.py:1008
msgid "None"
msgstr ""
-#: documents/models.py:59 documents/models.py:1010
+#: documents/models.py:59 documents/models.py:1009
msgid "Any word"
msgstr ""
-#: documents/models.py:60 documents/models.py:1011
+#: documents/models.py:60 documents/models.py:1010
msgid "All words"
msgstr ""
-#: documents/models.py:61 documents/models.py:1012
+#: documents/models.py:61 documents/models.py:1011
msgid "Exact match"
msgstr ""
-#: documents/models.py:62 documents/models.py:1013
+#: documents/models.py:62 documents/models.py:1012
msgid "Regular expression"
msgstr ""
-#: documents/models.py:63 documents/models.py:1014
+#: documents/models.py:63 documents/models.py:1013
msgid "Fuzzy word"
msgstr ""
msgid "Automatic"
msgstr ""
-#: documents/models.py:67 documents/models.py:434 documents/models.py:1330
-#: paperless_mail/models.py:23 paperless_mail/models.py:137
+#: documents/models.py:67 documents/models.py:433 documents/models.py:1484
+#: paperless_mail/models.py:23 paperless_mail/models.py:136
msgid "name"
msgstr ""
-#: documents/models.py:69 documents/models.py:1070
+#: documents/models.py:69 documents/models.py:1076
msgid "match"
msgstr ""
-#: documents/models.py:72 documents/models.py:1073
+#: documents/models.py:72 documents/models.py:1079
msgid "matching algorithm"
msgstr ""
-#: documents/models.py:77 documents/models.py:1078
+#: documents/models.py:77 documents/models.py:1084
msgid "is insensitive"
msgstr ""
-#: documents/models.py:100 documents/models.py:152
+#: documents/models.py:100 documents/models.py:151
msgid "correspondent"
msgstr ""
msgid "tag"
msgstr ""
-#: documents/models.py:118 documents/models.py:190
+#: documents/models.py:118 documents/models.py:189
msgid "tags"
msgstr ""
-#: documents/models.py:123 documents/models.py:172
+#: documents/models.py:123 documents/models.py:171
msgid "document type"
msgstr ""
msgid "path"
msgstr ""
-#: documents/models.py:134 documents/models.py:161
+#: documents/models.py:133 documents/models.py:160
msgid "storage path"
msgstr ""
-#: documents/models.py:135
+#: documents/models.py:134
msgid "storage paths"
msgstr ""
-#: documents/models.py:142
+#: documents/models.py:141
msgid "Unencrypted"
msgstr ""
-#: documents/models.py:143
+#: documents/models.py:142
msgid "Encrypted with GNU Privacy Guard"
msgstr ""
-#: documents/models.py:164
+#: documents/models.py:163
msgid "title"
msgstr ""
-#: documents/models.py:176 documents/models.py:716
+#: documents/models.py:175 documents/models.py:715
msgid "content"
msgstr ""
-#: documents/models.py:179
+#: documents/models.py:178
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
msgstr ""
-#: documents/models.py:184
+#: documents/models.py:183
msgid "mime type"
msgstr ""
-#: documents/models.py:194
+#: documents/models.py:193
msgid "checksum"
msgstr ""
-#: documents/models.py:198
+#: documents/models.py:197
msgid "The checksum of the original document."
msgstr ""
-#: documents/models.py:202
+#: documents/models.py:201
msgid "archive checksum"
msgstr ""
-#: documents/models.py:207
+#: documents/models.py:206
msgid "The checksum of the archived document."
msgstr ""
-#: documents/models.py:211
+#: documents/models.py:210
msgid "page count"
msgstr ""
-#: documents/models.py:218
+#: documents/models.py:217
msgid "The number of pages of the document."
msgstr ""
-#: documents/models.py:222 documents/models.py:402 documents/models.py:722
-#: documents/models.py:760 documents/models.py:831 documents/models.py:889
+#: documents/models.py:221 documents/models.py:401 documents/models.py:721
+#: documents/models.py:759 documents/models.py:830 documents/models.py:888
msgid "created"
msgstr ""
-#: documents/models.py:225
+#: documents/models.py:224
msgid "modified"
msgstr ""
-#: documents/models.py:232
+#: documents/models.py:231
msgid "storage type"
msgstr ""
-#: documents/models.py:240
+#: documents/models.py:239
msgid "added"
msgstr ""
-#: documents/models.py:247
+#: documents/models.py:246
msgid "filename"
msgstr ""
-#: documents/models.py:253
+#: documents/models.py:252
msgid "Current filename in storage"
msgstr ""
-#: documents/models.py:257
+#: documents/models.py:256
msgid "archive filename"
msgstr ""
-#: documents/models.py:263
+#: documents/models.py:262
msgid "Current archive filename in storage"
msgstr ""
-#: documents/models.py:267
+#: documents/models.py:266
msgid "original filename"
msgstr ""
-#: documents/models.py:273
+#: documents/models.py:272
msgid "The original name of the file when it was uploaded"
msgstr ""
-#: documents/models.py:280
+#: documents/models.py:279
msgid "archive serial number"
msgstr ""
-#: documents/models.py:290
+#: documents/models.py:289
msgid "The position of this document in your physical document archive."
msgstr ""
-#: documents/models.py:296 documents/models.py:733 documents/models.py:787
+#: documents/models.py:295 documents/models.py:732 documents/models.py:786
+#: documents/models.py:1527
msgid "document"
msgstr ""
-#: documents/models.py:297
+#: documents/models.py:296
msgid "documents"
msgstr ""
-#: documents/models.py:385
+#: documents/models.py:384
msgid "debug"
msgstr ""
-#: documents/models.py:386
+#: documents/models.py:385
msgid "information"
msgstr ""
-#: documents/models.py:387
+#: documents/models.py:386
msgid "warning"
msgstr ""
-#: documents/models.py:388 paperless_mail/models.py:351
+#: documents/models.py:387 paperless_mail/models.py:350
msgid "error"
msgstr ""
-#: documents/models.py:389
+#: documents/models.py:388
msgid "critical"
msgstr ""
-#: documents/models.py:392
+#: documents/models.py:391
msgid "group"
msgstr ""
-#: documents/models.py:394
+#: documents/models.py:393
msgid "message"
msgstr ""
-#: documents/models.py:397
+#: documents/models.py:396
msgid "level"
msgstr ""
-#: documents/models.py:406
+#: documents/models.py:405
msgid "log"
msgstr ""
-#: documents/models.py:407
+#: documents/models.py:406
msgid "logs"
msgstr ""
-#: documents/models.py:415
+#: documents/models.py:414
msgid "Table"
msgstr ""
-#: documents/models.py:416
+#: documents/models.py:415
msgid "Small Cards"
msgstr ""
-#: documents/models.py:417
+#: documents/models.py:416
msgid "Large Cards"
msgstr ""
-#: documents/models.py:420
+#: documents/models.py:419
msgid "Title"
msgstr ""
-#: documents/models.py:421
+#: documents/models.py:420 documents/models.py:1028
msgid "Created"
msgstr ""
-#: documents/models.py:422
+#: documents/models.py:421 documents/models.py:1027
msgid "Added"
msgstr ""
-#: documents/models.py:423
+#: documents/models.py:422
msgid "Tags"
msgstr ""
-#: documents/models.py:424
+#: documents/models.py:423
msgid "Correspondent"
msgstr ""
-#: documents/models.py:425
+#: documents/models.py:424
msgid "Document Type"
msgstr ""
-#: documents/models.py:426
+#: documents/models.py:425
msgid "Storage Path"
msgstr ""
-#: documents/models.py:427
+#: documents/models.py:426
msgid "Note"
msgstr ""
-#: documents/models.py:428
+#: documents/models.py:427
msgid "Owner"
msgstr ""
-#: documents/models.py:429
+#: documents/models.py:428
msgid "Shared"
msgstr ""
-#: documents/models.py:430
+#: documents/models.py:429
msgid "ASN"
msgstr ""
-#: documents/models.py:431
+#: documents/models.py:430
msgid "Pages"
msgstr ""
-#: documents/models.py:437
+#: documents/models.py:436
msgid "show on dashboard"
msgstr ""
-#: documents/models.py:440
+#: documents/models.py:439
msgid "show in sidebar"
msgstr ""
-#: documents/models.py:444
+#: documents/models.py:443
msgid "sort field"
msgstr ""
-#: documents/models.py:449
+#: documents/models.py:448
msgid "sort reverse"
msgstr ""
-#: documents/models.py:452
+#: documents/models.py:451
msgid "View page size"
msgstr ""
-#: documents/models.py:460
+#: documents/models.py:459
msgid "View display mode"
msgstr ""
-#: documents/models.py:467
+#: documents/models.py:466
msgid "Document display fields"
msgstr ""
-#: documents/models.py:474 documents/models.py:532
+#: documents/models.py:473 documents/models.py:531
msgid "saved view"
msgstr ""
-#: documents/models.py:475
+#: documents/models.py:474
msgid "saved views"
msgstr ""
-#: documents/models.py:483
+#: documents/models.py:482
msgid "title contains"
msgstr ""
-#: documents/models.py:484
+#: documents/models.py:483
msgid "content contains"
msgstr ""
-#: documents/models.py:485
+#: documents/models.py:484
msgid "ASN is"
msgstr ""
-#: documents/models.py:486
+#: documents/models.py:485
msgid "correspondent is"
msgstr ""
-#: documents/models.py:487
+#: documents/models.py:486
msgid "document type is"
msgstr ""
-#: documents/models.py:488
+#: documents/models.py:487
msgid "is in inbox"
msgstr ""
-#: documents/models.py:489
+#: documents/models.py:488
msgid "has tag"
msgstr ""
-#: documents/models.py:490
+#: documents/models.py:489
msgid "has any tag"
msgstr ""
-#: documents/models.py:491
+#: documents/models.py:490
msgid "created before"
msgstr ""
-#: documents/models.py:492
+#: documents/models.py:491
msgid "created after"
msgstr ""
-#: documents/models.py:493
+#: documents/models.py:492
msgid "created year is"
msgstr ""
-#: documents/models.py:494
+#: documents/models.py:493
msgid "created month is"
msgstr ""
-#: documents/models.py:495
+#: documents/models.py:494
msgid "created day is"
msgstr ""
-#: documents/models.py:496
+#: documents/models.py:495
msgid "added before"
msgstr ""
-#: documents/models.py:497
+#: documents/models.py:496
msgid "added after"
msgstr ""
-#: documents/models.py:498
+#: documents/models.py:497
msgid "modified before"
msgstr ""
-#: documents/models.py:499
+#: documents/models.py:498
msgid "modified after"
msgstr ""
-#: documents/models.py:500
+#: documents/models.py:499
msgid "does not have tag"
msgstr ""
-#: documents/models.py:501
+#: documents/models.py:500
msgid "does not have ASN"
msgstr ""
-#: documents/models.py:502
+#: documents/models.py:501
msgid "title or content contains"
msgstr ""
-#: documents/models.py:503
+#: documents/models.py:502
msgid "fulltext query"
msgstr ""
-#: documents/models.py:504
+#: documents/models.py:503
msgid "more like this"
msgstr ""
-#: documents/models.py:505
+#: documents/models.py:504
msgid "has tags in"
msgstr ""
-#: documents/models.py:506
+#: documents/models.py:505
msgid "ASN greater than"
msgstr ""
-#: documents/models.py:507
+#: documents/models.py:506
msgid "ASN less than"
msgstr ""
-#: documents/models.py:508
+#: documents/models.py:507
msgid "storage path is"
msgstr ""
-#: documents/models.py:509
+#: documents/models.py:508
msgid "has correspondent in"
msgstr ""
-#: documents/models.py:510
+#: documents/models.py:509
msgid "does not have correspondent in"
msgstr ""
-#: documents/models.py:511
+#: documents/models.py:510
msgid "has document type in"
msgstr ""
-#: documents/models.py:512
+#: documents/models.py:511
msgid "does not have document type in"
msgstr ""
-#: documents/models.py:513
+#: documents/models.py:512
msgid "has storage path in"
msgstr ""
-#: documents/models.py:514
+#: documents/models.py:513
msgid "does not have storage path in"
msgstr ""
-#: documents/models.py:515
+#: documents/models.py:514
msgid "owner is"
msgstr ""
-#: documents/models.py:516
+#: documents/models.py:515
msgid "has owner in"
msgstr ""
-#: documents/models.py:517
+#: documents/models.py:516
msgid "does not have owner"
msgstr ""
-#: documents/models.py:518
+#: documents/models.py:517
msgid "does not have owner in"
msgstr ""
-#: documents/models.py:519
+#: documents/models.py:518
msgid "has custom field value"
msgstr ""
-#: documents/models.py:520
+#: documents/models.py:519
msgid "is shared by me"
msgstr ""
-#: documents/models.py:521
+#: documents/models.py:520
msgid "has custom fields"
msgstr ""
-#: documents/models.py:522
+#: documents/models.py:521
msgid "has custom field in"
msgstr ""
-#: documents/models.py:523
+#: documents/models.py:522
msgid "does not have custom field in"
msgstr ""
-#: documents/models.py:524
+#: documents/models.py:523
msgid "does not have custom field"
msgstr ""
-#: documents/models.py:525
+#: documents/models.py:524
msgid "custom fields query"
msgstr ""
-#: documents/models.py:535
+#: documents/models.py:534
msgid "rule type"
msgstr ""
-#: documents/models.py:537
+#: documents/models.py:536
msgid "value"
msgstr ""
-#: documents/models.py:540
+#: documents/models.py:539
msgid "filter rule"
msgstr ""
-#: documents/models.py:541
+#: documents/models.py:540
msgid "filter rules"
msgstr ""
-#: documents/models.py:652
+#: documents/models.py:651
msgid "Task ID"
msgstr ""
-#: documents/models.py:653
+#: documents/models.py:652
msgid "Celery ID for the Task that was run"
msgstr ""
-#: documents/models.py:658
+#: documents/models.py:657
msgid "Acknowledged"
msgstr ""
-#: documents/models.py:659
+#: documents/models.py:658
msgid "If the task is acknowledged via the frontend or API"
msgstr ""
-#: documents/models.py:665
+#: documents/models.py:664
msgid "Task Filename"
msgstr ""
-#: documents/models.py:666
+#: documents/models.py:665
msgid "Name of the file which the Task was run for"
msgstr ""
-#: documents/models.py:672
+#: documents/models.py:671
msgid "Task Name"
msgstr ""
-#: documents/models.py:673
+#: documents/models.py:672
msgid "Name of the Task which was run"
msgstr ""
-#: documents/models.py:680
+#: documents/models.py:679
msgid "Task State"
msgstr ""
-#: documents/models.py:681
+#: documents/models.py:680
msgid "Current state of the task being run"
msgstr ""
-#: documents/models.py:686
+#: documents/models.py:685
msgid "Created DateTime"
msgstr ""
-#: documents/models.py:687
+#: documents/models.py:686
msgid "Datetime field when the task result was created in UTC"
msgstr ""
-#: documents/models.py:692
+#: documents/models.py:691
msgid "Started DateTime"
msgstr ""
-#: documents/models.py:693
+#: documents/models.py:692
msgid "Datetime field when the task was started in UTC"
msgstr ""
-#: documents/models.py:698
+#: documents/models.py:697
msgid "Completed DateTime"
msgstr ""
-#: documents/models.py:699
+#: documents/models.py:698
msgid "Datetime field when the task was completed in UTC"
msgstr ""
-#: documents/models.py:704
+#: documents/models.py:703
msgid "Result Data"
msgstr ""
-#: documents/models.py:706
+#: documents/models.py:705
msgid "The data returned by the task"
msgstr ""
-#: documents/models.py:718
+#: documents/models.py:717
msgid "Note for the document"
msgstr ""
-#: documents/models.py:742
+#: documents/models.py:741
msgid "user"
msgstr ""
-#: documents/models.py:747
+#: documents/models.py:746
msgid "note"
msgstr ""
-#: documents/models.py:748
+#: documents/models.py:747
msgid "notes"
msgstr ""
-#: documents/models.py:756
+#: documents/models.py:755
msgid "Archive"
msgstr ""
-#: documents/models.py:757
+#: documents/models.py:756
msgid "Original"
msgstr ""
-#: documents/models.py:768 paperless_mail/models.py:76
+#: documents/models.py:767 paperless_mail/models.py:75
msgid "expiration"
msgstr ""
-#: documents/models.py:775
+#: documents/models.py:774
msgid "slug"
msgstr ""
-#: documents/models.py:807
+#: documents/models.py:806
msgid "share link"
msgstr ""
-#: documents/models.py:808
+#: documents/models.py:807
msgid "share links"
msgstr ""
-#: documents/models.py:820
+#: documents/models.py:819
msgid "String"
msgstr ""
-#: documents/models.py:821
+#: documents/models.py:820
msgid "URL"
msgstr ""
-#: documents/models.py:822
+#: documents/models.py:821
msgid "Date"
msgstr ""
-#: documents/models.py:823
+#: documents/models.py:822
msgid "Boolean"
msgstr ""
-#: documents/models.py:824
+#: documents/models.py:823
msgid "Integer"
msgstr ""
-#: documents/models.py:825
+#: documents/models.py:824
msgid "Float"
msgstr ""
-#: documents/models.py:826
+#: documents/models.py:825
msgid "Monetary"
msgstr ""
-#: documents/models.py:827
+#: documents/models.py:826
msgid "Document Link"
msgstr ""
-#: documents/models.py:828
+#: documents/models.py:827
msgid "Select"
msgstr ""
-#: documents/models.py:840
+#: documents/models.py:839
msgid "data type"
msgstr ""
-#: documents/models.py:847
+#: documents/models.py:846
msgid "extra data"
msgstr ""
-#: documents/models.py:851
+#: documents/models.py:850
msgid "Extra data for the custom field, such as select options"
msgstr ""
-#: documents/models.py:857
+#: documents/models.py:856
msgid "custom field"
msgstr ""
-#: documents/models.py:858
+#: documents/models.py:857
msgid "custom fields"
msgstr ""
-#: documents/models.py:955
+#: documents/models.py:954
msgid "custom field instance"
msgstr ""
-#: documents/models.py:956
+#: documents/models.py:955
msgid "custom field instances"
msgstr ""
-#: documents/models.py:1017
+#: documents/models.py:1016
msgid "Consumption Started"
msgstr ""
-#: documents/models.py:1018
+#: documents/models.py:1017
msgid "Document Added"
msgstr ""
-#: documents/models.py:1019
+#: documents/models.py:1018
msgid "Document Updated"
msgstr ""
+#: documents/models.py:1019
+msgid "Scheduled"
+msgstr ""
+
#: documents/models.py:1022
msgid "Consume Folder"
msgstr ""
msgid "Mail Fetch"
msgstr ""
-#: documents/models.py:1027
+#: documents/models.py:1029
+msgid "Modified"
+msgstr ""
+
+#: documents/models.py:1030
+msgid "Custom Field"
+msgstr ""
+
+#: documents/models.py:1033
msgid "Workflow Trigger Type"
msgstr ""
-#: documents/models.py:1039
+#: documents/models.py:1045
msgid "filter path"
msgstr ""
-#: documents/models.py:1044
+#: documents/models.py:1050
msgid ""
"Only consume documents with a path that matches this if specified. Wildcards "
"specified as * are allowed. Case insensitive."
msgstr ""
-#: documents/models.py:1051
+#: documents/models.py:1057
msgid "filter filename"
msgstr ""
-#: documents/models.py:1056 paperless_mail/models.py:194
+#: documents/models.py:1062 paperless_mail/models.py:193
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:1067
+#: documents/models.py:1073
msgid "filter documents from this mail rule"
msgstr ""
-#: documents/models.py:1083
+#: documents/models.py:1089
msgid "has these tag(s)"
msgstr ""
-#: documents/models.py:1091
+#: documents/models.py:1097
msgid "has this document type"
msgstr ""
-#: documents/models.py:1099
+#: documents/models.py:1105
msgid "has this correspondent"
msgstr ""
-#: documents/models.py:1103
+#: documents/models.py:1109
+msgid "schedule offset days"
+msgstr ""
+
+#: documents/models.py:1112
+msgid "The number of days to offset the schedule trigger by."
+msgstr ""
+
+#: documents/models.py:1117
+msgid "schedule is recurring"
+msgstr ""
+
+#: documents/models.py:1120
+msgid "If the schedule should be recurring."
+msgstr ""
+
+#: documents/models.py:1125
+msgid "schedule recurring delay in days"
+msgstr ""
+
+#: documents/models.py:1129
+msgid "The number of days between recurring schedule triggers."
+msgstr ""
+
+#: documents/models.py:1134
+msgid "schedule date field"
+msgstr ""
+
+#: documents/models.py:1139
+msgid "The field to check for a schedule trigger."
+msgstr ""
+
+#: documents/models.py:1148
+msgid "schedule date custom field"
+msgstr ""
+
+#: documents/models.py:1152
msgid "workflow trigger"
msgstr ""
-#: documents/models.py:1104
+#: documents/models.py:1153
msgid "workflow triggers"
msgstr ""
-#: documents/models.py:1114
+#: documents/models.py:1161
+msgid "email subject"
+msgstr ""
+
+#: documents/models.py:1165
+msgid ""
+"The subject of the email, can include some placeholders, see documentation."
+msgstr ""
+
+#: documents/models.py:1171
+msgid "email body"
+msgstr ""
+
+#: documents/models.py:1174
+msgid ""
+"The body (message) of the email, can include some placeholders, see "
+"documentation."
+msgstr ""
+
+#: documents/models.py:1180
+msgid "emails to"
+msgstr ""
+
+#: documents/models.py:1183
+msgid "The destination email addresses, comma separated."
+msgstr ""
+
+#: documents/models.py:1189
+msgid "include document in email"
+msgstr ""
+
+#: documents/models.py:1198
+msgid "webhook url"
+msgstr ""
+
+#: documents/models.py:1200
+msgid "The destination URL for the notification."
+msgstr ""
+
+#: documents/models.py:1205
+msgid "use parameters"
+msgstr ""
+
+#: documents/models.py:1209
+msgid "webhook parameters"
+msgstr ""
+
+#: documents/models.py:1212
+msgid "The parameters to send with the webhook URL if body not used."
+msgstr ""
+
+#: documents/models.py:1216
+msgid "webhook body"
+msgstr ""
+
+#: documents/models.py:1219
+msgid "The body to send with the webhook URL if parameters not used."
+msgstr ""
+
+#: documents/models.py:1223
+msgid "webhook headers"
+msgstr ""
+
+#: documents/models.py:1226
+msgid "The headers to send with the webhook URL."
+msgstr ""
+
+#: documents/models.py:1231
+msgid "include document in webhook"
+msgstr ""
+
+#: documents/models.py:1242
msgid "Assignment"
msgstr ""
-#: documents/models.py:1118
+#: documents/models.py:1246
msgid "Removal"
msgstr ""
-#: documents/models.py:1122
+#: documents/models.py:1250 documents/templates/account/password_reset.html:15
+msgid "Email"
+msgstr ""
+
+#: documents/models.py:1254
+msgid "Webhook"
+msgstr ""
+
+#: documents/models.py:1258
msgid "Workflow Action Type"
msgstr ""
-#: documents/models.py:1128
+#: documents/models.py:1264
msgid "assign title"
msgstr ""
-#: documents/models.py:1133
+#: documents/models.py:1269
msgid ""
"Assign a document title, can include some placeholders, see documentation."
msgstr ""
-#: documents/models.py:1142 paperless_mail/models.py:262
+#: documents/models.py:1278 paperless_mail/models.py:261
msgid "assign this tag"
msgstr ""
-#: documents/models.py:1151 paperless_mail/models.py:270
+#: documents/models.py:1287 paperless_mail/models.py:269
msgid "assign this document type"
msgstr ""
-#: documents/models.py:1160 paperless_mail/models.py:284
+#: documents/models.py:1296 paperless_mail/models.py:283
msgid "assign this correspondent"
msgstr ""
-#: documents/models.py:1169
+#: documents/models.py:1305
msgid "assign this storage path"
msgstr ""
-#: documents/models.py:1178
+#: documents/models.py:1314
msgid "assign this owner"
msgstr ""
-#: documents/models.py:1185
+#: documents/models.py:1321
msgid "grant view permissions to these users"
msgstr ""
-#: documents/models.py:1192
+#: documents/models.py:1328
msgid "grant view permissions to these groups"
msgstr ""
-#: documents/models.py:1199
+#: documents/models.py:1335
msgid "grant change permissions to these users"
msgstr ""
-#: documents/models.py:1206
+#: documents/models.py:1342
msgid "grant change permissions to these groups"
msgstr ""
-#: documents/models.py:1213
+#: documents/models.py:1349
msgid "assign these custom fields"
msgstr ""
-#: documents/models.py:1220
+#: documents/models.py:1356
msgid "remove these tag(s)"
msgstr ""
-#: documents/models.py:1225
+#: documents/models.py:1361
msgid "remove all tags"
msgstr ""
-#: documents/models.py:1232
+#: documents/models.py:1368
msgid "remove these document type(s)"
msgstr ""
-#: documents/models.py:1237
+#: documents/models.py:1373
msgid "remove all document types"
msgstr ""
-#: documents/models.py:1244
+#: documents/models.py:1380
msgid "remove these correspondent(s)"
msgstr ""
-#: documents/models.py:1249
+#: documents/models.py:1385
msgid "remove all correspondents"
msgstr ""
-#: documents/models.py:1256
+#: documents/models.py:1392
msgid "remove these storage path(s)"
msgstr ""
-#: documents/models.py:1261
+#: documents/models.py:1397
msgid "remove all storage paths"
msgstr ""
-#: documents/models.py:1268
+#: documents/models.py:1404
msgid "remove these owner(s)"
msgstr ""
-#: documents/models.py:1273
+#: documents/models.py:1409
msgid "remove all owners"
msgstr ""
-#: documents/models.py:1280
+#: documents/models.py:1416
msgid "remove view permissions for these users"
msgstr ""
-#: documents/models.py:1287
+#: documents/models.py:1423
msgid "remove view permissions for these groups"
msgstr ""
-#: documents/models.py:1294
+#: documents/models.py:1430
msgid "remove change permissions for these users"
msgstr ""
-#: documents/models.py:1301
+#: documents/models.py:1437
msgid "remove change permissions for these groups"
msgstr ""
-#: documents/models.py:1306
+#: documents/models.py:1442
msgid "remove all permissions"
msgstr ""
-#: documents/models.py:1313
+#: documents/models.py:1449
msgid "remove these custom fields"
msgstr ""
-#: documents/models.py:1318
+#: documents/models.py:1454
msgid "remove all custom fields"
msgstr ""
-#: documents/models.py:1322
+#: documents/models.py:1463
+msgid "email"
+msgstr ""
+
+#: documents/models.py:1472
+msgid "webhook"
+msgstr ""
+
+#: documents/models.py:1476
msgid "workflow action"
msgstr ""
-#: documents/models.py:1323
+#: documents/models.py:1477
msgid "workflow actions"
msgstr ""
-#: documents/models.py:1332 paperless_mail/models.py:139
+#: documents/models.py:1486 paperless_mail/models.py:138
msgid "order"
msgstr ""
-#: documents/models.py:1338
+#: documents/models.py:1492
msgid "triggers"
msgstr ""
-#: documents/models.py:1345
+#: documents/models.py:1499
msgid "actions"
msgstr ""
-#: documents/models.py:1348 paperless_mail/models.py:148
+#: documents/models.py:1502 paperless_mail/models.py:147
msgid "enabled"
msgstr ""
-#: documents/serialisers.py:125
+#: documents/models.py:1513
+msgid "workflow"
+msgstr ""
+
+#: documents/models.py:1517
+msgid "workflow trigger type"
+msgstr ""
+
+#: documents/models.py:1531
+msgid "date run"
+msgstr ""
+
+#: documents/models.py:1537
+msgid "workflow run"
+msgstr ""
+
+#: documents/models.py:1538
+msgid "workflow runs"
+msgstr ""
+
+#: documents/serialisers.py:127
#, python-format
msgid "Invalid regular expression: %(error)s"
msgstr ""
-#: documents/serialisers.py:472
+#: documents/serialisers.py:474
msgid "Invalid color."
msgstr ""
-#: documents/serialisers.py:1410
+#: documents/serialisers.py:1441
#, python-format
msgid "File type %(type)s not supported"
msgstr ""
-#: documents/serialisers.py:1499
+#: documents/serialisers.py:1530
msgid "Invalid variable detected."
msgstr ""
msgid "An error occurred. Please try again."
msgstr ""
-#: documents/templates/account/password_reset.html:15
-msgid "Email"
-msgstr ""
-
#: documents/templates/account/password_reset.html:21
msgid "Send me instructions!"
msgstr ""
msgid "paperless application settings"
msgstr ""
-#: paperless/settings.py:687
+#: paperless/settings.py:698
msgid "English (US)"
msgstr ""
-#: paperless/settings.py:688
+#: paperless/settings.py:699
msgid "Arabic"
msgstr ""
-#: paperless/settings.py:689
+#: paperless/settings.py:700
msgid "Afrikaans"
msgstr ""
-#: paperless/settings.py:690
+#: paperless/settings.py:701
msgid "Belarusian"
msgstr ""
-#: paperless/settings.py:691
+#: paperless/settings.py:702
msgid "Bulgarian"
msgstr ""
-#: paperless/settings.py:692
+#: paperless/settings.py:703
msgid "Catalan"
msgstr ""
-#: paperless/settings.py:693
+#: paperless/settings.py:704
msgid "Czech"
msgstr ""
-#: paperless/settings.py:694
+#: paperless/settings.py:705
msgid "Danish"
msgstr ""
-#: paperless/settings.py:695
+#: paperless/settings.py:706
msgid "German"
msgstr ""
-#: paperless/settings.py:696
+#: paperless/settings.py:707
msgid "Greek"
msgstr ""
-#: paperless/settings.py:697
+#: paperless/settings.py:708
msgid "English (GB)"
msgstr ""
-#: paperless/settings.py:698
+#: paperless/settings.py:709
msgid "Spanish"
msgstr ""
-#: paperless/settings.py:699
+#: paperless/settings.py:710
msgid "Finnish"
msgstr ""
-#: paperless/settings.py:700
+#: paperless/settings.py:711
msgid "French"
msgstr ""
-#: paperless/settings.py:701
+#: paperless/settings.py:712
msgid "Hungarian"
msgstr ""
-#: paperless/settings.py:702
+#: paperless/settings.py:713
msgid "Italian"
msgstr ""
-#: paperless/settings.py:703
+#: paperless/settings.py:714
msgid "Japanese"
msgstr ""
-#: paperless/settings.py:704
+#: paperless/settings.py:715
msgid "Korean"
msgstr ""
-#: paperless/settings.py:705
+#: paperless/settings.py:716
msgid "Luxembourgish"
msgstr ""
-#: paperless/settings.py:706
+#: paperless/settings.py:717
msgid "Norwegian"
msgstr ""
-#: paperless/settings.py:707
+#: paperless/settings.py:718
msgid "Dutch"
msgstr ""
-#: paperless/settings.py:708
+#: paperless/settings.py:719
msgid "Polish"
msgstr ""
-#: paperless/settings.py:709
+#: paperless/settings.py:720
msgid "Portuguese (Brazil)"
msgstr ""
-#: paperless/settings.py:710
+#: paperless/settings.py:721
msgid "Portuguese"
msgstr ""
-#: paperless/settings.py:711
+#: paperless/settings.py:722
msgid "Romanian"
msgstr ""
-#: paperless/settings.py:712
+#: paperless/settings.py:723
msgid "Russian"
msgstr ""
-#: paperless/settings.py:713
+#: paperless/settings.py:724
msgid "Slovak"
msgstr ""
-#: paperless/settings.py:714
+#: paperless/settings.py:725
msgid "Slovenian"
msgstr ""
-#: paperless/settings.py:715
+#: paperless/settings.py:726
msgid "Serbian"
msgstr ""
-#: paperless/settings.py:716
+#: paperless/settings.py:727
msgid "Swedish"
msgstr ""
-#: paperless/settings.py:717
+#: paperless/settings.py:728
msgid "Turkish"
msgstr ""
-#: paperless/settings.py:718
+#: paperless/settings.py:729
msgid "Ukrainian"
msgstr ""
-#: paperless/settings.py:719
+#: paperless/settings.py:730
msgid "Chinese Simplified"
msgstr ""
-#: paperless/urls.py:268
+#: paperless/urls.py:341
msgid "Paperless-ngx administration"
msgstr ""
msgid "refresh token"
msgstr ""
-#: paperless_mail/models.py:71
+#: paperless_mail/models.py:70
msgid "The refresh token to use for token authentication e.g. with oauth2."
msgstr ""
-#: paperless_mail/models.py:80
+#: paperless_mail/models.py:79
msgid "The expiration date of the refresh token. "
msgstr ""
-#: paperless_mail/models.py:90
+#: paperless_mail/models.py:89
msgid "mail rule"
msgstr ""
-#: paperless_mail/models.py:91
+#: paperless_mail/models.py:90
msgid "mail rules"
msgstr ""
-#: paperless_mail/models.py:105 paperless_mail/models.py:116
+#: paperless_mail/models.py:104 paperless_mail/models.py:115
msgid "Only process attachments."
msgstr ""
-#: paperless_mail/models.py:106
+#: paperless_mail/models.py:105
msgid "Process full Mail (with embedded attachments in file) as .eml"
msgstr ""
-#: paperless_mail/models.py:110
+#: paperless_mail/models.py:109
msgid ""
"Process full Mail (with embedded attachments in file) as .eml + process "
"attachments as separate documents"
msgstr ""
-#: paperless_mail/models.py:117
+#: paperless_mail/models.py:116
msgid "Process all files, including 'inline' attachments."
msgstr ""
-#: paperless_mail/models.py:120
+#: paperless_mail/models.py:119
msgid "Delete"
msgstr ""
-#: paperless_mail/models.py:121
+#: paperless_mail/models.py:120
msgid "Move to specified folder"
msgstr ""
-#: paperless_mail/models.py:122
+#: paperless_mail/models.py:121
msgid "Mark as read, don't process read mails"
msgstr ""
-#: paperless_mail/models.py:123
+#: paperless_mail/models.py:122
msgid "Flag the mail, don't process flagged mails"
msgstr ""
-#: paperless_mail/models.py:124
+#: paperless_mail/models.py:123
msgid "Tag the mail with specified tag, don't process tagged mails"
msgstr ""
-#: paperless_mail/models.py:127
+#: paperless_mail/models.py:126
msgid "Use subject as title"
msgstr ""
-#: paperless_mail/models.py:128
+#: paperless_mail/models.py:127
msgid "Use attachment filename as title"
msgstr ""
-#: paperless_mail/models.py:129
+#: paperless_mail/models.py:128
msgid "Do not assign title from rule"
msgstr ""
-#: paperless_mail/models.py:132
+#: paperless_mail/models.py:131
msgid "Do not assign a correspondent"
msgstr ""
-#: paperless_mail/models.py:133
+#: paperless_mail/models.py:132
msgid "Use mail address"
msgstr ""
-#: paperless_mail/models.py:134
+#: paperless_mail/models.py:133
msgid "Use name (or mail address if not available)"
msgstr ""
-#: paperless_mail/models.py:135
+#: paperless_mail/models.py:134
msgid "Use correspondent selected below"
msgstr ""
-#: paperless_mail/models.py:145
+#: paperless_mail/models.py:144
msgid "account"
msgstr ""
-#: paperless_mail/models.py:151 paperless_mail/models.py:306
+#: paperless_mail/models.py:150 paperless_mail/models.py:305
msgid "folder"
msgstr ""
-#: paperless_mail/models.py:155
+#: paperless_mail/models.py:154
msgid ""
"Subfolders must be separated by a delimiter, often a dot ('.') or slash "
"('/'), but it varies by mail server."
msgstr ""
-#: paperless_mail/models.py:161
+#: paperless_mail/models.py:160
msgid "filter from"
msgstr ""
-#: paperless_mail/models.py:168
+#: paperless_mail/models.py:167
msgid "filter to"
msgstr ""
-#: paperless_mail/models.py:175
+#: paperless_mail/models.py:174
msgid "filter subject"
msgstr ""
-#: paperless_mail/models.py:182
+#: paperless_mail/models.py:181
msgid "filter body"
msgstr ""
-#: paperless_mail/models.py:189
+#: paperless_mail/models.py:188
msgid "filter attachment filename inclusive"
msgstr ""
-#: paperless_mail/models.py:201
+#: paperless_mail/models.py:200
msgid "filter attachment filename exclusive"
msgstr ""
-#: paperless_mail/models.py:206
+#: paperless_mail/models.py:205
msgid ""
"Do not consume documents which entirely match this filename if specified. "
"Wildcards such as *.pdf or *invoice* are allowed. Case insensitive."
msgstr ""
-#: paperless_mail/models.py:213
+#: paperless_mail/models.py:212
msgid "maximum age"
msgstr ""
-#: paperless_mail/models.py:215
+#: paperless_mail/models.py:214
msgid "Specified in days."
msgstr ""
-#: paperless_mail/models.py:219
+#: paperless_mail/models.py:218
msgid "attachment type"
msgstr ""
-#: paperless_mail/models.py:223
+#: paperless_mail/models.py:222
msgid ""
"Inline attachments include embedded images, so it's best to combine this "
"option with a filename filter."
msgstr ""
-#: paperless_mail/models.py:229
+#: paperless_mail/models.py:228
msgid "consumption scope"
msgstr ""
-#: paperless_mail/models.py:235
+#: paperless_mail/models.py:234
msgid "action"
msgstr ""
-#: paperless_mail/models.py:241
+#: paperless_mail/models.py:240
msgid "action parameter"
msgstr ""
-#: paperless_mail/models.py:246
+#: paperless_mail/models.py:245
msgid ""
"Additional parameter for the action selected above, i.e., the target folder "
"of the move to folder action. Subfolders must be separated by dots."
msgstr ""
-#: paperless_mail/models.py:254
+#: paperless_mail/models.py:253
msgid "assign title from"
msgstr ""
-#: paperless_mail/models.py:274
+#: paperless_mail/models.py:273
msgid "assign correspondent from"
msgstr ""
-#: paperless_mail/models.py:288
+#: paperless_mail/models.py:287
msgid "Assign the rule owner to documents"
msgstr ""
-#: paperless_mail/models.py:314
+#: paperless_mail/models.py:313
msgid "uid"
msgstr ""
-#: paperless_mail/models.py:322
+#: paperless_mail/models.py:321
msgid "subject"
msgstr ""
-#: paperless_mail/models.py:330
+#: paperless_mail/models.py:329
msgid "received"
msgstr ""
-#: paperless_mail/models.py:337
+#: paperless_mail/models.py:336
msgid "processed"
msgstr ""
-#: paperless_mail/models.py:343
+#: paperless_mail/models.py:342
msgid "status"
msgstr ""
EMAIL_USE_TLS: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_TLS")
EMAIL_USE_SSL: Final[bool] = __get_boolean("PAPERLESS_EMAIL_USE_SSL")
EMAIL_SUBJECT_PREFIX: Final[str] = "[Paperless-ngx] "
+EMAIL_ENABLED = EMAIL_HOST != "localhost" or EMAIL_HOST_USER != ""
if DEBUG: # pragma: no cover
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = BASE_DIR / "sent_emails"