mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-01-28 22:59:03 -06:00
Compare commits
9 Commits
42e77ae614
...
ebd1689509
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebd1689509 | ||
|
|
3dc7cf3da1 | ||
|
|
819f606335 | ||
|
|
ad45e3f747 | ||
|
|
74b10db028 | ||
|
|
cffb9c34f0 | ||
|
|
6f52614817 | ||
|
|
a0d3527d20 | ||
|
|
4e64ca7ca6 |
@@ -294,6 +294,9 @@ The following methods are supported:
|
|||||||
- `"delete_original": true` to delete the original documents after editing.
|
- `"delete_original": true` to delete the original documents after editing.
|
||||||
- `"update_document": true` to update the existing document with the edited PDF.
|
- `"update_document": true` to update the existing document with the edited PDF.
|
||||||
- `"include_metadata": true` to copy metadata from the original document to the edited document.
|
- `"include_metadata": true` to copy metadata from the original document to the edited document.
|
||||||
|
- `remove_password`
|
||||||
|
- Requires `parameters`:
|
||||||
|
- `"password": "PASSWORD_STRING"` The password to remove from the PDF documents.
|
||||||
- `merge`
|
- `merge`
|
||||||
- No additional `parameters` required.
|
- No additional `parameters` required.
|
||||||
- The ordering of the merged document is determined by the list of IDs.
|
- The ordering of the merged document is determined by the list of IDs.
|
||||||
|
|||||||
@@ -297,11 +297,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">82</context>
|
<context context-type="linenumber">84</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">84</context>
|
<context context-type="linenumber">86</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>
|
||||||
@@ -316,11 +316,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">89</context>
|
<context context-type="linenumber">91</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">91</context>
|
<context context-type="linenumber">93</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>
|
||||||
@@ -363,11 +363,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">253</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/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">257</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2501522447884928778" datatype="html">
|
<trans-unit id="2501522447884928778" datatype="html">
|
||||||
@@ -658,11 +658,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">288</context>
|
<context context-type="linenumber">290</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">291</context>
|
<context context-type="linenumber">293</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2272120016352772836" datatype="html">
|
<trans-unit id="2272120016352772836" datatype="html">
|
||||||
@@ -672,11 +672,33 @@
|
|||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">4</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="8461842260159597706" datatype="html">
|
||||||
|
<source>Show</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
|
<context context-type="linenumber">8</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
|
<context context-type="linenumber">37</context>
|
||||||
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
|
<context context-type="linenumber">52</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="5724363929304709833" datatype="html">
|
||||||
|
<source>lines</source>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
|
<context context-type="linenumber">17</context>
|
||||||
|
</context-group>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="8838884664569764142" datatype="html">
|
<trans-unit id="8838884664569764142" datatype="html">
|
||||||
<source>Auto refresh</source>
|
<source>Auto refresh</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
<context context-type="linenumber">8</context>
|
<context context-type="linenumber">21</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
@@ -687,11 +709,11 @@
|
|||||||
<source>Loading...</source>
|
<source>Loading...</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
<context context-type="linenumber">24</context>
|
<context context-type="linenumber">38</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
<context context-type="linenumber">39</context>
|
<context context-type="linenumber">53</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
@@ -1003,11 +1025,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">213</context>
|
<context context-type="linenumber">215</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">215</context>
|
<context context-type="linenumber">217</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
@@ -1572,7 +1594,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">167</context>
|
<context context-type="linenumber">180</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2991443309752293110" datatype="html">
|
<trans-unit id="2991443309752293110" datatype="html">
|
||||||
@@ -1583,11 +1605,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">276</context>
|
<context context-type="linenumber">278</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">278</context>
|
<context context-type="linenumber">280</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="103921551219467537" datatype="html">
|
<trans-unit id="103921551219467537" datatype="html">
|
||||||
@@ -1999,11 +2021,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">236</context>
|
<context context-type="linenumber">238</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">239</context>
|
<context context-type="linenumber">241</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3818027200170621545" datatype="html">
|
<trans-unit id="3818027200170621545" datatype="html">
|
||||||
@@ -2368,11 +2390,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">267</context>
|
<context context-type="linenumber">269</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">269</context>
|
<context context-type="linenumber">271</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4569276013106377105" datatype="html">
|
<trans-unit id="4569276013106377105" datatype="html">
|
||||||
@@ -2709,58 +2731,58 @@
|
|||||||
</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">297</context>
|
<context context-type="linenumber">299</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">300</context>
|
<context context-type="linenumber">302</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="472206565520537964" datatype="html">
|
<trans-unit id="472206565520537964" datatype="html">
|
||||||
<source>Saved views</source>
|
<source>Saved views</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">99</context>
|
<context context-type="linenumber">101</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">104</context>
|
<context context-type="linenumber">106</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6988090220128974198" datatype="html">
|
<trans-unit id="6988090220128974198" datatype="html">
|
||||||
<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">139</context>
|
<context context-type="linenumber">141</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">159</context>
|
<context context-type="linenumber">161</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">161</context>
|
<context context-type="linenumber">163</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">170</context>
|
<context context-type="linenumber">172</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">176</context>
|
<context context-type="linenumber">178</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">180</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>
|
||||||
@@ -2771,11 +2793,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">183</context>
|
<context context-type="linenumber">185</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">186</context>
|
<context context-type="linenumber">188</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>
|
||||||
@@ -2806,11 +2828,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">192</context>
|
<context context-type="linenumber">194</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">194</context>
|
<context context-type="linenumber">196</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>
|
||||||
@@ -2821,11 +2843,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">199</context>
|
<context context-type="linenumber">201</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">201</context>
|
<context context-type="linenumber">203</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>
|
||||||
@@ -2836,11 +2858,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">206</context>
|
<context context-type="linenumber">208</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">208</context>
|
<context context-type="linenumber">210</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>
|
||||||
@@ -2855,11 +2877,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">222</context>
|
<context context-type="linenumber">224</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">224</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/manage/workflows/workflows.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
||||||
@@ -2870,92 +2892,92 @@
|
|||||||
<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">229</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">232</context>
|
<context context-type="linenumber">234</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">247</context>
|
<context context-type="linenumber">249</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">260</context>
|
<context context-type="linenumber">262</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">262</context>
|
<context context-type="linenumber">264</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">307</context>
|
<context context-type="linenumber">309</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">316,317</context>
|
<context context-type="linenumber">318,319</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">317</context>
|
<context context-type="linenumber">319</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">321</context>
|
<context context-type="linenumber">323</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">328,330</context>
|
<context context-type="linenumber">330,332</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">341</context>
|
<context context-type="linenumber">343</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">251</context>
|
<context context-type="linenumber">264</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">254</context>
|
<context context-type="linenumber">267</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">275</context>
|
<context context-type="linenumber">288</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4580988005648117665" datatype="html">
|
<trans-unit id="4580988005648117665" datatype="html">
|
||||||
@@ -7881,17 +7903,6 @@
|
|||||||
<context context-type="linenumber">45</context>
|
<context context-type="linenumber">45</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8461842260159597706" datatype="html">
|
|
||||||
<source>Show</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
|
||||||
<context context-type="linenumber">37</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
|
||||||
<context context-type="linenumber">52</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5146398958364876914" datatype="html">
|
<trans-unit id="5146398958364876914" datatype="html">
|
||||||
<source>Sort</source>
|
<source>Sort</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
|
|||||||
@@ -3,9 +3,23 @@
|
|||||||
i18n-title
|
i18n-title
|
||||||
info="Review the log files for the application and for email checking."
|
info="Review the log files for the application and for email checking."
|
||||||
i18n-info>
|
i18n-info>
|
||||||
<div class="form-check form-switch">
|
<div class="input-group input-group-sm align-items-center">
|
||||||
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
<div class="input-group input-group-sm me-3">
|
||||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
<span class="input-group-text text-muted" i18n>Show</span>
|
||||||
|
<input
|
||||||
|
class="form-control"
|
||||||
|
type="number"
|
||||||
|
min="100"
|
||||||
|
step="100"
|
||||||
|
[(ngModel)]="limit"
|
||||||
|
(ngModelChange)="onLimitChange($event)"
|
||||||
|
style="width: 100px;">
|
||||||
|
<span class="input-group-text text-muted" i18n>lines</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-switch mt-1">
|
||||||
|
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||||
|
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ describe('LogsComponent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should display logs with first log initially', () => {
|
it('should display logs with first log initially', () => {
|
||||||
expect(logSpy).toHaveBeenCalledWith('paperless')
|
expect(logSpy).toHaveBeenCalledWith('paperless', 5000)
|
||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||||
paperless_logs[0]
|
paperless_logs[0]
|
||||||
@@ -78,7 +78,7 @@ describe('LogsComponent', () => {
|
|||||||
fixture.debugElement
|
fixture.debugElement
|
||||||
.queryAll(By.directive(NgbNavLink))[1]
|
.queryAll(By.directive(NgbNavLink))[1]
|
||||||
.nativeElement.dispatchEvent(new MouseEvent('click'))
|
.nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||||
expect(logSpy).toHaveBeenCalledWith('mail')
|
expect(logSpy).toHaveBeenCalledWith('mail', 5000)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should handle error with no logs', () => {
|
it('should handle error with no logs', () => {
|
||||||
@@ -101,4 +101,13 @@ describe('LogsComponent', () => {
|
|||||||
jest.advanceTimersByTime(6000)
|
jest.advanceTimersByTime(6000)
|
||||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should debounce limit changes before reloading logs', () => {
|
||||||
|
const initialCalls = reloadSpy.mock.calls.length
|
||||||
|
component.onLimitChange(6000)
|
||||||
|
jest.advanceTimersByTime(299)
|
||||||
|
expect(reloadSpy).toHaveBeenCalledTimes(initialCalls)
|
||||||
|
jest.advanceTimersByTime(1)
|
||||||
|
expect(reloadSpy).toHaveBeenCalledTimes(initialCalls + 1)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { filter, takeUntil, timer } from 'rxjs'
|
import { Subject, debounceTime, filter, takeUntil, timer } from 'rxjs'
|
||||||
import { LogService } from 'src/app/services/rest/log.service'
|
import { LogService } from 'src/app/services/rest/log.service'
|
||||||
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
@@ -47,9 +47,17 @@ export class LogsComponent
|
|||||||
|
|
||||||
public autoRefreshEnabled: boolean = true
|
public autoRefreshEnabled: boolean = true
|
||||||
|
|
||||||
|
public limit: number = 5000
|
||||||
|
|
||||||
|
private readonly limitChange$ = new Subject<number>()
|
||||||
|
|
||||||
@ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
|
@ViewChild('logContainer') logContainer: CdkVirtualScrollViewport
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.limitChange$
|
||||||
|
.pipe(debounceTime(300), takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe(() => this.reloadLogs())
|
||||||
|
|
||||||
this.logService
|
this.logService
|
||||||
.list()
|
.list()
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
@@ -75,16 +83,33 @@ export class LogsComponent
|
|||||||
super.ngOnDestroy()
|
super.ngOnDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLimitChange(limit: number): void {
|
||||||
|
this.limitChange$.next(limit)
|
||||||
|
}
|
||||||
|
|
||||||
reloadLogs() {
|
reloadLogs() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.logService
|
this.logService
|
||||||
.get(this.activeLog)
|
.get(this.activeLog, this.limit)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
this.logs = this.parseLogsWithLevel(result)
|
|
||||||
this.loading = false
|
this.loading = false
|
||||||
this.scrollToBottom()
|
const parsed = this.parseLogsWithLevel(result)
|
||||||
|
const hasChanges =
|
||||||
|
parsed.length !== this.logs.length ||
|
||||||
|
parsed.some((log, idx) => {
|
||||||
|
const current = this.logs[idx]
|
||||||
|
return (
|
||||||
|
!current ||
|
||||||
|
current.message !== log.message ||
|
||||||
|
current.level !== log.level
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if (hasChanges) {
|
||||||
|
this.logs = parsed
|
||||||
|
this.scrollToBottom()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
this.logs = []
|
this.logs = []
|
||||||
|
|||||||
@@ -68,13 +68,15 @@
|
|||||||
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse"
|
<nav id="sidebarMenu" class="d-md-block bg-light sidebar collapse"
|
||||||
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
|
[ngClass]="slimSidebarEnabled ? 'slim' : 'col-md-3 col-lg-2 col-xxxl-1'" [class.animating]="slimSidebarAnimating"
|
||||||
[ngbCollapse]="isMenuCollapsed">
|
[ngbCollapse]="isMenuCollapsed">
|
||||||
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
@if (canSaveSettings) {
|
||||||
@if (slimSidebarEnabled) {
|
<button class="btn btn-sm btn-dark sidebar-slim-toggler" (click)="toggleSlimSidebar()">
|
||||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
@if (slimSidebarEnabled) {
|
||||||
} @else {
|
<i-bs width="0.9em" height="0.9em" name="chevron-double-right"></i-bs>
|
||||||
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
} @else {
|
||||||
}
|
<i-bs width="0.9em" height="0.9em" name="chevron-double-left"></i-bs>
|
||||||
</button>
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
<div class="sidebar-sticky pt-3 d-flex flex-column justify-space-around">
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item app-link">
|
<li class="nav-item app-link">
|
||||||
|
|||||||
@@ -152,6 +152,19 @@ export class AppFrameComponent
|
|||||||
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
return this.settingsService.get(SETTINGS_KEYS.APP_TITLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canSaveSettings(): boolean {
|
||||||
|
return (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.Change,
|
||||||
|
PermissionType.UISettings
|
||||||
|
) &&
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.Add,
|
||||||
|
PermissionType.UISettings
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
get slimSidebarEnabled(): boolean {
|
get slimSidebarEnabled(): boolean {
|
||||||
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
return this.settingsService.get(SETTINGS_KEYS.SLIM_SIDEBAR)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,12 @@
|
|||||||
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
<button ngbDropdownItem (click)="editPdf()" [disabled]="!userIsOwner || !userCanEdit || originalContentRenderType !== ContentRenderType.PDF">
|
||||||
<i-bs name="pencil"></i-bs> <ng-container i18n>PDF Editor</ng-container>
|
<i-bs name="pencil"></i-bs> <ng-container i18n>PDF Editor</ng-container>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
@if (requiresPassword || password) {
|
||||||
|
<button ngbDropdownItem (click)="removePassword()" [disabled]="!userIsOwner || !password">
|
||||||
|
<i-bs name="unlock"></i-bs> <ng-container i18n>Remove Password</ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1428,6 +1428,37 @@ export class DocumentDetailComponent
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removePassword() {
|
||||||
|
if (this.requiresPassword || !this.password) {
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Please enter the current password before attempting to remove it.`
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.networkActive = true
|
||||||
|
this.documentsService
|
||||||
|
.bulkEdit([this.document.id], 'remove_password', {
|
||||||
|
password: this.password,
|
||||||
|
})
|
||||||
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.toastService.showInfo(
|
||||||
|
$localize`Password removal operation for "${this.document.title}" will begin in the background.`
|
||||||
|
)
|
||||||
|
this.networkActive = false
|
||||||
|
this.openDocumentService.refreshDocument(this.documentId)
|
||||||
|
},
|
||||||
|
error: (error) => {
|
||||||
|
this.networkActive = false
|
||||||
|
this.toastService.showError(
|
||||||
|
$localize`Error executing password removal operation`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
printDocument() {
|
printDocument() {
|
||||||
const printUrl = this.documentsService.getDownloadUrl(
|
const printUrl = this.documentsService.getDownloadUrl(
|
||||||
this.document.id,
|
this.document.id,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<ng-template #errorPopover>
|
<ng-template #errorPopover>
|
||||||
<pre class="small text-light">
|
<pre class="small">
|
||||||
{{ mail.error }}
|
{{ mail.error }}
|
||||||
</pre>
|
</pre>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
::ng-deep .popover {
|
::ng-deep .popover {
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
|
|||||||
@@ -49,4 +49,14 @@ describe('LogService', () => {
|
|||||||
)
|
)
|
||||||
expect(req.request.method).toEqual('GET')
|
expect(req.request.method).toEqual('GET')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should pass limit param on logs get when provided', () => {
|
||||||
|
const id: string = 'mail'
|
||||||
|
const limit: number = 100
|
||||||
|
subscription = service.get(id, limit).subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}${endpoint}/${id}/?limit=${limit}`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('GET')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HttpClient } from '@angular/common/http'
|
import { HttpClient, HttpParams } from '@angular/common/http'
|
||||||
import { Injectable, inject } from '@angular/core'
|
import { Injectable, inject } from '@angular/core'
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs'
|
||||||
import { environment } from 'src/environments/environment'
|
import { environment } from 'src/environments/environment'
|
||||||
@@ -13,7 +13,13 @@ export class LogService {
|
|||||||
return this.http.get<string[]>(`${environment.apiBaseUrl}logs/`)
|
return this.http.get<string[]>(`${environment.apiBaseUrl}logs/`)
|
||||||
}
|
}
|
||||||
|
|
||||||
get(id: string): Observable<string[]> {
|
get(id: string, limit?: number): Observable<string[]> {
|
||||||
return this.http.get<string[]>(`${environment.apiBaseUrl}logs/${id}/`)
|
let params = new HttpParams()
|
||||||
|
if (limit !== undefined) {
|
||||||
|
params = params.set('limit', limit.toString())
|
||||||
|
}
|
||||||
|
return this.http.get<string[]>(`${environment.apiBaseUrl}logs/${id}/`, {
|
||||||
|
params,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ import {
|
|||||||
threeDotsVertical,
|
threeDotsVertical,
|
||||||
trash,
|
trash,
|
||||||
uiRadios,
|
uiRadios,
|
||||||
|
unlock,
|
||||||
upcScan,
|
upcScan,
|
||||||
windowStack,
|
windowStack,
|
||||||
x,
|
x,
|
||||||
@@ -346,6 +347,7 @@ const icons = {
|
|||||||
threeDotsVertical,
|
threeDotsVertical,
|
||||||
trash,
|
trash,
|
||||||
uiRadios,
|
uiRadios,
|
||||||
|
unlock,
|
||||||
upcScan,
|
upcScan,
|
||||||
windowStack,
|
windowStack,
|
||||||
x,
|
x,
|
||||||
|
|||||||
@@ -644,6 +644,77 @@ def edit_pdf(
|
|||||||
return "OK"
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
|
def remove_password(
|
||||||
|
doc_ids: list[int],
|
||||||
|
password: str,
|
||||||
|
*,
|
||||||
|
delete_original: bool = False,
|
||||||
|
update_document: bool = False,
|
||||||
|
include_metadata: bool = True,
|
||||||
|
user: User | None = None,
|
||||||
|
) -> Literal["OK"]:
|
||||||
|
"""
|
||||||
|
Remove password protection from PDF documents.
|
||||||
|
"""
|
||||||
|
import pikepdf
|
||||||
|
|
||||||
|
for doc_id in doc_ids:
|
||||||
|
doc = Document.objects.get(id=doc_id)
|
||||||
|
try:
|
||||||
|
logger.info(
|
||||||
|
f"Attempting password removal from document {doc_ids[0]}",
|
||||||
|
)
|
||||||
|
with pikepdf.open(doc.source_path, password=password) as pdf:
|
||||||
|
temp_path = doc.source_path.with_suffix(".tmp.pdf")
|
||||||
|
pdf.remove_unreferenced_resources()
|
||||||
|
pdf.save(temp_path)
|
||||||
|
|
||||||
|
if update_document:
|
||||||
|
# replace the original document with the unprotected one
|
||||||
|
temp_path.replace(doc.source_path)
|
||||||
|
doc.checksum = hashlib.md5(doc.source_path.read_bytes()).hexdigest()
|
||||||
|
doc.page_count = len(pdf.pages)
|
||||||
|
doc.save()
|
||||||
|
update_document_content_maybe_archive_file.delay(document_id=doc.id)
|
||||||
|
else:
|
||||||
|
consume_tasks = []
|
||||||
|
overrides = (
|
||||||
|
DocumentMetadataOverrides().from_document(doc)
|
||||||
|
if include_metadata
|
||||||
|
else DocumentMetadataOverrides()
|
||||||
|
)
|
||||||
|
if user is not None:
|
||||||
|
overrides.owner_id = user.id
|
||||||
|
|
||||||
|
filepath: Path = (
|
||||||
|
Path(tempfile.mkdtemp(dir=settings.SCRATCH_DIR))
|
||||||
|
/ f"{doc.id}_unprotected.pdf"
|
||||||
|
)
|
||||||
|
temp_path.replace(filepath)
|
||||||
|
consume_tasks.append(
|
||||||
|
consume_file.s(
|
||||||
|
ConsumableDocument(
|
||||||
|
source=DocumentSource.ConsumeFolder,
|
||||||
|
original_file=filepath,
|
||||||
|
),
|
||||||
|
overrides,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
if delete_original:
|
||||||
|
chord(header=consume_tasks, body=delete.si([doc.id])).delay()
|
||||||
|
else:
|
||||||
|
group(consume_tasks).delay()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Error removing password from document {doc.id}: {e}")
|
||||||
|
raise ValueError(
|
||||||
|
f"An error occurred while removing the password: {e}",
|
||||||
|
) from e
|
||||||
|
|
||||||
|
return "OK"
|
||||||
|
|
||||||
|
|
||||||
def reflect_doclinks(
|
def reflect_doclinks(
|
||||||
document: Document,
|
document: Document,
|
||||||
field: CustomField,
|
field: CustomField,
|
||||||
|
|||||||
@@ -1041,7 +1041,7 @@ class DocumentSerializer(
|
|||||||
request.version if request else settings.REST_FRAMEWORK["DEFAULT_VERSION"],
|
request.version if request else settings.REST_FRAMEWORK["DEFAULT_VERSION"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if api_version < 9:
|
if api_version < 9 and "created" in self.fields:
|
||||||
# provide created as a datetime for backwards compatibility
|
# provide created as a datetime for backwards compatibility
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
@@ -1400,6 +1400,7 @@ class BulkEditSerializer(
|
|||||||
"split",
|
"split",
|
||||||
"delete_pages",
|
"delete_pages",
|
||||||
"edit_pdf",
|
"edit_pdf",
|
||||||
|
"remove_password",
|
||||||
],
|
],
|
||||||
label="Method",
|
label="Method",
|
||||||
write_only=True,
|
write_only=True,
|
||||||
@@ -1475,6 +1476,8 @@ class BulkEditSerializer(
|
|||||||
return bulk_edit.delete_pages
|
return bulk_edit.delete_pages
|
||||||
elif method == "edit_pdf":
|
elif method == "edit_pdf":
|
||||||
return bulk_edit.edit_pdf
|
return bulk_edit.edit_pdf
|
||||||
|
elif method == "remove_password":
|
||||||
|
return bulk_edit.remove_password
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
# This will never happen as it is handled by the ChoiceField
|
# This will never happen as it is handled by the ChoiceField
|
||||||
raise serializers.ValidationError("Unsupported method.")
|
raise serializers.ValidationError("Unsupported method.")
|
||||||
@@ -1671,6 +1674,12 @@ class BulkEditSerializer(
|
|||||||
f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.",
|
f"Page {op['page']} is out of bounds for document with {doc.page_count} pages.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_parameters_remove_password(self, parameters):
|
||||||
|
if "password" not in parameters:
|
||||||
|
raise serializers.ValidationError("password not specified")
|
||||||
|
if not isinstance(parameters["password"], str):
|
||||||
|
raise serializers.ValidationError("password must be a string")
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
method = attrs["method"]
|
method = attrs["method"]
|
||||||
parameters = attrs["parameters"]
|
parameters = attrs["parameters"]
|
||||||
@@ -1711,6 +1720,8 @@ class BulkEditSerializer(
|
|||||||
"Edit PDF method only supports one document",
|
"Edit PDF method only supports one document",
|
||||||
)
|
)
|
||||||
self._validate_parameters_edit_pdf(parameters, attrs["documents"][0])
|
self._validate_parameters_edit_pdf(parameters, attrs["documents"][0])
|
||||||
|
elif method == bulk_edit.remove_password:
|
||||||
|
self.validate_parameters_remove_password(parameters)
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
|
|||||||
@@ -172,6 +172,35 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
results = response.data["results"]
|
results = response.data["results"]
|
||||||
self.assertEqual(len(results[0]), 0)
|
self.assertEqual(len(results[0]), 0)
|
||||||
|
|
||||||
|
def test_document_fields_api_version_8_respects_created(self):
|
||||||
|
Document.objects.create(
|
||||||
|
title="legacy",
|
||||||
|
checksum="123",
|
||||||
|
mime_type="application/pdf",
|
||||||
|
created=date(2024, 1, 15),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/documents/?fields=id",
|
||||||
|
headers={"Accept": "application/json; version=8"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
results = response.data["results"]
|
||||||
|
self.assertIn("id", results[0])
|
||||||
|
self.assertNotIn("created", results[0])
|
||||||
|
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/documents/?fields=id,created",
|
||||||
|
headers={"Accept": "application/json; version=8"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
results = response.data["results"]
|
||||||
|
self.assertIn("id", results[0])
|
||||||
|
self.assertIn("created", results[0])
|
||||||
|
self.assertRegex(results[0]["created"], r"^2024-01-15T00:00:00.*$")
|
||||||
|
|
||||||
def test_document_legacy_created_format(self):
|
def test_document_legacy_created_format(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@@ -2250,6 +2279,23 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
self.assertListEqual(response.data, ["test", "test2"])
|
self.assertListEqual(response.data, ["test", "test2"])
|
||||||
|
|
||||||
|
def test_get_log_with_limit(self):
|
||||||
|
log_data = "test1\ntest2\ntest3\n"
|
||||||
|
with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
|
||||||
|
f.write(log_data)
|
||||||
|
response = self.client.get("/api/logs/paperless/", {"limit": 2})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertListEqual(response.data, ["test2", "test3"])
|
||||||
|
|
||||||
|
def test_get_log_with_invalid_limit(self):
|
||||||
|
log_data = "test1\ntest2\n"
|
||||||
|
with (Path(settings.LOGGING_DIR) / "paperless.log").open("w") as f:
|
||||||
|
f.write(log_data)
|
||||||
|
response = self.client.get("/api/logs/paperless/", {"limit": "abc"})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
response = self.client.get("/api/logs/paperless/", {"limit": -5})
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
def test_invalid_regex_other_algorithm(self):
|
def test_invalid_regex_other_algorithm(self):
|
||||||
for endpoint in ["correspondents", "tags", "document_types"]:
|
for endpoint in ["correspondents", "tags", "document_types"]:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import re
|
|||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from time import mktime
|
from time import mktime
|
||||||
@@ -70,6 +71,7 @@ from rest_framework import parsers
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
from rest_framework.exceptions import NotFound
|
from rest_framework.exceptions import NotFound
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.filters import OrderingFilter
|
from rest_framework.filters import OrderingFilter
|
||||||
from rest_framework.filters import SearchFilter
|
from rest_framework.filters import SearchFilter
|
||||||
from rest_framework.generics import GenericAPIView
|
from rest_framework.generics import GenericAPIView
|
||||||
@@ -1363,6 +1365,13 @@ class UnifiedSearchViewSet(DocumentViewSet):
|
|||||||
type=OpenApiTypes.STR,
|
type=OpenApiTypes.STR,
|
||||||
location=OpenApiParameter.PATH,
|
location=OpenApiParameter.PATH,
|
||||||
),
|
),
|
||||||
|
OpenApiParameter(
|
||||||
|
name="limit",
|
||||||
|
type=OpenApiTypes.INT,
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
description="Return only the last N entries from the log file",
|
||||||
|
required=False,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
responses={
|
responses={
|
||||||
(200, "application/json"): serializers.ListSerializer(
|
(200, "application/json"): serializers.ListSerializer(
|
||||||
@@ -1394,8 +1403,22 @@ class LogViewSet(ViewSet):
|
|||||||
if not log_file.is_file():
|
if not log_file.is_file():
|
||||||
raise Http404
|
raise Http404
|
||||||
|
|
||||||
|
limit_param = request.query_params.get("limit")
|
||||||
|
if limit_param is not None:
|
||||||
|
try:
|
||||||
|
limit = int(limit_param)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
raise ValidationError({"limit": "Must be a positive integer"})
|
||||||
|
if limit < 1:
|
||||||
|
raise ValidationError({"limit": "Must be a positive integer"})
|
||||||
|
else:
|
||||||
|
limit = None
|
||||||
|
|
||||||
with log_file.open() as f:
|
with log_file.open() as f:
|
||||||
lines = [line.rstrip() for line in f.readlines()]
|
if limit is None:
|
||||||
|
lines = [line.rstrip() for line in f.readlines()]
|
||||||
|
else:
|
||||||
|
lines = [line.rstrip() for line in deque(f, maxlen=limit)]
|
||||||
|
|
||||||
return Response(lines)
|
return Response(lines)
|
||||||
|
|
||||||
@@ -1463,6 +1486,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
"merge": None,
|
"merge": None,
|
||||||
"edit_pdf": "checksum",
|
"edit_pdf": "checksum",
|
||||||
"reprocess": "checksum",
|
"reprocess": "checksum",
|
||||||
|
"remove_password": "checksum",
|
||||||
}
|
}
|
||||||
|
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
@@ -1481,6 +1505,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
bulk_edit.split,
|
bulk_edit.split,
|
||||||
bulk_edit.merge,
|
bulk_edit.merge,
|
||||||
bulk_edit.edit_pdf,
|
bulk_edit.edit_pdf,
|
||||||
|
bulk_edit.remove_password,
|
||||||
]:
|
]:
|
||||||
parameters["user"] = user
|
parameters["user"] = user
|
||||||
|
|
||||||
@@ -1509,6 +1534,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
bulk_edit.rotate,
|
bulk_edit.rotate,
|
||||||
bulk_edit.delete_pages,
|
bulk_edit.delete_pages,
|
||||||
bulk_edit.edit_pdf,
|
bulk_edit.edit_pdf,
|
||||||
|
bulk_edit.remove_password,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
or (
|
or (
|
||||||
@@ -1525,7 +1551,7 @@ class BulkEditView(PassUserMixin):
|
|||||||
and (
|
and (
|
||||||
method in [bulk_edit.split, bulk_edit.merge]
|
method in [bulk_edit.split, bulk_edit.merge]
|
||||||
or (
|
or (
|
||||||
method == bulk_edit.edit_pdf
|
method in [bulk_edit.edit_pdf, bulk_edit.remove_password]
|
||||||
and not parameters["update_document"]
|
and not parameters["update_document"]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,6 +53,15 @@ class TestUrlCanary:
|
|||||||
Verify certain URLs are still available so testing is valid still
|
Verify certain URLs are still available so testing is valid still
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Wikimedia rejects requests without a browser-like User-Agent header and returns 403.
|
||||||
|
_WIKIMEDIA_HEADERS = {
|
||||||
|
"User-Agent": (
|
||||||
|
"Mozilla/5.0 (X11; Linux x86_64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/123.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def test_online_image_exception_on_not_available(self):
|
def test_online_image_exception_on_not_available(self):
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@@ -70,6 +79,7 @@ class TestUrlCanary:
|
|||||||
with pytest.raises(httpx.HTTPStatusError) as exec_info:
|
with pytest.raises(httpx.HTTPStatusError) as exec_info:
|
||||||
resp = httpx.get(
|
resp = httpx.get(
|
||||||
"https://upload.wikimedia.org/wikipedia/en/f/f7/nonexistent.png",
|
"https://upload.wikimedia.org/wikipedia/en/f/f7/nonexistent.png",
|
||||||
|
headers=self._WIKIMEDIA_HEADERS,
|
||||||
)
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
@@ -90,7 +100,10 @@ class TestUrlCanary:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Now check the URL used in samples/sample.html
|
# Now check the URL used in samples/sample.html
|
||||||
resp = httpx.get("https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png")
|
resp = httpx.get(
|
||||||
|
"https://upload.wikimedia.org/wikipedia/en/f/f7/RickRoll.png",
|
||||||
|
headers=self._WIKIMEDIA_HEADERS,
|
||||||
|
)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user