Compare commits

...

141 Commits

Author SHA1 Message Date
Michael Shamoon
301ad7e07d v1.7.0-rc1 2022-04-11 21:03:02 -07:00
Trenton Holmes
cc93616019 Merge pull request #691 from paperless-ngx/dependabot/pip/dev/django-4.0.4
Bump django from 4.0.3 to 4.0.4
2022-04-11 16:04:44 -07:00
dependabot[bot]
88066563d3 Bump django from 4.0.3 to 4.0.4
Bumps [django](https://github.com/django/django) from 4.0.3 to 4.0.4.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-11 20:37:09 +00:00
shamoon
9e3590352c Merge pull request #690 from paperless-ngx/fix-scanners-table
Fix: scanners table
2022-04-11 12:56:45 -07:00
Quinn Casey
86ad52639f Fix name 2022-04-11 12:34:37 -07:00
Quinn Casey
c00be946a5 Remove explicit "no" as was done in f303a1c 2022-04-11 12:32:32 -07:00
Quinn Casey
322aeeb552 Add 994d502 2022-04-11 12:23:27 -07:00
Quinn Casey
501d4cafa9 Add f303a1c 2022-04-11 12:21:52 -07:00
Quinn Casey
e556fb3e3a Add f2f7b71 2022-04-11 12:17:44 -07:00
Quinn Casey
bb930297d6 Reset table to b318c0a 2022-04-11 12:12:01 -07:00
shamoon
83f10167e5 Merge pull request #483 from stumpylog/check-if-file-busy
Adds wait for file before consuming
2022-04-11 11:56:05 -07:00
Quinn Casey
d47bb21389 Merge pull request #654 from paperless-ngx/fix-doc-detail-time
Fix: frontend document editing erases time data
2022-04-11 11:54:24 -07:00
Florian
4dedff00b8 Merge pull request #532 from paperless-ngx/feature-barcode-splitter
Feature barcode splitter
2022-04-11 08:51:48 +02:00
Trenton Holmes
2414dad656 Adds additional checking for both inotify and polling around document still being busy before consuming it 2022-04-10 12:21:34 -07:00
shamoon
8f98cb4860 Merge pull request #550 from stumpylog/feature-mail-consume-improve-docs
Feature mail consume improve docs
2022-04-10 08:46:45 -07:00
shamoon
6e96b7e00a Merge pull request #612 from stumpylog/bug-fix-filter-len
Increase length of SavedViewFilterRule
2022-04-10 08:46:25 -07:00
shamoon
00287b27ab Merge pull request #680 from paperless-ngx/bug-fix-fnmatch
[bug] - Fixes attachment filename matching during mail fetching
2022-04-08 13:59:22 -07:00
Trenton Holmes
8f18b7fd6c Fixes the matching of attachment files names to patterns 2022-04-08 13:42:42 -07:00
Felix E
dde7771dc6 Merge pull request #674 from paperless-ngx/feature-django4-csrf
Add `PAPERLESS_URL` env variable & CSRF var
2022-04-08 21:00:57 +02:00
Quinn Casey
4165184e42 Chore: Implement release-drafter action for Changelogs (#669)
* Add inital release-drafter config

* Replace release action

* Add collapse-after 3 to dependencies
2022-04-08 11:12:19 -07:00
Michael Shamoon
e4953a756a docs for PAPERLESS_URL + PAPERLESS_CSRF_TRUSTED_ORIGINS 2022-04-08 09:59:59 -07:00
Trenton Holmes
471ac63a3a Increases SavedViewFilterRule value to the maximum length 2022-04-08 08:55:58 -07:00
Trenton Holmes
f358eda5c5 Updates the mail db model to use the built-in enum capabilities and runs the migrations 2022-04-08 08:55:40 -07:00
Trenton Holmes
035130ecdc Updates the folder input text and adds docs note for how to figure out the folder separator value 2022-04-08 08:55:40 -07:00
Michael Shamoon
ca0e86757b add PAPERLESS_URL env variable for CSRF 2022-04-08 00:27:53 -07:00
Michael Shamoon
d4153607c9 reorganizing scss for clarity
[ci skip]
2022-04-07 23:23:28 -07:00
Michael Shamoon
99d0a0845d fix scss organization
[ci skip]
2022-04-07 23:15:27 -07:00
Florian Brandes
5fae5a9ee0 fix conditional in separate_pages
add additional test for separate_pages

Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-08 08:09:48 +02:00
Michael Shamoon
b5a75be1db fix error case
[ci skip]
2022-04-07 23:06:46 -07:00
Trenton Holmes
eb5e0e0b9b Merge pull request #501 from stumpylog/configure-worker-retry
Adds settings for worker retry time
2022-04-07 15:24:53 -07:00
Trenton Holmes
dc90f58391 Merge pull request #641 from paperless-ngx/dependabot/pip/dev/pre-commit-2.18.1
Bump pre-commit from 2.17.0 to 2.18.1
2022-04-07 14:29:08 -07:00
Michael Shamoon
ca43c71cf5 fix some broken css vars 2022-04-07 14:27:49 -07:00
dependabot[bot]
931a311c48 Bump pre-commit from 2.17.0 to 2.18.1
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 2.17.0 to 2.18.1.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v2.17.0...v2.18.1)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-07 20:54:33 +00:00
Michael Shamoon
9f9d7da1ce minor css hover + focus state fixes
[ci skip]
2022-04-07 13:51:36 -07:00
Trenton Holmes
d00e8d3b0f Merge pull request #649 from paperless-ngx/dependabot/pip/dev/zipp-3.8.0
Bump zipp from 3.7.0 to 3.8.0
2022-04-07 13:49:50 -07:00
Trenton Holmes
d4124bae0c Merge pull request #653 from paperless-ngx/dependabot/pip/dev/tqdm-4.64.0
Bump tqdm from 4.63.1 to 4.64.0
2022-04-07 13:37:46 -07:00
shamoon
58eb2d6f63 Merge pull request #283 from paperless-ngx/feature-global-dragdrop
Feature: global drag'n'drop
2022-04-07 12:14:59 -07:00
Quinn Casey
01987f1b51 Merge pull request #667 from paperless-ngx/codeowners
Feature: Add CODEOWNERS
2022-04-07 08:31:48 -07:00
shamoon
0a35358e8d Merge pull request #666 from paperless-ngx/feature-update-base-image 2022-04-07 08:30:59 -07:00
Quinn Casey
5bacb85c33 Fix typo 2022-04-07 08:13:25 -07:00
Quinn Casey
6933ac523f Remove explicit team tagging from PR template 2022-04-07 08:13:12 -07:00
Quinn Casey
ba9120b417 Add CODEOWNERS 2022-04-07 08:12:05 -07:00
dependabot[bot]
3e71f5810f Bump tqdm from 4.63.1 to 4.64.0
Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.63.1 to 4.64.0.
- [Release notes](https://github.com/tqdm/tqdm/releases)
- [Commits](https://github.com/tqdm/tqdm/compare/v4.63.1...v4.64.0)

---
updated-dependencies:
- dependency-name: tqdm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-07 15:02:57 +00:00
Trenton Holmes
296a0edae4 Updates the base image tag to use the :dev tag from the base image dev branch 2022-04-07 08:00:22 -07:00
Florian Brandes
cdf5602dfb update documentation
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
e214f719c9 add more tests
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
08fbcf5158 use imported List
fail test if non-existing dir exists

Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
10ca515ac5 addes tests:
- barcode-39
- barcode-128
- qr barcodes
- test for consumption

Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
e59a14852b code formatting
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
c696b4f2f2 set barcode reader default to disabled
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
553153ba92 put code under CONSUMER_ENABLE_BARCODES
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
9d2bcf807e add more type hinting
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
422ac9befe decode bytecode in string
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
793f641af6 make list checks consistent
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
0ea5f5d584 use append method for lists
Signed-off-by: Florian Brandes <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
florian on nixos (Florian Brandes)
c024b846c3 add config options and documentation
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
florian on nixos (Florian Brandes)
37b3fde4e1 pre-commit hooks
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
florian on nixos (Florian Brandes)
e89ef5de25 renaming of documents to align with front end
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
florian on nixos (Florian Brandes)
06cac44d02 update ci.yml to include new dependency
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
florian on nixos (Florian Brandes)
488fe28ad3 flake8
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
florian on nixos (Florian Brandes)
50f474ae92 run black
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
florian on nixos (Florian Brandes)
78ca2ffaba add trailing commas
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
florian on nixos (Florian Brandes)
911f5bc78e reorder imports
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:29 +02:00
Florian Brandes
b227427916 add split logic to consume_file
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-07 11:14:17 +02:00
florian on nixos (Florian Brandes)
b5f77fd6e7 skip test if dir exists
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-06 21:16:41 +02:00
florian on nixos (Florian Brandes)
4fe966f534 more work on barcode
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-06 21:16:41 +02:00
florian on nixos (Florian Brandes)
bcce0838dd working split pages
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-06 21:16:41 +02:00
florian on nixos (Florian Brandes)
76e43bcb89 add first tests for barcode reader
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-06 21:16:41 +02:00
florian on nixos (Florian Brandes)
c666be32f4 add barcode packages
Signed-off-by: florian on nixos (Florian Brandes) <florian.brandes@posteo.de>
2022-04-06 21:16:41 +02:00
Trenton Holmes
2d850795d8 Merge pull request #642 from paperless-ngx/dependabot/pip/dev/ocrmypdf-13.4.2
Bump ocrmypdf from 13.4.1 to 13.4.2
2022-04-06 12:13:03 -07:00
Michael Shamoon
784982718e retain document time / tz in detail form 2022-04-05 22:11:09 -07:00
shamoon
2f8d263c9c Merge pull request #630 from paperless-ngx/fix-disableable-download-buttons
Fix: download buttons should disable while waiting
2022-04-05 19:44:43 -07:00
dependabot[bot]
b214163af3 Bump ocrmypdf from 13.4.1 to 13.4.2
Bumps [ocrmypdf](https://github.com/jbarlow83/ocrmypdf) from 13.4.1 to 13.4.2.
- [Release notes](https://github.com/jbarlow83/ocrmypdf/releases)
- [Changelog](https://github.com/ocrmypdf/OCRmyPDF/blob/master/docs/release_notes.rst)
- [Commits](https://github.com/jbarlow83/ocrmypdf/compare/v13.4.1...v13.4.2)

---
updated-dependencies:
- dependency-name: ocrmypdf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-06 02:04:09 +00:00
dependabot[bot]
a4fe1000c2 Bump zipp from 3.7.0 to 3.8.0
Bumps [zipp](https://github.com/jaraco/zipp) from 3.7.0 to 3.8.0.
- [Release notes](https://github.com/jaraco/zipp/releases)
- [Changelog](https://github.com/jaraco/zipp/blob/main/CHANGES.rst)
- [Commits](https://github.com/jaraco/zipp/compare/v3.7.0...v3.8.0)

---
updated-dependencies:
- dependency-name: zipp
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-06 02:03:28 +00:00
Trenton Holmes
9a275fa4ed Merge pull request #619 from paperless-ngx/dependabot/pip/dev/pillow-9.1.0
Bump pillow from 9.0.1 to 9.1.0
2022-04-05 18:57:52 -07:00
Michael Shamoon
f2c83f51de Cleanup file upload susbcriptions 2022-04-05 13:33:24 -07:00
Michael Shamoon
fb76b72787 Delete upload-documents.service.spec.ts 2022-04-05 13:33:24 -07:00
Michael Shamoon
bec6c4511c Cleaner activation with Angular directives 2022-04-05 13:33:24 -07:00
Michael Shamoon
77b9988d05 Refactor uploads to service, working global dd 2022-04-05 13:33:24 -07:00
Michael Shamoon
3e49f93816 100vh body height, fix filter editor horizontal overflow 2022-04-05 13:33:24 -07:00
Michael Shamoon
32f6932faf fix overlay background color in dark mode 2022-04-05 13:16:13 -07:00
Michael Shamoon
db76e1d65f code formatting 2022-04-05 13:16:13 -07:00
Michael Shamoon
0136ba504b Delete upload-documents.service.spec.ts 2022-04-05 13:16:13 -07:00
Michael Shamoon
459e026f16 Add translation directive for upload message 2022-04-05 13:16:13 -07:00
Michael Shamoon
b03a723c3e Fix dark mode alert close button 2022-04-05 13:16:13 -07:00
Michael Shamoon
7562636151 Cleaner activation with Angular directives 2022-04-05 13:16:13 -07:00
Michael Shamoon
3acc65ca0d Fix broken toast animations & DOM removal 2022-04-05 13:16:13 -07:00
Michael Shamoon
fde0f4ca0a Add initiating toast 2022-04-05 13:16:13 -07:00
Michael Shamoon
73cab2af2d Refactor uploads to service, working global dd 2022-04-05 13:16:12 -07:00
dependabot[bot]
94d2198b30 Bump pillow from 9.0.1 to 9.1.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 9.0.1 to 9.1.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/9.0.1...9.1.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-05 18:20:14 +00:00
Trenton Holmes
88a67c8703 Merge pull request #618 from paperless-ngx/dependabot/pip/dev/pikepdf-5.1.1
Bump pikepdf from 5.1.0 to 5.1.1
2022-04-05 11:14:21 -07:00
Felix E
ccf9b1291e Merge pull request #591 from paperless-ngx/feature-version-checker
Feature: Update checker
2022-04-05 20:11:21 +02:00
Quinn Casey
47dae716ae Merge pull request #599 from stumpylog/minor-bug-fixes
Fixes downloaded filename, add more consumer ignore settings
2022-04-05 09:56:54 -07:00
Quinn Casey
ea26e1c72f Merge pull request #576 from fantasticle/dev
Update regex date match patterns
2022-04-05 09:52:27 -07:00
Quinn Casey
e6d79f0673 Update docs, examples with new ignore settings 2022-04-05 09:21:18 -07:00
Quinn Casey
bf7002d0ae Merge pull request #616 from paperless-ngx/fix-save-vew-modal-errors
fix some api error displaying
2022-04-05 09:10:15 -07:00
Quinn Casey
cbae145da5 Merge pull request #564 from paperless-ngx/feature-password-protected-pdfs
Feature: Show prompt on password-protected pdfs
2022-04-05 08:59:29 -07:00
Trenton Holmes
88bbfe5961 Merge pull request #594 from danielBreitlauch/dev
FIX BUG: case-sensitive matching was not possible
2022-04-04 09:24:59 -07:00
Trenton Holmes
d60569b6d6 Quiets the pytest output 2022-04-04 17:59:37 +02:00
Michael Shamoon
91165e80ba Remove unused default 2022-04-04 07:05:00 -07:00
dependabot[bot]
a15f9552eb Bump pikepdf from 5.1.0 to 5.1.1
Bumps [pikepdf](https://github.com/pikepdf/pikepdf) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/pikepdf/pikepdf/releases)
- [Changelog](https://github.com/pikepdf/pikepdf/blob/master/docs/release_notes.rst)
- [Commits](https://github.com/pikepdf/pikepdf/compare/v5.1.0...v5.1.1)

---
updated-dependencies:
- dependency-name: pikepdf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-03 22:20:33 +00:00
Trenton Holmes
501d225f93 Merge pull request #557 from paperless-ngx/dependabot/pip/dev/sphinx-4.5.0
Bump sphinx from 4.4.0 to 4.5.0
2022-04-03 15:08:38 -07:00
dependabot[bot]
4942f244da Bump sphinx from 4.4.0 to 4.5.0
Bumps [sphinx](https://github.com/sphinx-doc/sphinx) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/sphinx-doc/sphinx/releases)
- [Changelog](https://github.com/sphinx-doc/sphinx/blob/4.x/CHANGES)
- [Commits](https://github.com/sphinx-doc/sphinx/compare/v4.4.0...v4.5.0)

---
updated-dependencies:
- dependency-name: sphinx
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-03 20:35:06 +00:00
Trenton Holmes
1be5c9af56 Merge pull request #554 from paperless-ngx/dependabot/pip/dev/black-22.3.0
Bump black from 22.1.0 to 22.3.0
2022-04-03 13:14:03 -07:00
fantasticle
818d383f2e Merge branch 'paperless-ngx:dev' into dev 2022-04-03 16:42:51 +02:00
shamoon
5fffa32630 disable download buttons while awaiting zip 2022-04-02 22:23:42 -07:00
Michael Shamoon
19d5feb483 Fix save view error for filter_rules
see #601
2022-04-02 19:49:58 -07:00
Michael Shamoon
199fc6be94 Properly display api errors on list filter & translate error message 2022-04-02 19:45:54 -07:00
Trenton Holmes
865729c033 Merge pull request #611 from stumpylog/bug-fix-install-script
Support docker-compose v2 in install
2022-04-02 15:36:52 -07:00
Trenton Holmes
6aa9071e24 Converts the docker compose check to use if statement 2022-04-02 14:34:19 -07:00
Trenton Holmes
6db3fc2eea Fixes an issue where the install script wouldn't recognize docker compose v2 command 2022-04-02 14:34:19 -07:00
shamoon
65ffaaa67c Merge pull request #623 from paperless-ngx/feature-fix-docker-tags
Fixes the Docker image tags
2022-04-02 13:46:23 -07:00
Michael Shamoon
440467e304 Allow notify user if update checking not explicitly set 2022-04-02 10:10:49 -07:00
Fantasticle
74422dd000 update name from master to main 2022-04-02 18:19:11 +02:00
Trenton Holmes
78d663bbb4 Fixes the Docker image tags 2022-04-02 08:48:58 -07:00
Fantasticle
db0a58ea04 fix link for post-consumption-example, 404d to jonas 2022-04-02 15:52:32 +02:00
fantasticle
eba1e69e64 Merge branch 'paperless-ngx:dev' into dev 2022-04-02 15:49:35 +02:00
shamoon
a4e7877033 Update docs/configuration.rst for consistency
Co-authored-by: Quinn Casey <quinn@quinncasey.com>
2022-04-02 01:37:37 -07:00
Michael Shamoon
c62260ab02 Use GH api URL
Update configuration.rst
2022-04-02 01:36:37 -07:00
Michael Shamoon
b58550bb79 refactor to update_available 2022-04-02 01:36:19 -07:00
Michael Shamoon
d02c7df75c json parse version string 2022-04-02 01:36:19 -07:00
Michael Shamoon
6dbebf4806 Fix version string regex 2022-04-02 01:33:51 -07:00
Michael Shamoon
1019660f6a Add config variable for update check 2022-04-02 01:33:51 -07:00
Michael Shamoon
bfd11060ec Add version check to app frame 2022-04-02 01:33:51 -07:00
Michael Shamoon
7b6dccf5ef RemoteVersionService 2022-04-02 01:33:51 -07:00
Michael Shamoon
d76eccad1c remote_version API endpoint 2022-04-02 01:33:51 -07:00
Michael Shamoon
6f0ac7ae45 Update bootstrap-icons.svg 2022-04-02 01:33:50 -07:00
dependabot[bot]
8f17ed1eb9 Bump black from 22.1.0 to 22.3.0
Bumps [black](https://github.com/psf/black) from 22.1.0 to 22.3.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/22.1.0...22.3.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-02 08:29:39 +00:00
Daniel Breitlauch
2b5562e376 adding case-sensitive tests 2022-04-01 21:15:30 +02:00
Trenton Holmes
35b3216fee Fixes a couple of minor bugs in the downloading of documents and additional consumer ignore items 2022-04-01 11:12:18 -07:00
Daniel Breitlauch
6f8020e30d removing transformation to lowercase since it is not needed at all. 2022-04-01 18:26:01 +02:00
Daniel Breitlauch
0bee3901b6 Merge branch 'paperless-ngx:dev' into dev 2022-04-01 12:04:51 +02:00
Daniel Breitlauch
7106c68032 FIX BUG: case-sensitive matching was not possible 2022-04-01 12:03:05 +02:00
fantasticle
be707536bd Merge branch 'paperless-ngx:dev' into dev 2022-04-01 11:35:00 +02:00
Fantasticle
4754ac2bd1 remove unnecessary import 2022-03-31 21:25:58 +02:00
Fantasticle
3cca77e748 add more tests for regex date parser, remove duplicate name 2022-03-31 21:24:57 +02:00
Trenton Holmes
4ec1aaabe6 Adds settings for worker retry time, with what should be sensible default 2022-03-31 10:24:46 -07:00
Fantasticle
0baacbef98 update new regex pattern for second boundary 2022-03-31 09:37:15 +02:00
fantasticle
d8261b3359 add test for new regex 2022-03-30 23:18:03 +02:00
fantasticle
1ecb26a3fb Update regex date match patterns 2022-03-30 12:19:30 +02:00
Michael Shamoon
85bf92ad2f Show prompt on password-protected pdfs 2022-03-29 15:38:09 -07:00
83 changed files with 2452 additions and 845 deletions

View File

@@ -1,23 +1,15 @@
<!--
<!--
Note: All PRs with code changes should be targeted to the `dev` branch, pure documentation changes can target `main`
-->
## Proposed change
<!--
Please include a summary of the change and which issue is fixed (if any) and any relevant motivation / context. List any dependencies that are required for this change. If appropriate, please include an explanation of how your poposed change can be tested. Screenshots and / or videos can also be helpful if appropriate.
Please include a summary of the change and which issue is fixed (if any) and any relevant motivation / context. List any dependencies that are required for this change. If appropriate, please include an explanation of how your proposed change can be tested. Screenshots and / or videos can also be helpful if appropriate.
-->
Fixes # (issue)
<!--
Please also tag the relevant team to help with review. You can tag any of the following:
@paperless-ngx/backend (Python / django, database, etc.)
@paperless-ngx/frontend (JavaScript/Typescript, HTML, CSS, etc.)
@paperless-ngx/ci-cd (GitHub Actions, deployment)
@paperless-ngx/test (General testing for larger PRs)
-->
## Type of change
<!--

34
.github/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,34 @@
categories:
- title: 'Features'
labels:
- 'enhancement'
- title: 'Bug Fixes'
labels:
- 'bug'
- title: 'Documentation'
label: 'documentation'
- title: 'Maintenance'
labels:
- 'chore'
- 'deployment'
- 'translation'
- title: 'Dependencies'
collapse-after: 3
label: 'dependencies'
include-labels:
- 'enhancement'
- 'bug'
- 'chore'
- 'deployment'
- 'translation'
- 'dependencies'
replacers: # Changes "Feature: Update checker" to "Update checker"
- search: '/Feature:|Feat:|\[feature\]/gi'
replace: ''
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
change-title-escapes: '\<*_&#@'
tag-prefix: "ngx-"
template: |
## Changelog
$CHANGES

View File

@@ -132,7 +132,7 @@ jobs:
name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng libzbar0 poppler-utils
-
name: Install Python dependencies
run: |
@@ -196,9 +196,8 @@ jobs:
images: ghcr.io/${{ github.repository }}
tags: |
type=match,pattern=ngx-(\d.\d.\d),group=1
type=semver,pattern=ngx-{{version}}
type=semver,pattern=ngx-{{major}}.{{minor}}
type=ref
type=ref,event=branch
type=ref,event=tag
-
name: Checkout
uses: actions/checkout@v3
@@ -328,24 +327,22 @@ jobs:
if [[ $GITHUB_REF == refs/tags/ngx-* ]]; then
echo ::set-output name=version::${GITHUB_REF#refs/tags/ngx-}
echo ::set-output name=prerelease::false
echo ::set-output name=body::"For a complete list of changes, see the changelog at https://paperless-ngx.readthedocs.io/en/latest/changelog.html"
elif [[ $GITHUB_REF == refs/tags/beta-* ]]; then
echo ::set-output name=version::${GITHUB_REF#refs/tags/beta-}
echo ::set-output name=prerelease::true
echo ::set-output name=body::"For a complete list of changes, see the changelog at https://github.com/paperless-ngx/paperless-ngx/blob/beta/docs/changelog.rst"
fi
-
name: Create release
id: create_release
uses: actions/create-release@v1
name: Create Release and Changelog
id: create-release
uses: release-drafter/release-drafter@v5
with:
name: Paperless-ngx ${{ steps.get_version.outputs.version }}
tag: ngx-${{ steps.get_version.outputs.version }}
version: ${{ steps.get_version.outputs.version }}
prerelease: ${{ steps.get_version.outputs.prerelease }}
publish: true # ensures release is not marked as draft
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ngx-${{ steps.get_version.outputs.version }}
release_name: Paperless-ngx ${{ steps.get_version.outputs.version }}
draft: false
prerelease: ${{ steps.get_version.outputs.prerelease }}
body: ${{ steps.get_version.outputs.body }}
-
name: Upload release archive
id: upload-release-asset
@@ -353,7 +350,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
upload_url: ${{ steps.create-release.outputs.upload_url }}
asset_path: ./paperless-ngx.tar.xz
asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz
asset_content_type: application/x-xz

View File

@@ -5,7 +5,7 @@
repos:
# General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
rev: v4.2.0
hooks:
- id: check-docstring-first
- id: check-json
@@ -27,7 +27,7 @@ repos:
- id: check-case-conflict
- id: detect-private-key
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v2.6.1"
rev: "v2.6.2"
hooks:
- id: prettier
types_or:
@@ -47,7 +47,7 @@ repos:
- id: yesqa
exclude: "(migrations)"
- repo: https://github.com/asottile/add-trailing-comma
rev: "v2.2.1"
rev: "v2.2.2"
hooks:
- id: add-trailing-comma
exclude: "(migrations)"

10
CODEOWNERS Normal file
View File

@@ -0,0 +1,10 @@
/.github/workflows/ @paperless-ngx/ci-cd
/docker/ @paperless-ngx/ci-cd
/scripts/ @paperless-ngx/ci-cd
/src-ui/ @paperless-ngx/frontend
/src/ @paperless-ngx/backend
Pipfile* @paperless-ngx/backend
*.py @paperless-ngx/backend
requirements.txt @paperless-ngx/backend

View File

@@ -6,7 +6,7 @@ WORKDIR /src/src-ui
RUN npm update npm -g && npm ci --no-optional
RUN ./node_modules/.bin/ng build --configuration production
FROM ghcr.io/paperless-ngx/builder/ngx-base:1.0 as main-app
FROM ghcr.io/paperless-ngx/builder/ngx-base:1.7.0 as main-app
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
LABEL org.opencontainers.image.documentation="https://paperless-ngx.readthedocs.io/en/latest/"

View File

@@ -22,7 +22,7 @@ gunicorn = "*"
imap-tools = "*"
langdetect = "*"
pathvalidate = "*"
pillow = "~=9.0"
pillow = "~=9.1"
# Any version update to pikepdf requires a base image update
pikepdf = "~=5.1"
python-gnupg = "*"
@@ -51,6 +51,8 @@ concurrent-log-handler = "*"
"backports.zoneinfo" = {version = "*", markers = "python_version < '3.9'"}
"importlib-resources" = {version = "*", markers = "python_version < '3.9'"}
zipp = {version = "*", markers = "python_version < '3.9'"}
pyzbar = "*"
pdf2image = "*"
[dev-packages]
coveralls = "*"
@@ -62,7 +64,7 @@ pytest-django = "*"
pytest-env = "*"
pytest-sugar = "*"
pytest-xdist = "*"
sphinx = "~=4.4.0"
sphinx = "~=4.5.0"
sphinx_rtd_theme = "*"
tox = "*"
black = "*"

348
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "f33c0aeb0cb880b444efd367ad5b47358dde2a9b18b55dcd6943dd81f6c7e2a6"
"sha256": "9573af313c811561d467d814c52c6bd1439bc48e3b31d7f56afed5f0ebe4b648"
},
"pipfile-spec": 6,
"requires": {},
@@ -68,10 +68,10 @@
},
"autobahn": {
"hashes": [
"sha256:60e1f4c602aacd052ffe3d46ae40b6b75f8286b3c46922c213b523162e58c17e"
"sha256:58a887c7a196bb08d8b6624cb3695f493a9e5c9f00fd350d8d6f829b47ff9036"
],
"markers": "python_version >= '3.7'",
"version": "==22.2.2"
"version": "==22.3.2"
},
"automat": {
"hashes": [
@@ -99,7 +99,6 @@
"sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac",
"sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"
],
"index": "pypi",
"markers": "python_version < '3.9'",
"version": "==0.2.1"
},
@@ -207,11 +206,11 @@
},
"click": {
"hashes": [
"sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
"sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
],
"markers": "python_version >= '3.6'",
"version": "==8.0.4"
"markers": "python_version >= '3.7'",
"version": "==8.1.2"
},
"coloredlogs": {
"hashes": [
@@ -280,11 +279,11 @@
},
"django": {
"hashes": [
"sha256:1239218849e922033a35d2a2f777cb8bee18bd725416744074f455f34ff50d0c",
"sha256:77ff2e7050e3324c9b67e29b6707754566f58514112a9ac73310f60cd5261930"
"sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687",
"sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"
],
"index": "pypi",
"version": "==4.0.3"
"version": "==4.0.4"
},
"django-cors-headers": {
"hashes": [
@@ -488,16 +487,15 @@
},
"img2pdf": {
"hashes": [
"sha256:8e51c5043efa95d751481b516071a006f87c2a4059961a9ac43ec238915de09f"
"sha256:8ec898a9646523fd3862b154f3f47cd52609c24cc3e2dc1fb5f0168f0cbe793c"
],
"version": "==0.4.3"
"version": "==0.4.4"
},
"importlib-resources": {
"hashes": [
"sha256:1b93238cbf23b4cde34240dd8321d99e9bf2eb4bc91c0c99b2886283e7baad85",
"sha256:a9dd72f6cc106aeb50f6e66b86b69b454766dd6e39b69ac68450253058706bcc"
],
"index": "pypi",
"markers": "python_version < '3.9'",
"version": "==5.6.0"
},
@@ -673,11 +671,11 @@
},
"ocrmypdf": {
"hashes": [
"sha256:201ed2f589f851be73908fce35fbb6fb05e4739289d3cd8765f9519f49ea1cd9",
"sha256:f42e60bc2b6534634dd08928584275b1c556dc875c849650afcc38f7da9e2856"
"sha256:7f0a6165b80ba1b37ce5943cf5b4faf93bf98c04c8f5157ef83c5f292491485f",
"sha256:d52410bc38cf5b66da27668e38c66ac41fd3136457c1ec388b311f0a78ee213c"
],
"index": "pypi",
"version": "==13.4.1"
"version": "==13.4.2"
},
"packaging": {
"hashes": [
@@ -695,87 +693,98 @@
"index": "pypi",
"version": "==2.5.0"
},
"pdfminer.six": {
"pdf2image": {
"hashes": [
"sha256:0351f17d362ee2d48b158be52bcde6576d96460efd038a3e89a043fba6d634d7",
"sha256:d3efb75c0249b51c1bf795e3a8bddf1726b276c77bf75fb136adea471ee2825b"
"sha256:84f79f2b8fad943e36323ea4e937fcb05f26ded0caa0a01181df66049e42fb65",
"sha256:d58ed94d978a70c73c2bb7fdf8acbaf2a7089c29ff8141be5f45433c0c4293bb"
],
"index": "pypi",
"version": "==20211012"
"version": "==1.16.0"
},
"pdfminer.six": {
"hashes": [
"sha256:af0630f98a292bad4170f54e80f82ca81b916dd0b2c996437ec45c02f11d8762",
"sha256:eff2ce0abeaa4df94dc3461f70eab104487c7b4a2b3c7e9fd0aeec6c5f44d6a6"
],
"index": "pypi",
"version": "==20220319"
},
"pikepdf": {
"hashes": [
"sha256:1567b74d15c16d1bef56a6d5f56fb6a35f4cc022ae252d35f33f56bb16b87966",
"sha256:179f24b4a4d0e89c7f592d85ba4daf7d34c709eb0a691425b881413fdce70734",
"sha256:22201b06db627a86cc91c1b76491dd9c57fce2df4e3bc8ba700ff66f7f7da04a",
"sha256:3e1a0b9ecf5d4aa106c3c0db558952f9a15f343f812c3bba6d6e1a56e25224ed",
"sha256:45ce2479e5ba74896ef389abf92831d7fbb34f25e6557adb9115710223a0bb13",
"sha256:4fa5c8494b011b19bd198dd9c3cd94676b905360b2231ad35171ae586644b823",
"sha256:559b3d502cc1a6813cbcb0766b0797fec034303f8f9b0734cf938fb1734e2b74",
"sha256:61731fceaab99850bc7045232301c2332bba727f78b53f7038fcbdcaf3d64309",
"sha256:6460d489341e7f8dc3f6b0dbf1f5a75a918ebd1e0ecb4c2b00877264a68ee1f4",
"sha256:716ca6fc8947502cb73a517c884066afa132ca998e085a309b8fb8c5796d6277",
"sha256:726450eb9baaad5697687c2621d481c80f868b68c06d2cff4be3f6a7ce28cfee",
"sha256:7e9c247ca384ad1606281eda4d841bc8cbff90875979ac3b520bcc5404bf9b26",
"sha256:8333d813b452daa4a066e135fd7ab6f7c07ccc02cb8381455d61d74f0a0ad0fe",
"sha256:83d0af374b103934de033f096205143fa9d6f78e789ba78c8aa6dfb0e5b73bc3",
"sha256:878c1c95298486d8cea7e8236c70613e7eae1426cbf362c3883ecd06e8f9c2d9",
"sha256:9abef24d929c4a08292dc4be4d6c4e5bf93832e747eef5f39e854348a332f46a",
"sha256:a474241dbeda246356b6448f607f4fb9fee5b9f5cb19511a768b88b471325865",
"sha256:a553fd06e5f6e78c5e840d066e7c8b1a988e16489fe0bd4a143ca601809ea4dd",
"sha256:a6154c6bb7606ef534444f54271a410a6337cee54dcdef20f0fa0686f622cf50",
"sha256:ac0082379cf6aa6c0c682bee4a3d2adbe6d60b9125e3632876d7c5e9665c07ab",
"sha256:c22e3fbfb76ad7838dde82c8d9fafd4c09fd419cee531b31d1b48a07344ac2b4",
"sha256:d42c52ff2e8fb00fad14182f67a8f076f38c75a874123b6776aa5c6af09e1126",
"sha256:d6ef14b722f80351e15c9163e0aa1f1df84f065c080765f8232ec51af6bd3368",
"sha256:d71a38445c80972572248815ebe61f9c814c53925a6b83b6596f3482a98a5f25",
"sha256:e81ebbdd53257f411827bbb301900cdbdca34ca60b4f7248f80c6e6980062498",
"sha256:f52f4ed655c25c408e8454cbcf0e7223b62f635d1e8ab738fd0c2d46531d28ed",
"sha256:fb407ae5820ee0bf71022d5a8f539d709dac590443270a13baf6a8872c76d46f"
"sha256:01be838a44430c4be84b748a33950fed09892472934a8041596c11189f365f7f",
"sha256:0cc95ef470169dfa5acc9196299bdba236716234a0d8b2746e2a563bc6f1f456",
"sha256:13e72d0aeeb3fc452569a3f7994acdd007de9aad804ced734d57cec269261b8b",
"sha256:2873503522ef26a09a6020c29c2efd221fa2ddc31e83bd902be27d317144cf63",
"sha256:2d5d6d3248b33ca5961d84bc3121a299cd27237fad56868d815e381c9a98d3d1",
"sha256:2f62e6c7bcf5d631e6ea74cf861f3e816f587c6ccb4ecbf6ac862e088ba2e4ac",
"sha256:51694d3d2f90510da6a8d7a4d07313ca868b373fffec6de270d9bbff1ce37180",
"sha256:5c23cbd7ae71f08fb5b5d9660eb0bc61abf345ada01bea6e1b6884c4261e17d6",
"sha256:6371bf02a436be2b7c63322b83a8e47523f2cd16438b2e93d546c7caf9ae308d",
"sha256:657293b74af8c7cf03f9905218a7935b26a4f3006803016b40b3db78e04cb35c",
"sha256:680d47377bb9fd6a36b6a81464ee269b4b29cbf29a84ae4f2ab8f6ea3665bf69",
"sha256:710535c679ab0d7b8249f72247832773e7a9a121dfbe9cad7f6465bd9bb45fae",
"sha256:7b4d7c09036d863915cb01007ca183d6fe64e2d57c0472453097bc9e029a58fb",
"sha256:978b6388ae99a024bdcae5a322c68e90c187cb568d09d43e6586b3479267121d",
"sha256:9917a03d500aab72715a9236136af7a5c8c7b26c034bf71ebdf028e177f0d25f",
"sha256:996faa6b119488f96d7271672a22af86e56e5544ec6b8eae6cd7d4432c70ae2d",
"sha256:9bac9e9d6b28dc0cc5a554051f183fbd070d0f9fe63c4e9aca939b8c44a5bb4d",
"sha256:aac14061de06843759ea6f5777fd8d7b71af808ed9264f57483a3311a09788ab",
"sha256:ad5361c3669fc0c8dbaf8fa0a590bddf59fad256bb2c527d5ce5cf991743a240",
"sha256:bc40b30c37f8f7c5bef873eca1f04e91ce34b6b74507d8d0019238a17d281fdc",
"sha256:bd9faae19787a5d05b9fcbe84d7cfe4d44e318068e06eca18906b9dba45425b6",
"sha256:c64e7905ec438b7a6c12626f2859df87f471892fab75b65b1441d9e1b38b4dde",
"sha256:d4db409b21a8ec0d3a79d2bbd894b997b13223c9ccf341cdc31b64360f1ee4c7",
"sha256:e0b635d6d9faefb4d0d32722279b8eb4e4d5d7b596c426f3433343de65e0c772",
"sha256:e62e9e8afe77fe2f06715faf10f38a4810d282d66f1e9e05208bb8d9723e6acf",
"sha256:f85d309bcfeeb3e2d344346a5050bfc41e332f19d390f79c20e4fc7de4b10a17",
"sha256:fe3fc2efe498aba6204b85c17c6a5d54ab7303354ecc5c3da624a6b6af0b3406"
],
"index": "pypi",
"version": "==5.1.0"
"version": "==5.1.1"
},
"pillow": {
"hashes": [
"sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97",
"sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049",
"sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c",
"sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae",
"sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28",
"sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030",
"sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56",
"sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976",
"sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e",
"sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e",
"sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f",
"sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b",
"sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a",
"sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e",
"sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa",
"sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7",
"sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00",
"sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838",
"sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360",
"sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b",
"sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a",
"sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd",
"sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4",
"sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70",
"sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204",
"sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc",
"sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b",
"sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669",
"sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7",
"sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e",
"sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c",
"sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092",
"sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c",
"sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5",
"sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"
"sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e",
"sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595",
"sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512",
"sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c",
"sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477",
"sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a",
"sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4",
"sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e",
"sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5",
"sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378",
"sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a",
"sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652",
"sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7",
"sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a",
"sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a",
"sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6",
"sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165",
"sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160",
"sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331",
"sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b",
"sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458",
"sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033",
"sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8",
"sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481",
"sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58",
"sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7",
"sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3",
"sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea",
"sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34",
"sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3",
"sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8",
"sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581",
"sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244",
"sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef",
"sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0",
"sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2",
"sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97",
"sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717"
],
"index": "pypi",
"version": "==9.0.1"
"version": "==9.1.0"
},
"pluggy": {
"hashes": [
@@ -862,11 +871,11 @@
},
"pyparsing": {
"hashes": [
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.8"
},
"python-dateutil": {
"hashes": [
@@ -959,6 +968,15 @@
],
"version": "==6.0"
},
"pyzbar": {
"hashes": [
"sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c",
"sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d",
"sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518"
],
"index": "pypi",
"version": "==0.1.9"
},
"redis": {
"hashes": [
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
@@ -1173,11 +1191,11 @@
},
"setuptools": {
"hashes": [
"sha256:89eef7b71423ab7fccc7dfafdc145410ef170c4a89567427f932448135e08cdf",
"sha256:92b15f45ab164eb0c410d2bf661a6e9d15e3b78c0dffb0325f2bf0f313071cae"
"sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8",
"sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592"
],
"markers": "python_version >= '3.7'",
"version": "==61.1.1"
"version": "==62.1.0"
},
"six": {
"hashes": [
@@ -1220,22 +1238,22 @@
},
"tqdm": {
"hashes": [
"sha256:4230a49119a416c88cc47d0d2d32d5d90f1a282d5e497d49801950704e49863d",
"sha256:6461b009d6792008d0000e1b0c7ca50195ec78c0e808a3a6b668a56a3236c3a5"
"sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d",
"sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"
],
"index": "pypi",
"version": "==4.63.1"
"version": "==4.64.0"
},
"twisted": {
"extras": [
"tls"
],
"hashes": [
"sha256:57f32b1f6838facb8c004c89467840367ad38e9e535f8252091345dba500b4f2",
"sha256:5c63c149eb6b8fe1e32a0215b1cef96fabdba04f705d8efb9174b1ccf5b49d49"
"sha256:a047990f57dfae1e0bd2b7df2526d4f16dcdc843774dc108b78c52f2a5f13680",
"sha256:f9f7a91f94932477a9fc3b169d57f54f96c6e74a23d78d9ce54039a7f48928a2"
],
"markers": "python_full_version >= '3.6.7'",
"version": "==22.2.0"
"version": "==22.4.0"
},
"txaio": {
"hashes": [
@@ -1263,11 +1281,11 @@
},
"tzlocal": {
"hashes": [
"sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09",
"sha256:28ba8d9fcb6c9a782d6e0078b4f6627af1ea26aeaa32b4eab5324abc7df4149f"
"sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745",
"sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"
],
"markers": "python_version >= '3.6'",
"version": "==4.1"
"version": "==4.2"
},
"urllib3": {
"hashes": [
@@ -1341,10 +1359,10 @@
},
"watchgod": {
"hashes": [
"sha256:4ba20c2fa3e63df706ab50e694b9453b05395fadb7cbbfd984d71fb1547d485d",
"sha256:c12d15f3df7d11e740704e45398277f75f1d78f46ad59ca9d7505bfd8b8d3086"
"sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce",
"sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"
],
"version": "==0.8.1"
"version": "==0.8.2"
},
"wcwidth": {
"hashes": [
@@ -1425,12 +1443,11 @@
},
"zipp": {
"hashes": [
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
"sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad",
"sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"
],
"index": "pypi",
"markers": "python_version < '3.9'",
"version": "==3.7.0"
"version": "==3.8.0"
},
"zope.interface": {
"hashes": [
@@ -1516,32 +1533,32 @@
},
"black": {
"hashes": [
"sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2",
"sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71",
"sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6",
"sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5",
"sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912",
"sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866",
"sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d",
"sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0",
"sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321",
"sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8",
"sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd",
"sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3",
"sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba",
"sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0",
"sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5",
"sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a",
"sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28",
"sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c",
"sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1",
"sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab",
"sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f",
"sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61",
"sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"
"sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b",
"sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176",
"sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09",
"sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a",
"sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015",
"sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79",
"sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb",
"sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20",
"sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464",
"sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968",
"sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82",
"sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21",
"sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0",
"sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265",
"sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b",
"sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a",
"sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72",
"sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce",
"sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0",
"sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a",
"sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163",
"sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad",
"sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"
],
"index": "pypi",
"version": "==22.1.0"
"version": "==22.3.0"
},
"certifi": {
"hashes": [
@@ -1568,15 +1585,15 @@
},
"click": {
"hashes": [
"sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
"sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
],
"markers": "python_version >= '3.6'",
"version": "==8.0.4"
"markers": "python_version >= '3.7'",
"version": "==8.1.2"
},
"coverage": {
"extras": [
"toml"
],
"hashes": [
"sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
@@ -1671,11 +1688,11 @@
},
"faker": {
"hashes": [
"sha256:5536ceb63380f0d598c026b7c330c17d719a19d1a495e9397ee8f5259420a696",
"sha256:85ed0cb379e3b7414bdb6b484994beaf9bddc74472c8c35f743c16cf5fc0c314"
"sha256:188961065fb5c78ea639f42176f55100f72c90c3a3179ac6c955c4bd712b0511",
"sha256:7758ece2593ce603db117db3d27393c31f4af03f783e176f3f0e14839a4f3426"
],
"markers": "python_version >= '3.6'",
"version": "==13.3.3"
"version": "==13.3.4"
},
"filelock": {
"hashes": [
@@ -1709,14 +1726,6 @@
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.3.0"
},
"importlib-metadata": {
"hashes": [
"sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6",
"sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"
],
"markers": "python_version < '3.10'",
"version": "==4.11.3"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
@@ -1825,11 +1834,11 @@
},
"pre-commit": {
"hashes": [
"sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616",
"sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"
"sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2",
"sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10"
],
"index": "pypi",
"version": "==2.17.0"
"version": "==2.18.1"
},
"py": {
"hashes": [
@@ -1857,11 +1866,11 @@
},
"pyparsing": {
"hashes": [
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.8"
},
"pytest": {
"hashes": [
@@ -1995,11 +2004,11 @@
},
"sphinx": {
"hashes": [
"sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe",
"sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc"
"sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6",
"sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"
],
"index": "pypi",
"version": "==4.4.0"
"version": "==4.5.0"
},
"sphinx-rtd-theme": {
"hashes": [
@@ -2087,14 +2096,6 @@
"index": "pypi",
"version": "==3.24.5"
},
"typing-extensions": {
"hashes": [
"sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42",
"sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"
],
"markers": "python_version >= '3.6'",
"version": "==4.1.1"
},
"urllib3": {
"hashes": [
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
@@ -2105,20 +2106,11 @@
},
"virtualenv": {
"hashes": [
"sha256:1e8588f35e8b42c6ec6841a13c5e88239de1e6e4e4cedfd3916b306dc826ec66",
"sha256:8e5b402037287126e81ccde9432b95a8be5b19d36584f64957060a3488c11ca8"
"sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a",
"sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==20.14.0"
},
"zipp": {
"hashes": [
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
],
"index": "pypi",
"markers": "python_version < '3.9'",
"version": "==3.7.0"
"version": "==20.14.1"
}
}
}

View File

@@ -22,6 +22,10 @@
# Docker setup does not use the configuration file.
# A few commonly adjusted settings are provided below.
# This is required if you will be exposing Paperless-ngx on a public domain
# (if doing so please consider security measures such as reverse proxy)
#PAPERLESS_URL=https://paperless.example.com
# Adjust this key if you plan to make paperless available publicly. It should
# be a very long sequence of random characters. You don't need to remember it.
#PAPERLESS_SECRET_KEY=change-me

View File

@@ -200,7 +200,7 @@ Troubleshooting:
- Check your script's permission e.g. in case of permission error ``sudo chmod 755 post-consumption-example.sh``
- Pipe your scripts's output to a log file e.g. ``echo "${DOCUMENT_ID}" | tee --append /usr/src/paperless/scripts/post-consumption-example.log``
.. _post-consumption-example.sh: https://github.com/jonaswinkler/paperless-ngx/blob/master/scripts/post-consumption-example.sh
.. _post-consumption-example.sh: https://github.com/paperless-ngx/paperless-ngx/blob/main/scripts/post-consumption-example.sh
.. _advanced-file_name_handling:

View File

@@ -5,6 +5,11 @@
Changelog
*********
paperless-ngx 1.7.0
###################
Changelog pending
paperless-ngx 1.6.0
###################

View File

@@ -142,7 +142,24 @@ PAPERLESS_SECRET_KEY=<key>
Default is listed in the file ``src/paperless/settings.py``.
PAPERLESS_ALLOWED_HOSTS<comma-separated-list>
PAPERLESS_URL=<url>
This setting can be used to set the three options below (ALLOWED_HOSTS,
CORS_ALLOWED_HOSTS and CSRF_TRUSTED_ORIGINS). If the other options are
set the values will be combined with this one. Do not include a trailing
slash. E.g. https://paperless.domain.com
Defaults to empty string, leaving the other settings unaffected.
PAPERLESS_CSRF_TRUSTED_ORIGINS=<comma-separated-list>
A list of trusted origins for unsafe requests (e.g. POST). As of Django 4.0
this is required to access the Django admin via the web.
See https://docs.djangoproject.com/en/4.0/ref/settings/#csrf-trusted-origins
Can also be set using PAPERLESS_URL (see above).
Defaults to empty string, which does not add any origins to the trusted list.
PAPERLESS_ALLOWED_HOSTS=<comma-separated-list>
If you're planning on putting Paperless on the open internet, then you
really should set this value to the domain name you're using. Failing to do
so leaves you open to HTTP host header attacks:
@@ -151,12 +168,16 @@ PAPERLESS_ALLOWED_HOSTS<comma-separated-list>
Just remember that this is a comma-separated list, so "example.com" is fine,
as is "example.com,www.example.com", but NOT " example.com" or "example.com,"
Can also be set using PAPERLESS_URL (see above).
Defaults to "*", which is all hosts.
PAPERLESS_CORS_ALLOWED_HOSTS<comma-separated-list>
PAPERLESS_CORS_ALLOWED_HOSTS=<comma-separated-list>
You need to add your servers to the list of allowed hosts that can do CORS
calls. Set this to your public domain name.
Can also be set using PAPERLESS_URL (see above).
Defaults to "http://localhost:8000".
PAPERLESS_FORCE_SCRIPT_NAME=<path>
@@ -540,6 +561,10 @@ PAPERLESS_WORKER_TIMEOUT=<num>
large documents within the default 1800 seconds. So extending this timeout
may prove to be useful on weak hardware setups.
PAPERLESS_WORKER_RETRY=<num>
If PAPERLESS_WORKER_TIMEOUT has been configured, the retry time for a task can
also be configured. By default, this value will be set to 10s more than the
worker timeout. This value should never be set less than the worker timeout.
PAPERLESS_TIME_ZONE=<timezone>
Set the time zone here.
@@ -588,6 +613,27 @@ PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=<bool>
Defaults to false.
PAPERLESS_CONSUMER_ENABLE_BARCODES=<bool>
Enables the scanning and page separation based on detected barcodes.
This allows for scanning and adding multiple documents per uploaded
file, which are separated by one or multiple barcode pages.
For ease of use, it is suggested to use a standardized separation page,
e.g. `here <https://www.alliancegroup.co.uk/patch-codes.htm>`_.
If no barcodes are detected in the uploaded file, no page separation
will happen.
Defaults to false.
PAPERLESS_CONSUMER_BARCODE_STRING=PATCHT
Defines the string to be detected as a separator barcode.
If paperless is used with the PATCH-T separator pages, users
shouldn't change this.
Defaults to "PATCHT"
PAPERLESS_CONVERT_MEMORY_LIMIT=<num>
On smaller systems, or even in the case of Very Large Documents, the consumer
@@ -671,7 +717,7 @@ PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>
This can be adjusted by configuring a custom json array with patterns to exclude.
Defautls to ``[".DS_STORE/*", "._*", ".stfolder/*"]``.
Defaults to ``[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]``.
Binaries
########
@@ -764,3 +810,26 @@ PAPERLESS_OCR_LANGUAGES=<list>
PAPERLESS_OCR_LANGUAGE=tur
Defaults to none, which does not install any additional languages.
.. _configuration-update-checking:
Update Checking
###############
PAPERLESS_ENABLE_UPDATE_CHECK=<bool>
Enable (or disable) the automatic check for available updates. This feature is disabled
by default but if it is not explicitly set Paperless-ngx will show a message about this.
If enabled, the feature works by pinging the the Github API for the latest release e.g.
https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest
to determine whether a new version is available.
Actual updating of the app must still be performed manually.
Note that for users of thirdy-party containers e.g. linuxserver.io this notification
may be 'ahead' of a new release from the third-party maintainers.
In either case, no tracking data is collected by the app in any way.
Defaults to none, which disables the feature.

View File

@@ -13,43 +13,43 @@ that works right for you based on recommendations from other Paperless users.
Physical scanners
=================
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brand | Model | Supports | Recommended By |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| | | FTP | NFS | SMB | SMTP | API [1]_ | |
+=========+================+=====+=====+=====+======+==========+================+
| Brother | `ADS-1700W`_ | yes | | yes | yes | |`holzhannes`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `ADS-1600W`_ | yes | | yes | yes | |`holzhannes`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `ADS-1500W`_ | yes | | yes | yes | |`danielquinn`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `ADS-1100W`_ | yes | | | | |`ytzelf`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `ADS-2800W`_ | yes | yes | | yes | yes |`philpagel`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `MFC-J6930DW`_ | yes | | | | |`ayounggun`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `MFC-L5850DW`_ | yes | | | yes | |`holzhannes`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `MFC-L2750DW`_ | yes | | yes | yes | |`muued`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `MFC-J5910DW`_ | yes | | | | |`bmsleight`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `MFC-8950DW`_ | yes | | | yes | yes |`philpagel`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Brother | `MFC-9142CDN`_ | yes | | yes | | |`REOLDEV`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Fujitsu | `ix500`_ | yes | | yes | | |`eonist`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Epson | `ES-580W`_ | yes | | yes | yes | |`fignew`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Epson | `WF-7710DWF`_ | yes | | yes | | |`Skylinar`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Fujitsu | `S1300i`_ | yes | | yes | | |`jonaswinkler`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
| Doxie | `Q2`_ | | | | | yes |`Unkn0wnCat`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brand | Model | Supports | Recommended By |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| | | FTP | SFTP | NFS | SMB | SMTP | API [1]_ | |
+=========+================+=====+======+=====+=====+======+==========+================+
| Brother | `ADS-1700W`_ | yes | | | yes | yes | |`holzhannes`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `ADS-1600W`_ | yes | | | yes | yes | |`holzhannes`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `ADS-1500W`_ | yes | | | yes | yes | |`danielquinn`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `ADS-1100W`_ | yes | | | | | |`ytzelf`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `ADS-2800W`_ | yes | yes | | yes | yes | |`philpagel`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `MFC-J6930DW`_ | yes | | | | | |`ayounggun`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `MFC-L5850DW`_ | yes | | | | yes | |`holzhannes`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `MFC-L2750DW`_ | yes | | | yes | yes | |`muued`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `MFC-J5910DW`_ | yes | | | | | |`bmsleight`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `MFC-8950DW`_ | yes | | | yes | yes | |`philpagel`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brother | `MFC-9142CDN`_ | yes | | | yes | | |`REOLDEV`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Fujitsu | `ix500`_ | yes | | | yes | | |`eonist`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Epson | `ES-580W`_ | yes | | | yes | yes | |`fignew`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Epson | `WF-7710DWF`_ | yes | | | yes | | |`Skylinar`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Fujitsu | `S1300i`_ | yes | | | yes | | |`jonaswinkler`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Doxie | `Q2`_ | | | | | | yes |`Unkn0wnCat`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
.. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw
.. _MFC-L2750DW: https://www.brother.de/drucker/laserdrucker/mfc-l2750dw
@@ -131,4 +131,3 @@ This part assumes your Doxie is connected to WiFi and you know its IP.
6. Click *Submit* at the bottom of the page
Congrats, you can now scan directly from your Doxie to your Paperless-ngx instance!

View File

@@ -178,6 +178,14 @@ These are as follows:
automatically or manually and tell paperless to move them to yet another folder
after consumption. It's up to you.
.. note::
When defining a mail rule with a folder, you may need to try different characters to
define how the sub-folders are separated. Common values include ".", "/" or "|", but
this varies by the mail server. Unfortunately, this isn't a value we can determine
automatically. Either check the documentation for your mail server, or check for
errors in the logs and try different folder separator values.
.. note::
Paperless will process the rules in the order defined in the admin page.

View File

@@ -47,24 +47,29 @@ if [[ $(id -u) == "0" ]] ; then
exit 1
fi
if [[ -z $(which wget) ]] ; then
if ! command -v wget &> /dev/null ; then
echo "wget executable not found. Is wget installed?"
exit 1
fi
if [[ -z $(which docker) ]] ; then
if ! command -v docker &> /dev/null ; then
echo "docker executable not found. Is docker installed?"
exit 1
fi
if [[ -z $(which docker-compose) ]] ; then
echo "docker-compose executable not found. Is docker-compose installed?"
exit 1
DOCKER_COMPOSE_CMD="docker-compose"
if ! command -v ${DOCKER_COMPOSE_CMD} ; then
if docker compose version &> /dev/null ; then
DOCKER_COMPOSE_CMD="docker compose"
else
echo "docker-compose executable not found. Is docker-compose installed?"
exit 1
fi
fi
# Check if user has permissions to run Docker by trying to get the status of Docker (docker status).
# If this fails, the user probably does not have permissions for Docker.
if [ ! "$(docker stats --no-stream 2>/dev/null 1>&2)" ] ; then
if ! docker stats --no-stream &> /dev/null ; then
echo ""
echo "WARN: It look like the current user does not have Docker permissions."
echo "WARN: Use 'sudo usermod -aG docker $USER' to assign Docker permissions to the user."
@@ -87,6 +92,14 @@ echo ""
echo "1. Application configuration"
echo "============================"
echo ""
echo "The URL paperless will be available at. This is required if the"
echo "installation will be accessible via the web, otherwise can be left blank."
echo ""
ask "URL" ""
URL=$ask_result
echo ""
echo "The port on which the paperless webserver will listen for incoming"
echo "connections."
@@ -273,6 +286,7 @@ if [[ "$DATABASE_BACKEND" == "postgres" ]] ; then
fi
fi
echo ""
echo "URL: $URL"
echo "Port: $PORT"
echo "Database: $DATABASE_BACKEND"
echo "Tika enabled: $TIKA_ENABLED"
@@ -308,6 +322,9 @@ SECRET_KEY=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | fold -w 64 | head -n 1)
DEFAULT_LANGUAGES="deu eng fra ita spa"
{
if [[ ! $URL == "" ]] ; then
echo "PAPERLESS_URL=$URL"
fi
if [[ ! $USERMAP_UID == "1000" ]] ; then
echo "USERMAP_UID=$USERMAP_UID"
fi
@@ -351,8 +368,8 @@ if [ "$l1" -eq "$l2" ] ; then
fi
docker-compose pull
${DOCKER_COMPOSE_CMD} pull
docker-compose run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
${DOCKER_COMPOSE_CMD} run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
docker-compose up -d
${DOCKER_COMPOSE_CMD} up -d

View File

@@ -27,8 +27,10 @@
# Security and hosting
#PAPERLESS_SECRET_KEY=change-me
#PAPERLESS_ALLOWED_HOSTS=example.com,www.example.com
#PAPERLESS_CORS_ALLOWED_HOSTS=http://example.com,http://localhost:8000
#PAPERLESS_URL=https://example.com
#PAPERLESS_CSRF_TRUSTED_ORIGINS=https://example.com # can be set using PAPERLESS_URL
#PAPERLESS_ALLOWED_HOSTS=example.com,www.example.com # can be set using PAPERLESS_URL
#PAPERLESS_CORS_ALLOWED_HOSTS=https://localhost:8080,https://example.com # can be set using PAPERLESS_URL
#PAPERLESS_FORCE_SCRIPT_NAME=
#PAPERLESS_STATIC_URL=/static/
#PAPERLESS_AUTO_LOGIN_USERNAME=
@@ -58,8 +60,10 @@
#PAPERLESS_CONSUMER_POLLING=10
#PAPERLESS_CONSUMER_DELETE_DUPLICATES=false
#PAPERLESS_CONSUMER_RECURSIVE=false
#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*"]
#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
#PAPERLESS_CONSUMER_ENABLE_BARCODES=false
#PAPERLESS_CONSUMER_ENABLE_BARCODES=PATCHT
#PAPERLESS_OPTIMIZE_THUMBNAILS=true
#PAPERLESS_PRE_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
@@ -67,6 +71,7 @@
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
#PAPERLESS_THUMBNAIL_FONT_NAME=
#PAPERLESS_IGNORE_DATES=
#PAPERLESS_ENABLE_UPDATE_CHECK=
# Tika settings

View File

@@ -5,15 +5,15 @@
# pipenv lock --requirements
#
-i https://pypi.python.org/simple
--extra-index-url https://www.piwheels.org/simple
-i https://pypi.python.org/simple/
--extra-index-url https://www.piwheels.org/simple/
aioredis==1.3.1
anyio==3.5.0; python_full_version >= '3.6.2'
arrow==1.2.2; python_version >= '3.6'
asgiref==3.5.0; python_version >= '3.7'
async-timeout==4.0.2; python_version >= '3.6'
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
autobahn==22.2.2; python_version >= '3.7'
autobahn==22.3.2; python_version >= '3.7'
automat==20.2.0
backports.zoneinfo==0.2.1; python_version < '3.9'
blessed==1.19.1; python_version >= '2.7'
@@ -23,7 +23,7 @@ channels-redis==3.4.0
channels==3.0.4
chardet==4.0.0; python_version >= '3.1'
charset-normalizer==2.0.12; python_version >= '3'
click==8.0.4; python_version >= '3.6'
click==8.1.2; python_version >= '3.7'
coloredlogs==15.0.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
concurrent-log-handler==0.9.20
constantly==15.1.0
@@ -35,7 +35,7 @@ django-extensions==3.1.5
django-filter==21.1
django-picklefield==3.0.1; python_version >= '3'
django-q==1.3.9
django==4.0.3
django==4.0.4
djangorestframework==3.13.1
filelock==3.6.0
fuzzywuzzy[speedup]==0.18.0
@@ -47,7 +47,7 @@ humanfriendly==10.0; python_version >= '2.7' and python_version not in '3.0, 3.1
hyperlink==21.0.0
idna==3.3; python_version >= '3.5'
imap-tools==0.53.0
img2pdf==0.4.3
img2pdf==0.4.4
importlib-resources==5.6.0; python_version < '3.9'
incremental==21.3.0
inotify-simple==1.3.5; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
@@ -57,12 +57,13 @@ langdetect==1.0.9
lxml==4.8.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
msgpack==1.0.3
numpy==1.22.3; python_version >= '3.8'
ocrmypdf==13.4.1
ocrmypdf==13.4.2
packaging==21.3; python_version >= '3.6'
pathvalidate==2.5.0
pdfminer.six==20211012
pikepdf==5.1.0
pillow==9.0.1
pdf2image==1.16.0
pdfminer.six==20220319
pikepdf==5.1.1
pillow==9.1.0
pluggy==1.0.0; python_version >= '3.6'
portalocker==2.4.0; python_version >= '3'
psycopg2==2.9.3
@@ -70,7 +71,7 @@ pyasn1-modules==0.2.8
pyasn1==0.4.8
pycparser==2.21
pyopenssl==22.0.0
pyparsing==3.0.7; python_version >= '3.6'
pyparsing==3.0.8; python_full_version >= '3.6.8'
python-dateutil==2.8.2
python-dotenv==0.20.0
python-gnupg==0.4.8
@@ -79,6 +80,7 @@ python-magic==0.4.25
pytz-deprecation-shim==0.1.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
pytz==2022.1
pyyaml==6.0
pyzbar==0.1.9
redis==3.5.3
regex==2022.3.2; python_version >= '3.6'
reportlab==3.6.9; python_version >= '3.7' and python_version < '4'
@@ -86,26 +88,26 @@ requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3
scikit-learn==1.0.2
scipy==1.8.0; python_version < '3.11' and python_version >= '3.8'
service-identity==21.1.0
setuptools==61.1.1; python_version >= '3.7'
setuptools==62.1.0; python_version >= '3.7'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sniffio==1.2.0; python_version >= '3.5'
sqlparse==0.4.2; python_version >= '3.5'
threadpoolctl==3.1.0; python_version >= '3.6'
tika==1.24
tqdm==4.63.1
twisted[tls]==22.2.0; python_full_version >= '3.6.7'
tqdm==4.64.0
twisted[tls]==22.4.0; python_full_version >= '3.6.7'
txaio==22.2.1; python_version >= '3.6'
typing-extensions==4.1.1; python_version >= '3.6'
tzdata==2022.1; python_version >= '3.6'
tzlocal==4.1; python_version >= '3.6'
tzlocal==4.2; python_version >= '3.6'
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
uvicorn[standard]==0.17.6
uvloop==0.16.0
watchdog==2.1.7
watchgod==0.8.1
watchgod==0.8.2
wcwidth==0.2.5
websockets==10.2
whitenoise==6.0.0
whoosh==2.7.4
zipp==3.7.0; python_version < '3.9'
zipp==3.8.0; python_version < '3.9'
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'

View File

@@ -1,3 +1,13 @@
<app-toasts></app-toasts>
<router-outlet></router-outlet>
<ngx-file-drop dropZoneClassName="main-dropzone" contentClassName="main-content" [disabled]="!dragDropEnabled"
(onFileDrop)="dropped($event)" (onFileOver)="fileOver()" (onFileLeave)="fileLeave()">
<ng-template ngx-file-drop-content-tmp>
<div class="global-dropzone-overlay fade" [class.show]="fileIsOver" [class.hide]="hidden">
<h2 i18n>Drop files to begin upload</h2>
</div>
<div [class.inert]="fileIsOver">
<router-outlet></router-outlet>
</div>
</ng-template>
</ngx-file-drop>

View File

@@ -4,6 +4,8 @@ import { Router } from '@angular/router'
import { Subscription } from 'rxjs'
import { ConsumerStatusService } from './services/consumer-status.service'
import { ToastService } from './services/toast.service'
import { NgxFileDropEntry } from 'ngx-file-drop'
import { UploadDocumentsService } from './services/upload-documents.service'
@Component({
selector: 'app-root',
@@ -15,11 +17,16 @@ export class AppComponent implements OnInit, OnDestroy {
successSubscription: Subscription
failedSubscription: Subscription
private fileLeaveTimeoutID: any
fileIsOver: boolean = false
hidden: boolean = true
constructor(
private settings: SettingsService,
private consumerStatusService: ConsumerStatusService,
private toastService: ToastService,
private router: Router
private router: Router,
private uploadDocumentsService: UploadDocumentsService
) {
let anyWindow = window as any
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
@@ -100,4 +107,36 @@ export class AppComponent implements OnInit, OnDestroy {
}
})
}
public get dragDropEnabled(): boolean {
return !this.router.url.includes('dashboard')
}
public fileOver() {
// allows transition
setTimeout(() => {
this.fileIsOver = true
}, 1)
this.hidden = false
// stop fileLeave timeout
clearTimeout(this.fileLeaveTimeoutID)
}
public fileLeave(immediate: boolean = false) {
const ms = immediate ? 0 : 500
this.fileLeaveTimeoutID = setTimeout(() => {
this.fileIsOver = false
// await transition completed
setTimeout(() => {
this.hidden = true
}, 150)
}, ms)
}
public dropped(files: NgxFileDropEntry[]) {
this.fileLeave(true)
this.uploadDocumentsService.uploadFiles(files)
this.toastService.showInfo($localize`Initiating upload...`, 3000)
}
}

View File

@@ -12,7 +12,7 @@
</a>
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1">
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
<svg width="1em" height="1em">
<svg width="1em" height="1em" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#search"/>
</svg>
<input class="form-control form-control-sm" type="text" placeholder="Search documents" aria-label="Search"
@@ -25,7 +25,7 @@
<span *ngIf="displayName" class="navbar-text small me-2 text-light d-none d-sm-inline">
{{displayName}}
</span>
<svg width="1.3em" height="1.3em">
<svg width="1.3em" height="1.3em" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person-circle"/>
</svg>
</button>
@@ -170,21 +170,46 @@
<li class="nav-item">
<div class="d-flex w-100 flex-wrap">
<a class="nav-link pe-2 pb-1" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon bi bi-github" viewBox="0 0 16 16">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" class="sidebaricon" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#github" />
</svg>&nbsp;<ng-container i18n>GitHub</ng-container>
</a>
<a class="nav-link-additional small text-muted ms-3" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea">
<svg xmlns="http://www.w3.org/2000/svg" width="1.3em" height="1.3em" fill="currentColor" class="bi bi-lightbulb pe-1" viewBox="0 0 16 16">
<path d="M2 6a6 6 0 1 1 10.174 4.31c-.203.196-.359.4-.453.619l-.762 1.769A.5.5 0 0 1 10.5 13a.5.5 0 0 1 0 1 .5.5 0 0 1 0 1l-.224.447a1 1 0 0 1-.894.553H6.618a1 1 0 0 1-.894-.553L5.5 15a.5.5 0 0 1 0-1 .5.5 0 0 1 0-1 .5.5 0 0 1-.46-.302l-.761-1.77a1.964 1.964 0 0 0-.453-.618A5.984 5.984 0 0 1 2 6zm6-5a5 5 0 0 0-3.479 8.592c.263.254.514.564.676.941L5.83 12h4.342l.632-1.467c.162-.377.413-.687.676-.941A5 5 0 0 0 8 1z"/>
<svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#lightbulb" />
</svg>
<ng-container i18n>Suggest an idea</ng-container>
</a>
</div>
</li>
<li class="nav-item mt-2">
<div class="px-3 py-2 text-muted small">
{{versionString}}
<div class="px-3 py-2 text-muted small d-flex align-items-center flex-wrap">
<div class="me-3">{{ versionString }}</div>
<div *ngIf="appRemoteVersion" class="version-check">
<ng-template #updateAvailablePopContent>
<span class="small">Paperless-ngx v{{ appRemoteVersion.version }} <ng-container i18n>is available.</ng-container><br/><ng-container i18n>Click to view.</ng-container></span>
</ng-template>
<ng-template #updateCheckingNotEnabledPopContent>
<span class="small"><ng-container i18n>Checking for updates is disabled.</ng-container><br/><ng-container i18n>Click for more information.</ng-container></span>
</ng-template>
<ng-container *ngIf="appRemoteVersion.feature_is_set; else updateCheckNotSet">
<a *ngIf="appRemoteVersion.update_available" class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/releases"
[ngbPopover]="updateAvailablePopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
<ng-container *ngIf="appRemoteVersion?.update_available" i18n>Update available</ng-container>
</a>
</ng-container>
<ng-template #updateCheckNotSet>
<a class="small text-decoration-none" target="_blank" rel="noopener noreferrer" href="https://paperless-ngx.readthedocs.io/en/latest/configuration.html#update-checking"
[ngbPopover]="updateCheckingNotEnabledPopContent" popoverClass="shadow" triggers="mouseenter:mouseleave" container="body">
<svg fill="currentColor" class="me-1" width="1.2em" height="1.2em" style="vertical-align: text-top;" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#info-circle" />
</svg>
</a>
</ng-template>
</div>
</div>
</li>
</ul>

View File

@@ -35,20 +35,19 @@
.sidebar .nav-link {
font-weight: 500;
}
.sidebar .nav-link .sidebaricon {
margin-right: 4px;
}
&:hover, &.active {
color: var(--bs-primary);
}
.sidebar .nav-link.active {
color: var(--bs-primary);
font-weight: bold;
}
&.active {
font-weight: bold;
}
.sidebar .nav-link.active .sidebaricon,
.sidebar .nav-link:hover .sidebaricon {
color: inherit;
.sidebaricon {
margin-right: 4px;
color: inherit;
}
}
.sidebar-heading {
@@ -171,8 +170,28 @@
&:focus {
background-color: rgba(0, 0, 0, 0.3);
color: var(--bs-light);
flex-grow: 1;
padding-left: 0.5rem;
}
}
}
.version-check {
animation: pulse 2s ease-in-out 0s 1;
}
@keyframes pulse {
0% {
opacity: 0;
}
25% {
opacity: 100%;
}
75% {
opacity: 0;
}
100% {
opacity: 100%;
}
}

View File

@@ -18,6 +18,10 @@ import { DocumentDetailComponent } from '../document-detail/document-detail.comp
import { Meta } from '@angular/platform-browser'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
import {
RemoteVersionService,
AppRemoteVersion,
} from 'src/app/services/rest/remote-version.service'
@Component({
selector: 'app-app-frame',
@@ -32,10 +36,18 @@ export class AppFrameComponent {
private searchService: SearchService,
public savedViewService: SavedViewService,
private list: DocumentListViewService,
private meta: Meta
) {}
private meta: Meta,
private remoteVersionService: RemoteVersionService
) {
this.remoteVersionService
.checkForUpdates()
.subscribe((appRemoteVersion: AppRemoteVersion) => {
this.appRemoteVersion = appRemoteVersion
})
}
versionString = `${environment.appTitle} ${environment.version}`
appRemoteVersion
isMenuCollapsed: boolean = true

View File

@@ -42,7 +42,7 @@
filter: brightness(0.5);
&.active {
background-color: var(--ngx-primary-lighten-30);
background-color: var(--pngx-primary-lighten-30);
}
}

View File

@@ -2,7 +2,7 @@
*ngFor="let toast of toasts"
[header]="toast.title" [autohide]="true" [delay]="toast.delay"
[class]="toast.classname"
(hide)="toastService.closeToast(toast)">
(hidden)="toastService.closeToast(toast)">
<p>{{toast.content}}</p>
<p *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
</ngb-toast>

View File

@@ -5,3 +5,7 @@
margin: 0.5em;
z-index: 1200;
}
.toast:not(.show) {
display: block; // this corrects an ng-bootstrap bug that prevented animations
}

View File

@@ -33,3 +33,7 @@ form {
mix-blend-mode: soft-light;
pointer-events: none;
}
::ng-deep .ngx-file-drop__drop-zone--over {
background-color: var(--pngx-primary-faded) !important;
}

View File

@@ -6,7 +6,7 @@ import {
FileStatus,
FileStatusPhase,
} from 'src/app/services/consumer-status.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
const MAX_ALERTS = 5
@@ -19,8 +19,8 @@ export class UploadFileWidgetComponent implements OnInit {
alertsExpanded = false
constructor(
private documentService: DocumentService,
private consumerStatusService: ConsumerStatusService
private consumerStatusService: ConsumerStatusService,
private uploadDocumentsService: UploadDocumentsService
) {}
getStatus() {
@@ -116,48 +116,6 @@ export class UploadFileWidgetComponent implements OnInit {
public fileLeave(event) {}
public dropped(files: NgxFileDropEntry[]) {
for (const droppedFile of files) {
if (droppedFile.fileEntry.isFile) {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry
fileEntry.file((file: File) => {
let formData = new FormData()
formData.append('document', file, file.name)
let status = this.consumerStatusService.newFileUpload(file.name)
status.message = $localize`Connecting...`
this.documentService.uploadDocument(formData).subscribe(
(event) => {
if (event.type == HttpEventType.UploadProgress) {
status.updateProgress(
FileStatusPhase.UPLOADING,
event.loaded,
event.total
)
status.message = $localize`Uploading...`
} else if (event.type == HttpEventType.Response) {
status.taskId = event.body['task_id']
status.message = $localize`Upload complete, waiting...`
}
},
(error) => {
switch (error.status) {
case 400: {
this.consumerStatusService.fail(status, error.error.document)
break
}
default: {
this.consumerStatusService.fail(
status,
$localize`HTTP error: ${error.status} ${error.statusText}`
)
break
}
}
}
)
})
}
}
this.uploadDocumentsService.uploadFiles(files)
}
}

View File

@@ -135,20 +135,27 @@
</li>
<li [ngbNavItem]="4" class="d-md-none">
<a ngbNavLink>Preview</a>
<ng-template ngbNavContent *ngIf="pdfPreview.offsetParent == undefined">
<ng-container *ngIf="getContentType() == 'application/pdf'">
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [render-text-mode]="2"></pdf-viewer>
<a ngbNavLink>Preview</a>
<ng-template ngbNavContent *ngIf="pdfPreview.offsetParent == undefined">
<div class="position-relative">
<ng-container *ngIf="getContentType() == 'application/pdf'">
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
<pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
</div>
<ng-template #nativePdfViewer>
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
</ng-template>
</ng-container>
<ng-container *ngIf="getContentType() == 'text/plain'">
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
</ng-container>
<div *ngIf="requiresPassword" class="password-prompt">
<form>
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</form>
</div>
</div>
<ng-template #nativePdfViewer>
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
</ng-template>
</ng-container>
<ng-container *ngIf="getContentType() == 'text/plain'">
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
</ng-container>
</ng-template>
</ng-template>
</li>
</ul>
@@ -160,10 +167,10 @@
</form>
</div>
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block" #pdfPreview>
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
<ng-container *ngIf="getContentType() == 'application/pdf'">
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
<pdf-viewer [src]="{ url: previewUrl, password: password }" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (error)="onError($event)" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
</div>
<ng-template #nativePdfViewer>
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
@@ -172,5 +179,11 @@
<ng-container *ngIf="getContentType() == 'text/plain'">
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
</ng-container>
<div *ngIf="requiresPassword" class="password-prompt">
<form>
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</form>
</div>
</div>
</div>

View File

@@ -17,3 +17,10 @@
--page-margin: 1px 0 -8px;
width: 100% !important;
}
.password-prompt {
position: absolute;
top: 30%;
left: 30%;
right: 30%;
}

View File

@@ -1,10 +1,4 @@
import {
Component,
OnInit,
OnDestroy,
ViewChild,
ElementRef,
} from '@angular/core'
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import { NgbModal, NgbNav } from '@ng-bootstrap/ng-bootstrap'
@@ -90,6 +84,11 @@ export class DocumentDetailComponent
isDirty$: Observable<boolean>
unsubscribeNotifier: Subject<any> = new Subject()
requiresPassword: boolean = false
password: string
ogDate: Date
@ViewChild('nav') nav: NgbNav
@ViewChild('pdfPreview') set pdfPreview(element) {
// this gets called when compontent added or removed from DOM
@@ -145,7 +144,21 @@ export class DocumentDetailComponent
ngOnInit(): void {
this.documentForm.valueChanges
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((wow) => {
.subscribe((changes) => {
if (this.ogDate) {
let newDate = new Date(changes['created'])
newDate.setHours(
this.ogDate.getHours(),
this.ogDate.getMinutes(),
this.ogDate.getSeconds(),
this.ogDate.getMilliseconds()
)
this.documentForm.patchValue(
{ created: this.formatDate(newDate) },
{ emitEvent: false }
)
}
Object.assign(this.document, this.documentForm.value)
})
@@ -186,17 +199,25 @@ export class DocumentDetailComponent
this.updateComponent(doc)
}
this.ogDate = new Date(doc.created)
// Initialize dirtyCheck
this.store = new BehaviorSubject({
title: doc.title,
content: doc.content,
created: doc.created,
created: this.formatDate(this.ogDate),
correspondent: doc.correspondent,
document_type: doc.document_type,
archive_serial_number: doc.archive_serial_number,
tags: [...doc.tags],
})
// ensure we're always starting with 24-char ISO8601 string
this.documentForm.patchValue(
{ created: this.formatDate(this.ogDate) },
{ emitEvent: false }
)
this.isDirty$ = dirtyCheck(
this.documentForm,
this.store.asObservable()
@@ -450,5 +471,22 @@ export class DocumentDetailComponent
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
this.previewNumPages = pdf.numPages
if (this.password) this.requiresPassword = false
}
onError(event) {
if (event.name == 'PasswordException') {
this.requiresPassword = true
}
}
onPasswordKeyUp(event: KeyboardEvent) {
if ('Enter' == event.key) {
this.password = (event.target as HTMLInputElement).value
}
}
formatDate(date: Date): string {
return date.toISOString().split('.')[0] + 'Z'
}
}

View File

@@ -57,13 +57,18 @@
</div>
<div class="col-auto ms-auto mb-2 mb-xl-0 d-flex">
<div class="btn-group btn-group-sm me-2">
<button type="button" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<button type="button" [disabled]="awaitingDownload" class="btn btn-outline-primary btn-sm" (click)="downloadSelected()">
<svg *ngIf="!awaitingDownload" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download" />
</svg>&nbsp;<ng-container i18n>Download</ng-container>
</svg>
<div *ngIf="awaitingDownload" class="spinner-border spinner-border-sm" role="status">
<span class="visually-hidden">Preparing download...</span>
</div>
&nbsp;
<ng-container i18n>Download</ng-container>
</button>
<div class="btn-group" ngbDropdown role="group" aria-label="Button group with nested dropdown">
<button class="btn btn-outline-primary btn-sm dropdown-toggle-split" ngbDropdownToggle></button>
<button [disabled]="awaitingDownload" class="btn btn-outline-primary btn-sm dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<button ngbDropdownItem i18n (click)="downloadSelected('originals')">Download originals</button>
</div>

View File

@@ -39,6 +39,7 @@ export class BulkEditorComponent {
tagSelectionModel = new FilterableDropdownSelectionModel()
correspondentSelectionModel = new FilterableDropdownSelectionModel()
documentTypeSelectionModel = new FilterableDropdownSelectionModel()
awaitingDownload: boolean
constructor(
private documentTypeService: DocumentTypeService,
@@ -317,10 +318,12 @@ export class BulkEditorComponent {
}
downloadSelected(content = 'archive') {
this.awaitingDownload = true
this.documentService
.bulkDownload(Array.from(this.list.selected), content)
.subscribe((result: any) => {
saveAs(result, 'documents.zip')
this.awaitingDownload = false
})
}
}

View File

@@ -60,7 +60,7 @@
}
.doc-img-background-selected {
background-color: var(--ngx-primary-faded);
background-color: var(--pngx-primary-faded);
}
.card-info {

View File

@@ -45,7 +45,7 @@
}
.doc-img-background-selected {
background-color: var(--ngx-primary-faded);
background-color: var(--pngx-primary-faded);
}
.card-info {

View File

@@ -75,7 +75,7 @@
</app-page-header>
<div class="sticky-top py-2 mt-n2 mt-sm-n3 py-sm-4 bg-body mx-n3 px-3">
<div class="row sticky-top pt-4 pb-2 pb-lg-4 bg-body">
<app-filter-editor [hidden]="isBulkEditing" [(filterRules)]="list.filterRules" [unmodifiedFilterRules]="unmodifiedFilterRules" #filterEditor></app-filter-editor>
<app-bulk-editor [hidden]="!isBulkEditing"></app-bulk-editor>
</div>
@@ -100,7 +100,7 @@
<ng-container *ngTemplateOutlet="pagination"></ng-container>
<ng-container *ngIf="list.error ; else documentListNoError">
<div class="alert alert-danger" role="alert">Error while loading documents: {{list.error}}</div>
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
</ng-container>
<ng-template #documentListNoError>
@@ -185,7 +185,7 @@
</tbody>
</table>
<div class="m-n2 row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
<div class="row row-cols-paperless-cards" *ngIf="displayMode == 'smallCards'">
<app-document-card-small class="p-0" [selected]="list.isSelected(d)" (toggleSelected)="toggleSelected(d, $event)" [document]="d" *ngFor="let d of list.documents; trackBy: trackByDocumentId" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)" (clickDocumentType)="clickDocumentType($event)"></app-document-card-small>
</div>
<div *ngIf="list.documents?.length > 15" class="mt-3">

View File

@@ -1,11 +1,15 @@
@import "/src/theme";
::ng-deep app-document-list app-page-header > div.mb-3 {
margin-bottom: 0 !important;
}
tr {
user-select: none;
}
.table-row-selected {
background-color: var(--ngx-primary-faded);
background-color: var(--pngx-primary-faded);
}
$paperless-card-breakpoints: (

View File

@@ -243,8 +243,12 @@ export class DocumentListComponent implements OnInit, OnDestroy, AfterViewInit {
$localize`View "${savedView.name}" created successfully.`
)
},
error: (error) => {
modal.componentInstance.error = error.error
error: (httpError) => {
let error = httpError.error
if (error.filter_rules) {
error.filter_rules = error.filter_rules.map((r) => r.value)
}
modal.componentInstance.error = error
modal.componentInstance.buttonsEnabled = true
},
})

View File

@@ -50,7 +50,7 @@
</div>
</div>
<div class="w-100 d-xl-none"></div>
<div class="col col-xl-auto mb-2 mb-xl-0">
<div class="col col-xl-auto">
<button class="btn btn-link btn-sm px-0 mx-0 ms-xl-n3" [disabled]="!rulesModified" (click)="resetSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x me-1" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>

View File

@@ -8,6 +8,11 @@
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
<app-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></app-input-check>
<app-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
<div *ngIf="error?.filter_rules" class="alert alert-danger" role="alert">
<h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>
<ng-container i18n>The error returned was</ng-container>:<br/>
{{ error.filter_rules }}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button>

View File

@@ -9,7 +9,7 @@ import {
import { PaperlessDocument } from '../data/paperless-document'
import { PaperlessSavedView } from '../data/paperless-saved-view'
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
import { DocumentService } from './rest/document.service'
import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service'
import { SettingsService, SETTINGS_KEYS } from './settings.service'
/**
@@ -160,7 +160,24 @@ export class DocumentListViewService {
activeListViewState.currentPage = 1
this.reload()
} else {
this.error = error.error
let errorMessage
if (
typeof error.error !== 'string' &&
Object.keys(error.error).length > 0
) {
// e.g. { archive_serial_number: Array<string> }
errorMessage = Object.keys(error.error)
.map((fieldName) => {
const fieldError: Array<string> = error.error[fieldName]
return `${
DOCUMENT_SORT_FIELDS.find((f) => f.field == fieldName)?.name
}: ${fieldError[0]}`
})
.join(', ')
} else {
errorMessage = error.error
}
this.error = errorMessage
}
},
})

View File

@@ -0,0 +1,23 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { map, Observable } from 'rxjs'
import { environment } from 'src/environments/environment'
export interface AppRemoteVersion {
version: string
update_available: boolean
feature_is_set: boolean
}
@Injectable({
providedIn: 'root',
})
export class RemoteVersionService {
constructor(private http: HttpClient) {}
public checkForUpdates(): Observable<AppRemoteVersion> {
return this.http.get<AppRemoteVersion>(
`${environment.apiBaseUrl}remote_version/`
)
}
}

View File

@@ -0,0 +1,74 @@
import { Injectable } from '@angular/core'
import { HttpEventType } from '@angular/common/http'
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop'
import {
ConsumerStatusService,
FileStatusPhase,
} from './consumer-status.service'
import { DocumentService } from './rest/document.service'
import { Subscription } from 'rxjs'
@Injectable({
providedIn: 'root',
})
export class UploadDocumentsService {
private uploadSubscriptions: Array<Subscription> = []
constructor(
private documentService: DocumentService,
private consumerStatusService: ConsumerStatusService
) {}
uploadFiles(files: NgxFileDropEntry[]) {
for (const droppedFile of files) {
if (droppedFile.fileEntry.isFile) {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry
fileEntry.file((file: File) => {
let formData = new FormData()
formData.append('document', file, file.name)
let status = this.consumerStatusService.newFileUpload(file.name)
status.message = $localize`Connecting...`
this.uploadSubscriptions[file.name] = this.documentService
.uploadDocument(formData)
.subscribe({
next: (event) => {
if (event.type == HttpEventType.UploadProgress) {
status.updateProgress(
FileStatusPhase.UPLOADING,
event.loaded,
event.total
)
status.message = $localize`Uploading...`
} else if (event.type == HttpEventType.Response) {
status.taskId = event.body['task_id']
status.message = $localize`Upload complete, waiting...`
this.uploadSubscriptions[file.name]?.complete()
}
},
error: (error) => {
switch (error.status) {
case 400: {
this.consumerStatusService.fail(
status,
error.error.document
)
break
}
default: {
this.consumerStatusService.fail(
status,
$localize`HTTP error: ${error.status} ${error.statusText}`
)
break
}
}
this.uploadSubscriptions[file.name]?.complete()
},
})
})
}
}
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 579 KiB

After

Width:  |  Height:  |  Size: 861 KiB

View File

@@ -5,7 +5,7 @@ export const environment = {
apiBaseUrl: document.baseURI + 'api/',
apiVersion: '2',
appTitle: 'Paperless-ngx',
version: '1.6.0',
version: '1.7.0-rc1',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',

View File

@@ -7,24 +7,112 @@ $enable-negative-margins: true;
@import "theme_dark";
@import "print";
.toolbaricon {
width: 1.2em;
height: 1.2em;
}
.buttonicon {
width: 1.2em;
height: 1.2em;
}
.sidebaricon {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
// Paperless-ngx styles
body {
font-size: 0.875rem;
height: 100vh;
}
* {
transition: background-color 0.3s ease, border-color 0.3s ease;
}
svg.logo {
.leaf {
fill: var(--bs-primary) !important;
}
.text {
fill: var(--bs-body-color) !important;
}
}
.nav-link, .list-group-item {
color: var(--bs-body-color);
}
.bg-body {
background-color: var(--bs-body-bg);
}
.bg-primary {
background-color: var(--bs-primary) !important;
}
.btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
&:hover, &:focus {
background-color: var(--pngx-primary-darken-10);
border-color: var(--pngx-primary-darken-10);
}
&:disabled, &.disabled {
background-color: var(--pngx-primary-darken-10) !important;
border-color: var(--pngx-primary-darken-10) !important;
}
}
.btn-outline-primary {
border-color: var(--bs-primary) !important;
color: var(--bs-primary) !important;
&:hover, &:focus, &.active, &:active {
background-color: var(--bs-primary) !important;
color: var(--bs-light) !important;
}
}
.btn-outline-secondary {
color: var(--bs-secondary);
&:hover {
color: var(--bs-light);
}
}
.nav-item .sidebaricon {
color: var(--bs-secondary);
}
.btn:focus,
.btn:active:focus,
.dropdown-item:focus,
.btn-check:focus + .btn,
.form-control:focus,
.form-check-input:focus,
.form-check-radio:focus,
.form-select:focus {
box-shadow: 0 0 0 0.25rem hsla(var(--pngx-primary), var(--pngx-primary-lightness), var(--pngx-focus-alpha));
}
.form-switch .form-check-input:focus {
background-image: escape-svg(url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#bbb'/></svg>"));
}
.nav-link:focus-visible, .nav-item a:focus-visible {
outline: none;
background-color: var(--pngx-bg-darker);
}
a.navbar-brand:focus-visible {
outline: none;
color: var(--pngx-primary-darken-10);
}
.dropdown.show {
> .btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
> .btn-outline-primary {
color: var(--pngx-primary-text-contrast) !important;
}
}
a, a:hover, .btn-link, .btn-link:hover {
color: var(--bs-primary);
}
.form-control-dark {
@@ -116,6 +204,245 @@ body {
}
}
.form-control:not(.btn),
input,
select,
textarea,
.form-select:not(.is-invalid):not(:disabled),
.form-check-input {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
&:focus {
background-color: var(--pngx-bg-darker);
color: var(--bs-body-color);
}
}
.form-check-input:checked {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
.form-check-input:focus {
border-color: var(--bs-primary);
}
.page-link {
color: var(--bs-secondary);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color) !important;
&:hover, &:focus {
background-color: var(--bs-primary) !important;
color: var(--bs-light) !important;
}
}
.page-item.active .page-link {
background-color: var(--bs-primary);
border-color: var(--bs-primary) !important;
color: var(--bs-light);
}
.page-item.disabled .page-link {
background-color: var(--pngx-bg-darker);
}
.nav-tabs {
border-bottom: 1px solid var(--bs-border-color);
.nav-link {
color: var(--bs-primary);
&.active, &:hover {
border-color: var(--bs-border-color);
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-bottom: 1px solid transparent;
}
&:focus {
border-color: var(--bs-border-color);
}
&.active:focus, &:active {
border-bottom: 1px solid transparent;
}
}
}
.ng-select-container,
.ng-select.ng-select-opened > .ng-select-container,
.ng-dropdown-panel,
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
background-color: var(--bs-body-bg) !important;
color: var(--bs-body-color) !important;
border-color: var(--bs-border-color) !important;
input:focus {
background-color: transparent !important;
}
}
.input-group-text {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
}
.list-group-item {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
&:hover, &:focus {
background-color: var(--bs-body-bg);
}
}
.dropdown-menu {
background-color: var(--bs-body-bg);
.dropdown-divider {
border-color: var(--bs-border-color);
}
.dropdown-item {
color: var(--bs-body-color);
&:hover, &:focus {
background-color: var(--pngx-bg-darker);
color: var(--bs-body-color);
}
&.active {
background-color: var(--bs-primary);
color: var(--pngx-primary-text-contrast);
}
}
}
// icons
.toolbaricon {
width: 1.2em;
height: 1.2em;
}
.buttonicon {
width: 1.2em;
height: 1.2em;
}
.sidebaricon {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
table.table {
color: var(--bs-body-color);
.des,.asc {
background-color: var(--bs-body-bg) !important;
}
}
.close {
color: var(--bs-body-color);
}
.modal .btn-close {
color: var(--bs-body-color);
}
.main-dropzone {
height: 100%;
width: 100%;
&.ngx-file-drop__drop-zone--over {
background-color: transparent !important;
}
}
.global-dropzone-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(23, 84, 31, .8);
z-index: 1055; // $zindex-modal
pointer-events: none !important;
user-select: none !important;
text-align: center;
padding-top: 25%;
&.show {
opacity: 1 !important;
}
&.hide {
display: none;
}
}
.ngx-file-drop__drop-zone--over .global-dropzone-overlay {
opacity: 0;
}
.inert {
pointer-events: none !important;
user-select: none !important;
}
.alert-danger {
color: var(--bs-body-color);
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.alert-secondary {
background-color: var(--pngx-primary-darken-18);
border-color: var(--pngx-primary-darken-15);
color: var(--bs-body-color);
}
.ngb-dp-header,
.ngb-dp-weekdays,
.ngb-dp-month {
background-color: var(--bs-body-bg);
}
.popover {
.popover-header,
.popover-body {
background-color: var(--pngx-bg-alt);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
}
// fix popover carat colors
.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] {
border-left-color: var(--pngx-bg-alt);
}
.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="right"] {
border-right-color: var(--pngx-bg-alt);
}
.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="top"] {
border-top-color: var(--pngx-bg-alt);
}
.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="bottom"] {
border-bottom-color: var(--pngx-bg-alt);
}
.bs-popover-bottom .popover-header::before,
.bs-popover-auto[x-placement^=bottom] .popover-header::before {
border-bottom-color: var(--pngx-bg-alt);
}
// Bootstrap 5 tweaks
a.badge {
text-decoration: none;

View File

@@ -2,293 +2,16 @@
// base color e.g. #17541f = hsl(128, 57%, 21%)
--pngx-primary: 128, 57%;
--pngx-primary-lightness: 21%;
--pngx-primary-text-contrast: var(--bs-light);
--bs-primary: hsl(var(--pngx-primary), var(--pngx-primary-lightness));
--bs-border-color: var(--bs-gray-400);
--ngx-primary-faded: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 72%));
--ngx-primary-lighten-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%));
--ngx-primary-lighten-30: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 30%));
--ngx-primary-darken-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 10%));
--ngx-primary-darken-15: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%));
--ngx-primary-darken-18: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%));
--ngx-bg-darker: var(--bs-gray-100);
--ngx-focus-alpha: 0.3;
}
svg.logo {
.leaf {
fill: var(--bs-primary) !important;
}
.text {
fill: var(--bs-body-color) !important;
}
}
.nav-link, .list-group-item {
color: var(--bs-body-color);
}
.bg-body {
background-color: var(--bs-body-bg);
}
.bg-primary {
background-color: var(--bs-primary) !important;
}
.btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
&:hover, &:focus {
background-color: var(--ngx-primary-darken-10);
border-color: var(--ngx-primary-darken-10);
}
&:disabled, &.disabled {
background-color: var(--ngx-primary-darken-10) !important;
border-color: var(--ngx-primary-darken-10) !important;
}
}
.btn-outline-primary {
border-color: var(--bs-primary) !important;
color: var(--bs-primary) !important;
&:hover, &:focus, &.active, &:active {
background-color: var(--bs-primary) !important;
color: var(--bs-light) !important;
}
}
.btn-outline-secondary {
color: var(--bs-secondary);
}
.nav-item .sidebaricon {
color: var(--bs-secondary);
}
.btn:focus,
.btn:active:focus,
.dropdown-item:focus,
.btn-check:focus + .btn,
.form-control:focus,
.form-check-input:focus,
.form-check-radio:focus,
.form-select:focus {
box-shadow: 0 0 0 0.25rem hsla(var(--pngx-primary), var(--pngx-primary-lightness), var(--ngx-focus-alpha));
}
.form-switch .form-check-input:focus {
background-image: escape-svg(url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'><circle r='3' fill='#bbb'/></svg>"));
}
.nav-link:focus-visible, .nav-item a:focus-visible {
outline: none;
background-color: var(--ngx-bg-darker);
}
a.navbar-brand:focus-visible {
outline: none;
color: var(--ngx-primary-darken-10);
}
.dropdown.show {
> .btn-primary {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
> .btn-outline-primary {
color: var(--bs-body-color) !important;
}
}
a, a:hover, .btn-link, .btn-link:hover {
color: var(--bs-primary);
}
.form-control:not(.btn),
input,
select,
textarea,
.form-select:not(.is-invalid):not(:disabled),
.form-check-input {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
&:focus {
background-color: var(--ngx-bg-darker);
color: var(--bs-body-color) !important;
}
}
.form-check-input:checked {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
.form-check-input:focus {
border-color: var(--bs-primary);
}
.page-link {
color: var(--bs-secondary);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color) !important;
&:hover, &:focus {
background-color: var(--bs-primary) !important;
color: var(--bs-light) !important;
}
}
.page-item.active .page-link {
background-color: var(--bs-primary);
border-color: var(--bs-primary) !important;
color: var(--bs-light);
}
.page-item.disabled .page-link {
background-color: var(--ngx-bg-darker);
}
.nav-tabs {
border-bottom: 1px solid var(--bs-border-color);
.nav-link {
color: var(--bs-primary);
&.active, &:hover {
border-color: var(--bs-border-color);
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-bottom: 1px solid transparent;
}
&:focus {
border-color: var(--bs-border-color);
}
&.active:focus, &:active {
border-bottom: 1px solid transparent;
}
}
}
.ng-select-container,
.ng-select.ng-select-opened > .ng-select-container,
.ng-dropdown-panel,
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
background-color: var(--bs-body-bg) !important;
color: var(--bs-body-color) !important;
border-color: var(--bs-border-color) !important;
input:focus {
background-color: transparent !important;
}
}
.input-group-text {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
}
.list-group-item {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
&:hover, &:focus {
background-color: var(--bs-body-bg);
}
}
.dropdown-menu {
background-color: var(--bs-body-bg);
.dropdown-divider {
border-color: var(--bs-border-color);
}
.dropdown-item {
color: var(--bs-body-color);
&:hover {
background-color: var(--ngx-bg-darker);
color: var(--bs-body-color);
}
&.active {
background-color: var(--bs-primary);
}
}
}
table.table {
color: var(--bs-body-color);
.des,.asc {
background-color: var(--bs-body-bg) !important;
}
}
.close {
color: var(--bs-body-color);
}
.modal .btn-close {
color: var(--bs-body-color);
}
.ngx-file-drop__drop-zone--over {
background-color: var(--ngx-primary-faded) !important;
}
.alert-danger {
color: var(--bs-body-color);
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.alert-secondary {
background-color: var(--ngx-primary-darken-18);
border-color: var(--ngx-primary-darken-15);
color: var(--bs-body-color);
}
.ngb-dp-header,
.ngb-dp-weekdays,
.ngb-dp-month {
background-color: var(--bs-body-bg);
}
.popover {
.popover-header,
.popover-body {
background-color: var(--ngx-bg-alt);
border-color: var(--bs-border-color);
}
}
// fix popover carat colors
.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] {
border-left-color: var(--ngx-bg-alt);
}
.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="right"] {
border-right-color: var(--ngx-bg-alt);
}
.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="top"] {
border-top-color: var(--ngx-bg-alt);
}
.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="bottom"] {
border-bottom-color: var(--ngx-bg-alt);
}
.bs-popover-bottom .popover-header::before,
.bs-popover-auto[x-placement^=bottom] .popover-header::before {
border-bottom-color: var(--ngx-bg-alt);
--pngx-primary-faded: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 72%));
--pngx-primary-lighten-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%));
--pngx-primary-lighten-30: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 30%));
--pngx-primary-darken-10: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 10%));
--pngx-primary-darken-15: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 15%));
--pngx-primary-darken-18: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) - 18%));
--pngx-bg-darker: var(--bs-gray-100);
--pngx-focus-alpha: 0.3;
}

View File

@@ -13,10 +13,6 @@ $text-color-dark-mode: #abb2bf;
$text-color-dark-mode-accent: lighten($text-color-dark-mode, 10%);
$border-color-dark-mode: #47494f;
* {
transition: background-color 0.3s ease, border-color 0.3s ease;
}
@mixin dark-mode {
--bs-primary: hsl(var(--pngx-primary), calc(var(--pngx-primary-lightness) + 10%));
--bs-danger: #{$danger-dark-mode};
@@ -27,11 +23,12 @@ $border-color-dark-mode: #47494f;
--bs-light: #{$bg-light-dark-mode};
--bs-light-rgb: #{$bg-light-dark-mode-rgb};
--bs-border-color: #{$border-color-dark-mode};
--ngx-bg-darker: #{$bg-dark-mode-accent};
--ngx-bg-alt: #{$bg-dark-mode-alt};
--ngx-body-color-accent: #{$text-color-dark-mode-accent};
--ngx-focus-alpha: 0.7;
--ngx-primary-faded: var(--ngx-primary-darken-15);
--pngx-bg-darker: #{$bg-dark-mode-accent};
--pngx-bg-alt: #{$bg-dark-mode-alt};
--pngx-body-color-accent: #{$text-color-dark-mode-accent};
--pngx-focus-alpha: 0.7;
--pngx-primary-faded: var(--pngx-primary-darken-15);
--pngx-primary-text-contrast: var(--bs-body-color);
.navbar.bg-primary{
--bs-primary: hsl(var(--pngx-primary),var(--pngx-primary-lightness));
@@ -64,17 +61,23 @@ $border-color-dark-mode: #47494f;
.btn-outline-primary, .btn-primary {
&:hover, &:focus, &.active, &:active {
color: var(--ngx-body-color-accent) !important;
color: var(--bs-light) !important;
}
}
.btn-outline-secondary {
&:hover, &:focus, &.active, &:active {
background-color: var(--ngx-bg-darker);
background-color: var(--pngx-bg-darker);
color: var(--bs-primary);
}
}
.search-form-container {
input, input:focus {
color: var(--bs-body-color) !important;
}
}
.card {
background-color: var(--bs-body-bg);
@@ -141,11 +144,11 @@ $border-color-dark-mode: #47494f;
color: $text-color-dark-mode-accent;
}
.close, .modal .btn-close {
.close, .modal .btn-close, .alert .btn-close {
text-shadow: 0 1px 0 #666;
}
.modal .btn-close {
.modal .btn-close, .alert .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}

View File

@@ -3,7 +3,9 @@ import os
from pathlib import Path
from pathlib import PurePath
from threading import Thread
from time import monotonic
from time import sleep
from typing import Final
from django.conf import settings
from django.core.management.base import BaseCommand
@@ -53,6 +55,25 @@ def _consume(filepath):
logger.warning(f"Not consuming file {filepath}: Unknown file extension.")
return
# Total wait time: up to 500ms
os_error_retry_count: Final[int] = 50
os_error_retry_wait: Final[float] = 0.01
read_try_count = 0
file_open_ok = False
while (read_try_count < os_error_retry_count) and not file_open_ok:
try:
with open(filepath, "rb"):
file_open_ok = True
except OSError:
read_try_count += 1
sleep(os_error_retry_wait)
if read_try_count >= os_error_retry_count:
logger.warning(f"Not consuming file {filepath}: OS reports file as busy still")
return
tag_ids = None
try:
if settings.CONSUMER_SUBDIRS_AS_TAGS:
@@ -81,19 +102,23 @@ def _consume_wait_unmodified(file):
logger.debug(f"Waiting for file {file} to remain unmodified")
mtime = -1
size = -1
current_try = 0
while current_try < settings.CONSUMER_POLLING_RETRY_COUNT:
try:
new_mtime = os.stat(file).st_mtime
stat_data = os.stat(file)
new_mtime = stat_data.st_mtime
new_size = stat_data.st_size
except FileNotFoundError:
logger.debug(
f"File {file} moved while waiting for it to remain " f"unmodified.",
)
return
if new_mtime == mtime:
if new_mtime == mtime and new_size == size:
_consume(file)
return
mtime = new_mtime
size = new_size
sleep(settings.CONSUMER_POLLING_DELAY)
current_try += 1
@@ -182,14 +207,32 @@ class Command(BaseCommand):
descriptor = inotify.add_watch(directory, inotify_flags)
try:
inotify_debounce: Final[float] = 0.5
notified_files = {}
while not self.stop_flag:
for event in inotify.read(timeout=1000):
if recursive:
path = inotify.get_path(event.wd)
else:
path = directory
filepath = os.path.join(path, event.name)
_consume(filepath)
notified_files[filepath] = monotonic()
# Check the files against the timeout
still_waiting = {}
for filepath in notified_files:
# Time of the last inotify event for this file
last_event_time = notified_files[filepath]
if (monotonic() - last_event_time) > inotify_debounce:
_consume(filepath)
else:
still_waiting[filepath] = last_event_time
# These files are still waiting to hit the timeout
notified_files = still_waiting
except KeyboardInterrupt:
pass

View File

@@ -60,7 +60,7 @@ def match_tags(document, classifier):
def matches(matching_model, document):
search_kwargs = {}
document_content = document.content.lower()
document_content = document.content
# Check that match is not empty
if matching_model.match.strip() == "":

View File

@@ -0,0 +1,20 @@
# Generated by Django 4.0.3 on 2022-04-01 22:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("documents", "1017_alter_savedviewfilterrule_rule_type"),
]
operations = [
migrations.AlterField(
model_name="savedviewfilterrule",
name="value",
field=models.CharField(
blank=True, max_length=255, null=True, verbose_name="value"
),
),
]

View File

@@ -375,7 +375,7 @@ class SavedViewFilterRule(models.Model):
rule_type = models.PositiveIntegerField(_("rule type"), choices=RULE_TYPES)
value = models.CharField(_("value"), max_length=128, blank=True, null=True)
value = models.CharField(_("value"), max_length=255, blank=True, null=True)
class Meta:
verbose_name = _("filter rule")

View File

@@ -23,6 +23,7 @@ from documents.signals import document_consumer_declaration
# - XX. MONTH ZZZZ with XX being 1 or 2 and ZZZZ being 2 or 4 digits
# - MONTH ZZZZ, with ZZZZ being 4 digits
# - MONTH XX, ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits
# - XX MON ZZZZ with XX being 1 or 2 and ZZZZ being 4 digits. MONTH is 3 letters
# TODO: isnt there a date parsing library for this?
@@ -31,7 +32,8 @@ DATE_REGEX = re.compile(
r"(\b|(?!=([_-])))([0-9]{4}|[0-9]{2})[\.\/-]([0-9]{1,2})[\.\/-]([0-9]{1,2})(\b|(?=([_-])))|" # noqa: E501
r"(\b|(?!=([_-])))([0-9]{1,2}[\. ]+[^ ]{3,9} ([0-9]{4}|[0-9]{2}))(\b|(?=([_-])))|" # noqa: E501
r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{1,2}, ([0-9]{4}))(\b|(?=([_-])))|"
r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{4})(\b|(?=([_-])))",
r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{4})(\b|(?=([_-])))|"
r"(\b|(?!=([_-])))(\b[0-9]{1,2}[ \.\/-][A-Z]{3}[ \.\/-][0-9]{4})(\b|(?=([_-])))", # noqa: E501
)

View File

@@ -1,6 +1,12 @@
import logging
import os
import shutil
import tempfile
from typing import List # for type hinting. Can be removed, if only Python >3.8 is used
import tqdm
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.conf import settings
from django.db.models.signals import post_save
from documents import index
@@ -14,8 +20,12 @@ from documents.models import Document
from documents.models import DocumentType
from documents.models import Tag
from documents.sanity_checker import SanityCheckFailedException
from pdf2image import convert_from_path
from pikepdf import Pdf
from pyzbar import pyzbar
from whoosh.writing import AsyncWriter
logger = logging.getLogger("paperless.tasks")
@@ -62,6 +72,115 @@ def train_classifier():
logger.warning("Classifier error: " + str(e))
def barcode_reader(image) -> List[str]:
"""
Read any barcodes contained in image
Returns a list containing all found barcodes
"""
barcodes = []
# Decode the barcode image
detected_barcodes = pyzbar.decode(image)
if detected_barcodes:
# Traverse through all the detected barcodes in image
for barcode in detected_barcodes:
if barcode.data:
decoded_barcode = barcode.data.decode("utf-8")
barcodes.append(decoded_barcode)
logger.debug(
f"Barcode of type {str(barcode.type)} found: {decoded_barcode}",
)
return barcodes
def scan_file_for_separating_barcodes(filepath: str) -> List[int]:
"""
Scan the provided file for page separating barcodes
Returns a list of pagenumbers, which separate the file
"""
separator_page_numbers = []
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
# use a temporary directory in case the file os too big to handle in memory
with tempfile.TemporaryDirectory() as path:
pages_from_path = convert_from_path(filepath, output_folder=path)
for current_page_number, page in enumerate(pages_from_path):
current_barcodes = barcode_reader(page)
if separator_barcode in current_barcodes:
separator_page_numbers.append(current_page_number)
return separator_page_numbers
def separate_pages(filepath: str, pages_to_split_on: List[int]) -> List[str]:
"""
Separate the provided file on the pages_to_split_on.
The pages which are defined by page_numbers will be removed.
Returns a list of (temporary) filepaths to consume.
These will need to be deleted later.
"""
os.makedirs(settings.SCRATCH_DIR, exist_ok=True)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
fname = os.path.splitext(os.path.basename(filepath))[0]
pdf = Pdf.open(filepath)
document_paths = []
logger.debug(f"Temp dir is {str(tempdir)}")
if not pages_to_split_on:
logger.warning("No pages to split on!")
else:
# go from the first page to the first separator page
dst = Pdf.new()
for n, page in enumerate(pdf.pages):
if n < pages_to_split_on[0]:
dst.pages.append(page)
output_filename = "{}_document_0.pdf".format(fname)
savepath = os.path.join(tempdir, output_filename)
with open(savepath, "wb") as out:
dst.save(out)
document_paths = [savepath]
# iterate through the rest of the document
for count, page_number in enumerate(pages_to_split_on):
logger.debug(f"Count: {str(count)} page_number: {str(page_number)}")
dst = Pdf.new()
try:
next_page = pages_to_split_on[count + 1]
except IndexError:
next_page = len(pdf.pages)
# skip the first page_number. This contains the barcode page
for page in range(page_number + 1, next_page):
logger.debug(
f"page_number: {str(page_number)} next_page: {str(next_page)}",
)
dst.pages.append(pdf.pages[page])
output_filename = "{}_document_{}.pdf".format(fname, str(count + 1))
logger.debug(f"pdf no:{str(count)} has {str(len(dst.pages))} pages")
savepath = os.path.join(tempdir, output_filename)
with open(savepath, "wb") as out:
dst.save(out)
document_paths.append(savepath)
logger.debug(f"Temp files are {str(document_paths)}")
return document_paths
def save_to_dir(
filepath: str,
newname: str = None,
target_dir: str = settings.CONSUMPTION_DIR,
):
"""
Copies filepath to target_dir.
Optionally rename the file.
"""
if os.path.isfile(filepath) and os.path.isdir(target_dir):
dst = shutil.copy(filepath, target_dir)
logging.debug(f"saved {str(filepath)} to {str(dst)}")
if newname:
dst_new = os.path.join(target_dir, newname)
logger.debug(f"moving {str(dst)} to {str(dst_new)}")
os.rename(dst, dst_new)
else:
logger.warning(f"{str(filepath)} or {str(target_dir)} don't exist.")
def consume_file(
path,
override_filename=None,
@@ -72,6 +191,48 @@ def consume_file(
task_id=None,
):
# check for separators in current document
if settings.CONSUMER_ENABLE_BARCODES:
separators = []
document_list = []
separators = scan_file_for_separating_barcodes(path)
if separators:
logger.debug(f"Pages with separators found in: {str(path)}")
document_list = separate_pages(path, separators)
if document_list:
for n, document in enumerate(document_list):
# save to consumption dir
# rename it to the original filename with number prefix
if override_filename:
newname = f"{str(n)}_" + override_filename
else:
newname = None
save_to_dir(document, newname=newname)
# if we got here, the document was successfully split
# and can safely be deleted
logger.debug("Deleting file {}".format(path))
os.unlink(path)
# notify the sender, otherwise the progress bar
# in the UI stays stuck
payload = {
"filename": override_filename,
"task_id": task_id,
"current_progress": 100,
"max_progress": 100,
"status": "SUCCESS",
"message": "finished",
}
try:
async_to_sync(get_channel_layer().group_send)(
"status_updates",
{"type": "status_update", "data": payload},
)
except OSError as e:
logger.warning("OSError. It could be, the broker cannot be reached.")
logger.warning(str(e))
return "File successfully split"
# continue with consumption if no barcode was found
document = Consumer().try_consume_file(
path,
override_filename=override_filename,

Binary file not shown.

After

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 891 B

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

@@ -100,6 +100,57 @@ class TestDate(TestCase):
datetime.datetime(2020, 3, 1, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_10(self):
text = "Customer Number Currency 22-MAR-2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_11(self):
text = "Customer Number Currency 22 MAR 2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_12(self):
text = "Customer Number Currency 22/MAR/2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_13(self):
text = "Customer Number Currency 22.MAR.2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_14(self):
text = "Customer Number Currency 22.MAR 2022 Credit Card 1934829304"
self.assertEqual(
parse_date("", text),
datetime.datetime(2022, 3, 22, 0, 0, tzinfo=tz.gettz(settings.TIME_ZONE)),
)
def test_date_format_15(self):
text = "Customer Number Currency 22.MAR.22 Credit Card 1934829304"
self.assertIsNone(parse_date("", text), None)
def test_date_format_16(self):
text = "Customer Number Currency 22.MAR,22 Credit Card 1934829304"
self.assertIsNone(parse_date("", text), None)
def test_date_format_17(self):
text = "Customer Number Currency 22,MAR,2022 Credit Card 1934829304"
self.assertIsNone(parse_date("", text), None)
def test_date_format_18(self):
text = "Customer Number Currency 22 MAR,2022 Credit Card 1934829304"
self.assertIsNone(parse_date("", text), None)
def test_crazy_date_past(self, *args):
self.assertIsNone(parse_date("", "01-07-0590 00:00:00"))

View File

@@ -260,6 +260,21 @@ class TestConsumer(DirectoriesMixin, ConsumerMixin, TransactionTestCase):
f'_is_ignored("{file_path}") != {expected_ignored}',
)
@mock.patch("documents.management.commands.document_consumer.open")
def test_consume_file_busy(self, open_mock):
# Calling this mock always raises this
open_mock.side_effect = OSError
self.t_start()
f = os.path.join(self.dirs.consumption_dir, "my_file.pdf")
shutil.copy(self.sample_file, f)
self.wait_for_task_mock_call()
self.task_mock.assert_not_called()
@override_settings(
CONSUMER_POLLING=1,

View File

@@ -1,6 +1,7 @@
import shutil
import tempfile
from random import randint
from typing import Iterable
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
@@ -15,27 +16,37 @@ from ..models import Tag
from ..signals import document_consumption_finished
class TestMatching(TestCase):
def _test_matching(self, text, algorithm, true, false):
class _TestMatchingBase(TestCase):
def _test_matching(
self,
match_text: str,
match_algorithm: str,
should_match: Iterable[str],
no_match: Iterable[str],
case_sensitive: bool = False,
):
for klass in (Tag, Correspondent, DocumentType):
instance = klass.objects.create(
name=str(randint(10000, 99999)),
match=text,
matching_algorithm=getattr(klass, algorithm),
match=match_text,
matching_algorithm=getattr(klass, match_algorithm),
is_insensitive=not case_sensitive,
)
for string in true:
for string in should_match:
doc = Document(content=string)
self.assertTrue(
matching.matches(instance, doc),
'"%s" should match "%s" but it does not' % (text, string),
'"%s" should match "%s" but it does not' % (match_text, string),
)
for string in false:
for string in no_match:
doc = Document(content=string)
self.assertFalse(
matching.matches(instance, doc),
'"%s" should not match "%s" but it does' % (text, string),
'"%s" should not match "%s" but it does' % (match_text, string),
)
class TestMatching(_TestMatchingBase):
def test_match_all(self):
self._test_matching(
@@ -202,6 +213,169 @@ class TestMatching(TestCase):
)
class TestCaseSensitiveMatching(_TestMatchingBase):
def test_match_all(self):
self._test_matching(
"alpha charlie gamma",
"MATCH_ALL",
(
"I have alpha, charlie, and gamma in me",
"I have gamma, charlie, and alpha in me",
),
(
"I have Alpha, charlie, and gamma in me",
"I have gamma, Charlie, and alpha in me",
"I have alpha, charlie, and Gamma in me",
"I have gamma, charlie, and ALPHA in me",
),
case_sensitive=True,
)
self._test_matching(
"Alpha charlie Gamma",
"MATCH_ALL",
(
"I have Alpha, charlie, and Gamma in me",
"I have Gamma, charlie, and Alpha in me",
),
(
"I have Alpha, charlie, and gamma in me",
"I have gamma, charlie, and alpha in me",
"I have alpha, charlie, and Gamma in me",
"I have Gamma, Charlie, and ALPHA in me",
),
case_sensitive=True,
)
self._test_matching(
'brown fox "lazy dogs"',
"MATCH_ALL",
(
"the quick brown fox jumped over the lazy dogs",
"the quick brown fox jumped over the lazy dogs",
),
(
"the quick Brown fox jumped over the lazy dogs",
"the quick brown Fox jumped over the lazy dogs",
"the quick brown fox jumped over the Lazy dogs",
"the quick brown fox jumped over the lazy Dogs",
),
case_sensitive=True,
)
def test_match_any(self):
self._test_matching(
"alpha charlie gamma",
"MATCH_ANY",
(
"I have alpha in me",
"I have charlie in me",
"I have gamma in me",
"I have alpha, charlie, and gamma in me",
"I have alpha and charlie in me",
),
(
"I have Alpha in me",
"I have chaRLie in me",
"I have gamMA in me",
"I have aLPha, cHArlie, and gAMma in me",
"I have AlphA and CharlIe in me",
),
case_sensitive=True,
)
self._test_matching(
"Alpha Charlie Gamma",
"MATCH_ANY",
(
"I have Alpha in me",
"I have Charlie in me",
"I have Gamma in me",
"I have Alpha, Charlie, and Gamma in me",
"I have Alpha and Charlie in me",
),
(
"I have alpha in me",
"I have ChaRLie in me",
"I have GamMA in me",
"I have ALPha, CHArlie, and GAMma in me",
"I have AlphA and CharlIe in me",
),
case_sensitive=True,
)
self._test_matching(
'"brown fox" " lazy dogs "',
"MATCH_ANY",
(
"the quick brown fox",
"jumped over the lazy dogs.",
),
(
"the quick Brown fox",
"jumped over the lazy Dogs.",
),
case_sensitive=True,
)
def test_match_literal(self):
self._test_matching(
"alpha charlie gamma",
"MATCH_LITERAL",
("I have 'alpha charlie gamma' in me",),
(
"I have 'Alpha charlie gamma' in me",
"I have 'alpha Charlie gamma' in me",
"I have 'alpha charlie Gamma' in me",
"I have 'Alpha Charlie Gamma' in me",
),
case_sensitive=True,
)
self._test_matching(
"Alpha Charlie Gamma",
"MATCH_LITERAL",
("I have 'Alpha Charlie Gamma' in me",),
(
"I have 'Alpha charlie gamma' in me",
"I have 'alpha Charlie gamma' in me",
"I have 'alpha charlie Gamma' in me",
"I have 'alpha charlie gamma' in me",
),
case_sensitive=True,
)
def test_match_regex(self):
self._test_matching(
r"alpha\w+gamma",
"MATCH_REGEX",
(
"I have alpha_and_gamma in me",
"I have alphas_and_gamma in me",
),
(
"I have Alpha_and_Gamma in me",
"I have alpHAs_and_gaMMa in me",
),
case_sensitive=True,
)
self._test_matching(
r"Alpha\w+gamma",
"MATCH_REGEX",
(
"I have Alpha_and_gamma in me",
"I have Alphas_and_gamma in me",
),
(
"I have Alpha_and_Gamma in me",
"I have alphas_and_gamma in me",
),
case_sensitive=True,
)
@override_settings(POST_CONSUME_SCRIPT=None)
class TestDocumentConsumptionFinishedSignal(TestCase):
"""

View File

@@ -1,7 +1,10 @@
import os
import shutil
import tempfile
from unittest import mock
from django.conf import settings
from django.test import override_settings
from django.test import TestCase
from django.utils import timezone
from documents import tasks
@@ -12,6 +15,7 @@ from documents.models import Tag
from documents.sanity_checker import SanityCheckFailedException
from documents.sanity_checker import SanityCheckMessages
from documents.tests.utils import DirectoriesMixin
from PIL import Image
class TestTasks(DirectoriesMixin, TestCase):
@@ -89,6 +93,318 @@ class TestTasks(DirectoriesMixin, TestCase):
mtime3 = os.stat(settings.MODEL_FILE).st_mtime
self.assertNotEqual(mtime2, mtime3)
def test_barcode_reader(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader2(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pbm",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_distorsion(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT-distorsion.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_distorsion2(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT-distorsion2.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_unreadable(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-PATCHT-unreadable.png",
)
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), [])
def test_barcode_reader_qr(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"qr-code-PATCHT.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_128(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-128-PATCHT.png",
)
img = Image.open(test_file)
separator_barcode = str(settings.CONSUMER_BARCODE_STRING)
self.assertEqual(tasks.barcode_reader(img), [separator_barcode])
def test_barcode_reader_no_barcode(self):
test_file = os.path.join(os.path.dirname(__file__), "samples", "simple.png")
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), [])
def test_barcode_reader_custom_separator(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-custom.png",
)
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), ["CUSTOM BARCODE"])
def test_barcode_reader_custom_qr_separator(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-qr-custom.png",
)
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), ["CUSTOM BARCODE"])
def test_barcode_reader_custom_128_separator(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-128-custom.png",
)
img = Image.open(test_file)
self.assertEqual(tasks.barcode_reader(img), ["CUSTOM BARCODE"])
def test_scan_file_for_separating_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
def test_scan_file_for_separating_barcodes2(self):
test_file = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [])
def test_scan_file_for_separating_barcodes3(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [1])
def test_scan_file_for_separating_barcodes4(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"several-patcht-codes.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [2, 5])
def test_scan_file_for_separating_barcodes_upsidedown(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle_reverse.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [1])
def test_scan_file_for_separating_qr_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-qr.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-custom.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_qr_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-qr-custom.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
@override_settings(CONSUMER_BARCODE_STRING="CUSTOM BARCODE")
def test_scan_file_for_separating_custom_128_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-128-custom.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [0])
def test_scan_file_for_separating_wrong_qr_barcodes(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"barcode-39-custom.pdf",
)
pages = tasks.scan_file_for_separating_barcodes(test_file)
self.assertEqual(pages, [])
def test_separate_pages(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
pages = tasks.separate_pages(test_file, [1])
self.assertEqual(len(pages), 2)
def test_separate_pages_no_list(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
with self.assertLogs("paperless.tasks", level="WARNING") as cm:
pages = tasks.separate_pages(test_file, [])
self.assertEqual(pages, [])
self.assertEqual(
cm.output,
[
f"WARNING:paperless.tasks:No pages to split on!",
],
)
def test_save_to_dir(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pdf",
)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
tasks.save_to_dir(test_file, target_dir=tempdir)
target_file = os.path.join(tempdir, "patch-code-t.pdf")
self.assertTrue(os.path.isfile(target_file))
def test_save_to_dir2(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pdf",
)
nonexistingdir = "/nowhere"
if os.path.isdir(nonexistingdir):
self.fail("non-existing dir exists")
else:
with self.assertLogs("paperless.tasks", level="WARNING") as cm:
tasks.save_to_dir(test_file, target_dir=nonexistingdir)
self.assertEqual(
cm.output,
[
f"WARNING:paperless.tasks:{str(test_file)} or {str(nonexistingdir)} don't exist.",
],
)
def test_save_to_dir3(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t.pdf",
)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
tasks.save_to_dir(test_file, newname="newname.pdf", target_dir=tempdir)
target_file = os.path.join(tempdir, "newname.pdf")
self.assertTrue(os.path.isfile(target_file))
def test_barcode_splitter(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
tempdir = tempfile.mkdtemp(prefix="paperless-", dir=settings.SCRATCH_DIR)
separators = tasks.scan_file_for_separating_barcodes(test_file)
self.assertTrue(separators)
document_list = tasks.separate_pages(test_file, separators)
self.assertTrue(document_list)
for document in document_list:
tasks.save_to_dir(document, target_dir=tempdir)
target_file1 = os.path.join(tempdir, "patch-code-t-middle_document_0.pdf")
target_file2 = os.path.join(tempdir, "patch-code-t-middle_document_1.pdf")
self.assertTrue(os.path.isfile(target_file1))
self.assertTrue(os.path.isfile(target_file2))
@override_settings(CONSUMER_ENABLE_BARCODES=True)
def test_consume_barcode_file(self):
test_file = os.path.join(
os.path.dirname(__file__),
"samples",
"barcodes",
"patch-code-t-middle.pdf",
)
dst = os.path.join(settings.SCRATCH_DIR, "patch-code-t-middle.pd")
shutil.copy(test_file, dst)
self.assertEqual(tasks.consume_file(dst), "File successfully split")
@mock.patch("documents.tasks.sanity_checker.check_sanity")
def test_sanity_check_success(self, m):
m.return_value = SanityCheckMessages()

View File

@@ -1,12 +1,14 @@
import json
import logging
import os
import tempfile
import urllib
import uuid
import zipfile
from datetime import datetime
from time import mktime
from unicodedata import normalize
from urllib.parse import quote_plus
from urllib.parse import quote
from django.conf import settings
from django.db.models import Case
@@ -24,6 +26,8 @@ from django.views.decorators.cache import cache_control
from django.views.generic import TemplateView
from django_filters.rest_framework import DjangoFilterBackend
from django_q.tasks import async_task
from packaging import version as packaging_version
from paperless import version
from paperless.db import GnuPG
from paperless.views import StandardPagination
from rest_framework import parsers
@@ -244,7 +248,7 @@ class DocumentViewSet(
# RFC 5987 addresses this issue
# see https://datatracker.ietf.org/doc/html/rfc5987#section-4.2
filename_normalized = normalize("NFKD", filename).encode("ascii", "ignore")
filename_encoded = quote_plus(filename)
filename_encoded = quote(filename)
content_disposition = (
f"{disposition}; "
f'filename="{filename_normalized}"; '
@@ -666,3 +670,40 @@ class BulkDownloadView(GenericAPIView):
)
return response
class RemoteVersionView(GenericAPIView):
def get(self, request, format=None):
remote_version = "0.0.0"
is_greater_than_current = False
# TODO: this can likely be removed when frontend settings are saved to DB
feature_is_set = settings.ENABLE_UPDATE_CHECK != "default"
if feature_is_set and settings.ENABLE_UPDATE_CHECK:
try:
with urllib.request.urlopen(
"https://api.github.com/repos/"
+ "paperless-ngx/paperless-ngx/releases/latest",
) as response:
remote = response.read().decode("utf-8")
try:
remote_json = json.loads(remote)
remote_version = remote_json["tag_name"].replace("ngx-", "")
except ValueError:
logger.debug("An error occured parsing remote version json")
except urllib.error.URLError:
logger.debug("An error occured checking for available updates")
current_version = ".".join([str(_) for _ in version.__version__[:3]])
is_greater_than_current = packaging_version.parse(
remote_version,
) > packaging_version.parse(
current_version,
)
return Response(
{
"version": remote_version,
"update_available": is_greater_than_current,
"feature_is_set": feature_is_set,
},
)

View File

@@ -3,6 +3,8 @@ import math
import multiprocessing
import os
import re
from typing import Final
from urllib.parse import urlparse
from concurrent_log_handler.queue import setup_logging_queues
from django.utils.translation import gettext_lazy as _
@@ -29,7 +31,7 @@ elif os.path.exists("/usr/local/etc/paperless.conf"):
os.environ["OMP_THREAD_LIMIT"] = "1"
def __get_boolean(key, default="NO"):
def __get_boolean(key: str, default: str = "NO") -> bool:
"""
Return a boolean value based on whatever the user has supplied in the
environment based on whether the value "looks like" it's True or not.
@@ -37,6 +39,13 @@ def __get_boolean(key, default="NO"):
return bool(os.getenv(key, default).lower() in ("yes", "y", "1", "t", "true"))
def __get_int(key: str, default: int) -> int:
"""
Return an integer value based on the environment variable or a default
"""
return int(os.getenv(key, default))
# NEVER RUN WITH DEBUG IN PRODUCTION.
DEBUG = __get_boolean("PAPERLESS_DEBUG", "NO")
@@ -211,7 +220,15 @@ if DEBUG:
else:
X_FRAME_OPTIONS = "SAMEORIGIN"
# We allow CORS from localhost:8080
# The next 3 settings can also be set using just PAPERLESS_URL
_csrf_origins = os.getenv("PAPERLESS_CSRF_TRUSTED_ORIGINS")
if _csrf_origins:
CSRF_TRUSTED_ORIGINS = _csrf_origins.split(",")
else:
CSRF_TRUSTED_ORIGINS = []
# We allow CORS from localhost:8000
CORS_ALLOWED_ORIGINS = tuple(
os.getenv("PAPERLESS_CORS_ALLOWED_HOSTS", "http://localhost:8000").split(","),
)
@@ -220,6 +237,22 @@ if DEBUG:
# Allow access from the angular development server during debugging
CORS_ALLOWED_ORIGINS += ("http://localhost:4200",)
_allowed_hosts = os.getenv("PAPERLESS_ALLOWED_HOSTS")
if _allowed_hosts:
ALLOWED_HOSTS = _allowed_hosts.split(",")
else:
ALLOWED_HOSTS = ["*"]
_paperless_url = os.getenv("PAPERLESS_URL")
if _paperless_url:
_paperless_uri = urlparse(_paperless_url)
CSRF_TRUSTED_ORIGINS.append(_paperless_url)
CORS_ALLOWED_ORIGINS += (_paperless_url,)
if _allowed_hosts:
ALLOWED_HOSTS.append(_paperless_uri.hostname)
else:
ALLOWED_HOSTS = [_paperless_uri.hostname]
# The secret key has a default that should be fine so long as you're hosting
# Paperless on a closed network. However, if you're putting this anywhere
# public, you should change the key to something unique and verbose.
@@ -228,12 +261,6 @@ SECRET_KEY = os.getenv(
"e11fl1oa-*ytql8p)(06fbj4ukrlo+n7k&q5+$1md7i+mge=ee",
)
_allowed_hosts = os.getenv("PAPERLESS_ALLOWED_HOSTS")
if _allowed_hosts:
ALLOWED_HOSTS = _allowed_hosts.split(",")
else:
ALLOWED_HOSTS = ["*"]
AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
@@ -396,7 +423,7 @@ LOGGING = {
# in total.
def default_task_workers():
def default_task_workers() -> int:
# always leave one core open
available_cores = max(multiprocessing.cpu_count(), 1)
try:
@@ -407,20 +434,29 @@ def default_task_workers():
return 1
TASK_WORKERS = int(os.getenv("PAPERLESS_TASK_WORKERS", default_task_workers()))
TASK_WORKERS = __get_int("PAPERLESS_TASK_WORKERS", default_task_workers())
PAPERLESS_WORKER_TIMEOUT: Final[int] = __get_int("PAPERLESS_WORKER_TIMEOUT", 1800)
# Per django-q docs, timeout must be smaller than retry
# We default retry to 10s more than the timeout
PAPERLESS_WORKER_RETRY: Final[int] = __get_int(
"PAPERLESS_WORKER_RETRY",
PAPERLESS_WORKER_TIMEOUT + 10,
)
Q_CLUSTER = {
"name": "paperless",
"catch_up": False,
"recycle": 1,
"retry": 1800,
"timeout": int(os.getenv("PAPERLESS_WORKER_TIMEOUT", 1800)),
"retry": PAPERLESS_WORKER_RETRY,
"timeout": PAPERLESS_WORKER_TIMEOUT,
"workers": TASK_WORKERS,
"redis": os.getenv("PAPERLESS_REDIS", "redis://localhost:6379"),
}
def default_threads_per_worker(task_workers):
def default_threads_per_worker(task_workers) -> int:
# always leave one core open
available_cores = max(multiprocessing.cpu_count(), 1)
try:
@@ -455,13 +491,19 @@ CONSUMER_IGNORE_PATTERNS = list(
json.loads(
os.getenv(
"PAPERLESS_CONSUMER_IGNORE_PATTERNS",
'[".DS_STORE/*", "._*", ".stfolder/*"]',
'[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]',
),
),
)
CONSUMER_SUBDIRS_AS_TAGS = __get_boolean("PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS")
CONSUMER_ENABLE_BARCODES = __get_boolean(
"PAPERLESS_CONSUMER_ENABLE_BARCODES",
)
CONSUMER_BARCODE_STRING = os.getenv("PAPERLESS_CONSUMER_BARCODE_STRING", "PATCHT")
OPTIMIZE_THUMBNAILS = __get_boolean("PAPERLESS_OPTIMIZE_THUMBNAILS", "true")
OCR_PAGES = int(os.getenv("PAPERLESS_OCR_PAGES", 0))
@@ -566,3 +608,7 @@ if os.getenv("PAPERLESS_IGNORE_DATES", ""):
d = dateparser.parse(s)
if d:
IGNORE_DATES.add(d.date())
ENABLE_UPDATE_CHECK = os.getenv("PAPERLESS_ENABLE_UPDATE_CHECK", "default")
if ENABLE_UPDATE_CHECK != "default":
ENABLE_UPDATE_CHECK = __get_boolean("PAPERLESS_ENABLE_UPDATE_CHECK")

View File

@@ -14,6 +14,7 @@ from documents.views import DocumentTypeViewSet
from documents.views import IndexView
from documents.views import LogViewSet
from documents.views import PostDocumentView
from documents.views import RemoteVersionView
from documents.views import SavedViewViewSet
from documents.views import SearchAutoCompleteView
from documents.views import SelectionDataView
@@ -72,6 +73,11 @@ urlpatterns = [
BulkDownloadView.as_view(),
name="bulk_download",
),
re_path(
r"^remote_version/",
RemoteVersionView.as_view(),
name="remoteversion",
),
path("token/", views.obtain_auth_token),
]
+ api_router.urls,

View File

@@ -61,13 +61,13 @@ class FlagMailAction(BaseMailAction):
def get_rule_action(rule):
if rule.action == MailRule.ACTION_FLAG:
if rule.action == MailRule.AttachmentAction.FLAG:
return FlagMailAction()
elif rule.action == MailRule.ACTION_DELETE:
elif rule.action == MailRule.AttachmentAction.DELETE:
return DeleteMailAction()
elif rule.action == MailRule.ACTION_MOVE:
elif rule.action == MailRule.AttachmentAction.MOVE:
return MoveMailAction()
elif rule.action == MailRule.ACTION_MARK_READ:
elif rule.action == MailRule.AttachmentAction.MARK_READ:
return MarkReadMailAction()
else:
raise NotImplementedError("Unknown action.") # pragma: nocover
@@ -89,11 +89,11 @@ def make_criterias(rule):
def get_mailbox(server, port, security):
if security == MailAccount.IMAP_SECURITY_NONE:
if security == MailAccount.ImapSecurity.NONE:
mailbox = MailBoxUnencrypted(server, port)
elif security == MailAccount.IMAP_SECURITY_STARTTLS:
elif security == MailAccount.ImapSecurity.STARTTLS:
mailbox = MailBox(server, port, starttls=True)
elif security == MailAccount.IMAP_SECURITY_SSL:
elif security == MailAccount.ImapSecurity.SSL:
mailbox = MailBox(server, port)
else:
raise NotImplementedError("Unknown IMAP security") # pragma: nocover
@@ -112,10 +112,10 @@ class MailAccountHandler(LoggingMixin):
return None
def get_title(self, message, att, rule):
if rule.assign_title_from == MailRule.TITLE_FROM_SUBJECT:
if rule.assign_title_from == MailRule.TitleSource.FROM_SUBJECT:
return message.subject
elif rule.assign_title_from == MailRule.TITLE_FROM_FILENAME:
elif rule.assign_title_from == MailRule.TitleSource.FROM_FILENAME:
return os.path.splitext(os.path.basename(att.filename))[0]
else:
@@ -126,20 +126,20 @@ class MailAccountHandler(LoggingMixin):
def get_correspondent(self, message: MailMessage, rule):
c_from = rule.assign_correspondent_from
if c_from == MailRule.CORRESPONDENT_FROM_NOTHING:
if c_from == MailRule.CorrespondentSource.FROM_NOTHING:
return None
elif c_from == MailRule.CORRESPONDENT_FROM_EMAIL:
elif c_from == MailRule.CorrespondentSource.FROM_EMAIL:
return self._correspondent_from_name(message.from_)
elif c_from == MailRule.CORRESPONDENT_FROM_NAME:
elif c_from == MailRule.CorrespondentSource.FROM_NAME:
from_values = message.from_values
if from_values is not None and len(from_values.name) > 0:
return self._correspondent_from_name(from_values.name)
else:
return self._correspondent_from_name(message.from_)
elif c_from == MailRule.CORRESPONDENT_FROM_CUSTOM:
elif c_from == MailRule.CorrespondentSource.FROM_CUSTOM:
return rule.assign_correspondent
else:
@@ -253,7 +253,7 @@ class MailAccountHandler(LoggingMixin):
return total_processed_files
def handle_message(self, message, rule):
def handle_message(self, message, rule) -> int:
if not message.attachments:
return 0
@@ -274,7 +274,8 @@ class MailAccountHandler(LoggingMixin):
if (
not att.content_disposition == "attachment"
and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY
and rule.attachment_type
== MailRule.AttachmentProcessing.ATTACHMENTS_ONLY
):
self.log(
"debug",
@@ -285,7 +286,12 @@ class MailAccountHandler(LoggingMixin):
continue
if rule.filter_attachment_filename:
if not fnmatch(att.filename, rule.filter_attachment_filename):
# Force the filename and pattern to the lowercase
# as this is system dependent otherwise
if not fnmatch(
att.filename.lower(),
rule.filter_attachment_filename.lower(),
):
continue
title = self.get_title(message, att, rule)

View File

@@ -0,0 +1,37 @@
# Generated by Django 4.0.3 on 2022-03-28 17:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("paperless_mail", "0008_auto_20210516_0940"),
]
operations = [
migrations.AlterField(
model_name="mailrule",
name="action",
field=models.PositiveIntegerField(
choices=[
(1, "Mark as read, don't process read mails"),
(2, "Flag the mail, don't process flagged mails"),
(3, "Move to specified folder"),
(4, "Delete"),
],
default=3,
verbose_name="action",
),
),
migrations.AlterField(
model_name="mailrule",
name="folder",
field=models.CharField(
default="INBOX",
help_text="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server.",
max_length=256,
verbose_name="folder",
),
),
]

View File

@@ -8,15 +8,10 @@ class MailAccount(models.Model):
verbose_name = _("mail account")
verbose_name_plural = _("mail accounts")
IMAP_SECURITY_NONE = 1
IMAP_SECURITY_SSL = 2
IMAP_SECURITY_STARTTLS = 3
IMAP_SECURITY_OPTIONS = (
(IMAP_SECURITY_NONE, _("No encryption")),
(IMAP_SECURITY_SSL, _("Use SSL")),
(IMAP_SECURITY_STARTTLS, _("Use STARTTLS")),
)
class ImapSecurity(models.IntegerChoices):
NONE = 1, _("No encryption")
SSL = 2, _("Use SSL")
STARTTLS = 3, _("Use STARTTLS")
name = models.CharField(_("name"), max_length=256, unique=True)
@@ -34,8 +29,8 @@ class MailAccount(models.Model):
imap_security = models.PositiveIntegerField(
_("IMAP security"),
choices=IMAP_SECURITY_OPTIONS,
default=IMAP_SECURITY_SSL,
choices=ImapSecurity.choices,
default=ImapSecurity.SSL,
)
username = models.CharField(_("username"), max_length=256)
@@ -61,48 +56,25 @@ class MailRule(models.Model):
verbose_name = _("mail rule")
verbose_name_plural = _("mail rules")
ATTACHMENT_TYPE_ATTACHMENTS_ONLY = 1
ATTACHMENT_TYPE_EVERYTHING = 2
class AttachmentProcessing(models.IntegerChoices):
ATTACHMENTS_ONLY = 1, _("Only process attachments.")
EVERYTHING = 2, _("Process all files, including 'inline' " "attachments.")
ATTACHMENT_TYPES = (
(ATTACHMENT_TYPE_ATTACHMENTS_ONLY, _("Only process attachments.")),
(
ATTACHMENT_TYPE_EVERYTHING,
_("Process all files, including 'inline' " "attachments."),
),
)
class AttachmentAction(models.IntegerChoices):
DELETE = 1, _("Mark as read, don't process read mails")
MOVE = 2, _("Flag the mail, don't process flagged mails")
MARK_READ = 3, _("Move to specified folder")
FLAG = 4, _("Delete")
ACTION_DELETE = 1
ACTION_MOVE = 2
ACTION_MARK_READ = 3
ACTION_FLAG = 4
class TitleSource(models.IntegerChoices):
FROM_SUBJECT = 1, _("Use subject as title")
FROM_FILENAME = 2, _("Use attachment filename as title")
ACTIONS = (
(ACTION_MARK_READ, _("Mark as read, don't process read mails")),
(ACTION_FLAG, _("Flag the mail, don't process flagged mails")),
(ACTION_MOVE, _("Move to specified folder")),
(ACTION_DELETE, _("Delete")),
)
TITLE_FROM_SUBJECT = 1
TITLE_FROM_FILENAME = 2
TITLE_SELECTOR = (
(TITLE_FROM_SUBJECT, _("Use subject as title")),
(TITLE_FROM_FILENAME, _("Use attachment filename as title")),
)
CORRESPONDENT_FROM_NOTHING = 1
CORRESPONDENT_FROM_EMAIL = 2
CORRESPONDENT_FROM_NAME = 3
CORRESPONDENT_FROM_CUSTOM = 4
CORRESPONDENT_SELECTOR = (
(CORRESPONDENT_FROM_NOTHING, _("Do not assign a correspondent")),
(CORRESPONDENT_FROM_EMAIL, _("Use mail address")),
(CORRESPONDENT_FROM_NAME, _("Use name (or mail address if not available)")),
(CORRESPONDENT_FROM_CUSTOM, _("Use correspondent selected below")),
)
class CorrespondentSource(models.IntegerChoices):
FROM_NOTHING = 1, _("Do not assign a correspondent")
FROM_EMAIL = 2, _("Use mail address")
FROM_NAME = 3, _("Use name (or mail address if not available)")
FROM_CUSTOM = 4, _("Use correspondent selected below")
name = models.CharField(_("name"), max_length=256, unique=True)
@@ -119,7 +91,10 @@ class MailRule(models.Model):
_("folder"),
default="INBOX",
max_length=256,
help_text=_("Subfolders must be separated by dots."),
help_text=_(
"Subfolders must be separated by a delimiter, often a dot ('.') or"
" slash ('/'), but it varies by mail server.",
),
)
filter_from = models.CharField(
@@ -161,8 +136,8 @@ class MailRule(models.Model):
attachment_type = models.PositiveIntegerField(
_("attachment type"),
choices=ATTACHMENT_TYPES,
default=ATTACHMENT_TYPE_ATTACHMENTS_ONLY,
choices=AttachmentProcessing.choices,
default=AttachmentProcessing.ATTACHMENTS_ONLY,
help_text=_(
"Inline attachments include embedded images, so it's best "
"to combine this option with a filename filter.",
@@ -171,8 +146,8 @@ class MailRule(models.Model):
action = models.PositiveIntegerField(
_("action"),
choices=ACTIONS,
default=ACTION_MARK_READ,
choices=AttachmentAction.choices,
default=AttachmentAction.MARK_READ,
)
action_parameter = models.CharField(
@@ -190,8 +165,8 @@ class MailRule(models.Model):
assign_title_from = models.PositiveIntegerField(
_("assign title from"),
choices=TITLE_SELECTOR,
default=TITLE_FROM_SUBJECT,
choices=TitleSource.choices,
default=TitleSource.FROM_SUBJECT,
)
assign_tag = models.ForeignKey(
@@ -212,8 +187,8 @@ class MailRule(models.Model):
assign_correspondent_from = models.PositiveIntegerField(
_("assign correspondent from"),
choices=CORRESPONDENT_SELECTOR,
default=CORRESPONDENT_FROM_NOTHING,
choices=CorrespondentSource.choices,
default=CorrespondentSource.FROM_NOTHING,
)
assign_correspondent = models.ForeignKey(

View File

@@ -246,13 +246,13 @@ class TestMail(DirectoriesMixin, TestCase):
rule = MailRule(
name="a",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_NOTHING,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_NOTHING,
)
self.assertIsNone(handler.get_correspondent(message, rule))
rule = MailRule(
name="b",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_EMAIL,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_EMAIL,
)
c = handler.get_correspondent(message, rule)
self.assertIsNotNone(c)
@@ -264,7 +264,7 @@ class TestMail(DirectoriesMixin, TestCase):
rule = MailRule(
name="c",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_NAME,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_NAME,
)
c = handler.get_correspondent(message, rule)
self.assertIsNotNone(c)
@@ -275,7 +275,7 @@ class TestMail(DirectoriesMixin, TestCase):
rule = MailRule(
name="d",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_CUSTOM,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_CUSTOM,
assign_correspondent=someone_else,
)
c = handler.get_correspondent(message, rule)
@@ -289,9 +289,15 @@ class TestMail(DirectoriesMixin, TestCase):
handler = MailAccountHandler()
rule = MailRule(name="a", assign_title_from=MailRule.TITLE_FROM_FILENAME)
rule = MailRule(
name="a",
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
)
self.assertEqual(handler.get_title(message, att, rule), "this_is_the_file")
rule = MailRule(name="b", assign_title_from=MailRule.TITLE_FROM_SUBJECT)
rule = MailRule(
name="b",
assign_title_from=MailRule.TitleSource.FROM_SUBJECT,
)
self.assertEqual(handler.get_title(message, att, rule), "the message title")
def test_handle_message(self):
@@ -302,7 +308,10 @@ class TestMail(DirectoriesMixin, TestCase):
)
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account)
rule = MailRule(
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -346,7 +355,10 @@ class TestMail(DirectoriesMixin, TestCase):
)
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account)
rule = MailRule(
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -369,7 +381,10 @@ class TestMail(DirectoriesMixin, TestCase):
)
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account)
rule = MailRule(
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -392,9 +407,9 @@ class TestMail(DirectoriesMixin, TestCase):
account = MailAccount()
rule = MailRule(
assign_title_from=MailRule.TITLE_FROM_FILENAME,
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
attachment_type=MailRule.ATTACHMENT_TYPE_EVERYTHING,
attachment_type=MailRule.AttachmentProcessing.EVERYTHING,
)
result = self.mail_account_handler.handle_message(message, rule)
@@ -409,33 +424,36 @@ class TestMail(DirectoriesMixin, TestCase):
_AttachmentDef(filename="f2.pdf"),
_AttachmentDef(filename="f3.pdf"),
_AttachmentDef(filename="f2.png"),
_AttachmentDef(filename="file.PDf"),
_AttachmentDef(filename="f1.Pdf"),
],
)
tests = [
("*.pdf", ["f1.pdf", "f2.pdf", "f3.pdf"]),
("f1.pdf", ["f1.pdf"]),
("*.pdf", ["f1.pdf", "f1.Pdf", "f2.pdf", "f3.pdf", "file.PDf"]),
("f1.pdf", ["f1.pdf", "f1.Pdf"]),
("f1", []),
("*", ["f1.pdf", "f2.pdf", "f3.pdf", "f2.png"]),
("*", ["f1.pdf", "f2.pdf", "f3.pdf", "f2.png", "f1.Pdf", "file.PDf"]),
("*.png", ["f2.png"]),
]
for (pattern, matches) in tests:
matches.sort()
self.async_task.reset_mock()
account = MailAccount()
rule = MailRule(
assign_title_from=MailRule.TITLE_FROM_FILENAME,
assign_title_from=MailRule.TitleSource.FROM_FILENAME,
account=account,
filter_attachment_filename=pattern,
)
result = self.mail_account_handler.handle_message(message, rule)
self.assertEqual(result, len(matches))
filenames = [
a[1]["override_filename"] for a in self.async_task.call_args_list
]
self.assertCountEqual(filenames, matches)
self.assertEqual(result, len(matches), f"Error with pattern: {pattern}")
filenames = sorted(
[a[1]["override_filename"] for a in self.async_task.call_args_list],
)
self.assertListEqual(filenames, matches)
def test_handle_mail_account_mark_read(self):
@@ -449,7 +467,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MARK_READ,
action=MailRule.AttachmentAction.MARK_READ,
)
self.assertEqual(len(self.bogus_mailbox.messages), 3)
@@ -472,7 +490,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_DELETE,
action=MailRule.AttachmentAction.DELETE,
filter_subject="Invoice",
)
@@ -493,7 +511,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_FLAG,
action=MailRule.AttachmentAction.FLAG,
filter_subject="Invoice",
)
@@ -516,7 +534,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
filter_subject="Claim",
)
@@ -562,7 +580,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
filter_subject="Claim",
)
@@ -583,7 +601,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
filter_subject="Claim",
order=1,
@@ -592,7 +610,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule2",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
filter_subject="Claim",
order=2,
@@ -622,7 +640,7 @@ class TestMail(DirectoriesMixin, TestCase):
_ = MailRule.objects.create(
name="testrule",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
)
@@ -647,9 +665,9 @@ class TestMail(DirectoriesMixin, TestCase):
name="testrule",
filter_from="amazon@amazon.de",
account=account,
action=MailRule.ACTION_MOVE,
action=MailRule.AttachmentAction.MOVE,
action_parameter="spam",
assign_correspondent_from=MailRule.CORRESPONDENT_FROM_EMAIL,
assign_correspondent_from=MailRule.CorrespondentSource.FROM_EMAIL,
)
self.mail_account_handler.handle_mail_account(account)
@@ -684,7 +702,7 @@ class TestMail(DirectoriesMixin, TestCase):
rule = MailRule.objects.create(
name="testrule3",
account=account,
action=MailRule.ACTION_DELETE,
action=MailRule.AttachmentAction.DELETE,
filter_subject="Claim",
)

View File

@@ -7,7 +7,7 @@ max-line-length = 88
[tool:pytest]
DJANGO_SETTINGS_MODULE=paperless.settings
addopts = --pythonwarnings=all --cov --cov-report=html -n auto
addopts = --pythonwarnings=all --cov --cov-report=html --numprocesses auto --quiet
env =
PAPERLESS_DISABLE_DBHANDLER=true