Compare commits

...

9 Commits

Author SHA1 Message Date
Sebastian Steinbeißer
44900b9728
Chore: switch from os.path to pathlib.Path 2025-03-19 17:41:09 +01:00
github-actions[bot]
5d6cfa7349
New Crowdin translations by GitHub Action (#9430)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-18 19:17:49 -07:00
shamoon
3105317137
Fix: close email dialog after email send 2025-03-18 19:17:49 -07:00
github-actions[bot]
1456169d7f
New Crowdin translations by GitHub Action (#9408)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-17 23:36:52 -07:00
shamoon
22a6fe5e10
Enhancement: support more 'not assigned' filtering, refactor (#9429) 2025-03-17 23:35:03 -07:00
github-actions[bot]
a0c1a19263
New Crowdin translations by GitHub Action (#9401) 2025-03-14 10:30:45 -07:00
github-actions[bot]
1f5d1b6f26
New Crowdin translations by GitHub Action (#8929)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-03-14 17:22:24 +00:00
shamoon
3b19a727b8
Change: better handle permissions in patch requests (#9393) 2025-03-14 08:53:00 -07:00
shamoon
7146a5f4fc
Fix: use correct filename with webhook (#9392) 2025-03-14 07:44:40 -07:00
106 changed files with 92350 additions and 61579 deletions

View File

@ -227,27 +227,9 @@ lint.per-file-ignores."src/documents/tests/test_consumer.py" = [
lint.per-file-ignores."src/documents/tests/test_file_handling.py" = [ lint.per-file-ignores."src/documents/tests/test_file_handling.py" = [
"PTH", "PTH",
] # TODO Enable & remove ] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_management.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_management_consumer.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_management_exporter.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_migration_archive_files.py" = [ lint.per-file-ignores."src/documents/tests/test_migration_archive_files.py" = [
"PTH", "PTH",
] # TODO Enable & remove ] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_migration_document_pages_count.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_migration_mime_type.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/tests/test_sanity_check.py" = [
"PTH",
] # TODO Enable & remove
lint.per-file-ignores."src/documents/views.py" = [ lint.per-file-ignores."src/documents/views.py" = [
"PTH", "PTH",
] # TODO Enable & remove ] # TODO Enable & remove

View File

@ -1120,7 +1120,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">193</context> <context context-type="linenumber">190</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8508424367627989968" datatype="html"> <trans-unit id="8508424367627989968" datatype="html">
@ -1194,11 +1194,11 @@
</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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">104</context> <context context-type="linenumber">97</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">106</context> <context context-type="linenumber">101</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context> <context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
@ -1793,7 +1793,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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">110</context> <context context-type="linenumber">103</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@ -2086,7 +2086,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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">164</context> <context context-type="linenumber">157</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.html</context>
@ -2553,15 +2553,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">796</context> <context context-type="linenumber">794</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">829</context> <context context-type="linenumber">827</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">848</context> <context context-type="linenumber">846</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context> <context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
@ -2986,7 +2986,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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">136</context> <context context-type="linenumber">129</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-card-large/document-card-large.component.html</context>
@ -3161,27 +3161,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">439</context> <context context-type="linenumber">437</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">479</context> <context context-type="linenumber">477</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">517</context> <context context-type="linenumber">515</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">555</context> <context context-type="linenumber">553</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">617</context> <context context-type="linenumber">615</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">750</context> <context context-type="linenumber">748</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1407560924967345762" datatype="html"> <trans-unit id="1407560924967345762" datatype="html">
@ -5177,14 +5177,14 @@
<source>Email sent</source> <source>Email sent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">66</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3742745894977668908" datatype="html"> <trans-unit id="3742745894977668908" datatype="html">
<source>Error emailing document</source> <source>Error emailing document</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/email-document-dialog/email-document-dialog.component.ts</context>
<context context-type="linenumber">69</context> <context context-type="linenumber">70</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6381578200008167206" datatype="html"> <trans-unit id="6381578200008167206" datatype="html">
@ -5246,7 +5246,7 @@
<source>Not assigned</source> <source>Not assigned</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context> <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
<context context-type="linenumber">392</context> <context context-type="linenumber">81</context>
</context-group> </context-group>
<note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note> <note priority="1" from="description">Filter drop down element to filter for documents with no correspondent/type/tag assigned</note>
</trans-unit> </trans-unit>
@ -5254,7 +5254,7 @@
<source>Open <x id="PH" equiv-text="this.title"/> filter</source> <source>Open <x id="PH" equiv-text="this.title"/> filter</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context> <context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts</context>
<context context-type="linenumber">513</context> <context context-type="linenumber">554</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7005745151564974365" datatype="html"> <trans-unit id="7005745151564974365" datatype="html">
@ -6415,7 +6415,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">384</context> <context context-type="linenumber">382</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>
@ -6490,7 +6490,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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">114</context> <context context-type="linenumber">107</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1418444397960583910" datatype="html"> <trans-unit id="1418444397960583910" datatype="html">
@ -6519,7 +6519,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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">117</context> <context context-type="linenumber">110</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4399672576012609374" datatype="html"> <trans-unit id="4399672576012609374" datatype="html">
@ -6562,7 +6562,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">180</context> <context context-type="linenumber">177</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@ -6595,7 +6595,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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">37</context> <context context-type="linenumber">35</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -6603,7 +6603,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">50</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@ -6622,7 +6622,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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">52</context> <context context-type="linenumber">49</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -6630,7 +6630,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">64</context> <context context-type="linenumber">61</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@ -6649,7 +6649,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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">67</context> <context context-type="linenumber">63</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context> <context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
@ -6657,7 +6657,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">76</context> <context context-type="linenumber">72</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@ -6940,7 +6940,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">752</context> <context context-type="linenumber">750</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2048798344356757326" datatype="html"> <trans-unit id="2048798344356757326" datatype="html">
@ -6951,7 +6951,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">754</context> <context context-type="linenumber">752</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7295637485862454066" datatype="html"> <trans-unit id="7295637485862454066" datatype="html">
@ -6969,7 +6969,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">792</context> <context context-type="linenumber">790</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2951161989614003846" datatype="html"> <trans-unit id="2951161989614003846" datatype="html">
@ -7050,7 +7050,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">826</context> <context context-type="linenumber">824</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="857641176955257111" datatype="html"> <trans-unit id="857641176955257111" datatype="html">
@ -7149,122 +7149,122 @@
<source>Filter correspondents</source> <source>Filter correspondents</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">38</context> <context context-type="linenumber">36</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">53</context> <context context-type="linenumber">51</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2947613869920454977" datatype="html"> <trans-unit id="2947613869920454977" datatype="html">
<source>Filter document types</source> <source>Filter document types</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">53</context> <context context-type="linenumber">50</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">65</context> <context context-type="linenumber">62</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8816999377397522522" datatype="html"> <trans-unit id="8816999377397522522" datatype="html">
<source>Filter storage paths</source> <source>Filter storage paths</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">68</context> <context context-type="linenumber">64</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">77</context> <context context-type="linenumber">73</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="9149498548977462220" datatype="html"> <trans-unit id="9149498548977462220" datatype="html">
<source>Custom fields</source> <source>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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">82</context> <context context-type="linenumber">77</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">89</context> <context context-type="linenumber">84</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">188</context> <context context-type="linenumber">185</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6475890479659129881" datatype="html"> <trans-unit id="6475890479659129881" datatype="html">
<source>Filter custom fields</source> <source>Filter 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.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">83</context> <context context-type="linenumber">78</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5139192806922838657" datatype="html"> <trans-unit id="5139192806922838657" datatype="html">
<source>Set values</source> <source>Set values</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">93</context> <context context-type="linenumber">86</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3206542606001340679" datatype="html"> <trans-unit id="3206542606001340679" datatype="html">
<source>Merge</source> <source>Merge</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">120</context> <context context-type="linenumber">113</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1015374532025907183" datatype="html"> <trans-unit id="1015374532025907183" datatype="html">
<source>Include:</source> <source>Include:</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">142</context> <context context-type="linenumber">135</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1537670659786159738" datatype="html"> <trans-unit id="1537670659786159738" datatype="html">
<source>Archived files</source> <source>Archived files</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">146</context> <context context-type="linenumber">139</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2520291319362448498" datatype="html"> <trans-unit id="2520291319362448498" datatype="html">
<source>Original files</source> <source>Original files</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">150</context> <context context-type="linenumber">143</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8009862506882713059" datatype="html"> <trans-unit id="8009862506882713059" datatype="html">
<source>Use formatted filename</source> <source>Use formatted filename</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
<context context-type="linenumber">155</context> <context context-type="linenumber">148</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1215215387232313677" datatype="html"> <trans-unit id="1215215387232313677" datatype="html">
<source>Error executing bulk operation</source> <source>Error executing bulk operation</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">288</context> <context context-type="linenumber">286</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7894972847287473517" datatype="html"> <trans-unit id="7894972847287473517" datatype="html">
<source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot;</source> <source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot;</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">376</context> <context context-type="linenumber">374</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">382</context> <context context-type="linenumber">380</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8639884465898458690" datatype="html"> <trans-unit id="8639884465898458690" datatype="html">
<source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</source> <source>&quot;<x id="PH" equiv-text="items[0].name"/>&quot; and &quot;<x id="PH_1" equiv-text="items[1].name"/>&quot;</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">378</context> <context context-type="linenumber">376</context>
</context-group> </context-group>
<note priority="1" from="description">This is for messages like &apos;modify &quot;tag1&quot; and &quot;tag2&quot;&apos;</note> <note priority="1" from="description">This is for messages like &apos;modify &quot;tag1&quot; and &quot;tag2&quot;&apos;</note>
</trans-unit> </trans-unit>
@ -7272,7 +7272,7 @@
<source><x id="PH" equiv-text="list"/> and &quot;<x id="PH_1" equiv-text="items[items.length - 1].name"/>&quot;</source> <source><x id="PH" equiv-text="list"/> and &quot;<x id="PH_1" equiv-text="items[items.length - 1].name"/>&quot;</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">386,388</context> <context context-type="linenumber">384,386</context>
</context-group> </context-group>
<note priority="1" from="description">this is for messages like &apos;modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;&apos;</note> <note priority="1" from="description">this is for messages like &apos;modify &quot;tag1&quot;, &quot;tag2&quot; and &quot;tag3&quot;&apos;</note>
</trans-unit> </trans-unit>
@ -7280,14 +7280,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">403</context> <context context-type="linenumber">401</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 &quot;<x id="PH" equiv-text="tag.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will add the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> 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">409</context> <context context-type="linenumber">407</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1894412783609570695" datatype="html"> <trans-unit id="1894412783609570695" datatype="html">
@ -7296,14 +7296,14 @@
)"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> )"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> 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">414,416</context> <context context-type="linenumber">412,414</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 &quot;<x id="PH" equiv-text="tag.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will remove the tag &quot;<x id="PH" equiv-text="tag.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> 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">422</context> <context context-type="linenumber">420</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3819792277998068944" datatype="html"> <trans-unit id="3819792277998068944" datatype="html">
@ -7312,7 +7312,7 @@
)"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> )"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> 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">427,429</context> <context context-type="linenumber">425,427</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2739066218579571288" datatype="html"> <trans-unit id="2739066218579571288" datatype="html">
@ -7323,84 +7323,84 @@
)"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source> )"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> 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">431,435</context> <context context-type="linenumber">429,433</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">472</context> <context context-type="linenumber">470</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 &quot;<x id="PH" equiv-text="correspondent.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will assign the correspondent &quot;<x id="PH" equiv-text="correspondent.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> 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">474</context> <context context-type="linenumber">472</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.list.selected.size"/> selected document(s).</source> <source>This operation will remove the correspondent from <x id="PH" equiv-text="this.list.selected.size"/> 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">476</context> <context context-type="linenumber">474</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">510</context> <context context-type="linenumber">508</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 &quot;<x id="PH" equiv-text="documentType.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will assign the document type &quot;<x id="PH" equiv-text="documentType.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> 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">512</context> <context context-type="linenumber">510</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.list.selected.size"/> selected document(s).</source> <source>This operation will remove the document type from <x id="PH" equiv-text="this.list.selected.size"/> 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">514</context> <context context-type="linenumber">512</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">548</context> <context context-type="linenumber">546</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 &quot;<x id="PH" equiv-text="storagePath.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will assign the storage path &quot;<x id="PH" equiv-text="storagePath.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> 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">550</context> <context context-type="linenumber">548</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.list.selected.size"/> selected document(s).</source> <source>This operation will remove the storage path from <x id="PH" equiv-text="this.list.selected.size"/> 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">552</context> <context context-type="linenumber">550</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">581</context> <context context-type="linenumber">579</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 &quot;<x id="PH" equiv-text="customField.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will assign the custom field &quot;<x id="PH" equiv-text="customField.name"/>&quot; to <x id="PH_1" equiv-text="this.list.selected.size"/> 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">587</context> <context context-type="linenumber">585</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5789455969634598553" datatype="html"> <trans-unit id="5789455969634598553" datatype="html">
@ -7409,14 +7409,14 @@
)"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> )"/> to <x id="PH_1" equiv-text="this.list.selected.size"/> 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">592,594</context> <context context-type="linenumber">590,592</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 &quot;<x id="PH" equiv-text="customField.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> <source>This operation will remove the custom field &quot;<x id="PH" equiv-text="customField.name"/>&quot; from <x id="PH_1" equiv-text="this.list.selected.size"/> 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">600</context> <context context-type="linenumber">598</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6666899594015948817" datatype="html"> <trans-unit id="6666899594015948817" datatype="html">
@ -7425,7 +7425,7 @@
)"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> selected document(s).</source> )"/> from <x id="PH_1" equiv-text="this.list.selected.size"/> 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">605,607</context> <context context-type="linenumber">603,605</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8050047262594964176" datatype="html"> <trans-unit id="8050047262594964176" datatype="html">
@ -7436,56 +7436,56 @@
)"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> selected document(s).</source> )"/> on <x id="PH_2" equiv-text="this.list.selected.size"/> 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">609,613</context> <context context-type="linenumber">607,611</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.list.selected.size"/> selected document(s) to the trash?</source> <source>Move <x id="PH" equiv-text="this.list.selected.size"/> 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">751</context> <context context-type="linenumber">749</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.list.selected.size"/> selected document(s).</source> <source>This operation will permanently recreate the archive files for <x id="PH" equiv-text="this.list.selected.size"/> 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">793</context> <context context-type="linenumber">791</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">794</context> <context context-type="linenumber">792</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6390006284731990222" datatype="html"> <trans-unit id="6390006284731990222" datatype="html">
<source>This operation will permanently rotate the original version of <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source> <source>This operation will permanently rotate the original version of <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source>
<context-group purpose="location"> <context-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">827</context> <context context-type="linenumber">825</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">846</context> <context context-type="linenumber">844</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.list.selected.size"/> selected documents into a new document.</source> <source>This operation will merge <x id="PH" equiv-text="this.list.selected.size"/> 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">847</context> <context context-type="linenumber">845</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">866</context> <context context-type="linenumber">864</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="476913782630693351" datatype="html"> <trans-unit id="476913782630693351" datatype="html">
@ -7767,7 +7767,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">107</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1559883523769732271" datatype="html"> <trans-unit id="1559883523769732271" datatype="html">
@ -7792,7 +7792,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">185</context> <context context-type="linenumber">182</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/data/document.ts</context> <context context-type="sourcefile">src/app/data/document.ts</context>
@ -7981,161 +7981,167 @@
<source>Dates</source> <source>Dates</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
<context context-type="linenumber">95</context> <context context-type="linenumber">90</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3100631071441658964" datatype="html"> <trans-unit id="3100631071441658964" datatype="html">
<source>Title &amp; content</source> <source>Title &amp; content</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">183</context> <context context-type="linenumber">180</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7408932238599462499" datatype="html"> <trans-unit id="7408932238599462499" datatype="html">
<source>File type</source> <source>File type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">190</context> <context context-type="linenumber">187</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="2649431021108393503" datatype="html"> <trans-unit id="2649431021108393503" datatype="html">
<source>More like</source> <source>More like</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">199</context> <context context-type="linenumber">196</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3697582909018473071" datatype="html"> <trans-unit id="3697582909018473071" datatype="html">
<source>equals</source> <source>equals</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">205</context> <context context-type="linenumber">202</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5325481293405718739" datatype="html"> <trans-unit id="5325481293405718739" datatype="html">
<source>is empty</source> <source>is empty</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">209</context> <context context-type="linenumber">206</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6166785695326182482" datatype="html"> <trans-unit id="6166785695326182482" datatype="html">
<source>is not empty</source> <source>is not empty</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">213</context> <context context-type="linenumber">210</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4686622206659266699" datatype="html"> <trans-unit id="4686622206659266699" datatype="html">
<source>greater than</source> <source>greater than</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">217</context> <context context-type="linenumber">214</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8014012170270529279" datatype="html"> <trans-unit id="8014012170270529279" datatype="html">
<source>less than</source> <source>less than</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">221</context> <context context-type="linenumber">218</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="5195932016807797291" datatype="html"> <trans-unit id="5195932016807797291" datatype="html">
<source>Correspondent: <x id="PH" equiv-text="this.correspondents.find((c) =&gt; c.id == +rule.value)?.name"/></source> <source>Correspondent: <x id="PH" equiv-text="this.correspondentSelectionModel.items.find(
(c) =&gt; c.id == +rule.value
)?.name"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">253,255</context> <context context-type="linenumber">250,254</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8170755470576301659" datatype="html"> <trans-unit id="8170755470576301659" datatype="html">
<source>Without correspondent</source> <source>Without correspondent</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">257</context> <context context-type="linenumber">256</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="317796810569008208" datatype="html"> <trans-unit id="317796810569008208" datatype="html">
<source>Document type: <x id="PH" equiv-text="this.documentTypes.find((dt) =&gt; dt.id == +rule.value)?.name"/></source> <source>Document type: <x id="PH" equiv-text="this.documentTypeSelectionModel.items.find(
(dt) =&gt; dt.id == +rule.value
)?.name"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">263,265</context> <context context-type="linenumber">262,266</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4362173610367509215" datatype="html"> <trans-unit id="4362173610367509215" datatype="html">
<source>Without document type</source> <source>Without document type</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">267</context> <context context-type="linenumber">268</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="232202047340644471" datatype="html"> <trans-unit id="232202047340644471" datatype="html">
<source>Storage path: <x id="PH" equiv-text="this.storagePaths.find((sp) =&gt; sp.id == +rule.value)?.name"/></source> <source>Storage path: <x id="PH" equiv-text="this.storagePathSelectionModel.items.find(
(sp) =&gt; sp.id == +rule.value
)?.name"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">273,275</context> <context context-type="linenumber">274,278</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1562820715074533164" datatype="html"> <trans-unit id="1562820715074533164" datatype="html">
<source>Without storage path</source> <source>Without storage path</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">277</context> <context context-type="linenumber">280</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8180755793012580465" datatype="html"> <trans-unit id="8180755793012580465" datatype="html">
<source>Tag: <x id="PH" equiv-text="this.tags.find((t) =&gt; t.id == +rule.value)?.name"/></source> <source>Tag: <x id="PH" equiv-text="this.tagSelectionModel.items.find((t) =&gt; t.id == +rule.value)?.name"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">281,283</context> <context context-type="linenumber">284,286</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6494566478302448576" datatype="html"> <trans-unit id="6494566478302448576" datatype="html">
<source>Without any tag</source> <source>Without any tag</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">287</context> <context context-type="linenumber">290</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8644099678903817943" datatype="html"> <trans-unit id="8644099678903817943" datatype="html">
<source>Custom fields query</source> <source>Custom fields query</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">291</context> <context context-type="linenumber">294</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6523384805359286307" datatype="html"> <trans-unit id="6523384805359286307" datatype="html">
<source>Title: <x id="PH" equiv-text="rule.value"/></source> <source>Title: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">294</context> <context context-type="linenumber">297</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1872523635812236432" datatype="html"> <trans-unit id="1872523635812236432" datatype="html">
<source>ASN: <x id="PH" equiv-text="rule.value"/></source> <source>ASN: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">297</context> <context context-type="linenumber">300</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="102674688969746976" datatype="html"> <trans-unit id="102674688969746976" datatype="html">
<source>Owner: <x id="PH" equiv-text="rule.value"/></source> <source>Owner: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">300</context> <context context-type="linenumber">303</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="3550877650686009106" datatype="html"> <trans-unit id="3550877650686009106" datatype="html">
<source>Owner not in: <x id="PH" equiv-text="rule.value"/></source> <source>Owner not in: <x id="PH" equiv-text="rule.value"/></source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">303</context> <context context-type="linenumber">306</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="1082034558646673343" datatype="html"> <trans-unit id="1082034558646673343" datatype="html">
<source>Without an owner</source> <source>Without an owner</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context> <context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
<context context-type="linenumber">306</context> <context context-type="linenumber">309</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="7210076240260527720" datatype="html"> <trans-unit id="7210076240260527720" datatype="html">

