Merge branch 'dev' into feature-doc-detail-fixes
This commit is contained in:
commit
3db7ab4675
@ -4,9 +4,9 @@ Paperless provides a wide range of customizations. Depending on how you
|
|||||||
run paperless, these settings have to be defined in different places.
|
run paperless, these settings have to be defined in different places.
|
||||||
|
|
||||||
Certain configuration options may be set via the UI. This currently includes
|
Certain configuration options may be set via the UI. This currently includes
|
||||||
common [OCR](#ocr) related settings. If set, these will take preference over the
|
common [OCR](#ocr) related settings and some frontend settings. If set, these will take
|
||||||
settings via environment variables. If not set, the environment setting or applicable
|
preference over the settings via environment variables. If not set, the environment setting
|
||||||
default will be utilized instead.
|
or applicable default will be utilized instead.
|
||||||
|
|
||||||
- If you run paperless on docker, `paperless.conf` is not used.
|
- If you run paperless on docker, `paperless.conf` is not used.
|
||||||
Rather, configure paperless by copying necessary options to
|
Rather, configure paperless by copying necessary options to
|
||||||
@ -1329,7 +1329,15 @@ started by the container.
|
|||||||
|
|
||||||
You can read more about this in the [advanced documentation](advanced_usage.md#celery-monitoring).
|
You can read more about this in the [advanced documentation](advanced_usage.md#celery-monitoring).
|
||||||
|
|
||||||
## Update Checking {#update-checking}
|
## Frontend Settings
|
||||||
|
|
||||||
|
#### [`PAPERLESS_APP_TITLE=<bool>`](#PAPERLESS_APP_TITLE) {#PAPERLESS_APP_TITLE}
|
||||||
|
|
||||||
|
: If set, overrides the default name "Paperless-ngx"
|
||||||
|
|
||||||
|
#### [`PAPERLESS_APP_LOGO=<path>`](#PAPERLESS_APP_LOGO) {#PAPERLESS_APP_LOGO}
|
||||||
|
|
||||||
|
: Path to an image file in the /media/logo directory, must include 'logo', e.g. `/logo/Atari_logo.svg`
|
||||||
|
|
||||||
#### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK}
|
#### [`PAPERLESS_ENABLE_UPDATE_CHECK=<bool>`](#PAPERLESS_ENABLE_UPDATE_CHECK) {#PAPERLESS_ENABLE_UPDATE_CHECK}
|
||||||
|
|
||||||
|
@ -439,7 +439,7 @@
|
|||||||
<source>Discard</source>
|
<source>Discard</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/config/config.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/config/config.component.html</context>
|
||||||
<context context-type="linenumber">48</context>
|
<context context-type="linenumber">49</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
@ -450,7 +450,7 @@
|
|||||||
<source>Save</source>
|
<source>Save</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/config/config.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/config/config.component.html</context>
|
||||||
<context context-type="linenumber">51</context>
|
<context context-type="linenumber">52</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
@ -513,28 +513,42 @@
|
|||||||
<source>Error retrieving config</source>
|
<source>Error retrieving config</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
||||||
<context context-type="linenumber">79</context>
|
<context context-type="linenumber">81</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1172622527269118932" datatype="html">
|
<trans-unit id="1172622527269118932" datatype="html">
|
||||||
<source>Invalid JSON</source>
|
<source>Invalid JSON</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
||||||
<context context-type="linenumber">105</context>
|
<context context-type="linenumber">107</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5103146006962696736" datatype="html">
|
<trans-unit id="5103146006962696736" datatype="html">
|
||||||
<source>Configuration updated</source>
|
<source>Configuration updated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
||||||
<context context-type="linenumber">148</context>
|
<context context-type="linenumber">151</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1664963291286452273" datatype="html">
|
<trans-unit id="1664963291286452273" datatype="html">
|
||||||
<source>An error occurred updating configuration</source>
|
<source>An error occurred updating configuration</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
||||||
<context context-type="linenumber">153</context>
|
<context context-type="linenumber">156</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2653081282186526824" datatype="html">
|
||||||
|
<source>File successfully updated</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
||||||
|
<context context-type="linenumber">178</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5902783625859504265" datatype="html">
|
||||||
|
<source>An error occurred uploading file</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/config/config.component.ts</context>
|
||||||
|
<context context-type="linenumber">183</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4804785061014590286" datatype="html">
|
<trans-unit id="4804785061014590286" datatype="html">
|
||||||
@ -545,11 +559,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">309</context>
|
<context context-type="linenumber">319</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">314</context>
|
<context context-type="linenumber">324</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8838884664569764142" datatype="html">
|
<trans-unit id="8838884664569764142" datatype="html">
|
||||||
@ -646,15 +660,15 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">59</context>
|
<context context-type="linenumber">69</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">267</context>
|
<context context-type="linenumber">277</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">271</context>
|
<context context-type="linenumber">281</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1685061484835793745" datatype="html">
|
<trans-unit id="1685061484835793745" datatype="html">
|
||||||
@ -1123,7 +1137,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">116</context>
|
<context context-type="linenumber">126</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1595668988802980095" datatype="html">
|
<trans-unit id="1595668988802980095" datatype="html">
|
||||||
@ -1517,7 +1531,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">117</context>
|
<context context-type="linenumber">121</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5260584511980773458" datatype="html">
|
<trans-unit id="5260584511980773458" datatype="html">
|
||||||
@ -1535,7 +1549,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">296</context>
|
<context context-type="linenumber">306</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="103921551219467537" datatype="html">
|
<trans-unit id="103921551219467537" datatype="html">
|
||||||
@ -1722,11 +1736,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">285</context>
|
<context context-type="linenumber">295</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">289</context>
|
<context context-type="linenumber">299</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4555457172864212828" datatype="html">
|
<trans-unit id="4555457172864212828" datatype="html">
|
||||||
@ -1917,7 +1931,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">704</context>
|
<context context-type="linenumber">701</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@ -1956,7 +1970,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">706</context>
|
<context context-type="linenumber">703</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@ -2035,66 +2049,65 @@
|
|||||||
<context context-type="linenumber">180</context>
|
<context context-type="linenumber">180</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2173456130768795374" datatype="html">
|
<trans-unit id="7931334600001636863" datatype="html">
|
||||||
<source>Paperless-ngx</source>
|
<source>by Paperless-ngx</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">15</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">app title</note>
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7100953725264790651" datatype="html">
|
<trans-unit id="7100953725264790651" datatype="html">
|
||||||
<source>Search documents</source>
|
<source>Search documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">23</context>
|
<context context-type="linenumber">33</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2448391510242468907" datatype="html">
|
<trans-unit id="2448391510242468907" datatype="html">
|
||||||
<source>Logged in as <x id="INTERPOLATION" equiv-text="{{this.settingsService.displayName}}"/></source>
|
<source>Logged in as <x id="INTERPOLATION" equiv-text="{{this.settingsService.displayName}}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">47</context>
|
<context context-type="linenumber">57</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2127032578120864096" datatype="html">
|
<trans-unit id="2127032578120864096" datatype="html">
|
||||||
<source>My Profile</source>
|
<source>My Profile</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">53</context>
|
<context context-type="linenumber">63</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3797778920049399855" datatype="html">
|
<trans-unit id="3797778920049399855" datatype="html">
|
||||||
<source>Logout</source>
|
<source>Logout</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">64</context>
|
<context context-type="linenumber">74</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4895326106573044490" datatype="html">
|
<trans-unit id="4895326106573044490" datatype="html">
|
||||||
<source>Documentation</source>
|
<source>Documentation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">71</context>
|
<context context-type="linenumber">81</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">319</context>
|
<context context-type="linenumber">329</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">324</context>
|
<context context-type="linenumber">334</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6570363013146073520" datatype="html">
|
<trans-unit id="6570363013146073520" datatype="html">
|
||||||
<source>Dashboard</source>
|
<source>Dashboard</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">96</context>
|
<context context-type="linenumber">106</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">100</context>
|
<context context-type="linenumber">110</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
|
||||||
@ -2105,11 +2118,11 @@
|
|||||||
<source>Documents</source>
|
<source>Documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">105</context>
|
<context context-type="linenumber">115</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">109</context>
|
<context context-type="linenumber">119</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
|
||||||
@ -2136,36 +2149,36 @@
|
|||||||
<source>Open documents</source>
|
<source>Open documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">150</context>
|
<context context-type="linenumber">160</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5687256342387781369" datatype="html">
|
<trans-unit id="5687256342387781369" datatype="html">
|
||||||
<source>Close all</source>
|
<source>Close all</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">174</context>
|
<context context-type="linenumber">184</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">178</context>
|
<context context-type="linenumber">188</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3897348120591552265" datatype="html">
|
<trans-unit id="3897348120591552265" datatype="html">
|
||||||
<source>Manage</source>
|
<source>Manage</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">186</context>
|
<context context-type="linenumber">196</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7437910965833684826" datatype="html">
|
<trans-unit id="7437910965833684826" datatype="html">
|
||||||
<source>Correspondents</source>
|
<source>Correspondents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">192</context>
|
<context context-type="linenumber">202</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">196</context>
|
<context context-type="linenumber">206</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
@ -2176,11 +2189,11 @@
|
|||||||
<source>Tags</source>
|
<source>Tags</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">201</context>
|
<context context-type="linenumber">211</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">206</context>
|
<context context-type="linenumber">216</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context>
|
||||||
@ -2207,11 +2220,11 @@
|
|||||||
<source>Document Types</source>
|
<source>Document Types</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">212</context>
|
<context context-type="linenumber">222</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">216</context>
|
<context context-type="linenumber">226</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
@ -2222,11 +2235,11 @@
|
|||||||
<source>Storage Paths</source>
|
<source>Storage Paths</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">221</context>
|
<context context-type="linenumber">231</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">225</context>
|
<context context-type="linenumber">235</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
@ -2237,11 +2250,11 @@
|
|||||||
<source>Custom Fields</source>
|
<source>Custom Fields</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">230</context>
|
<context context-type="linenumber">240</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">234</context>
|
<context context-type="linenumber">244</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context>
|
||||||
@ -2256,11 +2269,11 @@
|
|||||||
<source>Workflows</source>
|
<source>Workflows</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">241</context>
|
<context context-type="linenumber">251</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">245</context>
|
<context context-type="linenumber">255</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
||||||
@ -2271,99 +2284,99 @@
|
|||||||
<source>Mail</source>
|
<source>Mail</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">250</context>
|
<context context-type="linenumber">260</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">255</context>
|
<context context-type="linenumber">265</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7844706011418789951" datatype="html">
|
<trans-unit id="7844706011418789951" datatype="html">
|
||||||
<source>Administration</source>
|
<source>Administration</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">261</context>
|
<context context-type="linenumber">271</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3008420115644088420" datatype="html">
|
<trans-unit id="3008420115644088420" datatype="html">
|
||||||
<source>Configuration</source>
|
<source>Configuration</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">276</context>
|
<context context-type="linenumber">286</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">280</context>
|
<context context-type="linenumber">290</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6626289114556551491" datatype="html">
|
<trans-unit id="6626289114556551491" datatype="html">
|
||||||
<source>File Tasks<x id="START_BLOCK_IF" equiv-text="@if (tasksService.failedFileTasks.length > 0) {"/><x id="START_TAG_SPAN_1" ctype="x-span_1" equiv-text="<span>"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-danger ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
|
<source>File Tasks<x id="START_BLOCK_IF" equiv-text="@if (tasksService.failedFileTasks.length > 0) {"/><x id="START_TAG_SPAN_1" ctype="x-span_1" equiv-text="<span>"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-danger ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.failedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">303,305</context>
|
<context context-type="linenumber">313,315</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1534029177398918729" datatype="html">
|
<trans-unit id="1534029177398918729" datatype="html">
|
||||||
<source>GitHub</source>
|
<source>GitHub</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">331</context>
|
<context context-type="linenumber">341</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4112664765954374539" datatype="html">
|
<trans-unit id="4112664765954374539" datatype="html">
|
||||||
<source>is available.</source>
|
<source>is available.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">340,341</context>
|
<context context-type="linenumber">350,351</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1175891574282637937" datatype="html">
|
<trans-unit id="1175891574282637937" datatype="html">
|
||||||
<source>Click to view.</source>
|
<source>Click to view.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">341</context>
|
<context context-type="linenumber">351</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9811291095862612" datatype="html">
|
<trans-unit id="9811291095862612" datatype="html">
|
||||||
<source>Paperless-ngx can automatically check for updates</source>
|
<source>Paperless-ngx can automatically check for updates</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">345</context>
|
<context context-type="linenumber">355</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="894819944961861800" datatype="html">
|
<trans-unit id="894819944961861800" datatype="html">
|
||||||
<source> How does this work? </source>
|
<source> How does this work? </source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">352,354</context>
|
<context context-type="linenumber">362,364</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="509090351011426949" datatype="html">
|
<trans-unit id="509090351011426949" datatype="html">
|
||||||
<source>Update available</source>
|
<source>Update available</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">368</context>
|
<context context-type="linenumber">378</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1542489069631984294" datatype="html">
|
<trans-unit id="1542489069631984294" datatype="html">
|
||||||
<source>Sidebar views updated</source>
|
<source>Sidebar views updated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">259</context>
|
<context context-type="linenumber">263</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3547923076537026828" datatype="html">
|
<trans-unit id="3547923076537026828" datatype="html">
|
||||||
<source>Error updating sidebar views</source>
|
<source>Error updating sidebar views</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">262</context>
|
<context context-type="linenumber">266</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2526035785704676448" datatype="html">
|
<trans-unit id="2526035785704676448" datatype="html">
|
||||||
<source>An error occurred while saving update checking settings.</source>
|
<source>An error occurred while saving update checking settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">283</context>
|
<context context-type="linenumber">287</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8700121026680200191" datatype="html">
|
<trans-unit id="8700121026680200191" datatype="html">
|
||||||
@ -2381,13 +2394,6 @@
|
|||||||
<context context-type="linenumber">63</context>
|
<context context-type="linenumber">63</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5000042972069710005" datatype="html">
|
|
||||||
<source><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="d-inline-block" style="padding-bottom: 1px;" >"/>Cancel<x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/></source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">16</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1234709746630139322" datatype="html">
|
<trans-unit id="1234709746630139322" datatype="html">
|
||||||
<source>Confirmation</source>
|
<source>Confirmation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@ -2422,6 +2428,73 @@
|
|||||||
<context context-type="linenumber">439</context>
|
<context context-type="linenumber">439</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="2159130950882492111" datatype="html">
|
||||||
|
<source>Cancel</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
||||||
|
<context context-type="linenumber">44</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">24</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">15</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">26</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">16</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">36</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">48</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">25</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">27</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">35</context>
|
||||||
|
</context-group>
|
||||||
|
<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">170</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">22</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">57</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/select-dialog/select-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">12</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
|
<context context-type="linenumber">6</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
|
||||||
|
<context context-type="linenumber">20</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="3972154626835212608" datatype="html">
|
<trans-unit id="3972154626835212608" datatype="html">
|
||||||
<source>Create New Field</source>
|
<source>Create New Field</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@ -2586,69 +2659,6 @@
|
|||||||
<context context-type="linenumber">194</context>
|
<context context-type="linenumber">194</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2159130950882492111" datatype="html">
|
|
||||||
<source>Cancel</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">24</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">15</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">26</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/group-edit-dialog/group-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">16</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">36</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">48</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">25</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">27</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/user-edit-dialog/user-edit-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">35</context>
|
|
||||||
</context-group>
|
|
||||||
<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">170</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">22</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">57</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/select-dialog/select-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">12</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
|
||||||
<context context-type="linenumber">6</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/save-view-config-dialog/save-view-config-dialog.component.html</context>
|
|
||||||
<context context-type="linenumber">20</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6457471243969293847" datatype="html">
|
<trans-unit id="6457471243969293847" datatype="html">
|
||||||
<source>Create new correspondent</source>
|
<source>Create new correspondent</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@ -3696,6 +3706,14 @@
|
|||||||
<context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/input/document-link/document-link.component.html</context>
|
||||||
<context context-type="linenumber">11</context>
|
<context context-type="linenumber">11</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/input/file/file.component.html</context>
|
||||||
|
<context context-type="linenumber">11</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/input/file/file.component.html</context>
|
||||||
|
<context context-type="linenumber">25</context>
|
||||||
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/input/number/number.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/input/number/number.component.html</context>
|
||||||
<context context-type="linenumber">11</context>
|
<context context-type="linenumber">11</context>
|
||||||
@ -3757,6 +3775,13 @@
|
|||||||
<context context-type="linenumber">44</context>
|
<context context-type="linenumber">44</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="6932865105766151309" datatype="html">
|
||||||
|
<source>Upload</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/common/input/file/file.component.html</context>
|
||||||
|
<context context-type="linenumber">17</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="5554528553553249088" datatype="html">
|
<trans-unit id="5554528553553249088" datatype="html">
|
||||||
<source>Show password</source>
|
<source>Show password</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
@ -4188,32 +4213,32 @@
|
|||||||
<context context-type="linenumber">44</context>
|
<context context-type="linenumber">44</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1865646076514070962" datatype="html">
|
<trans-unit id="6581372518205328477" datatype="html">
|
||||||
<source>Hello <x id="PH" equiv-text="this.settingsService.displayName"/>, welcome to Paperless-ngx</source>
|
<source>Hello <x id="PH" equiv-text="this.settingsService.displayName"/>, welcome to <x id="PH_1" equiv-text="appTitle"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
|
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
|
||||||
<context context-type="linenumber">38</context>
|
<context context-type="linenumber">41</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5334686081082652461" datatype="html">
|
<trans-unit id="2901300640157872718" datatype="html">
|
||||||
<source>Welcome to Paperless-ngx</source>
|
<source>Welcome to <x id="PH" equiv-text="appTitle"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
|
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
|
||||||
<context context-type="linenumber">40</context>
|
<context context-type="linenumber">43</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1325877348738783391" datatype="html">
|
<trans-unit id="1325877348738783391" datatype="html">
|
||||||
<source>Dashboard updated</source>
|
<source>Dashboard updated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
|
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
|
||||||
<context context-type="linenumber">71</context>
|
<context context-type="linenumber">74</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3214475953924351473" datatype="html">
|
<trans-unit id="3214475953924351473" datatype="html">
|
||||||
<source>Error updating dashboard</source>
|
<source>Error updating dashboard</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
|
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
|
||||||
<context context-type="linenumber">74</context>
|
<context context-type="linenumber">77</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2946624699882754313" datatype="html">
|
<trans-unit id="2946624699882754313" datatype="html">
|
||||||
@ -4727,7 +4752,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7206723502037428235" datatype="html">
|
<trans-unit id="7206723502037428235" datatype="html">
|
||||||
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
|
<source>Notes <x id="START_BLOCK_IF" equiv-text="@if (document?.notes.length) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge text-bg-secondary ms-1">"/><x id="INTERPOLATION" equiv-text="ngth}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="}</a>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.html</context>
|
||||||
<context context-type="linenumber">286,289</context>
|
<context context-type="linenumber">286,289</context>
|
||||||
@ -4762,50 +4787,78 @@
|
|||||||
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
<source>An error occurred loading content: <x id="PH" equiv-text="err.message ?? err.toString()"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">284,286</context>
|
<context context-type="linenumber">276,278</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="3200733026060976258" datatype="html">
|
||||||
|
<source>Document changes detected</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
|
<context context-type="linenumber">298</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2887155916749964" datatype="html">
|
||||||
|
<source>The version of this document in your browser session appears older than the existing version.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
|
<context context-type="linenumber">299</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="237142428785956348" datatype="html">
|
||||||
|
<source>Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
|
<context context-type="linenumber">300</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="8720977247725652816" datatype="html">
|
||||||
|
<source>Ok</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
|
<context context-type="linenumber">301</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5758784066858623886" datatype="html">
|
<trans-unit id="5758784066858623886" datatype="html">
|
||||||
<source>Error retrieving metadata</source>
|
<source>Error retrieving metadata</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">436</context>
|
<context context-type="linenumber">437</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3456881259945295697" datatype="html">
|
<trans-unit id="3456881259945295697" datatype="html">
|
||||||
<source>Error retrieving suggestions.</source>
|
<source>Error retrieving suggestions.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">461</context>
|
<context context-type="linenumber">458</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8348337312757497317" datatype="html">
|
<trans-unit id="8348337312757497317" datatype="html">
|
||||||
<source>Document saved successfully.</source>
|
<source>Document saved successfully.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">579</context>
|
<context context-type="linenumber">576</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">588</context>
|
<context context-type="linenumber">585</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="448882439049417053" datatype="html">
|
<trans-unit id="448882439049417053" datatype="html">
|
||||||
<source>Error saving document</source>
|
<source>Error saving document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">592</context>
|
<context context-type="linenumber">589</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">633</context>
|
<context context-type="linenumber">630</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9021887951960049161" datatype="html">
|
<trans-unit id="9021887951960049161" datatype="html">
|
||||||
<source>Confirm delete</source>
|
<source>Confirm delete</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">659</context>
|
<context context-type="linenumber">656</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
||||||
@ -4816,35 +4869,35 @@
|
|||||||
<source>Do you really want to delete document "<x id="PH" equiv-text="this.document.title"/>"?</source>
|
<source>Do you really want to delete document "<x id="PH" equiv-text="this.document.title"/>"?</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">660</context>
|
<context context-type="linenumber">657</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6691075929777935948" datatype="html">
|
<trans-unit id="6691075929777935948" datatype="html">
|
||||||
<source>The files for this document will be deleted permanently. This operation cannot be undone.</source>
|
<source>The files for this document will be deleted permanently. This operation cannot be undone.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">661</context>
|
<context context-type="linenumber">658</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="719892092227206532" datatype="html">
|
<trans-unit id="719892092227206532" datatype="html">
|
||||||
<source>Delete document</source>
|
<source>Delete document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">663</context>
|
<context context-type="linenumber">660</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7295637485862454066" datatype="html">
|
<trans-unit id="7295637485862454066" datatype="html">
|
||||||
<source>Error deleting document</source>
|
<source>Error deleting document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">682</context>
|
<context context-type="linenumber">679</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7362691899087997122" datatype="html">
|
<trans-unit id="7362691899087997122" datatype="html">
|
||||||
<source>Redo OCR confirm</source>
|
<source>Redo OCR confirm</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">702</context>
|
<context context-type="linenumber">699</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
|
||||||
@ -4855,28 +4908,28 @@
|
|||||||
<source>This operation will permanently redo OCR for this document.</source>
|
<source>This operation will permanently redo OCR for this document.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">703</context>
|
<context context-type="linenumber">700</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5729001209753056399" datatype="html">
|
<trans-unit id="5729001209753056399" datatype="html">
|
||||||
<source>Redo OCR operation will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.</source>
|
<source>Redo OCR 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-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">714</context>
|
<context context-type="linenumber">711</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4409560272830824468" datatype="html">
|
<trans-unit id="4409560272830824468" datatype="html">
|
||||||
<source>Error executing operation</source>
|
<source>Error executing operation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">725</context>
|
<context context-type="linenumber">722</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4458954481601077369" datatype="html">
|
<trans-unit id="4458954481601077369" datatype="html">
|
||||||
<source>Page Fit</source>
|
<source>Page Fit</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-detail/document-detail.component.ts</context>
|
||||||
<context context-type="linenumber">794</context>
|
<context context-type="linenumber">791</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6857598786757174736" datatype="html">
|
<trans-unit id="6857598786757174736" datatype="html">
|
||||||
@ -6443,102 +6496,123 @@
|
|||||||
<context context-type="linenumber">46</context>
|
<context context-type="linenumber">46</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="432834967329800065" datatype="html">
|
||||||
|
<source>General Settings</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
|
<context context-type="linenumber">50</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="2762851116637676072" datatype="html">
|
<trans-unit id="2762851116637676072" datatype="html">
|
||||||
<source>OCR Settings</source>
|
<source>OCR Settings</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">49</context>
|
<context context-type="linenumber">51</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1313137480169642057" datatype="html">
|
<trans-unit id="1313137480169642057" datatype="html">
|
||||||
<source>Output Type</source>
|
<source>Output Type</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">73</context>
|
<context context-type="linenumber">75</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2826581353496868063" datatype="html">
|
<trans-unit id="2826581353496868063" datatype="html">
|
||||||
<source>Language</source>
|
<source>Language</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">81</context>
|
<context context-type="linenumber">83</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3817498941817715969" datatype="html">
|
<trans-unit id="3817498941817715969" datatype="html">
|
||||||
<source>Pages</source>
|
<source>Pages</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">88</context>
|
<context context-type="linenumber">90</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1713271461473302108" datatype="html">
|
<trans-unit id="1713271461473302108" datatype="html">
|
||||||
<source>Mode</source>
|
<source>Mode</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">95</context>
|
<context context-type="linenumber">97</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6114528299376689399" datatype="html">
|
<trans-unit id="6114528299376689399" datatype="html">
|
||||||
<source>Skip Archive File</source>
|
<source>Skip Archive File</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1115402553541327390" datatype="html">
|
<trans-unit id="1115402553541327390" datatype="html">
|
||||||
<source>Image DPI</source>
|
<source>Image DPI</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">111</context>
|
<context context-type="linenumber">113</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6352596107300820129" datatype="html">
|
<trans-unit id="6352596107300820129" datatype="html">
|
||||||
<source>Clean</source>
|
<source>Clean</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">118</context>
|
<context context-type="linenumber">120</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="725308589819024010" datatype="html">
|
<trans-unit id="725308589819024010" datatype="html">
|
||||||
<source>Deskew</source>
|
<source>Deskew</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">126</context>
|
<context context-type="linenumber">128</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6256076128297775802" datatype="html">
|
<trans-unit id="6256076128297775802" datatype="html">
|
||||||
<source>Rotate Pages</source>
|
<source>Rotate Pages</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">133</context>
|
<context context-type="linenumber">135</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8527188778859256947" datatype="html">
|
<trans-unit id="8527188778859256947" datatype="html">
|
||||||
<source>Rotate Pages Threshold</source>
|
<source>Rotate Pages Threshold</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">140</context>
|
<context context-type="linenumber">142</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3762131309176747817" datatype="html">
|
<trans-unit id="3762131309176747817" datatype="html">
|
||||||
<source>Max Image Pixels</source>
|
<source>Max Image Pixels</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">147</context>
|
<context context-type="linenumber">149</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7846583355792281769" datatype="html">
|
<trans-unit id="7846583355792281769" datatype="html">
|
||||||
<source>Color Conversion Strategy</source>
|
<source>Color Conversion Strategy</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">154</context>
|
<context context-type="linenumber">156</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4696480417479207939" datatype="html">
|
<trans-unit id="4696480417479207939" datatype="html">
|
||||||
<source>OCR Arguments</source>
|
<source>OCR Arguments</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
<context context-type="linenumber">162</context>
|
<context context-type="linenumber">164</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="7106327322456204362" datatype="html">
|
||||||
|
<source>Application Logo</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
|
<context context-type="linenumber">171</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="2684743776608068095" datatype="html">
|
||||||
|
<source>Application Title</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/data/paperless-config.ts</context>
|
||||||
|
<context context-type="linenumber">178</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5948496158474272829" datatype="html">
|
<trans-unit id="5948496158474272829" datatype="html">
|
||||||
|
@ -110,6 +110,7 @@ import { DocumentLinkComponent } from './components/common/input/document-link/d
|
|||||||
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
import { PreviewPopupComponent } from './components/common/preview-popup/preview-popup.component'
|
||||||
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
import { SwitchComponent } from './components/common/input/switch/switch.component'
|
||||||
import { ConfigComponent } from './components/admin/config/config.component'
|
import { ConfigComponent } from './components/admin/config/config.component'
|
||||||
|
import { FileComponent } from './components/common/input/file/file.component'
|
||||||
|
|
||||||
import localeAf from '@angular/common/locales/af'
|
import localeAf from '@angular/common/locales/af'
|
||||||
import localeAr from '@angular/common/locales/ar'
|
import localeAr from '@angular/common/locales/ar'
|
||||||
@ -267,6 +268,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
PreviewPopupComponent,
|
PreviewPopupComponent,
|
||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
ConfigComponent,
|
ConfigComponent,
|
||||||
|
FileComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [showUnsetNote]="true" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
|
@case (ConfigOptionType.Boolean) { <pngx-input-switch [formControlName]="option.key" [error]="errors[option.key]" [showUnsetNote]="true" [horizontal]="true" title="Enable" i18n-title></pngx-input-switch> }
|
||||||
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
@case (ConfigOptionType.String) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||||
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
@case (ConfigOptionType.JSON) { <pngx-input-text [formControlName]="option.key" [error]="errors[option.key]"></pngx-input-text> }
|
||||||
|
@case (ConfigOptionType.File) { <pngx-input-file [formControlName]="option.key" (upload)="uploadFile($event, option.key)" [error]="errors[option.key]"></pngx-input-file> }
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,12 +15,15 @@ import { SwitchComponent } from '../../common/input/switch/switch.component'
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { SelectComponent } from '../../common/input/select/select.component'
|
import { SelectComponent } from '../../common/input/select/select.component'
|
||||||
|
import { FileComponent } from '../../common/input/file/file.component'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
|
||||||
describe('ConfigComponent', () => {
|
describe('ConfigComponent', () => {
|
||||||
let component: ConfigComponent
|
let component: ConfigComponent
|
||||||
let fixture: ComponentFixture<ConfigComponent>
|
let fixture: ComponentFixture<ConfigComponent>
|
||||||
let configService: ConfigService
|
let configService: ConfigService
|
||||||
let toastService: ToastService
|
let toastService: ToastService
|
||||||
|
let settingService: SettingsService
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@ -30,6 +33,7 @@ describe('ConfigComponent', () => {
|
|||||||
SelectComponent,
|
SelectComponent,
|
||||||
NumberComponent,
|
NumberComponent,
|
||||||
SwitchComponent,
|
SwitchComponent,
|
||||||
|
FileComponent,
|
||||||
PageHeaderComponent,
|
PageHeaderComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
@ -44,6 +48,7 @@ describe('ConfigComponent', () => {
|
|||||||
|
|
||||||
configService = TestBed.inject(ConfigService)
|
configService = TestBed.inject(ConfigService)
|
||||||
toastService = TestBed.inject(ToastService)
|
toastService = TestBed.inject(ToastService)
|
||||||
|
settingService = TestBed.inject(SettingsService)
|
||||||
fixture = TestBed.createComponent(ConfigComponent)
|
fixture = TestBed.createComponent(ConfigComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
@ -100,4 +105,39 @@ describe('ConfigComponent', () => {
|
|||||||
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
component.configForm.patchValue({ user_args: '{ "foo": "bar" }' })
|
||||||
expect(component.errors).toEqual({ user_args: null })
|
expect(component.errors).toEqual({ user_args: null })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should upload file, show error if necessary', () => {
|
||||||
|
const uploadSpy = jest.spyOn(configService, 'uploadFile')
|
||||||
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
uploadSpy.mockReturnValueOnce(
|
||||||
|
throwError(() => new Error('Error uploading file'))
|
||||||
|
)
|
||||||
|
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||||
|
expect(uploadSpy).toHaveBeenCalled()
|
||||||
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
|
uploadSpy.mockReturnValueOnce(
|
||||||
|
of({ app_logo: 'https://example.com/logo/test.png' } as any)
|
||||||
|
)
|
||||||
|
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||||
|
expect(component.initialConfig).toEqual({
|
||||||
|
app_logo: 'https://example.com/logo/test.png',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should refresh ui settings after save or upload', () => {
|
||||||
|
const saveSpy = jest.spyOn(configService, 'saveConfig')
|
||||||
|
const initSpy = jest.spyOn(settingService, 'initializeSettings')
|
||||||
|
saveSpy.mockReturnValueOnce(
|
||||||
|
of({ output_type: OutputTypeConfig.PDF_A } as any)
|
||||||
|
)
|
||||||
|
component.saveConfig()
|
||||||
|
expect(initSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
const uploadSpy = jest.spyOn(configService, 'uploadFile')
|
||||||
|
uploadSpy.mockReturnValueOnce(
|
||||||
|
of({ app_logo: 'https://example.com/logo/test.png' } as any)
|
||||||
|
)
|
||||||
|
component.uploadFile(new File([], 'test.png'), 'app_logo')
|
||||||
|
expect(initSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -19,6 +19,7 @@ import { ConfigService } from 'src/app/services/config.service'
|
|||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-config',
|
selector: 'pngx-config',
|
||||||
@ -55,7 +56,8 @@ export class ConfigComponent
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private toastService: ToastService
|
private toastService: ToastService,
|
||||||
|
private settingsService: SettingsService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.configForm.addControl('id', new FormControl())
|
this.configForm.addControl('id', new FormControl())
|
||||||
@ -145,6 +147,7 @@ export class ConfigComponent
|
|||||||
this.loading = false
|
this.loading = false
|
||||||
this.initialize(config)
|
this.initialize(config)
|
||||||
this.store.next(config)
|
this.store.next(config)
|
||||||
|
this.settingsService.initializeSettings().subscribe()
|
||||||
this.toastService.showInfo($localize`Configuration updated`)
|
this.toastService.showInfo($localize`Configuration updated`)
|
||||||
},
|
},
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
@ -160,4 +163,27 @@ export class ConfigComponent
|
|||||||
public discardChanges() {
|
public discardChanges() {
|
||||||
this.configForm.reset(this.initialConfig)
|
this.configForm.reset(this.initialConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public uploadFile(file: File, key: string) {
|
||||||
|
this.loading = true
|
||||||
|
this.configService
|
||||||
|
.uploadFile(file, this.configForm.value['id'], key)
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier), first())
|
||||||
|
.subscribe({
|
||||||
|
next: (config) => {
|
||||||
|
this.loading = false
|
||||||
|
this.initialize(config)
|
||||||
|
this.store.next(config)
|
||||||
|
this.settingsService.initializeSettings().subscribe()
|
||||||
|
this.toastService.showInfo($localize`File successfully updated`)
|
||||||
|
},
|
||||||
|
error: (e) => {
|
||||||
|
this.loading = false
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`An error occurred uploading file`,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,25 @@
|
|||||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
(click)="isMenuCollapsed = !isMenuCollapsed">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
|
<a class="navbar-brand d-flex col-auto col-md-3 col-lg-2 me-0 px-3 py-3 order-sm-0"
|
||||||
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-auto col-md-3 col-lg-2'" routerLink="/dashboard"
|
[ngClass]="{ 'slim': slimSidebarEnabled, 'd-flex col-auto col-md-3 col-lg-2' : !slimSidebarEnabled, 'py-3' : !customAppTitle?.length || slimSidebarEnabled, 'py-2': customAppTitle?.length }"
|
||||||
|
routerLink="/dashboard"
|
||||||
tourAnchor="tour.intro">
|
tourAnchor="tour.intro">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.43 238.91" width="1em" class="me-2" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
|
d="M194.7,0C164.22,70.94,17.64,79.74,64.55,194.06c.58,1.47-10.85,17-18.47,29.9-1.76-6.45-3.81-13.48-3.52-14.07,38.11-45.14-27.26-70.65-30.78-107.58C-4.64,131.62-10.5,182.92,39,212.53c.3,0,2.64,11.14,3.81,16.71a58.55,58.55,0,0,0-2.93,6.45c-1.17,2.93,7.62,2.64,7.62,3.22.88-.29,21.7-36.93,22.28-37.23C187.67,174.72,208.48,68.6,194.7,0ZM134.61,74.75C79.5,124,70.12,160.64,71.88,178.53,53.41,134.85,107.64,86.77,134.61,74.75ZM28.2,145.11c10.55,9.67,28.14,39.28,13.19,56.57C44.91,193.77,46.08,175.89,28.2,145.11Z"
|
||||||
transform="translate(0 0)" />
|
transform="translate(0 0)" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="ms-2" [class.visually-hidden]="slimSidebarEnabled" i18n="app title">Paperless-ngx</span>
|
<div class="ms-2 d-inline-block" [class.visually-hidden]="slimSidebarEnabled">
|
||||||
|
@if (customAppTitle?.length) {
|
||||||
|
<div class="d-flex flex-column align-items-start">
|
||||||
|
<span class="title">{{customAppTitle}}</span>
|
||||||
|
<span class="byline text-uppercase font-monospace" i18n>by Paperless-ngx</span>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
Paperless-ngx
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
|
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||||
|
@ -217,9 +217,16 @@ main {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.navbar-brand {
|
.navbar-brand {
|
||||||
padding-top: 0.75rem;
|
|
||||||
padding-bottom: 0.75rem;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
||||||
|
.flex-column {
|
||||||
|
padding: 0.15rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.byline {
|
||||||
|
font-size: 0.5rem;
|
||||||
|
letter-spacing: 0.1rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
@media screen and (min-width: 768px) {
|
||||||
|
@ -102,6 +102,10 @@ export class AppFrameComponent
|
|||||||
}, 200) // slightly longer than css animation for slim sidebar
|
}, 200) // slightly longer than css animation for slim sidebar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get customAppTitle(): string {
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
||||||
|
}
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
}
|
}
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" [disabled]="!buttonsEnabled" i18n>
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;" >Cancel</span>
|
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
|
||||||
<span>
|
<span>
|
||||||
|
@ -37,6 +37,12 @@ export class ConfirmDialogComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
alternativeBtnCaption
|
alternativeBtnCaption
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
cancelBtnClass = 'btn-outline-secondary'
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
cancelBtnCaption = $localize`Cancel`
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
buttonsEnabled = true
|
buttonsEnabled = true
|
||||||
|
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
<div class="mb-3" [class.pb-3]="error">
|
||||||
|
<div class="row">
|
||||||
|
<div class="d-flex align-items-center position-relative hidden-button-container" [class.col-md-3]="horizontal">
|
||||||
|
@if (title) {
|
||||||
|
<label class="form-label" [class.mb-md-0]="horizontal" [for]="inputId">{{title}}</label>
|
||||||
|
}
|
||||||
|
@if (removable) {
|
||||||
|
<button type="button" class="btn btn-sm btn-danger position-absolute left-0" (click)="removed.emit(this)">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||||
|
</svg> <ng-container i18n>Remove</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="input-group" [class.col-md-9]="horizontal" [class.is-invalid]="error">
|
||||||
|
<input #fileInput type="file" class="form-control" [id]="inputId" (change)="onFile($event)" [disabled]="disabled">
|
||||||
|
<button class="btn btn-outline-primary py-0" type="button" (click)="uploadClicked()" [disabled]="disabled || !file" i18n>Upload</button>
|
||||||
|
</div>
|
||||||
|
@if (filename) {
|
||||||
|
<div class="form-text d-flex align-items-center">
|
||||||
|
<span class="text-muted">{{filename}}</span>
|
||||||
|
<button type="button" class="btn btn-link btn-sm text-danger ms-2" (click)="clear()">
|
||||||
|
<svg class="sidebaricon" fill="currentColor">
|
||||||
|
<use xlink:href="assets/bootstrap-icons.svg#x"/>
|
||||||
|
</svg><small class="ms-1" i18n>Remove</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<input #inputField type="hidden" class="form-control small" [(ngModel)]="value" [disabled]="true">
|
||||||
|
@if (hint) {
|
||||||
|
<small class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||||
|
}
|
||||||
|
<div class="invalid-feedback position-absolute top-100">
|
||||||
|
{{error}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,41 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { FileComponent } from './file.component'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
|
|
||||||
|
describe('FileComponent', () => {
|
||||||
|
let component: FileComponent
|
||||||
|
let fixture: ComponentFixture<FileComponent>
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [FileComponent],
|
||||||
|
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(FileComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update file on change', () => {
|
||||||
|
const event = { target: { files: [new File([], 'test.png')] } }
|
||||||
|
component.onFile(event as any)
|
||||||
|
expect(component.file.name).toEqual('test.png')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get filename', () => {
|
||||||
|
component.value = 'https://example.com:8000/logo/filename.svg'
|
||||||
|
expect(component.filename).toEqual('filename.svg')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fire upload event', () => {
|
||||||
|
let firedFile
|
||||||
|
component.file = new File([], 'test.png')
|
||||||
|
component.upload.subscribe((file) => (firedFile = file))
|
||||||
|
component.uploadClicked()
|
||||||
|
expect(firedFile.name).toEqual('test.png')
|
||||||
|
expect(component.file).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,53 @@
|
|||||||
|
import {
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Output,
|
||||||
|
ViewChild,
|
||||||
|
forwardRef,
|
||||||
|
} from '@angular/core'
|
||||||
|
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||||
|
import { AbstractInputComponent } from '../abstract-input'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: NG_VALUE_ACCESSOR,
|
||||||
|
useExisting: forwardRef(() => FileComponent),
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
selector: 'pngx-input-file',
|
||||||
|
templateUrl: './file.component.html',
|
||||||
|
styleUrl: './file.component.scss',
|
||||||
|
})
|
||||||
|
export class FileComponent extends AbstractInputComponent<string> {
|
||||||
|
@Output()
|
||||||
|
upload = new EventEmitter<File>()
|
||||||
|
|
||||||
|
public file: File
|
||||||
|
|
||||||
|
@ViewChild('fileInput') fileInput: ElementRef
|
||||||
|
|
||||||
|
get filename(): string {
|
||||||
|
return this.value
|
||||||
|
? this.value.substring(this.value.lastIndexOf('/') + 1)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
onFile(event: Event) {
|
||||||
|
this.file = (event.target as HTMLInputElement).files[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadClicked() {
|
||||||
|
this.upload.emit(this.file)
|
||||||
|
this.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.file = undefined
|
||||||
|
this.fileInput.nativeElement.value = null
|
||||||
|
this.writeValue(null)
|
||||||
|
this.onChange(null)
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
@if (customLogo) {
|
||||||
|
<img src="{{customLogo}}" height="100%" width="100%" [attr.style]="'height:'+height" />
|
||||||
|
} @else {
|
||||||
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
|
<svg [class]="getClasses()" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2897.4 896.6" [attr.style]="'height:'+height">
|
||||||
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
|
<path class="leaf" d="M140,713.7c-3.4-16.4-10.3-49.1-11.2-49.1c-145.7-87.1-128.4-238-80.2-324.2C59,449,251.2,524,139.1,656.8 c-0.9,1.7,5.2,22.4,10.3,41.4c22.4-37.9,56-83.6,54.3-87.9C65.9,273.9,496.9,248.1,586.6,39.4c40.5,201.8-20.7,513.9-367.2,593.2 c-1.7,0.9-62.9,108.6-65.5,109.5c0-1.7-25.9-0.9-22.4-9.5C133.1,727.4,136.6,720.6,140,713.7L140,713.7z M135.7,632.6 c44-50.9-7.8-137.9-38.8-166.4C149.5,556.7,146,609.3,135.7,632.6L135.7,632.6z" transform="translate(0)" style="fill:#17541f"/>
|
||||||
<g class="text" style="fill:#000">
|
<g class="text" style="fill:#000">
|
||||||
@ -16,3 +19,4 @@
|
|||||||
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
|
<path d="M757.6,293.7c-20-10.8-42.6-16.2-67.8-16.2H600c-8.5,39.2-21.1,76.4-37.6,111.3c-9.9,20.8-21.1,40.6-33.6,59.4v207.2h88.9 V521.5h72c25.2,0,47.8-5.4,67.8-16.2s35.7-25.6,47.1-44.2c11.4-18.7,17.1-39.1,17.1-61.3c0.1-22.7-5.6-43.3-17-61.9 C793.3,319.2,777.6,304.5,757.6,293.7z M716.6,434.3c-9.3,8.9-21.6,13.3-36.7,13.3l-62.2,0.4v-92.5l62.2-0.4 c15.1,0,27.3,4.4,36.7,13.3c9.4,8.9,14,19.9,14,32.9C730.6,414.5,726,425.4,716.6,434.3z" transform="translate(0)"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.5 KiB |
@ -2,15 +2,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
|
|||||||
|
|
||||||
import { LogoComponent } from './logo.component'
|
import { LogoComponent } from './logo.component'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
|
||||||
describe('LogoComponent', () => {
|
describe('LogoComponent', () => {
|
||||||
let component: LogoComponent
|
let component: LogoComponent
|
||||||
let fixture: ComponentFixture<LogoComponent>
|
let fixture: ComponentFixture<LogoComponent>
|
||||||
|
let settingsService: SettingsService
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [LogoComponent],
|
declarations: [LogoComponent],
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
})
|
})
|
||||||
|
settingsService = TestBed.inject(SettingsService)
|
||||||
fixture = TestBed.createComponent(LogoComponent)
|
fixture = TestBed.createComponent(LogoComponent)
|
||||||
component = fixture.componentInstance
|
component = fixture.componentInstance
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
@ -33,4 +39,9 @@ describe('LogoComponent', () => {
|
|||||||
'height:10em'
|
'height:10em'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support getting custom logo', () => {
|
||||||
|
settingsService.set(SETTINGS_KEYS.APP_LOGO, '/logo/test.png')
|
||||||
|
expect(component.customLogo).toEqual('http://localhost:8000/logo/test.png')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||||
|
import { SettingsService } from 'src/app/services/settings.service'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-logo',
|
selector: 'pngx-logo',
|
||||||
@ -12,6 +15,17 @@ export class LogoComponent {
|
|||||||
@Input()
|
@Input()
|
||||||
height = '6em'
|
height = '6em'
|
||||||
|
|
||||||
|
get customLogo(): string {
|
||||||
|
return this.settingsService.get(SETTINGS_KEYS.APP_LOGO)?.length
|
||||||
|
? environment.apiBaseUrl.replace(
|
||||||
|
/\/api\/$/,
|
||||||
|
this.settingsService.get(SETTINGS_KEYS.APP_LOGO)
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private settingsService: SettingsService) {}
|
||||||
|
|
||||||
getClasses() {
|
getClasses() {
|
||||||
return ['logo'].concat(this.extra_classes).join(' ')
|
return ['logo'].concat(this.extra_classes).join(' ')
|
||||||
}
|
}
|
||||||
|
@ -5,13 +5,13 @@ import { ComponentWithPermissions } from '../with-permissions/with-permissions.c
|
|||||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import { SavedView } from 'src/app/data/saved-view'
|
import { SavedView } from 'src/app/data/saved-view'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
|
||||||
import {
|
import {
|
||||||
CdkDragDrop,
|
CdkDragDrop,
|
||||||
CdkDragEnd,
|
CdkDragEnd,
|
||||||
CdkDragStart,
|
CdkDragStart,
|
||||||
moveItemInArray,
|
moveItemInArray,
|
||||||
} from '@angular/cdk/drag-drop'
|
} from '@angular/cdk/drag-drop'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-dashboard',
|
selector: 'pngx-dashboard',
|
||||||
@ -35,9 +35,9 @@ export class DashboardComponent extends ComponentWithPermissions {
|
|||||||
|
|
||||||
get subtitle() {
|
get subtitle() {
|
||||||
if (this.settingsService.displayName) {
|
if (this.settingsService.displayName) {
|
||||||
return $localize`Hello ${this.settingsService.displayName}, welcome to Paperless-ngx`
|
return $localize`Hello ${this.settingsService.displayName}, welcome to ${environment.appTitle}`
|
||||||
} else {
|
} else {
|
||||||
return $localize`Welcome to Paperless-ngx`
|
return $localize`Welcome to ${environment.appTitle}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,8 +79,9 @@ const doc: Document = {
|
|||||||
storage_path: 31,
|
storage_path: 31,
|
||||||
tags: [41, 42, 43],
|
tags: [41, 42, 43],
|
||||||
content: 'text content',
|
content: 'text content',
|
||||||
added: new Date(),
|
added: new Date('May 4, 2014 03:24:00'),
|
||||||
created: new Date(),
|
created: new Date('May 4, 2014 03:24:00'),
|
||||||
|
modified: new Date('May 4, 2014 03:24:00'),
|
||||||
archive_serial_number: null,
|
archive_serial_number: null,
|
||||||
original_file_name: 'file.pdf',
|
original_file_name: 'file.pdf',
|
||||||
owner: null,
|
owner: null,
|
||||||
@ -966,6 +967,26 @@ describe('DocumentDetailComponent', () => {
|
|||||||
expect(errorSpy).toHaveBeenCalled()
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should warn when open document does not match doc retrieved from backend on init', () => {
|
||||||
|
const modalSpy = jest.spyOn(modalService, 'open')
|
||||||
|
const openDoc = Object.assign({}, doc)
|
||||||
|
// simulate a document being modified elsewhere and db updated
|
||||||
|
doc.modified = new Date()
|
||||||
|
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
|
||||||
|
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc)
|
||||||
|
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
|
||||||
|
of({
|
||||||
|
count: customFields.length,
|
||||||
|
all: customFields.map((f) => f.id),
|
||||||
|
results: customFields,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
fixture.detectChanges() // calls ngOnInit
|
||||||
|
expect(modalSpy).toHaveBeenCalledWith(ConfirmDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
function initNormally() {
|
function initNormally() {
|
||||||
jest
|
jest
|
||||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||||
|
@ -297,7 +297,23 @@ export class DocumentDetailComponent
|
|||||||
const openDocument = this.openDocumentService.getOpenDocument(
|
const openDocument = this.openDocumentService.getOpenDocument(
|
||||||
this.documentId
|
this.documentId
|
||||||
)
|
)
|
||||||
|
|
||||||
if (openDocument) {
|
if (openDocument) {
|
||||||
|
if (
|
||||||
|
new Date(doc.modified) > new Date(openDocument.modified) &&
|
||||||
|
!this.modalService.hasOpenModals()
|
||||||
|
) {
|
||||||
|
let modal = this.modalService.open(ConfirmDialogComponent, {
|
||||||
|
backdrop: 'static',
|
||||||
|
})
|
||||||
|
modal.componentInstance.title = $localize`Document changes detected`
|
||||||
|
modal.componentInstance.messageBold = $localize`The version of this document in your browser session appears older than the existing version.`
|
||||||
|
modal.componentInstance.message = $localize`Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.`
|
||||||
|
modal.componentInstance.cancelBtnCaption = $localize`Ok`
|
||||||
|
modal.componentInstance.cancelBtnClass = 'btn-primary'
|
||||||
|
modal.componentInstance.btnClass = 'visually-hidden'
|
||||||
|
}
|
||||||
|
|
||||||
if (this.documentForm.dirty) {
|
if (this.documentForm.dirty) {
|
||||||
Object.assign(openDocument, this.documentForm.value)
|
Object.assign(openDocument, this.documentForm.value)
|
||||||
openDocument['owner'] =
|
openDocument['owner'] =
|
||||||
|
@ -2,6 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'
|
|||||||
import { NotFoundComponent } from './not-found.component'
|
import { NotFoundComponent } from './not-found.component'
|
||||||
import { By } from '@angular/platform-browser'
|
import { By } from '@angular/platform-browser'
|
||||||
import { LogoComponent } from '../common/logo/logo.component'
|
import { LogoComponent } from '../common/logo/logo.component'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
|
||||||
describe('NotFoundComponent', () => {
|
describe('NotFoundComponent', () => {
|
||||||
let component: NotFoundComponent
|
let component: NotFoundComponent
|
||||||
@ -10,6 +11,7 @@ describe('NotFoundComponent', () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
declarations: [NotFoundComponent, LogoComponent],
|
declarations: [NotFoundComponent, LogoComponent],
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
fixture = TestBed.createComponent(NotFoundComponent)
|
fixture = TestBed.createComponent(NotFoundComponent)
|
||||||
|
@ -43,9 +43,11 @@ export enum ConfigOptionType {
|
|||||||
Select = 'select',
|
Select = 'select',
|
||||||
Boolean = 'boolean',
|
Boolean = 'boolean',
|
||||||
JSON = 'json',
|
JSON = 'json',
|
||||||
|
File = 'file',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ConfigCategory = {
|
export const ConfigCategory = {
|
||||||
|
General: $localize`General Settings`,
|
||||||
OCR: $localize`OCR Settings`,
|
OCR: $localize`OCR Settings`,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,6 +166,20 @@ export const PaperlessConfigOptions: ConfigOption[] = [
|
|||||||
config_key: 'PAPERLESS_OCR_USER_ARGS',
|
config_key: 'PAPERLESS_OCR_USER_ARGS',
|
||||||
category: ConfigCategory.OCR,
|
category: ConfigCategory.OCR,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'app_logo',
|
||||||
|
title: $localize`Application Logo`,
|
||||||
|
type: ConfigOptionType.File,
|
||||||
|
config_key: 'PAPERLESS_APP_LOGO',
|
||||||
|
category: ConfigCategory.General,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'app_title',
|
||||||
|
title: $localize`Application Title`,
|
||||||
|
type: ConfigOptionType.String,
|
||||||
|
config_key: 'PAPERLESS_APP_TITLE',
|
||||||
|
category: ConfigCategory.General,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export interface PaperlessConfig extends ObjectWithId {
|
export interface PaperlessConfig extends ObjectWithId {
|
||||||
@ -180,4 +196,6 @@ export interface PaperlessConfig extends ObjectWithId {
|
|||||||
max_image_pixels: number
|
max_image_pixels: number
|
||||||
color_conversion_strategy: ColorConvertConfig
|
color_conversion_strategy: ColorConvertConfig
|
||||||
user_args: object
|
user_args: object
|
||||||
|
app_logo: string
|
||||||
|
app_title: string
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ export interface UiSetting {
|
|||||||
|
|
||||||
export const SETTINGS_KEYS = {
|
export const SETTINGS_KEYS = {
|
||||||
LANGUAGE: 'language',
|
LANGUAGE: 'language',
|
||||||
|
APP_LOGO: 'app_logo',
|
||||||
|
APP_TITLE: 'app_title',
|
||||||
// maintain old general-settings: for backwards compatibility
|
// maintain old general-settings: for backwards compatibility
|
||||||
BULK_EDIT_CONFIRMATION_DIALOGS:
|
BULK_EDIT_CONFIRMATION_DIALOGS:
|
||||||
'general-settings:bulk-edit:confirmation-dialogs',
|
'general-settings:bulk-edit:confirmation-dialogs',
|
||||||
@ -194,4 +196,14 @@ export const SETTINGS: UiSetting[] = [
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.APP_LOGO,
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: SETTINGS_KEYS.APP_TITLE,
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
@ -39,4 +39,26 @@ describe('ConfigService', () => {
|
|||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('PATCH')
|
expect(req.request.method).toEqual('PATCH')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should support upload file with form data', () => {
|
||||||
|
service.uploadFile(new File([], 'test.png'), 1, 'app_logo').subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}config/1/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('PATCH')
|
||||||
|
expect(req.request.body).not.toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not pass string app_logo', () => {
|
||||||
|
service
|
||||||
|
.saveConfig({
|
||||||
|
id: 1,
|
||||||
|
app_logo: '/logo/foobar.png',
|
||||||
|
} as PaperlessConfig)
|
||||||
|
.subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}config/1/`
|
||||||
|
)
|
||||||
|
expect(req.request.body).toEqual({ id: 1 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -20,8 +20,22 @@ export class ConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveConfig(config: PaperlessConfig): Observable<PaperlessConfig> {
|
saveConfig(config: PaperlessConfig): Observable<PaperlessConfig> {
|
||||||
|
// dont pass string
|
||||||
|
if (typeof config.app_logo === 'string') delete config.app_logo
|
||||||
return this.http
|
return this.http
|
||||||
.patch<PaperlessConfig>(`${this.baseUrl}${config.id}/`, config)
|
.patch<PaperlessConfig>(`${this.baseUrl}${config.id}/`, config)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadFile(
|
||||||
|
file: File,
|
||||||
|
configID: number,
|
||||||
|
configKey: string
|
||||||
|
): Observable<PaperlessConfig> {
|
||||||
|
let formData = new FormData()
|
||||||
|
formData.append(configKey, file, file.name)
|
||||||
|
return this.http
|
||||||
|
.patch<PaperlessConfig>(`${this.baseUrl}${configID}/`, formData)
|
||||||
|
.pipe(first())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -301,4 +301,16 @@ describe('SettingsService', () => {
|
|||||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||||
.flush(ui_settings)
|
.flush(ui_settings)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should update environment app title if set', () => {
|
||||||
|
const settings = Object.assign({}, ui_settings)
|
||||||
|
settings.settings['app_title'] = 'FooBar'
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}ui_settings/`
|
||||||
|
)
|
||||||
|
req.flush(settings)
|
||||||
|
expect(environment.appTitle).toEqual('FooBar')
|
||||||
|
// post for migrate
|
||||||
|
httpTestingController.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -270,6 +270,9 @@ export class SettingsService {
|
|||||||
first(),
|
first(),
|
||||||
tap((uisettings) => {
|
tap((uisettings) => {
|
||||||
Object.assign(this.settings, uisettings.settings)
|
Object.assign(this.settings, uisettings.settings)
|
||||||
|
if (this.get(SETTINGS_KEYS.APP_TITLE)?.length) {
|
||||||
|
environment.appTitle = this.get(SETTINGS_KEYS.APP_TITLE)
|
||||||
|
}
|
||||||
this.maybeMigrateSettings()
|
this.maybeMigrateSettings()
|
||||||
// to update lang cookie
|
// to update lang cookie
|
||||||
if (this.settings['language']?.length)
|
if (this.settings['language']?.length)
|
||||||
|
@ -11,6 +11,9 @@ $grid-breakpoints: (
|
|||||||
xxxl: 2400px
|
xxxl: 2400px
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$form-file-button-bg: var(--bs-body-bg);
|
||||||
|
$form-file-button-hover-bg: var(--pngx-bg-alt);
|
||||||
|
|
||||||
@import "node_modules/bootstrap/scss/bootstrap";
|
@import "node_modules/bootstrap/scss/bootstrap";
|
||||||
@import "theme";
|
@import "theme";
|
||||||
@import "~@ng-select/ng-select/themes/default.theme.css";
|
@import "~@ng-select/ng-select/themes/default.theme.css";
|
||||||
|
@ -3,7 +3,6 @@ import re
|
|||||||
import tempfile
|
import tempfile
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Final
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -15,8 +14,9 @@ from PIL import Image
|
|||||||
|
|
||||||
from documents.converters import convert_from_tiff_to_pdf
|
from documents.converters import convert_from_tiff_to_pdf
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.plugins.base import ConsumeTaskPlugin
|
||||||
from documents.data_models import DocumentSource
|
from documents.plugins.base import StopConsumeTaskError
|
||||||
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
from documents.utils import copy_basic_file_stats
|
from documents.utils import copy_basic_file_stats
|
||||||
from documents.utils import copy_file_with_basic_stats
|
from documents.utils import copy_file_with_basic_stats
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ logger = logging.getLogger("paperless.barcodes")
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Barcode:
|
class Barcode:
|
||||||
"""
|
"""
|
||||||
Holds the information about a single barcode and its location
|
Holds the information about a single barcode and its location in a document
|
||||||
"""
|
"""
|
||||||
|
|
||||||
page: int
|
page: int
|
||||||
@ -49,77 +49,111 @@ class Barcode:
|
|||||||
return self.value.startswith(settings.CONSUMER_ASN_BARCODE_PREFIX)
|
return self.value.startswith(settings.CONSUMER_ASN_BARCODE_PREFIX)
|
||||||
|
|
||||||
|
|
||||||
class BarcodeReader:
|
class BarcodePlugin(ConsumeTaskPlugin):
|
||||||
def __init__(self, filepath: Path, mime_type: str) -> None:
|
NAME: str = "BarcodePlugin"
|
||||||
self.file: Final[Path] = filepath
|
|
||||||
self.mime: Final[str] = mime_type
|
|
||||||
self.pdf_file: Path = self.file
|
|
||||||
self.barcodes: list[Barcode] = []
|
|
||||||
self._tiff_conversion_done = False
|
|
||||||
self.temp_dir: Optional[tempfile.TemporaryDirectory] = None
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def able_to_run(self) -> bool:
|
||||||
|
"""
|
||||||
|
Able to run if:
|
||||||
|
- ASN from barcode detection is enabled or
|
||||||
|
- Barcode support is enabled and the mime type is supported
|
||||||
|
"""
|
||||||
if settings.CONSUMER_BARCODE_TIFF_SUPPORT:
|
if settings.CONSUMER_BARCODE_TIFF_SUPPORT:
|
||||||
self.SUPPORTED_FILE_MIMES = {"application/pdf", "image/tiff"}
|
supported_mimes = {"application/pdf", "image/tiff"}
|
||||||
else:
|
else:
|
||||||
self.SUPPORTED_FILE_MIMES = {"application/pdf"}
|
supported_mimes = {"application/pdf"}
|
||||||
|
|
||||||
def __enter__(self):
|
return (
|
||||||
if self.supported_mime_type:
|
settings.CONSUMER_ENABLE_ASN_BARCODE or settings.CONSUMER_ENABLE_BARCODES
|
||||||
self.temp_dir = tempfile.TemporaryDirectory(prefix="paperless-barcodes")
|
) and self.input_doc.mime_type in supported_mimes
|
||||||
return self
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
def setup(self):
|
||||||
if self.temp_dir is not None:
|
self.temp_dir = tempfile.TemporaryDirectory(
|
||||||
self.temp_dir.cleanup()
|
dir=self.base_tmp_dir,
|
||||||
self.temp_dir = None
|
prefix="barcode",
|
||||||
|
)
|
||||||
|
self.pdf_file = self.input_doc.original_file
|
||||||
|
self._tiff_conversion_done = False
|
||||||
|
self.barcodes: list[Barcode] = []
|
||||||
|
|
||||||
@property
|
def run(self) -> Optional[str]:
|
||||||
def supported_mime_type(self) -> bool:
|
# Maybe do the conversion of TIFF to PDF
|
||||||
"""
|
self.convert_from_tiff_to_pdf()
|
||||||
Return True if the given mime type is supported for barcodes, false otherwise
|
|
||||||
"""
|
|
||||||
return self.mime in self.SUPPORTED_FILE_MIMES
|
|
||||||
|
|
||||||
@property
|
# Locate any barcodes in the files
|
||||||
def asn(self) -> Optional[int]:
|
|
||||||
"""
|
|
||||||
Search the parsed barcodes for any ASNs.
|
|
||||||
The first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX
|
|
||||||
is considered the ASN to be used.
|
|
||||||
Returns the detected ASN (or None)
|
|
||||||
"""
|
|
||||||
asn = None
|
|
||||||
|
|
||||||
if not self.supported_mime_type:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Ensure the barcodes have been read
|
|
||||||
self.detect()
|
self.detect()
|
||||||
|
|
||||||
# get the first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX
|
# Update/overwrite an ASN if possible
|
||||||
asn_text = next(
|
located_asn = self.asn
|
||||||
(x.value for x in self.barcodes if x.is_asn),
|
if located_asn is not None:
|
||||||
None,
|
logger.info(f"Found ASN in barcode: {located_asn}")
|
||||||
|
self.metadata.asn = located_asn
|
||||||
|
|
||||||
|
separator_pages = self.get_separation_pages()
|
||||||
|
if not separator_pages:
|
||||||
|
return "No pages to split on!"
|
||||||
|
|
||||||
|
# We have pages to split against
|
||||||
|
|
||||||
|
# Note this does NOT use the base_temp_dir, as that will be removed
|
||||||
|
tmp_dir = Path(
|
||||||
|
tempfile.mkdtemp(
|
||||||
|
dir=settings.SCRATCH_DIR,
|
||||||
|
prefix="paperless-barcode-split-",
|
||||||
|
),
|
||||||
|
).resolve()
|
||||||
|
|
||||||
|
from documents import tasks
|
||||||
|
|
||||||
|
# Create the split document tasks
|
||||||
|
for new_document in self.separate_pages(separator_pages):
|
||||||
|
copy_file_with_basic_stats(new_document, tmp_dir / new_document.name)
|
||||||
|
|
||||||
|
task = tasks.consume_file.delay(
|
||||||
|
ConsumableDocument(
|
||||||
|
# Same source, for templates
|
||||||
|
source=self.input_doc.source,
|
||||||
|
mailrule_id=self.input_doc.mailrule_id,
|
||||||
|
# Can't use same folder or the consume might grab it again
|
||||||
|
original_file=(tmp_dir / new_document.name).resolve(),
|
||||||
|
),
|
||||||
|
# All the same metadata
|
||||||
|
self.metadata,
|
||||||
)
|
)
|
||||||
|
logger.info(f"Created new task {task.id} for {new_document.name}")
|
||||||
|
|
||||||
if asn_text:
|
# This file is now two or more files
|
||||||
logger.debug(f"Found ASN Barcode: {asn_text}")
|
self.input_doc.original_file.unlink()
|
||||||
# remove the prefix and remove whitespace
|
|
||||||
asn_text = asn_text[len(settings.CONSUMER_ASN_BARCODE_PREFIX) :].strip()
|
|
||||||
|
|
||||||
# remove non-numeric parts of the remaining string
|
msg = "Barcode splitting complete!"
|
||||||
asn_text = re.sub(r"\D", "", asn_text)
|
|
||||||
|
|
||||||
# now, try parsing the ASN number
|
# Update the progress to complete
|
||||||
try:
|
self.status_mgr.send_progress(ProgressStatusOptions.SUCCESS, msg, 100, 100)
|
||||||
asn = int(asn_text)
|
|
||||||
except ValueError as e:
|
|
||||||
logger.warning(f"Failed to parse ASN number because: {e}")
|
|
||||||
|
|
||||||
return asn
|
# Request the consume task stops
|
||||||
|
raise StopConsumeTaskError(msg)
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
self.temp_dir.cleanup()
|
||||||
|
|
||||||
|
def convert_from_tiff_to_pdf(self):
|
||||||
|
"""
|
||||||
|
May convert a TIFF image into a PDF, if the input is a TIFF and
|
||||||
|
the TIFF has not been made into a PDF
|
||||||
|
"""
|
||||||
|
# Nothing to do, pdf_file is already assigned correctly
|
||||||
|
if self.input_doc.mime_type != "image/tiff" or self._tiff_conversion_done:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.pdf_file = convert_from_tiff_to_pdf(
|
||||||
|
self.input_doc.original_file,
|
||||||
|
Path(self.temp_dir.name),
|
||||||
|
)
|
||||||
|
self._tiff_conversion_done = True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def read_barcodes_zxing(image: Image) -> list[str]:
|
def read_barcodes_zxing(image: Image.Image) -> list[str]:
|
||||||
barcodes = []
|
barcodes = []
|
||||||
|
|
||||||
import zxingcpp
|
import zxingcpp
|
||||||
@ -135,7 +169,7 @@ class BarcodeReader:
|
|||||||
return barcodes
|
return barcodes
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def read_barcodes_pyzbar(image: Image) -> list[str]:
|
def read_barcodes_pyzbar(image: Image.Image) -> list[str]:
|
||||||
barcodes = []
|
barcodes = []
|
||||||
|
|
||||||
from pyzbar import pyzbar
|
from pyzbar import pyzbar
|
||||||
@ -154,18 +188,6 @@ class BarcodeReader:
|
|||||||
|
|
||||||
return barcodes
|
return barcodes
|
||||||
|
|
||||||
def convert_from_tiff_to_pdf(self):
|
|
||||||
"""
|
|
||||||
May convert a TIFF image into a PDF, if the input is a TIFF and
|
|
||||||
the TIFF has not been made into a PDF
|
|
||||||
"""
|
|
||||||
# Nothing to do, pdf_file is already assigned correctly
|
|
||||||
if self.mime != "image/tiff" or self._tiff_conversion_done:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._tiff_conversion_done = True
|
|
||||||
self.pdf_file = convert_from_tiff_to_pdf(self.file, Path(self.temp_dir.name))
|
|
||||||
|
|
||||||
def detect(self) -> None:
|
def detect(self) -> None:
|
||||||
"""
|
"""
|
||||||
Scan all pages of the PDF as images, updating barcodes and the pages
|
Scan all pages of the PDF as images, updating barcodes and the pages
|
||||||
@ -218,10 +240,45 @@ class BarcodeReader:
|
|||||||
# This file is really borked, allow the consumption to continue
|
# This file is really borked, allow the consumption to continue
|
||||||
# but it may fail further on
|
# but it may fail further on
|
||||||
except Exception as e: # pragma: no cover
|
except Exception as e: # pragma: no cover
|
||||||
logger.exception(
|
logger.warning(
|
||||||
f"Exception during barcode scanning: {e}",
|
f"Exception during barcode scanning: {e}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def asn(self) -> Optional[int]:
|
||||||
|
"""
|
||||||
|
Search the parsed barcodes for any ASNs.
|
||||||
|
The first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX
|
||||||
|
is considered the ASN to be used.
|
||||||
|
Returns the detected ASN (or None)
|
||||||
|
"""
|
||||||
|
asn = None
|
||||||
|
|
||||||
|
# Ensure the barcodes have been read
|
||||||
|
self.detect()
|
||||||
|
|
||||||
|
# get the first barcode that starts with CONSUMER_ASN_BARCODE_PREFIX
|
||||||
|
asn_text = next(
|
||||||
|
(x.value for x in self.barcodes if x.is_asn),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if asn_text:
|
||||||
|
logger.debug(f"Found ASN Barcode: {asn_text}")
|
||||||
|
# remove the prefix and remove whitespace
|
||||||
|
asn_text = asn_text[len(settings.CONSUMER_ASN_BARCODE_PREFIX) :].strip()
|
||||||
|
|
||||||
|
# remove non-numeric parts of the remaining string
|
||||||
|
asn_text = re.sub(r"\D", "", asn_text)
|
||||||
|
|
||||||
|
# now, try parsing the ASN number
|
||||||
|
try:
|
||||||
|
asn = int(asn_text)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"Failed to parse ASN number because: {e}")
|
||||||
|
|
||||||
|
return asn
|
||||||
|
|
||||||
def get_separation_pages(self) -> dict[int, bool]:
|
def get_separation_pages(self) -> dict[int, bool]:
|
||||||
"""
|
"""
|
||||||
Search the parsed barcodes for separators and returns a dict of page
|
Search the parsed barcodes for separators and returns a dict of page
|
||||||
@ -251,7 +308,7 @@ class BarcodeReader:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
document_paths = []
|
document_paths = []
|
||||||
fname = self.file.stem
|
fname = self.input_doc.original_file.stem
|
||||||
with Pdf.open(self.pdf_file) as input_pdf:
|
with Pdf.open(self.pdf_file) as input_pdf:
|
||||||
# Start with an empty document
|
# Start with an empty document
|
||||||
current_document: list[Page] = []
|
current_document: list[Page] = []
|
||||||
@ -292,58 +349,8 @@ class BarcodeReader:
|
|||||||
with open(savepath, "wb") as out:
|
with open(savepath, "wb") as out:
|
||||||
dst.save(out)
|
dst.save(out)
|
||||||
|
|
||||||
copy_basic_file_stats(self.file, savepath)
|
copy_basic_file_stats(self.input_doc.original_file, savepath)
|
||||||
|
|
||||||
document_paths.append(savepath)
|
document_paths.append(savepath)
|
||||||
|
|
||||||
return document_paths
|
return document_paths
|
||||||
|
|
||||||
def separate(
|
|
||||||
self,
|
|
||||||
source: DocumentSource,
|
|
||||||
overrides: DocumentMetadataOverrides,
|
|
||||||
) -> bool:
|
|
||||||
"""
|
|
||||||
Separates the document, based on barcodes and configuration, creating new
|
|
||||||
documents as required in the appropriate location.
|
|
||||||
|
|
||||||
Returns True if a split happened, False otherwise
|
|
||||||
"""
|
|
||||||
# Do nothing
|
|
||||||
if not self.supported_mime_type:
|
|
||||||
logger.warning(f"Unsupported file format for barcode reader: {self.mime}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does nothing unless needed
|
|
||||||
self.convert_from_tiff_to_pdf()
|
|
||||||
|
|
||||||
# Actually read the codes, if any
|
|
||||||
self.detect()
|
|
||||||
|
|
||||||
separator_pages = self.get_separation_pages()
|
|
||||||
|
|
||||||
# Also do nothing
|
|
||||||
if not separator_pages:
|
|
||||||
logger.warning("No pages to split on!")
|
|
||||||
return False
|
|
||||||
|
|
||||||
tmp_dir = Path(tempfile.mkdtemp(prefix="paperless-barcode-split-")).resolve()
|
|
||||||
|
|
||||||
from documents import tasks
|
|
||||||
|
|
||||||
# Create the split document tasks
|
|
||||||
for new_document in self.separate_pages(separator_pages):
|
|
||||||
copy_file_with_basic_stats(new_document, tmp_dir / new_document.name)
|
|
||||||
|
|
||||||
tasks.consume_file.delay(
|
|
||||||
ConsumableDocument(
|
|
||||||
# Same source, for templates
|
|
||||||
source=source,
|
|
||||||
# Can't use same folder or the consume might grab it again
|
|
||||||
original_file=(tmp_dir / new_document.name).resolve(),
|
|
||||||
),
|
|
||||||
# All the same metadata
|
|
||||||
overrides,
|
|
||||||
)
|
|
||||||
logger.info("Barcode splitting complete!")
|
|
||||||
return True
|
|
||||||
|
@ -21,7 +21,6 @@ from filelock import FileLock
|
|||||||
from rest_framework.reverse import reverse
|
from rest_framework.reverse import reverse
|
||||||
|
|
||||||
from documents.classifier import load_classifier
|
from documents.classifier import load_classifier
|
||||||
from documents.data_models import ConsumableDocument
|
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.file_handling import generate_unique_filename
|
from documents.file_handling import generate_unique_filename
|
||||||
@ -42,12 +41,83 @@ from documents.parsers import ParseError
|
|||||||
from documents.parsers import get_parser_class_for_mime_type
|
from documents.parsers import get_parser_class_for_mime_type
|
||||||
from documents.parsers import parse_date
|
from documents.parsers import parse_date
|
||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_object
|
||||||
|
from documents.plugins.base import AlwaysRunPluginMixin
|
||||||
|
from documents.plugins.base import ConsumeTaskPlugin
|
||||||
|
from documents.plugins.base import NoCleanupPluginMixin
|
||||||
|
from documents.plugins.base import NoSetupPluginMixin
|
||||||
from documents.signals import document_consumption_finished
|
from documents.signals import document_consumption_finished
|
||||||
from documents.signals import document_consumption_started
|
from documents.signals import document_consumption_started
|
||||||
from documents.utils import copy_basic_file_stats
|
from documents.utils import copy_basic_file_stats
|
||||||
from documents.utils import copy_file_with_basic_stats
|
from documents.utils import copy_file_with_basic_stats
|
||||||
|
|
||||||
|
|
||||||
|
class WorkflowTriggerPlugin(
|
||||||
|
NoCleanupPluginMixin,
|
||||||
|
NoSetupPluginMixin,
|
||||||
|
AlwaysRunPluginMixin,
|
||||||
|
ConsumeTaskPlugin,
|
||||||
|
):
|
||||||
|
NAME: str = "WorkflowTriggerPlugin"
|
||||||
|
|
||||||
|
def run(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Get overrides from matching workflows
|
||||||
|
"""
|
||||||
|
overrides = DocumentMetadataOverrides()
|
||||||
|
for workflow in Workflow.objects.filter(enabled=True).order_by("order"):
|
||||||
|
template_overrides = DocumentMetadataOverrides()
|
||||||
|
|
||||||
|
if document_matches_workflow(
|
||||||
|
self.input_doc,
|
||||||
|
workflow,
|
||||||
|
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
|
):
|
||||||
|
for action in workflow.actions.all():
|
||||||
|
if action.assign_title is not None:
|
||||||
|
template_overrides.title = action.assign_title
|
||||||
|
if action.assign_tags is not None:
|
||||||
|
template_overrides.tag_ids = [
|
||||||
|
tag.pk for tag in action.assign_tags.all()
|
||||||
|
]
|
||||||
|
if action.assign_correspondent is not None:
|
||||||
|
template_overrides.correspondent_id = (
|
||||||
|
action.assign_correspondent.pk
|
||||||
|
)
|
||||||
|
if action.assign_document_type is not None:
|
||||||
|
template_overrides.document_type_id = (
|
||||||
|
action.assign_document_type.pk
|
||||||
|
)
|
||||||
|
if action.assign_storage_path is not None:
|
||||||
|
template_overrides.storage_path_id = (
|
||||||
|
action.assign_storage_path.pk
|
||||||
|
)
|
||||||
|
if action.assign_owner is not None:
|
||||||
|
template_overrides.owner_id = action.assign_owner.pk
|
||||||
|
if action.assign_view_users is not None:
|
||||||
|
template_overrides.view_users = [
|
||||||
|
user.pk for user in action.assign_view_users.all()
|
||||||
|
]
|
||||||
|
if action.assign_view_groups is not None:
|
||||||
|
template_overrides.view_groups = [
|
||||||
|
group.pk for group in action.assign_view_groups.all()
|
||||||
|
]
|
||||||
|
if action.assign_change_users is not None:
|
||||||
|
template_overrides.change_users = [
|
||||||
|
user.pk for user in action.assign_change_users.all()
|
||||||
|
]
|
||||||
|
if action.assign_change_groups is not None:
|
||||||
|
template_overrides.change_groups = [
|
||||||
|
group.pk for group in action.assign_change_groups.all()
|
||||||
|
]
|
||||||
|
if action.assign_custom_fields is not None:
|
||||||
|
template_overrides.custom_field_ids = [
|
||||||
|
field.pk for field in action.assign_custom_fields.all()
|
||||||
|
]
|
||||||
|
|
||||||
|
overrides.update(template_overrides)
|
||||||
|
self.metadata.update(overrides)
|
||||||
|
|
||||||
|
|
||||||
class ConsumerError(Exception):
|
class ConsumerError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -602,70 +672,6 @@ class Consumer(LoggingMixin):
|
|||||||
|
|
||||||
return document
|
return document
|
||||||
|
|
||||||
def get_workflow_overrides(
|
|
||||||
self,
|
|
||||||
input_doc: ConsumableDocument,
|
|
||||||
) -> DocumentMetadataOverrides:
|
|
||||||
"""
|
|
||||||
Get overrides from matching workflows
|
|
||||||
"""
|
|
||||||
overrides = DocumentMetadataOverrides()
|
|
||||||
for workflow in Workflow.objects.filter(enabled=True).order_by("order"):
|
|
||||||
template_overrides = DocumentMetadataOverrides()
|
|
||||||
|
|
||||||
if document_matches_workflow(
|
|
||||||
input_doc,
|
|
||||||
workflow,
|
|
||||||
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
|
||||||
):
|
|
||||||
for action in workflow.actions.all():
|
|
||||||
self.log.info(
|
|
||||||
f"Applying overrides in {action} from {workflow}",
|
|
||||||
)
|
|
||||||
if action.assign_title is not None:
|
|
||||||
template_overrides.title = action.assign_title
|
|
||||||
if action.assign_tags is not None:
|
|
||||||
template_overrides.tag_ids = [
|
|
||||||
tag.pk for tag in action.assign_tags.all()
|
|
||||||
]
|
|
||||||
if action.assign_correspondent is not None:
|
|
||||||
template_overrides.correspondent_id = (
|
|
||||||
action.assign_correspondent.pk
|
|
||||||
)
|
|
||||||
if action.assign_document_type is not None:
|
|
||||||
template_overrides.document_type_id = (
|
|
||||||
action.assign_document_type.pk
|
|
||||||
)
|
|
||||||
if action.assign_storage_path is not None:
|
|
||||||
template_overrides.storage_path_id = (
|
|
||||||
action.assign_storage_path.pk
|
|
||||||
)
|
|
||||||
if action.assign_owner is not None:
|
|
||||||
template_overrides.owner_id = action.assign_owner.pk
|
|
||||||
if action.assign_view_users is not None:
|
|
||||||
template_overrides.view_users = [
|
|
||||||
user.pk for user in action.assign_view_users.all()
|
|
||||||
]
|
|
||||||
if action.assign_view_groups is not None:
|
|
||||||
template_overrides.view_groups = [
|
|
||||||
group.pk for group in action.assign_view_groups.all()
|
|
||||||
]
|
|
||||||
if action.assign_change_users is not None:
|
|
||||||
template_overrides.change_users = [
|
|
||||||
user.pk for user in action.assign_change_users.all()
|
|
||||||
]
|
|
||||||
if action.assign_change_groups is not None:
|
|
||||||
template_overrides.change_groups = [
|
|
||||||
group.pk for group in action.assign_change_groups.all()
|
|
||||||
]
|
|
||||||
if action.assign_custom_fields is not None:
|
|
||||||
template_overrides.custom_field_ids = [
|
|
||||||
field.pk for field in action.assign_custom_fields.all()
|
|
||||||
]
|
|
||||||
|
|
||||||
overrides.update(template_overrides)
|
|
||||||
return overrides
|
|
||||||
|
|
||||||
def _parse_title_placeholders(self, title: str) -> str:
|
def _parse_title_placeholders(self, title: str) -> str:
|
||||||
local_added = timezone.localtime(timezone.now())
|
local_added = timezone.localtime(timezone.now())
|
||||||
|
|
||||||
|
@ -3,24 +3,41 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from pikepdf import Pdf
|
from pikepdf import Pdf
|
||||||
|
|
||||||
from documents.consumer import ConsumerError
|
from documents.consumer import ConsumerError
|
||||||
from documents.converters import convert_from_tiff_to_pdf
|
from documents.converters import convert_from_tiff_to_pdf
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.plugins.base import ConsumeTaskPlugin
|
||||||
|
from documents.plugins.base import NoCleanupPluginMixin
|
||||||
|
from documents.plugins.base import NoSetupPluginMixin
|
||||||
|
from documents.plugins.base import StopConsumeTaskError
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.double_sided")
|
logger = logging.getLogger("paperless.double_sided")
|
||||||
|
|
||||||
# Hardcoded for now, could be made a configurable setting if needed
|
# Hardcoded for now, could be made a configurable setting if needed
|
||||||
TIMEOUT_MINUTES = 30
|
TIMEOUT_MINUTES: Final[int] = 30
|
||||||
|
TIMEOUT_SECONDS: Final[int] = TIMEOUT_MINUTES * 60
|
||||||
|
|
||||||
# Used by test cases
|
# Used by test cases
|
||||||
STAGING_FILE_NAME = "double-sided-staging.pdf"
|
STAGING_FILE_NAME = "double-sided-staging.pdf"
|
||||||
|
|
||||||
|
|
||||||
def collate(input_doc: ConsumableDocument) -> str:
|
class CollatePlugin(NoCleanupPluginMixin, NoSetupPluginMixin, ConsumeTaskPlugin):
|
||||||
|
NAME: str = "CollatePlugin"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def able_to_run(self) -> bool:
|
||||||
|
return (
|
||||||
|
settings.CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED
|
||||||
|
and settings.CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME
|
||||||
|
in self.input_doc.original_file.parts
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Tries to collate pages from 2 single sided scans of a double sided
|
Tries to collate pages from 2 single sided scans of a double sided
|
||||||
document.
|
document.
|
||||||
@ -39,33 +56,32 @@ def collate(input_doc: ConsumableDocument) -> str:
|
|||||||
in case of failure.
|
in case of failure.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Make sure scratch dir exists, Consumer might not have run yet
|
if self.input_doc.mime_type == "application/pdf":
|
||||||
settings.SCRATCH_DIR.mkdir(exist_ok=True)
|
pdf_file = self.input_doc.original_file
|
||||||
|
|
||||||
if input_doc.mime_type == "application/pdf":
|
|
||||||
pdf_file = input_doc.original_file
|
|
||||||
elif (
|
elif (
|
||||||
input_doc.mime_type == "image/tiff"
|
self.input_doc.mime_type == "image/tiff"
|
||||||
and settings.CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT
|
and settings.CONSUMER_COLLATE_DOUBLE_SIDED_TIFF_SUPPORT
|
||||||
):
|
):
|
||||||
pdf_file = convert_from_tiff_to_pdf(
|
pdf_file = convert_from_tiff_to_pdf(
|
||||||
input_doc.original_file,
|
self.input_doc.original_file,
|
||||||
settings.SCRATCH_DIR,
|
self.base_tmp_dir,
|
||||||
)
|
)
|
||||||
input_doc.original_file.unlink()
|
self.input_doc.original_file.unlink()
|
||||||
else:
|
else:
|
||||||
raise ConsumerError("Unsupported file type for collation of double-sided scans")
|
raise ConsumerError(
|
||||||
|
"Unsupported file type for collation of double-sided scans",
|
||||||
|
)
|
||||||
|
|
||||||
staging = settings.SCRATCH_DIR / STAGING_FILE_NAME
|
staging: Path = settings.SCRATCH_DIR / STAGING_FILE_NAME
|
||||||
|
|
||||||
valid_staging_exists = False
|
valid_staging_exists = False
|
||||||
if staging.exists():
|
if staging.exists():
|
||||||
stats = os.stat(str(staging))
|
stats = staging.stat()
|
||||||
# if the file is older than the timeout, we don't consider
|
# if the file is older than the timeout, we don't consider
|
||||||
# it valid
|
# it valid
|
||||||
if dt.datetime.now().timestamp() - stats.st_mtime > TIMEOUT_MINUTES * 60:
|
if (dt.datetime.now().timestamp() - stats.st_mtime) > TIMEOUT_SECONDS:
|
||||||
logger.warning("Outdated double sided staging file exists, deleting it")
|
logger.warning("Outdated double sided staging file exists, deleting it")
|
||||||
os.unlink(str(staging))
|
staging.unlink()
|
||||||
else:
|
else:
|
||||||
valid_staging_exists = True
|
valid_staging_exists = True
|
||||||
|
|
||||||
@ -88,23 +104,24 @@ def collate(input_doc: ConsumableDocument) -> str:
|
|||||||
# Merged file has the same path, but without the
|
# Merged file has the same path, but without the
|
||||||
# double-sided subdir. Therefore, it is also in the
|
# double-sided subdir. Therefore, it is also in the
|
||||||
# consumption dir and will be picked up for processing
|
# consumption dir and will be picked up for processing
|
||||||
old_file = input_doc.original_file
|
old_file = self.input_doc.original_file
|
||||||
new_file = Path(
|
new_file = Path(
|
||||||
*(
|
*(
|
||||||
part
|
part
|
||||||
for part in old_file.with_name(
|
for part in old_file.with_name(
|
||||||
f"{old_file.stem}-collated.pdf",
|
f"{old_file.stem}-collated.pdf",
|
||||||
).parts
|
).parts
|
||||||
if part != settings.CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME
|
if part
|
||||||
|
!= settings.CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# If the user didn't create the subdirs yet, do it for them
|
# If the user didn't create the subdirs yet, do it for them
|
||||||
new_file.parent.mkdir(parents=True, exist_ok=True)
|
new_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
pdf1.save(new_file)
|
pdf1.save(new_file)
|
||||||
logger.info("Collated documents into new file %s", new_file)
|
logger.info("Collated documents into new file %s", new_file)
|
||||||
return (
|
raise StopConsumeTaskError(
|
||||||
"Success. Even numbered pages of double sided scan collated "
|
"Success. Even numbered pages of double sided scan collated "
|
||||||
"with odd pages"
|
"with odd pages",
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
# Delete staging and recently uploaded file no matter what.
|
# Delete staging and recently uploaded file no matter what.
|
||||||
@ -118,12 +135,13 @@ def collate(input_doc: ConsumableDocument) -> str:
|
|||||||
shutil.move(pdf_file, staging)
|
shutil.move(pdf_file, staging)
|
||||||
# update access to modification time so we know if the file
|
# update access to modification time so we know if the file
|
||||||
# is outdated when another file gets uploaded
|
# is outdated when another file gets uploaded
|
||||||
os.utime(staging, (dt.datetime.now().timestamp(),) * 2)
|
timestamp = dt.datetime.now().timestamp()
|
||||||
|
os.utime(staging, (timestamp, timestamp))
|
||||||
logger.info(
|
logger.info(
|
||||||
"Got scan with odd numbered pages of double-sided scan, moved it to %s",
|
"Got scan with odd numbered pages of double-sided scan, moved it to %s",
|
||||||
staging,
|
staging,
|
||||||
)
|
)
|
||||||
return (
|
raise StopConsumeTaskError(
|
||||||
"Received odd numbered pages of double sided scan, waiting up to "
|
"Received odd numbered pages of double sided scan, waiting up to "
|
||||||
f"{TIMEOUT_MINUTES} minutes for even numbered pages"
|
f"{TIMEOUT_MINUTES} minutes for even numbered pages",
|
||||||
)
|
)
|
||||||
|
0
src/documents/plugins/__init__.py
Normal file
0
src/documents/plugins/__init__.py
Normal file
131
src/documents/plugins/base.py
Normal file
131
src/documents/plugins/base.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import abc
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Final
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from documents.data_models import ConsumableDocument
|
||||||
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
|
from documents.plugins.helpers import ProgressManager
|
||||||
|
|
||||||
|
|
||||||
|
class StopConsumeTaskError(Exception):
|
||||||
|
"""
|
||||||
|
A plugin setup or run may raise this to exit the asynchronous consume task.
|
||||||
|
|
||||||
|
Most likely, this means it has created one or more new tasks to execute instead,
|
||||||
|
such as when a barcode has been used to create new documents
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, message: str) -> None:
|
||||||
|
self.message = message
|
||||||
|
super().__init__(message)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsumeTaskPlugin(abc.ABC):
|
||||||
|
"""
|
||||||
|
Defines the interface for a plugin for the document consume task
|
||||||
|
Meanings as per RFC2119 (https://datatracker.ietf.org/doc/html/rfc2119)
|
||||||
|
|
||||||
|
Plugin Implementation
|
||||||
|
|
||||||
|
The plugin SHALL implement property able_to_run and methods setup, run and cleanup.
|
||||||
|
The plugin property able_to_run SHALL return True if the plugin is able to run, given the conditions, settings and document information.
|
||||||
|
The plugin property able_to_run MAY be hardcoded to return True.
|
||||||
|
The plugin setup SHOULD perform any resource creation or additional initialization needed to run the document.
|
||||||
|
The plugin setup MAY be a non-operation.
|
||||||
|
The plugin cleanup SHOULD perform resource cleanup, including in the event of an error.
|
||||||
|
The plugin cleanup MAY be a non-operation.
|
||||||
|
The plugin run SHALL perform any operations against the document or system state required for the plugin.
|
||||||
|
The plugin run MAY update the document metadata.
|
||||||
|
The plugin run MAY return an informational message.
|
||||||
|
The plugin run MAY raise StopConsumeTaskError to cease any further operations against the document.
|
||||||
|
|
||||||
|
Plugin Manager Implementation
|
||||||
|
|
||||||
|
The plugin manager SHALL provide the plugin with the input document, document metadata, progress manager and a created temporary directory.
|
||||||
|
The plugin manager SHALL execute the plugin setup, run and cleanup, in that order IF the plugin property able_to_run is True.
|
||||||
|
The plugin manager SHOULD log the return message of executing a plugin's run.
|
||||||
|
The plugin manager SHALL always execute the plugin cleanup, IF the plugin property able_to_run is True.
|
||||||
|
The plugin manager SHALL cease calling plugins and exit the task IF a plugin raises StopConsumeTaskError.
|
||||||
|
The plugin manager SHOULD return the StopConsumeTaskError message IF a plugin raises StopConsumeTaskError.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NAME: str = "ConsumeTaskPlugin"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
input_doc: ConsumableDocument,
|
||||||
|
metadata: DocumentMetadataOverrides,
|
||||||
|
status_mgr: ProgressManager,
|
||||||
|
base_tmp_dir: Path,
|
||||||
|
task_id: str,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.input_doc = input_doc
|
||||||
|
self.metadata = metadata
|
||||||
|
self.base_tmp_dir: Final = base_tmp_dir
|
||||||
|
self.status_mgr = status_mgr
|
||||||
|
self.task_id: Final = task_id
|
||||||
|
|
||||||
|
@abc.abstractproperty
|
||||||
|
def able_to_run(self) -> bool:
|
||||||
|
"""
|
||||||
|
Return True if the conditions are met for the plugin to run, False otherwise
|
||||||
|
|
||||||
|
If False, setup(), run() and cleanup() will not be called
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def setup(self) -> None:
|
||||||
|
"""
|
||||||
|
Allows the plugin to perform any additional setup it may need, such as creating
|
||||||
|
a temporary directory, copying a file somewhere, etc.
|
||||||
|
|
||||||
|
Executed before run()
|
||||||
|
|
||||||
|
In general, this should be the "light" work, not the bulk of processing
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def run(self) -> Optional[str]:
|
||||||
|
"""
|
||||||
|
The bulk of plugin processing, this does whatever action the plugin is for.
|
||||||
|
|
||||||
|
Executed after setup() and before cleanup()
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
"""
|
||||||
|
Allows the plugin to execute any cleanup it may require
|
||||||
|
|
||||||
|
Executed after run(), even in the case of error
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class AlwaysRunPluginMixin(ConsumeTaskPlugin):
|
||||||
|
"""
|
||||||
|
A plugin which is always able to run
|
||||||
|
"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def able_to_run(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class NoSetupPluginMixin(ConsumeTaskPlugin):
|
||||||
|
"""
|
||||||
|
A plugin which requires no setup
|
||||||
|
"""
|
||||||
|
|
||||||
|
def setup(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoCleanupPluginMixin(ConsumeTaskPlugin):
|
||||||
|
"""
|
||||||
|
A plugin which needs to clean up no files
|
||||||
|
"""
|
||||||
|
|
||||||
|
def cleanup(self) -> None:
|
||||||
|
pass
|
82
src/documents/plugins/helpers.py
Normal file
82
src/documents/plugins/helpers.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import enum
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from asgiref.sync import async_to_sync
|
||||||
|
from channels.layers import get_channel_layer
|
||||||
|
from channels_redis.pubsub import RedisPubSubChannelLayer
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressStatusOptions(str, enum.Enum):
|
||||||
|
STARTED = "STARTED"
|
||||||
|
WORKING = "WORKING"
|
||||||
|
SUCCESS = "SUCCESS"
|
||||||
|
FAILED = "FAILED"
|
||||||
|
|
||||||
|
|
||||||
|
class ProgressManager:
|
||||||
|
"""
|
||||||
|
Handles sending of progress information via the channel layer, with proper management
|
||||||
|
of the open/close of the layer to ensure messages go out and everything is cleaned up
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename: str, task_id: Optional[str] = None) -> None:
|
||||||
|
self.filename = filename
|
||||||
|
self._channel: Optional[RedisPubSubChannelLayer] = None
|
||||||
|
self.task_id = task_id
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.open()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def open(self) -> None:
|
||||||
|
"""
|
||||||
|
If not already opened, gets the default channel layer
|
||||||
|
opened and ready to send messages
|
||||||
|
"""
|
||||||
|
if self._channel is None:
|
||||||
|
self._channel = get_channel_layer()
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""
|
||||||
|
If it was opened, flushes the channel layer
|
||||||
|
"""
|
||||||
|
if self._channel is not None:
|
||||||
|
async_to_sync(self._channel.flush)
|
||||||
|
self._channel = None
|
||||||
|
|
||||||
|
def send_progress(
|
||||||
|
self,
|
||||||
|
status: ProgressStatusOptions,
|
||||||
|
message: str,
|
||||||
|
current_progress: int,
|
||||||
|
max_progress: int,
|
||||||
|
extra_args: Optional[dict[str, Union[str, int]]] = None,
|
||||||
|
) -> None:
|
||||||
|
# Ensure the layer is open
|
||||||
|
self.open()
|
||||||
|
|
||||||
|
# Just for IDEs
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert self._channel is not None
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"type": "status_update",
|
||||||
|
"data": {
|
||||||
|
"filename": self.filename,
|
||||||
|
"task_id": self.task_id,
|
||||||
|
"current_progress": current_progress,
|
||||||
|
"max_progress": max_progress,
|
||||||
|
"status": status,
|
||||||
|
"message": message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if extra_args is not None:
|
||||||
|
payload["data"].update(extra_args)
|
||||||
|
|
||||||
|
# Construct and send the update
|
||||||
|
async_to_sync(self._channel.group_send)("status_updates", payload)
|
@ -13,6 +13,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from django.core.validators import DecimalValidator
|
from django.core.validators import DecimalValidator
|
||||||
from django.core.validators import MaxLengthValidator
|
from django.core.validators import MaxLengthValidator
|
||||||
from django.core.validators import integer_validator
|
from django.core.validators import integer_validator
|
||||||
|
from django.utils import timezone
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
@ -580,6 +581,7 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
|
|||||||
custom_field_instances_to_update,
|
custom_field_instances_to_update,
|
||||||
["value_document_ids"],
|
["value_document_ids"],
|
||||||
)
|
)
|
||||||
|
Document.objects.filter(id__in=target_doc_ids).update(modified=timezone.now())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def remove_doclink(
|
def remove_doclink(
|
||||||
@ -600,6 +602,7 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
|
|||||||
):
|
):
|
||||||
target_doc_field_instance.value.remove(document.id)
|
target_doc_field_instance.value.remove(document.id)
|
||||||
target_doc_field_instance.save()
|
target_doc_field_instance.save()
|
||||||
|
Document.objects.filter(id=target_doc_id).update(modified=timezone.now())
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CustomFieldInstance
|
model = CustomFieldInstance
|
||||||
|
@ -2,30 +2,30 @@ import hashlib
|
|||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import tqdm
|
import tqdm
|
||||||
from asgiref.sync import async_to_sync
|
|
||||||
from celery import Task
|
from celery import Task
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from channels.layers import get_channel_layer
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models.signals import post_save
|
from django.db.models.signals import post_save
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
from redis.exceptions import ConnectionError
|
|
||||||
from whoosh.writing import AsyncWriter
|
from whoosh.writing import AsyncWriter
|
||||||
|
|
||||||
from documents import index
|
from documents import index
|
||||||
from documents import sanity_checker
|
from documents import sanity_checker
|
||||||
from documents.barcodes import BarcodeReader
|
from documents.barcodes import BarcodePlugin
|
||||||
from documents.classifier import DocumentClassifier
|
from documents.classifier import DocumentClassifier
|
||||||
from documents.classifier import load_classifier
|
from documents.classifier import load_classifier
|
||||||
from documents.consumer import Consumer
|
from documents.consumer import Consumer
|
||||||
from documents.consumer import ConsumerError
|
from documents.consumer import ConsumerError
|
||||||
|
from documents.consumer import WorkflowTriggerPlugin
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.double_sided import collate
|
from documents.double_sided import CollatePlugin
|
||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.file_handling import generate_unique_filename
|
from documents.file_handling import generate_unique_filename
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
@ -35,6 +35,10 @@ from documents.models import StoragePath
|
|||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.parsers import DocumentParser
|
from documents.parsers import DocumentParser
|
||||||
from documents.parsers import get_parser_class_for_mime_type
|
from documents.parsers import get_parser_class_for_mime_type
|
||||||
|
from documents.plugins.base import ConsumeTaskPlugin
|
||||||
|
from documents.plugins.base import ProgressManager
|
||||||
|
from documents.plugins.base import StopConsumeTaskError
|
||||||
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
from documents.sanity_checker import SanityCheckFailedException
|
from documents.sanity_checker import SanityCheckFailedException
|
||||||
from documents.signals import document_updated
|
from documents.signals import document_updated
|
||||||
|
|
||||||
@ -102,70 +106,60 @@ def consume_file(
|
|||||||
input_doc: ConsumableDocument,
|
input_doc: ConsumableDocument,
|
||||||
overrides: Optional[DocumentMetadataOverrides] = None,
|
overrides: Optional[DocumentMetadataOverrides] = None,
|
||||||
):
|
):
|
||||||
def send_progress(status="SUCCESS", message="finished"):
|
|
||||||
payload = {
|
|
||||||
"filename": overrides.filename or input_doc.original_file.name,
|
|
||||||
"task_id": None,
|
|
||||||
"current_progress": 100,
|
|
||||||
"max_progress": 100,
|
|
||||||
"status": status,
|
|
||||||
"message": message,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
async_to_sync(get_channel_layer().group_send)(
|
|
||||||
"status_updates",
|
|
||||||
{"type": "status_update", "data": payload},
|
|
||||||
)
|
|
||||||
except ConnectionError as e:
|
|
||||||
logger.warning(f"ConnectionError on status send: {e!s}")
|
|
||||||
|
|
||||||
# Default no overrides
|
# Default no overrides
|
||||||
if overrides is None:
|
if overrides is None:
|
||||||
overrides = DocumentMetadataOverrides()
|
overrides = DocumentMetadataOverrides()
|
||||||
|
|
||||||
# Handle collation of double-sided documents scanned in two parts
|
plugins: list[type[ConsumeTaskPlugin]] = [
|
||||||
if settings.CONSUMER_ENABLE_COLLATE_DOUBLE_SIDED and (
|
CollatePlugin,
|
||||||
settings.CONSUMER_COLLATE_DOUBLE_SIDED_SUBDIR_NAME
|
BarcodePlugin,
|
||||||
in input_doc.original_file.parts
|
WorkflowTriggerPlugin,
|
||||||
):
|
]
|
||||||
try:
|
|
||||||
msg = collate(input_doc)
|
|
||||||
send_progress(message=msg)
|
|
||||||
return msg
|
|
||||||
except ConsumerError as e:
|
|
||||||
send_progress(status="FAILURE", message=e.args[0])
|
|
||||||
raise e
|
|
||||||
|
|
||||||
# read all barcodes in the current document
|
with ProgressManager(
|
||||||
if settings.CONSUMER_ENABLE_BARCODES or settings.CONSUMER_ENABLE_ASN_BARCODE:
|
overrides.filename or input_doc.original_file.name,
|
||||||
with BarcodeReader(input_doc.original_file, input_doc.mime_type) as reader:
|
self.request.id,
|
||||||
if settings.CONSUMER_ENABLE_BARCODES and reader.separate(
|
) as status_mgr, TemporaryDirectory(dir=settings.SCRATCH_DIR) as tmp_dir:
|
||||||
input_doc.source,
|
tmp_dir = Path(tmp_dir)
|
||||||
|
for plugin_class in plugins:
|
||||||
|
plugin_name = plugin_class.NAME
|
||||||
|
|
||||||
|
plugin = plugin_class(
|
||||||
|
input_doc,
|
||||||
overrides,
|
overrides,
|
||||||
):
|
status_mgr,
|
||||||
# notify the sender, otherwise the progress bar
|
tmp_dir,
|
||||||
# in the UI stays stuck
|
self.request.id,
|
||||||
send_progress()
|
|
||||||
# consuming stops here, since the original document with
|
|
||||||
# the barcodes has been split and will be consumed separately
|
|
||||||
input_doc.original_file.unlink()
|
|
||||||
return "File successfully split"
|
|
||||||
|
|
||||||
# try reading the ASN from barcode
|
|
||||||
if (
|
|
||||||
settings.CONSUMER_ENABLE_ASN_BARCODE
|
|
||||||
and (located_asn := reader.asn) is not None
|
|
||||||
):
|
|
||||||
# Note this will take precedence over an API provided ASN
|
|
||||||
# But it's from a physical barcode, so that's good
|
|
||||||
overrides.asn = located_asn
|
|
||||||
logger.info(f"Found ASN in barcode: {overrides.asn}")
|
|
||||||
|
|
||||||
template_overrides = Consumer().get_workflow_overrides(
|
|
||||||
input_doc=input_doc,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
overrides.update(template_overrides)
|
if not plugin.able_to_run:
|
||||||
|
logger.debug(f"Skipping plugin {plugin_name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug(f"Executing plugin {plugin_name}")
|
||||||
|
plugin.setup()
|
||||||
|
|
||||||
|
msg = plugin.run()
|
||||||
|
|
||||||
|
if msg is not None:
|
||||||
|
logger.info(f"{plugin_name} completed with: {msg}")
|
||||||
|
else:
|
||||||
|
logger.info(f"{plugin_name} completed with no message")
|
||||||
|
|
||||||
|
overrides = plugin.metadata
|
||||||
|
|
||||||
|
except StopConsumeTaskError as e:
|
||||||
|
logger.info(f"{plugin_name} requested task exit: {e.message}")
|
||||||
|
return e.message
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"{plugin_name} failed: {e}")
|
||||||
|
status_mgr.send_progress(ProgressStatusOptions.FAILED, f"{e}", 100, 100)
|
||||||
|
raise
|
||||||
|
|
||||||
|
finally:
|
||||||
|
plugin.cleanup()
|
||||||
|
|
||||||
# continue with consumption if no barcode was found
|
# continue with consumption if no barcode was found
|
||||||
document = Consumer().try_consume_file(
|
document = Consumer().try_consume_file(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@ -49,10 +50,34 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
"rotate_pages_threshold": None,
|
"rotate_pages_threshold": None,
|
||||||
"max_image_pixels": None,
|
"max_image_pixels": None,
|
||||||
"color_conversion_strategy": None,
|
"color_conversion_strategy": None,
|
||||||
|
"app_title": None,
|
||||||
|
"app_logo": None,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_api_get_ui_settings_with_config(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing config with app_title, app_logo specified
|
||||||
|
WHEN:
|
||||||
|
- API to retrieve uisettings is called
|
||||||
|
THEN:
|
||||||
|
- app_title and app_logo are included
|
||||||
|
"""
|
||||||
|
config = ApplicationConfiguration.objects.first()
|
||||||
|
config.app_title = "Fancy New Title"
|
||||||
|
config.app_logo = "/logo/example.jpg"
|
||||||
|
config.save()
|
||||||
|
response = self.client.get("/api/ui_settings/", format="json")
|
||||||
|
self.assertDictContainsSubset(
|
||||||
|
{
|
||||||
|
"app_title": config.app_title,
|
||||||
|
"app_logo": config.app_logo,
|
||||||
|
},
|
||||||
|
response.data["settings"],
|
||||||
|
)
|
||||||
|
|
||||||
def test_api_update_config(self):
|
def test_api_update_config(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@ -100,3 +125,37 @@ class TestApiAppConfig(DirectoriesMixin, APITestCase):
|
|||||||
config = ApplicationConfiguration.objects.first()
|
config = ApplicationConfiguration.objects.first()
|
||||||
self.assertEqual(config.user_args, None)
|
self.assertEqual(config.user_args, None)
|
||||||
self.assertEqual(config.language, None)
|
self.assertEqual(config.language, None)
|
||||||
|
|
||||||
|
def test_api_replace_app_logo(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing config with app_logo specified
|
||||||
|
WHEN:
|
||||||
|
- API to replace app_logo is called
|
||||||
|
THEN:
|
||||||
|
- old app_logo file is deleted
|
||||||
|
"""
|
||||||
|
with open(
|
||||||
|
os.path.join(os.path.dirname(__file__), "samples", "simple.jpg"),
|
||||||
|
"rb",
|
||||||
|
) as f:
|
||||||
|
self.client.patch(
|
||||||
|
f"{self.ENDPOINT}1/",
|
||||||
|
{
|
||||||
|
"app_logo": f,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
config = ApplicationConfiguration.objects.first()
|
||||||
|
old_logo = config.app_logo
|
||||||
|
self.assertTrue(os.path.exists(old_logo.path))
|
||||||
|
with open(
|
||||||
|
os.path.join(os.path.dirname(__file__), "samples", "simple.png"),
|
||||||
|
"rb",
|
||||||
|
) as f:
|
||||||
|
self.client.patch(
|
||||||
|
f"{self.ENDPOINT}1/",
|
||||||
|
{
|
||||||
|
"app_logo": f,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.assertFalse(os.path.exists(old_logo.path))
|
||||||
|
@ -35,6 +35,8 @@ class TestApiUiSettings(DirectoriesMixin, APITestCase):
|
|||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
response.data["settings"],
|
response.data["settings"],
|
||||||
{
|
{
|
||||||
|
"app_title": None,
|
||||||
|
"app_logo": None,
|
||||||
"update_checking": {
|
"update_checking": {
|
||||||
"backend_setting": "default",
|
"backend_setting": "default",
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import shutil
|
import shutil
|
||||||
|
from collections.abc import Generator
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -7,14 +10,13 @@ from django.test import TestCase
|
|||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from documents import tasks
|
from documents import tasks
|
||||||
from documents.barcodes import BarcodeReader
|
from documents.barcodes import BarcodePlugin
|
||||||
from documents.consumer import ConsumerError
|
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.models import Document
|
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
from documents.tests.utils import DocumentConsumeDelayMixin
|
from documents.tests.utils import DocumentConsumeDelayMixin
|
||||||
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from documents.tests.utils import SampleDirMixin
|
from documents.tests.utils import SampleDirMixin
|
||||||
|
|
||||||
@ -26,8 +28,29 @@ except ImportError:
|
|||||||
HAS_ZXING_LIB = False
|
HAS_ZXING_LIB = False
|
||||||
|
|
||||||
|
|
||||||
|
class GetReaderPluginMixin:
|
||||||
|
@contextmanager
|
||||||
|
def get_reader(self, filepath: Path) -> Generator[BarcodePlugin, None, None]:
|
||||||
|
reader = BarcodePlugin(
|
||||||
|
ConsumableDocument(DocumentSource.ConsumeFolder, original_file=filepath),
|
||||||
|
DocumentMetadataOverrides(),
|
||||||
|
DummyProgressManager(filepath.name, None),
|
||||||
|
self.dirs.scratch_dir,
|
||||||
|
"task-id",
|
||||||
|
)
|
||||||
|
reader.setup()
|
||||||
|
yield reader
|
||||||
|
reader.cleanup()
|
||||||
|
|
||||||
|
|
||||||
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
|
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
|
||||||
class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, TestCase):
|
class TestBarcode(
|
||||||
|
DirectoriesMixin,
|
||||||
|
FileSystemAssertsMixin,
|
||||||
|
SampleDirMixin,
|
||||||
|
GetReaderPluginMixin,
|
||||||
|
TestCase,
|
||||||
|
):
|
||||||
def test_scan_file_for_separating_barcodes(self):
|
def test_scan_file_for_separating_barcodes(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@ -39,7 +62,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -60,7 +83,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.tiff"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.tiff"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "image/tiff") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -80,7 +103,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle-alpha.tiff"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle-alpha.tiff"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "image/tiff") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -97,7 +120,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
- No pages to split on
|
- No pages to split on
|
||||||
"""
|
"""
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -115,7 +138,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -133,7 +156,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "several-patcht-codes.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "several-patcht-codes.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -158,7 +181,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
]:
|
]:
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / test_file
|
test_file = self.BARCODE_SAMPLE_DIR / test_file
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -177,7 +200,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle-unreadable.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle-unreadable.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -195,7 +218,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-fax-image.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-fax-image.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -214,7 +237,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-qr.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-qr.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -234,7 +257,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-custom.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-custom.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -255,7 +278,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-qr-custom.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-qr-custom.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -276,7 +299,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-128-custom.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-128-custom.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -296,7 +319,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-custom.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-custom.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -315,7 +338,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "many-qr-codes.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "many-qr-codes.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -334,7 +357,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.SAMPLE_DIR / "password-is-test.pdf"
|
test_file = self.SAMPLE_DIR / "password-is-test.pdf"
|
||||||
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
|
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
warning = cm.output[0]
|
warning = cm.output[0]
|
||||||
expected_str = "WARNING:paperless.barcodes:File is likely password protected, not checking for barcodes"
|
expected_str = "WARNING:paperless.barcodes:File is likely password protected, not checking for barcodes"
|
||||||
@ -356,7 +379,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-middle.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
documents = reader.separate_pages({1: False})
|
documents = reader.separate_pages({1: False})
|
||||||
|
|
||||||
self.assertEqual(reader.pdf_file, test_file)
|
self.assertEqual(reader.pdf_file, test_file)
|
||||||
@ -373,7 +396,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-double.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t-double.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
documents = reader.separate_pages({1: False, 2: False})
|
documents = reader.separate_pages({1: False, 2: False})
|
||||||
|
|
||||||
self.assertEqual(len(documents), 2)
|
self.assertEqual(len(documents), 2)
|
||||||
@ -385,32 +408,18 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
WHEN:
|
WHEN:
|
||||||
- No separation pages are provided
|
- No separation pages are provided
|
||||||
THEN:
|
THEN:
|
||||||
- No new documents are produced
|
- Nothing happens
|
||||||
- A warning is logged
|
|
||||||
"""
|
"""
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
|
with self.get_reader(test_file) as reader:
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
self.assertEqual("No pages to split on!", reader.run())
|
||||||
self.assertFalse(
|
|
||||||
reader.separate(
|
|
||||||
DocumentSource.ApiUpload,
|
|
||||||
DocumentMetadataOverrides(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
cm.output,
|
|
||||||
[
|
|
||||||
"WARNING:paperless.barcodes:No pages to split on!",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
CONSUMER_ENABLE_BARCODES=True,
|
CONSUMER_ENABLE_BARCODES=True,
|
||||||
CONSUMER_BARCODE_TIFF_SUPPORT=True,
|
CONSUMER_BARCODE_TIFF_SUPPORT=True,
|
||||||
)
|
)
|
||||||
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
def test_consume_barcode_unsupported_jpg_file(self):
|
||||||
def test_consume_barcode_unsupported_jpg_file(self, m):
|
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- JPEG image as input
|
- JPEG image as input
|
||||||
@ -422,35 +431,8 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.SAMPLE_DIR / "simple.jpg"
|
test_file = self.SAMPLE_DIR / "simple.jpg"
|
||||||
|
|
||||||
dst = settings.SCRATCH_DIR / "simple.jpg"
|
with self.get_reader(test_file) as reader:
|
||||||
shutil.copy(test_file, dst)
|
self.assertFalse(reader.able_to_run)
|
||||||
|
|
||||||
with self.assertLogs("paperless.barcodes", level="WARNING") as cm:
|
|
||||||
self.assertIn(
|
|
||||||
"Success",
|
|
||||||
tasks.consume_file(
|
|
||||||
ConsumableDocument(
|
|
||||||
source=DocumentSource.ConsumeFolder,
|
|
||||||
original_file=dst,
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertListEqual(
|
|
||||||
cm.output,
|
|
||||||
[
|
|
||||||
"WARNING:paperless.barcodes:Unsupported file format for barcode reader: image/jpeg",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
m.assert_called_once()
|
|
||||||
|
|
||||||
args, kwargs = m.call_args
|
|
||||||
self.assertIsNone(kwargs["override_filename"])
|
|
||||||
self.assertIsNone(kwargs["override_title"])
|
|
||||||
self.assertIsNone(kwargs["override_correspondent_id"])
|
|
||||||
self.assertIsNone(kwargs["override_document_type_id"])
|
|
||||||
self.assertIsNone(kwargs["override_tag_ids"])
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
CONSUMER_ENABLE_BARCODES=True,
|
CONSUMER_ENABLE_BARCODES=True,
|
||||||
@ -467,7 +449,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "split-by-asn-2.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "split-by-asn-2.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -504,7 +486,7 @@ class TestBarcode(DirectoriesMixin, FileSystemAssertsMixin, SampleDirMixin, Test
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "split-by-asn-1.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "split-by-asn-1.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
|
|
||||||
@ -550,7 +532,7 @@ class TestBarcodeNewConsume(
|
|||||||
|
|
||||||
overrides = DocumentMetadataOverrides(tag_ids=[1, 2, 9])
|
overrides = DocumentMetadataOverrides(tag_ids=[1, 2, 9])
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync") as progress_mocker:
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -559,10 +541,8 @@ class TestBarcodeNewConsume(
|
|||||||
),
|
),
|
||||||
overrides,
|
overrides,
|
||||||
),
|
),
|
||||||
"File successfully split",
|
"Barcode splitting complete!",
|
||||||
)
|
)
|
||||||
# We let the consumer know progress is done
|
|
||||||
progress_mocker.assert_called_once()
|
|
||||||
# 2 new document consume tasks created
|
# 2 new document consume tasks created
|
||||||
self.assertEqual(self.consume_file_mock.call_count, 2)
|
self.assertEqual(self.consume_file_mock.call_count, 2)
|
||||||
|
|
||||||
@ -580,7 +560,20 @@ class TestBarcodeNewConsume(
|
|||||||
self.assertEqual(overrides, new_doc_overrides)
|
self.assertEqual(overrides, new_doc_overrides)
|
||||||
|
|
||||||
|
|
||||||
class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, GetReaderPluginMixin, TestCase):
|
||||||
|
@contextmanager
|
||||||
|
def get_reader(self, filepath: Path) -> BarcodePlugin:
|
||||||
|
reader = BarcodePlugin(
|
||||||
|
ConsumableDocument(DocumentSource.ConsumeFolder, original_file=filepath),
|
||||||
|
DocumentMetadataOverrides(),
|
||||||
|
DummyProgressManager(filepath.name, None),
|
||||||
|
self.dirs.scratch_dir,
|
||||||
|
"task-id",
|
||||||
|
)
|
||||||
|
reader.setup()
|
||||||
|
yield reader
|
||||||
|
reader.cleanup()
|
||||||
|
|
||||||
@override_settings(CONSUMER_ASN_BARCODE_PREFIX="CUSTOM-PREFIX-")
|
@override_settings(CONSUMER_ASN_BARCODE_PREFIX="CUSTOM-PREFIX-")
|
||||||
def test_scan_file_for_asn_custom_prefix(self):
|
def test_scan_file_for_asn_custom_prefix(self):
|
||||||
"""
|
"""
|
||||||
@ -594,7 +587,7 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
|||||||
- The ASN integer value is correct
|
- The ASN integer value is correct
|
||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-custom-prefix.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-custom-prefix.pdf"
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
asn = reader.asn
|
asn = reader.asn
|
||||||
|
|
||||||
self.assertEqual(reader.pdf_file, test_file)
|
self.assertEqual(reader.pdf_file, test_file)
|
||||||
@ -613,7 +606,7 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-123.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-123.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
asn = reader.asn
|
asn = reader.asn
|
||||||
|
|
||||||
self.assertEqual(reader.pdf_file, test_file)
|
self.assertEqual(reader.pdf_file, test_file)
|
||||||
@ -630,55 +623,12 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "patch-code-t.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
asn = reader.asn
|
asn = reader.asn
|
||||||
|
|
||||||
self.assertEqual(reader.pdf_file, test_file)
|
self.assertEqual(reader.pdf_file, test_file)
|
||||||
self.assertEqual(asn, None)
|
self.assertEqual(asn, None)
|
||||||
|
|
||||||
@override_settings(CONSUMER_ENABLE_ASN_BARCODE=True)
|
|
||||||
def test_scan_file_for_asn_already_exists(self):
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- PDF with an ASN barcode
|
|
||||||
- ASN value already exists
|
|
||||||
WHEN:
|
|
||||||
- File is scanned for barcodes
|
|
||||||
THEN:
|
|
||||||
- ASN is retrieved from the document
|
|
||||||
- Consumption fails
|
|
||||||
"""
|
|
||||||
|
|
||||||
Document.objects.create(
|
|
||||||
title="WOW",
|
|
||||||
content="the content",
|
|
||||||
archive_serial_number=123,
|
|
||||||
checksum="456",
|
|
||||||
mime_type="application/pdf",
|
|
||||||
)
|
|
||||||
|
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-123.pdf"
|
|
||||||
|
|
||||||
dst = settings.SCRATCH_DIR / "barcode-39-asn-123.pdf"
|
|
||||||
shutil.copy(test_file, dst)
|
|
||||||
|
|
||||||
with mock.patch("documents.consumer.Consumer._send_progress"):
|
|
||||||
with self.assertRaises(ConsumerError) as cm, self.assertLogs(
|
|
||||||
"paperless.consumer",
|
|
||||||
level="ERROR",
|
|
||||||
) as logs_cm:
|
|
||||||
tasks.consume_file(
|
|
||||||
ConsumableDocument(
|
|
||||||
source=DocumentSource.ConsumeFolder,
|
|
||||||
original_file=dst,
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
self.assertIn("Not consuming barcode-39-asn-123.pdf", str(cm.exception))
|
|
||||||
error_str = logs_cm.output[0]
|
|
||||||
expected_str = "ERROR:paperless.consumer:Not consuming barcode-39-asn-123.pdf: Given ASN already exists!"
|
|
||||||
self.assertEqual(expected_str, error_str)
|
|
||||||
|
|
||||||
def test_scan_file_for_asn_barcode_invalid(self):
|
def test_scan_file_for_asn_barcode_invalid(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@ -692,7 +642,7 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-invalid.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-39-asn-invalid.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
asn = reader.asn
|
asn = reader.asn
|
||||||
|
|
||||||
self.assertEqual(reader.pdf_file, test_file)
|
self.assertEqual(reader.pdf_file, test_file)
|
||||||
@ -718,7 +668,9 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
|||||||
dst = settings.SCRATCH_DIR / "barcode-39-asn-123.pdf"
|
dst = settings.SCRATCH_DIR / "barcode-39-asn-123.pdf"
|
||||||
shutil.copy(test_file, dst)
|
shutil.copy(test_file, dst)
|
||||||
|
|
||||||
with mock.patch("documents.consumer.Consumer.try_consume_file") as mocked_call:
|
with mock.patch(
|
||||||
|
"documents.consumer.Consumer.try_consume_file",
|
||||||
|
) as mocked_consumer:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
@ -726,40 +678,11 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
|||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
mocked_consumer.assert_called_once()
|
||||||
args, kwargs = mocked_call.call_args
|
args, kwargs = mocked_consumer.call_args
|
||||||
|
|
||||||
self.assertEqual(kwargs["override_asn"], 123)
|
self.assertEqual(kwargs["override_asn"], 123)
|
||||||
|
|
||||||
@override_settings(CONSUMER_ENABLE_ASN_BARCODE=True)
|
|
||||||
def test_asn_too_large(self):
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- ASN from barcode enabled
|
|
||||||
- Barcode contains too large an ASN value
|
|
||||||
WHEN:
|
|
||||||
- ASN from barcode checked for correctness
|
|
||||||
THEN:
|
|
||||||
- Exception is raised regarding size limits
|
|
||||||
"""
|
|
||||||
src = self.BARCODE_SAMPLE_DIR / "barcode-128-asn-too-large.pdf"
|
|
||||||
|
|
||||||
dst = self.dirs.scratch_dir / "barcode-128-asn-too-large.pdf"
|
|
||||||
shutil.copy(src, dst)
|
|
||||||
|
|
||||||
input_doc = ConsumableDocument(
|
|
||||||
source=DocumentSource.ConsumeFolder,
|
|
||||||
original_file=dst,
|
|
||||||
)
|
|
||||||
|
|
||||||
with mock.patch("documents.consumer.Consumer._send_progress"):
|
|
||||||
self.assertRaisesMessage(
|
|
||||||
ConsumerError,
|
|
||||||
"Given ASN 4294967296 is out of range [0, 4,294,967,295]",
|
|
||||||
tasks.consume_file,
|
|
||||||
input_doc,
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
|
@override_settings(CONSUMER_BARCODE_SCANNER="PYZBAR")
|
||||||
def test_scan_file_for_qrcode_without_upscale(self):
|
def test_scan_file_for_qrcode_without_upscale(self):
|
||||||
"""
|
"""
|
||||||
@ -774,7 +697,7 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
|||||||
|
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-qr-asn-000123-upscale-dpi.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-qr-asn-000123-upscale-dpi.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
self.assertEqual(len(reader.barcodes), 0)
|
self.assertEqual(len(reader.barcodes), 0)
|
||||||
|
|
||||||
@ -796,7 +719,7 @@ class TestAsnBarcode(DirectoriesMixin, SampleDirMixin, TestCase):
|
|||||||
|
|
||||||
test_file = self.BARCODE_SAMPLE_DIR / "barcode-qr-asn-000123-upscale-dpi.pdf"
|
test_file = self.BARCODE_SAMPLE_DIR / "barcode-qr-asn-000123-upscale-dpi.pdf"
|
||||||
|
|
||||||
with BarcodeReader(test_file, "application/pdf") as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
self.assertEqual(len(reader.barcodes), 1)
|
self.assertEqual(len(reader.barcodes), 1)
|
||||||
self.assertEqual(reader.asn, 123)
|
self.assertEqual(reader.asn, 123)
|
||||||
|
@ -17,6 +17,7 @@ from documents.data_models import DocumentSource
|
|||||||
from documents.double_sided import STAGING_FILE_NAME
|
from documents.double_sided import STAGING_FILE_NAME
|
||||||
from documents.double_sided import TIMEOUT_MINUTES
|
from documents.double_sided import TIMEOUT_MINUTES
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
|
|
||||||
|
|
||||||
@ -42,9 +43,10 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
dst = self.dirs.double_sided_dir / dstname
|
dst = self.dirs.double_sided_dir / dstname
|
||||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
with mock.patch("documents.tasks.async_to_sync"), mock.patch(
|
with mock.patch(
|
||||||
"documents.consumer.async_to_sync",
|
"documents.tasks.ProgressManager",
|
||||||
):
|
DummyProgressManager,
|
||||||
|
), mock.patch("documents.consumer.async_to_sync"):
|
||||||
msg = tasks.consume_file(
|
msg = tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
@ -211,7 +213,7 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
msg = self.consume_file("simple.pdf", Path("..") / "simple.pdf")
|
msg = self.consume_file("simple.pdf", Path("..") / "simple.pdf")
|
||||||
self.assertIsNotFile(self.staging_file)
|
self.assertIsNotFile(self.staging_file)
|
||||||
self.assertRegex(msg, "Success. New document .* created")
|
self.assertRegex(msg, r"Success. New document id \d+ created")
|
||||||
|
|
||||||
def test_subdirectory_upload(self):
|
def test_subdirectory_upload(self):
|
||||||
"""
|
"""
|
||||||
@ -250,4 +252,4 @@ class TestDoubleSided(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
msg = self.consume_file("simple.pdf")
|
msg = self.consume_file("simple.pdf")
|
||||||
self.assertIsNotFile(self.staging_file)
|
self.assertIsNotFile(self.staging_file)
|
||||||
self.assertRegex(msg, "Success. New document .* created")
|
self.assertRegex(msg, r"Success. New document id \d+ created")
|
||||||
|
@ -24,6 +24,7 @@ from documents.models import WorkflowAction
|
|||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
from documents.signals import document_consumption_finished
|
from documents.signals import document_consumption_finished
|
||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
from documents.tests.utils import DummyProgressManager
|
||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
@ -126,7 +127,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -203,7 +204,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -294,7 +295,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -356,7 +357,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -407,7 +408,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -468,7 +469,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -529,7 +530,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -591,7 +592,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
with self.assertLogs("paperless.matching", level="DEBUG") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@ -686,7 +687,7 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
|
|
||||||
test_file = self.SAMPLE_DIR / "simple.pdf"
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with mock.patch("documents.tasks.async_to_sync"):
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
|
@ -9,6 +9,7 @@ from os import PathLike
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
from typing import Optional
|
||||||
from typing import Union
|
from typing import Union
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ from django.test import override_settings
|
|||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.parsers import ParseError
|
from documents.parsers import ParseError
|
||||||
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
|
|
||||||
|
|
||||||
def setup_directories():
|
def setup_directories():
|
||||||
@ -146,6 +148,11 @@ def util_call_with_backoff(
|
|||||||
|
|
||||||
|
|
||||||
class DirectoriesMixin:
|
class DirectoriesMixin:
|
||||||
|
"""
|
||||||
|
Creates and overrides settings for all folders and paths, then ensures
|
||||||
|
they are cleaned up on exit
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.dirs = None
|
self.dirs = None
|
||||||
@ -160,6 +167,10 @@ class DirectoriesMixin:
|
|||||||
|
|
||||||
|
|
||||||
class FileSystemAssertsMixin:
|
class FileSystemAssertsMixin:
|
||||||
|
"""
|
||||||
|
Utilities for checks various state information of the file system
|
||||||
|
"""
|
||||||
|
|
||||||
def assertIsFile(self, path: Union[PathLike, str]):
|
def assertIsFile(self, path: Union[PathLike, str]):
|
||||||
self.assertTrue(Path(path).resolve().is_file(), f"File does not exist: {path}")
|
self.assertTrue(Path(path).resolve().is_file(), f"File does not exist: {path}")
|
||||||
|
|
||||||
@ -188,6 +199,11 @@ class FileSystemAssertsMixin:
|
|||||||
|
|
||||||
|
|
||||||
class ConsumerProgressMixin:
|
class ConsumerProgressMixin:
|
||||||
|
"""
|
||||||
|
Mocks the Consumer _send_progress, preventing attempts to connect to Redis
|
||||||
|
and allowing access to its calls for verification
|
||||||
|
"""
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.send_progress_patcher = mock.patch(
|
self.send_progress_patcher = mock.patch(
|
||||||
"documents.consumer.Consumer._send_progress",
|
"documents.consumer.Consumer._send_progress",
|
||||||
@ -310,3 +326,59 @@ class SampleDirMixin:
|
|||||||
SAMPLE_DIR = Path(__file__).parent / "samples"
|
SAMPLE_DIR = Path(__file__).parent / "samples"
|
||||||
|
|
||||||
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
|
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
|
||||||
|
|
||||||
|
|
||||||
|
class DummyProgressManager:
|
||||||
|
"""
|
||||||
|
A dummy handler for progress management that doesn't actually try to
|
||||||
|
connect to Redis. Payloads are stored for test assertions if needed.
|
||||||
|
|
||||||
|
Use it with
|
||||||
|
mock.patch("documents.tasks.ProgressManager", DummyProgressManager)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, filename: str, task_id: Optional[str] = None) -> None:
|
||||||
|
self.filename = filename
|
||||||
|
self.task_id = task_id
|
||||||
|
print("hello world")
|
||||||
|
self.payloads = []
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.open()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
def open(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_progress(
|
||||||
|
self,
|
||||||
|
status: ProgressStatusOptions,
|
||||||
|
message: str,
|
||||||
|
current_progress: int,
|
||||||
|
max_progress: int,
|
||||||
|
extra_args: Optional[dict[str, Union[str, int]]] = None,
|
||||||
|
) -> None:
|
||||||
|
# Ensure the layer is open
|
||||||
|
self.open()
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"type": "status_update",
|
||||||
|
"data": {
|
||||||
|
"filename": self.filename,
|
||||||
|
"task_id": self.task_id,
|
||||||
|
"current_progress": current_progress,
|
||||||
|
"max_progress": max_progress,
|
||||||
|
"status": status,
|
||||||
|
"message": message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if extra_args is not None:
|
||||||
|
payload["data"].update(extra_args)
|
||||||
|
|
||||||
|
self.payloads.append(payload)
|
||||||
|
@ -120,6 +120,7 @@ from documents.serialisers import WorkflowTriggerSerializer
|
|||||||
from documents.signals import document_updated
|
from documents.signals import document_updated
|
||||||
from documents.tasks import consume_file
|
from documents.tasks import consume_file
|
||||||
from paperless import version
|
from paperless import version
|
||||||
|
from paperless.config import GeneralConfig
|
||||||
from paperless.db import GnuPG
|
from paperless.db import GnuPG
|
||||||
from paperless.views import StandardPagination
|
from paperless.views import StandardPagination
|
||||||
|
|
||||||
@ -1164,6 +1165,16 @@ class UiSettingsView(GenericAPIView):
|
|||||||
ui_settings["update_checking"] = {
|
ui_settings["update_checking"] = {
|
||||||
"backend_setting": settings.ENABLE_UPDATE_CHECK,
|
"backend_setting": settings.ENABLE_UPDATE_CHECK,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
general_config = GeneralConfig()
|
||||||
|
|
||||||
|
ui_settings["app_title"] = settings.APP_TITLE
|
||||||
|
if general_config.app_title is not None and len(general_config.app_title) > 0:
|
||||||
|
ui_settings["app_title"] = general_config.app_title
|
||||||
|
ui_settings["app_logo"] = settings.APP_LOGO
|
||||||
|
if general_config.app_logo is not None and len(general_config.app_logo) > 0:
|
||||||
|
ui_settings["app_logo"] = general_config.app_logo
|
||||||
|
|
||||||
user_resp = {
|
user_resp = {
|
||||||
"id": user.id,
|
"id": user.id,
|
||||||
"username": user.username,
|
"username": user.username,
|
||||||
|
@ -8,13 +8,11 @@ from paperless.models import ApplicationConfiguration
|
|||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class OutputTypeConfig:
|
class BaseConfig:
|
||||||
"""
|
"""
|
||||||
Almost all parsers care about the chosen PDF output format
|
Almost all parsers care about the chosen PDF output format
|
||||||
"""
|
"""
|
||||||
|
|
||||||
output_type: str = dataclasses.field(init=False)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_config_instance() -> ApplicationConfiguration:
|
def _get_config_instance() -> ApplicationConfiguration:
|
||||||
app_config = ApplicationConfiguration.objects.all().first()
|
app_config = ApplicationConfiguration.objects.all().first()
|
||||||
@ -24,6 +22,15 @@ class OutputTypeConfig:
|
|||||||
app_config = ApplicationConfiguration.objects.all().first()
|
app_config = ApplicationConfiguration.objects.all().first()
|
||||||
return app_config
|
return app_config
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class OutputTypeConfig(BaseConfig):
|
||||||
|
"""
|
||||||
|
Almost all parsers care about the chosen PDF output format
|
||||||
|
"""
|
||||||
|
|
||||||
|
output_type: str = dataclasses.field(init=False)
|
||||||
|
|
||||||
def __post_init__(self) -> None:
|
def __post_init__(self) -> None:
|
||||||
app_config = self._get_config_instance()
|
app_config = self._get_config_instance()
|
||||||
|
|
||||||
@ -86,3 +93,19 @@ class OcrConfig(OutputTypeConfig):
|
|||||||
user_args = {}
|
user_args = {}
|
||||||
|
|
||||||
self.user_args = user_args
|
self.user_args = user_args
|
||||||
|
|
||||||
|
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class GeneralConfig(BaseConfig):
|
||||||
|
"""
|
||||||
|
General application settings that require global scope
|
||||||
|
"""
|
||||||
|
|
||||||
|
app_title: str = dataclasses.field(init=False)
|
||||||
|
app_logo: str = dataclasses.field(init=False)
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
app_config = self._get_config_instance()
|
||||||
|
|
||||||
|
self.app_title = app_config.app_title or None
|
||||||
|
self.app_logo = app_config.app_logo.url if app_config.app_logo else None
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 4.2.9 on 2024-01-12 05:33
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("paperless", "0001_initial"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="app_logo",
|
||||||
|
field=models.FileField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
upload_to="",
|
||||||
|
verbose_name="Application logo",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="applicationconfiguration",
|
||||||
|
name="app_title",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
max_length=48,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Application title",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -1,3 +1,4 @@
|
|||||||
|
from django.core.validators import FileExtensionValidator
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -166,6 +167,23 @@ class ApplicationConfiguration(AbstractSingletonModel):
|
|||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
app_title = models.CharField(
|
||||||
|
verbose_name=_("Application title"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
max_length=48,
|
||||||
|
)
|
||||||
|
|
||||||
|
app_logo = models.FileField(
|
||||||
|
verbose_name=_("Application logo"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
validators=[
|
||||||
|
FileExtensionValidator(allowed_extensions=["jpg", "png", "gif", "svg"]),
|
||||||
|
],
|
||||||
|
upload_to="logo/",
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("paperless application settings")
|
verbose_name = _("paperless application settings")
|
||||||
|
|
||||||
|
@ -132,6 +132,11 @@ class ApplicationConfigurationSerializer(serializers.ModelSerializer):
|
|||||||
data["language"] = None
|
data["language"] = None
|
||||||
return super().run_validation(data)
|
return super().run_validation(data)
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
if instance.app_logo and "app_logo" in validated_data:
|
||||||
|
instance.app_logo.delete()
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = ApplicationConfiguration
|
model = ApplicationConfiguration
|
||||||
fields = "__all__"
|
fields = "__all__"
|
||||||
|
@ -367,6 +367,7 @@ STORAGES = {
|
|||||||
"staticfiles": {
|
"staticfiles": {
|
||||||
"BACKEND": _static_backend,
|
"BACKEND": _static_backend,
|
||||||
},
|
},
|
||||||
|
"default": {"BACKEND": "django.core.files.storage.FileSystemStorage"},
|
||||||
}
|
}
|
||||||
|
|
||||||
_CELERY_REDIS_URL, _CHANNELS_REDIS_URL = _parse_redis_url(
|
_CELERY_REDIS_URL, _CHANNELS_REDIS_URL = _parse_redis_url(
|
||||||
@ -999,6 +1000,9 @@ ENABLE_UPDATE_CHECK = os.getenv("PAPERLESS_ENABLE_UPDATE_CHECK", "default")
|
|||||||
if ENABLE_UPDATE_CHECK != "default":
|
if ENABLE_UPDATE_CHECK != "default":
|
||||||
ENABLE_UPDATE_CHECK = __get_boolean("PAPERLESS_ENABLE_UPDATE_CHECK")
|
ENABLE_UPDATE_CHECK = __get_boolean("PAPERLESS_ENABLE_UPDATE_CHECK")
|
||||||
|
|
||||||
|
APP_TITLE = os.getenv("PAPERLESS_APP_TITLE", None)
|
||||||
|
APP_LOGO = os.getenv("PAPERLESS_APP_LOGO", None)
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Machine Learning #
|
# Machine Learning #
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.conf.urls import include
|
from django.conf.urls import include
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
@ -8,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
from django.views.static import serve
|
||||||
from rest_framework.authtoken import views
|
from rest_framework.authtoken import views
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
|
|
||||||
@ -181,6 +184,12 @@ urlpatterns = [
|
|||||||
url=settings.STATIC_URL + "frontend/en-US/assets/%(path)s",
|
url=settings.STATIC_URL + "frontend/en-US/assets/%(path)s",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
# App logo
|
||||||
|
re_path(
|
||||||
|
r"^logo(?P<path>.*)$",
|
||||||
|
serve,
|
||||||
|
kwargs={"document_root": os.path.join(settings.MEDIA_ROOT, "logo")},
|
||||||
|
),
|
||||||
# TODO: with localization, this is even worse! :/
|
# TODO: with localization, this is even worse! :/
|
||||||
# login, logout
|
# login, logout
|
||||||
path("accounts/", include("django.contrib.auth.urls")),
|
path("accounts/", include("django.contrib.auth.urls")),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user