mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Feature: email, webhook workflow actions (#8108)
This commit is contained in:
parent
81a5baa451
commit
1d65628132
@ -322,6 +322,8 @@ fields and permissions, which will be merged.
|
||||
|
||||
### 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
|
||||
@ -380,25 +382,49 @@ Workflows allow you to filter by:
|
||||
|
||||
### Workflow Actions
|
||||
|
||||
There are currently two types of workflow actions, "Assignment", which can assign:
|
||||
#### Types
|
||||
|
||||
- Title, see [title placeholders](usage.md#title-placeholders) below
|
||||
The following workflow action types are available:
|
||||
|
||||
##### Assignment
|
||||
|
||||
"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
|
||||
|
||||
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
|
||||
"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
|
||||
|
||||
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
|
||||
@ -424,6 +450,7 @@ The following placeholders are only available for "added" or "updated" triggers
|
||||
- `{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
|
||||
|
||||
|
@ -1476,11 +1476,11 @@
|
||||
</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>
|
||||
@ -2199,25 +2199,25 @@
|
||||
<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">
|
||||
@ -2300,7 +2300,7 @@
|
||||
</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">
|
||||
@ -2577,19 +2577,19 @@
|
||||
</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>
|
||||
@ -3019,11 +3019,11 @@
|
||||
</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">
|
||||
@ -3172,7 +3172,7 @@
|
||||
</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>
|
||||
@ -3293,7 +3293,7 @@
|
||||
<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">
|
||||
@ -3312,7 +3312,7 @@
|
||||
</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">
|
||||
@ -4285,6 +4285,10 @@
|
||||
<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>
|
||||
@ -4788,6 +4792,76 @@
|
||||
<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">
|
||||
@ -4869,18 +4943,25 @@
|
||||
<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">
|
||||
@ -4901,7 +4982,7 @@
|
||||
<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>
|
||||
@ -4928,21 +5009,21 @@
|
||||
<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>
|
||||
@ -4950,7 +5031,7 @@
|
||||
<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">
|
||||
@ -5094,6 +5175,17 @@
|
||||
<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">
|
||||
@ -5146,7 +5238,7 @@
|
||||
</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">
|
||||
@ -5283,13 +5375,6 @@
|
||||
<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">
|
||||
@ -5308,14 +5393,7 @@
|
||||
<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">
|
||||
@ -5929,11 +6007,11 @@
|
||||
</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">
|
||||
@ -5944,11 +6022,11 @@
|
||||
</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">
|
||||
@ -6209,7 +6287,7 @@
|
||||
</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>
|
||||
@ -6573,36 +6651,36 @@
|
||||
<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>
|
||||
@ -6613,7 +6691,7 @@
|
||||
<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>
|
||||
@ -6624,7 +6702,7 @@
|
||||
<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>
|
||||
@ -6635,123 +6713,123 @@
|
||||
<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">
|
||||
@ -7096,6 +7174,13 @@
|
||||
</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>
|
||||
@ -7130,21 +7215,21 @@
|
||||
</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>
|
||||
@ -7159,7 +7244,7 @@
|
||||
<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>
|
||||
@ -7174,7 +7259,7 @@
|
||||
<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>
|
||||
@ -7189,7 +7274,7 @@
|
||||
<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>
|
||||
@ -7200,7 +7285,7 @@
|
||||
<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>
|
||||
@ -7219,7 +7304,7 @@
|
||||
<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">
|
||||
@ -7473,21 +7558,14 @@
|
||||
<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>
|
||||
@ -7498,7 +7576,7 @@
|
||||
<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>
|
||||
@ -9119,259 +9197,259 @@
|
||||
<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">
|
||||
|
@ -131,6 +131,7 @@ import { GlobalSearchComponent } from './components/app-frame/global-search/glob
|
||||
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,
|
||||
@ -522,6 +523,7 @@ function initializeApp(settings: SettingsService) {
|
||||
HotkeyDialogComponent,
|
||||
DeletePagesConfirmDialogComponent,
|
||||
TrashComponent,
|
||||
EntriesComponent,
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [
|
||||
|
@ -322,6 +322,33 @@
|
||||
</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>
|
||||
|
@ -347,4 +347,15 @@ describe('WorkflowEditDialogComponent', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
@ -96,6 +96,14 @@ export const WORKFLOW_ACTION_OPTIONS = [
|
||||
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(
|
||||
@ -402,6 +410,22 @@ export class WorkflowEditDialogComponent
|
||||
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 }
|
||||
)
|
||||
@ -503,6 +527,22 @@ export class WorkflowEditDialogComponent
|
||||
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)
|
||||
@ -533,4 +573,18 @@ export class WorkflowEditDialogComponent
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
<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>
|
@ -0,0 +1,65 @@
|
||||
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: '' })
|
||||
})
|
||||
})
|
@ -0,0 +1,48 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -3,7 +3,34 @@ import { ObjectWithId } from './object-with-id'
|
||||
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
|
||||
|
||||
@ -62,4 +89,8 @@ export interface WorkflowAction extends ObjectWithId {
|
||||
remove_custom_fields?: number[] // [CustomField.id]
|
||||
|
||||
remove_all_custom_fields?: boolean
|
||||
|
||||
email?: WorkflowActionEmail
|
||||
|
||||
webhook?: WorkflowActionWebhook
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ from documents.plugins.helpers import ProgressStatusOptions
|
||||
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
|
||||
@ -666,7 +666,7 @@ class ConsumerPlugin(
|
||||
else None
|
||||
)
|
||||
|
||||
return parse_doc_title_w_placeholders(
|
||||
return parse_w_workflow_placeholders(
|
||||
title,
|
||||
correspondent_name,
|
||||
doc_type_name,
|
||||
|
@ -18,8 +18,7 @@ def settings(request):
|
||||
)
|
||||
|
||||
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,
|
||||
|
@ -0,0 +1,154 @@
|
||||
# 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",
|
||||
),
|
||||
),
|
||||
]
|
@ -63,7 +63,7 @@ def reverse_migrate_customfield_selects(apps, schema_editor):
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1058_workflowtrigger_schedule_date_custom_field_and_more"),
|
||||
("documents", "1059_workflowactionemail_workflowactionwebhook_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
@ -1160,6 +1160,85 @@ class WorkflowTrigger(models.Model):
|
||||
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 = (
|
||||
@ -1170,6 +1249,14 @@ class WorkflowAction(models.Model):
|
||||
2,
|
||||
_("Removal"),
|
||||
)
|
||||
EMAIL = (
|
||||
3,
|
||||
_("Email"),
|
||||
)
|
||||
WEBHOOK = (
|
||||
4,
|
||||
_("Webhook"),
|
||||
)
|
||||
|
||||
type = models.PositiveIntegerField(
|
||||
_("Workflow Action Type"),
|
||||
@ -1371,6 +1458,24 @@ class WorkflowAction(models.Model):
|
||||
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")
|
||||
|
@ -49,6 +49,8 @@ from documents.models import Tag
|
||||
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
|
||||
@ -1818,12 +1820,44 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
|
||||
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
|
||||
@ -1858,6 +1892,8 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
||||
"remove_view_groups",
|
||||
"remove_change_users",
|
||||
"remove_change_groups",
|
||||
"email",
|
||||
"webhook",
|
||||
]
|
||||
|
||||
def validate(self, attrs):
|
||||
@ -1895,6 +1931,24 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
||||
{"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
|
||||
|
||||
|
||||
@ -1949,11 +2003,34 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
||||
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:
|
||||
@ -2006,6 +2083,9 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
||||
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")
|
||||
|
@ -2,6 +2,8 @@ import logging
|
||||
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
|
||||
@ -12,6 +14,7 @@ from django.contrib.admin.models import ADDITION
|
||||
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
|
||||
@ -41,7 +44,7 @@ from documents.models import WorkflowRun
|
||||
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")
|
||||
|
||||
@ -570,6 +573,30 @@ def run_workflows_updated(sender, document: Document, logging_group=None, **kwar
|
||||
)
|
||||
|
||||
|
||||
@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,
|
||||
@ -622,7 +649,7 @@ def run_workflows(
|
||||
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 "",
|
||||
@ -879,6 +906,151 @@ def run_workflows(
|
||||
):
|
||||
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 = []
|
||||
|
||||
@ -924,6 +1096,10 @@ def run_workflows(
|
||||
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
|
||||
|
@ -2,14 +2,16 @@ from datetime import datetime
|
||||
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,
|
||||
@ -43,4 +45,8 @@ def parse_doc_title_w_placeholders(
|
||||
"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()
|
@ -433,3 +433,158 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
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)
|
||||
|
@ -4,8 +4,8 @@ from documents.tests.utils import TestMigrations
|
||||
|
||||
|
||||
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")
|
||||
@ -43,8 +43,8 @@ class TestMigrateCustomFieldSelects(TestMigrations):
|
||||
|
||||
|
||||
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")
|
||||
|
@ -1,20 +1,25 @@
|
||||
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
|
||||
@ -29,19 +34,25 @@ from documents.models import StoragePath
|
||||
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")
|
||||
@ -2077,3 +2088,477 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
||||
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])
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1195,6 +1195,7 @@ DEFAULT_FROM_EMAIL: Final[str] = os.getenv("PAPERLESS_EMAIL_FROM", EMAIL_HOST_US
|
||||
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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user