mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 19:17:13 -05:00 
			
		
		
		
	Merge branch 'dev' into feature-ai
This commit is contained in:
		
						commit
						540539643c
					
				@ -159,6 +159,23 @@ Available options are `postgresql` and `mariadb`.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    Defaults to unset, which uses Django’s built-in defaults.
 | 
					    Defaults to unset, which uses Django’s built-in defaults.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### [`PAPERLESS_DB_POOLSIZE=<int>`](#PAPERLESS_DB_POOLSIZE) {#PAPERLESS_DB_POOLSIZE}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					: Defines the maximum number of database connections to keep in the pool.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Only applies to PostgreSQL. This setting is ignored for other database engines.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    The value must be greater than or equal to 1 to be used.
 | 
				
			||||||
 | 
					    Defaults to unset, which disables connection pooling.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    !!! note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    A small pool is typically sufficient — for example, a size of 4.
 | 
				
			||||||
 | 
					    Make sure your PostgreSQL server's max_connections setting is large enough to handle:
 | 
				
			||||||
 | 
					    ```(Paperless workers + Celery workers) × pool size + safety margin```
 | 
				
			||||||
 | 
					    For example, with 4 Paperless workers and 2 Celery workers, and a pool size of 4:
 | 
				
			||||||
 | 
					    (4 + 2) × 4 + 10 = 34 connections required.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
 | 
					#### [`PAPERLESS_DB_READ_CACHE_ENABLED=<bool>`](#PAPERLESS_DB_READ_CACHE_ENABLED) {#PAPERLESS_DB_READ_CACHE_ENABLED}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
: Caches the database read query results into Redis. This can significantly improve application response times by caching database queries, at the cost of slightly increased memory usage.
 | 
					: Caches the database read query results into Redis. This can significantly improve application response times by caching database queries, at the cost of slightly increased memory usage.
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,9 @@ Each document has data fields that you can assign to them:
 | 
				
			|||||||