View File

@ -62,6 +62,7 @@ export class EmailDocumentDialogComponent extends LoadingComponentWithPermission
this.emailAddress = '' this.emailAddress = ''
this.emailSubject = '' this.emailSubject = ''
this.emailMessage = '' this.emailMessage = ''
this.close()
this.toastService.showInfo($localize`Email sent`) this.toastService.showInfo($localize`Email sent`)
}, },
error: (e) => { error: (e) => {

View File

@ -7,6 +7,7 @@ import {
tick, tick,
} from '@angular/core/testing' } from '@angular/core/testing'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
import { import {
DEFAULT_MATCHING_ALGORITHM, DEFAULT_MATCHING_ALGORITHM,
MATCH_ALL, MATCH_ALL,
@ -44,6 +45,11 @@ const nullItem = {
name: 'Not assigned', name: 'Not assigned',
} }
const negativeNullItem = {
id: NEGATIVE_NULL_FILTER_VALUE,
name: 'Not assigned',
}
let selectionModel: FilterableDropdownSelectionModel let selectionModel: FilterableDropdownSelectionModel
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => { describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
@ -64,6 +70,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
hotkeyService = TestBed.inject(HotKeyService) hotkeyService = TestBed.inject(HotKeyService)
fixture = TestBed.createComponent(FilterableDropdownComponent) fixture = TestBed.createComponent(FilterableDropdownComponent)
component = fixture.componentInstance component = fixture.componentInstance
component.selectionModel = new FilterableDropdownSelectionModel()
selectionModel = new FilterableDropdownSelectionModel() selectionModel = new FilterableDropdownSelectionModel()
}) })
@ -74,7 +81,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should support reset', () => { it('should support reset', () => {
component.items = items component.selectionModel.items = items
component.selectionModel = selectionModel component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.Selected) selectionModel.set(items[0].id, ToggleableItemState.Selected)
expect(selectionModel.getSelectedItems()).toHaveLength(1) expect(selectionModel.getSelectedItems()).toHaveLength(1)
@ -96,7 +103,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should emit change when items selected', () => { it('should emit change when items selected', () => {
component.items = items component.selectionModel.items = items
component.selectionModel = selectionModel component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model)) component.selectionModelChange.subscribe((model) => (newModel = model))
@ -110,11 +117,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
selectionModel.set(items[0].id, ToggleableItemState.NotSelected) selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
expect(newModel.getSelectedItems()).toEqual([]) expect(newModel.getSelectedItems()).toEqual([])
expect(component.items).toEqual([nullItem, ...items]) expect(component.selectionModel.items).toEqual([nullItem, ...items])
}) })
it('should emit change when items excluded', () => { it('should emit change when items excluded', () => {
component.items = items component.selectionModel.items = items
component.selectionModel = selectionModel component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model)) component.selectionModelChange.subscribe((model) => (newModel = model))
@ -124,7 +131,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should emit change when items excluded', () => { it('should emit change when items excluded', () => {
component.items = items component.selectionModel.items = items
component.selectionModel = selectionModel component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model)) component.selectionModelChange.subscribe((model) => (newModel = model))
@ -139,8 +146,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should exclude items when excluded and not editing', () => { it('should exclude items when excluded and not editing', () => {
component.items = items component.selectionModel.items = items
component.manyToOne = true component.selectionModel.manyToOne = true
component.selectionModel = selectionModel component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.Selected) selectionModel.set(items[0].id, ToggleableItemState.Selected)
component.excludeClicked(items[0].id) component.excludeClicked(items[0].id)
@ -149,8 +156,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should toggle when items excluded and editing', () => { it('should toggle when items excluded and editing', () => {
component.items = items component.selectionModel.items = items
component.manyToOne = true component.selectionModel.manyToOne = true
component.editing = true component.editing = true
component.selectionModel = selectionModel component.selectionModel = selectionModel
selectionModel.set(items[0].id, ToggleableItemState.NotSelected) selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
@ -160,8 +167,8 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should hide count for item if adding will increase size of set', () => { it('should hide count for item if adding will increase size of set', () => {
component.items = items component.selectionModel.items = items
component.manyToOne = true component.selectionModel.manyToOne = true
component.selectionModel = selectionModel component.selectionModel = selectionModel
expect(component.hideCount(items[0])).toBeFalsy() expect(component.hideCount(items[0])).toBeFalsy()
selectionModel.logicalOperator = LogicalOperator.Or selectionModel.logicalOperator = LogicalOperator.Or
@ -170,7 +177,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
it('should enforce single select when editing', () => { it('should enforce single select when editing', () => {
component.editing = true component.editing = true
component.items = items component.selectionModel.items = items
component.selectionModel = selectionModel component.selectionModel = selectionModel
let newModel: FilterableDropdownSelectionModel let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model)) component.selectionModelChange.subscribe((model) => (newModel = model))
@ -182,11 +189,11 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should support manyToOne selecting', () => { it('should support manyToOne selecting', () => {
component.items = items component.selectionModel.items = items
selectionModel.manyToOne = false selectionModel.manyToOne = false
component.selectionModel = selectionModel component.selectionModel = selectionModel
component.manyToOne = true component.selectionModel.manyToOne = true
expect(component.manyToOne).toBeTruthy() expect(component.selectionModel.manyToOne).toBeTruthy()
let newModel: FilterableDropdownSelectionModel let newModel: FilterableDropdownSelectionModel
component.selectionModelChange.subscribe((model) => (newModel = model)) component.selectionModelChange.subscribe((model) => (newModel = model))
@ -197,12 +204,10 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should dynamically enable / disable modifier toggle', () => { it('should dynamically enable / disable modifier toggle', () => {
component.items = items component.selectionModel.items = items
component.selectionModel = selectionModel component.selectionModel = selectionModel
expect(component.modifierToggleEnabled).toBeTruthy() expect(component.modifierToggleEnabled).toBeTruthy()
selectionModel.toggle(null) component.selectionModel.manyToOne = true
expect(component.modifierToggleEnabled).toBeFalsy()
component.manyToOne = true
expect(component.modifierToggleEnabled).toBeFalsy() expect(component.modifierToggleEnabled).toBeFalsy()
selectionModel.toggle(items[0].id) selectionModel.toggle(items[0].id)
selectionModel.toggle(items[1].id) selectionModel.toggle(items[1].id)
@ -210,7 +215,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should apply changes and close when apply button clicked', () => { it('should apply changes and close when apply button clicked', () => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
component.editing = true component.editing = true
component.selectionModel = selectionModel component.selectionModel = selectionModel
@ -232,7 +237,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should apply on close if enabled', () => { it('should apply on close if enabled', () => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
component.editing = true component.editing = true
component.applyOnClose = true component.applyOnClose = true
@ -250,7 +255,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => { it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
@ -277,7 +282,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})) }))
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => { it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
expect(component.selectionModel.getSelectedItems()).toEqual([]) expect(component.selectionModel.getSelectedItems()).toEqual([])
fixture.nativeElement fixture.nativeElement
@ -297,7 +302,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})) }))
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => { it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
component.editing = true component.editing = true
let applyResult: ChangedItems let applyResult: ChangedItems
@ -319,7 +324,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})) }))
it('should support arrow keyboard navigation', fakeAsync(() => { it('should support arrow keyboard navigation', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
@ -364,7 +369,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})) }))
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => { it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
@ -400,7 +405,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})) }))
it('should support arrow keyboard navigation after click', fakeAsync(() => { it('should support arrow keyboard navigation after click', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
fixture.nativeElement fixture.nativeElement
.querySelector('button') .querySelector('button')
@ -425,9 +430,9 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})) }))
it('should toggle logical operator', fakeAsync(() => { it('should toggle logical operator', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
component.manyToOne = true component.selectionModel.manyToOne = true
selectionModel.set(items[0].id, ToggleableItemState.Selected) selectionModel.set(items[0].id, ToggleableItemState.Selected)
selectionModel.set(items[1].id, ToggleableItemState.Selected) selectionModel.set(items[1].id, ToggleableItemState.Selected)
component.selectionModel = selectionModel component.selectionModel = selectionModel
@ -454,7 +459,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})) }))
it('should toggle intersection include / exclude', fakeAsync(() => { it('should toggle intersection include / exclude', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
selectionModel.set(items[0].id, ToggleableItemState.Selected) selectionModel.set(items[0].id, ToggleableItemState.Selected)
selectionModel.set(items[1].id, ToggleableItemState.Selected) selectionModel.set(items[1].id, ToggleableItemState.Selected)
@ -483,22 +488,55 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
expect(changedResult.getExcludedItems()).toEqual(items) expect(changedResult.getExcludedItems()).toEqual(items)
})) }))
it('selection model should sort items by state', () => { it('should update null item selection on toggleIntersection', () => {
component.items = items.concat([{ id: null, name: 'Null B' }]) component.selectionModel.items = items
component.selectionModel = selectionModel component.selectionModel = selectionModel
component.selectionModel.intersection = Intersection.Include
console.log(component.selectionModel.items[0])
component.selectionModel.set(null, ToggleableItemState.Selected)
component.selectionModel.intersection = Intersection.Exclude
component.selectionModel.toggleIntersection()
console.log(component.selectionModel)
expect(component.selectionModel.getExcludedItems()).toEqual([
negativeNullItem,
])
component.selectionModel.intersection = Intersection.Include
component.selectionModel.toggleIntersection()
expect(component.selectionModel.getSelectedItems()).toEqual([nullItem])
})
it('selection model should sort items by state', () => {
component.selectionModel = selectionModel
component.selectionModel.items = items.concat([{ id: 3, name: 'Item3' }])
selectionModel.toggle(items[1].id) selectionModel.toggle(items[1].id)
selectionModel.apply() selectionModel.apply()
expect(selectionModel.items.length).toEqual(4)
expect(selectionModel.items).toEqual([ expect(selectionModel.items).toEqual([
nullItem, nullItem,
{ id: null, name: 'Null B' },
items[1], items[1],
{ id: 3, name: 'Item3' },
items[0], items[0],
]) ])
selectionModel.intersection = Intersection.Exclude
selectionModel.toggleIntersection()
selectionModel.apply()
expect(selectionModel.items).toEqual([
negativeNullItem,
items[1],
{ id: 3, name: 'Item3' },
items[0],
])
// coverage
selectionModel.items = selectionModel.items.reverse()
selectionModel.apply()
}) })
it('selection model should sort items by state and document counts = 0, if set', () => { it('selection model should sort items by state and document counts = 0, if set', () => {
const tagA = { id: 4, name: 'Tag A' } const tagA = { id: 4, name: 'Tag A' }
component.items = items.concat([tagA]) component.selectionModel.items = items.concat([tagA])
component.selectionModel = selectionModel component.selectionModel = selectionModel
component.documentCounts = [ component.documentCounts = [
{ id: 1, document_count: 0 }, // Tag1 { id: 1, document_count: 0 }, // Tag1
@ -529,7 +567,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should set support create, keep open model and call createRef method', fakeAsync(() => { it('should set support create, keep open model and call createRef method', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
component.selectionModel = selectionModel component.selectionModel = selectionModel
fixture.nativeElement fixture.nativeElement
@ -549,7 +587,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
})) }))
it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => { it('should call create on enter inside filter field if 0 items remain while editing', fakeAsync(() => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
component.editing = true component.editing = true
component.createRef = jest.fn() component.createRef = jest.fn()
@ -569,7 +607,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
const id = 1 const id = 1
const state = ToggleableItemState.Selected const state = ToggleableItemState.Selected
component.selectionModel = selectionModel component.selectionModel = selectionModel
component.manyToOne = true component.selectionModel.manyToOne = true
component.selectionModel.singleSelect = true component.selectionModel.singleSelect = true
component.selectionModel.intersection = Intersection.Include component.selectionModel.intersection = Intersection.Include
component.selectionModel['temporarySelectionStates'].set(id, state) component.selectionModel['temporarySelectionStates'].set(id, state)
@ -596,7 +634,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should support shortcut keys', () => { it('should support shortcut keys', () => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
component.shortcutKey = 't' component.shortcutKey = 't'
fixture.detectChanges() fixture.detectChanges()
@ -606,7 +644,7 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
}) })
it('should support an extra button and not apply changes when clicked', () => { it('should support an extra button and not apply changes when clicked', () => {
component.items = items component.selectionModel.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'
component.extraButtonTitle = 'Extra' component.extraButtonTitle = 'Extra'
component.selectionModel = selectionModel component.selectionModel = selectionModel

View File

@ -12,6 +12,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap' import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, filter, takeUntil } from 'rxjs' import { Subject, filter, takeUntil } from 'rxjs'
import { NEGATIVE_NULL_FILTER_VALUE } from 'src/app/data/filter-rule-type'
import { MatchingModel } from 'src/app/data/matching-model' import { MatchingModel } from 'src/app/data/matching-model'
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions' import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
import { FilterPipe } from 'src/app/pipes/filter.pipe' import { FilterPipe } from 'src/app/pipes/filter.pipe'
@ -61,15 +62,56 @@ export class FilterableDropdownSelectionModel {
} }
set items(items: MatchingModel[]) { set items(items: MatchingModel[]) {
this._items = items if (items) {
this.sortItems() this._items = Array.from(items)
this.sortItems()
this.setNullItem()
}
}
private setNullItem() {
if (this.manyToOne && this.logicalOperator === LogicalOperator.Or) {
if (this._items[0]?.id === null) {
this._items.shift()
}
return
}
const item = {
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
id:
this.manyToOne || this.intersection === Intersection.Include
? null
: NEGATIVE_NULL_FILTER_VALUE,
}
if (
this._items[0]?.id === null ||
this._items[0]?.id === NEGATIVE_NULL_FILTER_VALUE
) {
this._items[0] = item
} else if (this._items) {
this._items.unshift(item)
}
}
constructor(manyToOne: boolean = false) {
this.manyToOne = manyToOne
} }
private sortItems() { private sortItems() {
this._items.sort((a, b) => { this._items.sort((a, b) => {
if (a.id == null && b.id != null) { if (
(a.id == null && b.id != null) ||
(a.id == NEGATIVE_NULL_FILTER_VALUE &&
b.id != NEGATIVE_NULL_FILTER_VALUE)
) {
return -1 return -1
} else if (a.id != null && b.id == null) { } else if (
(a.id != null && b.id == null) ||
(a.id != NEGATIVE_NULL_FILTER_VALUE &&
b.id == NEGATIVE_NULL_FILTER_VALUE)
) {
return 1 return 1
} else if ( } else if (
this.getNonTemporary(a.id) == ToggleableItemState.NotSelected && this.getNonTemporary(a.id) == ToggleableItemState.NotSelected &&
@ -230,6 +272,7 @@ export class FilterableDropdownSelectionModel {
set logicalOperator(operator: LogicalOperator) { set logicalOperator(operator: LogicalOperator) {
this.temporaryLogicalOperator = operator this.temporaryLogicalOperator = operator
this.setNullItem()
} }
toggleOperator() { toggleOperator() {
@ -242,6 +285,7 @@ export class FilterableDropdownSelectionModel {
set intersection(intersection: Intersection) { set intersection(intersection: Intersection) {
this.temporaryIntersection = intersection this.temporaryIntersection = intersection
this.setNullItem()
} }
toggleIntersection() { toggleIntersection() {
@ -250,9 +294,20 @@ export class FilterableDropdownSelectionModel {
this.intersection == Intersection.Include this.intersection == Intersection.Include
? ToggleableItemState.Selected ? ToggleableItemState.Selected
: ToggleableItemState.Excluded : ToggleableItemState.Excluded
this.temporarySelectionStates.forEach((state, key) => { this.temporarySelectionStates.forEach((state, key) => {
this.temporarySelectionStates.set(key, newState) if (key === null && this.intersection === Intersection.Exclude) {
this.temporarySelectionStates.set(NEGATIVE_NULL_FILTER_VALUE, newState)
} else if (
key === NEGATIVE_NULL_FILTER_VALUE &&
this.intersection === Intersection.Include
) {
this.temporarySelectionStates.set(null, newState)
} else {
this.temporarySelectionStates.set(key, newState)
}
}) })
this.changed.next(this) this.changed.next(this)
} }
@ -274,6 +329,7 @@ export class FilterableDropdownSelectionModel {
this.temporarySelectionStates.clear() this.temporarySelectionStates.clear()
this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And this.temporaryLogicalOperator = this._logicalOperator = LogicalOperator.And
this.temporaryIntersection = this._intersection = Intersection.Include this.temporaryIntersection = this._intersection = Intersection.Include
this.setNullItem()
if (fireEvent) { if (fireEvent) {
this.changed.next(this) this.changed.next(this)
} }
@ -305,8 +361,10 @@ export class FilterableDropdownSelectionModel {
isNoneSelected() { isNoneSelected() {
return ( return (
this.selectionSize() == 1 && (this.selectionSize() == 1 &&
this.get(null) == ToggleableItemState.Selected this.get(null) == ToggleableItemState.Selected) ||
(this.intersection == Intersection.Exclude &&
this.get(NEGATIVE_NULL_FILTER_VALUE) == ToggleableItemState.Excluded)
) )
} }
@ -384,25 +442,13 @@ export class FilterableDropdownComponent
filterText: string filterText: string
@Input() _selectionModel: FilterableDropdownSelectionModel
set items(items: MatchingModel[]) {
if (items) {
this._selectionModel.items = Array.from(items)
this._selectionModel.items.unshift({
name: $localize`:Filter drop down element to filter for documents with no correspondent/type/tag assigned:Not assigned`,
id: null,
})
}
}
get items(): MatchingModel[] { get items(): MatchingModel[] {
return this._selectionModel.items return this._selectionModel.items
} }
_selectionModel: FilterableDropdownSelectionModel = @Input({ required: true })
new FilterableDropdownSelectionModel()
@Input()
set selectionModel(model: FilterableDropdownSelectionModel) { set selectionModel(model: FilterableDropdownSelectionModel) {
if (this.selectionModel) { if (this.selectionModel) {
this.selectionModel.changed.complete() this.selectionModel.changed.complete()
@ -423,11 +469,6 @@ export class FilterableDropdownComponent
@Output() @Output()
selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>() selectionModelChange = new EventEmitter<FilterableDropdownSelectionModel>()
@Input()
set manyToOne(manyToOne: boolean) {
this.selectionModel.manyToOne = manyToOne
}
get manyToOne() { get manyToOne() {
return this.selectionModel.manyToOne return this.selectionModel.manyToOne
} }
@ -484,7 +525,7 @@ export class FilterableDropdownComponent
return this.manyToOne return this.manyToOne
? this.selectionModel.selectionSize() > 1 && ? this.selectionModel.selectionSize() > 1 &&
this.selectionModel.getExcludedItems().length == 0 this.selectionModel.getExcludedItems().length == 0
: !this.selectionModel.isNoneSelected() : true
} }
get name(): string { get name(): string {

View File

@ -20,10 +20,8 @@
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) { @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Tag)) {
<pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title <pngx-filterable-dropdown title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags"
[disabled]="!userCanEditAll || disabled" [disabled]="!userCanEditAll || disabled"
[editing]="true" [editing]="true"
[manyToOne]="true"
[applyOnClose]="applyOnClose" [applyOnClose]="applyOnClose"
[createRef]="createTag.bind(this)" [createRef]="createTag.bind(this)"
(opened)="openTagsDropdown()" (opened)="openTagsDropdown()"
@ -36,7 +34,6 @@
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title <pngx-filterable-dropdown title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents"
[disabled]="!userCanEditAll || disabled" [disabled]="!userCanEditAll || disabled"
[editing]="true" [editing]="true"
[applyOnClose]="applyOnClose" [applyOnClose]="applyOnClose"
@ -51,7 +48,6 @@
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title <pngx-filterable-dropdown title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes"
[disabled]="!userCanEditAll || disabled" [disabled]="!userCanEditAll || disabled"
[editing]="true" [editing]="true"
[applyOnClose]="applyOnClose" [applyOnClose]="applyOnClose"
@ -66,7 +62,6 @@
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) { @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title <pngx-filterable-dropdown title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths"
[disabled]="!userCanEditAll || disabled" [disabled]="!userCanEditAll || disabled"
[editing]="true" [editing]="true"
[applyOnClose]="applyOnClose" [applyOnClose]="applyOnClose"
@ -81,10 +76,8 @@
@if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) { @if (permissionService.currentUserCan(PermissionAction.View, PermissionType.CustomField)) {
<pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title <pngx-filterable-dropdown title="Custom fields" icon="ui-radios" i18n-title
filterPlaceholder="Filter custom fields" i18n-filterPlaceholder filterPlaceholder="Filter custom fields" i18n-filterPlaceholder
[items]="customFields"
[disabled]="!userCanEditAll" [disabled]="!userCanEditAll"
[editing]="true" [editing]="true"
[manyToOne]="true"
[applyOnClose]="applyOnClose" [applyOnClose]="applyOnClose"
[createRef]="createCustomField.bind(this)" [createRef]="createCustomField.bind(this)"
(opened)="openCustomFieldsDropdown()" (opened)="openCustomFieldsDropdown()"

View File

@ -1150,10 +1150,10 @@ describe('BulkEditorComponent', () => {
it('should not attempt to retrieve objects if user does not have permissions', () => { it('should not attempt to retrieve objects if user does not have permissions', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.tags).toBeUndefined() expect(component.tagSelectionModel.items.length).toEqual(0)
expect(component.correspondents).toBeUndefined() expect(component.correspondentSelectionModel.items.length).toEqual(0)
expect(component.documentTypes).toBeUndefined() expect(component.documentTypeSelectionModel.items.length).toEqual(0)
expect(component.storagePaths).toBeUndefined() expect(component.storagePathsSelectionModel.items.length).toEqual(0)
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`) httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
httpTestingController.expectNone( httpTestingController.expectNone(
`${environment.apiBaseUrl}documents/correspondents/` `${environment.apiBaseUrl}documents/correspondents/`
@ -1204,7 +1204,9 @@ describe('BulkEditorComponent', () => {
expect(tagListAllSpy).toHaveBeenCalled() expect(tagListAllSpy).toHaveBeenCalled()
expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id) expect(tagSelectionModelToggleSpy).toHaveBeenCalledWith(newTag.id)
expect(component.tags).toEqual(tags.results) expect(component.tagSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(tags.results as any)
)
}) })
it('should support create new correspondent', () => { it('should support create new correspondent', () => {
@ -1251,7 +1253,9 @@ describe('BulkEditorComponent', () => {
expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith( expect(correspondentSelectionModelToggleSpy).toHaveBeenCalledWith(
newCorrespondent.id newCorrespondent.id
) )
expect(component.correspondents).toEqual(correspondents.results) expect(component.correspondentSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(correspondents.results as any)
)
}) })
it('should support create new document type', () => { it('should support create new document type', () => {
@ -1295,7 +1299,9 @@ describe('BulkEditorComponent', () => {
expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith( expect(documentTypeSelectionModelToggleSpy).toHaveBeenCalledWith(
newDocumentType.id newDocumentType.id
) )
expect(component.documentTypes).toEqual(documentTypes.results) expect(component.documentTypeSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(documentTypes.results as any)
)
}) })
it('should support create new storage path', () => { it('should support create new storage path', () => {
@ -1339,7 +1345,9 @@ describe('BulkEditorComponent', () => {
expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith( expect(storagePathsSelectionModelToggleSpy).toHaveBeenCalledWith(
newStoragePath.id newStoragePath.id
) )
expect(component.storagePaths).toEqual(storagePaths.results) expect(component.storagePathsSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(storagePaths.results as any)
)
}) })
it('should support create new custom field', () => { it('should support create new custom field', () => {
@ -1391,7 +1399,9 @@ describe('BulkEditorComponent', () => {
expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith( expect(customFieldsSelectionModelToggleSpy).toHaveBeenCalledWith(
newCustomField.id newCustomField.id
) )
expect(component.customFields).toEqual(customFields.results) expect(component.customFieldsSelectionModel.items).toEqual(
[{ id: null, name: 'Not assigned' }].concat(customFields.results as any)
)
}) })
it('should open the bulk edit custom field values dialog with correct parameters', () => { it('should open the bulk edit custom field values dialog with correct parameters', () => {
@ -1416,17 +1426,17 @@ describe('BulkEditorComponent', () => {
const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError') const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
const listReloadSpy = jest.spyOn(documentListViewService, 'reload') const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
component.customFields = [ component.customFieldsSelectionModel.items = [
{ id: 1, name: 'Custom Field 1', data_type: CustomFieldDataType.String }, { id: 1, name: 'Custom Field 1', data_type: CustomFieldDataType.String },
{ id: 2, name: 'Custom Field 2', data_type: CustomFieldDataType.String }, { id: 2, name: 'Custom Field 2', data_type: CustomFieldDataType.String },
] ] as any
component.setCustomFieldValues({ component.setCustomFieldValues({
itemsToAdd: [{ id: 1 }, { id: 2 }], itemsToAdd: [{ id: 1 }, { id: 2 }],
itemsToRemove: [1], itemsToRemove: [1],
} as any) } as any)
expect(modal.componentInstance.customFields).toEqual(component.customFields) expect(modal.componentInstance.customFields.length).toEqual(2)
expect(modal.componentInstance.fieldsToAddIds).toEqual([1, 2]) expect(modal.componentInstance.fieldsToAddIds).toEqual([1, 2])
expect(modal.componentInstance.documents).toEqual([3, 4]) expect(modal.componentInstance.documents).toEqual([3, 4])

View File

@ -14,12 +14,8 @@ import { saveAs } from 'file-saver'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { first, map, Subject, switchMap, takeUntil } from 'rxjs' import { first, map, Subject, switchMap, takeUntil } from 'rxjs'
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component' import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
import { Correspondent } from 'src/app/data/correspondent'
import { CustomField } from 'src/app/data/custom-field' import { CustomField } from 'src/app/data/custom-field'
import { DocumentType } from 'src/app/data/document-type'
import { MatchingModel } from 'src/app/data/matching-model' import { MatchingModel } from 'src/app/data/matching-model'
import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { DocumentListViewService } from 'src/app/services/document-list-view.service' import { DocumentListViewService } from 'src/app/services/document-list-view.service'
@ -75,17 +71,11 @@ export class BulkEditorComponent
extends ComponentWithPermissions extends ComponentWithPermissions
implements OnInit, OnDestroy implements OnInit, OnDestroy
{ {
tags: Tag[] tagSelectionModel = new FilterableDropdownSelectionModel(true)
correspondents: Correspondent[]
documentTypes: DocumentType[]
storagePaths: StoragePath[]
customFields: CustomField[]
tagSelectionModel = new FilterableDropdownSelectionModel()
correspondentSelectionModel = new FilterableDropdownSelectionModel() correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel() documentTypeSelectionModel = new FilterableDropdownSelectionModel()
storagePathsSelectionModel = new FilterableDropdownSelectionModel() storagePathsSelectionModel = new FilterableDropdownSelectionModel()
customFieldsSelectionModel = new FilterableDropdownSelectionModel() customFieldsSelectionModel = new FilterableDropdownSelectionModel(true)
tagDocumentCounts: SelectionDataItem[] tagDocumentCounts: SelectionDataItem[]
correspondentDocumentCounts: SelectionDataItem[] correspondentDocumentCounts: SelectionDataItem[]
documentTypeDocumentCounts: SelectionDataItem[] documentTypeDocumentCounts: SelectionDataItem[]
@ -176,7 +166,7 @@ export class BulkEditorComponent
this.tagService this.tagService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.tags = result.results)) .subscribe((result) => (this.tagSelectionModel.items = result.results))
} }
if ( if (
this.permissionService.currentUserCan( this.permissionService.currentUserCan(
@ -187,7 +177,9 @@ export class BulkEditorComponent
this.correspondentService this.correspondentService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.correspondents = result.results)) .subscribe(
(result) => (this.correspondentSelectionModel.items = result.results)
)
} }
if ( if (
this.permissionService.currentUserCan( this.permissionService.currentUserCan(
@ -198,7 +190,9 @@ export class BulkEditorComponent
this.documentTypeService this.documentTypeService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.documentTypes = result.results)) .subscribe(
(result) => (this.documentTypeSelectionModel.items = result.results)
)
} }
if ( if (
this.permissionService.currentUserCan( this.permissionService.currentUserCan(
@ -209,7 +203,9 @@ export class BulkEditorComponent
this.storagePathService this.storagePathService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.storagePaths = result.results)) .subscribe(
(result) => (this.storagePathsSelectionModel.items = result.results)
)
} }
if ( if (
this.permissionService.currentUserCan( this.permissionService.currentUserCan(
@ -220,7 +216,9 @@ export class BulkEditorComponent
this.customFieldService this.customFieldService
.listAll() .listAll()
.pipe(first()) .pipe(first())
.subscribe((result) => (this.customFields = result.results)) .subscribe(
(result) => (this.customFieldsSelectionModel.items = result.results)
)
} }
this.downloadForm this.downloadForm
@ -651,7 +649,7 @@ export class BulkEditorComponent
) )
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newTag, tags }) => { .subscribe(({ newTag, tags }) => {
this.tags = tags.results this.tagSelectionModel.items = tags.results
this.tagSelectionModel.toggle(newTag.id) this.tagSelectionModel.toggle(newTag.id)
}) })
} }
@ -674,7 +672,7 @@ export class BulkEditorComponent
) )
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newCorrespondent, correspondents }) => { .subscribe(({ newCorrespondent, correspondents }) => {
this.correspondents = correspondents.results this.correspondentSelectionModel.items = correspondents.results
this.correspondentSelectionModel.toggle(newCorrespondent.id) this.correspondentSelectionModel.toggle(newCorrespondent.id)
}) })
} }
@ -695,7 +693,7 @@ export class BulkEditorComponent
) )
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newDocumentType, documentTypes }) => { .subscribe(({ newDocumentType, documentTypes }) => {
this.documentTypes = documentTypes.results this.documentTypeSelectionModel.items = documentTypes.results
this.documentTypeSelectionModel.toggle(newDocumentType.id) this.documentTypeSelectionModel.toggle(newDocumentType.id)
}) })
} }
@ -716,7 +714,7 @@ export class BulkEditorComponent
) )
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newStoragePath, storagePaths }) => { .subscribe(({ newStoragePath, storagePaths }) => {
this.storagePaths = storagePaths.results this.storagePathsSelectionModel.items = storagePaths.results
this.storagePathsSelectionModel.toggle(newStoragePath.id) this.storagePathsSelectionModel.toggle(newStoragePath.id)
}) })
} }
@ -737,7 +735,7 @@ export class BulkEditorComponent
) )
.pipe(takeUntil(this.unsubscribeNotifier)) .pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(({ newCustomField, customFields }) => { .subscribe(({ newCustomField, customFields }) => {
this.customFields = customFields.results this.customFieldsSelectionModel.items = customFields.results
this.customFieldsSelectionModel.toggle(newCustomField.id) this.customFieldsSelectionModel.toggle(newCustomField.id)
}) })
} }
@ -875,7 +873,9 @@ export class BulkEditorComponent
}) })
const dialog = const dialog =
modal.componentInstance as CustomFieldsBulkEditDialogComponent modal.componentInstance as CustomFieldsBulkEditDialogComponent
dialog.customFields = this.customFields dialog.customFields = (
this.customFieldsSelectionModel.items as CustomField[]
).filter((f) => f.id !== null)
dialog.fieldsToAddIds = changedCustomFields.itemsToAdd.map( dialog.fieldsToAddIds = changedCustomFields.itemsToAdd.map(
(item) => item.id (item) => item.id
) )

View File

@ -35,11 +35,9 @@
<div class="col-auto"> <div class="col-auto">
<div class="d-flex flex-wrap gap-3"> <div class="d-flex flex-wrap gap-3">
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tags.length > 0) { @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tagSelectionModel.items.length > 0) {
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Tags" icon="tag-fill" i18n-title <pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder filterPlaceholder="Filter tags" i18n-filterPlaceholder
[items]="tags"
[manyToOne]="true"
[(selectionModel)]="tagSelectionModel" [(selectionModel)]="tagSelectionModel"
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(opened)="onTagsDropdownOpen()" (opened)="onTagsDropdownOpen()"
@ -48,10 +46,9 @@
[disabled]="disabled" [disabled]="disabled"
shortcutKey="t"></pngx-filterable-dropdown> shortcutKey="t"></pngx-filterable-dropdown>
} }
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondents.length > 0) { @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) {
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Correspondent" icon="person-fill" i18n-title <pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[items]="correspondents"
[(selectionModel)]="correspondentSelectionModel" [(selectionModel)]="correspondentSelectionModel"
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(opened)="onCorrespondentDropdownOpen()" (opened)="onCorrespondentDropdownOpen()"
@ -60,10 +57,9 @@
[disabled]="disabled" [disabled]="disabled"
shortcutKey="y"></pngx-filterable-dropdown> shortcutKey="y"></pngx-filterable-dropdown>
} }
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypes.length > 0) { @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) {
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Document type" icon="file-earmark-fill" i18n-title <pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder filterPlaceholder="Filter document types" i18n-filterPlaceholder
[items]="documentTypes"
[(selectionModel)]="documentTypeSelectionModel" [(selectionModel)]="documentTypeSelectionModel"
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(opened)="onDocumentTypeDropdownOpen()" (opened)="onDocumentTypeDropdownOpen()"
@ -72,10 +68,9 @@
[disabled]="disabled" [disabled]="disabled"
shortcutKey="u"></pngx-filterable-dropdown> shortcutKey="u"></pngx-filterable-dropdown>
} }
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePaths.length > 0) { @if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) {
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Storage path" icon="folder-fill" i18n-title <pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[items]="storagePaths"
[(selectionModel)]="storagePathSelectionModel" [(selectionModel)]="storagePathSelectionModel"
(selectionModelChange)="updateRules()" (selectionModelChange)="updateRules()"
(opened)="onStoragePathDropdownOpen()" (opened)="onStoragePathDropdownOpen()"

View File

@ -69,6 +69,7 @@ import {
FILTER_STORAGE_PATH, FILTER_STORAGE_PATH,
FILTER_TITLE, FILTER_TITLE,
FILTER_TITLE_CONTENT, FILTER_TITLE_CONTENT,
NEGATIVE_NULL_FILTER_VALUE,
} from 'src/app/data/filter-rule-type' } from 'src/app/data/filter-rule-type'
import { StoragePath } from 'src/app/data/storage-path' import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag' import { Tag } from 'src/app/data/tag'
@ -671,9 +672,6 @@ describe('FilterEditorComponent', () => {
value: '12', value: '12',
}, },
] ]
expect(component.correspondentSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.correspondentSelectionModel.intersection).toEqual( expect(component.correspondentSelectionModel.intersection).toEqual(
Intersection.Include Intersection.Include
) )
@ -681,6 +679,19 @@ describe('FilterEditorComponent', () => {
correspondents[0], correspondents[0],
]) ])
component.toggleCorrespondent(12) // coverage component.toggleCorrespondent(12) // coverage
component.filterRules = [
{
rule_type: FILTER_CORRESPONDENT,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
]
expect(component.correspondentSelectionModel.intersection).toEqual(
Intersection.Exclude
)
expect(component.correspondentSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
})) }))
it('should ingest filter rules for has any of correspondents', fakeAsync(() => { it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
@ -754,9 +765,6 @@ describe('FilterEditorComponent', () => {
value: '22', value: '22',
}, },
] ]
expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.documentTypeSelectionModel.intersection).toEqual( expect(component.documentTypeSelectionModel.intersection).toEqual(
Intersection.Include Intersection.Include
) )
@ -764,6 +772,19 @@ describe('FilterEditorComponent', () => {
document_types[0], document_types[0],
]) ])
component.toggleDocumentType(22) // coverage component.toggleDocumentType(22) // coverage
component.filterRules = [
{
rule_type: FILTER_DOCUMENT_TYPE,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
]
expect(component.documentTypeSelectionModel.intersection).toEqual(
Intersection.Exclude
)
expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
})) }))
it('should ingest filter rules for has any of document types', fakeAsync(() => { it('should ingest filter rules for has any of document types', fakeAsync(() => {
@ -780,9 +801,6 @@ describe('FilterEditorComponent', () => {
value: '23', value: '23',
}, },
] ]
expect(component.documentTypeSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.documentTypeSelectionModel.intersection).toEqual( expect(component.documentTypeSelectionModel.intersection).toEqual(
Intersection.Include Intersection.Include
) )
@ -837,9 +855,6 @@ describe('FilterEditorComponent', () => {
value: '32', value: '32',
}, },
] ]
expect(component.storagePathSelectionModel.logicalOperator).toEqual(
LogicalOperator.Or
)
expect(component.storagePathSelectionModel.intersection).toEqual( expect(component.storagePathSelectionModel.intersection).toEqual(
Intersection.Include Intersection.Include
) )
@ -847,6 +862,19 @@ describe('FilterEditorComponent', () => {
storage_paths[0], storage_paths[0],
]) ])
component.toggleStoragePath(32) // coverage component.toggleStoragePath(32) // coverage
component.filterRules = [
{
rule_type: FILTER_STORAGE_PATH,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
]
expect(component.storagePathSelectionModel.intersection).toEqual(
Intersection.Exclude
)
expect(component.storagePathSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
})) }))
it('should ingest filter rules for has any of storage paths', fakeAsync(() => { it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
@ -1398,6 +1426,19 @@ describe('FilterEditorComponent', () => {
value: null, value: null,
}, },
]) ])
const excludeButton = correspondentsFilterableDropdown.queryAll(
By.css('input[value=exclude]')
)[0]
excludeButton.nativeElement.checked = true
excludeButton.triggerEventHandler('change')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_CORRESPONDENT,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
})) }))
it('should convert user input to correct filter rules on document type selections', fakeAsync(() => { it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
@ -1455,6 +1496,19 @@ describe('FilterEditorComponent', () => {
value: null, value: null,
}, },
]) ])
const excludeButton = docTypesFilterableDropdown.queryAll(
By.css('input[value=exclude]')
)[0]
excludeButton.nativeElement.checked = true
excludeButton.triggerEventHandler('change')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_DOCUMENT_TYPE,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
})) }))
it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => { it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
@ -1512,6 +1566,19 @@ describe('FilterEditorComponent', () => {
value: null, value: null,
}, },
]) ])
const excludeButton = storagePathsFilterableDropdown.queryAll(
By.css('input[value=exclude]')
)[0]
excludeButton.nativeElement.checked = true
excludeButton.triggerEventHandler('change')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_STORAGE_PATH,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
})) }))
it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => { it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {

View File

@ -26,14 +26,12 @@ import {
switchMap, switchMap,
takeUntil, takeUntil,
} from 'rxjs/operators' } from 'rxjs/operators'
import { Correspondent } from 'src/app/data/correspondent'
import { CustomField } from 'src/app/data/custom-field' import { CustomField } from 'src/app/data/custom-field'
import { import {
CustomFieldQueryLogicalOperator, CustomFieldQueryLogicalOperator,
CustomFieldQueryOperator, CustomFieldQueryOperator,
} from 'src/app/data/custom-field-query' } from 'src/app/data/custom-field-query'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import { DocumentType } from 'src/app/data/document-type'
import { FilterRule } from 'src/app/data/filter-rule' import { FilterRule } from 'src/app/data/filter-rule'
import { import {
FILTER_ADDED_AFTER, FILTER_ADDED_AFTER,
@ -75,9 +73,8 @@ import {
FILTER_STORAGE_PATH, FILTER_STORAGE_PATH,
FILTER_TITLE, FILTER_TITLE,
FILTER_TITLE_CONTENT, FILTER_TITLE_CONTENT,
NEGATIVE_NULL_FILTER_VALUE,
} from 'src/app/data/filter-rule-type' } from 'src/app/data/filter-rule-type'
import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag'
import { import {
PermissionAction, PermissionAction,
PermissionType, PermissionType,
@ -251,7 +248,9 @@ export class FilterEditorComponent
case FILTER_HAS_CORRESPONDENT_ANY: case FILTER_HAS_CORRESPONDENT_ANY:
if (rule.value) { if (rule.value) {
return $localize`Correspondent: ${ return $localize`Correspondent: ${
this.correspondents.find((c) => c.id == +rule.value)?.name this.correspondentSelectionModel.items.find(
(c) => c.id == +rule.value
)?.name
}` }`
} else { } else {
return $localize`Without correspondent` return $localize`Without correspondent`
@ -261,7 +260,9 @@ export class FilterEditorComponent
case FILTER_HAS_DOCUMENT_TYPE_ANY: case FILTER_HAS_DOCUMENT_TYPE_ANY:
if (rule.value) { if (rule.value) {
return $localize`Document type: ${ return $localize`Document type: ${
this.documentTypes.find((dt) => dt.id == +rule.value)?.name this.documentTypeSelectionModel.items.find(
(dt) => dt.id == +rule.value
)?.name
}` }`
} else { } else {
return $localize`Without document type` return $localize`Without document type`
@ -271,7 +272,9 @@ export class FilterEditorComponent
case FILTER_HAS_STORAGE_PATH_ANY: case FILTER_HAS_STORAGE_PATH_ANY:
if (rule.value) { if (rule.value) {
return $localize`Storage path: ${ return $localize`Storage path: ${
this.storagePaths.find((sp) => sp.id == +rule.value)?.name this.storagePathSelectionModel.items.find(
(sp) => sp.id == +rule.value
)?.name
}` }`
} else { } else {
return $localize`Without storage path` return $localize`Without storage path`
@ -279,7 +282,7 @@ export class FilterEditorComponent
case FILTER_HAS_TAGS_ALL: case FILTER_HAS_TAGS_ALL:
return $localize`Tag: ${ return $localize`Tag: ${
this.tags.find((t) => t.id == +rule.value)?.name this.tagSelectionModel.items.find((t) => t.id == +rule.value)?.name
}` }`
case FILTER_HAS_ANY_TAG: case FILTER_HAS_ANY_TAG:
@ -326,10 +329,6 @@ export class FilterEditorComponent
@ViewChild('textFilterInput') @ViewChild('textFilterInput')
textFilterInput: ElementRef textFilterInput: ElementRef
tags: Tag[] = []
correspondents: Correspondent[] = []
documentTypes: DocumentType[] = []
storagePaths: StoragePath[] = []
customFields: CustomField[] = [] customFields: CustomField[] = []
tagDocumentCounts: SelectionDataItem[] tagDocumentCounts: SelectionDataItem[]
@ -370,7 +369,7 @@ export class FilterEditorComponent
) )
} }
tagSelectionModel = new FilterableDropdownSelectionModel() tagSelectionModel = new FilterableDropdownSelectionModel(true)
correspondentSelectionModel = new FilterableDropdownSelectionModel() correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel() documentTypeSelectionModel = new FilterableDropdownSelectionModel()
storagePathSelectionModel = new FilterableDropdownSelectionModel() storagePathSelectionModel = new FilterableDropdownSelectionModel()
@ -551,6 +550,19 @@ export class FilterEditorComponent
) )
break break
case FILTER_CORRESPONDENT: case FILTER_CORRESPONDENT:
this.correspondentSelectionModel.intersection =
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
? Intersection.Exclude
: Intersection.Include
this.correspondentSelectionModel.set(
rule.value ? +rule.value : null,
this.correspondentSelectionModel.intersection ==
Intersection.Include
? ToggleableItemState.Selected
: ToggleableItemState.Excluded,
false
)
break
case FILTER_HAS_CORRESPONDENT_ANY: case FILTER_HAS_CORRESPONDENT_ANY:
this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or this.correspondentSelectionModel.logicalOperator = LogicalOperator.Or
this.correspondentSelectionModel.intersection = Intersection.Include this.correspondentSelectionModel.intersection = Intersection.Include
@ -569,6 +581,18 @@ export class FilterEditorComponent
) )
break break
case FILTER_DOCUMENT_TYPE: case FILTER_DOCUMENT_TYPE:
this.documentTypeSelectionModel.intersection =
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
? Intersection.Exclude
: Intersection.Include
this.documentTypeSelectionModel.set(
rule.value ? +rule.value : null,
this.documentTypeSelectionModel.intersection == Intersection.Include
? ToggleableItemState.Selected
: ToggleableItemState.Excluded,
false
)
break
case FILTER_HAS_DOCUMENT_TYPE_ANY: case FILTER_HAS_DOCUMENT_TYPE_ANY:
this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or this.documentTypeSelectionModel.logicalOperator = LogicalOperator.Or
this.documentTypeSelectionModel.intersection = Intersection.Include this.documentTypeSelectionModel.intersection = Intersection.Include
@ -587,6 +611,18 @@ export class FilterEditorComponent
) )
break break
case FILTER_STORAGE_PATH: case FILTER_STORAGE_PATH:
this.storagePathSelectionModel.intersection =
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
? Intersection.Exclude
: Intersection.Include
this.storagePathSelectionModel.set(
rule.value ? +rule.value : null,
this.storagePathSelectionModel.intersection == Intersection.Include
? ToggleableItemState.Selected
: ToggleableItemState.Excluded,
false
)
break
case FILTER_HAS_STORAGE_PATH_ANY: case FILTER_HAS_STORAGE_PATH_ANY:
this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or this.storagePathSelectionModel.logicalOperator = LogicalOperator.Or
this.storagePathSelectionModel.intersection = Intersection.Include this.storagePathSelectionModel.intersection = Intersection.Include
@ -809,9 +845,21 @@ export class FilterEditorComponent
}) })
}) })
} }
if (this.correspondentSelectionModel.isNoneSelected()) { if (
this.correspondentSelectionModel.isNoneSelected() &&
this.correspondentSelectionModel.intersection == Intersection.Include
) {
filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null }) filterRules.push({ rule_type: FILTER_CORRESPONDENT, value: null })
} else { } else {
if (
this.correspondentSelectionModel.isNoneSelected() &&
this.correspondentSelectionModel.intersection == Intersection.Exclude
) {
filterRules.push({
rule_type: FILTER_CORRESPONDENT,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
})
}
this.correspondentSelectionModel this.correspondentSelectionModel
.getSelectedItems() .getSelectedItems()
.forEach((correspondent) => { .forEach((correspondent) => {
@ -822,6 +870,7 @@ export class FilterEditorComponent
}) })
this.correspondentSelectionModel this.correspondentSelectionModel
.getExcludedItems() .getExcludedItems()
.filter((correspondent) => correspondent.id > 0)
.forEach((correspondent) => { .forEach((correspondent) => {
filterRules.push({ filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT, rule_type: FILTER_DOES_NOT_HAVE_CORRESPONDENT,
@ -829,9 +878,21 @@ export class FilterEditorComponent
}) })
}) })
} }
if (this.documentTypeSelectionModel.isNoneSelected()) { if (
this.documentTypeSelectionModel.isNoneSelected() &&
this.documentTypeSelectionModel.intersection === Intersection.Include
) {
filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null }) filterRules.push({ rule_type: FILTER_DOCUMENT_TYPE, value: null })
} else { } else {
if (
this.documentTypeSelectionModel.isNoneSelected() &&
this.documentTypeSelectionModel.intersection == Intersection.Exclude
) {
filterRules.push({
rule_type: FILTER_DOCUMENT_TYPE,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
})
}
this.documentTypeSelectionModel this.documentTypeSelectionModel
.getSelectedItems() .getSelectedItems()
.forEach((documentType) => { .forEach((documentType) => {
@ -842,6 +903,7 @@ export class FilterEditorComponent
}) })
this.documentTypeSelectionModel this.documentTypeSelectionModel
.getExcludedItems() .getExcludedItems()
.filter((documentType) => documentType.id > 0)
.forEach((documentType) => { .forEach((documentType) => {
filterRules.push({ filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE, rule_type: FILTER_DOES_NOT_HAVE_DOCUMENT_TYPE,
@ -849,9 +911,21 @@ export class FilterEditorComponent
}) })
}) })
} }
if (this.storagePathSelectionModel.isNoneSelected()) { if (
this.storagePathSelectionModel.isNoneSelected() &&
this.storagePathSelectionModel.intersection == Intersection.Include
) {
filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null }) filterRules.push({ rule_type: FILTER_STORAGE_PATH, value: null })
} else { } else {
if (
this.storagePathSelectionModel.isNoneSelected() &&
this.storagePathSelectionModel.intersection == Intersection.Exclude
) {
filterRules.push({
rule_type: FILTER_STORAGE_PATH,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
})
}
this.storagePathSelectionModel this.storagePathSelectionModel
.getSelectedItems() .getSelectedItems()
.forEach((storagePath) => { .forEach((storagePath) => {
@ -862,6 +936,7 @@ export class FilterEditorComponent
}) })
this.storagePathSelectionModel this.storagePathSelectionModel
.getExcludedItems() .getExcludedItems()
.filter((storagePath) => storagePath.id > 0)
.forEach((storagePath) => { .forEach((storagePath) => {
filterRules.push({ filterRules.push({
rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH, rule_type: FILTER_DOES_NOT_HAVE_STORAGE_PATH,
@ -1062,7 +1137,7 @@ export class FilterEditorComponent
) { ) {
this.loadingCountTotal++ this.loadingCountTotal++
this.tagService.listAll().subscribe((result) => { this.tagService.listAll().subscribe((result) => {
this.tags = result.results this.tagSelectionModel.items = result.results
this.maybeCompleteLoading() this.maybeCompleteLoading()
}) })
} }
@ -1074,7 +1149,7 @@ export class FilterEditorComponent
) { ) {
this.loadingCountTotal++ this.loadingCountTotal++
this.correspondentService.listAll().subscribe((result) => { this.correspondentService.listAll().subscribe((result) => {
this.correspondents = result.results this.correspondentSelectionModel.items = result.results
this.maybeCompleteLoading() this.maybeCompleteLoading()
}) })
} }
@ -1086,7 +1161,7 @@ export class FilterEditorComponent
) { ) {
this.loadingCountTotal++ this.loadingCountTotal++
this.documentTypeService.listAll().subscribe((result) => { this.documentTypeService.listAll().subscribe((result) => {
this.documentTypes = result.results this.documentTypeSelectionModel.items = result.results
this.maybeCompleteLoading() this.maybeCompleteLoading()
}) })
} }
@ -1098,7 +1173,7 @@ export class FilterEditorComponent
) { ) {
this.loadingCountTotal++ this.loadingCountTotal++
this.storagePathService.listAll().subscribe((result) => { this.storagePathService.listAll().subscribe((result) => {
this.storagePaths = result.results this.storagePathSelectionModel.items = result.results
this.maybeCompleteLoading() this.maybeCompleteLoading()
}) })
} }

View File

@ -1,5 +1,7 @@
import { DataType } from './datatype' import { DataType } from './datatype'
export const NEGATIVE_NULL_FILTER_VALUE = -1
// These correspond to src/documents/models.py and changes here require a DB migration (and vice versa) // These correspond to src/documents/models.py and changes here require a DB migration (and vice versa)
export const FILTER_TITLE = 0 export const FILTER_TITLE = 0
export const FILTER_CONTENT = 1 export const FILTER_CONTENT = 1

View File

@ -8,6 +8,7 @@ import {
FILTER_HAS_CUSTOM_FIELDS_ALL, FILTER_HAS_CUSTOM_FIELDS_ALL,
FILTER_HAS_CUSTOM_FIELDS_ANY, FILTER_HAS_CUSTOM_FIELDS_ANY,
FILTER_HAS_TAGS_ALL, FILTER_HAS_TAGS_ALL,
NEGATIVE_NULL_FILTER_VALUE,
} from '../data/filter-rule-type' } from '../data/filter-rule-type'
import { import {
filterRulesFromQueryParams, filterRulesFromQueryParams,
@ -97,6 +98,16 @@ describe('QueryParams Utils', () => {
correspondent__isnull: 1, correspondent__isnull: 1,
}) })
params = queryParamsFromFilterRules([
{
rule_type: FILTER_CORRESPONDENT,
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
expect(params).toEqual({
correspondent__isnull: 0,
})
params = queryParamsFromFilterRules([ params = queryParamsFromFilterRules([
{ {
rule_type: FILTER_HAS_ANY_TAG, rule_type: FILTER_HAS_ANY_TAG,

View File

@ -10,6 +10,7 @@ import {
FILTER_HAS_CUSTOM_FIELDS_ANY, FILTER_HAS_CUSTOM_FIELDS_ANY,
FILTER_RULE_TYPES, FILTER_RULE_TYPES,
FilterRuleType, FilterRuleType,
NEGATIVE_NULL_FILTER_VALUE,
} from '../data/filter-rule-type' } from '../data/filter-rule-type'
import { ListViewState } from '../services/document-list-view.service' import { ListViewState } from '../services/document-list-view.service'
@ -113,6 +114,10 @@ export function filterRulesFromQueryParams(
rt.isnull_filtervar == filterQueryParamName rt.isnull_filtervar == filterQueryParamName
) )
const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName const isNullRuleType = rule_type.isnull_filtervar == filterQueryParamName
const nullRuleValue =
queryParams.get(filterQueryParamName) == '1'
? null
: NEGATIVE_NULL_FILTER_VALUE.toString()
const valueURIComponent: string = queryParams.get(filterQueryParamName) const valueURIComponent: string = queryParams.get(filterQueryParamName)
const filterQueryParamValues: string[] = rule_type.multi const filterQueryParamValues: string[] = rule_type.multi
? valueURIComponent.split(',') ? valueURIComponent.split(',')
@ -125,7 +130,7 @@ export function filterRulesFromQueryParams(
val = val.replace('1', 'true').replace('0', 'false') val = val.replace('1', 'true').replace('0', 'false')
return { return {
rule_type: rule_type.id, rule_type: rule_type.id,
value: isNullRuleType ? null : val, value: isNullRuleType ? nullRuleValue : val,
} }
}) })
) )
@ -143,6 +148,11 @@ export function queryParamsFromFilterRules(filterRules: FilterRule[]): Params {
let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type) let ruleType = FILTER_RULE_TYPES.find((t) => t.id == rule.rule_type)
if (ruleType.isnull_filtervar && rule.value == null) { if (ruleType.isnull_filtervar && rule.value == null) {
params[ruleType.isnull_filtervar] = 1 params[ruleType.isnull_filtervar] = 1
} else if (
ruleType.isnull_filtervar &&
rule.value == NEGATIVE_NULL_FILTER_VALUE.toString()
) {
params[ruleType.isnull_filtervar] = 0
} else if (ruleType.multi) { } else if (ruleType.multi) {
params[ruleType.filtervar] = params[ruleType.filtervar] params[ruleType.filtervar] = params[ruleType.filtervar]
? params[ruleType.filtervar] + ',' + rule.value ? params[ruleType.filtervar] + ',' + rule.value

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

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

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

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

View File

@ -70,57 +70,59 @@ def set_permissions_for_object(permissions: list[str], object, *, merge: bool =
for action in permissions: for action in permissions:
permission = f"{action}_{object.__class__.__name__.lower()}" permission = f"{action}_{object.__class__.__name__.lower()}"
# users if "users" in permissions[action]:
users_to_add = User.objects.filter(id__in=permissions[action]["users"]) # users
users_to_remove = ( users_to_add = User.objects.filter(id__in=permissions[action]["users"])
get_users_with_perms( users_to_remove = (
object, get_users_with_perms(
only_with_perms_in=[permission], object,
with_group_users=False, only_with_perms_in=[permission],
with_group_users=False,
)
if not merge
else User.objects.none()
) )
if not merge if len(users_to_add) > 0 and len(users_to_remove) > 0:
else User.objects.none() users_to_remove = users_to_remove.exclude(id__in=users_to_add)
) if len(users_to_remove) > 0:
if len(users_to_add) > 0 and len(users_to_remove) > 0: for user in users_to_remove:
users_to_remove = users_to_remove.exclude(id__in=users_to_add) remove_perm(permission, user, object)
if len(users_to_remove) > 0: if len(users_to_add) > 0:
for user in users_to_remove: for user in users_to_add:
remove_perm(permission, user, object) assign_perm(permission, user, object)
if len(users_to_add) > 0: if action == "change":
for user in users_to_add: # change gives view too
assign_perm(permission, user, object) assign_perm(
if action == "change": f"view_{object.__class__.__name__.lower()}",
# change gives view too user,
assign_perm( object,
f"view_{object.__class__.__name__.lower()}", )
user, if "groups" in permissions[action]:
object, # groups
) groups_to_add = Group.objects.filter(id__in=permissions[action]["groups"])
# groups groups_to_remove = (
groups_to_add = Group.objects.filter(id__in=permissions[action]["groups"]) get_groups_with_only_permission(
groups_to_remove = ( object,
get_groups_with_only_permission( permission,
object, )
permission, if not merge
else Group.objects.none()
) )
if not merge if len(groups_to_add) > 0 and len(groups_to_remove) > 0:
else Group.objects.none() groups_to_remove = groups_to_remove.exclude(id__in=groups_to_add)
) if len(groups_to_remove) > 0:
if len(groups_to_add) > 0 and len(groups_to_remove) > 0: for group in groups_to_remove:
groups_to_remove = groups_to_remove.exclude(id__in=groups_to_add) remove_perm(permission, group, object)
if len(groups_to_remove) > 0: if len(groups_to_add) > 0:
for group in groups_to_remove: for group in groups_to_add:
remove_perm(permission, group, object) assign_perm(permission, group, object)
if len(groups_to_add) > 0: if action == "change":
for group in groups_to_add: # change gives view too
assign_perm(permission, group, object) assign_perm(
if action == "change": f"view_{object.__class__.__name__.lower()}",
# change gives view too group,
assign_perm( object,
f"view_{object.__class__.__name__.lower()}", )
group,
object,
)
def get_objects_for_user_owner_aware(user, perms, Model) -> QuerySet: def get_objects_for_user_owner_aware(user, perms, Model) -> QuerySet:

View File

@ -160,24 +160,24 @@ class SetPermissionsMixin:
def validate_set_permissions(self, set_permissions=None): def validate_set_permissions(self, set_permissions=None):
permissions_dict = { permissions_dict = {
"view": { "view": {},
"users": User.objects.none(), "change": {},
"groups": Group.objects.none(),
},
"change": {
"users": User.objects.none(),
"groups": Group.objects.none(),
},
} }
if set_permissions is not None: if set_permissions is not None:
for action, _ in permissions_dict.items(): for action in ["view", "change"]:
if action in set_permissions: if action in set_permissions:
users = set_permissions[action]["users"] if "users" in set_permissions[action]:
permissions_dict[action]["users"] = self._validate_user_ids(users) users = set_permissions[action]["users"]
groups = set_permissions[action]["groups"] permissions_dict[action]["users"] = self._validate_user_ids(
permissions_dict[action]["groups"] = self._validate_group_ids( users,
groups, )
) if "groups" in set_permissions[action]:
groups = set_permissions[action]["groups"]
permissions_dict[action]["groups"] = self._validate_group_ids(
groups,
)
else:
del permissions_dict[action]
return permissions_dict return permissions_dict
def _set_permissions(self, permissions, object): def _set_permissions(self, permissions, object):

View File

@ -1162,7 +1162,7 @@ def run_workflows(
) as f: ) as f:
files = { files = {
"file": ( "file": (
document.original_filename, filename,
f.read(), f.read(),
document.mime_type, document.mime_type,
), ),

View File

@ -395,6 +395,52 @@ class TestApiAuth(DirectoriesMixin, APITestCase):
self.assertTrue(checker.has_perm("view_document", doc)) self.assertTrue(checker.has_perm("view_document", doc))
self.assertIn("view_document", get_perms(group1, doc)) self.assertIn("view_document", get_perms(group1, doc))
def test_patch_doesnt_remove_permissions(self):
"""
GIVEN:
- existing document with permissions set
WHEN:
- PATCH API request to update doc that is not json
THEN:
- Object permissions are not removed
"""
doc = Document.objects.create(
title="test",
mime_type="application/pdf",
content="this is a document",
)
user1 = User.objects.create_superuser(username="user1")
user2 = User.objects.create(username="user2")
group1 = Group.objects.create(name="group1")
doc.owner = user1
doc.save()
assign_perm("view_document", user2, doc)
assign_perm("change_document", user2, doc)
assign_perm("view_document", group1, doc)
assign_perm("change_document", group1, doc)
self.client.force_authenticate(user1)
response = self.client.patch(
f"/api/documents/{doc.id}/",
{
"archive_serial_number": "123",
},
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
doc = Document.objects.get(pk=doc.id)
self.assertEqual(doc.owner, user1)
from guardian.core import ObjectPermissionChecker
checker = ObjectPermissionChecker(user2)
self.assertTrue(checker.has_perm("view_document", doc))
self.assertIn("view_document", get_perms(group1, doc))
self.assertTrue(checker.has_perm("change_document", doc))
self.assertIn("change_document", get_perms(group1, doc))
def test_dynamic_permissions_fields(self): def test_dynamic_permissions_fields(self):
user1 = User.objects.create_user(username="user1") user1 = User.objects.create_user(username="user1")
user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) user1.user_permissions.add(*Permission.objects.filter(codename="view_document"))

View File

@ -1,6 +1,5 @@
import filecmp import filecmp
import hashlib import hashlib
import os
import shutil import shutil
import tempfile import tempfile
from io import StringIO from io import StringIO
@ -19,7 +18,7 @@ from documents.tasks import update_document_content_maybe_archive_file
from documents.tests.utils import DirectoriesMixin from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import FileSystemAssertsMixin from documents.tests.utils import FileSystemAssertsMixin
sample_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf") sample_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
@override_settings(FILENAME_FORMAT="{correspondent}/{title}") @override_settings(FILENAME_FORMAT="{correspondent}/{title}")
@ -34,19 +33,13 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
def test_archiver(self): def test_archiver(self):
doc = self.make_models() doc = self.make_models()
shutil.copy( shutil.copy(sample_file, Path(self.dirs.originals_dir) / f"{doc.id:07}.pdf")
sample_file,
os.path.join(self.dirs.originals_dir, f"{doc.id:07}.pdf"),
)
call_command("document_archiver", "--processes", "1") call_command("document_archiver", "--processes", "1")
def test_handle_document(self): def test_handle_document(self):
doc = self.make_models() doc = self.make_models()
shutil.copy( shutil.copy(sample_file, Path(self.dirs.originals_dir) / f"{doc.id:07}.pdf")
sample_file,
os.path.join(self.dirs.originals_dir, f"{doc.id:07}.pdf"),
)
update_document_content_maybe_archive_file(doc.pk) update_document_content_maybe_archive_file(doc.pk)
@ -90,11 +83,8 @@ class TestArchiver(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
mime_type="application/pdf", mime_type="application/pdf",
filename="document_01.pdf", filename="document_01.pdf",
) )
shutil.copy(sample_file, os.path.join(self.dirs.originals_dir, "document.pdf")) shutil.copy(sample_file, Path(self.dirs.originals_dir) / "document.pdf")
shutil.copy( shutil.copy(sample_file, Path(self.dirs.originals_dir) / "document_01.pdf")
sample_file,
os.path.join(self.dirs.originals_dir, "document_01.pdf"),
)
update_document_content_maybe_archive_file(doc2.pk) update_document_content_maybe_archive_file(doc2.pk)
update_document_content_maybe_archive_file(doc1.pk) update_document_content_maybe_archive_file(doc1.pk)
@ -136,22 +126,22 @@ class TestDecryptDocuments(FileSystemAssertsMixin, TestCase):
) )
shutil.copy( shutil.copy(
os.path.join( (
os.path.dirname(__file__), Path(__file__).parent
"samples", / "samples"
"documents", / "documents"
"originals", / "originals"
"0000004.pdf.gpg", / "0000004.pdf.gpg"
), ),
originals_dir / "0000004.pdf.gpg", originals_dir / "0000004.pdf.gpg",
) )
shutil.copy( shutil.copy(
os.path.join( (
os.path.dirname(__file__), Path(__file__).parent
"samples", / "samples"
"documents", / "documents"
"thumbnails", / "thumbnails"
"0000004.webp.gpg", / "0000004.webp.gpg"
), ),
thumb_dir / f"{doc.id:07}.webp.gpg", thumb_dir / f"{doc.id:07}.webp.gpg",
) )
@ -162,13 +152,13 @@ class TestDecryptDocuments(FileSystemAssertsMixin, TestCase):
self.assertEqual(doc.storage_type, Document.STORAGE_TYPE_UNENCRYPTED) self.assertEqual(doc.storage_type, Document.STORAGE_TYPE_UNENCRYPTED)
self.assertEqual(doc.filename, "0000004.pdf") self.assertEqual(doc.filename, "0000004.pdf")
self.assertIsFile(os.path.join(originals_dir, "0000004.pdf")) self.assertIsFile(Path(originals_dir) / "0000004.pdf")
self.assertIsFile(doc.source_path) self.assertIsFile(doc.source_path)
self.assertIsFile(os.path.join(thumb_dir, f"{doc.id:07}.webp")) self.assertIsFile(Path(thumb_dir) / f"{doc.id:07}.webp")
self.assertIsFile(doc.thumbnail_path) self.assertIsFile(doc.thumbnail_path)
with doc.source_file as f: with doc.source_file as f:
checksum = hashlib.md5(f.read()).hexdigest() checksum: str = hashlib.md5(f.read()).hexdigest()
self.assertEqual(checksum, doc.checksum) self.assertEqual(checksum, doc.checksum)

View File

@ -1,5 +1,4 @@
import filecmp import filecmp
import os
import shutil import shutil
from pathlib import Path from pathlib import Path
from threading import Thread from threading import Thread
@ -94,13 +93,13 @@ class ConsumerThreadMixin(DocumentConsumeDelayMixin):
print("Consumed a perfectly valid file.") # noqa: T201 print("Consumed a perfectly valid file.") # noqa: T201
def slow_write_file(self, target, *, incomplete=False): def slow_write_file(self, target, *, incomplete=False):
with open(self.sample_file, "rb") as f: with Path(self.sample_file).open("rb") as f:
pdf_bytes = f.read() pdf_bytes = f.read()
if incomplete: if incomplete:
pdf_bytes = pdf_bytes[: len(pdf_bytes) - 100] pdf_bytes = pdf_bytes[: len(pdf_bytes) - 100]
with open(target, "wb") as f: with Path(target).open("wb") as f:
# this will take 2 seconds, since the file is about 20k. # this will take 2 seconds, since the file is about 20k.
print("Start writing file.") # noqa: T201 print("Start writing file.") # noqa: T201
for b in chunked(1000, pdf_bytes): for b in chunked(1000, pdf_bytes):
@ -116,7 +115,7 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
def test_consume_file(self): def test_consume_file(self):
self.t_start() self.t_start()
f = Path(os.path.join(self.dirs.consumption_dir, "my_file.pdf")) f = Path(self.dirs.consumption_dir) / "my_file.pdf"
shutil.copy(self.sample_file, f) shutil.copy(self.sample_file, f)
self.wait_for_task_mock_call() self.wait_for_task_mock_call()
@ -130,7 +129,7 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
def test_consume_file_invalid_ext(self): def test_consume_file_invalid_ext(self):
self.t_start() self.t_start()
f = os.path.join(self.dirs.consumption_dir, "my_file.wow") f = Path(self.dirs.consumption_dir) / "my_file.wow"
shutil.copy(self.sample_file, f) shutil.copy(self.sample_file, f)
self.wait_for_task_mock_call() self.wait_for_task_mock_call()
@ -138,7 +137,7 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
self.consume_file_mock.assert_not_called() self.consume_file_mock.assert_not_called()
def test_consume_existing_file(self): def test_consume_existing_file(self):
f = Path(os.path.join(self.dirs.consumption_dir, "my_file.pdf")) f = Path(self.dirs.consumption_dir) / "my_file.pdf"
shutil.copy(self.sample_file, f) shutil.copy(self.sample_file, f)
self.t_start() self.t_start()
@ -154,7 +153,7 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
self.t_start() self.t_start()
fname = Path(os.path.join(self.dirs.consumption_dir, "my_file.pdf")) fname = Path(self.dirs.consumption_dir) / "my_file.pdf"
self.slow_write_file(fname) self.slow_write_file(fname)
@ -174,8 +173,8 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
self.t_start() self.t_start()
fname = Path(os.path.join(self.dirs.consumption_dir, "my_file.~df")) fname = Path(self.dirs.consumption_dir) / "my_file.~df"
fname2 = Path(os.path.join(self.dirs.consumption_dir, "my_file.pdf")) fname2 = Path(self.dirs.consumption_dir) / "my_file.pdf"
self.slow_write_file(fname) self.slow_write_file(fname)
shutil.move(fname, fname2) shutil.move(fname, fname2)
@ -196,7 +195,7 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
self.t_start() self.t_start()
fname = Path(os.path.join(self.dirs.consumption_dir, "my_file.pdf")) fname = Path(self.dirs.consumption_dir) / "my_file.pdf"
self.slow_write_file(fname, incomplete=True) self.slow_write_file(fname, incomplete=True)
self.wait_for_task_mock_call() self.wait_for_task_mock_call()
@ -225,23 +224,23 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
shutil.copy( shutil.copy(
self.sample_file, self.sample_file,
os.path.join(self.dirs.consumption_dir, ".DS_STORE"), Path(self.dirs.consumption_dir) / ".DS_STORE",
) )
shutil.copy( shutil.copy(
self.sample_file, self.sample_file,
os.path.join(self.dirs.consumption_dir, "my_file.pdf"), Path(self.dirs.consumption_dir) / "my_file.pdf",
) )
shutil.copy( shutil.copy(
self.sample_file, self.sample_file,
os.path.join(self.dirs.consumption_dir, "._my_file.pdf"), Path(self.dirs.consumption_dir) / "._my_file.pdf",
) )
shutil.copy( shutil.copy(
self.sample_file, self.sample_file,
os.path.join(self.dirs.consumption_dir, "my_second_file.pdf"), Path(self.dirs.consumption_dir) / "my_second_file.pdf",
) )
shutil.copy( shutil.copy(
self.sample_file, self.sample_file,
os.path.join(self.dirs.consumption_dir, "._my_second_file.pdf"), Path(self.dirs.consumption_dir) / "._my_second_file.pdf",
) )
sleep(5) sleep(5)
@ -259,60 +258,66 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
def test_is_ignored(self): def test_is_ignored(self):
test_paths = [ test_paths = [
{ {
"path": os.path.join(self.dirs.consumption_dir, "foo.pdf"), "path": (Path(self.dirs.consumption_dir) / "foo.pdf").as_posix(),
"ignore": False, "ignore": False,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, "foo", "bar.pdf"), "path": (
Path(self.dirs.consumption_dir) / "foo" / "bar.pdf"
).as_posix(),
"ignore": False, "ignore": False,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, ".DS_STORE"), "path": (Path(self.dirs.consumption_dir) / ".DS_STORE").as_posix(),
"ignore": True, "ignore": True,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, ".DS_Store"), "path": (Path(self.dirs.consumption_dir) / ".DS_Store").as_posix(),
"ignore": True, "ignore": True,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, ".stfolder", "foo.pdf"), "path": (
Path(self.dirs.consumption_dir) / ".stfolder" / "foo.pdf"
).as_posix(),
"ignore": True, "ignore": True,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, ".stfolder.pdf"), "path": (Path(self.dirs.consumption_dir) / ".stfolder.pdf").as_posix(),
"ignore": False, "ignore": False,
}, },
{ {
"path": os.path.join( "path": (
self.dirs.consumption_dir, Path(self.dirs.consumption_dir) / ".stversions" / "foo.pdf"
".stversions", ).as_posix(),
"foo.pdf",
),
"ignore": True, "ignore": True,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, ".stversions.pdf"), "path": (
Path(self.dirs.consumption_dir) / ".stversions.pdf"
).as_posix(),
"ignore": False, "ignore": False,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, "._foo.pdf"), "path": (Path(self.dirs.consumption_dir) / "._foo.pdf").as_posix(),
"ignore": True, "ignore": True,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, "my_foo.pdf"), "path": (Path(self.dirs.consumption_dir) / "my_foo.pdf").as_posix(),
"ignore": False, "ignore": False,
}, },
{ {
"path": os.path.join(self.dirs.consumption_dir, "._foo", "bar.pdf"), "path": (
Path(self.dirs.consumption_dir) / "._foo" / "bar.pdf"
).as_posix(),
"ignore": True, "ignore": True,
}, },
{ {
"path": os.path.join( "path": (
self.dirs.consumption_dir, Path(self.dirs.consumption_dir)
"@eaDir", / "@eaDir"
"SYNO@.fileindexdb", / "SYNO@.fileindexdb"
"_1jk.fnm", / "_1jk.fnm"
), ).as_posix(),
"ignore": True, "ignore": True,
}, },
] ]
@ -332,7 +337,7 @@ class TestConsumer(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCase):
self.t_start() self.t_start()
f = os.path.join(self.dirs.consumption_dir, "my_file.pdf") f = Path(self.dirs.consumption_dir) / "my_file.pdf"
shutil.copy(self.sample_file, f) shutil.copy(self.sample_file, f)
self.wait_for_task_mock_call() self.wait_for_task_mock_call()
@ -380,9 +385,9 @@ class TestConsumerTags(DirectoriesMixin, ConsumerThreadMixin, TransactionTestCas
self.t_start() self.t_start()
path = os.path.join(self.dirs.consumption_dir, *tag_names) path = Path(self.dirs.consumption_dir) / "/".join(tag_names)
os.makedirs(path, exist_ok=True) path.mkdir(parents=True, exist_ok=True)
f = Path(os.path.join(path, "my_file.pdf")) f = path / "my_file.pdf"
# Wait at least inotify read_delay for recursive watchers # Wait at least inotify read_delay for recursive watchers
# to be created for the new directories # to be created for the new directories
sleep(1) sleep(1)

View File

@ -1,6 +1,5 @@
import hashlib import hashlib
import json import json
import os
import shutil import shutil
import tempfile import tempfile
from io import StringIO from io import StringIO
@ -183,16 +182,16 @@ class TestExportImport(
call_command(*args) call_command(*args)
with open(os.path.join(self.target, "manifest.json")) as f: with (self.target / "manifest.json").open() as f:
manifest = json.load(f) manifest = json.load(f)
return manifest return manifest
def test_exporter(self, *, use_filename_format=False): def test_exporter(self, *, use_filename_format=False):
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
num_permission_objects = Permission.objects.count() num_permission_objects = Permission.objects.count()
@ -210,7 +209,7 @@ class TestExportImport(
4, 4,
) )
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
self.assertEqual( self.assertEqual(
self._get_document_from_manifest(manifest, self.d1.id)["fields"]["title"], self._get_document_from_manifest(manifest, self.d1.id)["fields"]["title"],
@ -231,19 +230,17 @@ class TestExportImport(
for element in manifest: for element in manifest:
if element["model"] == "documents.document": if element["model"] == "documents.document":
fname = os.path.join( fname = (
self.target, self.target / element[document_exporter.EXPORTER_FILE_NAME]
element[document_exporter.EXPORTER_FILE_NAME], ).as_posix()
)
self.assertIsFile(fname) self.assertIsFile(fname)
self.assertIsFile( self.assertIsFile(
os.path.join( (
self.target, self.target / element[document_exporter.EXPORTER_THUMBNAIL_NAME]
element[document_exporter.EXPORTER_THUMBNAIL_NAME], ).as_posix(),
),
) )
with open(fname, "rb") as f: with Path(fname).open("rb") as f:
checksum = hashlib.md5(f.read()).hexdigest() checksum = hashlib.md5(f.read()).hexdigest()
self.assertEqual(checksum, element["fields"]["checksum"]) self.assertEqual(checksum, element["fields"]["checksum"])
@ -253,13 +250,12 @@ class TestExportImport(
) )
if document_exporter.EXPORTER_ARCHIVE_NAME in element: if document_exporter.EXPORTER_ARCHIVE_NAME in element:
fname = os.path.join( fname = (
self.target, self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME]
element[document_exporter.EXPORTER_ARCHIVE_NAME], ).as_posix()
)
self.assertIsFile(fname) self.assertIsFile(fname)
with open(fname, "rb") as f: with Path(fname).open("rb") as f:
checksum = hashlib.md5(f.read()).hexdigest() checksum = hashlib.md5(f.read()).hexdigest()
self.assertEqual(checksum, element["fields"]["archive_checksum"]) self.assertEqual(checksum, element["fields"]["archive_checksum"])
@ -297,10 +293,10 @@ class TestExportImport(
self.assertEqual(len(messages), 0) self.assertEqual(len(messages), 0)
def test_exporter_with_filename_format(self): def test_exporter_with_filename_format(self):
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
with override_settings( with override_settings(
@ -309,16 +305,16 @@ class TestExportImport(
self.test_exporter(use_filename_format=True) self.test_exporter(use_filename_format=True)
def test_update_export_changed_time(self): def test_update_export_changed_time(self):
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
self._do_export() self._do_export()
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
st_mtime_1 = os.stat(os.path.join(self.target, "manifest.json")).st_mtime st_mtime_1 = (self.target / "manifest.json").stat().st_mtime
with mock.patch( with mock.patch(
"documents.management.commands.document_exporter.copy_file_with_basic_stats", "documents.management.commands.document_exporter.copy_file_with_basic_stats",
@ -326,8 +322,8 @@ class TestExportImport(
self._do_export() self._do_export()
m.assert_not_called() m.assert_not_called()
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
st_mtime_2 = os.stat(os.path.join(self.target, "manifest.json")).st_mtime st_mtime_2 = (self.target / "manifest.json").stat().st_mtime
Path(self.d1.source_path).touch() Path(self.d1.source_path).touch()
@ -337,26 +333,26 @@ class TestExportImport(
self._do_export() self._do_export()
self.assertEqual(m.call_count, 1) self.assertEqual(m.call_count, 1)
st_mtime_3 = os.stat(os.path.join(self.target, "manifest.json")).st_mtime st_mtime_3 = (self.target / "manifest.json").stat().st_mtime
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
self.assertNotEqual(st_mtime_1, st_mtime_2) self.assertNotEqual(st_mtime_1, st_mtime_2)
self.assertNotEqual(st_mtime_2, st_mtime_3) self.assertNotEqual(st_mtime_2, st_mtime_3)
self._do_export(compare_json=True) self._do_export(compare_json=True)
st_mtime_4 = os.stat(os.path.join(self.target, "manifest.json")).st_mtime st_mtime_4 = (self.target / "manifest.json").stat().st_mtime
self.assertEqual(st_mtime_3, st_mtime_4) self.assertEqual(st_mtime_3, st_mtime_4)
def test_update_export_changed_checksum(self): def test_update_export_changed_checksum(self):
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
self._do_export() self._do_export()
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
with mock.patch( with mock.patch(
"documents.management.commands.document_exporter.copy_file_with_basic_stats", "documents.management.commands.document_exporter.copy_file_with_basic_stats",
@ -364,7 +360,7 @@ class TestExportImport(
self._do_export() self._do_export()
m.assert_not_called() m.assert_not_called()
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
self.d2.checksum = "asdfasdgf3" self.d2.checksum = "asdfasdgf3"
self.d2.save() self.d2.save()
@ -375,13 +371,13 @@ class TestExportImport(
self._do_export(compare_checksums=True) self._do_export(compare_checksums=True)
self.assertEqual(m.call_count, 1) self.assertEqual(m.call_count, 1)
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
def test_update_export_deleted_document(self): def test_update_export_deleted_document(self):
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
manifest = self._do_export() manifest = self._do_export()
@ -389,7 +385,7 @@ class TestExportImport(
self.assertTrue(len(manifest), 7) self.assertTrue(len(manifest), 7)
doc_from_manifest = self._get_document_from_manifest(manifest, self.d3.id) doc_from_manifest = self._get_document_from_manifest(manifest, self.d3.id)
self.assertIsFile( self.assertIsFile(
os.path.join(self.target, doc_from_manifest[EXPORTER_FILE_NAME]), (self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
) )
self.d3.delete() self.d3.delete()
@ -401,39 +397,39 @@ class TestExportImport(
self.d3.id, self.d3.id,
) )
self.assertIsFile( self.assertIsFile(
os.path.join(self.target, doc_from_manifest[EXPORTER_FILE_NAME]), (self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
) )
manifest = self._do_export(delete=True) manifest = self._do_export(delete=True)
self.assertIsNotFile( self.assertIsNotFile(
os.path.join(self.target, doc_from_manifest[EXPORTER_FILE_NAME]), (self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
) )
self.assertTrue(len(manifest), 6) self.assertTrue(len(manifest), 6)
@override_settings(FILENAME_FORMAT="{title}/{correspondent}") @override_settings(FILENAME_FORMAT="{title}/{correspondent}")
def test_update_export_changed_location(self): def test_update_export_changed_location(self):
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
self._do_export(use_filename_format=True) self._do_export(use_filename_format=True)
self.assertIsFile(os.path.join(self.target, "wow1", "c.pdf")) self.assertIsFile((self.target / "wow1" / "c.pdf").as_posix())
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
self.d1.title = "new_title" self.d1.title = "new_title"
self.d1.save() self.d1.save()
self._do_export(use_filename_format=True, delete=True) self._do_export(use_filename_format=True, delete=True)
self.assertIsNotFile(os.path.join(self.target, "wow1", "c.pdf")) self.assertIsNotFile((self.target / "wow1" / "c.pdf").as_posix())
self.assertIsNotDir(os.path.join(self.target, "wow1")) self.assertIsNotDir((self.target / "wow1").as_posix())
self.assertIsFile(os.path.join(self.target, "new_title", "c.pdf")) self.assertIsFile((self.target / "new_title" / "c.pdf").as_posix())
self.assertIsFile(os.path.join(self.target, "manifest.json")) self.assertIsFile((self.target / "manifest.json").as_posix())
self.assertIsFile(os.path.join(self.target, "wow2", "none.pdf")) self.assertIsFile((self.target / "wow2" / "none.pdf").as_posix())
self.assertIsFile( self.assertIsFile(
os.path.join(self.target, "wow2", "none_01.pdf"), (self.target / "wow2" / "none_01.pdf").as_posix(),
) )
def test_export_missing_files(self): def test_export_missing_files(self):
@ -458,20 +454,19 @@ class TestExportImport(
- Zipfile is created - Zipfile is created
- Zipfile contains exported files - Zipfile contains exported files
""" """
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
args = ["document_exporter", self.target, "--zip"] args = ["document_exporter", self.target, "--zip"]
call_command(*args) call_command(*args)
expected_file = os.path.join( expected_file = (
self.target, self.target / f"export-{timezone.localdate().isoformat()}.zip"
f"export-{timezone.localdate().isoformat()}.zip", ).as_posix()
)
self.assertIsFile(expected_file) self.assertIsFile(expected_file)
@ -492,10 +487,10 @@ class TestExportImport(
- Zipfile is created - Zipfile is created
- Zipfile contains exported files - Zipfile contains exported files
""" """
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
args = ["document_exporter", self.target, "--zip", "--use-filename-format"] args = ["document_exporter", self.target, "--zip", "--use-filename-format"]
@ -505,10 +500,9 @@ class TestExportImport(
): ):
call_command(*args) call_command(*args)
expected_file = os.path.join( expected_file = (
self.target, self.target / f"export-{timezone.localdate().isoformat()}.zip"
f"export-{timezone.localdate().isoformat()}.zip", ).as_posix()
)
self.assertIsFile(expected_file) self.assertIsFile(expected_file)
@ -533,10 +527,10 @@ class TestExportImport(
- Zipfile contains exported files - Zipfile contains exported files
- The existing file and directory in target are removed - The existing file and directory in target are removed
""" """
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
# Create stuff in target directory # Create stuff in target directory
@ -552,10 +546,9 @@ class TestExportImport(
call_command(*args) call_command(*args)
expected_file = os.path.join( expected_file = (
self.target, self.target / f"export-{timezone.localdate().isoformat()}.zip"
f"export-{timezone.localdate().isoformat()}.zip", ).as_posix()
)
self.assertIsFile(expected_file) self.assertIsFile(expected_file)
self.assertIsNotFile(existing_file) self.assertIsNotFile(existing_file)
@ -610,7 +603,7 @@ class TestExportImport(
- Error is raised - Error is raised
""" """
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
os.chmod(tmp_dir, 0o000) Path(tmp_dir).chmod(0o000)
args = ["document_exporter", tmp_dir] args = ["document_exporter", tmp_dir]
@ -629,10 +622,10 @@ class TestExportImport(
- Manifest.json doesn't contain information about archive files - Manifest.json doesn't contain information about archive files
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
manifest = self._do_export() manifest = self._do_export()
@ -670,10 +663,10 @@ class TestExportImport(
- Manifest.json doesn't contain information about thumbnails - Manifest.json doesn't contain information about thumbnails
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
manifest = self._do_export() manifest = self._do_export()
@ -713,10 +706,10 @@ class TestExportImport(
- Main manifest.json file doesn't contain information about documents - Main manifest.json file doesn't contain information about documents
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
manifest = self._do_export(split_manifest=True) manifest = self._do_export(split_manifest=True)
@ -744,10 +737,10 @@ class TestExportImport(
THEN: THEN:
- Documents can be imported again - Documents can be imported again
""" """
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
self._do_export(use_folder_prefix=True) self._do_export(use_folder_prefix=True)
@ -769,10 +762,10 @@ class TestExportImport(
- ContentType & Permission objects are not deleted, db transaction rolled back - ContentType & Permission objects are not deleted, db transaction rolled back
""" """
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
num_content_type_objects = ContentType.objects.count() num_content_type_objects = ContentType.objects.count()
@ -804,10 +797,10 @@ class TestExportImport(
self.assertEqual(Permission.objects.count(), num_permission_objects + 1) self.assertEqual(Permission.objects.count(), num_permission_objects + 1)
def test_exporter_with_auditlog_disabled(self): def test_exporter_with_auditlog_disabled(self):
shutil.rmtree(os.path.join(self.dirs.media_dir, "documents")) shutil.rmtree(Path(self.dirs.media_dir) / "documents")
shutil.copytree( shutil.copytree(
os.path.join(os.path.dirname(__file__), "samples", "documents"), Path(__file__).parent / "samples" / "documents",
os.path.join(self.dirs.media_dir, "documents"), Path(self.dirs.media_dir) / "documents",
) )
with override_settings( with override_settings(

View File

@ -1,4 +1,3 @@
import os
import shutil import shutil
from pathlib import Path from pathlib import Path
@ -8,11 +7,11 @@ from documents.tests.utils import DirectoriesMixin
from documents.tests.utils import TestMigrations from documents.tests.utils import TestMigrations
def source_path_before(self): def source_path_before(self) -> Path:
if self.filename: if self.filename:
fname = str(self.filename) fname = str(self.filename)
return os.path.join(settings.ORIGINALS_DIR, fname) return Path(settings.ORIGINALS_DIR) / fname
class TestMigrateDocumentPageCount(DirectoriesMixin, TestMigrations): class TestMigrateDocumentPageCount(DirectoriesMixin, TestMigrations):

View File

@ -1,5 +1,5 @@
import os
import shutil import shutil
from pathlib import Path
from django.conf import settings from django.conf import settings
from django.test import override_settings from django.test import override_settings
@ -20,7 +20,7 @@ def source_path_before(self):
if self.storage_type == STORAGE_TYPE_GPG: if self.storage_type == STORAGE_TYPE_GPG:
fname += ".gpg" fname += ".gpg"
return os.path.join(settings.ORIGINALS_DIR, fname) return (Path(settings.ORIGINALS_DIR) / fname).as_posix()
def file_type_after(self): def file_type_after(self):
@ -35,7 +35,7 @@ def source_path_after(doc):
if doc.storage_type == STORAGE_TYPE_GPG: if doc.storage_type == STORAGE_TYPE_GPG:
fname += ".gpg" # pragma: no cover fname += ".gpg" # pragma: no cover
return os.path.join(settings.ORIGINALS_DIR, fname) return (Path(settings.ORIGINALS_DIR) / fname).as_posix()
@override_settings(PASSPHRASE="test") @override_settings(PASSPHRASE="test")
@ -52,7 +52,7 @@ class TestMigrateMimeType(DirectoriesMixin, TestMigrations):
) )
self.doc_id = doc.id self.doc_id = doc.id
shutil.copy( shutil.copy(
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), Path(__file__).parent / "samples" / "simple.pdf",
source_path_before(doc), source_path_before(doc),
) )
@ -63,12 +63,12 @@ class TestMigrateMimeType(DirectoriesMixin, TestMigrations):
) )
self.doc2_id = doc2.id self.doc2_id = doc2.id
shutil.copy( shutil.copy(
os.path.join( (
os.path.dirname(__file__), Path(__file__).parent
"samples", / "samples"
"documents", / "documents"
"originals", / "originals"
"0000004.pdf.gpg", / "0000004.pdf.gpg"
), ),
source_path_before(doc2), source_path_before(doc2),
) )
@ -97,7 +97,7 @@ class TestMigrateMimeTypeBackwards(DirectoriesMixin, TestMigrations):
) )
self.doc_id = doc.id self.doc_id = doc.id
shutil.copy( shutil.copy(
os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), Path(__file__).parent / "samples" / "simple.pdf",
source_path_after(doc), source_path_after(doc),
) )

View File

@ -1,5 +1,4 @@
import logging import logging
import os
import shutil import shutil
from pathlib import Path from pathlib import Path
@ -17,34 +16,34 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
with filelock.FileLock(settings.MEDIA_LOCK): with filelock.FileLock(settings.MEDIA_LOCK):
# just make sure that the lockfile is present. # just make sure that the lockfile is present.
shutil.copy( shutil.copy(
os.path.join( (
os.path.dirname(__file__), Path(__file__).parent
"samples", / "samples"
"documents", / "documents"
"originals", / "originals"
"0000001.pdf", / "0000001.pdf"
), ),
os.path.join(self.dirs.originals_dir, "0000001.pdf"), Path(self.dirs.originals_dir) / "0000001.pdf",
) )
shutil.copy( shutil.copy(
os.path.join( (
os.path.dirname(__file__), Path(__file__).parent
"samples", / "samples"
"documents", / "documents"
"archive", / "archive"
"0000001.pdf", / "0000001.pdf"
), ),
os.path.join(self.dirs.archive_dir, "0000001.pdf"), Path(self.dirs.archive_dir) / "0000001.pdf",
) )
shutil.copy( shutil.copy(
os.path.join( (
os.path.dirname(__file__), Path(__file__).parent
"samples", / "samples"
"documents", / "documents"
"thumbnails", / "thumbnails"
"0000001.webp", / "0000001.webp"
), ),
os.path.join(self.dirs.thumbnail_dir, "0000001.webp"), Path(self.dirs.thumbnail_dir) / "0000001.webp",
) )
return Document.objects.create( return Document.objects.create(
@ -92,25 +91,25 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
def test_no_thumbnail(self): def test_no_thumbnail(self):
doc = self.make_test_data() doc = self.make_test_data()
os.remove(doc.thumbnail_path) Path(doc.thumbnail_path).unlink()
self.assertSanityError(doc, "Thumbnail of document does not exist") self.assertSanityError(doc, "Thumbnail of document does not exist")
def test_thumbnail_no_access(self): def test_thumbnail_no_access(self):
doc = self.make_test_data() doc = self.make_test_data()
os.chmod(doc.thumbnail_path, 0o000) Path(doc.thumbnail_path).chmod(0o000)
self.assertSanityError(doc, "Cannot read thumbnail file of document") self.assertSanityError(doc, "Cannot read thumbnail file of document")
os.chmod(doc.thumbnail_path, 0o777) Path(doc.thumbnail_path).chmod(0o777)
def test_no_original(self): def test_no_original(self):
doc = self.make_test_data() doc = self.make_test_data()
os.remove(doc.source_path) Path(doc.source_path).unlink()
self.assertSanityError(doc, "Original of document does not exist.") self.assertSanityError(doc, "Original of document does not exist.")
def test_original_no_access(self): def test_original_no_access(self):
doc = self.make_test_data() doc = self.make_test_data()
os.chmod(doc.source_path, 0o000) Path(doc.source_path).chmod(0o000)
self.assertSanityError(doc, "Cannot read original file of document") self.assertSanityError(doc, "Cannot read original file of document")
os.chmod(doc.source_path, 0o777) Path(doc.source_path).chmod(0o777)
def test_original_checksum_mismatch(self): def test_original_checksum_mismatch(self):
doc = self.make_test_data() doc = self.make_test_data()
@ -120,14 +119,14 @@ class TestSanityCheck(DirectoriesMixin, TestCase):
def test_no_archive(self): def test_no_archive(self):
doc = self.make_test_data() doc = self.make_test_data()
os.remove(doc.archive_path) Path(doc.archive_path).unlink()
self.assertSanityError(doc, "Archived version of document does not exist.") self.assertSanityError(doc, "Archived version of document does not exist.")
def test_archive_no_access(self): def test_archive_no_access(self):
doc = self.make_test_data() doc = self.make_test_data()
os.chmod(doc.archive_path, 0o000) Path(doc.archive_path).chmod(0o000)
self.assertSanityError(doc, "Cannot read archive file of document") self.assertSanityError(doc, "Cannot read archive file of document")
os.chmod(doc.archive_path, 0o777) Path(doc.archive_path).chmod(0o777)
def test_archive_checksum_mismatch(self): def test_archive_checksum_mismatch(self):
doc = self.make_test_data() doc = self.make_test_data()

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

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

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

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