mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-07 19:08:32 -05:00
Compare commits
1 Commits
dependabot
...
more-info-
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1b6a9d3816 |
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -15,7 +15,6 @@ env:
|
|||||||
DEFAULT_UV_VERSION: "0.8.x"
|
DEFAULT_UV_VERSION: "0.8.x"
|
||||||
# This is the default version of Python to use in most steps which aren't specific
|
# This is the default version of Python to use in most steps which aren't specific
|
||||||
DEFAULT_PYTHON_VERSION: "3.11"
|
DEFAULT_PYTHON_VERSION: "3.11"
|
||||||
NLTK_DATA: "/usr/share/nltk_data"
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-commit:
|
pre-commit:
|
||||||
# We want to run on external PRs, but not on our own internal PRs as they'll be run
|
# We want to run on external PRs, but not on our own internal PRs as they'll be run
|
||||||
@@ -122,11 +121,8 @@ jobs:
|
|||||||
- name: List installed Python dependencies
|
- name: List installed Python dependencies
|
||||||
run: |
|
run: |
|
||||||
uv pip list
|
uv pip list
|
||||||
- name: Install or update NLTK dependencies
|
|
||||||
run: uv run python -m nltk.downloader punkt punkt_tab snowball_data stopwords -d ${{ env.NLTK_DATA }}
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
env:
|
env:
|
||||||
NLTK_DATA: ${{ env.NLTK_DATA }}
|
|
||||||
PAPERLESS_CI_TEST: 1
|
PAPERLESS_CI_TEST: 1
|
||||||
# Enable paperless_mail testing against real server
|
# Enable paperless_mail testing against real server
|
||||||
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
||||||
|
@@ -31,7 +31,7 @@ repos:
|
|||||||
rev: v2.4.1
|
rev: v2.4.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: codespell
|
- id: codespell
|
||||||
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)|(^src/documents/tests/samples/)"
|
exclude: "(^src-ui/src/locale/)|(^src-ui/pnpm-lock.yaml)|(^src-ui/e2e/)|(^src/paperless_mail/tests/samples/)"
|
||||||
exclude_types:
|
exclude_types:
|
||||||
- pofile
|
- pofile
|
||||||
- json
|
- json
|
||||||
|
@@ -179,14 +179,10 @@ following:
|
|||||||
|
|
||||||
### Database Upgrades
|
### Database Upgrades
|
||||||
|
|
||||||
In general, Paperless-ngx supports current version of PostgreSQL and MariaDB and it is generally
|
In general, paperless does not require a specific version of PostgreSQL or MariaDB and it is
|
||||||
safe to update them to newer versions. However, you should always take a backup and follow
|
safe to update them to newer versions. However, you should always take a backup and follow
|
||||||
the instructions from your database's documentation for how to upgrade between major versions.
|
the instructions from your database's documentation for how to upgrade between major versions.
|
||||||
|
|
||||||
!!! note
|
|
||||||
|
|
||||||
As of Paperless-ngx v2.18, the minimum supported version of PostgreSQL is 13.
|
|
||||||
|
|
||||||
For PostgreSQL, refer to [Upgrading a PostgreSQL Cluster](https://www.postgresql.org/docs/current/upgrading.html).
|
For PostgreSQL, refer to [Upgrading a PostgreSQL Cluster](https://www.postgresql.org/docs/current/upgrading.html).
|
||||||
|
|
||||||
For MariaDB, refer to [Upgrading MariaDB](https://mariadb.com/kb/en/upgrading/)
|
For MariaDB, refer to [Upgrading MariaDB](https://mariadb.com/kb/en/upgrading/)
|
||||||
|
@@ -23,22 +23,22 @@ dependencies = [
|
|||||||
"dateparser~=1.2",
|
"dateparser~=1.2",
|
||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
"django~=5.2.5",
|
"django~=5.1.7",
|
||||||
"django-allauth[socialaccount,mfa]~=65.4.0",
|
"django-allauth[socialaccount,mfa]~=65.4.0",
|
||||||
"django-auditlog~=3.2.1",
|
"django-auditlog~=3.1.2",
|
||||||
"django-cachalot~=2.8.0",
|
"django-cachalot~=2.8.0",
|
||||||
"django-celery-results~=2.6.0",
|
"django-celery-results~=2.6.0",
|
||||||
"django-compression-middleware~=0.5.0",
|
"django-compression-middleware~=0.5.0",
|
||||||
"django-cors-headers~=4.7.0",
|
"django-cors-headers~=4.7.0",
|
||||||
"django-extensions~=4.1",
|
"django-extensions~=4.1",
|
||||||
"django-filter~=25.1",
|
"django-filter~=25.1",
|
||||||
"django-guardian~=3.0.3",
|
"django-guardian~=2.4.0",
|
||||||
"django-multiselectfield~=1.0.1",
|
"django-multiselectfield~=0.1.13",
|
||||||
"django-soft-delete~=1.0.18",
|
"django-soft-delete~=1.0.18",
|
||||||
"djangorestframework~=3.15",
|
"djangorestframework~=3.15",
|
||||||
"djangorestframework-guardian~=0.4.0",
|
"djangorestframework-guardian~=0.3.0",
|
||||||
"drf-spectacular~=0.28",
|
"drf-spectacular~=0.28",
|
||||||
"drf-spectacular-sidecar~=2025.8.1",
|
"drf-spectacular-sidecar~=2025.4.1",
|
||||||
"drf-writable-nested~=0.7.1",
|
"drf-writable-nested~=0.7.1",
|
||||||
"filelock~=3.18.0",
|
"filelock~=3.18.0",
|
||||||
"flower~=2.0.1",
|
"flower~=2.0.1",
|
||||||
@@ -63,7 +63,7 @@ dependencies = [
|
|||||||
"redis[hiredis]~=5.2.1",
|
"redis[hiredis]~=5.2.1",
|
||||||
"scikit-learn~=1.7.0",
|
"scikit-learn~=1.7.0",
|
||||||
"setproctitle~=1.3.4",
|
"setproctitle~=1.3.4",
|
||||||
"tika-client~=0.10.0",
|
"tika-client~=0.9.0",
|
||||||
"tqdm~=4.67.1",
|
"tqdm~=4.67.1",
|
||||||
"watchdog~=6.0",
|
"watchdog~=6.0",
|
||||||
"whitenoise~=6.9",
|
"whitenoise~=6.9",
|
||||||
@@ -103,7 +103,7 @@ testing = [
|
|||||||
"imagehash",
|
"imagehash",
|
||||||
"pytest~=8.4.1",
|
"pytest~=8.4.1",
|
||||||
"pytest-cov~=6.2.1",
|
"pytest-cov~=6.2.1",
|
||||||
"pytest-django~=4.11.1",
|
"pytest-django~=4.10.0",
|
||||||
"pytest-env",
|
"pytest-env",
|
||||||
"pytest-httpx",
|
"pytest-httpx",
|
||||||
"pytest-mock",
|
"pytest-mock",
|
||||||
@@ -204,9 +204,15 @@ lint.per-file-ignores."docker/wait-for-redis.py" = [
|
|||||||
"INP001",
|
"INP001",
|
||||||
"T201",
|
"T201",
|
||||||
]
|
]
|
||||||
|
lint.per-file-ignores."src/documents/file_handling.py" = [
|
||||||
|
"PTH",
|
||||||
|
] # TODO Enable & remove
|
||||||
lint.per-file-ignores."src/documents/management/commands/document_consumer.py" = [
|
lint.per-file-ignores."src/documents/management/commands/document_consumer.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
|
lint.per-file-ignores."src/documents/management/commands/document_exporter.py" = [
|
||||||
|
"PTH",
|
||||||
|
] # TODO Enable & remove
|
||||||
lint.per-file-ignores."src/documents/migrations/1012_fix_archive_files.py" = [
|
lint.per-file-ignores."src/documents/migrations/1012_fix_archive_files.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
@@ -216,6 +222,9 @@ lint.per-file-ignores."src/documents/models.py" = [
|
|||||||
lint.per-file-ignores."src/documents/parsers.py" = [
|
lint.per-file-ignores."src/documents/parsers.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
|
lint.per-file-ignores."src/documents/signals/handlers.py" = [
|
||||||
|
"PTH",
|
||||||
|
] # TODO Enable & remove
|
||||||
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
|
lint.per-file-ignores."src/paperless_tesseract/tests/test_parser.py" = [
|
||||||
"RUF001",
|
"RUF001",
|
||||||
]
|
]
|
||||||
|
@@ -332,19 +332,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">102</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">102</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">102</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">102</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4930506384627295710" datatype="html">
|
<trans-unit id="4930506384627295710" datatype="html">
|
||||||
@@ -545,7 +545,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">362</context>
|
<context context-type="linenumber">361</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component.html</context>
|
||||||
@@ -605,7 +605,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
<context context-type="linenumber">74</context>
|
<context context-type="linenumber">73</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5079885666748292382" datatype="html">
|
<trans-unit id="5079885666748292382" datatype="html">
|
||||||
@@ -763,19 +763,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">52</context>
|
<context context-type="linenumber">51</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">52</context>
|
<context context-type="linenumber">51</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">52</context>
|
<context context-type="linenumber">51</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">52</context>
|
<context context-type="linenumber">51</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
@@ -1225,19 +1225,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">7</context>
|
<context context-type="linenumber">6</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">7</context>
|
<context context-type="linenumber">6</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">7</context>
|
<context context-type="linenumber">6</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">7</context>
|
<context context-type="linenumber">6</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="309314153079578337" datatype="html">
|
<trans-unit id="309314153079578337" datatype="html">
|
||||||
@@ -1432,7 +1432,7 @@
|
|||||||
<source>Cancel</source>
|
<source>Cancel</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
|
||||||
<context context-type="linenumber">361</context>
|
<context context-type="linenumber">362</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/confirm-dialog/confirm-dialog.component.ts</context>
|
||||||
@@ -1500,7 +1500,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
<context context-type="linenumber">73</context>
|
<context context-type="linenumber">74</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6839066544204061364" datatype="html">
|
<trans-unit id="6839066544204061364" datatype="html">
|
||||||
@@ -1598,19 +1598,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">3</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">3</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">3</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">4</context>
|
<context context-type="linenumber">3</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4880728824338713664" datatype="html">
|
<trans-unit id="4880728824338713664" datatype="html">
|
||||||
@@ -1696,35 +1696,35 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">21</context>
|
<context context-type="linenumber">20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">38</context>
|
<context context-type="linenumber">37</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">38</context>
|
<context context-type="linenumber">37</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">38</context>
|
<context context-type="linenumber">37</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">38</context>
|
<context context-type="linenumber">37</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
||||||
@@ -1816,19 +1816,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">44</context>
|
<context context-type="linenumber">43</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">44</context>
|
<context context-type="linenumber">43</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">44</context>
|
<context context-type="linenumber">43</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">44</context>
|
<context context-type="linenumber">43</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
|
||||||
@@ -2121,51 +2121,51 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">10</context>
|
<context context-type="linenumber">9</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">10</context>
|
<context context-type="linenumber">9</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">10</context>
|
<context context-type="linenumber">9</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">10</context>
|
<context context-type="linenumber">9</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">85</context>
|
<context context-type="linenumber">84</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">85</context>
|
<context context-type="linenumber">84</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">85</context>
|
<context context-type="linenumber">84</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">85</context>
|
<context context-type="linenumber">84</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">97</context>
|
<context context-type="linenumber">96</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">97</context>
|
<context context-type="linenumber">96</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">97</context>
|
<context context-type="linenumber">96</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">97</context>
|
<context context-type="linenumber">96</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.ts</context>
|
||||||
@@ -2440,35 +2440,35 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">84</context>
|
<context context-type="linenumber">83</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">84</context>
|
<context context-type="linenumber">83</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">84</context>
|
<context context-type="linenumber">83</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">84</context>
|
<context context-type="linenumber">83</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">94</context>
|
<context context-type="linenumber">93</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">94</context>
|
<context context-type="linenumber">93</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">94</context>
|
<context context-type="linenumber">93</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">94</context>
|
<context context-type="linenumber">93</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
||||||
@@ -5227,19 +5227,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">12</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">12</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">12</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">12</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4391289919356861627" datatype="html">
|
<trans-unit id="4391289919356861627" datatype="html">
|
||||||
@@ -8333,19 +8333,19 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">87</context>
|
<context context-type="linenumber">86</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">87</context>
|
<context context-type="linenumber">86</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">87</context>
|
<context context-type="linenumber">86</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">87</context>
|
<context context-type="linenumber">86</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="651372623796033489" datatype="html">
|
<trans-unit id="651372623796033489" datatype="html">
|
||||||
@@ -8672,76 +8672,76 @@
|
|||||||
<source>Filter by:</source>
|
<source>Filter by:</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">19</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">19</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">19</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">19</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1383365546483928780" datatype="html">
|
<trans-unit id="1383365546483928780" datatype="html">
|
||||||
<source>Matching</source>
|
<source>Matching</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">39</context>
|
<context context-type="linenumber">38</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">39</context>
|
<context context-type="linenumber">38</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">39</context>
|
<context context-type="linenumber">38</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">39</context>
|
<context context-type="linenumber">38</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1488347670280290838" datatype="html">
|
<trans-unit id="1488347670280290838" datatype="html">
|
||||||
<source>Document count</source>
|
<source>Document count</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">40</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">40</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">40</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">40</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8095412801504464756" datatype="html">
|
<trans-unit id="8095412801504464756" datatype="html">
|
||||||
<source>{VAR_PLURAL, plural, =1 {One <x id="INTERPOLATION"/>} other {<x id="INTERPOLATION_1"/> total <x id="INTERPOLATION_2"/>}}</source>
|
<source>{VAR_PLURAL, plural, =1 {One <x id="INTERPOLATION"/>} other {<x id="INTERPOLATION_1"/> total <x id="INTERPOLATION_2"/>}}</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">119</context>
|
<context context-type="linenumber">118</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">119</context>
|
<context context-type="linenumber">118</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">119</context>
|
<context context-type="linenumber">118</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/management-list/management-list.component.html</context>
|
||||||
<context context-type="linenumber">119</context>
|
<context context-type="linenumber">118</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="810888510148304696" datatype="html">
|
<trans-unit id="810888510148304696" datatype="html">
|
||||||
|
@@ -50,7 +50,7 @@
|
|||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
<div class="btn-toolbar" role="toolbar">
|
<div class="btn-toolbar" role="toolbar">
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group me-2">
|
||||||
<button type="button" (click)="discardChanges()" class="btn btn-outline-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
|
<button type="button" (click)="discardChanges()" class="btn btn-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>
|
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>
|
||||||
|
@@ -358,6 +358,6 @@
|
|||||||
|
|
||||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||||
|
|
||||||
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -164,7 +164,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const createButton = fixture.debugElement.queryAll(By.css('button'))[4]
|
const createButton = fixture.debugElement.queryAll(By.css('button'))[3]
|
||||||
createButton.triggerEventHandler('click')
|
createButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
@@ -188,7 +188,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const editButton = fixture.debugElement.queryAll(By.css('button'))[7]
|
const editButton = fixture.debugElement.queryAll(By.css('button'))[6]
|
||||||
editButton.triggerEventHandler('click')
|
editButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
@@ -213,7 +213,7 @@ describe('ManagementListComponent', () => {
|
|||||||
const deleteSpy = jest.spyOn(tagService, 'delete')
|
const deleteSpy = jest.spyOn(tagService, 'delete')
|
||||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||||
|
|
||||||
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8]
|
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[7]
|
||||||
deleteButton.triggerEventHandler('click')
|
deleteButton.triggerEventHandler('click')
|
||||||
|
|
||||||
expect(modal).not.toBeUndefined()
|
expect(modal).not.toBeUndefined()
|
||||||
@@ -233,7 +233,7 @@ describe('ManagementListComponent', () => {
|
|||||||
|
|
||||||
it('should support quick filter for objects', () => {
|
it('should support quick filter for objects', () => {
|
||||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||||
const filterButton = fixture.debugElement.queryAll(By.css('button'))[9]
|
const filterButton = fixture.debugElement.queryAll(By.css('button'))[8]
|
||||||
filterButton.triggerEventHandler('click')
|
filterButton.triggerEventHandler('click')
|
||||||
expect(qfSpy).toHaveBeenCalledWith([
|
expect(qfSpy).toHaveBeenCalledWith([
|
||||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
|
{ rule_type: FILTER_HAS_TAGS_ALL, value: tags[0].id.toString() },
|
||||||
|
@@ -70,6 +70,6 @@
|
|||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
<button type="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
||||||
<button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
|
<button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -1,23 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import pickle
|
|
||||||
from binascii import hexlify
|
from binascii import hexlify
|
||||||
from collections import OrderedDict
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Any
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.cache import caches
|
|
||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django.core.cache.backends.base import BaseCache
|
|
||||||
|
|
||||||
from documents.classifier import DocumentClassifier
|
from documents.classifier import DocumentClassifier
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.caching")
|
logger = logging.getLogger("paperless.caching")
|
||||||
@@ -46,80 +39,6 @@ CACHE_1_MINUTE: Final[int] = 60
|
|||||||
CACHE_5_MINUTES: Final[int] = 5 * CACHE_1_MINUTE
|
CACHE_5_MINUTES: Final[int] = 5 * CACHE_1_MINUTE
|
||||||
CACHE_50_MINUTES: Final[int] = 50 * CACHE_1_MINUTE
|
CACHE_50_MINUTES: Final[int] = 50 * CACHE_1_MINUTE
|
||||||
|
|
||||||
read_cache = caches["read-cache"]
|
|
||||||
|
|
||||||
|
|
||||||
class LRUCache:
|
|
||||||
def __init__(self, capacity: int = 128):
|
|
||||||
self._data = OrderedDict()
|
|
||||||
self.capacity = capacity
|
|
||||||
|
|
||||||
def get(self, key, default=None) -> Any | None:
|
|
||||||
if key in self._data:
|
|
||||||
self._data.move_to_end(key)
|
|
||||||
return self._data[key]
|
|
||||||
return default
|
|
||||||
|
|
||||||
def set(self, key, value) -> None:
|
|
||||||
self._data[key] = value
|
|
||||||
self._data.move_to_end(key)
|
|
||||||
while len(self._data) > self.capacity:
|
|
||||||
self._data.popitem(last=False)
|
|
||||||
|
|
||||||
|
|
||||||
class StoredLRUCache(LRUCache):
|
|
||||||
"""
|
|
||||||
LRU cache that can persist its entire contents as a single entry in a backend cache.
|
|
||||||
|
|
||||||
Useful for sharing a cache across multiple workers or processes.
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. Load the cache state from the backend using `load()`.
|
|
||||||
2. Use `get()` and `set()` locally as usual.
|
|
||||||
3. Persist changes back to the backend using `save()`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
backend_key: str,
|
|
||||||
capacity: int = 128,
|
|
||||||
backend: BaseCache = read_cache,
|
|
||||||
backend_ttl=settings.CACHALOT_TIMEOUT,
|
|
||||||
):
|
|
||||||
if backend_key is None:
|
|
||||||
raise ValueError("backend_key is mandatory")
|
|
||||||
super().__init__(capacity)
|
|
||||||
self._backend_key = backend_key
|
|
||||||
self._backend = backend
|
|
||||||
self.backend_ttl = backend_ttl
|
|
||||||
|
|
||||||
def load(self) -> None:
|
|
||||||
"""
|
|
||||||
Load the whole cache content from backend storage.
|
|
||||||
|
|
||||||
If no valid cached data exists in the backend, the local cache is cleared.
|
|
||||||
"""
|
|
||||||
serialized_data = self._backend.get(self._backend_key)
|
|
||||||
try:
|
|
||||||
self._data = (
|
|
||||||
pickle.loads(serialized_data) if serialized_data else OrderedDict()
|
|
||||||
)
|
|
||||||
except pickle.PickleError:
|
|
||||||
logger.warning(
|
|
||||||
"Cache exists in backend but could not be read (possibly invalid format)",
|
|
||||||
)
|
|
||||||
|
|
||||||
def save(self) -> None:
|
|
||||||
"""Save the entire local cache to the backend as a serialized object.
|
|
||||||
|
|
||||||
The backend entry will expire after the configured TTL.
|
|
||||||
"""
|
|
||||||
self._backend.set(
|
|
||||||
self._backend_key,
|
|
||||||
pickle.dumps(self._data),
|
|
||||||
self.backend_ttl,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_suggestion_cache_key(document_id: int) -> str:
|
def get_suggestion_cache_key(document_id: int) -> str:
|
||||||
"""
|
"""
|
||||||
|
@@ -16,29 +16,16 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.core.cache import caches
|
|
||||||
|
|
||||||
from documents.caching import CACHE_5_MINUTES
|
|
||||||
from documents.caching import CACHE_50_MINUTES
|
from documents.caching import CACHE_50_MINUTES
|
||||||
from documents.caching import CLASSIFIER_HASH_KEY
|
from documents.caching import CLASSIFIER_HASH_KEY
|
||||||
from documents.caching import CLASSIFIER_MODIFIED_KEY
|
from documents.caching import CLASSIFIER_MODIFIED_KEY
|
||||||
from documents.caching import CLASSIFIER_VERSION_KEY
|
from documents.caching import CLASSIFIER_VERSION_KEY
|
||||||
from documents.caching import StoredLRUCache
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.classifier")
|
logger = logging.getLogger("paperless.classifier")
|
||||||
|
|
||||||
ADVANCED_TEXT_PROCESSING_ENABLED = (
|
|
||||||
settings.NLTK_LANGUAGE is not None and settings.NLTK_ENABLED
|
|
||||||
)
|
|
||||||
|
|
||||||
read_cache = caches["read-cache"]
|
|
||||||
|
|
||||||
|
|
||||||
RE_DIGIT = re.compile(r"\d")
|
|
||||||
RE_WORD = re.compile(r"\b[\w]+\b") # words that may contain digits
|
|
||||||
|
|
||||||
|
|
||||||
class IncompatibleClassifierVersionError(Exception):
|
class IncompatibleClassifierVersionError(Exception):
|
||||||
def __init__(self, message: str, *args: object) -> None:
|
def __init__(self, message: str, *args: object) -> None:
|
||||||
@@ -105,27 +92,14 @@ class DocumentClassifier:
|
|||||||
self.last_auto_type_hash: bytes | None = None
|
self.last_auto_type_hash: bytes | None = None
|
||||||
|
|
||||||
self.data_vectorizer = None
|
self.data_vectorizer = None
|
||||||
self.data_vectorizer_hash = None
|
|
||||||
self.tags_binarizer = None
|
self.tags_binarizer = None
|
||||||
self.tags_classifier = None
|
self.tags_classifier = None
|
||||||
self.correspondent_classifier = None
|
self.correspondent_classifier = None
|
||||||
self.document_type_classifier = None
|
self.document_type_classifier = None
|
||||||
self.storage_path_classifier = None
|
self.storage_path_classifier = None
|
||||||
self._stemmer = None
|
|
||||||
# 10,000 elements roughly use 200 to 500 KB per worker,
|
|
||||||
# and also in the shared Redis cache,
|
|
||||||
# Keep this cache small to minimize lookup and I/O latency.
|
|
||||||
if ADVANCED_TEXT_PROCESSING_ENABLED:
|
|
||||||
self._stem_cache = StoredLRUCache(
|
|
||||||
f"stem_cache_v{self.FORMAT_VERSION}",
|
|
||||||
capacity=10000,
|
|
||||||
)
|
|
||||||
self._stop_words = None
|
|
||||||
|
|
||||||
def _update_data_vectorizer_hash(self):
|
self._stemmer = None
|
||||||
self.data_vectorizer_hash = sha256(
|
self._stop_words = None
|
||||||
pickle.dumps(self.data_vectorizer),
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
from sklearn.exceptions import InconsistentVersionWarning
|
from sklearn.exceptions import InconsistentVersionWarning
|
||||||
@@ -145,7 +119,6 @@ class DocumentClassifier:
|
|||||||
self.last_auto_type_hash = pickle.load(f)
|
self.last_auto_type_hash = pickle.load(f)
|
||||||
|
|
||||||
self.data_vectorizer = pickle.load(f)
|
self.data_vectorizer = pickle.load(f)
|
||||||
self._update_data_vectorizer_hash()
|
|
||||||
self.tags_binarizer = pickle.load(f)
|
self.tags_binarizer = pickle.load(f)
|
||||||
|
|
||||||
self.tags_classifier = pickle.load(f)
|
self.tags_classifier = pickle.load(f)
|
||||||
@@ -296,7 +269,7 @@ class DocumentClassifier:
|
|||||||
Generates the content for documents, but once at a time
|
Generates the content for documents, but once at a time
|
||||||
"""
|
"""
|
||||||
for doc in docs_queryset:
|
for doc in docs_queryset:
|
||||||
yield self.preprocess_content(doc.content, shared_cache=False)
|
yield self.preprocess_content(doc.content)
|
||||||
|
|
||||||
self.data_vectorizer = CountVectorizer(
|
self.data_vectorizer = CountVectorizer(
|
||||||
analyzer="word",
|
analyzer="word",
|
||||||
@@ -374,7 +347,6 @@ class DocumentClassifier:
|
|||||||
|
|
||||||
self.last_doc_change_time = latest_doc_change
|
self.last_doc_change_time = latest_doc_change
|
||||||
self.last_auto_type_hash = hasher.digest()
|
self.last_auto_type_hash = hasher.digest()
|
||||||
self._update_data_vectorizer_hash()
|
|
||||||
|
|
||||||
# Set the classifier information into the cache
|
# Set the classifier information into the cache
|
||||||
# Caching for 50 minutes, so slightly less than the normal retrain time
|
# Caching for 50 minutes, so slightly less than the normal retrain time
|
||||||
@@ -384,15 +356,30 @@ class DocumentClassifier:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _init_advanced_text_processing(self):
|
def preprocess_content(self, content: str) -> str: # pragma: no cover
|
||||||
if self._stop_words is None or self._stemmer is None:
|
"""
|
||||||
|
Process to contents of a document, distilling it down into
|
||||||
|
words which are meaningful to the content
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Lower case the document
|
||||||
|
content = content.lower().strip()
|
||||||
|
# Reduce spaces
|
||||||
|
content = re.sub(r"\s+", " ", content)
|
||||||
|
# Get only the letters
|
||||||
|
content = re.sub(r"[^\w\s]", " ", content)
|
||||||
|
|
||||||
|
# If the NLTK language is supported, do further processing
|
||||||
|
if settings.NLTK_LANGUAGE is not None and settings.NLTK_ENABLED:
|
||||||
import nltk
|
import nltk
|
||||||
from nltk.corpus import stopwords
|
from nltk.corpus import stopwords
|
||||||
from nltk.stem import SnowballStemmer
|
from nltk.stem import SnowballStemmer
|
||||||
|
from nltk.tokenize import word_tokenize
|
||||||
|
|
||||||
# Not really hacky, since it isn't private and is documented, but
|
# Not really hacky, since it isn't private and is documented, but
|
||||||
# set the search path for NLTK data to the single location it should be in
|
# set the search path for NLTK data to the single location it should be in
|
||||||
nltk.data.path = [settings.NLTK_DIR]
|
nltk.data.path = [settings.NLTK_DIR]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Preload the corpus early, to force the lazy loader to transform
|
# Preload the corpus early, to force the lazy loader to transform
|
||||||
stopwords.ensure_loaded()
|
stopwords.ensure_loaded()
|
||||||
@@ -400,100 +387,41 @@ class DocumentClassifier:
|
|||||||
# Do some one time setup
|
# Do some one time setup
|
||||||
# Sometimes, somehow, there's multiple threads loading the corpus
|
# Sometimes, somehow, there's multiple threads loading the corpus
|
||||||
# and it's not thread safe, raising an AttributeError
|
# and it's not thread safe, raising an AttributeError
|
||||||
self._stemmer = SnowballStemmer(settings.NLTK_LANGUAGE)
|
if self._stemmer is None:
|
||||||
self._stop_words = frozenset(stopwords.words(settings.NLTK_LANGUAGE))
|
self._stemmer = SnowballStemmer(settings.NLTK_LANGUAGE)
|
||||||
|
if self._stop_words is None:
|
||||||
|
self._stop_words = set(stopwords.words(settings.NLTK_LANGUAGE))
|
||||||
|
|
||||||
|
# Tokenize
|
||||||
|
# This splits the content into tokens, roughly words
|
||||||
|
words: list[str] = word_tokenize(
|
||||||
|
content,
|
||||||
|
language=settings.NLTK_LANGUAGE,
|
||||||
|
)
|
||||||
|
|
||||||
|
meaningful_words = []
|
||||||
|
for word in words:
|
||||||
|
# Skip stop words
|
||||||
|
# These are words like "a", "and", "the" which add little meaning
|
||||||
|
if word in self._stop_words:
|
||||||
|
continue
|
||||||
|
# Stem the words
|
||||||
|
# This reduces the words to their stems.
|
||||||
|
# "amazement" returns "amaz"
|
||||||
|
# "amaze" returns "amaz
|
||||||
|
# "amazed" returns "amaz"
|
||||||
|
meaningful_words.append(self._stemmer.stem(word))
|
||||||
|
|
||||||
|
return " ".join(meaningful_words)
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.debug("Could not initialize NLTK for advanced text processing.")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def stem_and_skip_stop_words(self, words: list[str], *, shared_cache=True):
|
|
||||||
"""
|
|
||||||
Reduce a list of words to their stem. Stop words are converted to empty strings.
|
|
||||||
:param words: the list of words to stem
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _stem_and_skip_stop_word(word: str):
|
|
||||||
"""
|
|
||||||
Reduce a given word to its stem. If it's a stop word, return an empty string.
|
|
||||||
E.g. "amazement", "amaze" and "amazed" all return "amaz".
|
|
||||||
"""
|
|
||||||
cached = self._stem_cache.get(word)
|
|
||||||
if cached is not None:
|
|
||||||
return cached
|
|
||||||
elif word in self._stop_words:
|
|
||||||
return ""
|
|
||||||
# Assumption: words that contain numbers are never stemmed
|
|
||||||
elif RE_DIGIT.search(word):
|
|
||||||
return word
|
|
||||||
else:
|
|
||||||
result = self._stemmer.stem(word)
|
|
||||||
self._stem_cache.set(word, result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
if shared_cache:
|
|
||||||
self._stem_cache.load()
|
|
||||||
|
|
||||||
# Stem the words and skip stop words
|
|
||||||
result = " ".join(
|
|
||||||
filter(None, (_stem_and_skip_stop_word(w) for w in words)),
|
|
||||||
)
|
|
||||||
if shared_cache:
|
|
||||||
self._stem_cache.save()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def preprocess_content(
|
|
||||||
self,
|
|
||||||
content: str,
|
|
||||||
*,
|
|
||||||
shared_cache=True,
|
|
||||||
) -> str:
|
|
||||||
"""
|
|
||||||
Process the contents of a document, distilling it down into
|
|
||||||
words which are meaningful to the content.
|
|
||||||
|
|
||||||
A stemmer cache is shared across workers with the parameter "shared_cache".
|
|
||||||
This is unnecessary when training the classifier.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Lower case the document, reduce space,
|
|
||||||
# and keep only letters and digits.
|
|
||||||
content = " ".join(match.group().lower() for match in RE_WORD.finditer(content))
|
|
||||||
|
|
||||||
if ADVANCED_TEXT_PROCESSING_ENABLED:
|
|
||||||
from nltk.tokenize import word_tokenize
|
|
||||||
|
|
||||||
if not self._init_advanced_text_processing():
|
|
||||||
return content
|
return content
|
||||||
# Tokenize
|
|
||||||
# This splits the content into tokens, roughly words
|
|
||||||
words = word_tokenize(content, language=settings.NLTK_LANGUAGE)
|
|
||||||
# Stem the words and skip stop words
|
|
||||||
content = self.stem_and_skip_stop_words(words, shared_cache=shared_cache)
|
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def _get_vectorizer_cache_key(self, content: str):
|
|
||||||
hash = sha256(content.encode())
|
|
||||||
hash.update(
|
|
||||||
f"|{self.FORMAT_VERSION}|{settings.NLTK_LANGUAGE}|{settings.NLTK_ENABLED}|{self.data_vectorizer_hash}".encode(),
|
|
||||||
)
|
|
||||||
return f"vectorized_content_{hash.hexdigest()}"
|
|
||||||
|
|
||||||
def _vectorize(self, content: str):
|
|
||||||
key = self._get_vectorizer_cache_key(content)
|
|
||||||
serialized_result = read_cache.get(key)
|
|
||||||
if serialized_result is None:
|
|
||||||
result = self.data_vectorizer.transform([self.preprocess_content(content)])
|
|
||||||
read_cache.set(key, pickle.dumps(result), CACHE_5_MINUTES)
|
|
||||||
else:
|
|
||||||
read_cache.touch(key, CACHE_5_MINUTES)
|
|
||||||
result = pickle.loads(serialized_result)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def predict_correspondent(self, content: str) -> int | None:
|
def predict_correspondent(self, content: str) -> int | None:
|
||||||
if self.correspondent_classifier:
|
if self.correspondent_classifier:
|
||||||
X = self._vectorize(content)
|
X = self.data_vectorizer.transform([self.preprocess_content(content)])
|
||||||
correspondent_id = self.correspondent_classifier.predict(X)
|
correspondent_id = self.correspondent_classifier.predict(X)
|
||||||
if correspondent_id != -1:
|
if correspondent_id != -1:
|
||||||
return correspondent_id
|
return correspondent_id
|
||||||
@@ -504,7 +432,7 @@ class DocumentClassifier:
|
|||||||
|
|
||||||
def predict_document_type(self, content: str) -> int | None:
|
def predict_document_type(self, content: str) -> int | None:
|
||||||
if self.document_type_classifier:
|
if self.document_type_classifier:
|
||||||
X = self._vectorize(content)
|
X = self.data_vectorizer.transform([self.preprocess_content(content)])
|
||||||
document_type_id = self.document_type_classifier.predict(X)
|
document_type_id = self.document_type_classifier.predict(X)
|
||||||
if document_type_id != -1:
|
if document_type_id != -1:
|
||||||
return document_type_id
|
return document_type_id
|
||||||
@@ -517,7 +445,7 @@ class DocumentClassifier:
|
|||||||
from sklearn.utils.multiclass import type_of_target
|
from sklearn.utils.multiclass import type_of_target
|
||||||
|
|
||||||
if self.tags_classifier:
|
if self.tags_classifier:
|
||||||
X = self._vectorize(content)
|
X = self.data_vectorizer.transform([self.preprocess_content(content)])
|
||||||
y = self.tags_classifier.predict(X)
|
y = self.tags_classifier.predict(X)
|
||||||
tags_ids = self.tags_binarizer.inverse_transform(y)[0]
|
tags_ids = self.tags_binarizer.inverse_transform(y)[0]
|
||||||
if type_of_target(y).startswith("multilabel"):
|
if type_of_target(y).startswith("multilabel"):
|
||||||
@@ -536,7 +464,7 @@ class DocumentClassifier:
|
|||||||
|
|
||||||
def predict_storage_path(self, content: str) -> int | None:
|
def predict_storage_path(self, content: str) -> int | None:
|
||||||
if self.storage_path_classifier:
|
if self.storage_path_classifier:
|
||||||
X = self._vectorize(content)
|
X = self.data_vectorizer.transform([self.preprocess_content(content)])
|
||||||
storage_path_id = self.storage_path_classifier.predict(X)
|
storage_path_id = self.storage_path_classifier.predict(X)
|
||||||
if storage_path_id != -1:
|
if storage_path_id != -1:
|
||||||
return storage_path_id
|
return storage_path_id
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
@@ -8,15 +7,19 @@ from documents.templating.filepath import validate_filepath_template_and_render
|
|||||||
from documents.templating.utils import convert_format_str_to_template_format
|
from documents.templating.utils import convert_format_str_to_template_format
|
||||||
|
|
||||||
|
|
||||||
def create_source_path_directory(source_path: Path) -> None:
|
def create_source_path_directory(source_path):
|
||||||
source_path.parent.mkdir(parents=True, exist_ok=True)
|
os.makedirs(os.path.dirname(source_path), exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def delete_empty_directories(directory: Path, root: Path) -> None:
|
def delete_empty_directories(directory, root):
|
||||||
if not directory.is_dir():
|
if not os.path.isdir(directory):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not directory.is_relative_to(root):
|
# Go up in the directory hierarchy and try to delete all directories
|
||||||
|
directory = os.path.normpath(directory)
|
||||||
|
root = os.path.normpath(root)
|
||||||
|
|
||||||
|
if not directory.startswith(root + os.path.sep):
|
||||||
# don't do anything outside our originals folder.
|
# don't do anything outside our originals folder.
|
||||||
|
|
||||||
# append os.path.set so that we avoid these cases:
|
# append os.path.set so that we avoid these cases:
|
||||||
@@ -24,12 +27,11 @@ def delete_empty_directories(directory: Path, root: Path) -> None:
|
|||||||
# root = /home/originals ("/" gets appended and startswith fails)
|
# root = /home/originals ("/" gets appended and startswith fails)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Go up in the directory hierarchy and try to delete all directories
|
|
||||||
while directory != root:
|
while directory != root:
|
||||||
if not list(directory.iterdir()):
|
if not os.listdir(directory):
|
||||||
# it's empty
|
# it's empty
|
||||||
try:
|
try:
|
||||||
directory.rmdir()
|
os.rmdir(directory)
|
||||||
except OSError:
|
except OSError:
|
||||||
# whatever. empty directories aren't that bad anyway.
|
# whatever. empty directories aren't that bad anyway.
|
||||||
return
|
return
|
||||||
@@ -38,10 +40,10 @@ def delete_empty_directories(directory: Path, root: Path) -> None:
|
|||||||
return
|
return
|
||||||
|
|
||||||
# go one level up
|
# go one level up
|
||||||
directory = directory.parent
|
directory = os.path.normpath(os.path.dirname(directory))
|
||||||
|
|
||||||
|
|
||||||
def generate_unique_filename(doc, *, archive_filename=False) -> Path:
|
def generate_unique_filename(doc, *, archive_filename=False):
|
||||||
"""
|
"""
|
||||||
Generates a unique filename for doc in settings.ORIGINALS_DIR.
|
Generates a unique filename for doc in settings.ORIGINALS_DIR.
|
||||||
|
|
||||||
@@ -54,32 +56,21 @@ def generate_unique_filename(doc, *, archive_filename=False) -> Path:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if archive_filename:
|
if archive_filename:
|
||||||
old_filename: Path | None = (
|
old_filename = doc.archive_filename
|
||||||
Path(doc.archive_filename) if doc.archive_filename else None
|
|
||||||
)
|
|
||||||
root = settings.ARCHIVE_DIR
|
root = settings.ARCHIVE_DIR
|
||||||
else:
|
else:
|
||||||
old_filename = Path(doc.filename) if doc.filename else None
|
old_filename = doc.filename
|
||||||
root = settings.ORIGINALS_DIR
|
root = settings.ORIGINALS_DIR
|
||||||
|
|
||||||
# If generating archive filenames, try to make a name that is similar to
|
# If generating archive filenames, try to make a name that is similar to
|
||||||
# the original filename first.
|
# the original filename first.
|
||||||
|
|
||||||
if archive_filename and doc.filename:
|
if archive_filename and doc.filename:
|
||||||
# Generate the full path using the same logic as generate_filename
|
new_filename = os.path.splitext(doc.filename)[0] + ".pdf"
|
||||||
base_generated = generate_filename(doc, archive_filename=archive_filename)
|
if new_filename == old_filename or not os.path.exists(
|
||||||
|
os.path.join(root, new_filename),
|
||||||
# Try to create a simple PDF version based on the original filename
|
):
|
||||||
# but preserve any directory structure from the template
|
return new_filename
|
||||||
if str(base_generated.parent) != ".":
|
|
||||||
# Has directory structure, preserve it
|
|
||||||
simple_pdf_name = base_generated.parent / (Path(doc.filename).stem + ".pdf")
|
|
||||||
else:
|
|
||||||
# No directory structure
|
|
||||||
simple_pdf_name = Path(Path(doc.filename).stem + ".pdf")
|
|
||||||
|
|
||||||
if simple_pdf_name == old_filename or not (root / simple_pdf_name).exists():
|
|
||||||
return simple_pdf_name
|
|
||||||
|
|
||||||
counter = 0
|
counter = 0
|
||||||
|
|
||||||
@@ -93,7 +84,7 @@ def generate_unique_filename(doc, *, archive_filename=False) -> Path:
|
|||||||
# still the same as before.
|
# still the same as before.
|
||||||
return new_filename
|
return new_filename
|
||||||
|
|
||||||
if (root / new_filename).exists():
|
if os.path.exists(os.path.join(root, new_filename)):
|
||||||
counter += 1
|
counter += 1
|
||||||
else:
|
else:
|
||||||
return new_filename
|
return new_filename
|
||||||
@@ -105,8 +96,8 @@ def generate_filename(
|
|||||||
counter=0,
|
counter=0,
|
||||||
append_gpg=True,
|
append_gpg=True,
|
||||||
archive_filename=False,
|
archive_filename=False,
|
||||||
) -> Path:
|
):
|
||||||
base_path: Path | None = None
|
path = ""
|
||||||
|
|
||||||
def format_filename(document: Document, template_str: str) -> str | None:
|
def format_filename(document: Document, template_str: str) -> str | None:
|
||||||
rendered_filename = validate_filepath_template_and_render(
|
rendered_filename = validate_filepath_template_and_render(
|
||||||
@@ -143,34 +134,17 @@ def generate_filename(
|
|||||||
|
|
||||||
# If we have one, render it
|
# If we have one, render it
|
||||||
if filename_format is not None:
|
if filename_format is not None:
|
||||||
rendered_path: str | None = format_filename(doc, filename_format)
|
path = format_filename(doc, filename_format)
|
||||||
if rendered_path:
|
|
||||||
base_path = Path(rendered_path)
|
|
||||||
|
|
||||||
counter_str = f"_{counter:02}" if counter else ""
|
counter_str = f"_{counter:02}" if counter else ""
|
||||||
filetype_str = ".pdf" if archive_filename else doc.file_type
|
filetype_str = ".pdf" if archive_filename else doc.file_type
|
||||||
|
|
||||||
if base_path:
|
if path:
|
||||||
# Split the path into directory and filename parts
|
filename = f"{path}{counter_str}{filetype_str}"
|
||||||
directory = base_path.parent
|
|
||||||
# Use the full name (not just stem) as the base filename
|
|
||||||
base_filename = base_path.name
|
|
||||||
|
|
||||||
# Build the final filename with counter and filetype
|
|
||||||
final_filename = f"{base_filename}{counter_str}{filetype_str}"
|
|
||||||
|
|
||||||
# If we have a directory component, include it
|
|
||||||
if str(directory) != ".":
|
|
||||||
full_path = directory / final_filename
|
|
||||||
else:
|
|
||||||
full_path = Path(final_filename)
|
|
||||||
else:
|
else:
|
||||||
# No template, use document ID
|
filename = f"{doc.pk:07}{counter_str}{filetype_str}"
|
||||||
final_filename = f"{doc.pk:07}{counter_str}{filetype_str}"
|
|
||||||
full_path = Path(final_filename)
|
|
||||||
|
|
||||||
# Add GPG extension if needed
|
|
||||||
if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG:
|
if append_gpg and doc.storage_type == doc.STORAGE_TYPE_GPG:
|
||||||
full_path = full_path.with_suffix(full_path.suffix + ".gpg")
|
filename += ".gpg"
|
||||||
|
|
||||||
return full_path
|
return filename
|
||||||
|
@@ -236,7 +236,10 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
# now make an archive in the original target, with all files stored
|
# now make an archive in the original target, with all files stored
|
||||||
if self.zip_export and temp_dir is not None:
|
if self.zip_export and temp_dir is not None:
|
||||||
shutil.make_archive(
|
shutil.make_archive(
|
||||||
self.original_target / options["zip_name"],
|
os.path.join(
|
||||||
|
self.original_target,
|
||||||
|
options["zip_name"],
|
||||||
|
),
|
||||||
format="zip",
|
format="zip",
|
||||||
root_dir=temp_dir.name,
|
root_dir=temp_dir.name,
|
||||||
)
|
)
|
||||||
@@ -339,7 +342,7 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.split_manifest:
|
if self.split_manifest:
|
||||||
manifest_name = base_name.with_name(f"{base_name.stem}-manifest.json")
|
manifest_name = Path(base_name + "-manifest.json")
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
manifest_name = Path("json") / manifest_name
|
manifest_name = Path("json") / manifest_name
|
||||||
manifest_name = (self.target / manifest_name).resolve()
|
manifest_name = (self.target / manifest_name).resolve()
|
||||||
@@ -413,7 +416,7 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
else:
|
else:
|
||||||
item.unlink()
|
item.unlink()
|
||||||
|
|
||||||
def generate_base_name(self, document: Document) -> Path:
|
def generate_base_name(self, document: Document) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a unique name for the document, one which hasn't already been exported (or will be)
|
Generates a unique name for the document, one which hasn't already been exported (or will be)
|
||||||
"""
|
"""
|
||||||
@@ -433,12 +436,12 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
filename_counter += 1
|
filename_counter += 1
|
||||||
return Path(base_name)
|
return base_name
|
||||||
|
|
||||||
def generate_document_targets(
|
def generate_document_targets(
|
||||||
self,
|
self,
|
||||||
document: Document,
|
document: Document,
|
||||||
base_name: Path,
|
base_name: str,
|
||||||
document_dict: dict,
|
document_dict: dict,
|
||||||
) -> tuple[Path, Path | None, Path | None]:
|
) -> tuple[Path, Path | None, Path | None]:
|
||||||
"""
|
"""
|
||||||
@@ -446,25 +449,25 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
"""
|
"""
|
||||||
original_name = base_name
|
original_name = base_name
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
original_name = Path("originals") / original_name
|
original_name = os.path.join("originals", original_name)
|
||||||
original_target = (self.target / original_name).resolve()
|
original_target = (self.target / Path(original_name)).resolve()
|
||||||
document_dict[EXPORTER_FILE_NAME] = str(original_name)
|
document_dict[EXPORTER_FILE_NAME] = original_name
|
||||||
|
|
||||||
if not self.no_thumbnail:
|
if not self.no_thumbnail:
|
||||||
thumbnail_name = base_name.parent / (base_name.stem + "-thumbnail.webp")
|
thumbnail_name = base_name + "-thumbnail.webp"
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
thumbnail_name = Path("thumbnails") / thumbnail_name
|
thumbnail_name = os.path.join("thumbnails", thumbnail_name)
|
||||||
thumbnail_target = (self.target / thumbnail_name).resolve()
|
thumbnail_target = (self.target / Path(thumbnail_name)).resolve()
|
||||||
document_dict[EXPORTER_THUMBNAIL_NAME] = str(thumbnail_name)
|
document_dict[EXPORTER_THUMBNAIL_NAME] = thumbnail_name
|
||||||
else:
|
else:
|
||||||
thumbnail_target = None
|
thumbnail_target = None
|
||||||
|
|
||||||
if not self.no_archive and document.has_archive_version:
|
if not self.no_archive and document.has_archive_version:
|
||||||
archive_name = base_name.parent / (base_name.stem + "-archive.pdf")
|
archive_name = base_name + "-archive.pdf"
|
||||||
if self.use_folder_prefix:
|
if self.use_folder_prefix:
|
||||||
archive_name = Path("archive") / archive_name
|
archive_name = os.path.join("archive", archive_name)
|
||||||
archive_target = (self.target / archive_name).resolve()
|
archive_target = (self.target / Path(archive_name)).resolve()
|
||||||
document_dict[EXPORTER_ARCHIVE_NAME] = str(archive_name)
|
document_dict[EXPORTER_ARCHIVE_NAME] = archive_name
|
||||||
else:
|
else:
|
||||||
archive_target = None
|
archive_target = None
|
||||||
|
|
||||||
@@ -569,7 +572,7 @@ class Command(CryptMixin, BaseCommand):
|
|||||||
perform_copy = False
|
perform_copy = False
|
||||||
|
|
||||||
if target.exists():
|
if target.exists():
|
||||||
source_stat = source.stat()
|
source_stat = os.stat(source)
|
||||||
target_stat = target.stat()
|
target_stat = target.stat()
|
||||||
if self.compare_checksums and source_checksum:
|
if self.compare_checksums and source_checksum:
|
||||||
target_checksum = hashlib.md5(target.read_bytes()).hexdigest()
|
target_checksum = hashlib.md5(target.read_bytes()).hexdigest()
|
||||||
|
@@ -125,14 +125,14 @@ class Command(MultiProcessMixin, ProgressBarMixin, BaseCommand):
|
|||||||
messages.append(
|
messages.append(
|
||||||
self.style.NOTICE(
|
self.style.NOTICE(
|
||||||
f"Document {result.doc_one_pk} fuzzy match"
|
f"Document {result.doc_one_pk} fuzzy match"
|
||||||
f" to {result.doc_two_pk} (confidence {result.ratio:.3f})\n",
|
f" to {result.doc_two_pk} (confidence {result.ratio:.3f})",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
maybe_delete_ids.append(result.doc_two_pk)
|
maybe_delete_ids.append(result.doc_two_pk)
|
||||||
|
|
||||||
if len(messages) == 0:
|
if len(messages) == 0:
|
||||||
messages.append(
|
messages.append(
|
||||||
self.style.SUCCESS("No matches found\n"),
|
self.style.SUCCESS("No matches found"),
|
||||||
)
|
)
|
||||||
self.stdout.writelines(
|
self.stdout.writelines(
|
||||||
messages,
|
messages,
|
||||||
|
@@ -63,11 +63,11 @@ class Document:
|
|||||||
/ "documents"
|
/ "documents"
|
||||||
/ "originals"
|
/ "originals"
|
||||||
/ f"{self.pk:07}.{self.file_type}.gpg"
|
/ f"{self.pk:07}.{self.file_type}.gpg"
|
||||||
)
|
).as_posix()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def source_file(self):
|
def source_file(self):
|
||||||
return self.source_path.open("rb")
|
return Path(self.source_path).open("rb")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_name(self):
|
def file_name(self):
|
||||||
|
@@ -2038,24 +2038,6 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def normalize_workflow_trigger_sources(trigger):
|
|
||||||
"""
|
|
||||||
Convert sources to strings to handle django-multiselectfield v1.0 changes
|
|
||||||
"""
|
|
||||||
if trigger and "sources" in trigger:
|
|
||||||
trigger["sources"] = [
|
|
||||||
str(s.value if hasattr(s, "value") else s) for s in trigger["sources"]
|
|
||||||
]
|
|
||||||
|
|
||||||
def create(self, validated_data):
|
|
||||||
WorkflowTriggerSerializer.normalize_workflow_trigger_sources(validated_data)
|
|
||||||
return super().create(validated_data)
|
|
||||||
|
|
||||||
def update(self, instance, validated_data):
|
|
||||||
WorkflowTriggerSerializer.normalize_workflow_trigger_sources(validated_data)
|
|
||||||
return super().update(instance, validated_data)
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowActionEmailSerializer(serializers.ModelSerializer):
|
class WorkflowActionEmailSerializer(serializers.ModelSerializer):
|
||||||
id = serializers.IntegerField(allow_null=True, required=False)
|
id = serializers.IntegerField(allow_null=True, required=False)
|
||||||
@@ -2220,8 +2202,6 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
|||||||
if triggers is not None and triggers is not serializers.empty:
|
if triggers is not None and triggers is not serializers.empty:
|
||||||
for trigger in triggers:
|
for trigger in triggers:
|
||||||
filter_has_tags = trigger.pop("filter_has_tags", None)
|
filter_has_tags = trigger.pop("filter_has_tags", None)
|
||||||
# Convert sources to strings to handle django-multiselectfield v1.0 changes
|
|
||||||
WorkflowTriggerSerializer.normalize_workflow_trigger_sources(trigger)
|
|
||||||
trigger_instance, _ = WorkflowTrigger.objects.update_or_create(
|
trigger_instance, _ = WorkflowTrigger.objects.update_or_create(
|
||||||
id=trigger.get("id"),
|
id=trigger.get("id"),
|
||||||
defaults=trigger,
|
defaults=trigger,
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -51,6 +51,8 @@ from documents.permissions import set_permissions_for_object
|
|||||||
from documents.templating.workflows import parse_w_workflow_placeholders
|
from documents.templating.workflows import parse_w_workflow_placeholders
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from documents.classifier import DocumentClassifier
|
from documents.classifier import DocumentClassifier
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
@@ -327,16 +329,15 @@ def cleanup_document_deletion(sender, instance, **kwargs):
|
|||||||
# Find a non-conflicting filename in case a document with the same
|
# Find a non-conflicting filename in case a document with the same
|
||||||
# name was moved to trash earlier
|
# name was moved to trash earlier
|
||||||
counter = 0
|
counter = 0
|
||||||
old_filename = Path(instance.source_path).name
|
old_filename = os.path.split(instance.source_path)[1]
|
||||||
old_filebase = Path(old_filename).stem
|
(old_filebase, old_fileext) = os.path.splitext(old_filename)
|
||||||
old_fileext = Path(old_filename).suffix
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
new_file_path = settings.EMPTY_TRASH_DIR / (
|
new_file_path = settings.EMPTY_TRASH_DIR / (
|
||||||
old_filebase + (f"_{counter:02}" if counter else "") + old_fileext
|
old_filebase + (f"_{counter:02}" if counter else "") + old_fileext
|
||||||
)
|
)
|
||||||
|
|
||||||
if new_file_path.exists():
|
if os.path.exists(new_file_path):
|
||||||
counter += 1
|
counter += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
@@ -360,26 +361,26 @@ def cleanup_document_deletion(sender, instance, **kwargs):
|
|||||||
files += (instance.source_path,)
|
files += (instance.source_path,)
|
||||||
|
|
||||||
for filename in files:
|
for filename in files:
|
||||||
if filename and filename.is_file():
|
if filename and os.path.isfile(filename):
|
||||||
try:
|
try:
|
||||||
filename.unlink()
|
os.unlink(filename)
|
||||||
logger.debug(f"Deleted file {filename}.")
|
logger.debug(f"Deleted file {filename}.")
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"While deleting document {instance!s}, the file "
|
f"While deleting document {instance!s}, the file "
|
||||||
f"{filename} could not be deleted: {e}",
|
f"{filename} could not be deleted: {e}",
|
||||||
)
|
)
|
||||||
elif filename and not filename.is_file():
|
elif filename and not os.path.isfile(filename):
|
||||||
logger.warning(f"Expected {filename} to exist, but it did not")
|
logger.warning(f"Expected {filename} to exist, but it did not")
|
||||||
|
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
Path(instance.source_path).parent,
|
os.path.dirname(instance.source_path),
|
||||||
root=settings.ORIGINALS_DIR,
|
root=settings.ORIGINALS_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
if instance.has_archive_version:
|
if instance.has_archive_version:
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
Path(instance.archive_path).parent,
|
os.path.dirname(instance.archive_path),
|
||||||
root=settings.ARCHIVE_DIR,
|
root=settings.ARCHIVE_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -400,14 +401,14 @@ def update_filename_and_move_files(
|
|||||||
if isinstance(instance, CustomFieldInstance):
|
if isinstance(instance, CustomFieldInstance):
|
||||||
instance = instance.document
|
instance = instance.document
|
||||||
|
|
||||||
def validate_move(instance, old_path: Path, new_path: Path):
|
def validate_move(instance, old_path, new_path):
|
||||||
if not old_path.is_file():
|
if not os.path.isfile(old_path):
|
||||||
# Can't do anything if the old file does not exist anymore.
|
# Can't do anything if the old file does not exist anymore.
|
||||||
msg = f"Document {instance!s}: File {old_path} doesn't exist."
|
msg = f"Document {instance!s}: File {old_path} doesn't exist."
|
||||||
logger.fatal(msg)
|
logger.fatal(msg)
|
||||||
raise CannotMoveFilesException(msg)
|
raise CannotMoveFilesException(msg)
|
||||||
|
|
||||||
if new_path.is_file():
|
if os.path.isfile(new_path):
|
||||||
# Can't do anything if the new file already exists. Skip updating file.
|
# Can't do anything if the new file already exists. Skip updating file.
|
||||||
msg = f"Document {instance!s}: Cannot rename file since target path {new_path} already exists."
|
msg = f"Document {instance!s}: Cannot rename file since target path {new_path} already exists."
|
||||||
logger.warning(msg)
|
logger.warning(msg)
|
||||||
@@ -435,20 +436,16 @@ def update_filename_and_move_files(
|
|||||||
old_filename = instance.filename
|
old_filename = instance.filename
|
||||||
old_source_path = instance.source_path
|
old_source_path = instance.source_path
|
||||||
|
|
||||||
# Need to convert to string to be able to save it to the db
|
instance.filename = generate_unique_filename(instance)
|
||||||
instance.filename = str(generate_unique_filename(instance))
|
|
||||||
move_original = old_filename != instance.filename
|
move_original = old_filename != instance.filename
|
||||||
|
|
||||||
old_archive_filename = instance.archive_filename
|
old_archive_filename = instance.archive_filename
|
||||||
old_archive_path = instance.archive_path
|
old_archive_path = instance.archive_path
|
||||||
|
|
||||||
if instance.has_archive_version:
|
if instance.has_archive_version:
|
||||||
# Need to convert to string to be able to save it to the db
|
instance.archive_filename = generate_unique_filename(
|
||||||
instance.archive_filename = str(
|
instance,
|
||||||
generate_unique_filename(
|
archive_filename=True,
|
||||||
instance,
|
|
||||||
archive_filename=True,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
move_archive = old_archive_filename != instance.archive_filename
|
move_archive = old_archive_filename != instance.archive_filename
|
||||||
@@ -490,11 +487,11 @@ def update_filename_and_move_files(
|
|||||||
|
|
||||||
# Try to move files to their original location.
|
# Try to move files to their original location.
|
||||||
try:
|
try:
|
||||||
if move_original and instance.source_path.is_file():
|
if move_original and os.path.isfile(instance.source_path):
|
||||||
logger.info("Restoring previous original path")
|
logger.info("Restoring previous original path")
|
||||||
shutil.move(instance.source_path, old_source_path)
|
shutil.move(instance.source_path, old_source_path)
|
||||||
|
|
||||||
if move_archive and instance.archive_path.is_file():
|
if move_archive and os.path.isfile(instance.archive_path):
|
||||||
logger.info("Restoring previous archive path")
|
logger.info("Restoring previous archive path")
|
||||||
shutil.move(instance.archive_path, old_archive_path)
|
shutil.move(instance.archive_path, old_archive_path)
|
||||||
|
|
||||||
@@ -515,15 +512,17 @@ def update_filename_and_move_files(
|
|||||||
|
|
||||||
# finally, remove any empty sub folders. This will do nothing if
|
# finally, remove any empty sub folders. This will do nothing if
|
||||||
# something has failed above.
|
# something has failed above.
|
||||||
if not old_source_path.is_file():
|
if not os.path.isfile(old_source_path):
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
Path(old_source_path).parent,
|
os.path.dirname(old_source_path),
|
||||||
root=settings.ORIGINALS_DIR,
|
root=settings.ORIGINALS_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
if instance.has_archive_version and not old_archive_path.is_file():
|
if instance.has_archive_version and not os.path.isfile(
|
||||||
|
old_archive_path,
|
||||||
|
):
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
Path(old_archive_path).parent,
|
os.path.dirname(old_archive_path),
|
||||||
root=settings.ARCHIVE_DIR,
|
root=settings.ARCHIVE_DIR,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1220,7 +1219,10 @@ def run_workflows(
|
|||||||
)
|
)
|
||||||
files = None
|
files = None
|
||||||
if action.webhook.include_document:
|
if action.webhook.include_document:
|
||||||
with original_file.open("rb") as f:
|
with open(
|
||||||
|
original_file,
|
||||||
|
"rb",
|
||||||
|
) as f:
|
||||||
files = {
|
files = {
|
||||||
"file": (
|
"file": (
|
||||||
filename,
|
filename,
|
||||||
|
@@ -1,34 +0,0 @@
|
|||||||
Sample textual document content.
|
|
||||||
Include as many characters as possible, to check the classifier's vectorization.
|
|
||||||
|
|
||||||
Hey 00, this is "a" test0707 content.
|
|
||||||
This is an example document — created on 2025-06-25.
|
|
||||||
|
|
||||||
Digits: 0123456789
|
|
||||||
Punctuation: . , ; : ! ? ' " ( ) [ ] { } — – …
|
|
||||||
English text: The quick brown fox jumps over the lazy dog.
|
|
||||||
English stop words: We’ve been doing it before.
|
|
||||||
Accented Latin (diacritics): àâäæçéèêëîïôœùûüÿñ
|
|
||||||
Arabic: لقد قام المترجم بعمل جيد
|
|
||||||
Greek: Αλφα, Βήτα, Γάμμα, Δέλτα, Ωμέγα
|
|
||||||
Cyrillic: Привет, как дела? Добро пожаловать!
|
|
||||||
Chinese (Simplified): 你好,世界!今天的天气很好。
|
|
||||||
Chinese (Traditional): 歡迎來到世界,今天天氣很好。
|
|
||||||
Japanese (Kanji, Hiragana, Katakana): 東京へ行きます。カタカナ、ひらがな、漢字。
|
|
||||||
Korean (Hangul): 안녕하세요. 오늘 날씨 어때요?
|
|
||||||
Arabic: مرحبًا، كيف حالك؟
|
|
||||||
Hebrew: שלום, מה שלומך?
|
|
||||||
Emoji: 😀 🐍 📘 ✅ ©️ 🇺🇳
|
|
||||||
Symbols: © ® ™ § ¶ † ‡ ∞ µ ∑ ∆ √
|
|
||||||
Math: ∫₀^∞ x² dx = ∞, π ≈ 3.14159, ∇·E = ρ/ε₀
|
|
||||||
Currency: 1$ € ¥ £ ₹
|
|
||||||
Date formats: 25/06/2025, June 25, 2025, 2025年6月25日
|
|
||||||
Quote in French: « Bonjour, ça va ? »
|
|
||||||
Quote in German: „Guten Tag! Wie geht's?“
|
|
||||||
Newline test:
|
|
||||||
\r\n
|
|
||||||
\r
|
|
||||||
|
|
||||||
Tab\ttest\tspacing
|
|
||||||
/ = +) ( []) ~ * #192 +33601010101 § ¤
|
|
||||||
End of document.
|
|
@@ -1 +0,0 @@
|
|||||||
sample textual document content include as many characters as possible to check the classifier s vectorization hey 00 this is a test0707 content this is an example document created on 2025 06 25 digits 0123456789 punctuation english text the quick brown fox jumps over the lazy dog english stop words we ve been doing it before accented latin diacritics àâäæçéèêëîïôœùûüÿñ arabic لقد قام المترجم بعمل جيد greek αλφα βήτα γάμμα δέλτα ωμέγα cyrillic привет как дела добро пожаловать chinese simplified 你好 世界 今天的天气很好 chinese traditional 歡迎來到世界 今天天氣很好 japanese kanji hiragana katakana 東京へ行きます カタカナ ひらがな 漢字 korean hangul 안녕하세요 오늘 날씨 어때요 arabic مرحب ا كيف حالك hebrew שלום מה שלומך emoji symbols µ math ₀ x² dx π 3 14159 e ρ ε₀ currency 1 date formats 25 06 2025 june 25 2025 2025年6月25日 quote in french bonjour ça va quote in german guten tag wie geht s newline test r n r tab ttest tspacing 192 33601010101 end of document
|
|
@@ -1 +0,0 @@
|
|||||||
sampl textual document content includ mani charact possibl check classifi vector hey 00 test0707 content exampl document creat 2025 06 25 digit 0123456789 punctuat english text quick brown fox jump lazi dog english stop word accent latin diacrit àâäæçéèêëîïôœùûüÿñ arab لقد قام المترجم بعمل جيد greek αλφα βήτα γάμμα δέλτα ωμέγα cyril привет как дела добро пожаловать chines simplifi 你好 世界 今天的天气很好 chines tradit 歡迎來到世界 今天天氣很好 japanes kanji hiragana katakana 東京へ行きます カタカナ ひらがな 漢字 korean hangul 안녕하세요 오늘 날씨 어때요 arab مرحب ا كيف حالك hebrew שלום מה שלומך emoji symbol µ math ₀ x² dx π 3 14159 e ρ ε₀ currenc 1 date format 25 06 2025 june 25 2025 2025年6月25日 quot french bonjour ça va quot german guten tag wie geht newlin test r n r tab ttest tspace 192 33601010101 end document
|
|
@@ -1,45 +0,0 @@
|
|||||||
import pickle
|
|
||||||
|
|
||||||
from documents.caching import StoredLRUCache
|
|
||||||
|
|
||||||
|
|
||||||
def test_lru_cache_entries():
|
|
||||||
CACHE_TTL = 1
|
|
||||||
# LRU cache with a capacity of 2 elements
|
|
||||||
cache = StoredLRUCache("test_lru_cache_key", 2, backend_ttl=CACHE_TTL)
|
|
||||||
cache.set(1, 1)
|
|
||||||
cache.set(2, 2)
|
|
||||||
assert cache.get(2) == 2
|
|
||||||
assert cache.get(1) == 1
|
|
||||||
|
|
||||||
# The oldest entry (2) should be removed
|
|
||||||
cache.set(3, 3)
|
|
||||||
assert cache.get(3) == 3
|
|
||||||
assert not cache.get(2)
|
|
||||||
assert cache.get(1) == 1
|
|
||||||
|
|
||||||
# Save the cache, restore it and check it overwrites the current cache in memory
|
|
||||||
cache.save()
|
|
||||||
cache.set(4, 4)
|
|
||||||
assert not cache.get(3)
|
|
||||||
cache.load()
|
|
||||||
assert not cache.get(4)
|
|
||||||
assert cache.get(3) == 3
|
|
||||||
assert cache.get(1) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_stored_lru_cache_key_ttl(mocker):
|
|
||||||
mock_backend = mocker.Mock()
|
|
||||||
cache = StoredLRUCache("test_key", backend=mock_backend, backend_ttl=321)
|
|
||||||
|
|
||||||
# Simulate storing values
|
|
||||||
cache.set("x", "X")
|
|
||||||
cache.set("y", "Y")
|
|
||||||
cache.save()
|
|
||||||
|
|
||||||
# Assert backend.set was called with pickled data, key and TTL
|
|
||||||
mock_backend.set.assert_called_once()
|
|
||||||
key, data, timeout = mock_backend.set.call_args[0]
|
|
||||||
assert key == "test_key"
|
|
||||||
assert timeout == 321
|
|
||||||
assert pickle.loads(data) == {"x": "X", "y": "Y"}
|
|
@@ -21,7 +21,7 @@ from documents.models import Tag
|
|||||||
from documents.tests.utils import DirectoriesMixin
|
from documents.tests.utils import DirectoriesMixin
|
||||||
|
|
||||||
|
|
||||||
def dummy_preprocess(content: str, **kwargs):
|
def dummy_preprocess(content: str):
|
||||||
"""
|
"""
|
||||||
Simpler, faster pre-processing for testing purposes
|
Simpler, faster pre-processing for testing purposes
|
||||||
"""
|
"""
|
||||||
@@ -223,47 +223,24 @@ class TestClassifier(DirectoriesMixin, TestCase):
|
|||||||
self.generate_test_data()
|
self.generate_test_data()
|
||||||
self.classifier.train()
|
self.classifier.train()
|
||||||
|
|
||||||
with (
|
self.assertEqual(
|
||||||
mock.patch.object(
|
self.classifier.predict_correspondent(self.doc1.content),
|
||||||
self.classifier.data_vectorizer,
|
self.c1.pk,
|
||||||
"transform",
|
)
|
||||||
wraps=self.classifier.data_vectorizer.transform,
|
self.assertEqual(self.classifier.predict_correspondent(self.doc2.content), None)
|
||||||
) as mock_transform,
|
self.assertListEqual(
|
||||||
mock.patch.object(
|
self.classifier.predict_tags(self.doc1.content),
|
||||||
self.classifier,
|
[self.t1.pk],
|
||||||
"preprocess_content",
|
)
|
||||||
wraps=self.classifier.preprocess_content,
|
self.assertListEqual(
|
||||||
) as mock_preprocess_content,
|
self.classifier.predict_tags(self.doc2.content),
|
||||||
):
|
[self.t1.pk, self.t3.pk],
|
||||||
self.assertEqual(
|
)
|
||||||
self.classifier.predict_correspondent(self.doc1.content),
|
self.assertEqual(
|
||||||
self.c1.pk,
|
self.classifier.predict_document_type(self.doc1.content),
|
||||||
)
|
self.dt.pk,
|
||||||
self.assertEqual(
|
)
|
||||||
self.classifier.predict_correspondent(self.doc2.content),
|
self.assertEqual(self.classifier.predict_document_type(self.doc2.content), None)
|
||||||
None,
|
|
||||||
)
|
|
||||||
self.assertListEqual(
|
|
||||||
self.classifier.predict_tags(self.doc1.content),
|
|
||||||
[self.t1.pk],
|
|
||||||
)
|
|
||||||
self.assertListEqual(
|
|
||||||
self.classifier.predict_tags(self.doc2.content),
|
|
||||||
[self.t1.pk, self.t3.pk],
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.classifier.predict_document_type(self.doc1.content),
|
|
||||||
self.dt.pk,
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
self.classifier.predict_document_type(self.doc2.content),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check that the classifier vectorized content and text preprocessing has been cached
|
|
||||||
# It should be called once per document (doc1 and doc2)
|
|
||||||
self.assertEqual(mock_preprocess_content.call_count, 2)
|
|
||||||
self.assertEqual(mock_transform.call_count, 2)
|
|
||||||
|
|
||||||
def test_no_retrain_if_no_change(self):
|
def test_no_retrain_if_no_change(self):
|
||||||
"""
|
"""
|
||||||
@@ -717,67 +694,3 @@ class TestClassifier(DirectoriesMixin, TestCase):
|
|||||||
mock_load.side_effect = Exception()
|
mock_load.side_effect = Exception()
|
||||||
with self.assertRaises(Exception):
|
with self.assertRaises(Exception):
|
||||||
load_classifier(raise_exception=True)
|
load_classifier(raise_exception=True)
|
||||||
|
|
||||||
|
|
||||||
def test_preprocess_content():
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- Advanced text processing is enabled (default)
|
|
||||||
WHEN:
|
|
||||||
- Classifier preprocesses a document's content
|
|
||||||
THEN:
|
|
||||||
- Processed content matches the expected output (stemmed words)
|
|
||||||
"""
|
|
||||||
with (Path(__file__).parent / "samples" / "content.txt").open("r") as f:
|
|
||||||
content = f.read()
|
|
||||||
with (Path(__file__).parent / "samples" / "preprocessed_content_advanced.txt").open(
|
|
||||||
"r",
|
|
||||||
) as f:
|
|
||||||
expected_preprocess_content = f.read().rstrip()
|
|
||||||
classifier = DocumentClassifier()
|
|
||||||
result = classifier.preprocess_content(content)
|
|
||||||
assert result == expected_preprocess_content
|
|
||||||
|
|
||||||
|
|
||||||
def test_preprocess_content_nltk_disabled():
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- Advanced text processing is disabled
|
|
||||||
WHEN:
|
|
||||||
- Classifier preprocesses a document's content
|
|
||||||
THEN:
|
|
||||||
- Processed content matches the expected output (unstemmed words)
|
|
||||||
"""
|
|
||||||
with (Path(__file__).parent / "samples" / "content.txt").open("r") as f:
|
|
||||||
content = f.read()
|
|
||||||
with (Path(__file__).parent / "samples" / "preprocessed_content.txt").open(
|
|
||||||
"r",
|
|
||||||
) as f:
|
|
||||||
expected_preprocess_content = f.read().rstrip()
|
|
||||||
classifier = DocumentClassifier()
|
|
||||||
with mock.patch("documents.classifier.ADVANCED_TEXT_PROCESSING_ENABLED", new=False):
|
|
||||||
result = classifier.preprocess_content(content)
|
|
||||||
assert result == expected_preprocess_content
|
|
||||||
|
|
||||||
|
|
||||||
def test_preprocess_content_nltk_load_fail(mocker):
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- NLTK stop words fail to load
|
|
||||||
WHEN:
|
|
||||||
- Classifier preprocesses a document's content
|
|
||||||
THEN:
|
|
||||||
- Processed content matches the expected output (unstemmed words)
|
|
||||||
"""
|
|
||||||
_module = mocker.MagicMock(name="nltk_corpus_mock")
|
|
||||||
_module.stopwords.words.side_effect = AttributeError()
|
|
||||||
mocker.patch.dict("sys.modules", {"nltk.corpus": _module})
|
|
||||||
classifier = DocumentClassifier()
|
|
||||||
with (Path(__file__).parent / "samples" / "content.txt").open("r") as f:
|
|
||||||
content = f.read()
|
|
||||||
with (Path(__file__).parent / "samples" / "preprocessed_content.txt").open(
|
|
||||||
"r",
|
|
||||||
) as f:
|
|
||||||
expected_preprocess_content = f.read().rstrip()
|
|
||||||
result = classifier.preprocess_content(content)
|
|
||||||
assert result == expected_preprocess_content
|
|
||||||
|
@@ -41,9 +41,11 @@ class TestDocument(TestCase):
|
|||||||
Path(file_path).touch()
|
Path(file_path).touch()
|
||||||
Path(thumb_path).touch()
|
Path(thumb_path).touch()
|
||||||
|
|
||||||
with mock.patch("documents.signals.handlers.Path.unlink") as mock_unlink:
|
with mock.patch("documents.signals.handlers.os.unlink") as mock_unlink:
|
||||||
document.delete()
|
document.delete()
|
||||||
empty_trash([document.pk])
|
empty_trash([document.pk])
|
||||||
|
mock_unlink.assert_any_call(file_path)
|
||||||
|
mock_unlink.assert_any_call(thumb_path)
|
||||||
self.assertEqual(mock_unlink.call_count, 2)
|
self.assertEqual(mock_unlink.call_count, 2)
|
||||||
|
|
||||||
def test_document_soft_delete(self):
|
def test_document_soft_delete(self):
|
||||||
@@ -61,7 +63,7 @@ class TestDocument(TestCase):
|
|||||||
Path(file_path).touch()
|
Path(file_path).touch()
|
||||||
Path(thumb_path).touch()
|
Path(thumb_path).touch()
|
||||||
|
|
||||||
with mock.patch("documents.signals.handlers.Path.unlink") as mock_unlink:
|
with mock.patch("documents.signals.handlers.os.unlink") as mock_unlink:
|
||||||
document.delete()
|
document.delete()
|
||||||
self.assertEqual(mock_unlink.call_count, 0)
|
self.assertEqual(mock_unlink.call_count, 0)
|
||||||
|
|
||||||
|
@@ -34,12 +34,12 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
document.save()
|
document.save()
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path(f"{document.pk:07d}.pdf"))
|
self.assertEqual(generate_filename(document), f"{document.pk:07d}.pdf")
|
||||||
|
|
||||||
document.storage_type = Document.STORAGE_TYPE_GPG
|
document.storage_type = Document.STORAGE_TYPE_GPG
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(document),
|
generate_filename(document),
|
||||||
Path(f"{document.pk:07d}.pdf.gpg"),
|
f"{document.pk:07d}.pdf.gpg",
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}")
|
||||||
@@ -58,12 +58,12 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf"))
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
|
|
||||||
# Enable encryption and check again
|
# Enable encryption and check again
|
||||||
document.storage_type = Document.STORAGE_TYPE_GPG
|
document.storage_type = Document.STORAGE_TYPE_GPG
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf.gpg"))
|
self.assertEqual(document.filename, "none/none.pdf.gpg")
|
||||||
|
|
||||||
document.save()
|
document.save()
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf"))
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
document.source_path.touch()
|
document.source_path.touch()
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf"))
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
Path(document.source_path).touch()
|
Path(document.source_path).touch()
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("none/none.pdf"))
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
|
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
|
|
||||||
@@ -269,11 +269,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
dt = DocumentType.objects.create(name="my_doc_type")
|
dt = DocumentType.objects.create(name="my_doc_type")
|
||||||
d = Document.objects.create(title="the_doc", mime_type="application/pdf")
|
d = Document.objects.create(title="the_doc", mime_type="application/pdf")
|
||||||
|
|
||||||
self.assertEqual(generate_filename(d), Path("none - the_doc.pdf"))
|
self.assertEqual(generate_filename(d), "none - the_doc.pdf")
|
||||||
|
|
||||||
d.document_type = dt
|
d.document_type = dt
|
||||||
|
|
||||||
self.assertEqual(generate_filename(d), Path("my_doc_type - the_doc.pdf"))
|
self.assertEqual(generate_filename(d), "my_doc_type - the_doc.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{asn} - {title}")
|
@override_settings(FILENAME_FORMAT="{asn} - {title}")
|
||||||
def test_asn(self):
|
def test_asn(self):
|
||||||
@@ -289,8 +289,8 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
archive_serial_number=None,
|
archive_serial_number=None,
|
||||||
checksum="B",
|
checksum="B",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(d1), Path("652 - the_doc.pdf"))
|
self.assertEqual(generate_filename(d1), "652 - the_doc.pdf")
|
||||||
self.assertEqual(generate_filename(d2), Path("none - the_doc.pdf"))
|
self.assertEqual(generate_filename(d2), "none - the_doc.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{title} {tag_list}")
|
@override_settings(FILENAME_FORMAT="{title} {tag_list}")
|
||||||
def test_tag_list(self):
|
def test_tag_list(self):
|
||||||
@@ -298,7 +298,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
doc.tags.create(name="tag2")
|
doc.tags.create(name="tag2")
|
||||||
doc.tags.create(name="tag1")
|
doc.tags.create(name="tag1")
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc), Path("doc1 tag1,tag2.pdf"))
|
self.assertEqual(generate_filename(doc), "doc1 tag1,tag2.pdf")
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="doc2",
|
title="doc2",
|
||||||
@@ -306,7 +306,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc), Path("doc2.pdf"))
|
self.assertEqual(generate_filename(doc), "doc2.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="//etc/something/{title}")
|
@override_settings(FILENAME_FORMAT="//etc/something/{title}")
|
||||||
def test_filename_relative(self):
|
def test_filename_relative(self):
|
||||||
@@ -330,11 +330,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
created=d1,
|
created=d1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc1), Path("2020-03-06.pdf"))
|
self.assertEqual(generate_filename(doc1), "2020-03-06.pdf")
|
||||||
|
|
||||||
doc1.created = datetime.date(2020, 11, 16)
|
doc1.created = datetime.date(2020, 11, 16)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc1), Path("2020-11-16.pdf"))
|
self.assertEqual(generate_filename(doc1), "2020-11-16.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{added_year}-{added_month}-{added_day}",
|
FILENAME_FORMAT="{added_year}-{added_month}-{added_day}",
|
||||||
@@ -347,11 +347,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
added=d1,
|
added=d1,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc1), Path("232-01-09.pdf"))
|
self.assertEqual(generate_filename(doc1), "232-01-09.pdf")
|
||||||
|
|
||||||
doc1.added = timezone.make_aware(datetime.datetime(2020, 11, 16, 1, 1, 1))
|
doc1.added = timezone.make_aware(datetime.datetime(2020, 11, 16, 1, 1, 1))
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc1), Path("2020-11-16.pdf"))
|
self.assertEqual(generate_filename(doc1), "2020-11-16.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{correspondent}/{correspondent}/{correspondent}",
|
FILENAME_FORMAT="{correspondent}/{correspondent}/{correspondent}",
|
||||||
@@ -389,11 +389,11 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.mime_type = "application/pdf"
|
document.mime_type = "application/pdf"
|
||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0000001.pdf"))
|
self.assertEqual(generate_filename(document), "0000001.pdf")
|
||||||
|
|
||||||
document.pk = 13579
|
document.pk = 13579
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0013579.pdf"))
|
self.assertEqual(generate_filename(document), "0013579.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT=None)
|
@override_settings(FILENAME_FORMAT=None)
|
||||||
def test_format_none(self):
|
def test_format_none(self):
|
||||||
@@ -402,7 +402,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.mime_type = "application/pdf"
|
document.mime_type = "application/pdf"
|
||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0000001.pdf"))
|
self.assertEqual(generate_filename(document), "0000001.pdf")
|
||||||
|
|
||||||
def test_try_delete_empty_directories(self):
|
def test_try_delete_empty_directories(self):
|
||||||
# Create our working directory
|
# Create our working directory
|
||||||
@@ -428,7 +428,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.mime_type = "application/pdf"
|
document.mime_type = "application/pdf"
|
||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0000001.pdf"))
|
self.assertEqual(generate_filename(document), "0000001.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{created__year}")
|
@override_settings(FILENAME_FORMAT="{created__year}")
|
||||||
def test_invalid_format_key(self):
|
def test_invalid_format_key(self):
|
||||||
@@ -437,7 +437,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.mime_type = "application/pdf"
|
document.mime_type = "application/pdf"
|
||||||
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
document.storage_type = Document.STORAGE_TYPE_UNENCRYPTED
|
||||||
|
|
||||||
self.assertEqual(generate_filename(document), Path("0000001.pdf"))
|
self.assertEqual(generate_filename(document), "0000001.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{title}")
|
@override_settings(FILENAME_FORMAT="{title}")
|
||||||
def test_duplicates(self):
|
def test_duplicates(self):
|
||||||
@@ -564,7 +564,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
value_select="abc123",
|
value_select="abc123",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc), Path("document_apple.pdf"))
|
self.assertEqual(generate_filename(doc), "document_apple.pdf")
|
||||||
|
|
||||||
# handler should not have been called
|
# handler should not have been called
|
||||||
self.assertEqual(m.call_count, 0)
|
self.assertEqual(m.call_count, 0)
|
||||||
@@ -576,7 +576,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
cf.save()
|
cf.save()
|
||||||
self.assertEqual(generate_filename(doc), Path("document_aubergine.pdf"))
|
self.assertEqual(generate_filename(doc), "document_aubergine.pdf")
|
||||||
# handler should have been called
|
# handler should have been called
|
||||||
self.assertEqual(m.call_count, 1)
|
self.assertEqual(m.call_count, 1)
|
||||||
|
|
||||||
@@ -897,7 +897,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=1,
|
pk=1,
|
||||||
checksum="1",
|
checksum="1",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("This. is the title.pdf"))
|
self.assertEqual(generate_filename(doc), "This. is the title.pdf")
|
||||||
|
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
title="my\\invalid/../title:yay",
|
title="my\\invalid/../title:yay",
|
||||||
@@ -905,7 +905,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=2,
|
pk=2,
|
||||||
checksum="2",
|
checksum="2",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("my-invalid-..-title-yay.pdf"))
|
self.assertEqual(generate_filename(doc), "my-invalid-..-title-yay.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{created}")
|
@override_settings(FILENAME_FORMAT="{created}")
|
||||||
def test_date(self):
|
def test_date(self):
|
||||||
@@ -916,7 +916,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=2,
|
pk=2,
|
||||||
checksum="2",
|
checksum="2",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("2020-05-21.pdf"))
|
self.assertEqual(generate_filename(doc), "2020-05-21.pdf")
|
||||||
|
|
||||||
def test_dynamic_path(self):
|
def test_dynamic_path(self):
|
||||||
"""
|
"""
|
||||||
@@ -935,7 +935,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
checksum="2",
|
checksum="2",
|
||||||
storage_path=StoragePath.objects.create(path="TestFolder/{{created}}"),
|
storage_path=StoragePath.objects.create(path="TestFolder/{{created}}"),
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("TestFolder/2020-06-25.pdf"))
|
self.assertEqual(generate_filename(doc), "TestFolder/2020-06-25.pdf")
|
||||||
|
|
||||||
def test_dynamic_path_with_none(self):
|
def test_dynamic_path_with_none(self):
|
||||||
"""
|
"""
|
||||||
@@ -956,7 +956,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
checksum="2",
|
checksum="2",
|
||||||
storage_path=StoragePath.objects.create(path="{{asn}} - {{created}}"),
|
storage_path=StoragePath.objects.create(path="{{asn}} - {{created}}"),
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("none - 2020-06-25.pdf"))
|
self.assertEqual(generate_filename(doc), "none - 2020-06-25.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT_REMOVE_NONE=True,
|
FILENAME_FORMAT_REMOVE_NONE=True,
|
||||||
@@ -984,7 +984,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
checksum="2",
|
checksum="2",
|
||||||
storage_path=sp,
|
storage_path=sp,
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("TestFolder/2020-06-25.pdf"))
|
self.assertEqual(generate_filename(doc), "TestFolder/2020-06-25.pdf")
|
||||||
|
|
||||||
# Special case, undefined variable, then defined at the start of the template
|
# Special case, undefined variable, then defined at the start of the template
|
||||||
# This could lead to an absolute path after we remove the leading -none-, but leave the leading /
|
# This could lead to an absolute path after we remove the leading -none-, but leave the leading /
|
||||||
@@ -993,7 +993,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
"{{ owner_username }}/{{ created_year }}/{{ correspondent }}/{{ title }}"
|
"{{ owner_username }}/{{ created_year }}/{{ correspondent }}/{{ title }}"
|
||||||
)
|
)
|
||||||
sp.save()
|
sp.save()
|
||||||
self.assertEqual(generate_filename(doc), Path("2020/does not matter.pdf"))
|
self.assertEqual(generate_filename(doc), "2020/does not matter.pdf")
|
||||||
|
|
||||||
def test_multiple_doc_paths(self):
|
def test_multiple_doc_paths(self):
|
||||||
"""
|
"""
|
||||||
@@ -1028,14 +1028,8 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(generate_filename(doc_a), "ThisIsAFolder/4/2020-06-25.pdf")
|
||||||
generate_filename(doc_a),
|
self.assertEqual(generate_filename(doc_b), "SomeImportantNone/2020-07-25.pdf")
|
||||||
Path("ThisIsAFolder/4/2020-06-25.pdf"),
|
|
||||||
)
|
|
||||||
self.assertEqual(
|
|
||||||
generate_filename(doc_b),
|
|
||||||
Path("SomeImportantNone/2020-07-25.pdf"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT=None,
|
FILENAME_FORMAT=None,
|
||||||
@@ -1070,11 +1064,8 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc_a), Path("0000002.pdf"))
|
self.assertEqual(generate_filename(doc_a), "0000002.pdf")
|
||||||
self.assertEqual(
|
self.assertEqual(generate_filename(doc_b), "SomeImportantNone/2020-07-25.pdf")
|
||||||
generate_filename(doc_b),
|
|
||||||
Path("SomeImportantNone/2020-07-25.pdf"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{created_year_short}/{created_month_name_short}/{created_month_name}/{title}",
|
FILENAME_FORMAT="{created_year_short}/{created_month_name_short}/{created_month_name}/{title}",
|
||||||
@@ -1087,7 +1078,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=2,
|
pk=2,
|
||||||
checksum="2",
|
checksum="2",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("89/Dec/December/The Title.pdf"))
|
self.assertEqual(generate_filename(doc), "89/Dec/December/The Title.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{added_year_short}/{added_month_name}/{added_month_name_short}/{title}",
|
FILENAME_FORMAT="{added_year_short}/{added_month_name}/{added_month_name_short}/{title}",
|
||||||
@@ -1100,7 +1091,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
pk=2,
|
pk=2,
|
||||||
checksum="2",
|
checksum="2",
|
||||||
)
|
)
|
||||||
self.assertEqual(generate_filename(doc), Path("84/August/Aug/The Title.pdf"))
|
self.assertEqual(generate_filename(doc), "84/August/Aug/The Title.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{owner_username}/{title}",
|
FILENAME_FORMAT="{owner_username}/{title}",
|
||||||
@@ -1133,8 +1124,8 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
checksum="3",
|
checksum="3",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(owned_doc), Path("user1/The Title.pdf"))
|
self.assertEqual(generate_filename(owned_doc), "user1/The Title.pdf")
|
||||||
self.assertEqual(generate_filename(no_owner_doc), Path("none/does matter.pdf"))
|
self.assertEqual(generate_filename(no_owner_doc), "none/does matter.pdf")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{original_name}",
|
FILENAME_FORMAT="{original_name}",
|
||||||
@@ -1180,20 +1171,17 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
original_filename="logs.txt",
|
original_filename="logs.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc_with_original), Path("someepdf.pdf"))
|
self.assertEqual(generate_filename(doc_with_original), "someepdf.pdf")
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(tricky_with_original),
|
generate_filename(tricky_with_original),
|
||||||
Path("some pdf with spaces and stuff.pdf"),
|
"some pdf with spaces and stuff.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(generate_filename(no_original), Path("none.pdf"))
|
self.assertEqual(generate_filename(no_original), "none.pdf")
|
||||||
|
|
||||||
self.assertEqual(generate_filename(text_doc), Path("logs.txt"))
|
self.assertEqual(generate_filename(text_doc), "logs.txt")
|
||||||
self.assertEqual(
|
self.assertEqual(generate_filename(text_doc, archive_filename=True), "logs.pdf")
|
||||||
generate_filename(text_doc, archive_filename=True),
|
|
||||||
Path("logs.pdf"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="XX{correspondent}/{title}",
|
FILENAME_FORMAT="XX{correspondent}/{title}",
|
||||||
@@ -1218,7 +1206,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
# Ensure that filename is properly generated
|
# Ensure that filename is properly generated
|
||||||
document.filename = generate_filename(document)
|
document.filename = generate_filename(document)
|
||||||
self.assertEqual(document.filename, Path("XX/doc1.pdf"))
|
self.assertEqual(document.filename, "XX/doc1.pdf")
|
||||||
|
|
||||||
def test_complex_template_strings(self):
|
def test_complex_template_strings(self):
|
||||||
"""
|
"""
|
||||||
@@ -1256,19 +1244,19 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("somepath/some where/2020-06-25/Does Matter.pdf"),
|
"somepath/some where/2020-06-25/Does Matter.pdf",
|
||||||
)
|
)
|
||||||
doc_a.checksum = "5"
|
doc_a.checksum = "5"
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("somepath/2024-10-01/Does Matter.pdf"),
|
"somepath/2024-10-01/Does Matter.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
sp.path = "{{ document.title|lower }}{{ document.archive_serial_number - 2 }}"
|
sp.path = "{{ document.title|lower }}{{ document.archive_serial_number - 2 }}"
|
||||||
sp.save()
|
sp.save()
|
||||||
|
|
||||||
self.assertEqual(generate_filename(doc_a), Path("does matter23.pdf"))
|
self.assertEqual(generate_filename(doc_a), "does matter23.pdf")
|
||||||
|
|
||||||
sp.path = """
|
sp.path = """
|
||||||
somepath/
|
somepath/
|
||||||
@@ -1287,13 +1275,13 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
sp.save()
|
sp.save()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("somepath/asn-000-200/Does Matter/Does Matter.pdf"),
|
"somepath/asn-000-200/Does Matter/Does Matter.pdf",
|
||||||
)
|
)
|
||||||
doc_a.archive_serial_number = 301
|
doc_a.archive_serial_number = 301
|
||||||
doc_a.save()
|
doc_a.save()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("somepath/asn-201-400/asn-3xx/Does Matter.pdf"),
|
"somepath/asn-201-400/asn-3xx/Does Matter.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
@@ -1322,7 +1310,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
with self.assertLogs(level=logging.WARNING) as capture:
|
with self.assertLogs(level=logging.WARNING) as capture:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("0000002.pdf"),
|
"0000002.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(len(capture.output), 1)
|
self.assertEqual(len(capture.output), 1)
|
||||||
@@ -1357,7 +1345,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
with self.assertLogs(level=logging.WARNING) as capture:
|
with self.assertLogs(level=logging.WARNING) as capture:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("0000002.pdf"),
|
"0000002.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(len(capture.output), 1)
|
self.assertEqual(len(capture.output), 1)
|
||||||
@@ -1425,7 +1413,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("invoices/1234.pdf"),
|
"invoices/1234.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
@@ -1439,7 +1427,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("Some Title_ChoiceOne.pdf"),
|
"Some Title_ChoiceOne.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for handling Nones well
|
# Check for handling Nones well
|
||||||
@@ -1448,7 +1436,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("Some Title_Default Value.pdf"),
|
"Some Title_Default Value.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
cf.name = "Invoice Number"
|
cf.name = "Invoice Number"
|
||||||
@@ -1461,7 +1449,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("invoices/4567.pdf"),
|
"invoices/4567.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
@@ -1469,7 +1457,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("invoices/0.pdf"),
|
"invoices/0.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_datetime_filter(self):
|
def test_datetime_filter(self):
|
||||||
@@ -1508,7 +1496,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("2020/Some Title.pdf"),
|
"2020/Some Title.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
@@ -1516,7 +1504,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("2020-06-25/Some Title.pdf"),
|
"2020-06-25/Some Title.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
with override_settings(
|
with override_settings(
|
||||||
@@ -1524,7 +1512,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc_a),
|
generate_filename(doc_a),
|
||||||
Path("2024-10-01/Some Title.pdf"),
|
"2024-10-01/Some Title.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_slugify_filter(self):
|
def test_slugify_filter(self):
|
||||||
@@ -1551,7 +1539,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc),
|
generate_filename(doc),
|
||||||
Path("some-title-with-special-characters.pdf"),
|
"some-title-with-special-characters.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test with correspondent name containing spaces and special chars
|
# Test with correspondent name containing spaces and special chars
|
||||||
@@ -1565,7 +1553,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc),
|
generate_filename(doc),
|
||||||
Path("johns-office-workplace/some-title-with-special-characters.pdf"),
|
"johns-office-workplace/some-title-with-special-characters.pdf",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Test with custom fields
|
# Test with custom fields
|
||||||
@@ -1584,5 +1572,5 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
|
|||||||
):
|
):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
generate_filename(doc),
|
generate_filename(doc),
|
||||||
Path("brussels-belgium/some-title-with-special-characters.pdf"),
|
"brussels-belgium/some-title-with-special-characters.pdf",
|
||||||
)
|
)
|
||||||
|
@@ -123,7 +123,7 @@ class TestExportImport(
|
|||||||
|
|
||||||
self.trigger = WorkflowTrigger.objects.create(
|
self.trigger = WorkflowTrigger.objects.create(
|
||||||
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
sources=[str(WorkflowTrigger.DocumentSourceChoices.CONSUME_FOLDER.value)],
|
sources=[1],
|
||||||
filter_filename="*",
|
filter_filename="*",
|
||||||
)
|
)
|
||||||
self.action = WorkflowAction.objects.create(assign_title="new title")
|
self.action = WorkflowAction.objects.create(assign_title="new title")
|
||||||
@@ -209,7 +209,7 @@ class TestExportImport(
|
|||||||
4,
|
4,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self._get_document_from_manifest(manifest, self.d1.id)["fields"]["title"],
|
self._get_document_from_manifest(manifest, self.d1.id)["fields"]["title"],
|
||||||
@@ -235,7 +235,9 @@ class TestExportImport(
|
|||||||
).as_posix()
|
).as_posix()
|
||||||
self.assertIsFile(fname)
|
self.assertIsFile(fname)
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
self.target / element[document_exporter.EXPORTER_THUMBNAIL_NAME],
|
(
|
||||||
|
self.target / element[document_exporter.EXPORTER_THUMBNAIL_NAME]
|
||||||
|
).as_posix(),
|
||||||
)
|
)
|
||||||
|
|
||||||
with Path(fname).open("rb") as f:
|
with Path(fname).open("rb") as f:
|
||||||
@@ -250,7 +252,7 @@ class TestExportImport(
|
|||||||
if document_exporter.EXPORTER_ARCHIVE_NAME in element:
|
if document_exporter.EXPORTER_ARCHIVE_NAME in element:
|
||||||
fname = (
|
fname = (
|
||||||
self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME]
|
self.target / element[document_exporter.EXPORTER_ARCHIVE_NAME]
|
||||||
)
|
).as_posix()
|
||||||
self.assertIsFile(fname)
|
self.assertIsFile(fname)
|
||||||
|
|
||||||
with Path(fname).open("rb") as f:
|
with Path(fname).open("rb") as f:
|
||||||
@@ -310,7 +312,7 @@ class TestExportImport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._do_export()
|
self._do_export()
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
st_mtime_1 = (self.target / "manifest.json").stat().st_mtime
|
st_mtime_1 = (self.target / "manifest.json").stat().st_mtime
|
||||||
|
|
||||||
@@ -320,7 +322,7 @@ class TestExportImport(
|
|||||||
self._do_export()
|
self._do_export()
|
||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
st_mtime_2 = (self.target / "manifest.json").stat().st_mtime
|
st_mtime_2 = (self.target / "manifest.json").stat().st_mtime
|
||||||
|
|
||||||
Path(self.d1.source_path).touch()
|
Path(self.d1.source_path).touch()
|
||||||
@@ -332,7 +334,7 @@ class TestExportImport(
|
|||||||
self.assertEqual(m.call_count, 1)
|
self.assertEqual(m.call_count, 1)
|
||||||
|
|
||||||
st_mtime_3 = (self.target / "manifest.json").stat().st_mtime
|
st_mtime_3 = (self.target / "manifest.json").stat().st_mtime
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
self.assertNotEqual(st_mtime_1, st_mtime_2)
|
self.assertNotEqual(st_mtime_1, st_mtime_2)
|
||||||
self.assertNotEqual(st_mtime_2, st_mtime_3)
|
self.assertNotEqual(st_mtime_2, st_mtime_3)
|
||||||
@@ -350,7 +352,7 @@ class TestExportImport(
|
|||||||
|
|
||||||
self._do_export()
|
self._do_export()
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
with mock.patch(
|
with mock.patch(
|
||||||
"documents.management.commands.document_exporter.copy_file_with_basic_stats",
|
"documents.management.commands.document_exporter.copy_file_with_basic_stats",
|
||||||
@@ -358,7 +360,7 @@ class TestExportImport(
|
|||||||
self._do_export()
|
self._do_export()
|
||||||
m.assert_not_called()
|
m.assert_not_called()
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
self.d2.checksum = "asdfasdgf3"
|
self.d2.checksum = "asdfasdgf3"
|
||||||
self.d2.save()
|
self.d2.save()
|
||||||
@@ -369,7 +371,7 @@ class TestExportImport(
|
|||||||
self._do_export(compare_checksums=True)
|
self._do_export(compare_checksums=True)
|
||||||
self.assertEqual(m.call_count, 1)
|
self.assertEqual(m.call_count, 1)
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
def test_update_export_deleted_document(self):
|
def test_update_export_deleted_document(self):
|
||||||
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
shutil.rmtree(Path(self.dirs.media_dir) / "documents")
|
||||||
@@ -383,7 +385,7 @@ class TestExportImport(
|
|||||||
self.assertTrue(len(manifest), 7)
|
self.assertTrue(len(manifest), 7)
|
||||||
doc_from_manifest = self._get_document_from_manifest(manifest, self.d3.id)
|
doc_from_manifest = self._get_document_from_manifest(manifest, self.d3.id)
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
str(self.target / doc_from_manifest[EXPORTER_FILE_NAME]),
|
(self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
|
||||||
)
|
)
|
||||||
self.d3.delete()
|
self.d3.delete()
|
||||||
|
|
||||||
@@ -395,12 +397,12 @@ class TestExportImport(
|
|||||||
self.d3.id,
|
self.d3.id,
|
||||||
)
|
)
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
self.target / doc_from_manifest[EXPORTER_FILE_NAME],
|
(self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
|
||||||
)
|
)
|
||||||
|
|
||||||
manifest = self._do_export(delete=True)
|
manifest = self._do_export(delete=True)
|
||||||
self.assertIsNotFile(
|
self.assertIsNotFile(
|
||||||
self.target / doc_from_manifest[EXPORTER_FILE_NAME],
|
(self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertTrue(len(manifest), 6)
|
self.assertTrue(len(manifest), 6)
|
||||||
@@ -414,20 +416,20 @@ class TestExportImport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
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").as_posix())
|
||||||
|
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
|
|
||||||
self.d1.title = "new_title"
|
self.d1.title = "new_title"
|
||||||
self.d1.save()
|
self.d1.save()
|
||||||
self._do_export(use_filename_format=True, delete=True)
|
self._do_export(use_filename_format=True, delete=True)
|
||||||
self.assertIsNotFile(self.target / "wow1" / "c.pdf")
|
self.assertIsNotFile((self.target / "wow1" / "c.pdf").as_posix())
|
||||||
self.assertIsNotDir(self.target / "wow1")
|
self.assertIsNotDir((self.target / "wow1").as_posix())
|
||||||
self.assertIsFile(self.target / "new_title" / "c.pdf")
|
self.assertIsFile((self.target / "new_title" / "c.pdf").as_posix())
|
||||||
self.assertIsFile(self.target / "manifest.json")
|
self.assertIsFile((self.target / "manifest.json").as_posix())
|
||||||
self.assertIsFile(self.target / "wow2" / "none.pdf")
|
self.assertIsFile((self.target / "wow2" / "none.pdf").as_posix())
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
self.target / "wow2" / "none_01.pdf",
|
(self.target / "wow2" / "none_01.pdf").as_posix(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_export_missing_files(self):
|
def test_export_missing_files(self):
|
||||||
|
@@ -87,7 +87,7 @@ class TestFuzzyMatchCommand(TestCase):
|
|||||||
filename="other_test.pdf",
|
filename="other_test.pdf",
|
||||||
)
|
)
|
||||||
stdout, _ = self.call_command()
|
stdout, _ = self.call_command()
|
||||||
self.assertIn("No matches found", stdout)
|
self.assertEqual(stdout, "No matches found\n")
|
||||||
|
|
||||||
def test_with_matches(self):
|
def test_with_matches(self):
|
||||||
"""
|
"""
|
||||||
@@ -116,7 +116,7 @@ class TestFuzzyMatchCommand(TestCase):
|
|||||||
filename="other_test.pdf",
|
filename="other_test.pdf",
|
||||||
)
|
)
|
||||||
stdout, _ = self.call_command("--processes", "1")
|
stdout, _ = self.call_command("--processes", "1")
|
||||||
self.assertRegex(stdout, self.MSG_REGEX)
|
self.assertRegex(stdout, self.MSG_REGEX + "\n")
|
||||||
|
|
||||||
def test_with_3_matches(self):
|
def test_with_3_matches(self):
|
||||||
"""
|
"""
|
||||||
@@ -152,10 +152,11 @@ class TestFuzzyMatchCommand(TestCase):
|
|||||||
filename="final_test.pdf",
|
filename="final_test.pdf",
|
||||||
)
|
)
|
||||||
stdout, _ = self.call_command()
|
stdout, _ = self.call_command()
|
||||||
lines = [x.strip() for x in stdout.splitlines() if x.strip()]
|
lines = [x.strip() for x in stdout.split("\n") if len(x.strip())]
|
||||||
self.assertEqual(len(lines), 3)
|
self.assertEqual(len(lines), 3)
|
||||||
for line in lines:
|
self.assertRegex(lines[0], self.MSG_REGEX)
|
||||||
self.assertRegex(line, self.MSG_REGEX)
|
self.assertRegex(lines[1], self.MSG_REGEX)
|
||||||
|
self.assertRegex(lines[2], self.MSG_REGEX)
|
||||||
|
|
||||||
def test_document_deletion(self):
|
def test_document_deletion(self):
|
||||||
"""
|
"""
|
||||||
@@ -196,12 +197,14 @@ class TestFuzzyMatchCommand(TestCase):
|
|||||||
|
|
||||||
stdout, _ = self.call_command("--delete")
|
stdout, _ = self.call_command("--delete")
|
||||||
|
|
||||||
self.assertIn(
|
lines = [x.strip() for x in stdout.split("\n") if len(x.strip())]
|
||||||
|
self.assertEqual(len(lines), 3)
|
||||||
|
self.assertEqual(
|
||||||
|
lines[0],
|
||||||
"The command is configured to delete documents. Use with caution",
|
"The command is configured to delete documents. Use with caution",
|
||||||
stdout,
|
|
||||||
)
|
)
|
||||||
self.assertRegex(stdout, self.MSG_REGEX)
|
self.assertRegex(lines[1], self.MSG_REGEX)
|
||||||
self.assertIn("Deleting 1 documents based on ratio matches", stdout)
|
self.assertEqual(lines[2], "Deleting 1 documents based on ratio matches")
|
||||||
|
|
||||||
self.assertEqual(Document.objects.count(), 2)
|
self.assertEqual(Document.objects.count(), 2)
|
||||||
self.assertIsNotNone(Document.objects.get(pk=1))
|
self.assertIsNotNone(Document.objects.get(pk=1))
|
||||||
|
@@ -20,7 +20,7 @@ def source_path_before(self):
|
|||||||
if self.storage_type == STORAGE_TYPE_GPG:
|
if self.storage_type == STORAGE_TYPE_GPG:
|
||||||
fname += ".gpg"
|
fname += ".gpg"
|
||||||
|
|
||||||
return Path(settings.ORIGINALS_DIR) / fname
|
return (Path(settings.ORIGINALS_DIR) / fname).as_posix()
|
||||||
|
|
||||||
|
|
||||||
def file_type_after(self):
|
def file_type_after(self):
|
||||||
@@ -35,7 +35,7 @@ def source_path_after(doc):
|
|||||||
if doc.storage_type == STORAGE_TYPE_GPG:
|
if doc.storage_type == STORAGE_TYPE_GPG:
|
||||||
fname += ".gpg" # pragma: no cover
|
fname += ".gpg" # pragma: no cover
|
||||||
|
|
||||||
return Path(settings.ORIGINALS_DIR) / fname
|
return (Path(settings.ORIGINALS_DIR) / fname).as_posix()
|
||||||
|
|
||||||
|
|
||||||
@override_settings(PASSPHRASE="test")
|
@override_settings(PASSPHRASE="test")
|
||||||
|
@@ -104,7 +104,7 @@ class TestReverseMigrateWorkflow(TestMigrations):
|
|||||||
|
|
||||||
trigger = WorkflowTrigger.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
type=0,
|
type=0,
|
||||||
sources=[str(DocumentSource.ConsumeFolder)],
|
sources=[DocumentSource.ConsumeFolder],
|
||||||
filter_path="*/path/*",
|
filter_path="*/path/*",
|
||||||
filter_filename="*file*",
|
filter_filename="*file*",
|
||||||
)
|
)
|
||||||
|
@@ -54,7 +54,7 @@ class HttpRemoteUserMiddleware(PersistentRemoteUserMiddleware):
|
|||||||
|
|
||||||
header = settings.HTTP_REMOTE_USER_HEADER_NAME
|
header = settings.HTTP_REMOTE_USER_HEADER_NAME
|
||||||
|
|
||||||
def __call__(self, request: HttpRequest) -> None:
|
def process_request(self, request: HttpRequest) -> None:
|
||||||
# If remote user auth is enabled only for the frontend, not the API,
|
# If remote user auth is enabled only for the frontend, not the API,
|
||||||
# then we need dont want to authenticate the user for API requests.
|
# then we need dont want to authenticate the user for API requests.
|
||||||
if (
|
if (
|
||||||
@@ -62,8 +62,8 @@ class HttpRemoteUserMiddleware(PersistentRemoteUserMiddleware):
|
|||||||
and "paperless.auth.PaperlessRemoteUserAuthentication"
|
and "paperless.auth.PaperlessRemoteUserAuthentication"
|
||||||
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]
|
not in settings.REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"]
|
||||||
):
|
):
|
||||||
return self.get_response(request)
|
return
|
||||||
return super().__call__(request)
|
return super().process_request(request)
|
||||||
|
|
||||||
|
|
||||||
class PaperlessRemoteUserAuthentication(authentication.RemoteUserAuthentication):
|
class PaperlessRemoteUserAuthentication(authentication.RemoteUserAuthentication):
|
||||||
|
@@ -214,3 +214,31 @@ def audit_log_check(app_configs, **kwargs):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@register()
|
||||||
|
def check_postgres_version(app_configs, **kwargs):
|
||||||
|
"""
|
||||||
|
Django 5.2 removed PostgreSQL 13 support and thus it will be removed in
|
||||||
|
a future Paperless-ngx version. This check can be removed eventually.
|
||||||
|
See https://docs.djangoproject.com/en/5.2/releases/5.2/#dropped-support-for-postgresql-13
|
||||||
|
"""
|
||||||
|
db_conn = connections["default"]
|
||||||
|
result = []
|
||||||
|
if db_conn.vendor == "postgresql":
|
||||||
|
try:
|
||||||
|
with db_conn.cursor() as cursor:
|
||||||
|
cursor.execute("SHOW server_version;")
|
||||||
|
version = cursor.fetchone()[0]
|
||||||
|
if version.startswith("13"):
|
||||||
|
return [
|
||||||
|
Warning(
|
||||||
|
"PostgreSQL 13 is deprecated and will not be supported in a future Paperless-ngx release.",
|
||||||
|
hint="Upgrade to PostgreSQL 14 or newer.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
except Exception: # pragma: no cover
|
||||||
|
# Don't block checks on version query failure
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
@@ -9,6 +9,7 @@ from documents.tests.utils import DirectoriesMixin
|
|||||||
from documents.tests.utils import FileSystemAssertsMixin
|
from documents.tests.utils import FileSystemAssertsMixin
|
||||||
from paperless.checks import audit_log_check
|
from paperless.checks import audit_log_check
|
||||||
from paperless.checks import binaries_check
|
from paperless.checks import binaries_check
|
||||||
|
from paperless.checks import check_postgres_version
|
||||||
from paperless.checks import debug_mode_check
|
from paperless.checks import debug_mode_check
|
||||||
from paperless.checks import paths_check
|
from paperless.checks import paths_check
|
||||||
from paperless.checks import settings_values_check
|
from paperless.checks import settings_values_check
|
||||||
@@ -262,3 +263,39 @@ class TestAuditLogChecks(TestCase):
|
|||||||
("auditlog table was found but audit log is disabled."),
|
("auditlog table was found but audit log is disabled."),
|
||||||
msg.msg,
|
msg.msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostgresVersionCheck(TestCase):
|
||||||
|
@mock.patch("paperless.checks.connections")
|
||||||
|
def test_postgres_13_warns(self, mock_connections):
|
||||||
|
mock_connection = mock.MagicMock()
|
||||||
|
mock_connection.vendor = "postgresql"
|
||||||
|
mock_cursor = mock.MagicMock()
|
||||||
|
mock_cursor.__enter__.return_value.fetchone.return_value = ["13.11"]
|
||||||
|
mock_connection.cursor.return_value = mock_cursor
|
||||||
|
mock_connections.__getitem__.return_value = mock_connection
|
||||||
|
|
||||||
|
warnings = check_postgres_version(None)
|
||||||
|
self.assertEqual(len(warnings), 1)
|
||||||
|
self.assertIn("PostgreSQL 13 is deprecated", warnings[0].msg)
|
||||||
|
|
||||||
|
@mock.patch("paperless.checks.connections")
|
||||||
|
def test_postgres_14_passes(self, mock_connections):
|
||||||
|
mock_connection = mock.MagicMock()
|
||||||
|
mock_connection.vendor = "postgresql"
|
||||||
|
mock_cursor = mock.MagicMock()
|
||||||
|
mock_cursor.__enter__.return_value.fetchone.return_value = ["14.10"]
|
||||||
|
mock_connection.cursor.return_value = mock_cursor
|
||||||
|
mock_connections.__getitem__.return_value = mock_connection
|
||||||
|
|
||||||
|
warnings = check_postgres_version(None)
|
||||||
|
self.assertEqual(warnings, [])
|
||||||
|
|
||||||
|
@mock.patch("paperless.checks.connections")
|
||||||
|
def test_non_postgres_skipped(self, mock_connections):
|
||||||
|
mock_connection = mock.MagicMock()
|
||||||
|
mock_connection.vendor = "sqlite"
|
||||||
|
mock_connections.__getitem__.return_value = mock_connection
|
||||||
|
|
||||||
|
warnings = check_postgres_version(None)
|
||||||
|
self.assertEqual(warnings, [])
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
@@ -92,7 +91,6 @@ class TestRemoteUser(DirectoriesMixin, APITestCase):
|
|||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
REST_FRAMEWORK={
|
REST_FRAMEWORK={
|
||||||
**settings.REST_FRAMEWORK,
|
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||||
"rest_framework.authentication.BasicAuthentication",
|
"rest_framework.authentication.BasicAuthentication",
|
||||||
"rest_framework.authentication.TokenAuthentication",
|
"rest_framework.authentication.TokenAuthentication",
|
||||||
|
104
uv.lock
generated
104
uv.lock
generated
@@ -1,5 +1,5 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"sys_platform == 'darwin'",
|
"sys_platform == 'darwin'",
|
||||||
@@ -312,15 +312,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "channels"
|
name = "channels"
|
||||||
version = "4.3.1"
|
version = "4.3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/12/a0/46450fcf9e56af18a6b0440ba49db6635419bb7bc84142c35f4143b1a66c/channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb", size = 26896, upload-time = "2025-08-01T13:25:19.952Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/72/04/6768c7a887f9c593c4d49f99130c8aec4ea06e750bc17c306b689f6caf3b/channels-4.3.0.tar.gz", hash = "sha256:7db32c61dcd88eada1647e6c6f6ad2eb724b75d4852eeff26ad1c51ccd1a37f7", size = 26816, upload-time = "2025-07-28T13:52:50.334Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/1c/eae1c2a8c195760376e7f65d0bdcc3e966695d29cfbe5c54841ce5c71408/channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859", size = 31286, upload-time = "2025-08-01T13:25:18.845Z" },
|
{ url = "https://files.pythonhosted.org/packages/7c/59/0866202ee593e1b0dab0b472ebb8169e1b2b7886ad3008d193da2bbe10cb/channels-4.3.0-py3-none-any.whl", hash = "sha256:0497f3affb95e621b37d6bae1b6a5d9e8e1e1221007a2566f280091cf30ffcce", size = 31238, upload-time = "2025-07-28T13:52:49.117Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -626,15 +626,15 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "5.2.5"
|
version = "5.1.8"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/62/9b/779f853c3d2d58b9e08346061ff3e331cdec3fe3f53aae509e256412a593/django-5.2.5.tar.gz", hash = "sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae", size = 10859748, upload-time = "2025-08-06T08:26:29.978Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/00/40/45adc1b93435d1b418654a734b68351bb6ce0a0e5e37b2f0e9aeb1a2e233/Django-5.1.8.tar.gz", hash = "sha256:42e92a1dd2810072bcc40a39a212b693f94406d0ba0749e68eb642f31dc770b4", size = 10723602, upload-time = "2025-04-02T11:19:56.028Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/6e/98a1d23648e0085bb5825326af17612ecd8fc76be0ce96ea4dc35e17b926/django-5.2.5-py3-none-any.whl", hash = "sha256:2b2ada0ee8a5ff743a40e2b9820d1f8e24c11bac9ae6469cd548f0057ea6ddcd", size = 8302999, upload-time = "2025-08-06T08:26:23.562Z" },
|
{ url = "https://files.pythonhosted.org/packages/ec/0d/e6dd0ed898b920fec35c6eeeb9acbeb831fff19ad21c5e684744df1d4a36/Django-5.1.8-py3-none-any.whl", hash = "sha256:11b28fa4b00e59d0def004e9ee012fefbb1065a5beb39ee838983fd24493ad4f", size = 8277130, upload-time = "2025-04-02T11:19:51.591Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -660,15 +660,15 @@ socialaccount = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-auditlog"
|
name = "django-auditlog"
|
||||||
version = "3.2.1"
|
version = "3.1.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/46/9da1d94493832fa18d2f6324a76d387fa232001593866987a96047709f4e/django_auditlog-3.2.1.tar.gz", hash = "sha256:63a4c9f7793e94eed804bc31a04d9b0b58244b1d280e2ed273c8b406bff1f779", size = 72926, upload-time = "2025-07-03T20:08:17.734Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/0e/3a/61bfb180019d08db3f7a2e4097bda14ee32bd57f5dffda0c84b2d4c26304/django_auditlog-3.1.2.tar.gz", hash = "sha256:435345b4055d16abfb4ada4bf11320f9e2f6d343874464471fa0041f13f3a474", size = 69359, upload-time = "2025-04-26T11:01:56.553Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/06/67296d050a72dcd76f57f220df621cb27e5b9282ba7ad0f5f74870dce241/django_auditlog-3.2.1-py3-none-any.whl", hash = "sha256:99603ca9d015f7e9b062b1c34f3e0826a3ce6ae6e5950c81bb7e663f7802a899", size = 38330, upload-time = "2025-07-03T20:07:51.735Z" },
|
{ url = "https://files.pythonhosted.org/packages/af/34/47edd758abcb4426953b5ff2fa4dd9956c2304e96160ab1b95c3a1ab6e61/django_auditlog-3.1.2-py3-none-any.whl", hash = "sha256:6432a83fdf4397a726488d101fedcb62daafd6d4b825a0fc4c50e3657f5883cd", size = 37312, upload-time = "2025-04-26T11:01:16.776Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -764,38 +764,38 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-guardian"
|
name = "django-guardian"
|
||||||
version = "3.0.3"
|
version = "2.4.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/30/c2/3ed43813dd7313f729dbaa829b4f9ed4a647530151f672cfb5f843c12edf/django_guardian-3.0.3.tar.gz", hash = "sha256:4e59eab4d836da5a027cf0c176d14bc2a4e22cbbdf753159a03946c08c8a196d", size = 85410, upload-time = "2025-06-25T20:42:17.475Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6f/4c/d1f6923a0ad7f16c403a54c09e94acb76ac6c3765e02523fb09b2b03e1a8/django-guardian-2.4.0.tar.gz", hash = "sha256:c58a68ae76922d33e6bdc0e69af1892097838de56e93e78a8361090bcd9f89a0", size = 159008, upload-time = "2021-05-23T22:11:26.23Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/13/e6f629a978ef5fab8b8d2760cacc3e451016cef952cf4c049d672c5c6b07/django_guardian-3.0.3-py3-none-any.whl", hash = "sha256:d2164cea9f03c369d7ade21802710f3ab23ca6734bcc7dfcfb385906783916c7", size = 118198, upload-time = "2025-06-25T20:42:15.377Z" },
|
{ url = "https://files.pythonhosted.org/packages/a2/25/869df12e544b51f583254aadbba6c1a95e11d2d08edeb9e58dd715112db5/django_guardian-2.4.0-py3-none-any.whl", hash = "sha256:440ca61358427e575323648b25f8384739e54c38b3d655c81d75e0cd0d61b697", size = 106107, upload-time = "2021-05-23T22:11:22.75Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-multiselectfield"
|
name = "django-multiselectfield"
|
||||||
version = "1.0.1"
|
version = "0.1.13"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/04/9a/27060e8aa491ff2d286054df2e89df481a8dfe0e5e459fa36c0f48e3c10c/django_multiselectfield-1.0.1.tar.gz", hash = "sha256:3f8b4fff3e07d4a91c8bb4b809bc35caeb22b41769b606f4c9edc53b8d72a667", size = 22025, upload-time = "2025-06-12T14:41:21.599Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/dd/c3/1a326cc669fea63f22e63f6e2b2b014534a15966506e8d7fa3c232aced42/django_multiselectfield-0.1.13.tar.gz", hash = "sha256:437d72632f4c0ca416951917632529c3d1d42b62bb6c3c03e3396fa50265be94", size = 11704, upload-time = "2024-07-01T05:40:39.456Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/10/23c0644cf67567bbe4e3a2eeeec0e9c79b701990c0e07c5ee4a4f8897f91/django_multiselectfield-1.0.1-py3-none-any.whl", hash = "sha256:18dc14801f7eca844a48e21cba6d8ec35b9b581f2373bbb2cb75e6994518259a", size = 20481, upload-time = "2025-06-12T14:41:20.107Z" },
|
{ url = "https://files.pythonhosted.org/packages/be/9e/3ed6f072f1e806516dbc8c95e4ecae7b87af6757eb5d428857ea0a097e76/django_multiselectfield-0.1.13-py3-none-any.whl", hash = "sha256:f146ef568c823a409f4021b98781666ec2debabfceca9176116d749dc39cb8b3", size = 14804, upload-time = "2024-07-01T05:40:37.549Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-soft-delete"
|
name = "django-soft-delete"
|
||||||
version = "1.0.19"
|
version = "1.0.18"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/77/44a6615a7da3ca0ddc624039d399d17d6c3503e1c2dad08b443f8d4a3570/django_soft_delete-1.0.19.tar.gz", hash = "sha256:c67ee8920e1456eca84cc59b3304ef27fa9d476b516be726ce7e1fc558502908", size = 11993, upload-time = "2025-06-19T20:32:20.373Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ec/7e/89cba723dd5d34ccb6003f4812de7f5c69ba32bd73ab37f2bb21ff344c6c/django_soft_delete-1.0.18.tar.gz", hash = "sha256:d2f9db449a4f008e9786f82fa4bafbe4075f7a0b3284844735007e988b2a4df6", size = 11979, upload-time = "2025-02-01T13:43:53.804Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/9e/f8b5a02cdcba606eb40fbe30fe0c9c7493a2c18f83ec3b4620e4e86a34d3/django_soft_delete-1.0.19-py3-none-any.whl", hash = "sha256:46aa5fab513db566d3d7a832529ed27245b5900eaaa705535bc7674055801a46", size = 10889, upload-time = "2025-06-19T20:32:19.083Z" },
|
{ url = "https://files.pythonhosted.org/packages/f7/d0/6dcca209e48081213854088fc7014e9dbdcd24f4ec2118f8ee29d11c8623/django_soft_delete-1.0.18-py3-none-any.whl", hash = "sha256:603a29e82bbb7a5bada69f2754fad225ccd8cd7f485320ec06d0fc4e9dfddcf0", size = 10876, upload-time = "2025-02-01T13:43:52.109Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -835,28 +835,28 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djangorestframework"
|
name = "djangorestframework"
|
||||||
version = "3.16.1"
|
version = "3.16.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/95/5376fe618646fde6899b3cdc85fd959716bb67542e273a76a80d9f326f27/djangorestframework-3.16.1.tar.gz", hash = "sha256:166809528b1aced0a17dc66c24492af18049f2c9420dbd0be29422029cfc3ff7", size = 1089735, upload-time = "2025-08-06T17:50:53.251Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/97/112c5a72e6917949b6d8a18ad6c6e72c46da4290c8f36ee5f1c1dcbc9901/djangorestframework-3.16.0.tar.gz", hash = "sha256:f022ff46613584de994c0c6a4aebbace5fd700555fbe9d33b865ebf173eba6c9", size = 1068408, upload-time = "2025-03-28T14:18:42.065Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" },
|
{ url = "https://files.pythonhosted.org/packages/eb/3e/2448e93f4f87fc9a9f35e73e3c05669e0edd0c2526834686e949bb1fd303/djangorestframework-3.16.0-py3-none-any.whl", hash = "sha256:bea7e9f6b96a8584c5224bfb2e4348dfb3f8b5e34edbecb98da258e892089361", size = 1067305, upload-time = "2025-03-28T14:18:39.489Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djangorestframework-guardian"
|
name = "djangorestframework-guardian"
|
||||||
version = "0.4.0"
|
version = "0.3.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "django-guardian", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django-guardian", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "djangorestframework", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "djangorestframework", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c1/c4/67df9963395e9dddd4e16cbf75098953798e5135f73fb8f4855895505e39/djangorestframework_guardian-0.4.0.tar.gz", hash = "sha256:a8113659e062f65b74cc31af6982420c382642e782d38581b3fdc748a179756c", size = 8239, upload-time = "2025-07-01T07:22:10.809Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/e5/80/0f2190bacfe7c7b2e22d0e1e695882ec3123f9e58817c8392a258cd46442/djangorestframework-guardian-0.3.0.tar.gz", hash = "sha256:1883756452d9bfcc2a51fb4e039a6837a8f6697c756447aa83af085749b59330", size = 8647, upload-time = "2019-10-14T04:24:25.531Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/81/3d62f7ff71f7c45ec6664ebf03a4c736bf77f49481604361d40f8f4471e4/djangorestframework_guardian-0.4.0-py3-none-any.whl", hash = "sha256:30c2a349318c1cd603d6953d50d58159f9a0c833f5f8f5a811407d5984a39e14", size = 6064, upload-time = "2025-07-01T07:22:09.661Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/cc/35c1d8fb99172b2646f29e270e9ec443ffe09e0b63e61cd528d4fb4b8b07/djangorestframework_guardian-0.3.0-py2.py3-none-any.whl", hash = "sha256:3bd3dd6ea58e1bceca5048faf6f8b1a93bb5dcff30ba5eb91b9a0e190a48a0c7", size = 6931, upload-time = "2019-08-02T01:00:39.543Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -900,14 +900,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "drf-spectacular-sidecar"
|
name = "drf-spectacular-sidecar"
|
||||||
version = "2025.8.1"
|
version = "2025.4.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "django", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/f7/0cb2f520723f1823ef7b6651d447927f61ba92d152a5d68132599b90624f/drf_spectacular_sidecar-2025.8.1.tar.gz", hash = "sha256:1944ae0eb5136cff5aa135211bec31084cef1af03a04de9b7f2f912b3c59c251", size = 2407787, upload-time = "2025-08-01T11:28:01.319Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5d/b6/ce857d73b65b86a9034d0604b5dc1a002f7fa218e32c4dba479a197acd70/drf_spectacular_sidecar-2025.4.1.tar.gz", hash = "sha256:ea7dc4e674174616589d258b5c9676f3c451ec422e62b79e31234d39db53922d", size = 2402076, upload-time = "2025-04-01T11:23:30.627Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/3b/0fcdc6eb294a11ed6e3ddc02fc29968bf403d3ce31645764eedfc91f87a6/drf_spectacular_sidecar-2025.8.1-py3-none-any.whl", hash = "sha256:c65a2a423000cc067395150b4dc28e7398a762d66ee101c4c38a4fb0d29a42a2", size = 2427849, upload-time = "2025-08-01T11:27:59.648Z" },
|
{ url = "https://files.pythonhosted.org/packages/cf/c3/d2f31ef748f89d68121aa3d4a71f7dfd44ea54957b84602d70cda2491c43/drf_spectacular_sidecar-2025.4.1-py3-none-any.whl", hash = "sha256:343a24b0d03125fa76d07685072f55779c5c4124d90c10b14e315fdc143ad9b9", size = 2422415, upload-time = "2025-04-01T11:23:28.797Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1946,7 +1946,6 @@ dependencies = [
|
|||||||
{ name = "ocrmypdf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "ocrmypdf", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "pathvalidate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "pathvalidate", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "pdf2image", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "pdf2image", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "psycopg-pool", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "python-dotenv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "python-gnupg", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "python-gnupg", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -1976,7 +1975,7 @@ postgres = [
|
|||||||
{ name = "psycopg-c", version = "3.2.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin'" },
|
{ name = "psycopg-c", version = "3.2.9", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or sys_platform == 'darwin'" },
|
||||||
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'" },
|
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'" },
|
||||||
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||||
{ name = "psycopg-pool", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "psycopg-pool", version = "3.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'darwin'" },
|
||||||
]
|
]
|
||||||
webserver = [
|
webserver = [
|
||||||
{ name = "granian", extra = ["uvloop"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "granian", extra = ["uvloop"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -2050,22 +2049,22 @@ requires-dist = [
|
|||||||
{ name = "channels-redis", specifier = "~=4.2" },
|
{ name = "channels-redis", specifier = "~=4.2" },
|
||||||
{ name = "concurrent-log-handler", specifier = "~=0.9.25" },
|
{ name = "concurrent-log-handler", specifier = "~=0.9.25" },
|
||||||
{ name = "dateparser", specifier = "~=1.2" },
|
{ name = "dateparser", specifier = "~=1.2" },
|
||||||
{ name = "django", specifier = "~=5.2.5" },
|
{ name = "django", specifier = "~=5.1.7" },
|
||||||
{ name = "django-allauth", extras = ["socialaccount", "mfa"], specifier = "~=65.4.0" },
|
{ name = "django-allauth", extras = ["socialaccount", "mfa"], specifier = "~=65.4.0" },
|
||||||
{ name = "django-auditlog", specifier = "~=3.2.1" },
|
{ name = "django-auditlog", specifier = "~=3.1.2" },
|
||||||
{ name = "django-cachalot", specifier = "~=2.8.0" },
|
{ name = "django-cachalot", specifier = "~=2.8.0" },
|
||||||
{ name = "django-celery-results", specifier = "~=2.6.0" },
|
{ name = "django-celery-results", specifier = "~=2.6.0" },
|
||||||
{ name = "django-compression-middleware", specifier = "~=0.5.0" },
|
{ name = "django-compression-middleware", specifier = "~=0.5.0" },
|
||||||
{ name = "django-cors-headers", specifier = "~=4.7.0" },
|
{ name = "django-cors-headers", specifier = "~=4.7.0" },
|
||||||
{ name = "django-extensions", specifier = "~=4.1" },
|
{ name = "django-extensions", specifier = "~=4.1" },
|
||||||
{ name = "django-filter", specifier = "~=25.1" },
|
{ name = "django-filter", specifier = "~=25.1" },
|
||||||
{ name = "django-guardian", specifier = "~=3.0.3" },
|
{ name = "django-guardian", specifier = "~=2.4.0" },
|
||||||
{ name = "django-multiselectfield", specifier = "~=1.0.1" },
|
{ name = "django-multiselectfield", specifier = "~=0.1.13" },
|
||||||
{ name = "django-soft-delete", specifier = "~=1.0.18" },
|
{ name = "django-soft-delete", specifier = "~=1.0.18" },
|
||||||
{ name = "djangorestframework", specifier = "~=3.15" },
|
{ name = "djangorestframework", specifier = "~=3.15" },
|
||||||
{ name = "djangorestframework-guardian", specifier = "~=0.4.0" },
|
{ name = "djangorestframework-guardian", specifier = "~=0.3.0" },
|
||||||
{ name = "drf-spectacular", specifier = "~=0.28" },
|
{ name = "drf-spectacular", specifier = "~=0.28" },
|
||||||
{ name = "drf-spectacular-sidecar", specifier = "~=2025.8.1" },
|
{ name = "drf-spectacular-sidecar", specifier = "~=2025.4.1" },
|
||||||
{ name = "drf-writable-nested", specifier = "~=0.7.1" },
|
{ name = "drf-writable-nested", specifier = "~=0.7.1" },
|
||||||
{ name = "filelock", specifier = "~=3.18.0" },
|
{ name = "filelock", specifier = "~=3.18.0" },
|
||||||
{ name = "flower", specifier = "~=2.0.1" },
|
{ name = "flower", specifier = "~=2.0.1" },
|
||||||
@@ -2085,8 +2084,7 @@ requires-dist = [
|
|||||||
{ name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl" },
|
{ name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_aarch64.whl" },
|
||||||
{ name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" },
|
{ name = "psycopg-c", marker = "python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux' and extra == 'postgres'", url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" },
|
||||||
{ name = "psycopg-c", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and extra == 'postgres') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and extra == 'postgres') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'postgres') or (sys_platform != 'linux' and extra == 'postgres')", specifier = "==3.2.9" },
|
{ name = "psycopg-c", marker = "(python_full_version != '3.12.*' and platform_machine == 'aarch64' and extra == 'postgres') or (python_full_version != '3.12.*' and platform_machine == 'x86_64' and extra == 'postgres') or (platform_machine != 'aarch64' and platform_machine != 'x86_64' and extra == 'postgres') or (sys_platform != 'linux' and extra == 'postgres')", specifier = "==3.2.9" },
|
||||||
{ name = "psycopg-pool" },
|
{ name = "psycopg-pool", marker = "extra == 'postgres'" },
|
||||||
{ name = "psycopg-pool", marker = "extra == 'postgres'", specifier = "==3.2.6" },
|
|
||||||
{ name = "python-dateutil", specifier = "~=2.9.0" },
|
{ name = "python-dateutil", specifier = "~=2.9.0" },
|
||||||
{ name = "python-dotenv", specifier = "~=1.1.0" },
|
{ name = "python-dotenv", specifier = "~=1.1.0" },
|
||||||
{ name = "python-gnupg", specifier = "~=0.5.4" },
|
{ name = "python-gnupg", specifier = "~=0.5.4" },
|
||||||
@@ -2097,7 +2095,7 @@ requires-dist = [
|
|||||||
{ name = "redis", extras = ["hiredis"], specifier = "~=5.2.1" },
|
{ name = "redis", extras = ["hiredis"], specifier = "~=5.2.1" },
|
||||||
{ name = "scikit-learn", specifier = "~=1.7.0" },
|
{ name = "scikit-learn", specifier = "~=1.7.0" },
|
||||||
{ name = "setproctitle", specifier = "~=1.3.4" },
|
{ name = "setproctitle", specifier = "~=1.3.4" },
|
||||||
{ name = "tika-client", specifier = "~=0.10.0" },
|
{ name = "tika-client", specifier = "~=0.9.0" },
|
||||||
{ name = "tqdm", specifier = "~=4.67.1" },
|
{ name = "tqdm", specifier = "~=4.67.1" },
|
||||||
{ name = "watchdog", specifier = "~=6.0" },
|
{ name = "watchdog", specifier = "~=6.0" },
|
||||||
{ name = "whitenoise", specifier = "~=6.9" },
|
{ name = "whitenoise", specifier = "~=6.9" },
|
||||||
@@ -2119,7 +2117,7 @@ dev = [
|
|||||||
{ name = "pre-commit-uv", specifier = "~=4.1.3" },
|
{ name = "pre-commit-uv", specifier = "~=4.1.3" },
|
||||||
{ name = "pytest", specifier = "~=8.4.1" },
|
{ name = "pytest", specifier = "~=8.4.1" },
|
||||||
{ name = "pytest-cov", specifier = "~=6.2.1" },
|
{ name = "pytest-cov", specifier = "~=6.2.1" },
|
||||||
{ name = "pytest-django", specifier = "~=4.11.1" },
|
{ name = "pytest-django", specifier = "~=4.10.0" },
|
||||||
{ name = "pytest-env" },
|
{ name = "pytest-env" },
|
||||||
{ name = "pytest-httpx" },
|
{ name = "pytest-httpx" },
|
||||||
{ name = "pytest-mock" },
|
{ name = "pytest-mock" },
|
||||||
@@ -2143,7 +2141,7 @@ testing = [
|
|||||||
{ name = "imagehash" },
|
{ name = "imagehash" },
|
||||||
{ name = "pytest", specifier = "~=8.4.1" },
|
{ name = "pytest", specifier = "~=8.4.1" },
|
||||||
{ name = "pytest-cov", specifier = "~=6.2.1" },
|
{ name = "pytest-cov", specifier = "~=6.2.1" },
|
||||||
{ name = "pytest-django", specifier = "~=4.11.1" },
|
{ name = "pytest-django", specifier = "~=4.10.0" },
|
||||||
{ name = "pytest-env" },
|
{ name = "pytest-env" },
|
||||||
{ name = "pytest-httpx" },
|
{ name = "pytest-httpx" },
|
||||||
{ name = "pytest-mock" },
|
{ name = "pytest-mock" },
|
||||||
@@ -2438,7 +2436,7 @@ c = [
|
|||||||
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
{ name = "psycopg-c", version = "3.2.9", source = { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' and platform_machine == 'x86_64' and sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
pool = [
|
pool = [
|
||||||
{ name = "psycopg-pool", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "psycopg-pool", version = "3.2.6", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform == 'linux' or sys_platform == 'darwin'" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2477,14 +2475,12 @@ wheels = [
|
|||||||
name = "psycopg-pool"
|
name = "psycopg-pool"
|
||||||
version = "3.2.6"
|
version = "3.2.6"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
|
||||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/13/1e7850bb2c69a63267c3dbf37387d3f71a00fd0e2fa55c5db14d64ba1af4/psycopg_pool-3.2.6.tar.gz", hash = "sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5", size = 29770, upload-time = "2025-02-26T12:03:47.129Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/cf/13/1e7850bb2c69a63267c3dbf37387d3f71a00fd0e2fa55c5db14d64ba1af4/psycopg_pool-3.2.6.tar.gz", hash = "sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5", size = 29770, upload-time = "2025-02-26T12:03:47.129Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/fd/4feb52a55c1a4bd748f2acaed1903ab54a723c47f6d0242780f4d97104d4/psycopg_pool-3.2.6-py3-none-any.whl", hash = "sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7", size = 38252, upload-time = "2025-02-26T12:03:45.073Z" },
|
{ url = "https://files.pythonhosted.org/packages/47/fd/4feb52a55c1a4bd748f2acaed1903ab54a723c47f6d0242780f4d97104d4/psycopg_pool-3.2.6-py3-none-any.whl", hash = "sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7", size = 38252, upload-time = "2025-02-26T12:03:45.073Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyasn1"
|
name = "pyasn1"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
@@ -2597,14 +2593,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-django"
|
name = "pytest-django"
|
||||||
version = "4.11.1"
|
version = "4.10.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/fb/55d580352db26eb3d59ad50c64321ddfe228d3d8ac107db05387a2fadf3a/pytest_django-4.11.1.tar.gz", hash = "sha256:a949141a1ee103cb0e7a20f1451d355f83f5e4a5d07bdd4dcfdd1fd0ff227991", size = 86202, upload-time = "2025-04-03T18:56:09.338Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a5/10/a096573b4b896f18a8390d9dafaffc054c1f613c60bf838300732e538890/pytest_django-4.10.0.tar.gz", hash = "sha256:1091b20ea1491fd04a310fc9aaff4c01b4e8450e3b157687625e16a6b5f3a366", size = 84710, upload-time = "2025-02-10T14:52:57.337Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/ac/bd0608d229ec808e51a21044f3f2f27b9a37e7a0ebaca7247882e67876af/pytest_django-4.11.1-py3-none-any.whl", hash = "sha256:1b63773f648aa3d8541000c26929c1ea63934be1cfa674c76436966d73fe6a10", size = 25281, upload-time = "2025-04-03T18:56:07.678Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/4c/a4fe18205926216e1aebe1f125cba5bce444f91b6e4de4f49fa87e322775/pytest_django-4.10.0-py3-none-any.whl", hash = "sha256:57c74ef3aa9d89cae5a5d73fbb69a720a62673ade7ff13b9491872409a3f5918", size = 23975, upload-time = "2025-02-10T14:52:55.325Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2708,11 +2704,11 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-gnupg"
|
name = "python-gnupg"
|
||||||
version = "0.5.5"
|
version = "0.5.4"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/42/d0/72a14a79f26c6119b281f6ccc475a787432ef155560278e60df97ce68a86/python-gnupg-0.5.5.tar.gz", hash = "sha256:3fdcaf76f60a1b948ff8e37dc398d03cf9ce7427065d583082b92da7a4ff5a63", size = 66467, upload-time = "2025-08-04T19:26:55.778Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/3e/ba0dc69c9f4e0aeb24d93175230ef057c151790a7516012f61014918992d/python-gnupg-0.5.4.tar.gz", hash = "sha256:f2fdb5fb29615c77c2743e1cb3d9314353a6e87b10c37d238d91ae1c6feae086", size = 65705, upload-time = "2025-01-07T11:58:34.073Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/19/c147f78cc18c8788f54d4a16a22f6c05deba85ead5672d3ddf6dcba5a5fe/python_gnupg-0.5.5-py2.py3-none-any.whl", hash = "sha256:51fa7b8831ff0914bc73d74c59b99c613de7247b91294323c39733bb85ac3fc1", size = 21916, upload-time = "2025-08-04T19:26:54.307Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/6666ed5a0d3ce4d5444af62e373d5ba8ab253a03487c86f2f9f1078e7c31/python_gnupg-0.5.4-py2.py3-none-any.whl", hash = "sha256:40ce25cde9df29af91fe931ce9df3ce544e14a37f62b13ca878c897217b2de6c", size = 21730, upload-time = "2025-01-07T11:58:32.249Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3358,16 +3354,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tika-client"
|
name = "tika-client"
|
||||||
version = "0.10.0"
|
version = "0.9.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "anyio", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "httpx", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" },
|
{ name = "typing-extensions", marker = "(python_full_version < '3.11' and sys_platform == 'darwin') or (python_full_version < '3.11' and sys_platform == 'linux')" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/21/be/65bfc47e4689ecd5ead20cf47dc0084fd767b7e71e8cfabf5fddc42aae3c/tika_client-0.10.0.tar.gz", hash = "sha256:3101e8b2482ae4cb7f87be13ada970ff691bdc3404d94cd52f5e57a09c99370c", size = 2178257, upload-time = "2025-08-04T17:47:30.414Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/94/ad/3508e42b470a037b3f5c19ca9993893d0faa30ba7ec7e6ac33db9bc3bf51/tika_client-0.9.0.tar.gz", hash = "sha256:c10bba8e40ede23c039f84ccd821fb2d290d339cc26cbd267ab9b561a1e83659", size = 2175246, upload-time = "2025-01-15T18:46:23.901Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/31/002e0fa5bca67d6a19da8c294273486f6c46cbcc83d6879719a38a181461/tika_client-0.10.0-py3-none-any.whl", hash = "sha256:f5486cc884e4522575662aa295bda761bf9f101ac8d92840155b58ab8b96f6e2", size = 18237, upload-time = "2025-08-04T17:47:28.966Z" },
|
{ url = "https://files.pythonhosted.org/packages/36/8c/90ba51e014fb548ee34dd5ed14e85ec4a205ff97b89ca393e4de321304ac/tika_client-0.9.0-py3-none-any.whl", hash = "sha256:2464e8335b5e92c276641c729e7707f1e894a2bfb51cc59abdd3bdfb532da8a0", size = 17963, upload-time = "2025-01-15T18:46:21.143Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
Reference in New Issue
Block a user