-   A _document type_ is used to demarcate the type of a document such
 | 
					-   A _document type_ is used to demarcate the type of a document such
 | 
				
			||||||
    as letter, bank statement, invoice, contract, etc. It is used to
 | 
					    as letter, bank statement, invoice, contract, etc. It is used to
 | 
				
			||||||
    identify what a document is about.
 | 
					    identify what a document is about.
 | 
				
			||||||
 | 
					-   The document _storage path_ is the location where the document files
 | 
				
			||||||
 | 
					    are stored. See [Storage Paths](advanced_usage.md#storage-paths) for
 | 
				
			||||||
 | 
					    more information.
 | 
				
			||||||
-   The _date added_ of a document is the date the document was scanned
 | 
					-   The _date added_ of a document is the date the document was scanned
 | 
				
			||||||
    into paperless. You cannot and should not change this date.
 | 
					    into paperless. You cannot and should not change this date.
 | 
				
			||||||
-   The _date created_ of a document is the date the document was
 | 
					-   The _date created_ of a document is the date the document was
 | 
				
			||||||
 | 
				
			|||||||
@ -60,6 +60,7 @@ dependencies = [
 | 
				
			|||||||
  "openai>=1.76",
 | 
					  "openai>=1.76",
 | 
				
			||||||
  "pathvalidate~=3.3.1",
 | 
					  "pathvalidate~=3.3.1",
 | 
				
			||||||
  "pdf2image~=1.17.0",
 | 
					  "pdf2image~=1.17.0",
 | 
				
			||||||
 | 
					  "psycopg-pool",
 | 
				
			||||||
  "python-dateutil~=2.9.0",
 | 
					  "python-dateutil~=2.9.0",
 | 
				
			||||||
  "python-dotenv~=1.1.0",
 | 
					  "python-dotenv~=1.1.0",
 | 
				
			||||||
  "python-gnupg~=0.5.4",
 | 
					  "python-gnupg~=0.5.4",
 | 
				
			||||||
@ -71,7 +72,7 @@ dependencies = [
 | 
				
			|||||||
  "scikit-learn~=1.7.0",
 | 
					  "scikit-learn~=1.7.0",
 | 
				
			||||||
  "sentence-transformers>=4.1",
 | 
					  "sentence-transformers>=4.1",
 | 
				
			||||||
  "setproctitle~=1.3.4",
 | 
					  "setproctitle~=1.3.4",
 | 
				
			||||||
  "tika-client~=0.9.0",
 | 
					  "tika-client~=0.10.0",
 | 
				
			||||||
  "tqdm~=4.67.1",
 | 
					  "tqdm~=4.67.1",
 | 
				
			||||||
  "watchdog~=6.0",
 | 
					  "watchdog~=6.0",
 | 
				
			||||||
  "whitenoise~=6.9",
 | 
					  "whitenoise~=6.9",
 | 
				
			||||||
@ -83,9 +84,10 @@ optional-dependencies.mariadb = [
 | 
				
			|||||||
  "mysqlclient~=2.2.7",
 | 
					  "mysqlclient~=2.2.7",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
optional-dependencies.postgres = [
 | 
					optional-dependencies.postgres = [
 | 
				
			||||||
  "psycopg[c]==3.2.9",
 | 
					  "psycopg[c,pool]==3.2.9",
 | 
				
			||||||
  # Direct dependency for proper resolution of the pre-built wheels
 | 
					  # Direct dependency for proper resolution of the pre-built wheels
 | 
				
			||||||
  "psycopg-c==3.2.9",
 | 
					  "psycopg-c==3.2.9",
 | 
				
			||||||
 | 
					  "psycopg-pool==3.2.6",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
optional-dependencies.webserver = [
 | 
					optional-dependencies.webserver = [
 | 
				
			||||||
  "granian[uvloop]~=2.4.1",
 | 
					  "granian[uvloop]~=2.4.1",
 | 
				
			||||||
@ -211,15 +213,9 @@ 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
 | 
				
			||||||
@ -229,9 +225,6 @@ 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">102</context>
 | 
					          <context context-type="linenumber">103</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/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">102</context>
 | 
					          <context context-type="linenumber">103</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/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">102</context>
 | 
					          <context context-type="linenumber">103</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/manage/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">102</context>
 | 
					          <context context-type="linenumber">103</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">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/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">73</context>
 | 
					          <context context-type="linenumber">74</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">51</context>
 | 
					          <context context-type="linenumber">52</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">51</context>
 | 
					          <context context-type="linenumber">52</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">51</context>
 | 
					          <context context-type="linenumber">52</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">51</context>
 | 
					          <context context-type="linenumber">52</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">6</context>
 | 
					          <context context-type="linenumber">7</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">6</context>
 | 
					          <context context-type="linenumber">7</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">6</context>
 | 
					          <context context-type="linenumber">7</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">6</context>
 | 
					          <context context-type="linenumber">7</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">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/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">74</context>
 | 
					          <context context-type="linenumber">73</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">3</context>
 | 
					          <context context-type="linenumber">4</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">3</context>
 | 
					          <context context-type="linenumber">4</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">3</context>
 | 
					          <context context-type="linenumber">4</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">3</context>
 | 
					          <context context-type="linenumber">4</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">20</context>
 | 
					          <context context-type="linenumber">21</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/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">21</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">21</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">21</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">37</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">37</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">37</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">37</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/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">43</context>
 | 
					          <context context-type="linenumber">44</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">43</context>
 | 
					          <context context-type="linenumber">44</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">43</context>
 | 
					          <context context-type="linenumber">44</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">43</context>
 | 
					          <context context-type="linenumber">44</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">9</context>
 | 
					          <context context-type="linenumber">10</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">9</context>
 | 
					          <context context-type="linenumber">10</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">9</context>
 | 
					          <context context-type="linenumber">10</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">9</context>
 | 
					          <context context-type="linenumber">10</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">85</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">85</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">85</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">85</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">96</context>
 | 
					          <context context-type="linenumber">97</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/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">96</context>
 | 
					          <context context-type="linenumber">97</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/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">96</context>
 | 
					          <context context-type="linenumber">97</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/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">96</context>
 | 
					          <context context-type="linenumber">97</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
        <context-group purpose="location">
 | 
					        <context-group purpose="location">
 | 
				
			||||||
          <context context-type="sourcefile">src/app/components/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">83</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">83</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">83</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">83</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">93</context>
 | 
					          <context context-type="linenumber">94</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">93</context>
 | 
					          <context context-type="linenumber">94</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">93</context>
 | 
					          <context context-type="linenumber">94</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">93</context>
 | 
					          <context context-type="linenumber">94</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">12</context>
 | 
					          <context context-type="linenumber">13</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">12</context>
 | 
					          <context context-type="linenumber">13</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">12</context>
 | 
					          <context context-type="linenumber">13</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">12</context>
 | 
					          <context context-type="linenumber">13</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">86</context>
 | 
					          <context context-type="linenumber">87</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">86</context>
 | 
					          <context context-type="linenumber">87</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">86</context>
 | 
					          <context context-type="linenumber">87</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">86</context>
 | 
					          <context context-type="linenumber">87</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">19</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">19</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">19</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">19</context>
 | 
					          <context context-type="linenumber">20</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">38</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">38</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">38</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">38</context>
 | 
					          <context context-type="linenumber">39</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">39</context>
 | 
					          <context context-type="linenumber">40</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">40</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">40</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">40</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">118</context>
 | 
					          <context context-type="linenumber">119</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">118</context>
 | 
					          <context context-type="linenumber">119</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">118</context>
 | 
					          <context context-type="linenumber">119</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">118</context>
 | 
					          <context context-type="linenumber">119</context>
 | 
				
			||||||
        </context-group>
 | 
					        </context-group>
 | 
				
			||||||
      </trans-unit>
 | 
					      </trans-unit>
 | 
				
			||||||
      <trans-unit id="810888510148304696" datatype="html">
 | 
					      <trans-unit id="810888510148304696" datatype="html">
 | 
				
			||||||
 | 
				
			|||||||
@ -51,7 +51,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-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Discard</button>
 | 
					            <button type="button" (click)="discardChanges()" class="btn btn-outline-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="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
 | 
					  <button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
 | 
				
			||||||
  <button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
 | 
					  <button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
<pngx-page-header title="{{ typeNamePlural | titlecase }}">
 | 
					<pngx-page-header title="{{ typeNamePlural | titlecase }}" info="View, add, edit and delete {{ typeNamePlural }}." infoLink="usage/#terms-and-definitions">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
 | 
					  <button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
 | 
				
			||||||
    <i-bs  name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
 | 
					    <i-bs  name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
 | 
				
			|||||||
@ -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'))[3]
 | 
					    const createButton = fixture.debugElement.queryAll(By.css('button'))[4]
 | 
				
			||||||
    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'))[6]
 | 
					    const editButton = fixture.debugElement.queryAll(By.css('button'))[7]
 | 
				
			||||||
    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'))[7]
 | 
					    const deleteButton = fixture.debugElement.queryAll(By.css('button'))[8]
 | 
				
			||||||
    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'))[8]
 | 
					    const filterButton = fixture.debugElement.queryAll(By.css('button'))[9]
 | 
				
			||||||
    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="submit" class="btn btn-primary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
 | 
					  <button type="button" (click)="reset()" class="btn btn-outline-secondary mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
 | 
				
			||||||
  <button type="button" (click)="reset()" class="btn btn-secondary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Cancel</button>
 | 
					  <button type="submit" class="btn btn-primary ms-2 mb-2" [disabled]="(isDirty$ | async) === false" i18n>Save</button>
 | 
				
			||||||
</form>
 | 
					</form>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import os
 | 
					import os
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -7,19 +8,15 @@ 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):
 | 
					def create_source_path_directory(source_path: Path) -> None:
 | 
				
			||||||
    os.makedirs(os.path.dirname(source_path), exist_ok=True)
 | 
					    source_path.parent.mkdir(parents=True, exist_ok=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def delete_empty_directories(directory, root):
 | 
					def delete_empty_directories(directory: Path, root: Path) -> None:
 | 
				
			||||||
    if not os.path.isdir(directory):
 | 
					    if not directory.is_dir():
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Go up in the directory hierarchy and try to delete all directories
 | 
					    if not directory.is_relative_to(root):
 | 
				
			||||||
    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:
 | 
				
			||||||
@ -27,11 +24,12 @@ def delete_empty_directories(directory, root):
 | 
				
			|||||||
        #   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 os.listdir(directory):
 | 
					        if not list(directory.iterdir()):
 | 
				
			||||||
            # it's empty
 | 
					            # it's empty
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                os.rmdir(directory)
 | 
					                directory.rmdir()
 | 
				
			||||||
            except OSError:
 | 
					            except OSError:
 | 
				
			||||||
                # whatever. empty directories aren't that bad anyway.
 | 
					                # whatever. empty directories aren't that bad anyway.
 | 
				
			||||||
                return
 | 
					                return
 | 
				
			||||||
@ -40,10 +38,10 @@ def delete_empty_directories(directory, root):
 | 
				
			|||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # go one level up
 | 
					        # go one level up
 | 
				
			||||||
        directory = os.path.normpath(os.path.dirname(directory))
 | 
					        directory = directory.parent
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_unique_filename(doc, *, archive_filename=False):
 | 
					def generate_unique_filename(doc, *, archive_filename=False) -> Path:
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Generates a unique filename for doc in settings.ORIGINALS_DIR.
 | 
					    Generates a unique filename for doc in settings.ORIGINALS_DIR.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -56,21 +54,32 @@ def generate_unique_filename(doc, *, archive_filename=False):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    if archive_filename:
 | 
					    if archive_filename:
 | 
				
			||||||
        old_filename = doc.archive_filename
 | 
					        old_filename: Path | None = (
 | 
				
			||||||
 | 
					            Path(doc.archive_filename) if doc.archive_filename else None
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        root = settings.ARCHIVE_DIR
 | 
					        root = settings.ARCHIVE_DIR
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        old_filename = doc.filename
 | 
					        old_filename = Path(doc.filename) if doc.filename else None
 | 
				
			||||||
        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:
 | 
				
			||||||
        new_filename = os.path.splitext(doc.filename)[0] + ".pdf"
 | 
					        # Generate the full path using the same logic as generate_filename
 | 
				
			||||||
        if new_filename == old_filename or not os.path.exists(
 | 
					        base_generated = generate_filename(doc, archive_filename=archive_filename)
 | 
				
			||||||
            os.path.join(root, new_filename),
 | 
					
 | 
				
			||||||
        ):
 | 
					        # Try to create a simple PDF version based on the original filename
 | 
				
			||||||
            return new_filename
 | 
					        # but preserve any directory structure from the template
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -84,7 +93,7 @@ def generate_unique_filename(doc, *, archive_filename=False):
 | 
				
			|||||||
            # still the same as before.
 | 
					            # still the same as before.
 | 
				
			||||||
            return new_filename
 | 
					            return new_filename
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if os.path.exists(os.path.join(root, new_filename)):
 | 
					        if (root / new_filename).exists():
 | 
				
			||||||
            counter += 1
 | 
					            counter += 1
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            return new_filename
 | 
					            return new_filename
 | 
				
			||||||
@ -96,8 +105,8 @@ def generate_filename(
 | 
				
			|||||||
    counter=0,
 | 
					    counter=0,
 | 
				
			||||||
    append_gpg=True,
 | 
					    append_gpg=True,
 | 
				
			||||||
    archive_filename=False,
 | 
					    archive_filename=False,
 | 
				
			||||||
):
 | 
					) -> Path:
 | 
				
			||||||
    path = ""
 | 
					    base_path: Path | None = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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(
 | 
				
			||||||
@ -134,17 +143,34 @@ 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:
 | 
				
			||||||
        path = format_filename(doc, filename_format)
 | 
					        rendered_path: str | None = 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 path:
 | 
					    if base_path:
 | 
				
			||||||
        filename = f"{path}{counter_str}{filetype_str}"
 | 
					        # Split the path into directory and filename parts
 | 
				
			||||||
 | 
					        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:
 | 
				
			||||||
        filename = f"{doc.pk:07}{counter_str}{filetype_str}"
 | 
					        # No template, use document ID
 | 
				
			||||||
 | 
					        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:
 | 
				
			||||||
        filename += ".gpg"
 | 
					        full_path = full_path.with_suffix(full_path.suffix + ".gpg")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return filename
 | 
					    return full_path
 | 
				
			||||||
 | 
				
			|||||||
@ -236,10 +236,7 @@ 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(
 | 
				
			||||||
                        os.path.join(
 | 
					                        self.original_target / options["zip_name"],
 | 
				
			||||||
                            self.original_target,
 | 
					 | 
				
			||||||
                            options["zip_name"],
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                        format="zip",
 | 
					                        format="zip",
 | 
				
			||||||
                        root_dir=temp_dir.name,
 | 
					                        root_dir=temp_dir.name,
 | 
				
			||||||
                    )
 | 
					                    )
 | 
				
			||||||
@ -342,7 +339,7 @@ class Command(CryptMixin, BaseCommand):
 | 
				
			|||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if self.split_manifest:
 | 
					            if self.split_manifest:
 | 
				
			||||||
                manifest_name = Path(base_name + "-manifest.json")
 | 
					                manifest_name = base_name.with_name(f"{base_name.stem}-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()
 | 
				
			||||||
@ -416,7 +413,7 @@ class Command(CryptMixin, BaseCommand):
 | 
				
			|||||||
                    else:
 | 
					                    else:
 | 
				
			||||||
                        item.unlink()
 | 
					                        item.unlink()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def generate_base_name(self, document: Document) -> str:
 | 
					    def generate_base_name(self, document: Document) -> Path:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        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)
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -436,12 +433,12 @@ class Command(CryptMixin, BaseCommand):
 | 
				
			|||||||
                break
 | 
					                break
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
                filename_counter += 1
 | 
					                filename_counter += 1
 | 
				
			||||||
        return base_name
 | 
					        return Path(base_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def generate_document_targets(
 | 
					    def generate_document_targets(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        document: Document,
 | 
					        document: Document,
 | 
				
			||||||
        base_name: str,
 | 
					        base_name: Path,
 | 
				
			||||||
        document_dict: dict,
 | 
					        document_dict: dict,
 | 
				
			||||||
    ) -> tuple[Path, Path | None, Path | None]:
 | 
					    ) -> tuple[Path, Path | None, Path | None]:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -449,25 +446,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 = os.path.join("originals", original_name)
 | 
					            original_name = Path("originals") / original_name
 | 
				
			||||||
        original_target = (self.target / Path(original_name)).resolve()
 | 
					        original_target = (self.target / original_name).resolve()
 | 
				
			||||||
        document_dict[EXPORTER_FILE_NAME] = original_name
 | 
					        document_dict[EXPORTER_FILE_NAME] = str(original_name)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not self.no_thumbnail:
 | 
					        if not self.no_thumbnail:
 | 
				
			||||||
            thumbnail_name = base_name + "-thumbnail.webp"
 | 
					            thumbnail_name = base_name.parent / (base_name.stem + "-thumbnail.webp")
 | 
				
			||||||
            if self.use_folder_prefix:
 | 
					            if self.use_folder_prefix:
 | 
				
			||||||
                thumbnail_name = os.path.join("thumbnails", thumbnail_name)
 | 
					                thumbnail_name = Path("thumbnails") / thumbnail_name
 | 
				
			||||||
            thumbnail_target = (self.target / Path(thumbnail_name)).resolve()
 | 
					            thumbnail_target = (self.target / thumbnail_name).resolve()
 | 
				
			||||||
            document_dict[EXPORTER_THUMBNAIL_NAME] = thumbnail_name
 | 
					            document_dict[EXPORTER_THUMBNAIL_NAME] = str(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 + "-archive.pdf"
 | 
					            archive_name = base_name.parent / (base_name.stem + "-archive.pdf")
 | 
				
			||||||
            if self.use_folder_prefix:
 | 
					            if self.use_folder_prefix:
 | 
				
			||||||
                archive_name = os.path.join("archive", archive_name)
 | 
					                archive_name = Path("archive") / archive_name
 | 
				
			||||||
            archive_target = (self.target / Path(archive_name)).resolve()
 | 
					            archive_target = (self.target / archive_name).resolve()
 | 
				
			||||||
            document_dict[EXPORTER_ARCHIVE_NAME] = archive_name
 | 
					            document_dict[EXPORTER_ARCHIVE_NAME] = str(archive_name)
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            archive_target = None
 | 
					            archive_target = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -572,7 +569,7 @@ class Command(CryptMixin, BaseCommand):
 | 
				
			|||||||
        perform_copy = False
 | 
					        perform_copy = False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if target.exists():
 | 
					        if target.exists():
 | 
				
			||||||
            source_stat = os.stat(source)
 | 
					            source_stat = source.stat()
 | 
				
			||||||
            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()
 | 
				
			||||||
 | 
				
			|||||||
@ -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 Path(self.source_path).open("rb")
 | 
					        return self.source_path.open("rb")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def file_name(self):
 | 
					    def file_name(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
@ -12,11 +12,13 @@ from celery.signals import before_task_publish
 | 
				
			|||||||
from celery.signals import task_failure
 | 
					from celery.signals import task_failure
 | 
				
			||||||
from celery.signals import task_postrun
 | 
					from celery.signals import task_postrun
 | 
				
			||||||
from celery.signals import task_prerun
 | 
					from celery.signals import task_prerun
 | 
				
			||||||
 | 
					from celery.signals import worker_process_init
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.contrib.auth.models import Group
 | 
					from django.contrib.auth.models import Group
 | 
				
			||||||
from django.contrib.auth.models import User
 | 
					from django.contrib.auth.models import User
 | 
				
			||||||
from django.db import DatabaseError
 | 
					from django.db import DatabaseError
 | 
				
			||||||
from django.db import close_old_connections
 | 
					from django.db import close_old_connections
 | 
				
			||||||
 | 
					from django.db import connections
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.db.models import Q
 | 
					from django.db.models import Q
 | 
				
			||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
@ -51,8 +53,6 @@ from documents.templating.workflows import parse_w_workflow_placeholders
 | 
				
			|||||||
from paperless.config import AIConfig
 | 
					from paperless.config import AIConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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
 | 
				
			||||||
@ -329,15 +329,16 @@ 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 = os.path.split(instance.source_path)[1]
 | 
					            old_filename = Path(instance.source_path).name
 | 
				
			||||||
            (old_filebase, old_fileext) = os.path.splitext(old_filename)
 | 
					            old_filebase = Path(old_filename).stem
 | 
				
			||||||
 | 
					            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 os.path.exists(new_file_path):
 | 
					                if new_file_path.exists():
 | 
				
			||||||
                    counter += 1
 | 
					                    counter += 1
 | 
				
			||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    break
 | 
					                    break
 | 
				
			||||||
@ -361,26 +362,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 os.path.isfile(filename):
 | 
					            if filename and filename.is_file():
 | 
				
			||||||
                try:
 | 
					                try:
 | 
				
			||||||
                    os.unlink(filename)
 | 
					                    filename.unlink()
 | 
				
			||||||
                    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 os.path.isfile(filename):
 | 
					            elif filename and not filename.is_file():
 | 
				
			||||||
                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(
 | 
				
			||||||
            os.path.dirname(instance.source_path),
 | 
					            Path(instance.source_path).parent,
 | 
				
			||||||
            root=settings.ORIGINALS_DIR,
 | 
					            root=settings.ORIGINALS_DIR,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if instance.has_archive_version:
 | 
					        if instance.has_archive_version:
 | 
				
			||||||
            delete_empty_directories(
 | 
					            delete_empty_directories(
 | 
				
			||||||
                os.path.dirname(instance.archive_path),
 | 
					                Path(instance.archive_path).parent,
 | 
				
			||||||
                root=settings.ARCHIVE_DIR,
 | 
					                root=settings.ARCHIVE_DIR,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -401,14 +402,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, new_path):
 | 
					    def validate_move(instance, old_path: Path, new_path: Path):
 | 
				
			||||||
        if not os.path.isfile(old_path):
 | 
					        if not old_path.is_file():
 | 
				
			||||||
            # 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 os.path.isfile(new_path):
 | 
					        if new_path.is_file():
 | 
				
			||||||
            # 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)
 | 
				
			||||||
@ -436,16 +437,20 @@ 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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            instance.filename = generate_unique_filename(instance)
 | 
					            # Need to convert to string to be able to save it to the db
 | 
				
			||||||
 | 
					            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:
 | 
				
			||||||
                instance.archive_filename = generate_unique_filename(
 | 
					                # Need to convert to string to be able to save it to the db
 | 
				
			||||||
                    instance,
 | 
					                instance.archive_filename = str(
 | 
				
			||||||
                    archive_filename=True,
 | 
					                    generate_unique_filename(
 | 
				
			||||||
 | 
					                        instance,
 | 
				
			||||||
 | 
					                        archive_filename=True,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                move_archive = old_archive_filename != instance.archive_filename
 | 
					                move_archive = old_archive_filename != instance.archive_filename
 | 
				
			||||||
@ -487,11 +492,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 os.path.isfile(instance.source_path):
 | 
					                if move_original and instance.source_path.is_file():
 | 
				
			||||||
                    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 os.path.isfile(instance.archive_path):
 | 
					                if move_archive and instance.archive_path.is_file():
 | 
				
			||||||
                    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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -512,17 +517,15 @@ 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 os.path.isfile(old_source_path):
 | 
					        if not old_source_path.is_file():
 | 
				
			||||||
            delete_empty_directories(
 | 
					            delete_empty_directories(
 | 
				
			||||||
                os.path.dirname(old_source_path),
 | 
					                Path(old_source_path).parent,
 | 
				
			||||||
                root=settings.ORIGINALS_DIR,
 | 
					                root=settings.ORIGINALS_DIR,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if instance.has_archive_version and not os.path.isfile(
 | 
					        if instance.has_archive_version and not old_archive_path.is_file():
 | 
				
			||||||
            old_archive_path,
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            delete_empty_directories(
 | 
					            delete_empty_directories(
 | 
				
			||||||
                os.path.dirname(old_archive_path),
 | 
					                Path(old_archive_path).parent,
 | 
				
			||||||
                root=settings.ARCHIVE_DIR,
 | 
					                root=settings.ARCHIVE_DIR,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1228,10 +1231,7 @@ def run_workflows(
 | 
				
			|||||||
                    )
 | 
					                    )
 | 
				
			||||||
            files = None
 | 
					            files = None
 | 
				
			||||||
            if action.webhook.include_document:
 | 
					            if action.webhook.include_document:
 | 
				
			||||||
                with open(
 | 
					                with original_file.open("rb") as f:
 | 
				
			||||||
                    original_file,
 | 
					 | 
				
			||||||
                    "rb",
 | 
					 | 
				
			||||||
                ) as f:
 | 
					 | 
				
			||||||
                    files = {
 | 
					                    files = {
 | 
				
			||||||
                        "file": (
 | 
					                        "file": (
 | 
				
			||||||
                            filename,
 | 
					                            filename,
 | 
				
			||||||
@ -1452,6 +1452,21 @@ def task_failure_handler(
 | 
				
			|||||||
        logger.exception("Updating PaperlessTask failed")
 | 
					        logger.exception("Updating PaperlessTask failed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@worker_process_init.connect
 | 
				
			||||||
 | 
					def close_connection_pool_on_worker_init(**kwargs):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Close the DB connection pool for each Celery child process after it starts.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    This is necessary because the parent process parse the Django configuration,
 | 
				
			||||||
 | 
					    initializes connection pools then forks.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Closing these pools after forking ensures child processes have a valid connection.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    for conn in connections.all(initialized_only=True):
 | 
				
			||||||
 | 
					        if conn.alias == "default" and hasattr(conn, "pool") and conn.pool:
 | 
				
			||||||
 | 
					            conn.close_pool()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def add_or_update_document_in_llm_index(sender, document, **kwargs):
 | 
					def add_or_update_document_in_llm_index(sender, document, **kwargs):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Add or update a document in the LLM index when it is created or updated.
 | 
					    Add or update a document in the LLM index when it is created or updated.
 | 
				
			||||||
 | 
				
			|||||||
@ -41,11 +41,9 @@ 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.os.unlink") as mock_unlink:
 | 
					        with mock.patch("documents.signals.handlers.Path.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):
 | 
				
			||||||
@ -63,7 +61,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.os.unlink") as mock_unlink:
 | 
					        with mock.patch("documents.signals.handlers.Path.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), f"{document.pk:07d}.pdf")
 | 
					        self.assertEqual(generate_filename(document), Path(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),
 | 
				
			||||||
            f"{document.pk:07d}.pdf.gpg",
 | 
					            Path(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, "none/none.pdf")
 | 
					        self.assertEqual(document.filename, Path("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, "none/none.pdf.gpg")
 | 
					        self.assertEqual(document.filename, Path("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, "none/none.pdf")
 | 
					        self.assertEqual(document.filename, Path("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, "none/none.pdf")
 | 
					        self.assertEqual(document.filename, Path("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, "none/none.pdf")
 | 
					        self.assertEqual(document.filename, Path("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), "none - the_doc.pdf")
 | 
					        self.assertEqual(generate_filename(d), Path("none - the_doc.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        d.document_type = dt
 | 
					        d.document_type = dt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(d), "my_doc_type - the_doc.pdf")
 | 
					        self.assertEqual(generate_filename(d), Path("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), "652 - the_doc.pdf")
 | 
					        self.assertEqual(generate_filename(d1), Path("652 - the_doc.pdf"))
 | 
				
			||||||
        self.assertEqual(generate_filename(d2), "none - the_doc.pdf")
 | 
					        self.assertEqual(generate_filename(d2), Path("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), "doc1 tag1,tag2.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "doc2.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "2020-03-06.pdf")
 | 
					        self.assertEqual(generate_filename(doc1), Path("2020-03-06.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        doc1.created = datetime.date(2020, 11, 16)
 | 
					        doc1.created = datetime.date(2020, 11, 16)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(doc1), "2020-11-16.pdf")
 | 
					        self.assertEqual(generate_filename(doc1), Path("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), "232-01-09.pdf")
 | 
					        self.assertEqual(generate_filename(doc1), Path("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), "2020-11-16.pdf")
 | 
					        self.assertEqual(generate_filename(doc1), Path("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), "0000001.pdf")
 | 
					        self.assertEqual(generate_filename(document), Path("0000001.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        document.pk = 13579
 | 
					        document.pk = 13579
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(document), "0013579.pdf")
 | 
					        self.assertEqual(generate_filename(document), Path("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), "0000001.pdf")
 | 
					        self.assertEqual(generate_filename(document), Path("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), "0000001.pdf")
 | 
					        self.assertEqual(generate_filename(document), Path("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), "0000001.pdf")
 | 
					        self.assertEqual(generate_filename(document), Path("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), "document_apple.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "document_aubergine.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "This. is the title.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "my-invalid-..-title-yay.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "2020-05-21.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "TestFolder/2020-06-25.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "none - 2020-06-25.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "TestFolder/2020-06-25.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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), "2020/does not matter.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("2020/does not matter.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_multiple_doc_paths(self):
 | 
					    def test_multiple_doc_paths(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -1028,8 +1028,14 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(doc_a), "ThisIsAFolder/4/2020-06-25.pdf")
 | 
					        self.assertEqual(
 | 
				
			||||||
        self.assertEqual(generate_filename(doc_b), "SomeImportantNone/2020-07-25.pdf")
 | 
					            generate_filename(doc_a),
 | 
				
			||||||
 | 
					            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,
 | 
				
			||||||
@ -1064,8 +1070,11 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
            ),
 | 
					            ),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(doc_a), "0000002.pdf")
 | 
					        self.assertEqual(generate_filename(doc_a), Path("0000002.pdf"))
 | 
				
			||||||
        self.assertEqual(generate_filename(doc_b), "SomeImportantNone/2020-07-25.pdf")
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            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}",
 | 
				
			||||||
@ -1078,7 +1087,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
            pk=2,
 | 
					            pk=2,
 | 
				
			||||||
            checksum="2",
 | 
					            checksum="2",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(generate_filename(doc), "89/Dec/December/The Title.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("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}",
 | 
				
			||||||
@ -1091,7 +1100,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
            pk=2,
 | 
					            pk=2,
 | 
				
			||||||
            checksum="2",
 | 
					            checksum="2",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(generate_filename(doc), "84/August/Aug/The Title.pdf")
 | 
					        self.assertEqual(generate_filename(doc), Path("84/August/Aug/The Title.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override_settings(
 | 
					    @override_settings(
 | 
				
			||||||
        FILENAME_FORMAT="{owner_username}/{title}",
 | 
					        FILENAME_FORMAT="{owner_username}/{title}",
 | 
				
			||||||
@ -1124,8 +1133,8 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
            checksum="3",
 | 
					            checksum="3",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(owned_doc), "user1/The Title.pdf")
 | 
					        self.assertEqual(generate_filename(owned_doc), Path("user1/The Title.pdf"))
 | 
				
			||||||
        self.assertEqual(generate_filename(no_owner_doc), "none/does matter.pdf")
 | 
					        self.assertEqual(generate_filename(no_owner_doc), Path("none/does matter.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override_settings(
 | 
					    @override_settings(
 | 
				
			||||||
        FILENAME_FORMAT="{original_name}",
 | 
					        FILENAME_FORMAT="{original_name}",
 | 
				
			||||||
@ -1171,17 +1180,20 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
            original_filename="logs.txt",
 | 
					            original_filename="logs.txt",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(doc_with_original), "someepdf.pdf")
 | 
					        self.assertEqual(generate_filename(doc_with_original), Path("someepdf.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            generate_filename(tricky_with_original),
 | 
					            generate_filename(tricky_with_original),
 | 
				
			||||||
            "some pdf with spaces and stuff.pdf",
 | 
					            Path("some pdf with spaces and stuff.pdf"),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(no_original), "none.pdf")
 | 
					        self.assertEqual(generate_filename(no_original), Path("none.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(generate_filename(text_doc), "logs.txt")
 | 
					        self.assertEqual(generate_filename(text_doc), Path("logs.txt"))
 | 
				
			||||||
        self.assertEqual(generate_filename(text_doc, archive_filename=True), "logs.pdf")
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            generate_filename(text_doc, archive_filename=True),
 | 
				
			||||||
 | 
					            Path("logs.pdf"),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override_settings(
 | 
					    @override_settings(
 | 
				
			||||||
        FILENAME_FORMAT="XX{correspondent}/{title}",
 | 
					        FILENAME_FORMAT="XX{correspondent}/{title}",
 | 
				
			||||||
@ -1206,7 +1218,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, "XX/doc1.pdf")
 | 
					        self.assertEqual(document.filename, Path("XX/doc1.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_complex_template_strings(self):
 | 
					    def test_complex_template_strings(self):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@ -1244,19 +1256,19 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            generate_filename(doc_a),
 | 
					            generate_filename(doc_a),
 | 
				
			||||||
            "somepath/some where/2020-06-25/Does Matter.pdf",
 | 
					            Path("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),
 | 
				
			||||||
            "somepath/2024-10-01/Does Matter.pdf",
 | 
					            Path("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), "does matter23.pdf")
 | 
					        self.assertEqual(generate_filename(doc_a), Path("does matter23.pdf"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        sp.path = """
 | 
					        sp.path = """
 | 
				
			||||||
                 somepath/
 | 
					                 somepath/
 | 
				
			||||||
@ -1275,13 +1287,13 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        sp.save()
 | 
					        sp.save()
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(
 | 
				
			||||||
            generate_filename(doc_a),
 | 
					            generate_filename(doc_a),
 | 
				
			||||||
            "somepath/asn-000-200/Does Matter/Does Matter.pdf",
 | 
					            Path("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),
 | 
				
			||||||
            "somepath/asn-201-400/asn-3xx/Does Matter.pdf",
 | 
					            Path("somepath/asn-201-400/asn-3xx/Does Matter.pdf"),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @override_settings(
 | 
					    @override_settings(
 | 
				
			||||||
@ -1310,7 +1322,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),
 | 
				
			||||||
                "0000002.pdf",
 | 
					                Path("0000002.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertEqual(len(capture.output), 1)
 | 
					            self.assertEqual(len(capture.output), 1)
 | 
				
			||||||
@ -1345,7 +1357,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),
 | 
				
			||||||
                "0000002.pdf",
 | 
					                Path("0000002.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.assertEqual(len(capture.output), 1)
 | 
					            self.assertEqual(len(capture.output), 1)
 | 
				
			||||||
@ -1413,7 +1425,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc_a),
 | 
					                generate_filename(doc_a),
 | 
				
			||||||
                "invoices/1234.pdf",
 | 
					                Path("invoices/1234.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with override_settings(
 | 
					        with override_settings(
 | 
				
			||||||
@ -1427,7 +1439,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc_a),
 | 
					                generate_filename(doc_a),
 | 
				
			||||||
                "Some Title_ChoiceOne.pdf",
 | 
					                Path("Some Title_ChoiceOne.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Check for handling Nones well
 | 
					            # Check for handling Nones well
 | 
				
			||||||
@ -1436,7 +1448,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc_a),
 | 
					                generate_filename(doc_a),
 | 
				
			||||||
                "Some Title_Default Value.pdf",
 | 
					                Path("Some Title_Default Value.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        cf.name = "Invoice Number"
 | 
					        cf.name = "Invoice Number"
 | 
				
			||||||
@ -1449,7 +1461,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc_a),
 | 
					                generate_filename(doc_a),
 | 
				
			||||||
                "invoices/4567.pdf",
 | 
					                Path("invoices/4567.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with override_settings(
 | 
					        with override_settings(
 | 
				
			||||||
@ -1457,7 +1469,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc_a),
 | 
					                generate_filename(doc_a),
 | 
				
			||||||
                "invoices/0.pdf",
 | 
					                Path("invoices/0.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_datetime_filter(self):
 | 
					    def test_datetime_filter(self):
 | 
				
			||||||
@ -1496,7 +1508,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc_a),
 | 
					                generate_filename(doc_a),
 | 
				
			||||||
                "2020/Some Title.pdf",
 | 
					                Path("2020/Some Title.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with override_settings(
 | 
					        with override_settings(
 | 
				
			||||||
@ -1504,7 +1516,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc_a),
 | 
					                generate_filename(doc_a),
 | 
				
			||||||
                "2020-06-25/Some Title.pdf",
 | 
					                Path("2020-06-25/Some Title.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with override_settings(
 | 
					        with override_settings(
 | 
				
			||||||
@ -1512,7 +1524,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc_a),
 | 
					                generate_filename(doc_a),
 | 
				
			||||||
                "2024-10-01/Some Title.pdf",
 | 
					                Path("2024-10-01/Some Title.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_slugify_filter(self):
 | 
					    def test_slugify_filter(self):
 | 
				
			||||||
@ -1539,7 +1551,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc),
 | 
					                generate_filename(doc),
 | 
				
			||||||
                "some-title-with-special-characters.pdf",
 | 
					                Path("some-title-with-special-characters.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Test with correspondent name containing spaces and special chars
 | 
					        # Test with correspondent name containing spaces and special chars
 | 
				
			||||||
@ -1553,7 +1565,7 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc),
 | 
					                generate_filename(doc),
 | 
				
			||||||
                "johns-office-workplace/some-title-with-special-characters.pdf",
 | 
					                Path("johns-office-workplace/some-title-with-special-characters.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Test with custom fields
 | 
					        # Test with custom fields
 | 
				
			||||||
@ -1572,5 +1584,5 @@ class TestFilenameGeneration(DirectoriesMixin, TestCase):
 | 
				
			|||||||
        ):
 | 
					        ):
 | 
				
			||||||
            self.assertEqual(
 | 
					            self.assertEqual(
 | 
				
			||||||
                generate_filename(doc),
 | 
					                generate_filename(doc),
 | 
				
			||||||
                "brussels-belgium/some-title-with-special-characters.pdf",
 | 
					                Path("brussels-belgium/some-title-with-special-characters.pdf"),
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
				
			|||||||
@ -209,7 +209,7 @@ class TestExportImport(
 | 
				
			|||||||
            4,
 | 
					            4,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIsFile((self.target / "manifest.json").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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,9 +235,7 @@ 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:
 | 
				
			||||||
@ -252,7 +250,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:
 | 
				
			||||||
@ -312,7 +310,7 @@ class TestExportImport(
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self._do_export()
 | 
					        self._do_export()
 | 
				
			||||||
        self.assertIsFile((self.target / "manifest.json").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        st_mtime_1 = (self.target / "manifest.json").stat().st_mtime
 | 
					        st_mtime_1 = (self.target / "manifest.json").stat().st_mtime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -322,7 +320,7 @@ class TestExportImport(
 | 
				
			|||||||
            self._do_export()
 | 
					            self._do_export()
 | 
				
			||||||
            m.assert_not_called()
 | 
					            m.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIsFile((self.target / "manifest.json").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
        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()
 | 
				
			||||||
@ -334,7 +332,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").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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)
 | 
				
			||||||
@ -352,7 +350,7 @@ class TestExportImport(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self._do_export()
 | 
					        self._do_export()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIsFile((self.target / "manifest.json").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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",
 | 
				
			||||||
@ -360,7 +358,7 @@ class TestExportImport(
 | 
				
			|||||||
            self._do_export()
 | 
					            self._do_export()
 | 
				
			||||||
            m.assert_not_called()
 | 
					            m.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIsFile((self.target / "manifest.json").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.d2.checksum = "asdfasdgf3"
 | 
					        self.d2.checksum = "asdfasdgf3"
 | 
				
			||||||
        self.d2.save()
 | 
					        self.d2.save()
 | 
				
			||||||
@ -371,7 +369,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").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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")
 | 
				
			||||||
@ -385,7 +383,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(
 | 
				
			||||||
            (self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
 | 
					            str(self.target / doc_from_manifest[EXPORTER_FILE_NAME]),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.d3.delete()
 | 
					        self.d3.delete()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -397,12 +395,12 @@ class TestExportImport(
 | 
				
			|||||||
            self.d3.id,
 | 
					            self.d3.id,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertIsFile(
 | 
					        self.assertIsFile(
 | 
				
			||||||
            (self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
 | 
					            self.target / doc_from_manifest[EXPORTER_FILE_NAME],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        manifest = self._do_export(delete=True)
 | 
					        manifest = self._do_export(delete=True)
 | 
				
			||||||
        self.assertIsNotFile(
 | 
					        self.assertIsNotFile(
 | 
				
			||||||
            (self.target / doc_from_manifest[EXPORTER_FILE_NAME]).as_posix(),
 | 
					            self.target / doc_from_manifest[EXPORTER_FILE_NAME],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertTrue(len(manifest), 6)
 | 
					        self.assertTrue(len(manifest), 6)
 | 
				
			||||||
@ -416,20 +414,20 @@ class TestExportImport(
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self._do_export(use_filename_format=True)
 | 
					        self._do_export(use_filename_format=True)
 | 
				
			||||||
        self.assertIsFile((self.target / "wow1" / "c.pdf").as_posix())
 | 
					        self.assertIsFile(self.target / "wow1" / "c.pdf")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.assertIsFile((self.target / "manifest.json").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        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").as_posix())
 | 
					        self.assertIsNotFile(self.target / "wow1" / "c.pdf")
 | 
				
			||||||
        self.assertIsNotDir((self.target / "wow1").as_posix())
 | 
					        self.assertIsNotDir(self.target / "wow1")
 | 
				
			||||||
        self.assertIsFile((self.target / "new_title" / "c.pdf").as_posix())
 | 
					        self.assertIsFile(self.target / "new_title" / "c.pdf")
 | 
				
			||||||
        self.assertIsFile((self.target / "manifest.json").as_posix())
 | 
					        self.assertIsFile(self.target / "manifest.json")
 | 
				
			||||||
        self.assertIsFile((self.target / "wow2" / "none.pdf").as_posix())
 | 
					        self.assertIsFile(self.target / "wow2" / "none.pdf")
 | 
				
			||||||
        self.assertIsFile(
 | 
					        self.assertIsFile(
 | 
				
			||||||
            (self.target / "wow2" / "none_01.pdf").as_posix(),
 | 
					            self.target / "wow2" / "none_01.pdf",
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_export_missing_files(self):
 | 
					    def test_export_missing_files(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -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).as_posix()
 | 
					    return Path(settings.ORIGINALS_DIR) / fname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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).as_posix()
 | 
					    return Path(settings.ORIGINALS_DIR) / fname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@override_settings(PASSPHRASE="test")
 | 
					@override_settings(PASSPHRASE="test")
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,7 @@ msgid ""
 | 
				
			|||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
"Project-Id-Version: paperless-ngx\n"
 | 
					"Project-Id-Version: paperless-ngx\n"
 | 
				
			||||||
"Report-Msgid-Bugs-To: \n"
 | 
					"Report-Msgid-Bugs-To: \n"
 | 
				
			||||||
"POT-Creation-Date: 2025-07-08 21:14+0000\n"
 | 
					"POT-Creation-Date: 2025-08-02 12:55+0000\n"
 | 
				
			||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
 | 
					"PO-Revision-Date: 2022-02-17 04:17\n"
 | 
				
			||||||
"Last-Translator: \n"
 | 
					"Last-Translator: \n"
 | 
				
			||||||
"Language-Team: English\n"
 | 
					"Language-Team: English\n"
 | 
				
			||||||
@ -1645,147 +1645,147 @@ msgstr ""
 | 
				
			|||||||
msgid "paperless application settings"
 | 
					msgid "paperless application settings"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:762
 | 
					#: paperless/settings.py:774
 | 
				
			||||||
msgid "English (US)"
 | 
					msgid "English (US)"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:763
 | 
					#: paperless/settings.py:775
 | 
				
			||||||
msgid "Arabic"
 | 
					msgid "Arabic"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:764
 | 
					#: paperless/settings.py:776
 | 
				
			||||||
msgid "Afrikaans"
 | 
					msgid "Afrikaans"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:765
 | 
					#: paperless/settings.py:777
 | 
				
			||||||
msgid "Belarusian"
 | 
					msgid "Belarusian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:766
 | 
					#: paperless/settings.py:778
 | 
				
			||||||
msgid "Bulgarian"
 | 
					msgid "Bulgarian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:767
 | 
					#: paperless/settings.py:779
 | 
				
			||||||
msgid "Catalan"
 | 
					msgid "Catalan"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:768
 | 
					#: paperless/settings.py:780
 | 
				
			||||||
msgid "Czech"
 | 
					msgid "Czech"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:769
 | 
					#: paperless/settings.py:781
 | 
				
			||||||
msgid "Danish"
 | 
					msgid "Danish"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:770
 | 
					#: paperless/settings.py:782
 | 
				
			||||||
msgid "German"
 | 
					msgid "German"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:771
 | 
					#: paperless/settings.py:783
 | 
				
			||||||
msgid "Greek"
 | 
					msgid "Greek"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:772
 | 
					#: paperless/settings.py:784
 | 
				
			||||||
msgid "English (GB)"
 | 
					msgid "English (GB)"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:773
 | 
					#: paperless/settings.py:785
 | 
				
			||||||
msgid "Spanish"
 | 
					msgid "Spanish"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:774
 | 
					#: paperless/settings.py:786
 | 
				
			||||||
msgid "Persian"
 | 
					msgid "Persian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:775
 | 
					#: paperless/settings.py:787
 | 
				
			||||||
msgid "Finnish"
 | 
					msgid "Finnish"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:776
 | 
					#: paperless/settings.py:788
 | 
				
			||||||
msgid "French"
 | 
					msgid "French"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:777
 | 
					#: paperless/settings.py:789
 | 
				
			||||||
msgid "Hungarian"
 | 
					msgid "Hungarian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:778
 | 
					#: paperless/settings.py:790
 | 
				
			||||||
msgid "Italian"
 | 
					msgid "Italian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:779
 | 
					#: paperless/settings.py:791
 | 
				
			||||||
msgid "Japanese"
 | 
					msgid "Japanese"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:780
 | 
					#: paperless/settings.py:792
 | 
				
			||||||
msgid "Korean"
 | 
					msgid "Korean"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:781
 | 
					#: paperless/settings.py:793
 | 
				
			||||||
msgid "Luxembourgish"
 | 
					msgid "Luxembourgish"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:782
 | 
					#: paperless/settings.py:794
 | 
				
			||||||
msgid "Norwegian"
 | 
					msgid "Norwegian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:783
 | 
					#: paperless/settings.py:795
 | 
				
			||||||
msgid "Dutch"
 | 
					msgid "Dutch"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:784
 | 
					#: paperless/settings.py:796
 | 
				
			||||||
msgid "Polish"
 | 
					msgid "Polish"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:785
 | 
					#: paperless/settings.py:797
 | 
				
			||||||
msgid "Portuguese (Brazil)"
 | 
					msgid "Portuguese (Brazil)"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:786
 | 
					#: paperless/settings.py:798
 | 
				
			||||||
msgid "Portuguese"
 | 
					msgid "Portuguese"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:787
 | 
					#: paperless/settings.py:799
 | 
				
			||||||
msgid "Romanian"
 | 
					msgid "Romanian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:788
 | 
					#: paperless/settings.py:800
 | 
				
			||||||
msgid "Russian"
 | 
					msgid "Russian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:789
 | 
					#: paperless/settings.py:801
 | 
				
			||||||
msgid "Slovak"
 | 
					msgid "Slovak"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:790
 | 
					#: paperless/settings.py:802
 | 
				
			||||||
msgid "Slovenian"
 | 
					msgid "Slovenian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:791
 | 
					#: paperless/settings.py:803
 | 
				
			||||||
msgid "Serbian"
 | 
					msgid "Serbian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:792
 | 
					#: paperless/settings.py:804
 | 
				
			||||||
msgid "Swedish"
 | 
					msgid "Swedish"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:793
 | 
					#: paperless/settings.py:805
 | 
				
			||||||
msgid "Turkish"
 | 
					msgid "Turkish"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:794
 | 
					#: paperless/settings.py:806
 | 
				
			||||||
msgid "Ukrainian"
 | 
					msgid "Ukrainian"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:795
 | 
					#: paperless/settings.py:807
 | 
				
			||||||
msgid "Vietnamese"
 | 
					msgid "Vietnamese"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:796
 | 
					#: paperless/settings.py:808
 | 
				
			||||||
msgid "Chinese Simplified"
 | 
					msgid "Chinese Simplified"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#: paperless/settings.py:797
 | 
					#: paperless/settings.py:809
 | 
				
			||||||
msgid "Chinese Traditional"
 | 
					msgid "Chinese Traditional"
 | 
				
			||||||
msgstr ""
 | 
					msgstr ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -735,6 +735,9 @@ def _parse_db_settings() -> dict:
 | 
				
			|||||||
        # Leave room for future extensibility
 | 
					        # Leave room for future extensibility
 | 
				
			||||||
        if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
 | 
					        if os.getenv("PAPERLESS_DBENGINE") == "mariadb":
 | 
				
			||||||
            engine = "django.db.backends.mysql"
 | 
					            engine = "django.db.backends.mysql"
 | 
				
			||||||
 | 
					            # Contrary to Postgres, Django does not natively support connection pooling for MariaDB.
 | 
				
			||||||
 | 
					            # However, since MariaDB uses threads instead of forks, establishing connections is significantly faster
 | 
				
			||||||
 | 
					            # compared to PostgreSQL, so the lack of pooling is not an issue
 | 
				
			||||||
            options = {
 | 
					            options = {
 | 
				
			||||||
                "read_default_file": "/etc/mysql/my.cnf",
 | 
					                "read_default_file": "/etc/mysql/my.cnf",
 | 
				
			||||||
                "charset": "utf8mb4",
 | 
					                "charset": "utf8mb4",
 | 
				
			||||||
@ -754,6 +757,15 @@ def _parse_db_settings() -> dict:
 | 
				
			|||||||
                "sslcert": os.getenv("PAPERLESS_DBSSLCERT", None),
 | 
					                "sslcert": os.getenv("PAPERLESS_DBSSLCERT", None),
 | 
				
			||||||
                "sslkey": os.getenv("PAPERLESS_DBSSLKEY", None),
 | 
					                "sslkey": os.getenv("PAPERLESS_DBSSLKEY", None),
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            if int(os.getenv("PAPERLESS_DB_POOLSIZE", 0)) > 0:
 | 
				
			||||||
 | 
					                options.update(
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        "pool": {
 | 
				
			||||||
 | 
					                            "min_size": 1,
 | 
				
			||||||
 | 
					                            "max_size": int(os.getenv("PAPERLESS_DB_POOLSIZE")),
 | 
				
			||||||
 | 
					                        },
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        databases["default"]["ENGINE"] = engine
 | 
					        databases["default"]["ENGINE"] = engine
 | 
				
			||||||
        databases["default"]["OPTIONS"].update(options)
 | 
					        databases["default"]["OPTIONS"].update(options)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										45
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										45
									
								
								uv.lock
									
									
									
										generated
									
									
									
								
							@ -1,5 +1,5 @@
 | 
				
			|||||||
version = 1
 | 
					version = 1
 | 
				
			||||||
revision = 2
 | 
					revision = 3
 | 
				
			||||||
requires-python = ">=3.10"
 | 
					requires-python = ">=3.10"
 | 
				
			||||||
resolution-markers = [
 | 
					resolution-markers = [
 | 
				
			||||||
    "python_full_version >= '3.12' and sys_platform == 'darwin'",
 | 
					    "python_full_version >= '3.12' and sys_platform == 'darwin'",
 | 
				
			||||||
@ -436,15 +436,15 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "channels"
 | 
					name = "channels"
 | 
				
			||||||
version = "4.3.0"
 | 
					version = "4.3.1"
 | 
				
			||||||
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/72/04/6768c7a887f9c593c4d49f99130c8aec4ea06e750bc17c306b689f6caf3b/channels-4.3.0.tar.gz", hash = "sha256:7db32c61dcd88eada1647e6c6f6ad2eb724b75d4852eeff26ad1c51ccd1a37f7", size = 26816, upload-time = "2025-07-28T13:52:50.334Z" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -2762,6 +2762,7 @@ dependencies = [
 | 
				
			|||||||
    { name = "openai", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
					    { name = "openai", 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'" },
 | 
				
			||||||
@ -2788,10 +2789,11 @@ mariadb = [
 | 
				
			|||||||
    { name = "mysqlclient", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
					    { name = "mysqlclient", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
postgres = [
 | 
					postgres = [
 | 
				
			||||||
    { name = "psycopg", extra = ["c"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
					    { name = "psycopg", extra = ["c", "pool"], marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
				
			||||||
    { 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'" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
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'" },
 | 
				
			||||||
@ -2904,10 +2906,12 @@ requires-dist = [
 | 
				
			|||||||
    { name = "openai", specifier = ">=1.76" },
 | 
					    { name = "openai", specifier = ">=1.76" },
 | 
				
			||||||
    { name = "pathvalidate", specifier = "~=3.3.1" },
 | 
					    { name = "pathvalidate", specifier = "~=3.3.1" },
 | 
				
			||||||
    { name = "pdf2image", specifier = "~=1.17.0" },
 | 
					    { name = "pdf2image", specifier = "~=1.17.0" },
 | 
				
			||||||
    { name = "psycopg", extras = ["c"], marker = "extra == 'postgres'", specifier = "==3.2.9" },
 | 
					    { name = "psycopg", extras = ["c", "pool"], marker = "extra == 'postgres'", specifier = "==3.2.9" },
 | 
				
			||||||
    { 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'", 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" },
 | 
				
			||||||
@ -2919,7 +2923,7 @@ requires-dist = [
 | 
				
			|||||||
    { name = "scikit-learn", specifier = "~=1.7.0" },
 | 
					    { name = "scikit-learn", specifier = "~=1.7.0" },
 | 
				
			||||||
    { name = "sentence-transformers", specifier = ">=4.1" },
 | 
					    { name = "sentence-transformers", specifier = ">=4.1" },
 | 
				
			||||||
    { name = "setproctitle", specifier = "~=1.3.4" },
 | 
					    { name = "setproctitle", specifier = "~=1.3.4" },
 | 
				
			||||||
    { name = "tika-client", specifier = "~=0.9.0" },
 | 
					    { name = "tika-client", specifier = "~=0.10.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" },
 | 
				
			||||||
@ -3338,6 +3342,9 @@ 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_aarch64.whl" }, marker = "python_full_version == '3.12.*' and implementation_name != 'pypy' 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 implementation_name != 'pypy' 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 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 = [
 | 
				
			||||||
 | 
					    { name = "psycopg-pool", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "psycopg-c"
 | 
					name = "psycopg-c"
 | 
				
			||||||
@ -3375,6 +3382,18 @@ wheels = [
 | 
				
			|||||||
    { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl", hash = "sha256:250c357319242da102047b04c5cc78af872dbf85c2cb05abf114e1fb5f207917" },
 | 
					    { url = "https://github.com/paperless-ngx/builder/releases/download/psycopg-3.2.9/psycopg_c-3.2.9-cp312-cp312-linux_x86_64.whl", hash = "sha256:250c357319242da102047b04c5cc78af872dbf85c2cb05abf114e1fb5f207917" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "psycopg-pool"
 | 
				
			||||||
 | 
					version = "3.2.6"
 | 
				
			||||||
 | 
					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" }
 | 
				
			||||||
 | 
					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" },
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "pyasn1"
 | 
					name = "pyasn1"
 | 
				
			||||||
version = "0.6.1"
 | 
					version = "0.6.1"
 | 
				
			||||||
@ -3686,11 +3705,11 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "python-gnupg"
 | 
					name = "python-gnupg"
 | 
				
			||||||
version = "0.5.4"
 | 
					version = "0.5.5"
 | 
				
			||||||
source = { registry = "https://pypi.org/simple" }
 | 
					source = { registry = "https://pypi.org/simple" }
 | 
				
			||||||
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" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
@ -4438,16 +4457,16 @@ wheels = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "tika-client"
 | 
					name = "tika-client"
 | 
				
			||||||
version = "0.9.0"
 | 
					version = "0.10.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/94/ad/3508e42b470a037b3f5c19ca9993893d0faa30ba7ec7e6ac33db9bc3bf51/tika_client-0.9.0.tar.gz", hash = "sha256:c10bba8e40ede23c039f84ccd821fb2d290d339cc26cbd267ab9b561a1e83659", size = 2175246, upload-time = "2025-01-15T18:46:23.901Z" }
 | 
					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" }
 | 
				
			||||||
wheels = [
 | 
					wheels = [
 | 
				
			||||||
    { 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" },
 | 
					    { 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" },
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user