Compare commits
141 Commits
beta-1.6.1
...
beta-1.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
301ad7e07d | ||
|
|
cc93616019 | ||
|
|
88066563d3 | ||
|
|
9e3590352c | ||
|
|
86ad52639f | ||
|
|
c00be946a5 | ||
|
|
322aeeb552 | ||
|
|
501d4cafa9 | ||
|
|
e556fb3e3a | ||
|
|
bb930297d6 | ||
|
|
83f10167e5 | ||
|
|
d47bb21389 | ||
|
|
4dedff00b8 | ||
|
|
2414dad656 | ||
|
|
8f98cb4860 | ||
|
|
6e96b7e00a | ||
|
|
00287b27ab | ||
|
|
8f18b7fd6c | ||
|
|
dde7771dc6 | ||
|
|
4165184e42 | ||
|
|
e4953a756a | ||
|
|
471ac63a3a | ||
|
|
f358eda5c5 | ||
|
|
035130ecdc | ||
|
|
ca0e86757b | ||
|
|
d4153607c9 | ||
|
|
99d0a0845d | ||
|
|
5fae5a9ee0 | ||
|
|
b5a75be1db | ||
|
|
eb5e0e0b9b | ||
|
|
dc90f58391 | ||
|
|
ca43c71cf5 | ||
|
|
931a311c48 | ||
|
|
9f9d7da1ce | ||
|
|
d00e8d3b0f | ||
|
|
d4124bae0c | ||
|
|
58eb2d6f63 | ||
|
|
01987f1b51 | ||
|
|
0a35358e8d | ||
|
|
5bacb85c33 | ||
|
|
6933ac523f | ||
|
|
ba9120b417 | ||
|
|
3e71f5810f | ||
|
|
296a0edae4 | ||
|
|
cdf5602dfb | ||
|
|
e214f719c9 | ||
|
|
08fbcf5158 | ||
|
|
10ca515ac5 | ||
|
|
e59a14852b | ||
|
|
c696b4f2f2 | ||
|
|
553153ba92 | ||
|
|
9d2bcf807e | ||
|
|
422ac9befe | ||
|
|
793f641af6 | ||
|
|
0ea5f5d584 | ||
|
|
c024b846c3 | ||
|
|
37b3fde4e1 | ||
|
|
e89ef5de25 | ||
|
|
06cac44d02 | ||
|
|
488fe28ad3 | ||
|
|
50f474ae92 | ||
|
|
78ca2ffaba | ||
|
|
911f5bc78e | ||
|
|
b227427916 | ||
|
|
b5f77fd6e7 | ||
|
|
4fe966f534 | ||
|
|
bcce0838dd | ||
|
|
76e43bcb89 | ||
|
|
c666be32f4 | ||
|
|
2d850795d8 | ||
|
|
784982718e | ||
|
|
2f8d263c9c | ||
|
|
b214163af3 | ||
|
|
a4fe1000c2 | ||
|
|
9a275fa4ed | ||
|
|
f2c83f51de | ||
|
|
fb76b72787 | ||
|
|
bec6c4511c | ||
|
|
77b9988d05 | ||
|
|
3e49f93816 | ||
|
|
32f6932faf | ||
|
|
db76e1d65f | ||
|
|
0136ba504b | ||
|
|
459e026f16 | ||
|
|
b03a723c3e | ||
|
|
7562636151 | ||
|
|
3acc65ca0d | ||
|
|
fde0f4ca0a | ||
|
|
73cab2af2d | ||
|
|
94d2198b30 | ||
|
|
88a67c8703 | ||
|
|
ccf9b1291e | ||
|
|
47dae716ae | ||
|
|
ea26e1c72f | ||
|
|
e6d79f0673 | ||
|
|
bf7002d0ae | ||
|
|
cbae145da5 | ||
|
|
88bbfe5961 | ||
|
|
d60569b6d6 | ||
|
|
91165e80ba | ||
|
|
a15f9552eb | ||
|
|
501d225f93 | ||
|
|
4942f244da | ||
|
|
1be5c9af56 | ||
|
|
818d383f2e | ||
|
|
5fffa32630 | ||
|
|
19d5feb483 | ||
|
|
199fc6be94 | ||
|
|
865729c033 | ||
|
|
6aa9071e24 | ||
|
|
6db3fc2eea | ||
|
|
65ffaaa67c | ||
|
|
440467e304 | ||
|
|
74422dd000 | ||
|
|
78d663bbb4 | ||
|
|
db0a58ea04 | ||
|
|
eba1e69e64 | ||
|
|
a4e7877033 | ||
|
|
c62260ab02 | ||
|
|
b58550bb79 | ||
|
|
d02c7df75c | ||
|
|
6dbebf4806 | ||
|
|
1019660f6a | ||
|
|
bfd11060ec | ||
|
|
7b6dccf5ef | ||
|
|
d76eccad1c | ||
|
|
6f0ac7ae45 | ||
|
|
8f17ed1eb9 | ||
|
|
2b5562e376 | ||
|
|
35b3216fee | ||
|
|
6f8020e30d | ||
|
|
0bee3901b6 | ||
|
|
7106c68032 | ||
|
|
be707536bd | ||
|
|
4754ac2bd1 | ||
|
|
3cca77e748 | ||
|
|
4ec1aaabe6 | ||
|
|
0baacbef98 | ||
|
|
d8261b3359 | ||
|
|
1ecb26a3fb | ||
|
|
85bf92ad2f |
12
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
@@ -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
|
||||
29
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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/"
|
||||
|
||||
6
Pipfile
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
Changelog
|
||||
*********
|
||||
|
||||
paperless-ngx 1.7.0
|
||||
###################
|
||||
|
||||
Changelog pending
|
||||
|
||||
paperless-ngx 1.6.0
|
||||
###################
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> <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>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
filter: brightness(0.5);
|
||||
|
||||
&.active {
|
||||
background-color: var(--ngx-primary-lighten-30);
|
||||
background-color: var(--pngx-primary-lighten-30);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,3 +5,7 @@
|
||||
margin: 0.5em;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.toast:not(.show) {
|
||||
display: block; // this corrects an ng-bootstrap bug that prevented animations
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,3 +17,10 @@
|
||||
--page-margin: 1px 0 -8px;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.password-prompt {
|
||||
position: absolute;
|
||||
top: 30%;
|
||||
left: 30%;
|
||||
right: 30%;
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> <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>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
|
||||
.doc-img-background-selected {
|
||||
background-color: var(--ngx-primary-faded);
|
||||
background-color: var(--pngx-primary-faded);
|
||||
}
|
||||
|
||||
.card-info {
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
}
|
||||
|
||||
.doc-img-background-selected {
|
||||
background-color: var(--ngx-primary-faded);
|
||||
background-color: var(--pngx-primary-faded);
|
||||
}
|
||||
|
||||
.card-info {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
23
src-ui/src/app/services/rest/remote-version.service.ts
Normal 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/`
|
||||
)
|
||||
}
|
||||
}
|
||||
74
src-ui/src/app/services/upload-documents.service.ts
Normal 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()
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 579 KiB After Width: | Height: | Size: 861 KiB |
@@ -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/',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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() == "":
|
||||
|
||||
@@ -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"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
BIN
src/documents/tests/samples/barcodes/barcode-128-PATCHT.png
Normal file
|
After Width: | Height: | Size: 836 B |
BIN
src/documents/tests/samples/barcodes/barcode-128-custom.pdf
Normal file
BIN
src/documents/tests/samples/barcodes/barcode-128-custom.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
BIN
src/documents/tests/samples/barcodes/barcode-39-PATCHT.png
Normal file
|
After Width: | Height: | Size: 891 B |
243
src/documents/tests/samples/barcodes/barcode-39-custom.pdf
Normal file
BIN
src/documents/tests/samples/barcodes/barcode-39-custom.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/documents/tests/samples/barcodes/barcode-qr-custom.pdf
Normal file
BIN
src/documents/tests/samples/barcodes/barcode-qr-custom.png
Normal file
|
After Width: | Height: | Size: 337 B |
BIN
src/documents/tests/samples/barcodes/patch-code-t-middle.pdf
Normal file
BIN
src/documents/tests/samples/barcodes/patch-code-t-qr.pdf
Normal file
BIN
src/documents/tests/samples/barcodes/patch-code-t.pbm
Normal file
BIN
src/documents/tests/samples/barcodes/patch-code-t.pdf
Normal file
BIN
src/documents/tests/samples/barcodes/qr-code-PATCHT.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
src/documents/tests/samples/barcodes/several-patcht-codes.pdf
Normal 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"))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||