mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-13 22:33:19 +00:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acaf908ca4 | ||
|
|
01c12d9ea4 | ||
|
|
f5c0d118f7 | ||
|
|
ff13847d0a | ||
|
|
634f803872 | ||
|
|
639d566a7c | ||
|
|
0a94f8f0d4 |
@@ -299,6 +299,8 @@ optional arguments:
|
|||||||
-sm, --split-manifest
|
-sm, --split-manifest
|
||||||
-z, --zip
|
-z, --zip
|
||||||
-zn, --zip-name
|
-zn, --zip-name
|
||||||
|
--zip-compression
|
||||||
|
--zip-compression-level
|
||||||
--data-only
|
--data-only
|
||||||
--no-progress-bar
|
--no-progress-bar
|
||||||
--passphrase
|
--passphrase
|
||||||
@@ -361,6 +363,19 @@ If `-z` or `--zip` is provided, the export will be a zip file
|
|||||||
in the target directory, named according to the current local date or the
|
in the target directory, named according to the current local date or the
|
||||||
value set in `-zn` or `--zip-name`.
|
value set in `-zn` or `--zip-name`.
|
||||||
|
|
||||||
|
The compression method for the zip can be set with `--zip-compression`
|
||||||
|
(`stored`, `deflated` (default), `bzip2`, `lzma`, or `zstd`) and tuned with
|
||||||
|
`--zip-compression-level` (deflated: 0–9, bzip2: 1–9, zstd: -22–22; ignored
|
||||||
|
for `stored` and `lzma`). Both options require `--zip`.
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
`zstd` compression requires Python 3.14 or newer on **both** the machine
|
||||||
|
creating the export and any machine importing it. An archive compressed with
|
||||||
|
`zstd` (or `lzma`/`bzip2` where those modules are unavailable) cannot be
|
||||||
|
imported on a runtime that lacks the codec; the importer will refuse it with
|
||||||
|
a clear error. The default `deflated` is universally readable.
|
||||||
|
|
||||||
If `--data-only` is provided, only the database will be exported. This option is intended
|
If `--data-only` is provided, only the database will be exported. This option is intended
|
||||||
to facilitate database upgrades without needing to clean documents and thumbnails from the media directory.
|
to facilitate database upgrades without needing to clean documents and thumbnails from the media directory.
|
||||||
|
|
||||||
|
|||||||
+53
-53
@@ -539,7 +539,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">47</context>
|
<context context-type="linenumber">54</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<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="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
|
||||||
@@ -3098,15 +3098,15 @@
|
|||||||
</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>
|
||||||
<context context-type="linenumber">919</context>
|
<context context-type="linenumber">910</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>
|
||||||
<context context-type="linenumber">955</context>
|
<context context-type="linenumber">946</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>
|
||||||
<context context-type="linenumber">978</context>
|
<context context-type="linenumber">969</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/document-attributes/custom-fields/custom-fields.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/custom-fields/custom-fields.component.ts</context>
|
||||||
@@ -3684,14 +3684,14 @@
|
|||||||
<source>Confirmation</source>
|
<source>Confirmation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">23</context>
|
<context context-type="linenumber">30</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9178182467454450952" datatype="html">
|
<trans-unit id="9178182467454450952" datatype="html">
|
||||||
<source>Confirm</source>
|
<source>Confirm</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
||||||
<context context-type="linenumber">35</context>
|
<context context-type="linenumber">42</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/permissions-dialog/permissions-dialog.component.html</context>
|
||||||
@@ -3703,27 +3703,27 @@
|
|||||||
</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>
|
||||||
<context context-type="linenumber">556</context>
|
<context context-type="linenumber">547</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>
|
||||||
<context context-type="linenumber">596</context>
|
<context context-type="linenumber">587</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>
|
||||||
<context context-type="linenumber">634</context>
|
<context context-type="linenumber">625</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>
|
||||||
<context context-type="linenumber">672</context>
|
<context context-type="linenumber">663</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>
|
||||||
<context context-type="linenumber">734</context>
|
<context context-type="linenumber">725</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>
|
||||||
<context context-type="linenumber">867</context>
|
<context context-type="linenumber">858</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="994016933065248559" datatype="html">
|
<trans-unit id="994016933065248559" datatype="html">
|
||||||
@@ -5743,7 +5743,7 @@
|
|||||||
</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>
|
||||||
<context context-type="linenumber">871</context>
|
<context context-type="linenumber">862</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4522609911791833187" datatype="html">
|
<trans-unit id="4522609911791833187" datatype="html">
|
||||||
@@ -7613,7 +7613,7 @@
|
|||||||
</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>
|
||||||
<context context-type="linenumber">501</context>
|
<context context-type="linenumber">492</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note>
|
<note priority="1" from="description">this string is used to separate processing, failed and added on the file upload widget</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -8148,7 +8148,7 @@
|
|||||||
</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>
|
||||||
<context context-type="linenumber">869</context>
|
<context context-type="linenumber">860</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7295637485862454066" datatype="html">
|
<trans-unit id="7295637485862454066" datatype="html">
|
||||||
@@ -8166,7 +8166,7 @@
|
|||||||
</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>
|
||||||
<context context-type="linenumber">915</context>
|
<context context-type="linenumber">906</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2951161989614003846" datatype="html">
|
<trans-unit id="2951161989614003846" datatype="html">
|
||||||
@@ -8523,18 +8523,18 @@
|
|||||||
<source>"<x id="PH" equiv-text="items[0].name"/>"</source>
|
<source>"<x id="PH" equiv-text="items[0].name"/>"</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">493</context>
|
<context context-type="linenumber">484</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>
|
||||||
<context context-type="linenumber">499</context>
|
<context context-type="linenumber">490</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8639884465898458690" datatype="html">
|
<trans-unit id="8639884465898458690" datatype="html">
|
||||||
<source>"<x id="PH" equiv-text="items[0].name"/>" and "<x id="PH_1" equiv-text="items[1].name"/>"</source>
|
<source>"<x id="PH" equiv-text="items[0].name"/>" and "<x id="PH_1" equiv-text="items[1].name"/>"</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">495</context>
|
<context context-type="linenumber">486</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">This is for messages like 'modify "tag1" and "tag2"'</note>
|
<note priority="1" from="description">This is for messages like 'modify "tag1" and "tag2"'</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -8542,7 +8542,7 @@
|
|||||||
<source><x id="PH" equiv-text="list"/> and "<x id="PH_1" equiv-text="items[items.length - 1].name"/>"</source>
|
<source><x id="PH" equiv-text="list"/> and "<x id="PH_1" equiv-text="items[items.length - 1].name"/>"</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">503,505</context>
|
<context context-type="linenumber">494,496</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">this is for messages like 'modify "tag1", "tag2" and "tag3"'</note>
|
<note priority="1" from="description">this is for messages like 'modify "tag1", "tag2" and "tag3"'</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -8550,14 +8550,14 @@
|
|||||||
<source>Confirm tags assignment</source>
|
<source>Confirm tags assignment</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">520</context>
|
<context context-type="linenumber">511</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6619516195038467207" datatype="html">
|
<trans-unit id="6619516195038467207" datatype="html">
|
||||||
<source>This operation will add the tag "<x id="PH" equiv-text="tag.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will add the tag "<x id="PH" equiv-text="tag.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">526</context>
|
<context context-type="linenumber">517</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1894412783609570695" datatype="html">
|
<trans-unit id="1894412783609570695" datatype="html">
|
||||||
@@ -8566,14 +8566,14 @@
|
|||||||
)"/> to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
)"/> to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">531,533</context>
|
<context context-type="linenumber">522,524</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7181166515756808573" datatype="html">
|
<trans-unit id="7181166515756808573" datatype="html">
|
||||||
<source>This operation will remove the tag "<x id="PH" equiv-text="tag.name"/>" from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will remove the tag "<x id="PH" equiv-text="tag.name"/>" from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">539</context>
|
<context context-type="linenumber">530</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3819792277998068944" datatype="html">
|
<trans-unit id="3819792277998068944" datatype="html">
|
||||||
@@ -8582,7 +8582,7 @@
|
|||||||
)"/> from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
)"/> from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">544,546</context>
|
<context context-type="linenumber">535,537</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2739066218579571288" datatype="html">
|
<trans-unit id="2739066218579571288" datatype="html">
|
||||||
@@ -8593,84 +8593,84 @@
|
|||||||
)"/> on <x id="PH_2" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
)"/> on <x id="PH_2" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">548,552</context>
|
<context context-type="linenumber">539,543</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2996713129519325161" datatype="html">
|
<trans-unit id="2996713129519325161" datatype="html">
|
||||||
<source>Confirm correspondent assignment</source>
|
<source>Confirm correspondent assignment</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">589</context>
|
<context context-type="linenumber">580</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6900893559485781849" datatype="html">
|
<trans-unit id="6900893559485781849" datatype="html">
|
||||||
<source>This operation will assign the correspondent "<x id="PH" equiv-text="correspondent.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will assign the correspondent "<x id="PH" equiv-text="correspondent.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">591</context>
|
<context context-type="linenumber">582</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1257522660364398440" datatype="html">
|
<trans-unit id="1257522660364398440" datatype="html">
|
||||||
<source>This operation will remove the correspondent from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will remove the correspondent from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">593</context>
|
<context context-type="linenumber">584</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5393409374423140648" datatype="html">
|
<trans-unit id="5393409374423140648" datatype="html">
|
||||||
<source>Confirm document type assignment</source>
|
<source>Confirm document type assignment</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">627</context>
|
<context context-type="linenumber">618</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="332180123895325027" datatype="html">
|
<trans-unit id="332180123895325027" datatype="html">
|
||||||
<source>This operation will assign the document type "<x id="PH" equiv-text="documentType.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will assign the document type "<x id="PH" equiv-text="documentType.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">629</context>
|
<context context-type="linenumber">620</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2236642492594872779" datatype="html">
|
<trans-unit id="2236642492594872779" datatype="html">
|
||||||
<source>This operation will remove the document type from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will remove the document type from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">631</context>
|
<context context-type="linenumber">622</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6386555513013840736" datatype="html">
|
<trans-unit id="6386555513013840736" datatype="html">
|
||||||
<source>Confirm storage path assignment</source>
|
<source>Confirm storage path assignment</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">665</context>
|
<context context-type="linenumber">656</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8750527458618415924" datatype="html">
|
<trans-unit id="8750527458618415924" datatype="html">
|
||||||
<source>This operation will assign the storage path "<x id="PH" equiv-text="storagePath.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will assign the storage path "<x id="PH" equiv-text="storagePath.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">667</context>
|
<context context-type="linenumber">658</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="60728365335056946" datatype="html">
|
<trans-unit id="60728365335056946" datatype="html">
|
||||||
<source>This operation will remove the storage path from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will remove the storage path from <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">669</context>
|
<context context-type="linenumber">660</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4187352575310415704" datatype="html">
|
<trans-unit id="4187352575310415704" datatype="html">
|
||||||
<source>Confirm custom field assignment</source>
|
<source>Confirm custom field assignment</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">698</context>
|
<context context-type="linenumber">689</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7966494636326273856" datatype="html">
|
<trans-unit id="7966494636326273856" datatype="html">
|
||||||
<source>This operation will assign the custom field "<x id="PH" equiv-text="customField.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will assign the custom field "<x id="PH" equiv-text="customField.name"/>" to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">704</context>
|
<context context-type="linenumber">695</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5789455969634598553" datatype="html">
|
<trans-unit id="5789455969634598553" datatype="html">
|
||||||
@@ -8679,14 +8679,14 @@
|
|||||||
)"/> to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
)"/> to <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">709,711</context>
|
<context context-type="linenumber">700,702</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5648572354333199245" datatype="html">
|
<trans-unit id="5648572354333199245" datatype="html">
|
||||||
<source>This operation will remove the custom field "<x id="PH" equiv-text="customField.name"/>" from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will remove the custom field "<x id="PH" equiv-text="customField.name"/>" from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">717</context>
|
<context context-type="linenumber">708</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6666899594015948817" datatype="html">
|
<trans-unit id="6666899594015948817" datatype="html">
|
||||||
@@ -8695,7 +8695,7 @@
|
|||||||
)"/> from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
)"/> from <x id="PH_1" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">722,724</context>
|
<context context-type="linenumber">713,715</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8050047262594964176" datatype="html">
|
<trans-unit id="8050047262594964176" datatype="html">
|
||||||
@@ -8706,91 +8706,91 @@
|
|||||||
)"/> on <x id="PH_2" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
)"/> on <x id="PH_2" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">726,730</context>
|
<context context-type="linenumber">717,721</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8615059324209654051" datatype="html">
|
<trans-unit id="8615059324209654051" datatype="html">
|
||||||
<source>Move <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s) to the trash?</source>
|
<source>Move <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s) to the trash?</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">868</context>
|
<context context-type="linenumber">859</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8585195717323764335" datatype="html">
|
<trans-unit id="8585195717323764335" datatype="html">
|
||||||
<source>This operation will permanently recreate the archive files for <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
<source>This operation will permanently recreate the archive files for <x id="PH" equiv-text="this.getSelectionSize()"/> selected document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">916</context>
|
<context context-type="linenumber">907</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7366623494074776040" datatype="html">
|
<trans-unit id="7366623494074776040" datatype="html">
|
||||||
<source>The archive files will be re-generated with the current settings.</source>
|
<source>The archive files will be re-generated with the current settings.</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">917</context>
|
<context context-type="linenumber">908</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6555329262222566158" datatype="html">
|
<trans-unit id="6555329262222566158" datatype="html">
|
||||||
<source>Rotate confirm</source>
|
<source>Rotate confirm</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">952</context>
|
<context context-type="linenumber">943</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5203024009814367559" datatype="html">
|
<trans-unit id="5203024009814367559" datatype="html">
|
||||||
<source>This operation will add rotated versions of the <x id="PH" equiv-text="this.getSelectionSize()"/> document(s).</source>
|
<source>This operation will add rotated versions of the <x id="PH" equiv-text="this.getSelectionSize()"/> document(s).</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">953</context>
|
<context context-type="linenumber">944</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7910756456450124185" datatype="html">
|
<trans-unit id="7910756456450124185" datatype="html">
|
||||||
<source>Merge confirm</source>
|
<source>Merge confirm</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">976</context>
|
<context context-type="linenumber">967</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7643543647233874431" datatype="html">
|
<trans-unit id="7643543647233874431" datatype="html">
|
||||||
<source>This operation will merge <x id="PH" equiv-text="this.getSelectionSize()"/> selected documents into a new document.</source>
|
<source>This operation will merge <x id="PH" equiv-text="this.getSelectionSize()"/> selected documents into a new document.</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">977</context>
|
<context context-type="linenumber">968</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7869008840945899895" datatype="html">
|
<trans-unit id="7869008840945899895" datatype="html">
|
||||||
<source>Merged document will be queued for consumption.</source>
|
<source>Merged document will be queued for consumption.</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">1000</context>
|
<context context-type="linenumber">991</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="476913782630693351" datatype="html">
|
<trans-unit id="476913782630693351" datatype="html">
|
||||||
<source>Custom fields updated.</source>
|
<source>Custom fields updated.</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">1025</context>
|
<context context-type="linenumber">1016</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3873496751167944011" datatype="html">
|
<trans-unit id="3873496751167944011" datatype="html">
|
||||||
<source>Error updating custom fields.</source>
|
<source>Error updating custom fields.</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">1034</context>
|
<context context-type="linenumber">1025</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6144801143088984138" datatype="html">
|
<trans-unit id="6144801143088984138" datatype="html">
|
||||||
<source>Share link bundle creation requested.</source>
|
<source>Share link bundle creation requested.</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">1082</context>
|
<context context-type="linenumber">1073</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="46019676931295023" datatype="html">
|
<trans-unit id="46019676931295023" datatype="html">
|
||||||
<source>Share link bundle creation is not available yet.</source>
|
<source>Share link bundle creation is not available yet.</source>
|
||||||
<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>
|
||||||
<context context-type="linenumber">1089</context>
|
<context context-type="linenumber">1080</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6307402210351946694" datatype="html">
|
<trans-unit id="6307402210351946694" datatype="html">
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ describe('TasksComponent', () => {
|
|||||||
|
|
||||||
expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id, tasks[1].id]))
|
expect(dismissSpy).toHaveBeenCalledWith(new Set([tasks[0].id, tasks[1].id]))
|
||||||
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
|
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
|
||||||
expect(modal.componentInstance.buttonsEnabled).toBe(true)
|
expect(modal.componentInstance.buttonsEnabled()).toBe(true)
|
||||||
expect(component.selectedTasks.size).toBe(0)
|
expect(component.selectedTasks.size).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -642,7 +642,7 @@ describe('TasksComponent', () => {
|
|||||||
|
|
||||||
expect(dismissSpy).toHaveBeenCalled()
|
expect(dismissSpy).toHaveBeenCalled()
|
||||||
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
|
expect(toastSpy).toHaveBeenCalledWith('Error dismissing tasks', error)
|
||||||
expect(modal.componentInstance.buttonsEnabled).toBe(true)
|
expect(modal.componentInstance.buttonsEnabled()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should dismiss the currently visible scoped and filtered tasks', () => {
|
it('should dismiss the currently visible scoped and filtered tasks', () => {
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ export class TasksComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Dismiss`
|
modal.componentInstance.btnCaption = $localize`Dismiss`
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.tasksService.dismissTasks(tasks).subscribe({
|
this.tasksService.dismissTasks(tasks).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@@ -324,7 +324,7 @@ export class TasksComponent
|
|||||||
},
|
},
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
this.toastService.showError($localize`Error dismissing tasks`, e)
|
this.toastService.showError($localize`Error dismissing tasks`, e)
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
@@ -350,7 +350,7 @@ export class TasksComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Dismiss`
|
modal.componentInstance.btnCaption = $localize`Dismiss`
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.tasksService.dismissAllTasks().subscribe({
|
this.tasksService.dismissAllTasks().subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@@ -358,7 +358,7 @@ export class TasksComponent
|
|||||||
},
|
},
|
||||||
error: (e) => {
|
error: (e) => {
|
||||||
this.toastService.showError($localize`Error dismissing tasks`, e)
|
this.toastService.showError($localize`Error dismissing tasks`, e)
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.clearSelection()
|
this.clearSelection()
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export class TrashComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.trashService.emptyTrash([document.id]).subscribe({
|
this.trashService.emptyTrash([document.id]).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.showInfo(
|
this.toastService.showInfo(
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ export class UsersAndGroupsComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.usersService.delete(user).subscribe({
|
this.usersService.delete(user).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
@@ -199,7 +199,7 @@ export class UsersAndGroupsComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.groupsService.delete(group).subscribe({
|
this.groupsService.delete(group).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
|||||||
@@ -47,11 +47,12 @@
|
|||||||
|
|
||||||
.search-container {
|
.search-container {
|
||||||
max-height: 4.5rem;
|
max-height: 4.5rem;
|
||||||
overflow: hidden;
|
overflow: visible;
|
||||||
transition: max-height .2s ease, opacity .2s ease, padding-top .2s ease, padding-bottom .2s ease;
|
transition: max-height .2s ease, opacity .2s ease, padding-top .2s ease, padding-bottom .2s ease;
|
||||||
|
|
||||||
&.mobile-hidden {
|
&.mobile-hidden {
|
||||||
max-height: 0;
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</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>
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
<span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
@if (alternativeBtnCaption) {
|
@if (alternativeBtnCaption) {
|
||||||
<button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled">
|
<button type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled()">
|
||||||
{{alternativeBtnCaption}}
|
{{alternativeBtnCaption}}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,22 @@ describe('ConfirmDialogComponent', () => {
|
|||||||
expect(confirmSubjectResult).toBeTruthy()
|
expect(confirmSubjectResult).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should re-render the buttons when they are toggled from outside', async () => {
|
||||||
|
const confirmButton: HTMLButtonElement =
|
||||||
|
fixture.nativeElement.querySelectorAll('.modal-footer button')[1]
|
||||||
|
expect(confirmButton.disabled).toBeFalsy()
|
||||||
|
|
||||||
|
// Deliberately no detectChanges: a request callback toggling this is all
|
||||||
|
// that happens, and nothing else schedules a render for the modal
|
||||||
|
component.buttonsEnabled.set(false)
|
||||||
|
await fixture.whenStable()
|
||||||
|
expect(confirmButton.disabled).toBeTruthy()
|
||||||
|
|
||||||
|
component.buttonsEnabled.set(true)
|
||||||
|
await fixture.whenStable()
|
||||||
|
expect(confirmButton.disabled).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
it('should support cancel & close modal', () => {
|
it('should support cancel & close modal', () => {
|
||||||
let confirmSubjectResult
|
let confirmSubjectResult
|
||||||
const closeModalSpy = jest.spyOn(modal, 'close')
|
const closeModalSpy = jest.spyOn(modal, 'close')
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { DecimalPipe } from '@angular/common'
|
import { DecimalPipe } from '@angular/common'
|
||||||
import { Component, EventEmitter, Input, Output, inject } from '@angular/core'
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
Output,
|
||||||
|
inject,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { Subject } from 'rxjs'
|
import { Subject } from 'rxjs'
|
||||||
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
|
||||||
@@ -46,8 +53,7 @@ export class ConfirmDialogComponent extends LoadingComponentWithPermissions {
|
|||||||
@Input()
|
@Input()
|
||||||
cancelBtnCaption = $localize`Cancel`
|
cancelBtnCaption = $localize`Cancel`
|
||||||
|
|
||||||
@Input()
|
readonly buttonsEnabled = signal(true)
|
||||||
buttonsEnabled = true
|
|
||||||
|
|
||||||
confirmButtonEnabled = true
|
confirmButtonEnabled = true
|
||||||
alternativeButtonEnabled = true
|
alternativeButtonEnabled = true
|
||||||
|
|||||||
+2
-2
@@ -56,10 +56,10 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</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()">
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-2
@@ -57,7 +57,7 @@
|
|||||||
class="btn"
|
class="btn"
|
||||||
[class]="cancelBtnClass"
|
[class]="cancelBtnClass"
|
||||||
(click)="cancel()"
|
(click)="cancel()"
|
||||||
[disabled]="!buttonsEnabled"
|
[disabled]="!buttonsEnabled()"
|
||||||
>
|
>
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">
|
<span class="d-inline-block" style="padding-bottom: 1px;">
|
||||||
{{cancelBtnCaption}}
|
{{cancelBtnCaption}}
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
class="btn"
|
class="btn"
|
||||||
[class]="btnClass"
|
[class]="btnClass"
|
||||||
(click)="confirm()"
|
(click)="confirm()"
|
||||||
[disabled]="!confirmButtonEnabled || !buttonsEnabled"
|
[disabled]="!confirmButtonEnabled || !buttonsEnabled()"
|
||||||
>
|
>
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+2
-2
@@ -34,10 +34,10 @@
|
|||||||
<p class="mb-0 small"><b>{{messageBold}}</b></p>
|
<p class="mb-0 small"><b>{{messageBold}}</b></p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
|
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">
|
||||||
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</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 || degrees === 0">
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="!confirmButtonEnabled || !buttonsEnabled() || degrees === 0">
|
||||||
{{btnCaption}}
|
{{btnCaption}}
|
||||||
@if (!confirmButtonEnabled) {
|
@if (!confirmButtonEnabled) {
|
||||||
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
<ngb-progressbar style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="form-group ms-md-auto">
|
<div class="form-group ms-md-auto">
|
||||||
<button type="button" class="btn me-2" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">{{ cancelBtnCaption }}</button>
|
<button type="button" class="btn me-2" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled()">{{ cancelBtnCaption }}</button>
|
||||||
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
|
<button type="button" class="btn" [class]="btnClass" (click)="confirm()" [disabled]="pages.length === 0">{{ btnCaption }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+1
-1
@@ -119,7 +119,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary btn-sm d-inline-flex align-items-center gap-2 text-nowrap"
|
class="btn btn-primary btn-sm d-inline-flex align-items-center gap-2 text-nowrap"
|
||||||
(click)="submit()"
|
(click)="submit()"
|
||||||
[disabled]="loading() || !buttonsEnabled">
|
[disabled]="loading() || !buttonsEnabled()">
|
||||||
@if (loading()) {
|
@if (loading()) {
|
||||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ describe('ShareLinkBundleDialogComponent', () => {
|
|||||||
file_version: FileVersion.Original,
|
file_version: FileVersion.Original,
|
||||||
expiration_days: 3,
|
expiration_days: 3,
|
||||||
})
|
})
|
||||||
expect(component.buttonsEnabled).toBe(false)
|
expect(component.buttonsEnabled()).toBe(false)
|
||||||
expect(confirmSpy).toHaveBeenCalled()
|
expect(confirmSpy).toHaveBeenCalled()
|
||||||
|
|
||||||
component.form.setValue({
|
component.form.setValue({
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent {
|
|||||||
: FileVersion.Original,
|
: FileVersion.Original,
|
||||||
expiration_days: this.form.value.expirationDays,
|
expiration_days: this.form.value.expirationDays,
|
||||||
}
|
}
|
||||||
this.buttonsEnabled = false
|
this.buttonsEnabled.set(false)
|
||||||
super.confirm()
|
super.confirm()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1564,7 +1564,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
dialog.confirmClicked.next()
|
dialog.confirmClicked.next()
|
||||||
await openModal.result
|
await openModal.result
|
||||||
|
|
||||||
expect(dialog.buttonsEnabled).toBe(false)
|
expect(dialog.buttonsEnabled()).toBe(false)
|
||||||
expect(reloadSpy).toHaveBeenCalled()
|
expect(reloadSpy).toHaveBeenCalled()
|
||||||
expect((component as any).incomingUpdateModal).toBeNull()
|
expect((component as any).incomingUpdateModal).toBeNull()
|
||||||
})
|
})
|
||||||
@@ -1789,7 +1789,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
|
|
||||||
expect(errorSpy).toHaveBeenCalled()
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
expect(component.networkActive()).toBe(false)
|
expect(component.networkActive()).toBe(false)
|
||||||
expect(dialog.buttonsEnabled).toBe(true)
|
expect(dialog.buttonsEnabled()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should refresh the document when removing password in update mode', () => {
|
it('should refresh the document when removing password in update mode', () => {
|
||||||
|
|||||||
@@ -659,7 +659,7 @@ export class DocumentDetailComponent
|
|||||||
modal.componentInstance.cancelBtnCaption = $localize`Dismiss`
|
modal.componentInstance.cancelBtnCaption = $localize`Dismiss`
|
||||||
|
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.reloadRemoteVersion()
|
this.reloadRemoteVersion()
|
||||||
})
|
})
|
||||||
@@ -1374,7 +1374,7 @@ export class DocumentDetailComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap(() => {
|
switchMap(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
return this.documentsService.delete(this.document())
|
return this.documentsService.delete(this.document())
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -1386,7 +1386,7 @@ export class DocumentDetailComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.toastService.showError($localize`Error deleting document`, error)
|
this.toastService.showError($localize`Error deleting document`, error)
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
this.subscribeModalDelete(modal)
|
this.subscribeModalDelete(modal)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1411,7 +1411,7 @@ export class DocumentDetailComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.reprocessDocuments({ documents: [this.document().id] })
|
.reprocessDocuments({ documents: [this.document().id] })
|
||||||
.subscribe({
|
.subscribe({
|
||||||
@@ -1425,7 +1425,7 @@ export class DocumentDetailComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
}
|
}
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing operation`,
|
$localize`Error executing operation`,
|
||||||
@@ -1798,7 +1798,7 @@ export class DocumentDetailComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.editPdfDocuments([sourceDocumentId], {
|
.editPdfDocuments([sourceDocumentId], {
|
||||||
operations: modal.componentInstance.getOperations(),
|
operations: modal.componentInstance.getOperations(),
|
||||||
@@ -1821,7 +1821,7 @@ export class DocumentDetailComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
}
|
}
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing PDF edit operation`,
|
$localize`Error executing PDF edit operation`,
|
||||||
@@ -1855,7 +1855,7 @@ export class DocumentDetailComponent
|
|||||||
const sourceDocumentId = this.selectedVersionId() ?? this.document().id
|
const sourceDocumentId = this.selectedVersionId() ?? this.document().id
|
||||||
const dialog =
|
const dialog =
|
||||||
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
modal.componentInstance as PasswordRemovalConfirmDialogComponent
|
||||||
dialog.buttonsEnabled = false
|
dialog.buttonsEnabled.set(false)
|
||||||
this.networkActive.set(true)
|
this.networkActive.set(true)
|
||||||
this.documentsService
|
this.documentsService
|
||||||
.removePasswordDocuments([sourceDocumentId], {
|
.removePasswordDocuments([sourceDocumentId], {
|
||||||
@@ -1880,7 +1880,7 @@ export class DocumentDetailComponent
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
dialog.buttonsEnabled = true
|
dialog.buttonsEnabled.set(true)
|
||||||
this.networkActive.set(false)
|
this.networkActive.set(false)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing password removal operation`,
|
$localize`Error executing password removal operation`,
|
||||||
|
|||||||
@@ -1683,7 +1683,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expiration_days: 7,
|
expiration_days: 7,
|
||||||
},
|
},
|
||||||
loading: signal(false),
|
loading: signal(false),
|
||||||
buttonsEnabled: true,
|
buttonsEnabled: signal(true),
|
||||||
copied: signal(false),
|
copied: signal(false),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1715,7 +1715,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expiration_days: 7,
|
expiration_days: 7,
|
||||||
})
|
})
|
||||||
expect(dialogInstance.loading()).toBe(false)
|
expect(dialogInstance.loading()).toBe(false)
|
||||||
expect(dialogInstance.buttonsEnabled).toBe(false)
|
expect(dialogInstance.buttonsEnabled()).toBe(false)
|
||||||
expect(dialogInstance.createdBundle).toEqual({ id: 42 })
|
expect(dialogInstance.createdBundle).toEqual({ id: 42 })
|
||||||
expect(typeof dialogInstance.onOpenManage).toBe('function')
|
expect(typeof dialogInstance.onOpenManage).toBe('function')
|
||||||
expect(toastInfoSpy).toHaveBeenCalledWith(
|
expect(toastInfoSpy).toHaveBeenCalledWith(
|
||||||
@@ -1755,7 +1755,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expiration_days: null,
|
expiration_days: null,
|
||||||
},
|
},
|
||||||
loading: signal(false),
|
loading: signal(false),
|
||||||
buttonsEnabled: true,
|
buttonsEnabled: signal(true),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1777,7 +1777,7 @@ describe('BulkEditorComponent', () => {
|
|||||||
expect.any(Error)
|
expect.any(Error)
|
||||||
)
|
)
|
||||||
expect(dialogInstance.loading()).toBe(false)
|
expect(dialogInstance.loading()).toBe(false)
|
||||||
expect(dialogInstance.buttonsEnabled).toBe(true)
|
expect(dialogInstance.buttonsEnabled()).toBe(true)
|
||||||
openSpy.mockRestore()
|
openSpy.mockRestore()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ export class BulkEditorComponent
|
|||||||
overrideSelection?: DocumentSelectionQuery
|
overrideSelection?: DocumentSelectionQuery
|
||||||
) {
|
) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
this.setModalButtonsEnabled(modal, false)
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
}
|
}
|
||||||
this.documentService
|
this.documentService
|
||||||
.bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args)
|
.bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args)
|
||||||
@@ -290,7 +290,7 @@ export class BulkEditorComponent
|
|||||||
options: { deleteOriginals?: boolean } = {}
|
options: { deleteOriginals?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
this.setModalButtonsEnabled(modal, false)
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
}
|
}
|
||||||
request.pipe(first()).subscribe({
|
request.pipe(first()).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
@@ -320,7 +320,7 @@ export class BulkEditorComponent
|
|||||||
|
|
||||||
private handleOperationError(modal: NgbModalRef, error: any) {
|
private handleOperationError(modal: NgbModalRef, error: any) {
|
||||||
if (modal) {
|
if (modal) {
|
||||||
this.setModalButtonsEnabled(modal, true)
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
}
|
}
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error executing bulk operation`,
|
$localize`Error executing bulk operation`,
|
||||||
@@ -328,15 +328,6 @@ export class BulkEditorComponent
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private setModalButtonsEnabled(modal: NgbModalRef, enabled: boolean) {
|
|
||||||
const buttonsEnabled = modal.componentInstance.buttonsEnabled
|
|
||||||
if (typeof buttonsEnabled?.set === 'function') {
|
|
||||||
buttonsEnabled.set(enabled)
|
|
||||||
} else {
|
|
||||||
modal.componentInstance.buttonsEnabled = enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private applySelectionData(
|
private applySelectionData(
|
||||||
items: SelectionDataItem[],
|
items: SelectionDataItem[],
|
||||||
selectionModel: FilterableDropdownSelectionModel
|
selectionModel: FilterableDropdownSelectionModel
|
||||||
@@ -872,7 +863,7 @@ export class BulkEditorComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.executeDocumentAction(
|
this.executeDocumentAction(
|
||||||
modal,
|
modal,
|
||||||
this.documentService.deleteDocuments(this.getSelectionQuery())
|
this.documentService.deleteDocuments(this.getSelectionQuery())
|
||||||
@@ -920,7 +911,7 @@ export class BulkEditorComponent
|
|||||||
modal.componentInstance.confirmClicked
|
modal.componentInstance.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.executeDocumentAction(
|
this.executeDocumentAction(
|
||||||
modal,
|
modal,
|
||||||
this.documentService.reprocessDocuments(this.getSelectionQuery())
|
this.documentService.reprocessDocuments(this.getSelectionQuery())
|
||||||
@@ -957,7 +948,7 @@ export class BulkEditorComponent
|
|||||||
rotateDialog.confirmClicked
|
rotateDialog.confirmClicked
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
rotateDialog.buttonsEnabled = false
|
rotateDialog.buttonsEnabled.set(false)
|
||||||
this.executeDocumentAction(
|
this.executeDocumentAction(
|
||||||
modal,
|
modal,
|
||||||
this.documentService.rotateDocuments(
|
this.documentService.rotateDocuments(
|
||||||
@@ -990,7 +981,7 @@ export class BulkEditorComponent
|
|||||||
if (mergeDialog.archiveFallback()) {
|
if (mergeDialog.archiveFallback()) {
|
||||||
args.archive_fallback = true
|
args.archive_fallback = true
|
||||||
}
|
}
|
||||||
mergeDialog.buttonsEnabled = false
|
mergeDialog.buttonsEnabled.set(false)
|
||||||
this.executeDocumentAction(
|
this.executeDocumentAction(
|
||||||
modal,
|
modal,
|
||||||
this.documentService.mergeDocuments(mergeDialog.documentIDs(), args),
|
this.documentService.mergeDocuments(mergeDialog.documentIDs(), args),
|
||||||
@@ -1063,14 +1054,14 @@ export class BulkEditorComponent
|
|||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
dialog.loading.set(true)
|
dialog.loading.set(true)
|
||||||
dialog.buttonsEnabled = false
|
dialog.buttonsEnabled.set(false)
|
||||||
this.shareLinkBundleService
|
this.shareLinkBundleService
|
||||||
.createBundle(dialog.payload)
|
.createBundle(dialog.payload)
|
||||||
.pipe(first())
|
.pipe(first())
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: (result) => {
|
next: (result) => {
|
||||||
dialog.loading.set(false)
|
dialog.loading.set(false)
|
||||||
dialog.buttonsEnabled = false
|
dialog.buttonsEnabled.set(false)
|
||||||
dialog.createdBundle = result
|
dialog.createdBundle = result
|
||||||
dialog.copied.set(false)
|
dialog.copied.set(false)
|
||||||
dialog.payload = null
|
dialog.payload = null
|
||||||
@@ -1084,7 +1075,7 @@ export class BulkEditorComponent
|
|||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
dialog.loading.set(false)
|
dialog.loading.set(false)
|
||||||
dialog.buttonsEnabled = true
|
dialog.buttonsEnabled.set(true)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Share link bundle creation is not available yet.`,
|
$localize`Share link bundle creation is not available yet.`,
|
||||||
error
|
error
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@ export class CustomFieldsComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.customFieldsService.delete(field).subscribe({
|
this.customFieldsService.delete(field).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
|||||||
+4
-4
@@ -274,7 +274,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
activeModal.componentInstance.btnClass = 'btn-danger'
|
activeModal.componentInstance.btnClass = 'btn-danger'
|
||||||
activeModal.componentInstance.btnCaption = $localize`Delete`
|
activeModal.componentInstance.btnCaption = $localize`Delete`
|
||||||
activeModal.componentInstance.confirmClicked.subscribe(() => {
|
activeModal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
activeModal.componentInstance.buttonsEnabled = false
|
activeModal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.service
|
this.service
|
||||||
.delete(object)
|
.delete(object)
|
||||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
@@ -284,7 +284,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.reloadData()
|
this.reloadData()
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
activeModal.componentInstance.buttonsEnabled = true
|
activeModal.componentInstance.buttonsEnabled.set(true)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error while deleting element`,
|
$localize`Error while deleting element`,
|
||||||
error
|
error
|
||||||
@@ -455,7 +455,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.service
|
this.service
|
||||||
.bulk_edit_objects(
|
.bulk_edit_objects(
|
||||||
this.allSelectionActive ? [] : Array.from(this.selectedObjects),
|
this.allSelectionActive ? [] : Array.from(this.selectedObjects),
|
||||||
@@ -472,7 +472,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
|||||||
this.reloadData()
|
this.reloadData()
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
modal.componentInstance.buttonsEnabled = true
|
modal.componentInstance.buttonsEnabled.set(true)
|
||||||
this.toastService.showError(
|
this.toastService.showError(
|
||||||
$localize`Error deleting objects`,
|
$localize`Error deleting objects`,
|
||||||
error
|
error
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export class MailComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.mailAccountService.delete(account).subscribe({
|
this.mailAccountService.delete(account).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
@@ -298,7 +298,7 @@ export class MailComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.mailRuleService.delete(rule).subscribe({
|
this.mailRuleService.delete(rule).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export class WorkflowsComponent
|
|||||||
modal.componentInstance.btnClass = 'btn-danger'
|
modal.componentInstance.btnClass = 'btn-danger'
|
||||||
modal.componentInstance.btnCaption = $localize`Proceed`
|
modal.componentInstance.btnCaption = $localize`Proceed`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
this.workflowService.delete(workflow).subscribe({
|
this.workflowService.delete(workflow).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
modal.close()
|
modal.close()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export class DirtyFormGuard extends DirtyCheckGuard {
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Leave page`
|
modal.componentInstance.btnCaption = $localize`Leave page`
|
||||||
modal.componentInstance.confirmClicked.subscribe(() => {
|
modal.componentInstance.confirmClicked.subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
})
|
})
|
||||||
const subject = new Subject<boolean>()
|
const subject = new Subject<boolean>()
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ export class DirtySavedViewGuard {
|
|||||||
modal.componentInstance.alternativeBtnClass = 'btn-primary'
|
modal.componentInstance.alternativeBtnClass = 'btn-primary'
|
||||||
modal.componentInstance.alternativeBtnCaption = $localize`Save and close`
|
modal.componentInstance.alternativeBtnCaption = $localize`Save and close`
|
||||||
modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
component.saveViewConfig()
|
component.saveViewConfig()
|
||||||
modal.close()
|
modal.close()
|
||||||
})
|
})
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export class OpenDocumentsService {
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Close document`
|
modal.componentInstance.btnCaption = $localize`Close document`
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.openDocuments.splice(index, 1)
|
this.openDocuments.splice(index, 1)
|
||||||
this.dirtyDocuments.delete(doc.id)
|
this.dirtyDocuments.delete(doc.id)
|
||||||
@@ -165,7 +165,7 @@ export class OpenDocumentsService {
|
|||||||
modal.componentInstance.btnClass = 'btn-warning'
|
modal.componentInstance.btnClass = 'btn-warning'
|
||||||
modal.componentInstance.btnCaption = $localize`Close documents`
|
modal.componentInstance.btnCaption = $localize`Close documents`
|
||||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||||
modal.componentInstance.buttonsEnabled = false
|
modal.componentInstance.buttonsEnabled.set(false)
|
||||||
modal.close()
|
modal.close()
|
||||||
this.openDocuments.splice(0, this.openDocuments.length)
|
this.openDocuments.splice(0, this.openDocuments.length)
|
||||||
this.dirtyDocuments.clear()
|
this.dirtyDocuments.clear()
|
||||||
|
|||||||
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1336
-885
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1335
-884
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1359
-908
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+2583
-2133
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1370
-919
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1335
-884
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
+1333
-882
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
# ZIP_ZSTANDARD exists only on Python 3.14+ (PEP 784). None elsewhere.
|
||||||
|
ZSTD: int | None = getattr(zipfile, "ZIP_ZSTANDARD", None)
|
||||||
|
|
||||||
|
# CLI choices are fixed across runtimes so argparse never hides zstd; runtime
|
||||||
|
# availability is enforced separately in compression_available().
|
||||||
|
COMPRESSION_CHOICES: tuple[str, ...] = (
|
||||||
|
"stored",
|
||||||
|
"deflated",
|
||||||
|
"bzip2",
|
||||||
|
"lzma",
|
||||||
|
"zstd",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Method name -> zipfile compression constant (zstd only when supported).
|
||||||
|
COMPRESSION_METHODS: dict[str, int] = {
|
||||||
|
"stored": zipfile.ZIP_STORED,
|
||||||
|
"deflated": zipfile.ZIP_DEFLATED,
|
||||||
|
"bzip2": zipfile.ZIP_BZIP2,
|
||||||
|
"lzma": zipfile.ZIP_LZMA,
|
||||||
|
}
|
||||||
|
if ZSTD is not None:
|
||||||
|
COMPRESSION_METHODS["zstd"] = ZSTD
|
||||||
|
|
||||||
|
# Inclusive (min, max) level bounds per method; None => level not applicable.
|
||||||
|
# Verified on CPython 3.14.3.
|
||||||
|
#
|
||||||
|
# zstd's raw library bounds are (-131072, 22)
|
||||||
|
# (compression.zstd.CompressionParameter.compression_level.bounds()) — the
|
||||||
|
# minimum is an internal implementation constant (-ZSTD_TARGETLENGTH_MAX),
|
||||||
|
# not a meaningful distinct "level"; deeper negative values than -22 buy
|
||||||
|
# nothing over -22 in practice. We expose the conventional zstd CLI range
|
||||||
|
# instead of the raw library bounds.
|
||||||
|
LEVEL_BOUNDS: dict[str, tuple[int, int] | None] = {
|
||||||
|
"stored": None,
|
||||||
|
"deflated": (0, 9),
|
||||||
|
"bzip2": (1, 9),
|
||||||
|
"lzma": None,
|
||||||
|
"zstd": (-22, 22),
|
||||||
|
}
|
||||||
|
|
||||||
|
# zipfile compress_type id -> method name.
|
||||||
|
_COMPRESS_TYPE_TO_METHOD: dict[int, str] = {
|
||||||
|
zipfile.ZIP_STORED: "stored",
|
||||||
|
zipfile.ZIP_DEFLATED: "deflated",
|
||||||
|
zipfile.ZIP_BZIP2: "bzip2",
|
||||||
|
zipfile.ZIP_LZMA: "lzma",
|
||||||
|
93: "zstd",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compression_available(method: str) -> bool:
|
||||||
|
"""Whether the running interpreter can actually use the given method."""
|
||||||
|
if method in ("stored", "deflated"):
|
||||||
|
# zlib is a hard CPython dependency; stored needs nothing.
|
||||||
|
return True
|
||||||
|
if method == "bzip2":
|
||||||
|
return _module_importable("bz2")
|
||||||
|
if method == "lzma":
|
||||||
|
return _module_importable("lzma")
|
||||||
|
if method == "zstd":
|
||||||
|
return ZSTD is not None and _module_importable("compression.zstd")
|
||||||
|
return False # pragma: no cover -- method is always one of COMPRESSION_CHOICES
|
||||||
|
|
||||||
|
|
||||||
|
def _module_importable(name: str) -> bool:
|
||||||
|
try:
|
||||||
|
importlib.import_module(name)
|
||||||
|
except ImportError:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def level_error(method: str, level: int | None) -> str | None:
|
||||||
|
"""Return a human message if (method, level) is invalid, else None."""
|
||||||
|
if level is None:
|
||||||
|
return None
|
||||||
|
bounds = LEVEL_BOUNDS[method]
|
||||||
|
if bounds is None:
|
||||||
|
return f"--zip-compression-level has no effect for '{method}'"
|
||||||
|
low, high = bounds
|
||||||
|
if not (low <= level <= high):
|
||||||
|
return (
|
||||||
|
f"--zip-compression-level for '{method}' must be between {low} and {high}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def compress_type_readable(compress_type: int) -> bool:
|
||||||
|
"""Whether this interpreter can decompress an entry of the given type."""
|
||||||
|
method = _COMPRESS_TYPE_TO_METHOD.get(compress_type)
|
||||||
|
if method is None:
|
||||||
|
return False
|
||||||
|
return compression_available(method)
|
||||||
|
|
||||||
|
|
||||||
|
def unreadable_method_names(compress_types: set[int]) -> set[str]:
|
||||||
|
"""Map a set of compress_type ids to human method names for error messages."""
|
||||||
|
names: set[str] = set()
|
||||||
|
for ct in compress_types:
|
||||||
|
names.add(_COMPRESS_TYPE_TO_METHOD.get(ct, f"method {ct}"))
|
||||||
|
return names
|
||||||
@@ -243,11 +243,21 @@ class ZipExportSink(ExportSink):
|
|||||||
added as an entry at finalize (a zip entry cannot be interleaved with others).
|
added as an entry at finalize (a zip entry cannot be interleaved with others).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, target: Path, zip_name: str, *, delete: bool = False) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
target: Path,
|
||||||
|
zip_name: str,
|
||||||
|
*,
|
||||||
|
delete: bool = False,
|
||||||
|
compression: int = zipfile.ZIP_DEFLATED,
|
||||||
|
compresslevel: int | None = None,
|
||||||
|
) -> None:
|
||||||
self._target = target.resolve()
|
self._target = target.resolve()
|
||||||
self._zip_path = (self._target / zip_name).with_suffix(".zip")
|
self._zip_path = (self._target / zip_name).with_suffix(".zip")
|
||||||
self._tmp_path = self._zip_path.with_name(self._zip_path.name + ".tmp")
|
self._tmp_path = self._zip_path.with_name(self._zip_path.name + ".tmp")
|
||||||
self._delete = delete
|
self._delete = delete
|
||||||
|
self._compression = compression
|
||||||
|
self._compresslevel = compresslevel
|
||||||
self._zip: zipfile.ZipFile | None = None
|
self._zip: zipfile.ZipFile | None = None
|
||||||
self._dirs: set[str] = set()
|
self._dirs: set[str] = set()
|
||||||
self._pending_manifest: tuple[Path, str] | None = None
|
self._pending_manifest: tuple[Path, str] | None = None
|
||||||
@@ -258,7 +268,8 @@ class ZipExportSink(ExportSink):
|
|||||||
self._zip = zipfile.ZipFile(
|
self._zip = zipfile.ZipFile(
|
||||||
self._tmp_path,
|
self._tmp_path,
|
||||||
"w",
|
"w",
|
||||||
compression=zipfile.ZIP_DEFLATED,
|
compression=self._compression,
|
||||||
|
compresslevel=self._compresslevel,
|
||||||
allowZip64=True,
|
allowZip64=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,11 @@ if TYPE_CHECKING:
|
|||||||
if settings.AUDIT_LOG_ENABLED:
|
if settings.AUDIT_LOG_ENABLED:
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
|
|
||||||
|
from documents.export.compression import COMPRESSION_CHOICES
|
||||||
|
from documents.export.compression import COMPRESSION_METHODS
|
||||||
|
from documents.export.compression import ZSTD
|
||||||
|
from documents.export.compression import compression_available
|
||||||
|
from documents.export.compression import level_error
|
||||||
from documents.export.sinks import DirectoryExportSink
|
from documents.export.sinks import DirectoryExportSink
|
||||||
from documents.export.sinks import ExportSink
|
from documents.export.sinks import ExportSink
|
||||||
from documents.export.sinks import StreamingManifestWriter
|
from documents.export.sinks import StreamingManifestWriter
|
||||||
@@ -192,6 +197,28 @@ class Command(CryptMixin, PaperlessCommand):
|
|||||||
help="Sets the export zip file name",
|
help="Sets the export zip file name",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--zip-compression",
|
||||||
|
choices=COMPRESSION_CHOICES,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Compression method for the export zip (requires --zip). "
|
||||||
|
"Default: deflated. 'zstd' requires Python 3.14+ on both the "
|
||||||
|
"exporting and importing machine."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--zip-compression-level",
|
||||||
|
type=int,
|
||||||
|
default=None,
|
||||||
|
help=(
|
||||||
|
"Compression level for the export zip (requires --zip). "
|
||||||
|
"deflated: 0-9, bzip2: 1-9, zstd: -22..22; ignored for "
|
||||||
|
"stored/lzma."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--data-only",
|
"--data-only",
|
||||||
default=False,
|
default=False,
|
||||||
@@ -247,12 +274,39 @@ class Command(CryptMixin, PaperlessCommand):
|
|||||||
if not os.access(self.target, os.W_OK):
|
if not os.access(self.target, os.W_OK):
|
||||||
raise CommandError("That path doesn't appear to be writable")
|
raise CommandError("That path doesn't appear to be writable")
|
||||||
|
|
||||||
|
zip_compression: str | None = options["zip_compression"]
|
||||||
|
zip_compression_level: int | None = options["zip_compression_level"]
|
||||||
|
|
||||||
|
if not self.zip_export and (
|
||||||
|
zip_compression is not None or zip_compression_level is not None
|
||||||
|
):
|
||||||
|
raise CommandError(
|
||||||
|
"--zip-compression and --zip-compression-level require --zip",
|
||||||
|
)
|
||||||
|
|
||||||
|
compression_method = zip_compression or "deflated"
|
||||||
|
if self.zip_export:
|
||||||
|
if not compression_available(compression_method):
|
||||||
|
if compression_method == "zstd" and ZSTD is None:
|
||||||
|
raise CommandError(
|
||||||
|
"zstd compression requires Python 3.14 or newer",
|
||||||
|
)
|
||||||
|
raise CommandError(
|
||||||
|
f"Compression method '{compression_method}' is not "
|
||||||
|
f"available on this Python runtime",
|
||||||
|
)
|
||||||
|
level_msg = level_error(compression_method, zip_compression_level)
|
||||||
|
if level_msg is not None:
|
||||||
|
raise CommandError(level_msg)
|
||||||
|
|
||||||
sink: ExportSink
|
sink: ExportSink
|
||||||
if self.zip_export:
|
if self.zip_export:
|
||||||
sink = ZipExportSink(
|
sink = ZipExportSink(
|
||||||
self.target,
|
self.target,
|
||||||
options["zip_name"],
|
options["zip_name"],
|
||||||
delete=self.delete,
|
delete=self.delete,
|
||||||
|
compression=COMPRESSION_METHODS[compression_method],
|
||||||
|
compresslevel=zip_compression_level,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
sink = DirectoryExportSink(
|
sink = DirectoryExportSink(
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ from django.db.models.signals import post_save
|
|||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
from guardian.shortcuts import clear_ct_cache
|
from guardian.shortcuts import clear_ct_cache
|
||||||
|
|
||||||
|
from documents.export.compression import compress_type_readable
|
||||||
|
from documents.export.compression import unreadable_method_names
|
||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.management.commands.base import PaperlessCommand
|
from documents.management.commands.base import PaperlessCommand
|
||||||
from documents.management.commands.mixins import CryptMixin
|
from documents.management.commands.mixins import CryptMixin
|
||||||
@@ -460,6 +462,20 @@ class Command(CryptMixin, PaperlessCommand):
|
|||||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||||
if is_zipfile(self.source):
|
if is_zipfile(self.source):
|
||||||
with ZipFile(self.source) as zf:
|
with ZipFile(self.source) as zf:
|
||||||
|
unsupported = {
|
||||||
|
info.compress_type
|
||||||
|
for info in zf.infolist()
|
||||||
|
if not compress_type_readable(info.compress_type)
|
||||||
|
}
|
||||||
|
if unsupported:
|
||||||
|
names = sorted(unreadable_method_names(unsupported))
|
||||||
|
message = (
|
||||||
|
f"This archive uses compression this Python version cannot "
|
||||||
|
f"read ({', '.join(names)})."
|
||||||
|
)
|
||||||
|
if "zstd" in names:
|
||||||
|
message += " zstd archives require Python 3.14+."
|
||||||
|
raise CommandError(message)
|
||||||
zf.extractall(tmp_dir)
|
zf.extractall(tmp_dir)
|
||||||
self.source = Path(tmp_dir)
|
self.source = Path(tmp_dir)
|
||||||
self._run_import()
|
self._run_import()
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ from documents.permissions import set_permissions_for_object
|
|||||||
from documents.regex import validate_regex_pattern
|
from documents.regex import validate_regex_pattern
|
||||||
from documents.templating.filepath import validate_filepath_template_and_render
|
from documents.templating.filepath import validate_filepath_template_and_render
|
||||||
from documents.templating.utils import convert_format_str_to_template_format
|
from documents.templating.utils import convert_format_str_to_template_format
|
||||||
|
from documents.templating.workflows import validate_workflow_template
|
||||||
from documents.validators import uri_validator
|
from documents.validators import uri_validator
|
||||||
from documents.validators import url_validator
|
from documents.validators import url_validator
|
||||||
|
|
||||||
@@ -3185,33 +3186,10 @@ class WorkflowActionSerializer(serializers.ModelSerializer[WorkflowAction]):
|
|||||||
attrs["assign_title"] = None
|
attrs["assign_title"] = None
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
# test against all placeholders, see consumer.py `parse_doc_title_w_placeholders`
|
validate_workflow_template(attrs["assign_title"])
|
||||||
attrs["assign_title"].format(
|
|
||||||
correspondent="",
|
|
||||||
document_type="",
|
|
||||||
added="",
|
|
||||||
added_year="",
|
|
||||||
added_year_short="",
|
|
||||||
added_month="",
|
|
||||||
added_month_name="",
|
|
||||||
added_month_name_short="",
|
|
||||||
added_day="",
|
|
||||||
added_time="",
|
|
||||||
owner_username="",
|
|
||||||
original_filename="",
|
|
||||||
filename="",
|
|
||||||
created="",
|
|
||||||
created_year="",
|
|
||||||
created_year_short="",
|
|
||||||
created_month="",
|
|
||||||
created_month_name="",
|
|
||||||
created_month_name_short="",
|
|
||||||
created_day="",
|
|
||||||
created_time="",
|
|
||||||
)
|
|
||||||
except (ValueError, KeyError) as e:
|
except (ValueError, KeyError) as e:
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{"assign_title": f'Invalid f-string detected: "{e.args[0]}"'},
|
{"assign_title": f"{e.args[0]}"},
|
||||||
)
|
)
|
||||||
|
|
||||||
if attrs.get("assign_custom_fields_values"):
|
if attrs.get("assign_custom_fields_values"):
|
||||||
|
|||||||
@@ -6,9 +6,11 @@ from pathlib import Path
|
|||||||
from django.utils.text import slugify as django_slugify
|
from django.utils.text import slugify as django_slugify
|
||||||
from jinja2 import StrictUndefined
|
from jinja2 import StrictUndefined
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
|
from jinja2 import TemplateAssertionError
|
||||||
from jinja2 import TemplateSyntaxError
|
from jinja2 import TemplateSyntaxError
|
||||||
from jinja2 import UndefinedError
|
from jinja2 import UndefinedError
|
||||||
from jinja2 import make_logging_undefined
|
from jinja2 import make_logging_undefined
|
||||||
|
from jinja2.meta import find_undeclared_variables
|
||||||
from jinja2.sandbox import SecurityError
|
from jinja2.sandbox import SecurityError
|
||||||
|
|
||||||
from documents.templating.environment import _template_environment
|
from documents.templating.environment import _template_environment
|
||||||
@@ -29,6 +31,49 @@ _template_environment.filters["slugify"] = django_slugify
|
|||||||
_template_environment.filters["localize_date"] = localize_date
|
_template_environment.filters["localize_date"] = localize_date
|
||||||
|
|
||||||
|
|
||||||
|
_known_placeholder_names = {
|
||||||
|
"correspondent",
|
||||||
|
"document_type",
|
||||||
|
"added",
|
||||||
|
"added_year",
|
||||||
|
"added_year_short",
|
||||||
|
"added_month",
|
||||||
|
"added_month_name",
|
||||||
|
"added_month_name_short",
|
||||||
|
"added_day",
|
||||||
|
"added_time",
|
||||||
|
"owner_username",
|
||||||
|
"original_filename",
|
||||||
|
"filename",
|
||||||
|
"created",
|
||||||
|
"created_year",
|
||||||
|
"created_year_short",
|
||||||
|
"created_month",
|
||||||
|
"created_month_name",
|
||||||
|
"created_month_name_short",
|
||||||
|
"created_day",
|
||||||
|
"created_time",
|
||||||
|
"doc_title",
|
||||||
|
"doc_url",
|
||||||
|
"doc_id",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_workflow_template(text: str) -> None:
|
||||||
|
try:
|
||||||
|
ast = _template_environment.parse(text)
|
||||||
|
undeclared_vars = find_undeclared_variables(ast)
|
||||||
|
except TemplateAssertionError as e:
|
||||||
|
raise ValueError(f"Template assertion error: {e}")
|
||||||
|
except TemplateSyntaxError as e:
|
||||||
|
raise ValueError(f"Template syntax error: {e}")
|
||||||
|
unknown_vars = undeclared_vars - _known_placeholder_names
|
||||||
|
if unknown_vars:
|
||||||
|
raise KeyError(
|
||||||
|
f"Template references unknown placeholders: {', '.join(unknown_vars)}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def parse_w_workflow_placeholders(
|
def parse_w_workflow_placeholders(
|
||||||
text: str,
|
text: str,
|
||||||
correspondent_name: str,
|
correspondent_name: str,
|
||||||
|
|||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import sys
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_mock
|
||||||
|
|
||||||
|
from documents.export import compression
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompressionMethods:
|
||||||
|
def test_choices_always_include_zstd(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The compression policy module's CLI choices list
|
||||||
|
WHEN:
|
||||||
|
- Read on any runtime
|
||||||
|
THEN:
|
||||||
|
- zstd is always present; availability is checked separately so
|
||||||
|
argparse never hides it based on the current Python version
|
||||||
|
"""
|
||||||
|
assert compression.COMPRESSION_CHOICES == (
|
||||||
|
"stored",
|
||||||
|
"deflated",
|
||||||
|
"bzip2",
|
||||||
|
"lzma",
|
||||||
|
"zstd",
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("name", "constant"),
|
||||||
|
[
|
||||||
|
("stored", zipfile.ZIP_STORED),
|
||||||
|
("deflated", zipfile.ZIP_DEFLATED),
|
||||||
|
("bzip2", zipfile.ZIP_BZIP2),
|
||||||
|
("lzma", zipfile.ZIP_LZMA),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_method_maps_to_zipfile_constant(self, name: str, constant: int) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A compression method name
|
||||||
|
WHEN:
|
||||||
|
- Looked up in COMPRESSION_METHODS
|
||||||
|
THEN:
|
||||||
|
- It maps to the matching zipfile compression constant
|
||||||
|
"""
|
||||||
|
assert compression.COMPRESSION_METHODS[name] == constant
|
||||||
|
|
||||||
|
def test_stored_and_deflated_always_available(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The stored and deflated compression methods
|
||||||
|
WHEN:
|
||||||
|
- Checked with compression_available()
|
||||||
|
THEN:
|
||||||
|
- Both are always available (zlib is a hard CPython dependency)
|
||||||
|
"""
|
||||||
|
assert compression.compression_available("stored")
|
||||||
|
assert compression.compression_available("deflated")
|
||||||
|
|
||||||
|
def test_zstd_availability_tracks_runtime(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The zstd compression method
|
||||||
|
WHEN:
|
||||||
|
- Checked with compression_available() on this runtime
|
||||||
|
THEN:
|
||||||
|
- Availability matches whether Python is 3.14+
|
||||||
|
"""
|
||||||
|
expected: bool = sys.version_info >= (3, 14)
|
||||||
|
assert compression.compression_available("zstd") == expected
|
||||||
|
|
||||||
|
def test_unimportable_module_reports_unavailable(
|
||||||
|
self,
|
||||||
|
mocker: pytest_mock.MockerFixture,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A compression method whose backing module fails to import
|
||||||
|
(e.g. a minimal Python build without bz2/lzma compiled in)
|
||||||
|
WHEN:
|
||||||
|
- Checked with compression_available()
|
||||||
|
THEN:
|
||||||
|
- False is returned rather than the ImportError propagating
|
||||||
|
"""
|
||||||
|
mocker.patch(
|
||||||
|
"documents.export.compression.importlib.import_module",
|
||||||
|
side_effect=ImportError,
|
||||||
|
)
|
||||||
|
assert not compression.compression_available("bzip2")
|
||||||
|
|
||||||
|
|
||||||
|
class TestLevelError:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("method", "level"),
|
||||||
|
[
|
||||||
|
("deflated", 0),
|
||||||
|
("deflated", 9),
|
||||||
|
("bzip2", 1),
|
||||||
|
("bzip2", 9),
|
||||||
|
("zstd", -22),
|
||||||
|
("zstd", 22),
|
||||||
|
("deflated", None),
|
||||||
|
("stored", None),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_valid_levels_return_none(self, method: str, level: int | None) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A method and a level within its valid bounds (or no level)
|
||||||
|
WHEN:
|
||||||
|
- Checked with level_error()
|
||||||
|
THEN:
|
||||||
|
- No error message is returned
|
||||||
|
"""
|
||||||
|
assert compression.level_error(method, level) is None
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("method", "level"),
|
||||||
|
[
|
||||||
|
("deflated", 10),
|
||||||
|
("deflated", -1),
|
||||||
|
("bzip2", 0),
|
||||||
|
("bzip2", 10),
|
||||||
|
("zstd", -23),
|
||||||
|
("zstd", 23),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_out_of_range_levels_return_message(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
level: int,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A method and a level outside its valid bounds
|
||||||
|
WHEN:
|
||||||
|
- Checked with level_error()
|
||||||
|
THEN:
|
||||||
|
- An error message naming the valid range is returned
|
||||||
|
"""
|
||||||
|
msg: str | None = compression.level_error(method, level)
|
||||||
|
assert msg is not None
|
||||||
|
assert "between" in msg
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("method", ["stored", "lzma"])
|
||||||
|
def test_level_on_levelless_method_is_rejected(self, method: str) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A method that ignores compression level (stored, lzma)
|
||||||
|
WHEN:
|
||||||
|
- A level is passed to level_error() anyway
|
||||||
|
THEN:
|
||||||
|
- An error message noting the level has no effect is returned
|
||||||
|
"""
|
||||||
|
msg: str | None = compression.level_error(method, 5)
|
||||||
|
assert msg is not None
|
||||||
|
assert "no effect" in msg
|
||||||
|
|
||||||
|
|
||||||
|
class TestCompressTypeReadable:
|
||||||
|
@pytest.mark.parametrize("ct", [zipfile.ZIP_STORED, zipfile.ZIP_DEFLATED])
|
||||||
|
def test_stored_and_deflated_always_readable(self, ct: int) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A stored or deflated compress_type id
|
||||||
|
WHEN:
|
||||||
|
- Checked with compress_type_readable()
|
||||||
|
THEN:
|
||||||
|
- It is always readable
|
||||||
|
"""
|
||||||
|
assert compression.compress_type_readable(ct)
|
||||||
|
|
||||||
|
def test_zstd_compress_type_readability_tracks_runtime(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The zstd compress_type id (93, ZIP_ZSTANDARD)
|
||||||
|
WHEN:
|
||||||
|
- Checked with compress_type_readable() on this runtime
|
||||||
|
THEN:
|
||||||
|
- Readability matches whether Python is 3.14+
|
||||||
|
"""
|
||||||
|
expected: bool = sys.version_info >= (3, 14)
|
||||||
|
assert compression.compress_type_readable(93) == expected
|
||||||
|
|
||||||
|
def test_unknown_compress_type_is_unreadable(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- An unrecognized compress_type id
|
||||||
|
WHEN:
|
||||||
|
- Checked with compress_type_readable()
|
||||||
|
THEN:
|
||||||
|
- It is reported as unreadable
|
||||||
|
"""
|
||||||
|
assert not compression.compress_type_readable(9999)
|
||||||
|
|
||||||
|
def test_unreadable_method_names_lists_methods(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A set containing an unknown compress_type id
|
||||||
|
WHEN:
|
||||||
|
- Passed to unreadable_method_names()
|
||||||
|
THEN:
|
||||||
|
- It is reported generically as "method <id>"
|
||||||
|
"""
|
||||||
|
# An unknown method id maps to no name and is reported generically.
|
||||||
|
names: set[str] = compression.unreadable_method_names({9999})
|
||||||
|
assert names == {"method 9999"}
|
||||||
@@ -5,6 +5,7 @@ import zipfile
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import pytest_mock
|
||||||
from pytest_django.fixtures import SettingsWrapper
|
from pytest_django.fixtures import SettingsWrapper
|
||||||
|
|
||||||
from documents.export.sinks import DirectoryExportSink
|
from documents.export.sinks import DirectoryExportSink
|
||||||
@@ -305,6 +306,48 @@ class TestZipExportSink:
|
|||||||
assert not (target / "export.zip").exists()
|
assert not (target / "export.zip").exists()
|
||||||
|
|
||||||
|
|
||||||
|
class TestZipExportSinkCompression:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("method", "constant"),
|
||||||
|
[
|
||||||
|
("stored", zipfile.ZIP_STORED),
|
||||||
|
("deflated", zipfile.ZIP_DEFLATED),
|
||||||
|
("bzip2", zipfile.ZIP_BZIP2),
|
||||||
|
("lzma", zipfile.ZIP_LZMA),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_compression_and_level_forwarded_to_zipfile(
|
||||||
|
self,
|
||||||
|
mocker: pytest_mock.MockerFixture,
|
||||||
|
tmp_path: Path,
|
||||||
|
method: str,
|
||||||
|
constant: int,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A ZipExportSink constructed with a compression method and level
|
||||||
|
WHEN:
|
||||||
|
- The sink is opened
|
||||||
|
THEN:
|
||||||
|
- zipfile.ZipFile is constructed with those values forwarded
|
||||||
|
unchanged (whether ZipFile actually compresses is Python's own
|
||||||
|
contract, not ours, so this checks the call args, not a real
|
||||||
|
archive)
|
||||||
|
"""
|
||||||
|
target: Path = tmp_path / "out"
|
||||||
|
target.mkdir()
|
||||||
|
zip_cls = mocker.patch("documents.export.sinks.zipfile.ZipFile")
|
||||||
|
sink = ZipExportSink(target, "export", compression=constant, compresslevel=5)
|
||||||
|
sink._open()
|
||||||
|
zip_cls.assert_called_once_with(
|
||||||
|
mocker.ANY,
|
||||||
|
"w",
|
||||||
|
compression=constant,
|
||||||
|
compresslevel=5,
|
||||||
|
allowZip64=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestStreamContract:
|
class TestStreamContract:
|
||||||
@pytest.fixture(params=["dir", "zip"])
|
@pytest.fixture(params=["dir", "zip"])
|
||||||
def sink(self, request: pytest.FixtureRequest, tmp_path: Path) -> ExportSink:
|
def sink(self, request: pytest.FixtureRequest, tmp_path: Path) -> ExportSink:
|
||||||
|
|||||||
@@ -351,11 +351,45 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
self.assertEqual(WorkflowTrigger.objects.count(), 1)
|
self.assertEqual(WorkflowTrigger.objects.count(), 1)
|
||||||
|
|
||||||
def test_api_create_invalid_assign_title(self) -> None:
|
def test_api_create_complex_assign_title(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- API request to create a workflow
|
- API request to create a workflow
|
||||||
- Invalid f-string for assign_title
|
- Template using Jinja flow control statements
|
||||||
|
WHEN:
|
||||||
|
- API is called
|
||||||
|
THEN:
|
||||||
|
- Workflow is created
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
self.ENDPOINT,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"name": "Workflow 2",
|
||||||
|
"order": 1,
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"type": WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"assign_title": '{# this is a comment #}foo{% if created_year < 2000 %}bar{% endif %}{{ "{:04d}".format(42) }}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
self.assertEqual(Workflow.objects.count(), 2)
|
||||||
|
|
||||||
|
def test_api_create_invalid_assign_title_syntax_error(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- API request to create a workflow
|
||||||
|
- Invalid template for assign_title
|
||||||
WHEN:
|
WHEN:
|
||||||
- API is called
|
- API is called
|
||||||
THEN:
|
THEN:
|
||||||
@@ -366,7 +400,7 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
|||||||
self.ENDPOINT,
|
self.ENDPOINT,
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"name": "Workflow 1",
|
"name": "Workflow 2",
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"triggers": [
|
"triggers": [
|
||||||
{
|
{
|
||||||
@@ -375,7 +409,7 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
|||||||
],
|
],
|
||||||
"actions": [
|
"actions": [
|
||||||
{
|
{
|
||||||
"assign_title": "{created_year]",
|
"assign_title": "{{created_year}",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -384,7 +418,89 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"Invalid f-string detected",
|
"Template syntax error",
|
||||||
|
response.data["actions"][0]["assign_title"][0],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(Workflow.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_api_create_invalid_assign_title_assertion_error(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- API request to create a workflow
|
||||||
|
- Template using unknown filters for assign_title
|
||||||
|
WHEN:
|
||||||
|
- API is called
|
||||||
|
THEN:
|
||||||
|
- Correct HTTP 400 response
|
||||||
|
- No objects are created
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
self.ENDPOINT,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"name": "Workflow 2",
|
||||||
|
"order": 1,
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"type": WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"assign_title": "{{ created_year | foo }}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(
|
||||||
|
"Template assertion error",
|
||||||
|
response.data["actions"][0]["assign_title"][0],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(Workflow.objects.count(), 1)
|
||||||
|
|
||||||
|
def test_api_create_invalid_assign_title_unknown_placeholder(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- API request to create a workflow
|
||||||
|
- Template with unknown placeholders for assign_title
|
||||||
|
WHEN:
|
||||||
|
- API is called
|
||||||
|
THEN:
|
||||||
|
- Correct HTTP 400 response
|
||||||
|
- No objects are created
|
||||||
|
"""
|
||||||
|
response = self.client.post(
|
||||||
|
self.ENDPOINT,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"name": "Workflow 2",
|
||||||
|
"order": 1,
|
||||||
|
"triggers": [
|
||||||
|
{
|
||||||
|
"type": WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"assign_title": "{{creation_year}}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(
|
||||||
|
"Template references unknown placeholders",
|
||||||
|
response.data["actions"][0]["assign_title"][0],
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"creation_year",
|
||||||
response.data["actions"][0]["assign_title"][0],
|
response.data["actions"][0]["assign_title"][0],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from datetime import timedelta
|
|||||||
from io import StringIO
|
from io import StringIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
from zipfile import ZIP_DEFLATED
|
||||||
|
from zipfile import ZIP_LZMA
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -1078,6 +1080,197 @@ class TestExportImport(
|
|||||||
skip_checks=True,
|
skip_checks=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_compression_flags_require_zip(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A request to export without --zip
|
||||||
|
WHEN:
|
||||||
|
- --zip-compression or --zip-compression-level is passed anyway
|
||||||
|
THEN:
|
||||||
|
- A CommandError is raised (the flags are meaningless without --zip)
|
||||||
|
"""
|
||||||
|
cases = {
|
||||||
|
"zip-compression": ["--zip-compression", "lzma"],
|
||||||
|
"zip-compression-level": ["--zip-compression-level", "5"],
|
||||||
|
}
|
||||||
|
for case_id, args in cases.items():
|
||||||
|
with self.subTest(case_id), self.assertRaises(CommandError):
|
||||||
|
call_command(
|
||||||
|
"document_exporter",
|
||||||
|
self.target,
|
||||||
|
*args,
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_zip_compression_level_out_of_range_raises(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A request to export to a zip file
|
||||||
|
WHEN:
|
||||||
|
- --zip-compression-level is outside the chosen method's valid range
|
||||||
|
THEN:
|
||||||
|
- A CommandError is raised
|
||||||
|
"""
|
||||||
|
with self.assertRaises(CommandError):
|
||||||
|
call_command(
|
||||||
|
"document_exporter",
|
||||||
|
self.target,
|
||||||
|
"--zip",
|
||||||
|
"--zip-compression",
|
||||||
|
"deflated",
|
||||||
|
"--zip-compression-level",
|
||||||
|
"99",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_zip_compression_level_rejected_for_levelless_method(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A request to export to a zip file with a compression method
|
||||||
|
that ignores level entirely (stored, lzma)
|
||||||
|
WHEN:
|
||||||
|
- --zip-compression-level is also passed
|
||||||
|
THEN:
|
||||||
|
- A CommandError is raised
|
||||||
|
"""
|
||||||
|
for method in ("stored", "lzma"):
|
||||||
|
with self.subTest(method), self.assertRaises(CommandError):
|
||||||
|
call_command(
|
||||||
|
"document_exporter",
|
||||||
|
self.target,
|
||||||
|
"--zip",
|
||||||
|
"--zip-compression",
|
||||||
|
method,
|
||||||
|
"--zip-compression-level",
|
||||||
|
"5",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_zstd_unavailable_raises_friendly_error(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A Python runtime without zstd support (< 3.14)
|
||||||
|
WHEN:
|
||||||
|
- --zip-compression zstd is requested
|
||||||
|
THEN:
|
||||||
|
- A CommandError naming the Python version requirement is raised
|
||||||
|
|
||||||
|
zstd availability is mocked rather than relying on the actual
|
||||||
|
runtime: on a Python 3.14+ CI leg, ZSTD is not None, so without the
|
||||||
|
mock this check is skipped and the command falls through into the
|
||||||
|
real export, which fails on missing document files instead of
|
||||||
|
raising the expected CommandError.
|
||||||
|
"""
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"documents.management.commands.document_exporter.ZSTD",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
mock.patch(
|
||||||
|
"documents.management.commands.document_exporter.compression_available",
|
||||||
|
return_value=False,
|
||||||
|
),
|
||||||
|
self.assertRaises(CommandError) as e,
|
||||||
|
):
|
||||||
|
call_command(
|
||||||
|
"document_exporter",
|
||||||
|
self.target,
|
||||||
|
"--zip",
|
||||||
|
"--zip-compression",
|
||||||
|
"zstd",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
self.assertIn("3.14", str(e.exception))
|
||||||
|
|
||||||
|
def test_non_zstd_unavailable_raises_generic_error(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A Python runtime missing the module backing a non-zstd method
|
||||||
|
(e.g. bz2/lzma not compiled in on a minimal build)
|
||||||
|
WHEN:
|
||||||
|
- That method is requested via --zip-compression
|
||||||
|
THEN:
|
||||||
|
- A CommandError is raised naming the method, not the
|
||||||
|
zstd-specific "requires 3.14" message
|
||||||
|
"""
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"documents.management.commands.document_exporter.compression_available",
|
||||||
|
return_value=False,
|
||||||
|
),
|
||||||
|
self.assertRaises(CommandError) as e,
|
||||||
|
):
|
||||||
|
call_command(
|
||||||
|
"document_exporter",
|
||||||
|
self.target,
|
||||||
|
"--zip",
|
||||||
|
"--zip-compression",
|
||||||
|
"bzip2",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
self.assertIn("bzip2", str(e.exception))
|
||||||
|
self.assertNotIn("3.14", str(e.exception))
|
||||||
|
|
||||||
|
def test_zip_compression_flag_resolves_to_sink_constant(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A request to export to a zip file with --zip-compression lzma
|
||||||
|
WHEN:
|
||||||
|
- The export runs
|
||||||
|
THEN:
|
||||||
|
- ZipExportSink is constructed with the resolved ZIP_LZMA constant
|
||||||
|
(whether zipfile actually compresses with the chosen method is
|
||||||
|
Python's own contract, and ZipExportSink's own tests already
|
||||||
|
cover the forwarding; what this command owns is resolving the
|
||||||
|
CLI string to the right constant, so assert that resolution
|
||||||
|
directly)
|
||||||
|
"""
|
||||||
|
with mock.patch(
|
||||||
|
"documents.management.commands.document_exporter.ZipExportSink",
|
||||||
|
) as sink_cls:
|
||||||
|
call_command(
|
||||||
|
"document_exporter",
|
||||||
|
self.target,
|
||||||
|
"--zip",
|
||||||
|
"--zip-compression",
|
||||||
|
"lzma",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
sink_cls.assert_called_once_with(
|
||||||
|
mock.ANY,
|
||||||
|
mock.ANY,
|
||||||
|
delete=False,
|
||||||
|
compression=ZIP_LZMA,
|
||||||
|
compresslevel=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_default_zip_compression_resolves_to_deflate(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A request to export to a zip file with no --zip-compression flag
|
||||||
|
WHEN:
|
||||||
|
- The export runs
|
||||||
|
THEN:
|
||||||
|
- ZipExportSink is constructed with the default ZIP_DEFLATED
|
||||||
|
constant and compresslevel=None, matching pre-existing behavior
|
||||||
|
"""
|
||||||
|
with mock.patch(
|
||||||
|
"documents.management.commands.document_exporter.ZipExportSink",
|
||||||
|
) as sink_cls:
|
||||||
|
call_command(
|
||||||
|
"document_exporter",
|
||||||
|
self.target,
|
||||||
|
"--zip",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
sink_cls.assert_called_once_with(
|
||||||
|
mock.ANY,
|
||||||
|
mock.ANY,
|
||||||
|
delete=False,
|
||||||
|
compression=ZIP_DEFLATED,
|
||||||
|
compresslevel=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
class TestCryptExportImport(
|
class TestCryptExportImport(
|
||||||
|
|||||||
@@ -525,6 +525,71 @@ class TestCommandImport(
|
|||||||
self.assertEqual(doc.tags.count(), 1)
|
self.assertEqual(doc.tags.count(), 1)
|
||||||
self.assertEqual(doc.tags.first().name, "batch-flush-tag")
|
self.assertEqual(doc.tags.first().name, "batch-flush-tag")
|
||||||
|
|
||||||
|
def test_import_rejects_unreadable_compression(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A zip archive with an entry whose compression this Python can't read
|
||||||
|
WHEN:
|
||||||
|
- Import is attempted
|
||||||
|
THEN:
|
||||||
|
- A CommandError naming the issue is raised, before extraction
|
||||||
|
"""
|
||||||
|
import zipfile
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
archive = Path(self.dirs.scratch_dir) / "export.zip"
|
||||||
|
with zipfile.ZipFile(archive, "w") as zf:
|
||||||
|
zf.writestr("manifest.json", "[]")
|
||||||
|
|
||||||
|
with mock.patch(
|
||||||
|
"documents.management.commands.document_importer.compress_type_readable",
|
||||||
|
return_value=False,
|
||||||
|
):
|
||||||
|
with self.assertRaises(CommandError) as e:
|
||||||
|
call_command(
|
||||||
|
"document_importer",
|
||||||
|
str(archive),
|
||||||
|
"--no-progress-bar",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
self.assertIn("compression", str(e.exception))
|
||||||
|
|
||||||
|
def test_import_rejects_unreadable_zstd_with_version_hint(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A zip archive with an entry compressed with zstd
|
||||||
|
WHEN:
|
||||||
|
- Import is attempted on a Python runtime that can't read zstd
|
||||||
|
THEN:
|
||||||
|
- The CommandError names the 3.14+ requirement, not just the
|
||||||
|
generic "can't read" message
|
||||||
|
"""
|
||||||
|
import zipfile
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
archive = Path(self.dirs.scratch_dir) / "export.zip"
|
||||||
|
with zipfile.ZipFile(archive, "w") as zf:
|
||||||
|
zf.writestr("manifest.json", "[]")
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch(
|
||||||
|
"documents.management.commands.document_importer.compress_type_readable",
|
||||||
|
return_value=False,
|
||||||
|
),
|
||||||
|
mock.patch(
|
||||||
|
"documents.management.commands.document_importer.unreadable_method_names",
|
||||||
|
return_value={"zstd"},
|
||||||
|
),
|
||||||
|
):
|
||||||
|
with self.assertRaises(CommandError) as e:
|
||||||
|
call_command(
|
||||||
|
"document_importer",
|
||||||
|
str(archive),
|
||||||
|
"--no-progress-bar",
|
||||||
|
skip_checks=True,
|
||||||
|
)
|
||||||
|
self.assertIn("3.14", str(e.exception))
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: paperless-ngx\n"
|
"Project-Id-Version: paperless-ngx\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-08-12 19:04+0000\n"
|
"POT-Creation-Date: 2026-08-13 19:47+0000\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@@ -1575,49 +1575,49 @@ msgstr ""
|
|||||||
msgid "workflow runs"
|
msgid "workflow runs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:521 documents/serialisers.py:873
|
#: documents/serialisers.py:522 documents/serialisers.py:874
|
||||||
#: documents/serialisers.py:2768 documents/views.py:299 documents/views.py:2555
|
#: documents/serialisers.py:2769 documents/views.py:299 documents/views.py:2555
|
||||||
#: paperless_mail/serialisers.py:155
|
#: paperless_mail/serialisers.py:155
|
||||||
msgid "Insufficient permissions."
|
msgid "Insufficient permissions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:709
|
#: documents/serialisers.py:710
|
||||||
msgid "Invalid color."
|
msgid "Invalid color."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2245
|
#: documents/serialisers.py:2246
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "File type %(type)s not supported"
|
msgid "File type %(type)s not supported"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2289
|
#: documents/serialisers.py:2290
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Custom field id must be an integer: %(id)s"
|
msgid "Custom field id must be an integer: %(id)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2296
|
#: documents/serialisers.py:2297
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Custom field with id %(id)s does not exist"
|
msgid "Custom field with id %(id)s does not exist"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2313 documents/serialisers.py:2323
|
#: documents/serialisers.py:2314 documents/serialisers.py:2324
|
||||||
msgid ""
|
msgid ""
|
||||||
"Custom fields must be a list of integers or an object mapping ids to values."
|
"Custom fields must be a list of integers or an object mapping ids to values."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2318
|
#: documents/serialisers.py:2319
|
||||||
msgid "Some custom fields don't exist or were specified twice."
|
msgid "Some custom fields don't exist or were specified twice."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2465
|
#: documents/serialisers.py:2466
|
||||||
msgid "Invalid variable detected."
|
msgid "Invalid variable detected."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2824
|
#: documents/serialisers.py:2825
|
||||||
msgid "Duplicate document identifiers are not allowed."
|
msgid "Duplicate document identifiers are not allowed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2854 documents/views.py:4509
|
#: documents/serialisers.py:2855 documents/views.py:4509
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Documents not found: %(ids)s"
|
msgid "Documents not found: %(ids)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user