mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-29 19:15:43 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c3e543415 |
No files matched your search
@@ -39,7 +39,6 @@ declare -a scope=(
|
|||||||
"${site_packages}/allauth"
|
"${site_packages}/allauth"
|
||||||
"${site_packages}/drf_spectacular"
|
"${site_packages}/drf_spectacular"
|
||||||
"${site_packages}/drf_spectacular_sidecar"
|
"${site_packages}/drf_spectacular_sidecar"
|
||||||
"${site_packages}/treenode"
|
|
||||||
"${site_packages}/compression_middleware"
|
"${site_packages}/compression_middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
-11
@@ -162,17 +162,6 @@ including `gt` (>), `gte` (>=), `lt` (<), `lte` (<=), and `range`.
|
|||||||
Lastly, document link fields support a `contains` operator that behaves
|
Lastly, document link fields support a `contains` operator that behaves
|
||||||
like a "is superset of" check.
|
like a "is superset of" check.
|
||||||
|
|
||||||
Date fields also accept relative expressions supported by
|
|
||||||
[dateparser](https://dateparser.readthedocs.io/en/stable/), such as `today`,
|
|
||||||
`in 30 days`, or `1 month ago`. For example, dates within the next 30 days:
|
|
||||||
|
|
||||||
```json
|
|
||||||
["expiry", "range", ["today", "in 30 days"]]
|
|
||||||
```
|
|
||||||
|
|
||||||
Expressions resolve each time the query runs, using `PAPERLESS_TIME_ZONE` and
|
|
||||||
`PAPERLESS_DATE_PARSER_LANGUAGES` (automatic language detection when unset).
|
|
||||||
|
|
||||||
### `/api/search/autocomplete/`
|
### `/api/search/autocomplete/`
|
||||||
|
|
||||||
Get auto completions for a partial search term.
|
Get auto completions for a partial search term.
|
||||||
|
|||||||
@@ -726,7 +726,6 @@ applied. You can use the following placeholders in the template with any trigger
|
|||||||
- `{{added_time}}`: added time in HH:MM format
|
- `{{added_time}}`: added time in HH:MM format
|
||||||
- `{{original_filename}}`: original file name without extension
|
- `{{original_filename}}`: original file name without extension
|
||||||
- `{{filename}}`: current file name without extension (for "added" workflows this may not be final yet, you can use `{{original_filename}}`)
|
- `{{filename}}`: current file name without extension (for "added" workflows this may not be final yet, you can use `{{original_filename}}`)
|
||||||
- `{{custom_fields}}`: Custom field names mapped to their type and value, as in [file name templates](advanced_usage.md#additional-variables). For example, `{{ custom_fields.Invoice.value }}` or `{{ custom_fields | get_cf_value("Invoice", "") }}` for a fallback when the field is missing. In title assignments, fields assigned by the same action are available.
|
|
||||||
- `{{doc_title}}`: current document title (cannot be used in title assignment)
|
- `{{doc_title}}`: current document title (cannot be used in title assignment)
|
||||||
|
|
||||||
The following placeholders are only available for "added" or "updated" triggers
|
The following placeholders are only available for "added" or "updated" triggers
|
||||||
|
|||||||
+2
-3
@@ -38,7 +38,6 @@ dependencies = [
|
|||||||
"django-multiselectfield~=1.0.1",
|
"django-multiselectfield~=1.0.1",
|
||||||
"django-rich~=2.2.0",
|
"django-rich~=2.2.0",
|
||||||
"django-soft-delete~=1.0.18",
|
"django-soft-delete~=1.0.18",
|
||||||
"django-treenode>=0.24",
|
|
||||||
"djangorestframework~=3.16",
|
"djangorestframework~=3.16",
|
||||||
"drf-spectacular~=0.30",
|
"drf-spectacular~=0.30",
|
||||||
"drf-spectacular-sidecar>=2026.7.1,<2026.10",
|
"drf-spectacular-sidecar>=2026.7.1,<2026.10",
|
||||||
@@ -256,8 +255,8 @@ isort.force-single-line = true
|
|||||||
[tool.codespell]
|
[tool.codespell]
|
||||||
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish,NIN,nin,reprot"
|
ignore-words-list = "criterias,afterall,valeu,ureue,equest,ure,assertIn,Oktober,commitish,NIN,nin,reprot"
|
||||||
skip = """\
|
skip = """\
|
||||||
src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless/tests/samples/mail/*,src/documents/tests/samples\
|
src-ui/src/locale/*,src-ui/pnpm-lock.yaml,src-ui/e2e/*,src/paperless_mail/tests/samples/*,src/paperless/tests/samples\
|
||||||
/*,src/paperless_testing/sample_files/*,*.po,*.json\
|
/mail/*,src/documents/tests/samples/*,*.po,*.json\
|
||||||
"""
|
"""
|
||||||
write-changes = true
|
write-changes = true
|
||||||
|
|
||||||
|
|||||||
@@ -134,9 +134,7 @@ def seed_database() -> None:
|
|||||||
originals.mkdir(parents=True)
|
originals.mkdir(parents=True)
|
||||||
thumbnails = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "thumbnails"
|
thumbnails = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "thumbnails"
|
||||||
thumbnails.mkdir(parents=True)
|
thumbnails.mkdir(parents=True)
|
||||||
sample_pdf = (
|
sample_pdf = SOURCE_ROOT / "documents" / "tests" / "samples" / "simple.pdf"
|
||||||
SOURCE_ROOT / "paperless_testing" / "sample_files" / "simple-digital.pdf"
|
|
||||||
)
|
|
||||||
for document in documents:
|
for document in documents:
|
||||||
shutil.copyfile(sample_pdf, originals / document.filename)
|
shutil.copyfile(sample_pdf, originals / document.filename)
|
||||||
shutil.copyfile(
|
shutil.copyfile(
|
||||||
|
|||||||
+18
-142
@@ -2048,7 +2048,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">201,202</context>
|
<context context-type="linenumber">167,168</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
|
||||||
@@ -4414,68 +4414,11 @@
|
|||||||
<context context-type="linenumber">96</context>
|
<context context-type="linenumber">96</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3625677668275605418" datatype="html">
|
|
||||||
<source>Date mode</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">38</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="7572800523712857519" datatype="html">
|
|
||||||
<source>Fixed date</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">39</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">40</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="4108478337308251505" datatype="html">
|
|
||||||
<source>Fixed</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">41,42</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8283919074858117329" datatype="html">
|
|
||||||
<source>Relative date</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">43</context>
|
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">44</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2731996625798485199" datatype="html">
|
|
||||||
<source>Relative</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">45,46</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6533918067030990396" datatype="html">
|
|
||||||
<source>Amount</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">51</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="8932917860978648376" datatype="html">
|
|
||||||
<source>Unit</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">56,57</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="9138811584797805840" datatype="html">
|
<trans-unit id="9138811584797805840" datatype="html">
|
||||||
<source>Open date picker</source>
|
<source>Open date picker</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">72</context>
|
<context context-type="linenumber">42</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/input/date/date.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/input/date/date.component.html</context>
|
||||||
@@ -4486,11 +4429,7 @@
|
|||||||
<source>Today</source>
|
<source>Today</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">77,78</context>
|
<context context-type="linenumber">47,48</context>
|
||||||
</context-group>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">329</context>
|
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context>
|
||||||
@@ -4521,7 +4460,7 @@
|
|||||||
<source>Close</source>
|
<source>Close</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">78,79</context>
|
<context context-type="linenumber">48,49</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context>
|
||||||
@@ -4560,62 +4499,55 @@
|
|||||||
<source>True</source>
|
<source>True</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">88,89</context>
|
<context context-type="linenumber">55,56</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">141,142</context>
|
<context context-type="linenumber">107,108</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">147,148</context>
|
<context context-type="linenumber">113,114</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3800326155195149498" datatype="html">
|
<trans-unit id="3800326155195149498" datatype="html">
|
||||||
<source>False</source>
|
<source>False</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">89,90</context>
|
<context context-type="linenumber">56,57</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">142,143</context>
|
<context context-type="linenumber">108,109</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">148,149</context>
|
<context context-type="linenumber">114,115</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7551700625201096185" datatype="html">
|
<trans-unit id="7551700625201096185" datatype="html">
|
||||||
<source>Search docs...</source>
|
<source>Search docs...</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">106</context>
|
<context context-type="linenumber">73</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">164</context>
|
<context context-type="linenumber">130</context>
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="1179184907489210406" datatype="html">
|
|
||||||
<source>Operator</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
|
||||||
<context context-type="linenumber">119,120</context>
|
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2328549728463836983" datatype="html">
|
<trans-unit id="2328549728463836983" datatype="html">
|
||||||
<source>Remove query</source>
|
<source>Remove query</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">189</context>
|
<context context-type="linenumber">154</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3184700926171002527" datatype="html">
|
<trans-unit id="3184700926171002527" datatype="html">
|
||||||
<source>Any</source>
|
<source>Any</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">199,200</context>
|
<context context-type="linenumber">165,166</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/filterable-dropdown/filterable-dropdown.component.html</context>
|
||||||
@@ -4626,84 +4558,28 @@
|
|||||||
<source>Not</source>
|
<source>Not</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">204,205</context>
|
<context context-type="linenumber">170,171</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6548676277933116532" datatype="html">
|
<trans-unit id="6548676277933116532" datatype="html">
|
||||||
<source>Add query</source>
|
<source>Add query</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">223</context>
|
<context context-type="linenumber">189</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5599577087865387184" datatype="html">
|
<trans-unit id="5599577087865387184" datatype="html">
|
||||||
<source>Add expression</source>
|
<source>Add expression</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">226</context>
|
<context context-type="linenumber">192</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7394978038852129121" datatype="html">
|
<trans-unit id="7394978038852129121" datatype="html">
|
||||||
<source>Remove expression</source>
|
<source>Remove expression</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">230</context>
|
<context context-type="linenumber">196</context>
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5620397708418210833" datatype="html">
|
|
||||||
<source>days ago</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">330</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2871318661796659216" datatype="html">
|
|
||||||
<source>weeks ago</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">331</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6041340836190906216" datatype="html">
|
|
||||||
<source>months ago</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">332</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="963494111451627204" datatype="html">
|
|
||||||
<source>years ago</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">333</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="5555905617885007774" datatype="html">
|
|
||||||
<source>days from now</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">334</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="4298377021616940216" datatype="html">
|
|
||||||
<source>weeks from now</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">335</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="2772874767134148305" datatype="html">
|
|
||||||
<source>months from now</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">336</context>
|
|
||||||
</context-group>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="6363533917780225967" datatype="html">
|
|
||||||
<source>years from now</source>
|
|
||||||
<context-group purpose="location">
|
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts</context>
|
|
||||||
<context context-type="linenumber">337</context>
|
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6630533861307510614" datatype="html">
|
<trans-unit id="6630533861307510614" datatype="html">
|
||||||
|
|||||||
Generated
+35
-228
@@ -4,10 +4,6 @@ settings:
|
|||||||
autoInstallPeers: false
|
autoInstallPeers: false
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
overrides:
|
|
||||||
vite@>=8.3.0 <8.3.3: ^8.3.3
|
|
||||||
zod@<4.6.6: ^4.6.6
|
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
@@ -1654,9 +1650,6 @@ packages:
|
|||||||
'@oxc-project/types@0.150.0':
|
'@oxc-project/types@0.150.0':
|
||||||
resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
|
resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==}
|
||||||
|
|
||||||
'@oxc-project/types@0.155.0':
|
|
||||||
resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==}
|
|
||||||
|
|
||||||
'@parcel/watcher-android-arm64@2.6.0':
|
'@parcel/watcher-android-arm64@2.6.0':
|
||||||
resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==}
|
resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==}
|
||||||
engines: {node: '>= 10.0.0'}
|
engines: {node: '>= 10.0.0'}
|
||||||
@@ -1755,85 +1748,42 @@ packages:
|
|||||||
'@popperjs/core@2.11.8':
|
'@popperjs/core@2.11.8':
|
||||||
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm-eabi@1.2.15':
|
|
||||||
resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
'@rolldown/binding-android-arm-eabi@1.2.8':
|
'@rolldown/binding-android-arm-eabi@1.2.8':
|
||||||
resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==}
|
resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.2.15':
|
|
||||||
resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [android]
|
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.2.8':
|
'@rolldown/binding-android-arm64@1.2.8':
|
||||||
resolution: {integrity: sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==}
|
resolution: {integrity: sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.2.15':
|
|
||||||
resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.2.8':
|
'@rolldown/binding-darwin-arm64@1.2.8':
|
||||||
resolution: {integrity: sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==}
|
resolution: {integrity: sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.2.15':
|
|
||||||
resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [darwin]
|
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.2.8':
|
'@rolldown/binding-darwin-x64@1.2.8':
|
||||||
resolution: {integrity: sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==}
|
resolution: {integrity: sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.2.15':
|
|
||||||
resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [freebsd]
|
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.2.8':
|
'@rolldown/binding-freebsd-x64@1.2.8':
|
||||||
resolution: {integrity: sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==}
|
resolution: {integrity: sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.15':
|
|
||||||
resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [arm]
|
|
||||||
os: [linux]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.8':
|
'@rolldown/binding-linux-arm-gnueabihf@1.2.8':
|
||||||
resolution: {integrity: sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==}
|
resolution: {integrity: sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.2.15':
|
|
||||||
resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.2.8':
|
'@rolldown/binding-linux-arm64-gnu@1.2.8':
|
||||||
resolution: {integrity: sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==}
|
resolution: {integrity: sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1841,13 +1791,6 @@ packages:
|
|||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.2.15':
|
|
||||||
resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.2.8':
|
'@rolldown/binding-linux-arm64-musl@1.2.8':
|
||||||
resolution: {integrity: sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==}
|
resolution: {integrity: sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1855,13 +1798,6 @@ packages:
|
|||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.2.15':
|
|
||||||
resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [ppc64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.2.8':
|
'@rolldown/binding-linux-ppc64-gnu@1.2.8':
|
||||||
resolution: {integrity: sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==}
|
resolution: {integrity: sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1869,13 +1805,6 @@ packages:
|
|||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.2.15':
|
|
||||||
resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [s390x]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.2.8':
|
'@rolldown/binding-linux-s390x-gnu@1.2.8':
|
||||||
resolution: {integrity: sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==}
|
resolution: {integrity: sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1883,13 +1812,6 @@ packages:
|
|||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.2.15':
|
|
||||||
resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.2.8':
|
'@rolldown/binding-linux-x64-gnu@1.2.8':
|
||||||
resolution: {integrity: sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==}
|
resolution: {integrity: sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1897,13 +1819,6 @@ packages:
|
|||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.2.15':
|
|
||||||
resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [linux]
|
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.2.8':
|
'@rolldown/binding-linux-x64-musl@1.2.8':
|
||||||
resolution: {integrity: sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==}
|
resolution: {integrity: sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -1911,36 +1826,18 @@ packages:
|
|||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.2.15':
|
|
||||||
resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [openharmony]
|
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.2.8':
|
'@rolldown/binding-openharmony-arm64@1.2.8':
|
||||||
resolution: {integrity: sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==}
|
resolution: {integrity: sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [openharmony]
|
os: [openharmony]
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.2.15':
|
|
||||||
resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [arm64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.2.8':
|
'@rolldown/binding-win32-arm64-msvc@1.2.8':
|
||||||
resolution: {integrity: sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==}
|
resolution: {integrity: sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.2.15':
|
|
||||||
resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
cpu: [x64]
|
|
||||||
os: [win32]
|
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.2.8':
|
'@rolldown/binding-win32-x64-msvc@1.2.8':
|
||||||
resolution: {integrity: sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==}
|
resolution: {integrity: sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -2235,7 +2132,7 @@ packages:
|
|||||||
resolution: {integrity: sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==}
|
resolution: {integrity: sha512-bdyo8rB3NnQbikdMpHaML9Z1OZPBu6fFOBo+OtxsBlvMJtysWskmBcnbIDhUqgC8tcxNv/a+BcV5U+2nQMm1OQ==}
|
||||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^8.3.3
|
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||||
|
|
||||||
acorn-jsx@5.3.2:
|
acorn-jsx@5.3.2:
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
@@ -2364,8 +2261,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@popperjs/core': ^2.11.8
|
'@popperjs/core': ^2.11.8
|
||||||
|
|
||||||
brace-expansion@2.1.7:
|
brace-expansion@2.1.4:
|
||||||
resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==}
|
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
|
||||||
|
|
||||||
brace-expansion@5.0.12:
|
brace-expansion@5.0.12:
|
||||||
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
|
resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==}
|
||||||
@@ -2700,8 +2597,8 @@ packages:
|
|||||||
fast-string-width@3.0.2:
|
fast-string-width@3.0.2:
|
||||||
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
|
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
|
||||||
|
|
||||||
fast-uri@3.1.9:
|
fast-uri@3.1.8:
|
||||||
resolution: {integrity: sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==}
|
resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==}
|
||||||
|
|
||||||
fast-wrap-ansi@0.2.2:
|
fast-wrap-ansi@0.2.2:
|
||||||
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
|
resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==}
|
||||||
@@ -3132,8 +3029,8 @@ packages:
|
|||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
js-yaml@3.15.2:
|
js-yaml@3.15.1:
|
||||||
resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==}
|
resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
jsdom@26.1.0:
|
jsdom@26.1.0:
|
||||||
@@ -3383,11 +3280,6 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
nanoid@3.3.20:
|
|
||||||
resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
|
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
napi-postinstall@0.3.4:
|
napi-postinstall@0.3.4:
|
||||||
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
|
resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
@@ -3618,10 +3510,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
postcss@8.5.31:
|
|
||||||
resolution: {integrity: sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==}
|
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
|
||||||
|
|
||||||
prelude-ls@1.2.1:
|
prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -3713,11 +3601,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
rolldown@1.2.15:
|
|
||||||
resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==}
|
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
rolldown@1.2.8:
|
rolldown@1.2.8:
|
||||||
resolution: {integrity: sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==}
|
resolution: {integrity: sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -3890,8 +3773,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA==}
|
resolution: {integrity: sha512-aBY19bn/XA+hKOKX0qL7Z0UfdBsbQvc9hn993U8ALUjzxCvDcuZqLoRXjGJrUARWAlwMnsRVD9sw2AFNOYvalA==}
|
||||||
engines: {node: '>=22'}
|
engines: {node: '>=22'}
|
||||||
|
|
||||||
source-map-js@1.2.2:
|
source-map-js@1.2.1:
|
||||||
resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
|
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
source-map-support@0.5.21:
|
source-map-support@0.5.21:
|
||||||
@@ -4122,13 +4005,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==}
|
resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
|
|
||||||
vite@8.3.5:
|
vite@8.3.0:
|
||||||
resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==}
|
resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@types/node': ^20.19.0 || >=22.12.0
|
'@types/node': ^20.19.0 || >=22.12.0
|
||||||
'@vitejs/devtools': ^0.7.1 || ^0.8.0
|
'@vitejs/devtools': ^0.7.1
|
||||||
esbuild: ^0.27.0 || ^0.28.0
|
esbuild: ^0.27.0 || ^0.28.0
|
||||||
jiti: '>=1.21.0'
|
jiti: '>=1.21.0'
|
||||||
less: ^4.0.0
|
less: ^4.0.0
|
||||||
@@ -4285,8 +4168,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==}
|
resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
zod@4.6.7:
|
zod@4.6.5:
|
||||||
resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==}
|
resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
@@ -4486,7 +4369,7 @@ snapshots:
|
|||||||
'@babel/core': 8.0.5
|
'@babel/core': 8.0.5
|
||||||
'@inquirer/confirm': 6.3.2(@types/node@26.6.2)
|
'@inquirer/confirm': 6.3.2(@types/node@26.6.2)
|
||||||
'@parcel/watcher': 2.6.0
|
'@parcel/watcher': 2.6.0
|
||||||
'@vitejs/plugin-basic-ssl': 2.3.0(vite@8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1))
|
'@vitejs/plugin-basic-ssl': 2.3.0(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1))
|
||||||
beasties: 0.5.4
|
beasties: 0.5.4
|
||||||
browserslist: 4.29.1
|
browserslist: 4.29.1
|
||||||
chokidar: 5.0.0
|
chokidar: 5.0.0
|
||||||
@@ -4508,7 +4391,7 @@ snapshots:
|
|||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
typescript: 6.0.3
|
typescript: 6.0.3
|
||||||
vite: 8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)
|
vite: 8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)
|
||||||
xxhash-wasm: 1.1.0
|
xxhash-wasm: 1.1.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@angular/core': 22.2.2(@angular/compiler@22.2.2)(rxjs@7.8.2)
|
'@angular/core': 22.2.2(@angular/compiler@22.2.2)(rxjs@7.8.2)
|
||||||
@@ -4554,7 +4437,7 @@ snapshots:
|
|||||||
parse5-html-rewriting-stream: 8.0.1
|
parse5-html-rewriting-stream: 8.0.1
|
||||||
semver: 7.8.5
|
semver: 7.8.5
|
||||||
yargs: 18.1.0
|
yargs: 18.1.0
|
||||||
zod: 4.6.7
|
zod: 4.6.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- chokidar
|
- chokidar
|
||||||
@@ -4598,7 +4481,7 @@ snapshots:
|
|||||||
'@standard-schema/spec': 1.1.0
|
'@standard-schema/spec': 1.1.0
|
||||||
rxjs: 7.8.2
|
rxjs: 7.8.2
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
zod: 4.6.7
|
zod: 4.6.5
|
||||||
|
|
||||||
'@angular/localize@22.2.2(@angular/compiler-cli@22.2.2(@angular/compiler@22.2.2)(typescript@6.0.3))(@angular/compiler@22.2.2)':
|
'@angular/localize@22.2.2(@angular/compiler-cli@22.2.2(@angular/compiler@22.2.2)(typescript@6.0.3))(@angular/compiler@22.2.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5235,7 +5118,7 @@ snapshots:
|
|||||||
camelcase: 5.3.1
|
camelcase: 5.3.1
|
||||||
find-up: 4.1.0
|
find-up: 4.1.0
|
||||||
get-package-type: 0.1.0
|
get-package-type: 0.1.0
|
||||||
js-yaml: 3.15.2
|
js-yaml: 3.15.1
|
||||||
resolve-from: 5.0.0
|
resolve-from: 5.0.0
|
||||||
|
|
||||||
'@istanbuljs/schema@0.1.6': {}
|
'@istanbuljs/schema@0.1.6': {}
|
||||||
@@ -5529,12 +5412,12 @@ snapshots:
|
|||||||
|
|
||||||
'@modelcontextprotocol/core@2.3.1':
|
'@modelcontextprotocol/core@2.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 4.6.7
|
zod: 4.6.5
|
||||||
|
|
||||||
'@modelcontextprotocol/server@2.3.1':
|
'@modelcontextprotocol/server@2.3.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@modelcontextprotocol/core': 2.3.1
|
'@modelcontextprotocol/core': 2.3.1
|
||||||
zod: 4.6.7
|
zod: 4.6.5
|
||||||
|
|
||||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
|
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
|
||||||
optional: true
|
optional: true
|
||||||
@@ -5769,8 +5652,6 @@ snapshots:
|
|||||||
|
|
||||||
'@oxc-project/types@0.150.0': {}
|
'@oxc-project/types@0.150.0': {}
|
||||||
|
|
||||||
'@oxc-project/types@0.155.0': {}
|
|
||||||
|
|
||||||
'@parcel/watcher-android-arm64@2.6.0':
|
'@parcel/watcher-android-arm64@2.6.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -5838,93 +5719,48 @@ snapshots:
|
|||||||
|
|
||||||
'@popperjs/core@2.11.8': {}
|
'@popperjs/core@2.11.8': {}
|
||||||
|
|
||||||
'@rolldown/binding-android-arm-eabi@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-android-arm-eabi@1.2.8':
|
'@rolldown/binding-android-arm-eabi@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-android-arm64@1.2.8':
|
'@rolldown/binding-android-arm64@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-darwin-arm64@1.2.8':
|
'@rolldown/binding-darwin-arm64@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-darwin-x64@1.2.8':
|
'@rolldown/binding-darwin-x64@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-freebsd-x64@1.2.8':
|
'@rolldown/binding-freebsd-x64@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.8':
|
'@rolldown/binding-linux-arm-gnueabihf@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-gnu@1.2.8':
|
'@rolldown/binding-linux-arm64-gnu@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-arm64-musl@1.2.8':
|
'@rolldown/binding-linux-arm64-musl@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu@1.2.8':
|
'@rolldown/binding-linux-ppc64-gnu@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-s390x-gnu@1.2.8':
|
'@rolldown/binding-linux-s390x-gnu@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-gnu@1.2.8':
|
'@rolldown/binding-linux-x64-gnu@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-linux-x64-musl@1.2.8':
|
'@rolldown/binding-linux-x64-musl@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-openharmony-arm64@1.2.8':
|
'@rolldown/binding-openharmony-arm64@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-win32-arm64-msvc@1.2.8':
|
'@rolldown/binding-win32-arm64-msvc@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.2.15':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@rolldown/binding-win32-x64-msvc@1.2.8':
|
'@rolldown/binding-win32-x64-msvc@1.2.8':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -6215,9 +6051,9 @@ snapshots:
|
|||||||
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
'@unrs/resolver-binding-win32-x64-msvc@1.12.2':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@vitejs/plugin-basic-ssl@2.3.0(vite@8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1))':
|
'@vitejs/plugin-basic-ssl@2.3.0(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
vite: 8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)
|
vite: 8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1)
|
||||||
|
|
||||||
acorn-jsx@5.3.2(acorn@8.18.0):
|
acorn-jsx@5.3.2(acorn@8.18.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6247,7 +6083,7 @@ snapshots:
|
|||||||
ajv@8.20.0:
|
ajv@8.20.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
fast-uri: 3.1.9
|
fast-uri: 3.1.8
|
||||||
json-schema-traverse: 1.0.0
|
json-schema-traverse: 1.0.0
|
||||||
require-from-string: 2.0.2
|
require-from-string: 2.0.2
|
||||||
|
|
||||||
@@ -6362,7 +6198,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@popperjs/core': 2.11.8
|
'@popperjs/core': 2.11.8
|
||||||
|
|
||||||
brace-expansion@2.1.7:
|
brace-expansion@2.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match: 1.0.2
|
balanced-match: 1.0.2
|
||||||
|
|
||||||
@@ -6707,7 +6543,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
fast-string-truncated-width: 3.0.3
|
fast-string-truncated-width: 3.0.3
|
||||||
|
|
||||||
fast-uri@3.1.9: {}
|
fast-uri@3.1.8: {}
|
||||||
|
|
||||||
fast-wrap-ansi@0.2.2:
|
fast-wrap-ansi@0.2.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7357,7 +7193,7 @@ snapshots:
|
|||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@3.15.2:
|
js-yaml@3.15.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse: 1.0.10
|
argparse: 1.0.10
|
||||||
esprima: 4.0.1
|
esprima: 4.0.1
|
||||||
@@ -7557,7 +7393,7 @@ snapshots:
|
|||||||
|
|
||||||
minimatch@9.0.9:
|
minimatch@9.0.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 2.1.7
|
brace-expansion: 2.1.4
|
||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
@@ -7592,8 +7428,6 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.18: {}
|
nanoid@3.3.18: {}
|
||||||
|
|
||||||
nanoid@3.3.20: {}
|
|
||||||
|
|
||||||
napi-postinstall@0.3.4: {}
|
napi-postinstall@0.3.4: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
@@ -7840,13 +7674,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
nanoid: 3.3.18
|
nanoid: 3.3.18
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.2
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
postcss@8.5.31:
|
|
||||||
dependencies:
|
|
||||||
nanoid: 3.3.20
|
|
||||||
picocolors: 1.1.1
|
|
||||||
source-map-js: 1.2.2
|
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
@@ -7914,27 +7742,6 @@ snapshots:
|
|||||||
onetime: 7.0.0
|
onetime: 7.0.0
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
rolldown@1.2.15:
|
|
||||||
dependencies:
|
|
||||||
'@oxc-project/types': 0.155.0
|
|
||||||
'@rolldown/pluginutils': 1.0.1
|
|
||||||
optionalDependencies:
|
|
||||||
'@rolldown/binding-android-arm-eabi': 1.2.15
|
|
||||||
'@rolldown/binding-android-arm64': 1.2.15
|
|
||||||
'@rolldown/binding-darwin-arm64': 1.2.15
|
|
||||||
'@rolldown/binding-darwin-x64': 1.2.15
|
|
||||||
'@rolldown/binding-freebsd-x64': 1.2.15
|
|
||||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.15
|
|
||||||
'@rolldown/binding-linux-arm64-gnu': 1.2.15
|
|
||||||
'@rolldown/binding-linux-arm64-musl': 1.2.15
|
|
||||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.15
|
|
||||||
'@rolldown/binding-linux-s390x-gnu': 1.2.15
|
|
||||||
'@rolldown/binding-linux-x64-gnu': 1.2.15
|
|
||||||
'@rolldown/binding-linux-x64-musl': 1.2.15
|
|
||||||
'@rolldown/binding-openharmony-arm64': 1.2.15
|
|
||||||
'@rolldown/binding-win32-arm64-msvc': 1.2.15
|
|
||||||
'@rolldown/binding-win32-x64-msvc': 1.2.15
|
|
||||||
|
|
||||||
rolldown@1.2.8:
|
rolldown@1.2.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@oxc-project/types': 0.149.0
|
'@oxc-project/types': 0.149.0
|
||||||
@@ -8055,7 +7862,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 5.0.0
|
chokidar: 5.0.0
|
||||||
immutable: 5.1.9
|
immutable: 5.1.9
|
||||||
source-map-js: 1.2.2
|
source-map-js: 1.2.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@parcel/watcher': 2.6.0
|
'@parcel/watcher': 2.6.0
|
||||||
|
|
||||||
@@ -8084,7 +7891,7 @@ snapshots:
|
|||||||
ansi-styles: 6.2.3
|
ansi-styles: 6.2.3
|
||||||
is-fullwidth-code-point: 5.1.0
|
is-fullwidth-code-point: 5.1.0
|
||||||
|
|
||||||
source-map-js@1.2.2: {}
|
source-map-js@1.2.1: {}
|
||||||
|
|
||||||
source-map-support@0.5.21:
|
source-map-support@0.5.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8311,12 +8118,12 @@ snapshots:
|
|||||||
|
|
||||||
verkit@0.3.2: {}
|
verkit@0.3.2: {}
|
||||||
|
|
||||||
vite@8.3.5(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1):
|
vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(sass-embedded@1.104.1)(sass@1.104.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.33.0
|
lightningcss: 1.33.0
|
||||||
picomatch: 4.0.7
|
picomatch: 4.0.7
|
||||||
postcss: 8.5.31
|
postcss: 8.5.28
|
||||||
rolldown: 1.2.15
|
rolldown: 1.2.8
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.6.2
|
'@types/node': 26.6.2
|
||||||
@@ -8434,4 +8241,4 @@ snapshots:
|
|||||||
|
|
||||||
yoctocolors@2.2.0: {}
|
yoctocolors@2.2.0: {}
|
||||||
|
|
||||||
zod@4.6.7: {}
|
zod@4.6.5: {}
|
||||||
@@ -24,6 +24,3 @@ onlyBuiltDependencies:
|
|||||||
ignoredBuiltDependencies:
|
ignoredBuiltDependencies:
|
||||||
- normalize-diacritics
|
- normalize-diacritics
|
||||||
- unrs-resolver
|
- unrs-resolver
|
||||||
overrides:
|
|
||||||
"vite@>=8.3.0 <8.3.3": "^8.3.3"
|
|
||||||
"zod@<4.6.6": "^4.6.6"
|
|
||||||
+18
-52
@@ -33,54 +33,21 @@
|
|||||||
|
|
||||||
<ng-template #comparisonValueTemplate let-atom="atom">
|
<ng-template #comparisonValueTemplate let-atom="atom">
|
||||||
@if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Date) {
|
@if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Date) {
|
||||||
@let relativeDate = getRelativeDate(atom);
|
<input class="form-control" placeholder="yyyy-mm-dd"
|
||||||
<div class="d-flex gap-2 w-100 ms-0 mt-1">
|
[(ngModel)]="atom.value"
|
||||||
<fieldset class="btn-group btn-group-sm flex-shrink-0 date-mode" aria-label="Date mode" i18n-aria-label>
|
[ngModelOptions]="{standalone: true}"
|
||||||
<input type="radio" class="btn-check" [id]="atom.id + '_fixed'" [name]="atom.id + '_dateMode'" [checked]="!relativeDate" (change)="toggleRelativeDate(atom)" [disabled]="disabled" aria-label="Fixed date" i18n-aria-label>
|
ngbDatepicker
|
||||||
<label class="btn btn-outline-secondary text-nowrap" [for]="atom.id + '_fixed'" title="Fixed date" i18n-title>
|
#d="ngbDatepicker"
|
||||||
<i-bs name="calendar"></i-bs><span class="d-none d-md-inline ms-2" i18n>Fixed</span>
|
[footerTemplate]="datePickerFooterTemplate" />
|
||||||
</label>
|
<button class="btn btn-sm btn-outline-secondary rounded-end" (click)="d.toggle()" type="button" aria-label="Open date picker" i18n-aria-label>
|
||||||
<input type="radio" class="btn-check" [id]="atom.id + '_relative'" [name]="atom.id + '_dateMode'" [checked]="!!relativeDate" (change)="toggleRelativeDate(atom)" [disabled]="disabled" aria-label="Relative date" i18n-aria-label>
|
<i-bs name="calendar-event"></i-bs>
|
||||||
<label class="btn btn-outline-secondary text-nowrap" [for]="atom.id + '_relative'" title="Relative date" i18n-title>
|
</button>
|
||||||
<i-bs name="clock-history"></i-bs><span class="d-none d-md-inline ms-2" i18n>Relative</span>
|
<ng-template #datePickerFooterTemplate>
|
||||||
</label>
|
<div class="btn-group-xs border-top p-2 d-flex">
|
||||||
</fieldset>
|
<button type="button" class="btn btn-primary" (click)="atom.value = today; d.close()" i18n>Today</button>
|
||||||
<div class="input-group input-group-sm flex-grow-1">
|
<button type="button" class="btn btn-secondary ms-auto" (click)="d.close()" i18n>Close</button>
|
||||||
@if (relativeDate) {
|
|
||||||
@if (relativeDate.unit.includes('{n}')) {
|
|
||||||
<input class="form-control w-25 flex-grow-0" type="number" min="0" aria-label="Amount" i18n-aria-label
|
|
||||||
[ngModel]="relativeDate.amount"
|
|
||||||
(ngModelChange)="updateRelativeDate(atom, { amount: $event })"
|
|
||||||
[ngModelOptions]="{standalone: true}" />
|
|
||||||
}
|
|
||||||
<label class="visually-hidden" [for]="atom.id + '_unit'" i18n>Unit</label>
|
|
||||||
<select class="form-select" [id]="atom.id + '_unit'"
|
|
||||||
[ngModel]="relativeDate.unit"
|
|
||||||
(ngModelChange)="updateRelativeDate(atom, { unit: $event })"
|
|
||||||
[ngModelOptions]="{standalone: true}">
|
|
||||||
@for (unit of relativeDateUnits; track unit.id) {
|
|
||||||
<option [ngValue]="unit.id">{{unit.label}}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
} @else {
|
|
||||||
<input class="form-control" placeholder="yyyy-mm-dd"
|
|
||||||
[(ngModel)]="atom.value"
|
|
||||||
[ngModelOptions]="{standalone: true}"
|
|
||||||
ngbDatepicker
|
|
||||||
#d="ngbDatepicker"
|
|
||||||
[footerTemplate]="datePickerFooterTemplate" />
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" (click)="d.toggle()" type="button" aria-label="Open date picker" i18n-aria-label>
|
|
||||||
<i-bs name="calendar-event"></i-bs>
|
|
||||||
</button>
|
|
||||||
<ng-template #datePickerFooterTemplate>
|
|
||||||
<div class="btn-group-xs border-top p-2 d-flex">
|
|
||||||
<button type="button" class="btn btn-primary" (click)="atom.value = today; d.close()" i18n>Today</button>
|
|
||||||
<button type="button" class="btn btn-secondary ms-auto" (click)="d.close()" i18n>Close</button>
|
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ng-template>
|
||||||
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Float || getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Integer) {
|
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Float || getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Integer) {
|
||||||
<input class="w-25 form-control rounded-end" type="number" [(ngModel)]="atom.value" [ngModelOptions]="{standalone: true}" [disabled]="disabled">
|
<input class="w-25 form-control rounded-end" type="number" [(ngModel)]="atom.value" [ngModelOptions]="{standalone: true}" [disabled]="disabled">
|
||||||
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Boolean) {
|
} @else if (getCustomFieldByID(atom.field)?.data_type === CustomFieldDataType.Boolean) {
|
||||||
@@ -116,7 +83,6 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #queryAtom let-atom="atom">
|
<ng-template #queryAtom let-atom="atom">
|
||||||
<label class="visually-hidden" [for]="atom.id + '_operator'" i18n>Operator</label>
|
|
||||||
<div class="input-group input-group-sm">
|
<div class="input-group input-group-sm">
|
||||||
<ng-select #fieldSelects
|
<ng-select #fieldSelects
|
||||||
class="paperless-input-select"
|
class="paperless-input-select"
|
||||||
@@ -130,7 +96,7 @@
|
|||||||
[appendTo]="selectAppendTo"
|
[appendTo]="selectAppendTo"
|
||||||
(mousedown)="$event.stopImmediatePropagation()"
|
(mousedown)="$event.stopImmediatePropagation()"
|
||||||
></ng-select>
|
></ng-select>
|
||||||
<select class="w-25 form-select" [id]="atom.id + '_operator'" [class.rounded-end]="hasDateValueRow(atom)" [(ngModel)]="atom.operator" [ngModelOptions]="{standalone: true}" [disabled]="disabled">
|
<select class="w-25 form-select" [(ngModel)]="atom.operator" [ngModelOptions]="{standalone: true}" [disabled]="disabled">
|
||||||
@for (operator of getOperatorsForField(atom.field); track operator.label) {
|
@for (operator of getOperatorsForField(atom.field); track operator.label) {
|
||||||
<option [ngValue]="operator.value">{{operator.label}}</option>
|
<option [ngValue]="operator.value">{{operator.label}}</option>
|
||||||
}
|
}
|
||||||
@@ -185,10 +151,10 @@
|
|||||||
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="atom.value" [ngModelOptions]="{standalone: true}" [disabled]="disabled">
|
<input class="w-25 form-control rounded-end" type="text" [(ngModel)]="atom.value" [ngModelOptions]="{standalone: true}" [disabled]="disabled">
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
<button class="btn btn-link btn-sm text-danger pe-0" type="button" (click)="removeElement(atom)" [disabled]="disabled" aria-label="Remove query" i18n-aria-label>
|
||||||
|
<i-bs name="x-circle"></i-bs>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-link btn-sm text-danger pe-0 align-self-center" type="button" (click)="removeElement(atom)" [disabled]="disabled" aria-label="Remove query" i18n-aria-label>
|
|
||||||
<i-bs name="x-circle"></i-bs>
|
|
||||||
</button>
|
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ng-template #queryExpression let-expression="expression">
|
<ng-template #queryExpression let-expression="expression">
|
||||||
|
|||||||
-15
@@ -5,11 +5,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:host {
|
|
||||||
--ng-select-border-radius: var(--bs-border-radius);
|
|
||||||
--ng-select-height: calc(1.5em + 0.5rem + calc(var(--bs-border-width) * 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
:host ::ng-deep .ng-select-container {
|
:host ::ng-deep .ng-select-container {
|
||||||
border-top-right-radius: 0 !important;
|
border-top-right-radius: 0 !important;
|
||||||
border-bottom-right-radius: 0 !important;
|
border-bottom-right-radius: 0 !important;
|
||||||
@@ -46,13 +41,3 @@
|
|||||||
min-width: 140px;
|
min-width: 140px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-mode .btn {
|
|
||||||
--bs-btn-color: var(--bs-secondary-color);
|
|
||||||
--bs-btn-hover-bg: transparent;
|
|
||||||
--bs-btn-hover-color: var(--bs-body-color);
|
|
||||||
--bs-btn-hover-border-color: var(--bs-border-color);
|
|
||||||
--bs-btn-active-bg: transparent;
|
|
||||||
--bs-btn-active-color: var(--bs-primary);
|
|
||||||
--bs-btn-active-border-color: var(--bs-border-color);
|
|
||||||
}
|
|
||||||
+1
-78
@@ -3,7 +3,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
|
|||||||
import { LOCALE_ID } from '@angular/core'
|
import { LOCALE_ID } from '@angular/core'
|
||||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import { NgbDateAdapter, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { of } from 'rxjs'
|
import { of } from 'rxjs'
|
||||||
@@ -11,7 +11,6 @@ import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
|
|||||||
import {
|
import {
|
||||||
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
|
CUSTOM_FIELD_QUERY_OPERATORS_BY_GROUP,
|
||||||
CustomFieldQueryLogicalOperator,
|
CustomFieldQueryLogicalOperator,
|
||||||
CustomFieldQueryOperator,
|
|
||||||
CustomFieldQueryOperatorGroups,
|
CustomFieldQueryOperatorGroups,
|
||||||
} from 'src/app/data/custom-field-query'
|
} from 'src/app/data/custom-field-query'
|
||||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
CustomFieldQueryElement,
|
CustomFieldQueryElement,
|
||||||
CustomFieldQueryExpression,
|
CustomFieldQueryExpression,
|
||||||
} from 'src/app/utils/custom-field-query-element'
|
} from 'src/app/utils/custom-field-query-element'
|
||||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
|
||||||
import {
|
import {
|
||||||
CustomFieldQueriesModel,
|
CustomFieldQueriesModel,
|
||||||
CustomFieldsQueryDropdownComponent,
|
CustomFieldsQueryDropdownComponent,
|
||||||
@@ -50,12 +48,6 @@ const customFields = [
|
|||||||
data_type: CustomFieldDataType.Monetary,
|
data_type: CustomFieldDataType.Monetary,
|
||||||
extra_data: { default_currency: 'EUR' },
|
extra_data: { default_currency: 'EUR' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 4,
|
|
||||||
name: 'Test Date Field',
|
|
||||||
data_type: CustomFieldDataType.Date,
|
|
||||||
extra_data: {},
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
describe('CustomFieldsQueryDropdownComponent', () => {
|
describe('CustomFieldsQueryDropdownComponent', () => {
|
||||||
@@ -77,7 +69,6 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
provideHttpClient(withInterceptorsFromDi()),
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
provideHttpClientTesting(),
|
provideHttpClientTesting(),
|
||||||
{ provide: LOCALE_ID, useValue: 'de' },
|
{ provide: LOCALE_ID, useValue: 'de' },
|
||||||
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
|
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@@ -246,74 +237,6 @@ describe('CustomFieldsQueryDropdownComponent', () => {
|
|||||||
expect(component.selectionModel.queries.length).toBe(1)
|
expect(component.selectionModel.queries.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should build relative date values from amount and unit', async () => {
|
|
||||||
component.useDropdown = false
|
|
||||||
const atom = new CustomFieldQueryAtom([
|
|
||||||
4,
|
|
||||||
CustomFieldQueryOperator.GreaterThanOrEqual,
|
|
||||||
'2026-10-22',
|
|
||||||
])
|
|
||||||
component.selectionModel.addAtom(atom)
|
|
||||||
fixture.detectChanges()
|
|
||||||
await fixture.whenStable()
|
|
||||||
const query = (selector: string) =>
|
|
||||||
fixture.nativeElement.querySelector(selector)
|
|
||||||
expect(query('[ngbDatepicker]')).not.toBeNull()
|
|
||||||
expect(query('input[id$="_fixed"]').checked).toBe(true)
|
|
||||||
|
|
||||||
query('input[id$="_relative"]').click()
|
|
||||||
fixture.detectChanges()
|
|
||||||
await fixture.whenStable()
|
|
||||||
expect(atom.value).toBeNull()
|
|
||||||
const amount: HTMLInputElement = query('input[aria-label="Amount"]')
|
|
||||||
const unit: HTMLSelectElement = query('select[id$="_unit"]')
|
|
||||||
amount.value = '30'
|
|
||||||
amount.dispatchEvent(new Event('input'))
|
|
||||||
expect(atom.serialize()).toEqual([4, 'gte', '30 days ago'])
|
|
||||||
unit.selectedIndex = 6
|
|
||||||
unit.dispatchEvent(new Event('change'))
|
|
||||||
expect(atom.serialize()).toEqual([4, 'gte', 'in 30 weeks'])
|
|
||||||
unit.selectedIndex = 0
|
|
||||||
unit.dispatchEvent(new Event('change'))
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(atom.serialize()).toEqual([4, 'gte', 'today'])
|
|
||||||
expect(query('input[aria-label="Amount"]')).toBeNull()
|
|
||||||
|
|
||||||
query('input[id$="_fixed"]').click()
|
|
||||||
fixture.detectChanges()
|
|
||||||
expect(atom.value).toBeNull()
|
|
||||||
expect(query('[ngbDatepicker]')).not.toBeNull()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should restore a saved relative date value', async () => {
|
|
||||||
component.useDropdown = false
|
|
||||||
const atom = new CustomFieldQueryAtom([
|
|
||||||
4,
|
|
||||||
CustomFieldQueryOperator.Exact,
|
|
||||||
'in 2 months',
|
|
||||||
])
|
|
||||||
component.selectionModel.addAtom(atom)
|
|
||||||
fixture.detectChanges()
|
|
||||||
await fixture.whenStable()
|
|
||||||
expect(component.getRelativeDate(atom)).toEqual({
|
|
||||||
amount: 2,
|
|
||||||
unit: 'in {n} months',
|
|
||||||
})
|
|
||||||
expect(
|
|
||||||
fixture.nativeElement.querySelector('input[aria-label="Amount"]').value
|
|
||||||
).toBe('2')
|
|
||||||
|
|
||||||
const today = new CustomFieldQueryAtom([
|
|
||||||
4,
|
|
||||||
CustomFieldQueryOperator.Exact,
|
|
||||||
'today',
|
|
||||||
])
|
|
||||||
expect(component.getRelativeDate(today)).toEqual({
|
|
||||||
amount: null,
|
|
||||||
unit: 'today',
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('CustomFieldQueriesModel', () => {
|
describe('CustomFieldQueriesModel', () => {
|
||||||
let model: CustomFieldQueriesModel
|
let model: CustomFieldQueriesModel
|
||||||
|
|
||||||
|
|||||||
-81
@@ -49,26 +49,6 @@ import { LoadingComponentWithPermissions } from '../../loading-component/loading
|
|||||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||||
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
import { DocumentLinkComponent } from '../input/document-link/document-link.component'
|
||||||
|
|
||||||
interface RelativeDate {
|
|
||||||
// The expression sent to the API, with {n} standing in for the amount
|
|
||||||
amount: number | null
|
|
||||||
unit: string
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseRelativeDate(
|
|
||||||
value: string,
|
|
||||||
unit: string
|
|
||||||
): RelativeDate | undefined {
|
|
||||||
const [prefix, suffix] = unit.split('{n}')
|
|
||||||
if (suffix === undefined) {
|
|
||||||
return value === unit ? { amount: null, unit } : undefined
|
|
||||||
}
|
|
||||||
if (!value.startsWith(prefix) || !value.endsWith(suffix)) return undefined
|
|
||||||
const amount = value.slice(prefix.length, value.length - suffix.length)
|
|
||||||
if (!amount || !Number.isInteger(+amount) || +amount < 0) return undefined
|
|
||||||
return { amount: +amount, unit }
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CustomFieldQueriesModel {
|
export class CustomFieldQueriesModel {
|
||||||
private readonly _queries = signal<CustomFieldQueryElement[]>([])
|
private readonly _queries = signal<CustomFieldQueryElement[]>([])
|
||||||
private rootSubscriptions: Subscription[] = []
|
private rootSubscriptions: Subscription[] = []
|
||||||
@@ -325,67 +305,6 @@ export class CustomFieldsQueryDropdownComponent extends LoadingComponentWithPerm
|
|||||||
|
|
||||||
public readonly today: string = new Date().toLocaleDateString('en-CA')
|
public readonly today: string = new Date().toLocaleDateString('en-CA')
|
||||||
|
|
||||||
public readonly relativeDateUnits = [
|
|
||||||
{ id: 'today', label: $localize`Today` },
|
|
||||||
{ id: '{n} days ago', label: $localize`days ago` },
|
|
||||||
{ id: '{n} weeks ago', label: $localize`weeks ago` },
|
|
||||||
{ id: '{n} months ago', label: $localize`months ago` },
|
|
||||||
{ id: '{n} years ago', label: $localize`years ago` },
|
|
||||||
{ id: 'in {n} days', label: $localize`days from now` },
|
|
||||||
{ id: 'in {n} weeks', label: $localize`weeks from now` },
|
|
||||||
{ id: 'in {n} months', label: $localize`months from now` },
|
|
||||||
{ id: 'in {n} years', label: $localize`years from now` },
|
|
||||||
]
|
|
||||||
|
|
||||||
private readonly relativeDates = new Map<string, RelativeDate>()
|
|
||||||
|
|
||||||
getRelativeDate(atom: CustomFieldQueryAtom): RelativeDate | undefined {
|
|
||||||
if (!this.relativeDates.has(atom.id) && typeof atom.value === 'string') {
|
|
||||||
for (const { id } of this.relativeDateUnits) {
|
|
||||||
const relativeDate = parseRelativeDate(atom.value, id)
|
|
||||||
if (relativeDate) {
|
|
||||||
this.relativeDates.set(atom.id, relativeDate)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.relativeDates.get(atom.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasDateValueRow(atom: CustomFieldQueryAtom): boolean {
|
|
||||||
return (
|
|
||||||
this.getCustomFieldByID(atom.field)?.data_type ===
|
|
||||||
CustomFieldDataType.Date &&
|
|
||||||
atom.operator !== CustomFieldQueryOperator.Exists &&
|
|
||||||
atom.operator !== CustomFieldQueryOperator.IsNull
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleRelativeDate(atom: CustomFieldQueryAtom) {
|
|
||||||
if (this.getRelativeDate(atom)) {
|
|
||||||
this.relativeDates.delete(atom.id)
|
|
||||||
atom.value = null
|
|
||||||
} else {
|
|
||||||
this.relativeDates.set(atom.id, { amount: null, unit: '{n} days ago' })
|
|
||||||
this.updateRelativeDate(atom, {})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRelativeDate(
|
|
||||||
atom: CustomFieldQueryAtom,
|
|
||||||
change: Partial<RelativeDate>
|
|
||||||
) {
|
|
||||||
const { amount, unit } = Object.assign(
|
|
||||||
this.relativeDates.get(atom.id),
|
|
||||||
change
|
|
||||||
)
|
|
||||||
if (!unit.includes('{n}')) {
|
|
||||||
atom.value = unit
|
|
||||||
} else {
|
|
||||||
atom.value = amount === null ? null : unit.replace('{n}', `${amount}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public customFieldSearchFn = (term: string, field: CustomField): boolean =>
|
public customFieldSearchFn = (term: string, field: CustomField): boolean =>
|
||||||
matchesSearchText(field?.name, term)
|
matchesSearchText(field?.name, term)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -105,7 +105,7 @@
|
|||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="input-group-text overflow-hidden version-label">
|
<span class="input-group-text version-label">
|
||||||
@if (version.version_label) {
|
@if (version.version_label) {
|
||||||
{{ version.version_label }}
|
{{ version.version_label }}
|
||||||
} @else {
|
} @else {
|
||||||
|
|||||||
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
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
@@ -11,8 +11,6 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from paperless_testing import samples
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -151,39 +149,3 @@ def fake_progress_manager(
|
|||||||
|
|
||||||
monkeypatch.setattr("documents.tasks.ProgressManager", FakeProgressManager)
|
monkeypatch.setattr("documents.tasks.ProgressManager", FakeProgressManager)
|
||||||
return FakeProgressManager
|
return FakeProgressManager
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def shared_samples_dir() -> Path:
|
|
||||||
"""Directory of sample files used by more than one app's tests."""
|
|
||||||
return samples.SHARED_SAMPLES_DIR
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def simple_digital_pdf_file() -> Path:
|
|
||||||
"""One-page PDF with a text layer."""
|
|
||||||
return samples.SIMPLE_DIGITAL_PDF
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def multi_page_digital_pdf_file() -> Path:
|
|
||||||
"""Three-page PDF with a text layer."""
|
|
||||||
return samples.MULTI_PAGE_DIGITAL_PDF
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def with_form_pdf_file() -> Path:
|
|
||||||
"""PDF containing a fillable form."""
|
|
||||||
return samples.WITH_FORM_PDF
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def multi_page_images_pdf_file() -> Path:
|
|
||||||
"""Multi-page PDF of scanned images, no text layer."""
|
|
||||||
return samples.MULTI_PAGE_IMAGES_PDF
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def thumbnail_webp_file() -> Path:
|
|
||||||
"""Small WebP thumbnail."""
|
|
||||||
return samples.THUMBNAIL_WEBP
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from guardian.admin import GuardedModelAdmin
|
from guardian.admin import GuardedModelAdmin
|
||||||
from treenode.admin import TreeNodeModelAdmin
|
|
||||||
|
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
@@ -29,7 +28,7 @@ class CorrespondentAdmin(GuardedModelAdmin):
|
|||||||
list_editable = ("match", "matching_algorithm")
|
list_editable = ("match", "matching_algorithm")
|
||||||
|
|
||||||
|
|
||||||
class TagAdmin(GuardedModelAdmin, TreeNodeModelAdmin):
|
class TagAdmin(GuardedModelAdmin):
|
||||||
list_display = ("name", "color", "match", "matching_algorithm")
|
list_display = ("name", "color", "match", "matching_algorithm")
|
||||||
list_filter = ("matching_algorithm",)
|
list_filter = ("matching_algorithm",)
|
||||||
list_editable = ("color", "match", "matching_algorithm")
|
list_editable = ("color", "match", "matching_algorithm")
|
||||||
|
|||||||
@@ -255,19 +255,13 @@ def modify_tags(
|
|||||||
affected_docs = list(qs.values_list("pk", flat=True))
|
affected_docs = list(qs.values_list("pk", flat=True))
|
||||||
DocumentTagRelationship = Document.tags.through
|
DocumentTagRelationship = Document.tags.through
|
||||||
|
|
||||||
# add with all ancestors
|
hierarchy = Tag.get_hierarchy()
|
||||||
expanded_add_tags: set[int] = set()
|
expanded_add_tags = hierarchy.with_ancestors(
|
||||||
add_tag_objects = Tag.objects.filter(pk__in=add_tags)
|
Tag.objects.filter(pk__in=add_tags).values_list("pk", flat=True),
|
||||||
for t in add_tag_objects:
|
)
|
||||||
expanded_add_tags.add(int(t.id))
|
expanded_remove_tags = hierarchy.with_descendants(
|
||||||
expanded_add_tags.update(int(pk) for pk in t.get_ancestors_pks())
|
Tag.objects.filter(pk__in=remove_tags).values_list("pk", flat=True),
|
||||||
|
)
|
||||||
# remove with all descendants
|
|
||||||
expanded_remove_tags: set[int] = set()
|
|
||||||
remove_tag_objects = Tag.objects.filter(pk__in=remove_tags)
|
|
||||||
for t in remove_tag_objects:
|
|
||||||
expanded_remove_tags.add(int(t.id))
|
|
||||||
expanded_remove_tags.update(int(pk) for pk in t.get_descendants_pks())
|
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
if expanded_remove_tags:
|
if expanded_remove_tags:
|
||||||
|
|||||||
@@ -847,19 +847,6 @@ class ConsumerPlugin(
|
|||||||
local_added,
|
local_added,
|
||||||
self.filename,
|
self.filename,
|
||||||
self.filename,
|
self.filename,
|
||||||
custom_fields=[
|
|
||||||
CustomFieldInstance(
|
|
||||||
field=field,
|
|
||||||
**{
|
|
||||||
CustomFieldInstance.get_value_field_name(
|
|
||||||
field.data_type,
|
|
||||||
): self.metadata.custom_fields[field.pk],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
for field in CustomField.objects.filter(
|
|
||||||
pk__in=self.metadata.custom_fields or {},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _store(
|
def _store(
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ from decimal import InvalidOperation
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import dateparser
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Case
|
from django.db.models import Case
|
||||||
from django.db.models import CharField
|
from django.db.models import CharField
|
||||||
@@ -27,7 +25,6 @@ from django.db.models import Value
|
|||||||
from django.db.models import When
|
from django.db.models import When
|
||||||
from django.db.models.functions import Cast
|
from django.db.models.functions import Cast
|
||||||
from django.db.models.functions import NullIf
|
from django.db.models.functions import NullIf
|
||||||
from django.utils import timezone
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_filters import DateFilter
|
from django_filters import DateFilter
|
||||||
from django_filters.rest_framework import BooleanFilter
|
from django_filters.rest_framework import BooleanFilter
|
||||||
@@ -60,7 +57,6 @@ from documents.versioning import annotate_effective_content
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
|
CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
|
||||||
ID_KWARGS = ["in", "exact"]
|
ID_KWARGS = ["in", "exact"]
|
||||||
@@ -326,51 +322,6 @@ class MonetaryAmountField(serializers.Field):
|
|||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
class DateQueryField(serializers.DateField):
|
|
||||||
"""
|
|
||||||
Accepts either a fixed date or a relative date expression using `dateparser` and
|
|
||||||
resolves it against the provided reference datetime.
|
|
||||||
"""
|
|
||||||
|
|
||||||
default_error_messages = {
|
|
||||||
"expression": _(
|
|
||||||
'Expected a date or a relative date such as "today" or "in 30 days".',
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, *, reference_datetime: datetime, **kwargs):
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.reference_datetime = reference_datetime
|
|
||||||
|
|
||||||
def to_internal_value(self, value):
|
|
||||||
try:
|
|
||||||
# Let the parent class handle fixed-date formats and string validation first
|
|
||||||
return super().to_internal_value(value)
|
|
||||||
except serializers.ValidationError:
|
|
||||||
if not isinstance(value, str):
|
|
||||||
raise
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = dateparser.parse(
|
|
||||||
value,
|
|
||||||
# The UI always builds English expressions
|
|
||||||
locales=[*settings.DATE_PARSER_LANGUAGES, "en"]
|
|
||||||
if settings.DATE_PARSER_LANGUAGES
|
|
||||||
else None,
|
|
||||||
settings={
|
|
||||||
"PARSERS": ["relative-time"],
|
|
||||||
"RELATIVE_BASE": self.reference_datetime,
|
|
||||||
"TIMEZONE": str(self.reference_datetime.tzinfo),
|
|
||||||
"RETURN_AS_TIMEZONE_AWARE": True,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except (ValueError, OverflowError):
|
|
||||||
self.fail("expression")
|
|
||||||
if parsed is None:
|
|
||||||
self.fail("expression")
|
|
||||||
return parsed.astimezone(self.reference_datetime.tzinfo).date()
|
|
||||||
|
|
||||||
|
|
||||||
class SelectField(serializers.CharField):
|
class SelectField(serializers.CharField):
|
||||||
def __init__(self, custom_field: CustomField) -> None:
|
def __init__(self, custom_field: CustomField) -> None:
|
||||||
self._options = custom_field.extra_data["select_options"]
|
self._options = custom_field.extra_data["select_options"]
|
||||||
@@ -512,8 +463,6 @@ class CustomFieldQueryParser:
|
|||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
{self._validation_prefix: [_("Value must be valid JSON.")]},
|
{self._validation_prefix: [_("Value must be valid JSON.")]},
|
||||||
)
|
)
|
||||||
# Save the current local time as the reference for the query
|
|
||||||
self._reference_datetime = timezone.localtime()
|
|
||||||
return (
|
return (
|
||||||
self._parse_expr(expr, validation_prefix=self._validation_prefix),
|
self._parse_expr(expr, validation_prefix=self._validation_prefix),
|
||||||
self._annotations,
|
self._annotations,
|
||||||
@@ -700,8 +649,6 @@ class CustomFieldQueryParser:
|
|||||||
):
|
):
|
||||||
# DateField admits queries in the form of `year__exact`, etc. These take integers.
|
# DateField admits queries in the form of `year__exact`, etc. These take integers.
|
||||||
field = serializers.IntegerField()
|
field = serializers.IntegerField()
|
||||||
elif custom_field.data_type == CustomField.FieldDataType.DATE:
|
|
||||||
field = DateQueryField(reference_datetime=self._reference_datetime)
|
|
||||||
elif custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
|
elif custom_field.data_type == CustomField.FieldDataType.DOCUMENTLINK:
|
||||||
# We can be more specific here and make sure the value is a list.
|
# We can be more specific here and make sure the value is a list.
|
||||||
field = serializers.ListField(child=serializers.IntegerField())
|
field = serializers.ListField(child=serializers.IntegerField())
|
||||||
|
|||||||
@@ -108,6 +108,24 @@ def _deserialize_record(
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
for field_name, field_value in record.get("fields", {}).items():
|
for field_name, field_value in record.get("fields", {}).items():
|
||||||
|
# Older exports include django-treenode's derived bookkeeping. The
|
||||||
|
# parent FK is authoritative; accept only these known removed fields.
|
||||||
|
if Model is Tag and field_name in {
|
||||||
|
"tn_ancestors_count",
|
||||||
|
"tn_ancestors_pks",
|
||||||
|
"tn_children_count",
|
||||||
|
"tn_children_pks",
|
||||||
|
"tn_depth",
|
||||||
|
"tn_descendants_count",
|
||||||
|
"tn_descendants_pks",
|
||||||
|
"tn_index",
|
||||||
|
"tn_level",
|
||||||
|
"tn_order",
|
||||||
|
"tn_priority",
|
||||||
|
"tn_siblings_count",
|
||||||
|
"tn_siblings_pks",
|
||||||
|
}:
|
||||||
|
continue
|
||||||
field = Model._meta.get_field(field_name)
|
field = Model._meta.get_field(field_name)
|
||||||
remote = field.remote_field
|
remote = field.remote_field
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Generated by Django 5.2.17 on 2026-10-28 22:54
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "0027_documentbarcode"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_ancestors_count",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_ancestors_pks",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_children_count",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_children_pks",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_depth",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_descendants_count",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_descendants_pks",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_index",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_level",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_order",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_priority",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_siblings_count",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="tag",
|
||||||
|
name="tn_siblings_pks",
|
||||||
|
),
|
||||||
|
]
|
||||||
+53
-23
@@ -14,7 +14,6 @@ from django.db import models
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from multiselectfield import MultiSelectField
|
from multiselectfield import MultiSelectField
|
||||||
from treenode.models import TreeNodeModel
|
|
||||||
|
|
||||||
if settings.AUDIT_LOG_ENABLED:
|
if settings.AUDIT_LOG_ENABLED:
|
||||||
from auditlog.registry import auditlog
|
from auditlog.registry import auditlog
|
||||||
@@ -100,7 +99,17 @@ class Correspondent(MatchingModel):
|
|||||||
verbose_name_plural = _("correspondents")
|
verbose_name_plural = _("correspondents")
|
||||||
|
|
||||||
|
|
||||||
class Tag(MatchingModel, TreeNodeModel):
|
class Tag(MatchingModel):
|
||||||
|
# Keep the existing column and reverse relation during the transition.
|
||||||
|
tn_parent = models.ForeignKey(
|
||||||
|
"self",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=models.CASCADE,
|
||||||
|
related_name="tn_children",
|
||||||
|
verbose_name=_("Parent"),
|
||||||
|
)
|
||||||
|
|
||||||
color = models.CharField(_("color"), max_length=7, default="#a6cee3")
|
color = models.CharField(_("color"), max_length=7, default="#a6cee3")
|
||||||
# Maximum allowed nesting depth for tags (root = 1, max depth = 5)
|
# Maximum allowed nesting depth for tags (root = 1, max depth = 5)
|
||||||
MAX_NESTING_DEPTH: Final[int] = 5
|
MAX_NESTING_DEPTH: Final[int] = 5
|
||||||
@@ -114,27 +123,51 @@ class Tag(MatchingModel, TreeNodeModel):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta(MatchingModel.Meta, TreeNodeModel.Meta):
|
class Meta(MatchingModel.Meta):
|
||||||
verbose_name = _("tag")
|
verbose_name = _("tag")
|
||||||
verbose_name_plural = _("tags")
|
verbose_name_plural = _("tags")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_hierarchy(cls):
|
||||||
|
from documents.tag_hierarchy import TagHierarchy
|
||||||
|
|
||||||
|
return TagHierarchy(cls.objects.values_list("pk", "tn_parent_id"))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_pk(self):
|
||||||
|
return self.tn_parent_id
|
||||||
|
|
||||||
|
def get_parent(self):
|
||||||
|
return self.tn_parent
|
||||||
|
|
||||||
|
def get_ancestors_pks(self):
|
||||||
|
return self.get_hierarchy().ancestors(self.tn_parent_id)
|
||||||
|
|
||||||
|
def get_ancestors(self):
|
||||||
|
ids = self.get_ancestors_pks()
|
||||||
|
tags = self.__class__.objects.in_bulk(ids)
|
||||||
|
return [tags[pk] for pk in ids]
|
||||||
|
|
||||||
|
def get_descendants_pks(self):
|
||||||
|
return self.get_hierarchy().descendants([self.pk])
|
||||||
|
|
||||||
|
def get_descendants(self):
|
||||||
|
return self.__class__.objects.filter(pk__in=self.get_descendants_pks())
|
||||||
|
|
||||||
|
def get_children_queryset(self):
|
||||||
|
return self.__class__.objects.filter(tn_parent_id=self.pk)
|
||||||
|
|
||||||
def clean(self) -> None:
|
def clean(self) -> None:
|
||||||
# Prevent self-parenting and assigning a descendant as parent
|
hierarchy = self.get_hierarchy()
|
||||||
parent = self.get_parent()
|
if self.pk is not None and self.tn_parent_id == self.pk:
|
||||||
if parent == self:
|
raise ValidationError({"tn_parent": _("Cannot set itself as parent.")})
|
||||||
raise ValidationError({"parent": _("Cannot set itself as parent.")})
|
ancestors = hierarchy.ancestors(self.tn_parent_id)
|
||||||
if parent and self.pk is not None and self.is_ancestor_of(parent):
|
if self.pk is not None and self.pk in ancestors:
|
||||||
raise ValidationError({"parent": _("Cannot set parent to a descendant.")})
|
raise ValidationError(
|
||||||
|
{"tn_parent": _("Cannot set parent to a descendant.")},
|
||||||
# Enforce maximum nesting depth
|
)
|
||||||
new_parent_depth = 0
|
if len(ancestors) + 1 + hierarchy.height(self.pk) > self.MAX_NESTING_DEPTH:
|
||||||
if parent:
|
raise ValidationError({"tn_parent": _("Maximum nesting depth exceeded.")})
|
||||||
new_parent_depth = parent.get_ancestors_count() + 1
|
|
||||||
|
|
||||||
height = 0 if self.pk is None else self.get_depth()
|
|
||||||
deepest_new_depth = (new_parent_depth + 1) + height
|
|
||||||
if deepest_new_depth > self.MAX_NESTING_DEPTH:
|
|
||||||
raise ValidationError({"parent": _("Maximum nesting depth exceeded.")})
|
|
||||||
|
|
||||||
return super().clean()
|
return super().clean()
|
||||||
|
|
||||||
@@ -539,10 +572,7 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
|
|||||||
return self.created
|
return self.created
|
||||||
|
|
||||||
def add_nested_tags(self, tags) -> None:
|
def add_nested_tags(self, tags) -> None:
|
||||||
tag_ids = set()
|
tag_ids = Tag.get_hierarchy().with_ancestors(tag.pk for tag in tags)
|
||||||
for tag in tags:
|
|
||||||
tag_ids.add(tag.id)
|
|
||||||
tag_ids.update(tag.get_ancestors_pks())
|
|
||||||
|
|
||||||
tags_to_add = self.tags.model.objects.filter(id__in=tag_ids)
|
tags_to_add = self.tags.model.objects.filter(id__in=tag_ids)
|
||||||
self.tags.add(*tags_to_add)
|
self.tags.add(*tags_to_add)
|
||||||
|
|||||||
@@ -589,7 +589,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
|
|
||||||
text_color = serializers.SerializerMethodField()
|
text_color = serializers.SerializerMethodField()
|
||||||
|
|
||||||
# map to treenode's tn_parent
|
# Preserve the public API parent field.
|
||||||
parent = serializers.PrimaryKeyRelatedField(
|
parent = serializers.PrimaryKeyRelatedField(
|
||||||
queryset=Tag.objects.all(),
|
queryset=Tag.objects.all(),
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
@@ -692,7 +692,9 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
self.instance.clean()
|
self.instance.clean()
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.debug("Tag parent validation failed: %s", e)
|
logger.debug("Tag parent validation failed: %s", e)
|
||||||
raise e
|
raise serializers.ValidationError(
|
||||||
|
{"parent": e.message_dict["tn_parent"]},
|
||||||
|
) from e
|
||||||
finally:
|
finally:
|
||||||
self.instance.tn_parent = original_parent
|
self.instance.tn_parent = original_parent
|
||||||
else:
|
else:
|
||||||
@@ -702,7 +704,9 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
|||||||
temp.clean()
|
temp.clean()
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
logger.debug("Tag parent validation failed: %s", e)
|
logger.debug("Tag parent validation failed: %s", e)
|
||||||
raise e
|
raise serializers.ValidationError(
|
||||||
|
{"parent": e.message_dict["tn_parent"]},
|
||||||
|
) from e
|
||||||
|
|
||||||
return super().validate(attrs)
|
return super().validate(attrs)
|
||||||
|
|
||||||
@@ -1198,61 +1202,35 @@ class DocumentSerializer(
|
|||||||
doc_id,
|
doc_id,
|
||||||
)
|
)
|
||||||
if "tags" in validated_data:
|
if "tags" in validated_data:
|
||||||
# Respect tag hierarchy on updates:
|
hierarchy = Tag.get_hierarchy()
|
||||||
# - Adding a child adds its ancestors
|
previous_ids = set(instance.tags.values_list("pk", flat=True))
|
||||||
# - Removing a parent removes all its descendants
|
requested_ids = {tag.pk for tag in validated_data["tags"]}
|
||||||
prev_tags = set(instance.tags.all())
|
required_ids = hierarchy.with_ancestors(requested_ids - previous_ids)
|
||||||
requested_tags = set(validated_data["tags"])
|
blocked_ids = hierarchy.with_descendants(previous_ids - requested_ids)
|
||||||
|
blocked_ids.difference_update(required_ids)
|
||||||
|
final_ids = hierarchy.with_ancestors(requested_ids) - blocked_ids
|
||||||
|
validated_data["tags"] = list(Tag.objects.filter(pk__in=final_ids))
|
||||||
|
|
||||||
# Tags newly added in this update and the ancestors they require
|
|
||||||
added_tags = requested_tags - prev_tags
|
|
||||||
required_by_add_tags = set(added_tags)
|
|
||||||
for t in added_tags:
|
|
||||||
required_by_add_tags.update(t.get_ancestors())
|
|
||||||
|
|
||||||
# Tags being removed in this update and all descendants, except
|
|
||||||
# those required by a tag that is being added in this same update
|
|
||||||
removed_tags = prev_tags - requested_tags
|
|
||||||
blocked_tags = set(removed_tags)
|
|
||||||
for t in removed_tags:
|
|
||||||
blocked_tags.update(t.get_descendants())
|
|
||||||
blocked_tags.difference_update(required_by_add_tags)
|
|
||||||
|
|
||||||
# Add all parent tags
|
|
||||||
final_tags = set(requested_tags)
|
|
||||||
for t in requested_tags:
|
|
||||||
final_tags.update(t.get_ancestors())
|
|
||||||
|
|
||||||
# Drop removed parents and their descendants
|
|
||||||
final_tags.difference_update(blocked_tags)
|
|
||||||
|
|
||||||
validated_data["tags"] = list(final_tags)
|
|
||||||
if validated_data.get("remove_inbox_tags"):
|
if validated_data.get("remove_inbox_tags"):
|
||||||
current_tag_ids = {t.pk for t in instance.tags.all()}
|
hierarchy = Tag.get_hierarchy()
|
||||||
tags = (
|
tags = (
|
||||||
validated_data["tags"]
|
validated_data["tags"]
|
||||||
if "tags" in validated_data
|
if "tags" in validated_data
|
||||||
else list(instance.tags.all())
|
else list(instance.tags.all())
|
||||||
)
|
)
|
||||||
|
current_ids = set(instance.tags.values_list("pk", flat=True))
|
||||||
# Tags newly added in this update, plus their ancestors, are kept
|
keep_ids = hierarchy.with_ancestors(
|
||||||
keep_ids: set[int] = set()
|
tag.pk for tag in tags if tag.pk not in current_ids
|
||||||
for tag in tags:
|
)
|
||||||
if tag.pk not in current_tag_ids:
|
inbox_ids = (
|
||||||
keep_ids.add(tag.pk)
|
|
||||||
keep_ids.update(int(pk) for pk in tag.get_ancestors_pks())
|
|
||||||
|
|
||||||
# Remove inbox tags and their descendants, except those being kept
|
|
||||||
remove_ids: set[int] = set()
|
|
||||||
for inbox_tag in (
|
|
||||||
Tag.objects.filter(is_inbox_tag=True)
|
Tag.objects.filter(is_inbox_tag=True)
|
||||||
.exclude(pk__in=keep_ids)
|
.exclude(
|
||||||
.only("pk", "tn_descendants_pks")
|
pk__in=keep_ids,
|
||||||
):
|
)
|
||||||
remove_ids.add(inbox_tag.pk)
|
.values_list("pk", flat=True)
|
||||||
remove_ids.update(int(pk) for pk in inbox_tag.get_descendants_pks())
|
)
|
||||||
|
remove_ids = hierarchy.with_descendants(inbox_ids) - keep_ids
|
||||||
validated_data["tags"] = [t for t in tags if t.pk not in remove_ids]
|
validated_data["tags"] = [tag for tag in tags if tag.pk not in remove_ids]
|
||||||
|
|
||||||
if settings.AUDIT_LOG_ENABLED:
|
if settings.AUDIT_LOG_ENABLED:
|
||||||
with set_actor(self.user):
|
with set_actor(self.user):
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""An operation-local snapshot of tag edges, with no persistent derived fields.
|
||||||
|
|
||||||
|
Never cache this globally: tasks, bulk updates and other processes can change
|
||||||
|
parents. Callers traversing multiple tags should share one snapshot.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
|
||||||
|
class TagHierarchy:
|
||||||
|
def __init__(self, edges: Iterable[tuple[int, int | None]]) -> None:
|
||||||
|
self.parents = dict(edges)
|
||||||
|
self.children: dict[int | None, list[int]] = defaultdict(list)
|
||||||
|
for pk, parent in self.parents.items():
|
||||||
|
self.children[parent].append(pk)
|
||||||
|
|
||||||
|
def ancestors(self, parent_id: int | None) -> list[int]:
|
||||||
|
result = []
|
||||||
|
seen = set()
|
||||||
|
while parent_id is not None and parent_id not in seen:
|
||||||
|
seen.add(parent_id)
|
||||||
|
result.append(parent_id)
|
||||||
|
parent_id = self.parents.get(parent_id)
|
||||||
|
return result[::-1]
|
||||||
|
|
||||||
|
def with_ancestors(self, roots: Iterable[int]) -> set[int]:
|
||||||
|
result = set(roots)
|
||||||
|
for pk in list(result):
|
||||||
|
result.update(self.ancestors(self.parents.get(pk)))
|
||||||
|
return result
|
||||||
|
|
||||||
|
def with_descendants(self, roots: Iterable[int]) -> set[int]:
|
||||||
|
roots = set(roots)
|
||||||
|
return roots | self.descendants(roots)
|
||||||
|
|
||||||
|
def descendants(self, roots: Iterable[int]) -> set[int]:
|
||||||
|
roots = {pk for pk in roots if pk is not None}
|
||||||
|
seen = set(roots)
|
||||||
|
pending = list(roots)
|
||||||
|
while pending:
|
||||||
|
for child in self.children.get(pending.pop(), ()):
|
||||||
|
if child not in seen:
|
||||||
|
seen.add(child)
|
||||||
|
pending.append(child)
|
||||||
|
return seen - roots
|
||||||
|
|
||||||
|
def height(self, pk: int | None) -> int:
|
||||||
|
if pk is None:
|
||||||
|
return 0
|
||||||
|
seen = {pk}
|
||||||
|
pending = [(pk, 0)]
|
||||||
|
height = 0
|
||||||
|
while pending:
|
||||||
|
parent, depth = pending.pop()
|
||||||
|
height = max(height, depth)
|
||||||
|
for child in self.children.get(parent, ()):
|
||||||
|
if child not in seen:
|
||||||
|
seen.add(child)
|
||||||
|
pending.append((child, depth + 1))
|
||||||
|
return height
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import logging
|
import logging
|
||||||
from collections.abc import Iterable
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -14,9 +13,7 @@ from jinja2 import make_logging_undefined
|
|||||||
from jinja2.meta import find_undeclared_variables
|
from jinja2.meta import find_undeclared_variables
|
||||||
from jinja2.sandbox import SecurityError
|
from jinja2.sandbox import SecurityError
|
||||||
|
|
||||||
from documents.models import CustomFieldInstance
|
|
||||||
from documents.templating.environment import _template_environment
|
from documents.templating.environment import _template_environment
|
||||||
from documents.templating.filepath import get_custom_fields_context
|
|
||||||
from documents.templating.filters import format_datetime
|
from documents.templating.filters import format_datetime
|
||||||
from documents.templating.filters import localize_date
|
from documents.templating.filters import localize_date
|
||||||
|
|
||||||
@@ -59,7 +56,6 @@ _known_placeholder_names = {
|
|||||||
"doc_title",
|
"doc_title",
|
||||||
"doc_url",
|
"doc_url",
|
||||||
"doc_id",
|
"doc_id",
|
||||||
"custom_fields",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -90,7 +86,6 @@ def parse_w_workflow_placeholders(
|
|||||||
doc_title: str | None = None,
|
doc_title: str | None = None,
|
||||||
doc_url: str | None = None,
|
doc_url: str | None = None,
|
||||||
doc_id: int | None = None,
|
doc_id: int | None = None,
|
||||||
custom_fields: Iterable[CustomFieldInstance] = (),
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Available title placeholders for Workflows depend on what has already been assigned,
|
Available title placeholders for Workflows depend on what has already been assigned,
|
||||||
@@ -113,7 +108,6 @@ def parse_w_workflow_placeholders(
|
|||||||
"original_filename": Path(original_filename).stem,
|
"original_filename": Path(original_filename).stem,
|
||||||
"filename": Path(filename).stem,
|
"filename": Path(filename).stem,
|
||||||
}
|
}
|
||||||
formatting.update(get_custom_fields_context(custom_fields))
|
|
||||||
if created is not None:
|
if created is not None:
|
||||||
formatting.update(
|
formatting.update(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,25 +12,29 @@ if TYPE_CHECKING:
|
|||||||
from paperless_testing.dirs import PaperlessDirs
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def document_samples_dir() -> Path:
|
||||||
|
"""Path to the shared test sample documents."""
|
||||||
|
return Path(__file__).parent / "samples" / "documents"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def sample_doc(
|
def sample_doc(
|
||||||
paperless_dirs: "PaperlessDirs",
|
paperless_dirs: "PaperlessDirs",
|
||||||
simple_digital_pdf_file: Path,
|
document_samples_dir: Path,
|
||||||
multi_page_images_pdf_file: Path,
|
|
||||||
thumbnail_webp_file: Path,
|
|
||||||
) -> "Document":
|
) -> "Document":
|
||||||
"""Create a document with valid files and matching checksums."""
|
"""Create a document with valid files and matching checksums."""
|
||||||
with filelock.FileLock(paperless_dirs.media_lock):
|
with filelock.FileLock(paperless_dirs.media_lock):
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
simple_digital_pdf_file,
|
document_samples_dir / "originals" / "0000001.pdf",
|
||||||
paperless_dirs.originals_dir / "0000001.pdf",
|
paperless_dirs.originals_dir / "0000001.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
multi_page_images_pdf_file,
|
document_samples_dir / "archive" / "0000001.pdf",
|
||||||
paperless_dirs.archive_dir / "0000001.pdf",
|
paperless_dirs.archive_dir / "0000001.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
thumbnail_webp_file,
|
document_samples_dir / "thumbnails" / "0000001.webp",
|
||||||
paperless_dirs.thumbnail_dir / "0000001.webp",
|
paperless_dirs.thumbnail_dir / "0000001.webp",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
Binary file not shown.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
This is a test file.
|
||||||
@@ -53,7 +53,7 @@ class TestBulkDownload(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
archive_checksum="D",
|
archive_checksum="D",
|
||||||
)
|
)
|
||||||
|
|
||||||
shutil.copy(self.SIMPLE_PDF, self.doc2.source_path)
|
shutil.copy(self.SAMPLE_DIR / "simple.pdf", self.doc2.source_path)
|
||||||
shutil.copy(self.SAMPLE_DIR / "simple.png", self.doc2b.source_path)
|
shutil.copy(self.SAMPLE_DIR / "simple.png", self.doc2b.source_path)
|
||||||
shutil.copy(self.SAMPLE_DIR / "simple.jpg", self.doc3.source_path)
|
shutil.copy(self.SAMPLE_DIR / "simple.jpg", self.doc3.source_path)
|
||||||
shutil.copy(self.SAMPLE_DIR / "test_with_bom.pdf", self.doc3.archive_path)
|
shutil.copy(self.SAMPLE_DIR / "test_with_bom.pdf", self.doc3.archive_path)
|
||||||
|
|||||||
@@ -57,8 +57,6 @@ from paperless_testing.http import read_streaming_response
|
|||||||
from paperless_testing.permissions import grant_all_global
|
from paperless_testing.permissions import grant_all_global
|
||||||
from paperless_testing.permissions import grant_global
|
from paperless_testing.permissions import grant_global
|
||||||
from paperless_testing.permissions import grant_object
|
from paperless_testing.permissions import grant_object
|
||||||
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
|
|
||||||
from paperless_testing.samples import THUMBNAIL_WEBP
|
|
||||||
|
|
||||||
|
|
||||||
class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
||||||
@@ -1919,11 +1917,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SimpleUploadedFile(
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
"simple.pdf",
|
|
||||||
SIMPLE_DIGITAL_PDF.read_bytes(),
|
|
||||||
content_type="application/pdf",
|
|
||||||
) as f:
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f},
|
{"document": f},
|
||||||
@@ -1951,7 +1945,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
|
|
||||||
payload = SimpleUploadedFile(
|
payload = SimpleUploadedFile(
|
||||||
"../../outside.pdf",
|
"../../outside.pdf",
|
||||||
SIMPLE_DIGITAL_PDF.read_bytes(),
|
(Path(__file__).parent / "samples" / "simple.pdf").read_bytes(),
|
||||||
content_type="application/pdf",
|
content_type="application/pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1980,7 +1974,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
pdf_bytes = SIMPLE_DIGITAL_PDF.read_bytes()
|
pdf_bytes = (Path(__file__).parent / "samples" / "simple.pdf").read_bytes()
|
||||||
boundary = "paperless-boundary"
|
boundary = "paperless-boundary"
|
||||||
payload = (
|
payload = (
|
||||||
(
|
(
|
||||||
@@ -2056,7 +2050,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
def test_upload_insufficient_permissions(self) -> None:
|
def test_upload_insufficient_permissions(self) -> None:
|
||||||
self.client.force_authenticate(user=UserFactory(username="testuser2"))
|
self.client.force_authenticate(user=UserFactory(username="testuser2"))
|
||||||
|
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f},
|
{"document": f},
|
||||||
@@ -2069,11 +2063,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SimpleUploadedFile(
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
"simple.pdf",
|
|
||||||
SIMPLE_DIGITAL_PDF.read_bytes(),
|
|
||||||
content_type="application/pdf",
|
|
||||||
) as f:
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{
|
{
|
||||||
@@ -2106,7 +2096,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"documenst": f},
|
{"documenst": f},
|
||||||
@@ -2132,7 +2122,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "title": "my custom title"},
|
{"document": f, "title": "my custom title"},
|
||||||
@@ -2152,7 +2142,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
c = Correspondent.objects.create(name="test-corres")
|
c = Correspondent.objects.create(name="test-corres")
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "correspondent": c.id},
|
{"document": f, "correspondent": c.id},
|
||||||
@@ -2171,7 +2161,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "correspondent": 3456},
|
{"document": f, "correspondent": 3456},
|
||||||
@@ -2186,7 +2176,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
dt = DocumentType.objects.create(name="invoice")
|
dt = DocumentType.objects.create(name="invoice")
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "document_type": dt.id},
|
{"document": f, "document_type": dt.id},
|
||||||
@@ -2205,7 +2195,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "document_type": 34578},
|
{"document": f, "document_type": 34578},
|
||||||
@@ -2220,7 +2210,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
sp = StoragePath.objects.create(name="invoices")
|
sp = StoragePath.objects.create(name="invoices")
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "storage_path": sp.id},
|
{"document": f, "storage_path": sp.id},
|
||||||
@@ -2239,7 +2229,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "storage_path": 34578},
|
{"document": f, "storage_path": 34578},
|
||||||
@@ -2255,7 +2245,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
|
|
||||||
t1 = Tag.objects.create(name="tag1")
|
t1 = Tag.objects.create(name="tag1")
|
||||||
t2 = Tag.objects.create(name="tag2")
|
t2 = Tag.objects.create(name="tag2")
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "tags": [t2.id, t1.id]},
|
{"document": f, "tags": [t2.id, t1.id]},
|
||||||
@@ -2276,7 +2266,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
|
|
||||||
t1 = Tag.objects.create(name="tag1")
|
t1 = Tag.objects.create(name="tag1")
|
||||||
t2 = Tag.objects.create(name="tag2")
|
t2 = Tag.objects.create(name="tag2")
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "tags": [t2.id, t1.id, 734563]},
|
{"document": f, "tags": [t2.id, t1.id, 734563]},
|
||||||
@@ -2300,7 +2290,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
0,
|
0,
|
||||||
tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"),
|
tzinfo=zoneinfo.ZoneInfo("America/Los_Angeles"),
|
||||||
)
|
)
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "created": created},
|
{"document": f, "created": created},
|
||||||
@@ -2316,11 +2306,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SimpleUploadedFile(
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
"simple.pdf",
|
|
||||||
SIMPLE_DIGITAL_PDF.read_bytes(),
|
|
||||||
content_type="application/pdf",
|
|
||||||
) as f:
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "archive_serial_number": 500},
|
{"document": f, "archive_serial_number": 500},
|
||||||
@@ -2347,11 +2333,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
data_type=CustomField.FieldDataType.STRING,
|
data_type=CustomField.FieldDataType.STRING,
|
||||||
)
|
)
|
||||||
|
|
||||||
with SimpleUploadedFile(
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
"simple.pdf",
|
|
||||||
SIMPLE_DIGITAL_PDF.read_bytes(),
|
|
||||||
content_type="application/pdf",
|
|
||||||
) as f:
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{
|
{
|
||||||
@@ -2406,7 +2388,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
w1.actions.add(action1)
|
w1.actions.add(action1)
|
||||||
w1.save()
|
w1.save()
|
||||||
|
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{
|
{
|
||||||
@@ -2447,11 +2429,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
data_type=CustomField.FieldDataType.INT,
|
data_type=CustomField.FieldDataType.INT,
|
||||||
)
|
)
|
||||||
|
|
||||||
with SimpleUploadedFile(
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
"simple.pdf",
|
|
||||||
SIMPLE_DIGITAL_PDF.read_bytes(),
|
|
||||||
content_type="application/pdf",
|
|
||||||
) as f:
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{
|
{
|
||||||
@@ -2500,7 +2478,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
for payload in error_payloads:
|
for payload in error_payloads:
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
data = {"document": f, **payload}
|
data = {"document": f, **payload}
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
@@ -2558,7 +2536,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
id=str(uuid.uuid4()),
|
id=str(uuid.uuid4()),
|
||||||
)
|
)
|
||||||
|
|
||||||
with SIMPLE_DIGITAL_PDF.open("rb") as f:
|
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/post_document/",
|
"/api/documents/post_document/",
|
||||||
{"document": f, "from_webui": True},
|
{"document": f, "from_webui": True},
|
||||||
@@ -2597,8 +2575,14 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
archive_filename="archive.pdf",
|
archive_filename="archive.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
source_file: Path = THUMBNAIL_WEBP
|
source_file: Path = (
|
||||||
archive_file: Path = SIMPLE_DIGITAL_PDF
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "thumbnails"
|
||||||
|
/ "0000001.webp"
|
||||||
|
)
|
||||||
|
archive_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
|
||||||
|
|
||||||
shutil.copy(source_file, doc.source_path)
|
shutil.copy(source_file, doc.source_path)
|
||||||
shutil.copy(archive_file, doc.archive_path)
|
shutil.copy(archive_file, doc.archive_path)
|
||||||
@@ -2631,7 +2615,7 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
shutil.copy(SIMPLE_DIGITAL_PDF, doc.source_path)
|
shutil.copy(Path(__file__).parent / "samples" / "simple.pdf", doc.source_path)
|
||||||
|
|
||||||
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
|
response = self.client.get(f"/api/documents/{doc.pk}/metadata/")
|
||||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
@@ -4397,8 +4381,8 @@ class TestDocumentApi(DirectoriesMixin, ConsumeTaskMixin, APITestCase):
|
|||||||
filename="test2.pdf",
|
filename="test2.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
archive_file = SIMPLE_DIGITAL_PDF
|
archive_file = Path(__file__).parent / "samples" / "simple.pdf"
|
||||||
source_file = SIMPLE_DIGITAL_PDF
|
source_file = Path(__file__).parent / "samples" / "simple.pdf"
|
||||||
|
|
||||||
shutil.copy(archive_file, doc.archive_path)
|
shutil.copy(archive_file, doc.archive_path)
|
||||||
shutil.copy(source_file, doc2.source_path)
|
shutil.copy(source_file, doc2.source_path)
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ from documents.tests.utils import SampleDirMixin
|
|||||||
from paperless_testing.dirs import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
from paperless_testing.factories import UserFactory
|
from paperless_testing.factories import UserFactory
|
||||||
from paperless_testing.permissions import grant_global
|
from paperless_testing.permissions import grant_global
|
||||||
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
|
|
||||||
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
|
|
||||||
from paperless_testing.samples import WITH_FORM_PDF
|
|
||||||
|
|
||||||
|
|
||||||
class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
||||||
@@ -45,15 +42,15 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
|
|
||||||
# Copy sample files to document paths (using different files to distinguish versions)
|
# Copy sample files to document paths (using different files to distinguish versions)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
self.SAMPLE_DIR / "documents" / "originals" / "0000001.pdf",
|
||||||
self.doc1.archive_path,
|
self.doc1.archive_path,
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
MULTI_PAGE_DIGITAL_PDF,
|
self.SAMPLE_DIR / "documents" / "originals" / "0000002.pdf",
|
||||||
self.doc1.source_path,
|
self.doc1.source_path,
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
WITH_FORM_PDF,
|
self.SAMPLE_DIR / "documents" / "originals" / "0000003.pdf",
|
||||||
self.doc2.source_path,
|
self.doc2.source_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -380,7 +377,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
checksum="3",
|
checksum="3",
|
||||||
filename="test3.pdf",
|
filename="test3.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(self.SIMPLE_PDF, doc3.source_path)
|
shutil.copy(self.SAMPLE_DIR / "simple.pdf", doc3.source_path)
|
||||||
|
|
||||||
doc4 = Document.objects.create(
|
doc4 = Document.objects.create(
|
||||||
title="test1",
|
title="test1",
|
||||||
@@ -389,7 +386,7 @@ class TestEmail(DirectoriesMixin, SampleDirMixin, APITestCase):
|
|||||||
checksum="4",
|
checksum="4",
|
||||||
filename="test4.pdf",
|
filename="test4.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(self.SIMPLE_PDF, doc4.source_path)
|
shutil.copy(self.SAMPLE_DIR / "simple.pdf", doc4.source_path)
|
||||||
|
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
self.ENDPOINT,
|
self.ENDPOINT,
|
||||||
|
|||||||
@@ -2,12 +2,8 @@ import json
|
|||||||
import types
|
import types
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from unittest.mock import patch
|
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
|
|
||||||
import time_machine
|
|
||||||
from django.test import override_settings
|
|
||||||
from django.utils import timezone
|
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
@@ -447,65 +443,6 @@ class TestCustomFieldsSearch(DirectoriesMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(TIME_ZONE="America/Los_Angeles")
|
|
||||||
def test_relative_dates(self) -> None:
|
|
||||||
# Reevaluate the same mixed relative/fixed range on different local dates.
|
|
||||||
for today in (date(2024, 8, 22), date(2024, 11, 15)):
|
|
||||||
with (
|
|
||||||
self.subTest(today=today),
|
|
||||||
time_machine.travel(f"{today}T23:30:00-07:00", tick=False),
|
|
||||||
patch(
|
|
||||||
"documents.filters.timezone.localtime",
|
|
||||||
wraps=timezone.localtime,
|
|
||||||
) as localtime,
|
|
||||||
):
|
|
||||||
self._assert_query_match_predicate(
|
|
||||||
["date_field", "range", ["today", "2024-11-15"]],
|
|
||||||
lambda document, today=today: (
|
|
||||||
"date_field" in document
|
|
||||||
and document["date_field"] is not None
|
|
||||||
and today <= document["date_field"] <= date(2024, 11, 15)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
localtime.assert_called_once_with()
|
|
||||||
|
|
||||||
def test_relative_date_validation(self) -> None:
|
|
||||||
for value, keyword in (
|
|
||||||
("not a date", "date"),
|
|
||||||
("January 2024", "date"),
|
|
||||||
("today + " + "9" * 5000 + " days", "date"),
|
|
||||||
(True, "format"),
|
|
||||||
):
|
|
||||||
with self.subTest(value=str(value)[:50]):
|
|
||||||
self._assert_validation_error(
|
|
||||||
json.dumps(["date_field", "exact", value]),
|
|
||||||
["custom_field_query", "2"],
|
|
||||||
keyword,
|
|
||||||
)
|
|
||||||
|
|
||||||
@time_machine.travel("2024-08-22T12:00:00Z", tick=False)
|
|
||||||
def test_relative_date_languages(self) -> None:
|
|
||||||
for locales in (["de"], ["zh-Hans", "de"], None):
|
|
||||||
with (
|
|
||||||
self.subTest(locales=locales),
|
|
||||||
override_settings(
|
|
||||||
DATE_PARSER_LANGUAGES=locales,
|
|
||||||
),
|
|
||||||
):
|
|
||||||
self._assert_query_match_predicate(
|
|
||||||
["date_field", "exact", "heute"],
|
|
||||||
lambda document: (
|
|
||||||
"date_field" in document
|
|
||||||
and document["date_field"] == date(2024, 8, 22)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
with override_settings(DATE_PARSER_LANGUAGES=["en"]):
|
|
||||||
self._assert_validation_error(
|
|
||||||
json.dumps(["date_field", "exact", "heute"]),
|
|
||||||
["custom_field_query", "2"],
|
|
||||||
"date",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_gt_monetary(self) -> None:
|
def test_gt_monetary(self) -> None:
|
||||||
self._assert_query_match_predicate(
|
self._assert_query_match_predicate(
|
||||||
["monetary_field", "gt", "99"],
|
["monetary_field", "gt", "99"],
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ class TestBarcode(
|
|||||||
- No barcodes detected
|
- No barcodes detected
|
||||||
- No pages to split on
|
- No pages to split on
|
||||||
"""
|
"""
|
||||||
test_file = self.SIMPLE_PDF
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
with self.get_reader(test_file) as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
reader.detect()
|
reader.detect()
|
||||||
separator_page_numbers = reader.get_separation_pages()
|
separator_page_numbers = reader.get_separation_pages()
|
||||||
@@ -461,7 +461,7 @@ class TestBarcode(
|
|||||||
THEN:
|
THEN:
|
||||||
- Nothing happens
|
- Nothing happens
|
||||||
"""
|
"""
|
||||||
test_file = self.SIMPLE_PDF
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
with self.get_reader(test_file) as reader:
|
with self.get_reader(test_file) as reader:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -29,9 +29,6 @@ from documents.permissions import set_permissions_for_objects
|
|||||||
from paperless_testing.dirs import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
from paperless_testing.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from paperless_testing.permissions import grant_object
|
from paperless_testing.permissions import grant_object
|
||||||
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
|
|
||||||
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
|
|
||||||
from paperless_testing.samples import WITH_FORM_PDF
|
|
||||||
|
|
||||||
|
|
||||||
class TestBulkEdit(DirectoriesMixin, TestCase):
|
class TestBulkEdit(DirectoriesMixin, TestCase):
|
||||||
@@ -709,27 +706,47 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
super().setUp()
|
super().setUp()
|
||||||
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf",
|
||||||
sample1,
|
sample1,
|
||||||
)
|
)
|
||||||
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
|
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf",
|
||||||
sample1_archive,
|
sample1_archive,
|
||||||
)
|
)
|
||||||
sample2 = self.dirs.scratch_dir / "sample2.pdf"
|
sample2 = self.dirs.scratch_dir / "sample2.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
MULTI_PAGE_DIGITAL_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000002.pdf",
|
||||||
sample2,
|
sample2,
|
||||||
)
|
)
|
||||||
sample2_archive = self.dirs.archive_dir / "sample2_archive.pdf"
|
sample2_archive = self.dirs.archive_dir / "sample2_archive.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
MULTI_PAGE_DIGITAL_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000002.pdf",
|
||||||
sample2_archive,
|
sample2_archive,
|
||||||
)
|
)
|
||||||
sample3 = self.dirs.scratch_dir / "sample3.pdf"
|
sample3 = self.dirs.scratch_dir / "sample3.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
WITH_FORM_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000003.pdf",
|
||||||
sample3,
|
sample3,
|
||||||
)
|
)
|
||||||
self.doc1 = Document.objects.create(
|
self.doc1 = Document.objects.create(
|
||||||
@@ -763,7 +780,11 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
)
|
)
|
||||||
img_doc_archive = self.dirs.archive_dir / "sample_image.pdf"
|
img_doc_archive = self.dirs.archive_dir / "sample_image.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf",
|
||||||
img_doc_archive,
|
img_doc_archive,
|
||||||
)
|
)
|
||||||
self.img_doc = Document.objects.create(
|
self.img_doc = Document.objects.create(
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ from paperless_testing.factories import TagFactory
|
|||||||
from paperless_testing.factories import UserFactory
|
from paperless_testing.factories import UserFactory
|
||||||
from paperless_testing.fakes.progress import FakeProgressManager
|
from paperless_testing.fakes.progress import FakeProgressManager
|
||||||
from paperless_testing.permissions import grant_object
|
from paperless_testing.permissions import grant_object
|
||||||
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
|
|
||||||
from paperless_testing.samples import MULTI_PAGE_IMAGES_PDF
|
|
||||||
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
|
|
||||||
|
|
||||||
|
|
||||||
class _BaseNewStyleParser:
|
class _BaseNewStyleParser:
|
||||||
@@ -118,7 +115,9 @@ class _BaseNewStyleParser:
|
|||||||
|
|
||||||
|
|
||||||
class DummyParser(_BaseNewStyleParser):
|
class DummyParser(_BaseNewStyleParser):
|
||||||
_ARCHIVE_SRC = MULTI_PAGE_IMAGES_PDF
|
_ARCHIVE_SRC = (
|
||||||
|
Path(__file__).parent / "samples" / "documents" / "archive" / "0000001.pdf"
|
||||||
|
)
|
||||||
|
|
||||||
def parse(self, document_path, mime_type, *, produce_archive: bool = True) -> None:
|
def parse(self, document_path, mime_type, *, produce_archive: bool = True) -> None:
|
||||||
self._text = "The Text"
|
self._text = "The Text"
|
||||||
@@ -204,19 +203,33 @@ class TestConsumer(
|
|||||||
self.addCleanup(patcher.stop)
|
self.addCleanup(patcher.stop)
|
||||||
|
|
||||||
def get_test_file(self):
|
def get_test_file(self):
|
||||||
src = SIMPLE_DIGITAL_PDF
|
src = (
|
||||||
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf"
|
||||||
|
)
|
||||||
dst = self.dirs.scratch_dir / "sample.pdf"
|
dst = self.dirs.scratch_dir / "sample.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
return dst
|
return dst
|
||||||
|
|
||||||
def get_test_file2(self):
|
def get_test_file2(self):
|
||||||
src = MULTI_PAGE_DIGITAL_PDF
|
src = (
|
||||||
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000002.pdf"
|
||||||
|
)
|
||||||
dst = self.dirs.scratch_dir / "sample2.pdf"
|
dst = self.dirs.scratch_dir / "sample2.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
return dst
|
return dst
|
||||||
|
|
||||||
def get_test_archive_file(self):
|
def get_test_archive_file(self):
|
||||||
src = MULTI_PAGE_IMAGES_PDF
|
src = (
|
||||||
|
Path(__file__).parent / "samples" / "documents" / "archive" / "0000001.pdf"
|
||||||
|
)
|
||||||
dst = self.dirs.scratch_dir / "sample_archive.pdf"
|
dst = self.dirs.scratch_dir / "sample_archive.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
return dst
|
return dst
|
||||||
@@ -1111,7 +1124,7 @@ class TestConsumer(
|
|||||||
@mock.patch("documents.consumer.get_parser_registry")
|
@mock.patch("documents.consumer.get_parser_registry")
|
||||||
def test_similar_filenames(self, m) -> None:
|
def test_similar_filenames(self, m) -> None:
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent / "samples" / "simple.pdf",
|
||||||
settings.CONSUMPTION_DIR / "simple.pdf",
|
settings.CONSUMPTION_DIR / "simple.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
@@ -1652,7 +1665,13 @@ class TestConsumerRemoteOCR(
|
|||||||
self.addCleanup(patcher.stop)
|
self.addCleanup(patcher.stop)
|
||||||
|
|
||||||
def _consume(self, *, overrides: DocumentMetadataOverrides | None = None) -> bool:
|
def _consume(self, *, overrides: DocumentMetadataOverrides | None = None) -> bool:
|
||||||
src = SIMPLE_DIGITAL_PDF
|
src = (
|
||||||
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf"
|
||||||
|
)
|
||||||
dst = self.dirs.scratch_dir / "sample.pdf"
|
dst = self.dirs.scratch_dir / "sample.pdf"
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
|
|||||||
@@ -37,15 +37,12 @@ class TestDoubleSided(
|
|||||||
self.double_sided_dir.mkdir()
|
self.double_sided_dir.mkdir()
|
||||||
self.staging_file = self.dirs.scratch_dir / STAGING_FILE_NAME
|
self.staging_file = self.dirs.scratch_dir / STAGING_FILE_NAME
|
||||||
|
|
||||||
def _sample(self, name: str) -> Path:
|
|
||||||
return self.SIMPLE_PDF if name == "simple.pdf" else self.SAMPLE_DIR / name
|
|
||||||
|
|
||||||
def consume_file(self, srcname, dstname: str | Path = "foo.pdf"):
|
def consume_file(self, srcname, dstname: str | Path = "foo.pdf"):
|
||||||
"""
|
"""
|
||||||
Starts the consume process and also ensures the
|
Starts the consume process and also ensures the
|
||||||
destination file does not exist afterwards
|
destination file does not exist afterwards
|
||||||
"""
|
"""
|
||||||
src = self._sample(srcname)
|
src = self.SAMPLE_DIR / srcname
|
||||||
dst = self.double_sided_dir / dstname
|
dst = self.double_sided_dir / dstname
|
||||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
shutil.copy(src, dst)
|
shutil.copy(src, dst)
|
||||||
@@ -60,7 +57,7 @@ class TestDoubleSided(
|
|||||||
return msg
|
return msg
|
||||||
|
|
||||||
def create_staging_file(self, src="double-sided-odd.pdf", datetime=None) -> None:
|
def create_staging_file(self, src="double-sided-odd.pdf", datetime=None) -> None:
|
||||||
shutil.copy(self._sample(src), self.staging_file)
|
shutil.copy(self.SAMPLE_DIR / src, self.staging_file)
|
||||||
if datetime is None:
|
if datetime is None:
|
||||||
datetime = dt.datetime.now()
|
datetime = dt.datetime.now()
|
||||||
os.utime(str(self.staging_file), (datetime.timestamp(),) * 2)
|
os.utime(str(self.staging_file), (datetime.timestamp(),) * 2)
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ from documents.tasks import update_document_content_maybe_archive_file
|
|||||||
from paperless_testing.assertions import FileSystemAssertsMixin
|
from paperless_testing.assertions import FileSystemAssertsMixin
|
||||||
from paperless_testing.dirs import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
from paperless_testing.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
|
|
||||||
|
|
||||||
sample_file: Path = SIMPLE_DIGITAL_PDF
|
sample_file: Path = Path(__file__).parent / "samples" / "simple.pdf"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ from paperless_testing.dirs import paperless_environment
|
|||||||
from paperless_testing.factories import DocumentBarcodeFactory
|
from paperless_testing.factories import DocumentBarcodeFactory
|
||||||
from paperless_testing.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from paperless_testing.permissions import grant_object
|
from paperless_testing.permissions import grant_object
|
||||||
from paperless_testing.samples import install_document_samples
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
@@ -205,7 +204,10 @@ class TestExportImport(
|
|||||||
|
|
||||||
def test_exporter(self, *, use_filename_format=False) -> None:
|
def test_exporter(self, *, use_filename_format=False) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
num_permission_objects = Permission.objects.count()
|
num_permission_objects = Permission.objects.count()
|
||||||
|
|
||||||
@@ -309,7 +311,10 @@ class TestExportImport(
|
|||||||
|
|
||||||
def test_exporter_with_filename_format(self) -> None:
|
def test_exporter_with_filename_format(self) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
FILENAME_FORMAT="{created_year}/{correspondent}/{title}",
|
FILENAME_FORMAT="{created_year}/{correspondent}/{title}",
|
||||||
@@ -318,7 +323,10 @@ class TestExportImport(
|
|||||||
|
|
||||||
def test_exporter_includes_share_links_and_bundles(self) -> None:
|
def test_exporter_includes_share_links_and_bundles(self) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
share_link = ShareLink.objects.create(
|
share_link = ShareLink.objects.create(
|
||||||
slug="share-link-slug",
|
slug="share-link-slug",
|
||||||
@@ -417,7 +425,10 @@ class TestExportImport(
|
|||||||
|
|
||||||
def test_update_export_changed_time(self) -> None:
|
def test_update_export_changed_time(self) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
self._do_export()
|
self._do_export()
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile(self.target / "manifest.json")
|
||||||
@@ -453,7 +464,10 @@ class TestExportImport(
|
|||||||
|
|
||||||
def test_update_export_changed_checksum(self) -> None:
|
def test_update_export_changed_checksum(self) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
self._do_export()
|
self._do_export()
|
||||||
|
|
||||||
@@ -480,7 +494,10 @@ class TestExportImport(
|
|||||||
|
|
||||||
def test_update_export_deleted_document(self) -> None:
|
def test_update_export_deleted_document(self) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
manifest = self._do_export()
|
manifest = self._do_export()
|
||||||
|
|
||||||
@@ -512,7 +529,10 @@ class TestExportImport(
|
|||||||
@override_settings(FILENAME_FORMAT="{title}/{correspondent}")
|
@override_settings(FILENAME_FORMAT="{title}/{correspondent}")
|
||||||
def test_update_export_changed_location(self) -> None:
|
def test_update_export_changed_location(self) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
self._do_export(use_filename_format=True)
|
self._do_export(use_filename_format=True)
|
||||||
self.assertIsFile(self.target / "wow1" / "c.pdf")
|
self.assertIsFile(self.target / "wow1" / "c.pdf")
|
||||||
@@ -554,7 +574,10 @@ class TestExportImport(
|
|||||||
- Zipfile contains exported files
|
- Zipfile contains exported files
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
args = ["document_exporter", self.target, "--zip"]
|
args = ["document_exporter", self.target, "--zip"]
|
||||||
|
|
||||||
@@ -583,7 +606,10 @@ class TestExportImport(
|
|||||||
- Zipfile contains exported files
|
- Zipfile contains exported files
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "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"]
|
||||||
|
|
||||||
@@ -619,7 +645,10 @@ class TestExportImport(
|
|||||||
- The existing file and directory in target are removed
|
- The existing file and directory in target are removed
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
# Create stuff in target directory
|
# Create stuff in target directory
|
||||||
existing_file = self.target / "test.txt"
|
existing_file = self.target / "test.txt"
|
||||||
@@ -715,7 +744,10 @@ class TestExportImport(
|
|||||||
- Documents can be imported again
|
- Documents can be imported again
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
manifest = self._do_export()
|
manifest = self._do_export()
|
||||||
has_archive = False
|
has_archive = False
|
||||||
@@ -758,7 +790,10 @@ class TestExportImport(
|
|||||||
- Documents can be imported again
|
- Documents can be imported again
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
manifest = self._do_export()
|
manifest = self._do_export()
|
||||||
has_thumbnail = False
|
has_thumbnail = False
|
||||||
@@ -803,7 +838,10 @@ class TestExportImport(
|
|||||||
- Documents can be imported again
|
- Documents can be imported again
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
manifest = self._do_export(split_manifest=True)
|
manifest = self._do_export(split_manifest=True)
|
||||||
has_document = False
|
has_document = False
|
||||||
@@ -828,7 +866,10 @@ class TestExportImport(
|
|||||||
|
|
||||||
def _export_import_barcodes(self, *, split_manifest: bool) -> None:
|
def _export_import_barcodes(self, *, split_manifest: bool) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
DocumentBarcodeFactory(document=self.d1, value="https://example.com")
|
DocumentBarcodeFactory(document=self.d1, value="https://example.com")
|
||||||
DocumentBarcodeFactory(document=self.d2, page=2, value="DE8937")
|
DocumentBarcodeFactory(document=self.d2, page=2, value="DE8937")
|
||||||
|
|
||||||
@@ -864,7 +905,10 @@ class TestExportImport(
|
|||||||
- Documents can be imported again
|
- Documents can be imported again
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
self._do_export(use_folder_prefix=True)
|
self._do_export(use_folder_prefix=True)
|
||||||
|
|
||||||
@@ -891,7 +935,10 @@ class TestExportImport(
|
|||||||
- Documents can be imported again
|
- Documents can be imported again
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
self._do_export(use_folder_prefix=True, split_manifest=True)
|
self._do_export(use_folder_prefix=True, split_manifest=True)
|
||||||
|
|
||||||
@@ -918,7 +965,10 @@ class TestExportImport(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
num_content_type_objects = ContentType.objects.count()
|
num_content_type_objects = ContentType.objects.count()
|
||||||
num_permission_objects = Permission.objects.count()
|
num_permission_objects = Permission.objects.count()
|
||||||
@@ -955,7 +1005,10 @@ class TestExportImport(
|
|||||||
|
|
||||||
def test_exporter_with_auditlog_disabled(self) -> None:
|
def test_exporter_with_auditlog_disabled(self) -> None:
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
AUDIT_LOG_ENABLED=False,
|
AUDIT_LOG_ENABLED=False,
|
||||||
@@ -975,7 +1028,10 @@ class TestExportImport(
|
|||||||
survive the round-trip with deleted_at preserved
|
survive the round-trip with deleted_at preserved
|
||||||
"""
|
"""
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
install_document_samples(Path(self.dirs.media_dir) / "documents")
|
shutil.copytree(
|
||||||
|
Path(__file__).parent / "samples" / "documents",
|
||||||
|
Path(self.dirs.media_dir) / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
# d1 has self.note and self.cfi1 attached via setUp
|
# d1 has self.note and self.cfi1 attached via setUp
|
||||||
self.d1.delete()
|
self.d1.delete()
|
||||||
@@ -1019,7 +1075,10 @@ class TestExportImport(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
shutil.rmtree(self.dirs.media_dir / "documents")
|
shutil.rmtree(self.dirs.media_dir / "documents")
|
||||||
install_document_samples(self.dirs.media_dir / "documents")
|
shutil.copytree(
|
||||||
|
self.SAMPLE_DIR / "documents",
|
||||||
|
self.dirs.media_dir / "documents",
|
||||||
|
)
|
||||||
|
|
||||||
_ = self._do_export(data_only=True)
|
_ = self._do_export(data_only=True)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from documents.models import Document
|
|||||||
from documents.parsers import get_default_thumbnail
|
from documents.parsers import get_default_thumbnail
|
||||||
from paperless_testing.assertions import FileSystemAssertsMixin
|
from paperless_testing.assertions import FileSystemAssertsMixin
|
||||||
from paperless_testing.dirs import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
@@ -25,7 +24,7 @@ class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
filename="test.pdf",
|
filename="test.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent / "samples" / "simple.pdf",
|
||||||
self.d1.source_path,
|
self.d1.source_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ class TestMakeThumbnails(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
filename="test2.pdf",
|
filename="test2.pdf",
|
||||||
)
|
)
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent / "samples" / "simple.pdf",
|
||||||
self.d2.source_path,
|
self.d2.source_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ import pytest
|
|||||||
|
|
||||||
from documents import pdf_ops
|
from documents import pdf_ops
|
||||||
from documents.pdf_ops import PageSpec
|
from documents.pdf_ops import PageSpec
|
||||||
from paperless_testing.samples import MULTI_PAGE_DIGITAL_PDF
|
|
||||||
|
|
||||||
SRC_ROOT = Path(__file__).parents[2]
|
SRC_ROOT = Path(__file__).parents[2]
|
||||||
SAMPLES = Path(__file__).parent / "samples"
|
SAMPLES = Path(__file__).parent / "samples"
|
||||||
THREE_PAGES = MULTI_PAGE_DIGITAL_PDF
|
THREE_PAGES = SAMPLES / "documents" / "originals" / "0000002.pdf"
|
||||||
TWELVE_PAGES = SAMPLES / "barcodes" / "split-by-asn-2.pdf"
|
TWELVE_PAGES = SAMPLES / "barcodes" / "split-by-asn-2.pdf"
|
||||||
ENCRYPTED = SAMPLES / "password-is-test.pdf"
|
ENCRYPTED = SAMPLES / "password-is-test.pdf"
|
||||||
SIGNED = SRC_ROOT / "paperless" / "tests" / "samples" / "tesseract" / "signed.pdf"
|
SIGNED = SRC_ROOT / "paperless" / "tests" / "samples" / "tesseract" / "signed.pdf"
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
from django.core.exceptions import FieldDoesNotExist
|
||||||
|
from django.db import connection
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.utils import CaptureQueriesContext
|
||||||
|
|
||||||
|
from documents.management.commands.document_importer import _deserialize_record
|
||||||
|
from documents.models import Tag
|
||||||
|
from documents.tag_hierarchy import TagHierarchy
|
||||||
|
from paperless_testing.migrations import TestMigrations
|
||||||
|
|
||||||
|
|
||||||
|
class TestTagAdjacency(TestCase):
|
||||||
|
def test_held_instance_uses_current_parent_edges(self):
|
||||||
|
first = Tag.objects.create(name="First")
|
||||||
|
second = Tag.objects.create(name="Second")
|
||||||
|
child = Tag.objects.create(name="Child", tn_parent=first)
|
||||||
|
Tag.objects.filter(pk=child.pk).update(tn_parent=second)
|
||||||
|
assert first.get_descendants_pks() == set()
|
||||||
|
assert second.get_descendants_pks() == {child.pk}
|
||||||
|
|
||||||
|
def test_queryset_delete_preserves_cascade(self):
|
||||||
|
root = Tag.objects.create(name="Root")
|
||||||
|
child = Tag.objects.create(name="Child", tn_parent=root)
|
||||||
|
Tag.objects.create(name="Grandchild", tn_parent=child)
|
||||||
|
survivor = Tag.objects.create(name="Other root")
|
||||||
|
Tag.objects.filter(pk=root.pk).delete()
|
||||||
|
assert list(Tag.objects.values_list("pk", flat=True)) == [survivor.pk]
|
||||||
|
|
||||||
|
def test_flat_writes_do_not_update_other_tags(self):
|
||||||
|
Tag.objects.bulk_create([Tag(name=f"Tag {i}") for i in range(100)])
|
||||||
|
with CaptureQueriesContext(connection) as queries:
|
||||||
|
new = Tag.objects.create(name="New")
|
||||||
|
assert not any(q["sql"].startswith("UPDATE") for q in queries)
|
||||||
|
with CaptureQueriesContext(connection) as queries:
|
||||||
|
new.name = "Renamed"
|
||||||
|
new.save()
|
||||||
|
tag_updates = [
|
||||||
|
q["sql"] for q in queries if q["sql"].startswith('UPDATE "documents_tag"')
|
||||||
|
]
|
||||||
|
assert len(tag_updates) == 1
|
||||||
|
assert 'WHERE "documents_tag"."id" =' in tag_updates[0]
|
||||||
|
|
||||||
|
def test_batch_snapshot_is_one_query_and_includes_overlapping_roots(self):
|
||||||
|
root = Tag.objects.create(name="Root")
|
||||||
|
child = Tag.objects.create(name="Child", tn_parent=root)
|
||||||
|
leaf = Tag.objects.create(name="Leaf", tn_parent=child)
|
||||||
|
with self.assertNumQueries(1):
|
||||||
|
hierarchy = Tag.get_hierarchy()
|
||||||
|
assert hierarchy.with_ancestors([leaf.pk]) == {root.pk, child.pk, leaf.pk}
|
||||||
|
assert hierarchy.with_descendants([root.pk, child.pk]) == {
|
||||||
|
root.pk,
|
||||||
|
child.pk,
|
||||||
|
leaf.pk,
|
||||||
|
}
|
||||||
|
assert hierarchy.height(None) == 0
|
||||||
|
|
||||||
|
def test_legacy_export_fields_are_ignored_but_parent_is_preserved(self):
|
||||||
|
model, tag, m2m = _deserialize_record(
|
||||||
|
{
|
||||||
|
"model": "documents.tag",
|
||||||
|
"pk": 2,
|
||||||
|
"fields": {
|
||||||
|
"name": "Child",
|
||||||
|
"tn_parent": 1,
|
||||||
|
"tn_order": 3,
|
||||||
|
"tn_siblings_pks": "4,5",
|
||||||
|
"tn_descendants_pks": "6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert model is Tag
|
||||||
|
assert tag.tn_parent_id == 1
|
||||||
|
assert tag.name == "Child"
|
||||||
|
assert not m2m
|
||||||
|
with self.assertRaises(FieldDoesNotExist):
|
||||||
|
_deserialize_record(
|
||||||
|
{"model": "documents.tag", "pk": 3, "fields": {"tn_typo": "bad"}},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_malformed_cycles_terminate(self):
|
||||||
|
hierarchy = TagHierarchy([(1, 2), (2, 1)])
|
||||||
|
assert hierarchy.with_descendants([1]) == {1, 2}
|
||||||
|
assert set(hierarchy.ancestors(2)) == {1, 2}
|
||||||
|
|
||||||
|
|
||||||
|
class TestTagAdjacencyMigration(TestMigrations):
|
||||||
|
migrate_from = "0027_documentbarcode"
|
||||||
|
migrate_to = "0028_replace_tag_tree_fields"
|
||||||
|
|
||||||
|
def setUpBeforeMigration(self, apps):
|
||||||
|
tag = apps.get_model("documents", "Tag")
|
||||||
|
root = tag.objects.create(name="Root")
|
||||||
|
child = tag.objects.create(name="Child", tn_parent=root)
|
||||||
|
self.edges = [(root.pk, None), (child.pk, root.pk)]
|
||||||
|
|
||||||
|
def test_parent_edges_survive(self):
|
||||||
|
tag = self.apps.get_model("documents", "Tag")
|
||||||
|
assert (
|
||||||
|
list(tag.objects.order_by("pk").values_list("pk", "tn_parent_id"))
|
||||||
|
== self.edges
|
||||||
|
)
|
||||||
|
assert {
|
||||||
|
field.name for field in tag._meta.fields if field.name.startswith("tn_")
|
||||||
|
} == {"tn_parent"}
|
||||||
@@ -21,7 +21,6 @@ from documents.tests.helpers import dummy_preprocess
|
|||||||
from paperless_testing.assertions import FileSystemAssertsMixin
|
from paperless_testing.assertions import FileSystemAssertsMixin
|
||||||
from paperless_testing.dirs import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
from paperless_testing.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -230,12 +229,20 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf",
|
||||||
sample1,
|
sample1,
|
||||||
)
|
)
|
||||||
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
|
sample1_archive = self.dirs.archive_dir / "sample_archive.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf",
|
||||||
sample1_archive,
|
sample1_archive,
|
||||||
)
|
)
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@@ -263,7 +270,11 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
|
|||||||
"""
|
"""
|
||||||
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
||||||
shutil.copy(
|
shutil.copy(
|
||||||
SIMPLE_DIGITAL_PDF,
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf",
|
||||||
sample1,
|
sample1,
|
||||||
)
|
)
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@@ -279,7 +290,14 @@ class TestUpdateContent(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
def _create_root_with_version(self) -> tuple[Document, Document]:
|
def _create_root_with_version(self) -> tuple[Document, Document]:
|
||||||
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
sample1 = self.dirs.scratch_dir / "sample.pdf"
|
||||||
shutil.copy(SIMPLE_DIGITAL_PDF, sample1)
|
shutil.copy(
|
||||||
|
Path(__file__).parent
|
||||||
|
/ "samples"
|
||||||
|
/ "documents"
|
||||||
|
/ "originals"
|
||||||
|
/ "0000001.pdf",
|
||||||
|
sample1,
|
||||||
|
)
|
||||||
root = DocumentFactory(content="root content", mime_type="application/pdf")
|
root = DocumentFactory(content="root content", mime_type="application/pdf")
|
||||||
version = DocumentFactory(
|
version = DocumentFactory(
|
||||||
content="my document",
|
content="my document",
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ class TestWorkflows(
|
|||||||
self.assertEqual(action.__str__(), "WorkflowAction 1")
|
self.assertEqual(action.__str__(), "WorkflowAction 1")
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -290,7 +290,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -409,7 +409,7 @@ class TestWorkflows(
|
|||||||
w2.save()
|
w2.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -531,7 +531,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -608,7 +608,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -687,7 +687,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -765,7 +765,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -876,7 +876,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -928,7 +928,7 @@ class TestWorkflows(
|
|||||||
generated = generate_unique_filename(doc)
|
generated = generate_unique_filename(doc)
|
||||||
destination = (settings.ORIGINALS_DIR / generated).resolve()
|
destination = (settings.ORIGINALS_DIR / generated).resolve()
|
||||||
create_source_path_directory(destination)
|
create_source_path_directory(destination)
|
||||||
shutil.copy(self.SIMPLE_PDF, destination)
|
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
|
||||||
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
|
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
|
||||||
doc.refresh_from_db()
|
doc.refresh_from_db()
|
||||||
|
|
||||||
@@ -1846,50 +1846,6 @@ class TestWorkflows(
|
|||||||
expected_str = f"Document storage path {doc.storage_path} does not match {trigger.filter_has_storage_path}"
|
expected_str = f"Document storage path {doc.storage_path} does not match {trigger.filter_has_storage_path}"
|
||||||
self.assertIn(expected_str, cm.output[1])
|
self.assertIn(expected_str, cm.output[1])
|
||||||
|
|
||||||
def test_title_custom_fields(self) -> None:
|
|
||||||
from documents.consumer import ConsumerPlugin
|
|
||||||
from documents.templating.workflows import validate_workflow_template
|
|
||||||
|
|
||||||
field = CustomField.objects.create(
|
|
||||||
name="Invoice",
|
|
||||||
data_type=CustomField.FieldDataType.STRING,
|
|
||||||
)
|
|
||||||
template = '{{ custom_fields.Invoice.value }} {{ custom_fields | get_cf_value("Missing", "fallback") }}'
|
|
||||||
validate_workflow_template(template)
|
|
||||||
consumer = mock.Mock(
|
|
||||||
filename="invoice.pdf",
|
|
||||||
metadata=DocumentMetadataOverrides(custom_fields={field.pk: "INV-123"}),
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
ConsumerPlugin._parse_title_placeholders(consumer, template),
|
|
||||||
"INV-123 fallback",
|
|
||||||
)
|
|
||||||
trigger = WorkflowTrigger.objects.create(
|
|
||||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
|
||||||
)
|
|
||||||
action = WorkflowAction.objects.create(assign_title=template)
|
|
||||||
doc = Document.objects.create(title="Original")
|
|
||||||
action.assign_custom_fields.add(field)
|
|
||||||
action.assign_custom_fields_values = {str(field.pk): "INV-123"}
|
|
||||||
action.save()
|
|
||||||
workflow = Workflow.objects.create(name="Custom field title", order=0)
|
|
||||||
workflow.triggers.add(trigger)
|
|
||||||
workflow.actions.add(action)
|
|
||||||
|
|
||||||
document_consumption_finished.send(sender=self.__class__, document=doc)
|
|
||||||
|
|
||||||
doc.refresh_from_db()
|
|
||||||
self.assertEqual(doc.title, "INV-123 fallback")
|
|
||||||
|
|
||||||
# Existing fields remain available when the action assigns no custom fields.
|
|
||||||
action.assign_custom_fields.clear()
|
|
||||||
CustomFieldInstance.objects.filter(document=doc, field=field).update(
|
|
||||||
value_text="INV-456",
|
|
||||||
)
|
|
||||||
document_consumption_finished.send(sender=self.__class__, document=doc)
|
|
||||||
doc.refresh_from_db()
|
|
||||||
self.assertEqual(doc.title, "INV-456 fallback")
|
|
||||||
|
|
||||||
def test_document_added_invalid_title_placeholders(self) -> None:
|
def test_document_added_invalid_title_placeholders(self) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
@@ -2067,7 +2023,7 @@ class TestWorkflows(
|
|||||||
superuser = UserFactory(username="superuser", superuser=True)
|
superuser = UserFactory(username="superuser", superuser=True)
|
||||||
self.client.force_authenticate(user=superuser)
|
self.client.force_authenticate(user=superuser)
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
tasks.consume_file(
|
tasks.consume_file(
|
||||||
@@ -3053,7 +3009,7 @@ class TestWorkflows(
|
|||||||
generated = generate_unique_filename(doc)
|
generated = generate_unique_filename(doc)
|
||||||
destination = (settings.ORIGINALS_DIR / generated).resolve()
|
destination = (settings.ORIGINALS_DIR / generated).resolve()
|
||||||
create_source_path_directory(destination)
|
create_source_path_directory(destination)
|
||||||
shutil.copy(self.SIMPLE_PDF, destination)
|
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
|
||||||
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
|
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
|
||||||
doc.refresh_from_db()
|
doc.refresh_from_db()
|
||||||
doc.tags.set([self.t1, self.t2])
|
doc.tags.set([self.t1, self.t2])
|
||||||
@@ -3117,7 +3073,7 @@ class TestWorkflows(
|
|||||||
generated = generate_unique_filename(doc)
|
generated = generate_unique_filename(doc)
|
||||||
destination = (settings.ORIGINALS_DIR / generated).resolve()
|
destination = (settings.ORIGINALS_DIR / generated).resolve()
|
||||||
create_source_path_directory(destination)
|
create_source_path_directory(destination)
|
||||||
shutil.copy(self.SIMPLE_PDF, destination)
|
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
|
||||||
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
|
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
|
||||||
doc.refresh_from_db()
|
doc.refresh_from_db()
|
||||||
doc.tags.set([self.t1])
|
doc.tags.set([self.t1])
|
||||||
@@ -3266,7 +3222,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3390,7 +3346,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3540,7 +3496,7 @@ class TestWorkflows(
|
|||||||
workflow.actions.set([assignment_action, email_action])
|
workflow.actions.set([assignment_action, email_action])
|
||||||
|
|
||||||
temp_working_copy = shutil.copy(
|
temp_working_copy = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "working-copy.pdf",
|
self.dirs.scratch_dir / "working-copy.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3640,7 +3596,7 @@ class TestWorkflows(
|
|||||||
|
|
||||||
# move the file
|
# move the file
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3748,7 +3704,7 @@ class TestWorkflows(
|
|||||||
generated = generate_unique_filename(doc)
|
generated = generate_unique_filename(doc)
|
||||||
destination = (settings.ORIGINALS_DIR / generated).resolve()
|
destination = (settings.ORIGINALS_DIR / generated).resolve()
|
||||||
create_source_path_directory(destination)
|
create_source_path_directory(destination)
|
||||||
shutil.copy(self.SIMPLE_PDF, destination)
|
shutil.copy(self.SAMPLE_DIR / "simple.pdf", destination)
|
||||||
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
|
Document.objects.filter(pk=doc.pk).update(filename=generated.as_posix())
|
||||||
|
|
||||||
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc)
|
||||||
@@ -3959,7 +3915,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4080,7 +4036,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4431,7 +4387,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -4879,7 +4835,7 @@ class TestWorkflows(
|
|||||||
w.save()
|
w.save()
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5071,7 +5027,7 @@ class TestWorkflows(
|
|||||||
|
|
||||||
# Create a test file to be consumed
|
# Create a test file to be consumed
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
test_file_path = Path(test_file)
|
test_file_path = Path(test_file)
|
||||||
@@ -5135,7 +5091,7 @@ class TestWorkflows(
|
|||||||
|
|
||||||
# Create a test file to be consumed
|
# Create a test file to be consumed
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple2.pdf",
|
self.dirs.scratch_dir / "simple2.pdf",
|
||||||
)
|
)
|
||||||
test_file_path = Path(test_file)
|
test_file_path = Path(test_file)
|
||||||
@@ -5164,72 +5120,6 @@ class TestWorkflows(
|
|||||||
self.assertEqual(Document.objects.count(), 0)
|
self.assertEqual(Document.objects.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
@pytest.mark.parametrize("use_overrides", [False, True])
|
|
||||||
def test_custom_fields_in_email_and_webhook_templates(
|
|
||||||
use_overrides,
|
|
||||||
settings,
|
|
||||||
tmp_path,
|
|
||||||
):
|
|
||||||
from documents.workflows.actions import build_workflow_action_context
|
|
||||||
from documents.workflows.actions import execute_email_action
|
|
||||||
from documents.workflows.actions import execute_webhook_action
|
|
||||||
|
|
||||||
settings.EMAIL_ENABLED = True
|
|
||||||
field = CustomField.objects.create(
|
|
||||||
name="Invoice",
|
|
||||||
data_type=CustomField.FieldDataType.STRING,
|
|
||||||
)
|
|
||||||
if use_overrides:
|
|
||||||
invoice = tmp_path / "invoice.pdf"
|
|
||||||
invoice.write_bytes(b"%PDF-1.7")
|
|
||||||
document = ConsumableDocument(
|
|
||||||
source=DocumentSource.ConsumeFolder,
|
|
||||||
original_file=invoice,
|
|
||||||
)
|
|
||||||
overrides = DocumentMetadataOverrides(custom_fields={field.pk: "INV-123"})
|
|
||||||
else:
|
|
||||||
document = Document.objects.create(title="Invoice")
|
|
||||||
CustomFieldInstance.objects.create(
|
|
||||||
document=document,
|
|
||||||
field=field,
|
|
||||||
value_text="INV-123",
|
|
||||||
)
|
|
||||||
overrides = None
|
|
||||||
context = build_workflow_action_context(document, overrides)
|
|
||||||
template = '{{ custom_fields.Invoice.value }} {{ custom_fields | get_cf_value("Missing", "fallback") }}'
|
|
||||||
action = WorkflowAction(
|
|
||||||
email=WorkflowActionEmail(
|
|
||||||
subject=template,
|
|
||||||
body=template,
|
|
||||||
to="test@example.com",
|
|
||||||
),
|
|
||||||
webhook=WorkflowActionWebhook(
|
|
||||||
url="https://example.com",
|
|
||||||
body=template,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
with mock.patch("documents.workflows.actions.send_email") as send_email:
|
|
||||||
execute_email_action(
|
|
||||||
action,
|
|
||||||
document,
|
|
||||||
context,
|
|
||||||
None,
|
|
||||||
Path("invoice.pdf"),
|
|
||||||
WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
|
||||||
)
|
|
||||||
assert send_email.call_args.kwargs["subject"] == "INV-123 fallback"
|
|
||||||
assert send_email.call_args.kwargs["body"] == "INV-123 fallback"
|
|
||||||
with mock.patch("documents.workflows.actions.send_webhook.apply_async") as send:
|
|
||||||
for use_params in (False, True):
|
|
||||||
action.webhook.use_params = use_params
|
|
||||||
action.webhook.params = {"invoice": template}
|
|
||||||
execute_webhook_action(action, document, context, None, Path("invoice.pdf"))
|
|
||||||
assert send.call_args.kwargs["kwargs"]["data"] == (
|
|
||||||
{"invoice": "INV-123 fallback"} if use_params else "INV-123 fallback"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestWebhookSend:
|
class TestWebhookSend:
|
||||||
def test_send_webhook_data_or_json(
|
def test_send_webhook_data_or_json(
|
||||||
self,
|
self,
|
||||||
@@ -5639,7 +5529,7 @@ class TestDateWorkflowLocalization(
|
|||||||
)
|
)
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
tmp_path / "simple.pdf",
|
tmp_path / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -5706,7 +5596,7 @@ class TestRemoteOCRWorkflowAction(DirectoriesMixin, SampleDirMixin, APITestCase)
|
|||||||
self._make_workflow(WorkflowTrigger.WorkflowTriggerType.CONSUMPTION)
|
self._make_workflow(WorkflowTrigger.WorkflowTriggerType.CONSUMPTION)
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
overrides = DocumentMetadataOverrides()
|
overrides = DocumentMetadataOverrides()
|
||||||
@@ -5924,7 +5814,7 @@ class TestApplyAISuggestionsWorkflowAction(
|
|||||||
)
|
)
|
||||||
|
|
||||||
test_file = shutil.copy(
|
test_file = shutil.copy(
|
||||||
self.SIMPLE_PDF,
|
self.SAMPLE_DIR / "simple.pdf",
|
||||||
self.dirs.scratch_dir / "simple.pdf",
|
self.dirs.scratch_dir / "simple.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from documents.data_models import ConsumableDocument
|
|||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from paperless_testing.fakes.progress import FakeProgressManager
|
from paperless_testing.fakes.progress import FakeProgressManager
|
||||||
from paperless_testing.samples import SIMPLE_DIGITAL_PDF
|
|
||||||
|
|
||||||
|
|
||||||
class ConsumeTaskMixin:
|
class ConsumeTaskMixin:
|
||||||
@@ -54,8 +53,6 @@ class SampleDirMixin:
|
|||||||
|
|
||||||
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
|
BARCODE_SAMPLE_DIR = SAMPLE_DIR / "barcodes"
|
||||||
|
|
||||||
SIMPLE_PDF = SIMPLE_DIGITAL_PDF
|
|
||||||
|
|
||||||
|
|
||||||
class GetConsumerMixin:
|
class GetConsumerMixin:
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
|||||||
Loaded 100 of 166 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user