Compare commits

..

2 Commits

Author SHA1 Message Date
Trenton Holmes
7d8721c53c (delete me) 2022-04-02 07:14:14 -07:00
Trenton Holmes
aa67c851e3 (delete me) 2022-04-02 07:12:56 -07:00
83 changed files with 843 additions and 2450 deletions

View File

@@ -1,15 +1,23 @@
<!--
<!--
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 proposed 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 poposed 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
<!--

View File

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

View File

@@ -132,7 +132,7 @@ jobs:
name: Install system dependencies
run: |
sudo apt-get update -qq
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng libzbar0 poppler-utils
sudo apt-get install -qq --no-install-recommends unpaper tesseract-ocr imagemagick ghostscript optipng
-
name: Install Python dependencies
run: |
@@ -196,8 +196,9 @@ 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,event=branch
type=ref,event=tag
-
name: Checkout
uses: actions/checkout@v3
@@ -327,22 +328,24 @@ 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 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
name: Create release
id: create_release
uses: actions/create-release@v1
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
@@ -350,7 +353,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create-release.outputs.upload_url }}
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
asset_path: ./paperless-ngx.tar.xz
asset_name: paperless-ngx-${{ steps.get_version.outputs.version }}.tar.xz
asset_content_type: application/x-xz

View File

@@ -5,7 +5,7 @@
repos:
# General hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
rev: v4.1.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.2"
rev: "v2.6.1"
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.2"
rev: "v2.2.1"
hooks:
- id: add-trailing-comma
exclude: "(migrations)"

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ gunicorn = "*"
imap-tools = "*"
langdetect = "*"
pathvalidate = "*"
pillow = "~=9.1"
pillow = "~=9.0"
# Any version update to pikepdf requires a base image update
pikepdf = "~=5.1"
python-gnupg = "*"
@@ -51,8 +51,6 @@ 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 = "*"
@@ -64,7 +62,7 @@ pytest-django = "*"
pytest-env = "*"
pytest-sugar = "*"
pytest-xdist = "*"
sphinx = "~=4.5.0"
sphinx = "~=4.4.0"
sphinx_rtd_theme = "*"
tox = "*"
black = "*"

346
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "9573af313c811561d467d814c52c6bd1439bc48e3b31d7f56afed5f0ebe4b648"
"sha256": "f33c0aeb0cb880b444efd367ad5b47358dde2a9b18b55dcd6943dd81f6c7e2a6"
},
"pipfile-spec": 6,
"requires": {},
@@ -68,10 +68,10 @@
},
"autobahn": {
"hashes": [
"sha256:58a887c7a196bb08d8b6624cb3695f493a9e5c9f00fd350d8d6f829b47ff9036"
"sha256:60e1f4c602aacd052ffe3d46ae40b6b75f8286b3c46922c213b523162e58c17e"
],
"markers": "python_version >= '3.7'",
"version": "==22.3.2"
"version": "==22.2.2"
},
"automat": {
"hashes": [
@@ -99,6 +99,7 @@
"sha256:f04e857b59d9d1ccc39ce2da1021d196e47234873820cbeaad210724b1ee28ac",
"sha256:fadbfe37f74051d024037f223b8e001611eac868b5c5b06144ef4d8b799862f2"
],
"index": "pypi",
"markers": "python_version < '3.9'",
"version": "==0.2.1"
},
@@ -206,11 +207,11 @@
},
"click": {
"hashes": [
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
"sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
"sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.2"
"markers": "python_version >= '3.6'",
"version": "==8.0.4"
},
"coloredlogs": {
"hashes": [
@@ -279,11 +280,11 @@
},
"django": {
"hashes": [
"sha256:07c8638e7a7f548dc0acaaa7825d84b7bd42b10e8d22268b3d572946f1e9b687",
"sha256:4e8177858524417563cc0430f29ea249946d831eacb0068a1455686587df40b5"
"sha256:1239218849e922033a35d2a2f777cb8bee18bd725416744074f455f34ff50d0c",
"sha256:77ff2e7050e3324c9b67e29b6707754566f58514112a9ac73310f60cd5261930"
],
"index": "pypi",
"version": "==4.0.4"
"version": "==4.0.3"
},
"django-cors-headers": {
"hashes": [
@@ -487,15 +488,16 @@
},
"img2pdf": {
"hashes": [
"sha256:8ec898a9646523fd3862b154f3f47cd52609c24cc3e2dc1fb5f0168f0cbe793c"
"sha256:8e51c5043efa95d751481b516071a006f87c2a4059961a9ac43ec238915de09f"
],
"version": "==0.4.4"
"version": "==0.4.3"
},
"importlib-resources": {
"hashes": [
"sha256:1b93238cbf23b4cde34240dd8321d99e9bf2eb4bc91c0c99b2886283e7baad85",
"sha256:a9dd72f6cc106aeb50f6e66b86b69b454766dd6e39b69ac68450253058706bcc"
],
"index": "pypi",
"markers": "python_version < '3.9'",
"version": "==5.6.0"
},
@@ -671,11 +673,11 @@
},
"ocrmypdf": {
"hashes": [
"sha256:7f0a6165b80ba1b37ce5943cf5b4faf93bf98c04c8f5157ef83c5f292491485f",
"sha256:d52410bc38cf5b66da27668e38c66ac41fd3136457c1ec388b311f0a78ee213c"
"sha256:201ed2f589f851be73908fce35fbb6fb05e4739289d3cd8765f9519f49ea1cd9",
"sha256:f42e60bc2b6534634dd08928584275b1c556dc875c849650afcc38f7da9e2856"
],
"index": "pypi",
"version": "==13.4.2"
"version": "==13.4.1"
},
"packaging": {
"hashes": [
@@ -693,98 +695,87 @@
"index": "pypi",
"version": "==2.5.0"
},
"pdf2image": {
"hashes": [
"sha256:84f79f2b8fad943e36323ea4e937fcb05f26ded0caa0a01181df66049e42fb65",
"sha256:d58ed94d978a70c73c2bb7fdf8acbaf2a7089c29ff8141be5f45433c0c4293bb"
],
"index": "pypi",
"version": "==1.16.0"
},
"pdfminer.six": {
"hashes": [
"sha256:af0630f98a292bad4170f54e80f82ca81b916dd0b2c996437ec45c02f11d8762",
"sha256:eff2ce0abeaa4df94dc3461f70eab104487c7b4a2b3c7e9fd0aeec6c5f44d6a6"
"sha256:0351f17d362ee2d48b158be52bcde6576d96460efd038a3e89a043fba6d634d7",
"sha256:d3efb75c0249b51c1bf795e3a8bddf1726b276c77bf75fb136adea471ee2825b"
],
"index": "pypi",
"version": "==20220319"
"version": "==20211012"
},
"pikepdf": {
"hashes": [
"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"
"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"
],
"index": "pypi",
"version": "==5.1.1"
"version": "==5.1.0"
},
"pillow": {
"hashes": [
"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"
"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"
],
"index": "pypi",
"version": "==9.1.0"
"version": "==9.0.1"
},
"pluggy": {
"hashes": [
@@ -871,11 +862,11 @@
},
"pyparsing": {
"hashes": [
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.8"
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
},
"python-dateutil": {
"hashes": [
@@ -968,15 +959,6 @@
],
"version": "==6.0"
},
"pyzbar": {
"hashes": [
"sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c",
"sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d",
"sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518"
],
"index": "pypi",
"version": "==0.1.9"
},
"redis": {
"hashes": [
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
@@ -1191,11 +1173,11 @@
},
"setuptools": {
"hashes": [
"sha256:26ead7d1f93efc0f8c804d9fafafbe4a44b179580a7105754b245155f9af05a8",
"sha256:47c7b0c0f8fc10eec4cf1e71c6fdadf8decaa74ffa087e68cd1c20db7ad6a592"
"sha256:89eef7b71423ab7fccc7dfafdc145410ef170c4a89567427f932448135e08cdf",
"sha256:92b15f45ab164eb0c410d2bf661a6e9d15e3b78c0dffb0325f2bf0f313071cae"
],
"markers": "python_version >= '3.7'",
"version": "==62.1.0"
"version": "==61.1.1"
},
"six": {
"hashes": [
@@ -1238,22 +1220,22 @@
},
"tqdm": {
"hashes": [
"sha256:40be55d30e200777a307a7585aee69e4eabb46b4ec6a4b4a5f2d9f11e7d5408d",
"sha256:74a2cdefe14d11442cedf3ba4e21a3b84ff9a2dbdc6cfae2c34addb2a14a5ea6"
"sha256:4230a49119a416c88cc47d0d2d32d5d90f1a282d5e497d49801950704e49863d",
"sha256:6461b009d6792008d0000e1b0c7ca50195ec78c0e808a3a6b668a56a3236c3a5"
],
"index": "pypi",
"version": "==4.64.0"
"version": "==4.63.1"
},
"twisted": {
"extras": [
"tls"
],
"hashes": [
"sha256:a047990f57dfae1e0bd2b7df2526d4f16dcdc843774dc108b78c52f2a5f13680",
"sha256:f9f7a91f94932477a9fc3b169d57f54f96c6e74a23d78d9ce54039a7f48928a2"
"sha256:57f32b1f6838facb8c004c89467840367ad38e9e535f8252091345dba500b4f2",
"sha256:5c63c149eb6b8fe1e32a0215b1cef96fabdba04f705d8efb9174b1ccf5b49d49"
],
"markers": "python_full_version >= '3.6.7'",
"version": "==22.4.0"
"version": "==22.2.0"
},
"txaio": {
"hashes": [
@@ -1281,11 +1263,11 @@
},
"tzlocal": {
"hashes": [
"sha256:89885494684c929d9191c57aa27502afc87a579be5cdd3225c77c463ea043745",
"sha256:ee5842fa3a795f023514ac2d801c4a81d1743bbe642e3940143326b3a00addd7"
"sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09",
"sha256:28ba8d9fcb6c9a782d6e0078b4f6627af1ea26aeaa32b4eab5324abc7df4149f"
],
"markers": "python_version >= '3.6'",
"version": "==4.2"
"version": "==4.1"
},
"urllib3": {
"hashes": [
@@ -1359,10 +1341,10 @@
},
"watchgod": {
"hashes": [
"sha256:2f3e8137d98f493ff58af54ea00f4d1433a6afe2ed08ab331a657df468c6bfce",
"sha256:cb11ff66657befba94d828e3b622d5fb76f22fbda1376f355f3e6e51e97d9450"
"sha256:4ba20c2fa3e63df706ab50e694b9453b05395fadb7cbbfd984d71fb1547d485d",
"sha256:c12d15f3df7d11e740704e45398277f75f1d78f46ad59ca9d7505bfd8b8d3086"
],
"version": "==0.8.2"
"version": "==0.8.1"
},
"wcwidth": {
"hashes": [
@@ -1443,11 +1425,12 @@
},
"zipp": {
"hashes": [
"sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad",
"sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
],
"index": "pypi",
"markers": "python_version < '3.9'",
"version": "==3.8.0"
"version": "==3.7.0"
},
"zope.interface": {
"hashes": [
@@ -1533,32 +1516,32 @@
},
"black": {
"hashes": [
"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"
"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"
],
"index": "pypi",
"version": "==22.3.0"
"version": "==22.1.0"
},
"certifi": {
"hashes": [
@@ -1585,15 +1568,15 @@
},
"click": {
"hashes": [
"sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e",
"sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"
"sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
"sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.2"
"markers": "python_version >= '3.6'",
"version": "==8.0.4"
},
"coverage": {
"extras": [
"toml"
],
"hashes": [
"sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
@@ -1688,11 +1671,11 @@
},
"faker": {
"hashes": [
"sha256:188961065fb5c78ea639f42176f55100f72c90c3a3179ac6c955c4bd712b0511",
"sha256:7758ece2593ce603db117db3d27393c31f4af03f783e176f3f0e14839a4f3426"
"sha256:5536ceb63380f0d598c026b7c330c17d719a19d1a495e9397ee8f5259420a696",
"sha256:85ed0cb379e3b7414bdb6b484994beaf9bddc74472c8c35f743c16cf5fc0c314"
],
"markers": "python_version >= '3.6'",
"version": "==13.3.4"
"version": "==13.3.3"
},
"filelock": {
"hashes": [
@@ -1726,6 +1709,14 @@
"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",
@@ -1834,11 +1825,11 @@
},
"pre-commit": {
"hashes": [
"sha256:02226e69564ebca1a070bd1f046af866aa1c318dbc430027c50ab832ed2b73f2",
"sha256:5d445ee1fa8738d506881c5d84f83c62bb5be6b2838e32207433647e8e5ebe10"
"sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616",
"sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"
],
"index": "pypi",
"version": "==2.18.1"
"version": "==2.17.0"
},
"py": {
"hashes": [
@@ -1866,11 +1857,11 @@
},
"pyparsing": {
"hashes": [
"sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954",
"sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.8"
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
},
"pytest": {
"hashes": [
@@ -2004,11 +1995,11 @@
},
"sphinx": {
"hashes": [
"sha256:7bf8ca9637a4ee15af412d1a1d9689fec70523a68ca9bb9127c2f3eeb344e2e6",
"sha256:ebf612653238bcc8f4359627a9b7ce44ede6fdd75d9d30f68255c7383d3a6226"
"sha256:5da895959511473857b6d0200f56865ed62c31e8f82dd338063b84ec022701fe",
"sha256:6caad9786055cb1fa22b4a365c1775816b876f91966481765d7d50e9f0dd35cc"
],
"index": "pypi",
"version": "==4.5.0"
"version": "==4.4.0"
},
"sphinx-rtd-theme": {
"hashes": [
@@ -2096,6 +2087,14 @@
"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",
@@ -2106,11 +2105,20 @@
},
"virtualenv": {
"hashes": [
"sha256:e617f16e25b42eb4f6e74096b9c9e37713cf10bf30168fb4a739f3fa8f898a3a",
"sha256:ef589a79795589aada0c1c5b319486797c03b67ac3984c48c669c0e4f50df3a5"
"sha256:1e8588f35e8b42c6ec6841a13c5e88239de1e6e4e4cedfd3916b306dc826ec66",
"sha256:8e5b402037287126e81ccde9432b95a8be5b19d36584f64957060a3488c11ca8"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==20.14.1"
"version": "==20.14.0"
},
"zipp": {
"hashes": [
"sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d",
"sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"
],
"index": "pypi",
"markers": "python_version < '3.9'",
"version": "==3.7.0"
}
}
}

View File

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

View File

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

View File

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

View File

@@ -142,24 +142,7 @@ PAPERLESS_SECRET_KEY=<key>
Default is listed in the file ``src/paperless/settings.py``.
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>
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:
@@ -168,16 +151,12 @@ 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>
@@ -561,10 +540,6 @@ 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.
@@ -613,27 +588,6 @@ 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
@@ -717,7 +671,7 @@ PAPERLESS_CONSUMER_IGNORE_PATTERNS=<json>
This can be adjusted by configuring a custom json array with patterns to exclude.
Defaults to ``[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]``.
Defautls to ``[".DS_STORE/*", "._*", ".stfolder/*"]``.
Binaries
########
@@ -810,26 +764,3 @@ PAPERLESS_OCR_LANGUAGES=<list>
PAPERLESS_OCR_LANGUAGE=tur
Defaults to none, which does not install any additional languages.
.. _configuration-update-checking:
Update Checking
###############
PAPERLESS_ENABLE_UPDATE_CHECK=<bool>
Enable (or disable) the automatic check for available updates. This feature is disabled
by default but if it is not explicitly set Paperless-ngx will show a message about this.
If enabled, the feature works by pinging the the Github API for the latest release e.g.
https://api.github.com/repos/paperless-ngx/paperless-ngx/releases/latest
to determine whether a new version is available.
Actual updating of the app must still be performed manually.
Note that for users of thirdy-party containers e.g. linuxserver.io this notification
may be 'ahead' of a new release from the third-party maintainers.
In either case, no tracking data is collected by the app in any way.
Defaults to none, which disables the feature.

View File

@@ -13,43 +13,43 @@ that works right for you based on recommendations from other Paperless users.
Physical scanners
=================
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| Brand | Model | Supports | Recommended By |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
| | | FTP | 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`_ |
+---------+----------------+-----+------+-----+-----+------+----------+----------------+
+---------+----------------+-----+-----+-----+------+----------+----------------+
| 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`_ |
+---------+----------------+-----+-----+-----+------+----------+----------------+
.. _MFC-L5850DW: https://www.brother-usa.com/products/mfcl5850dw
.. _MFC-L2750DW: https://www.brother.de/drucker/laserdrucker/mfc-l2750dw
@@ -131,3 +131,4 @@ This part assumes your Doxie is connected to WiFi and you know its IP.
6. Click *Submit* at the bottom of the page
Congrats, you can now scan directly from your Doxie to your Paperless-ngx instance!

View File

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

View File

@@ -47,29 +47,24 @@ if [[ $(id -u) == "0" ]] ; then
exit 1
fi
if ! command -v wget &> /dev/null ; then
if [[ -z $(which wget) ]] ; then
echo "wget executable not found. Is wget installed?"
exit 1
fi
if ! command -v docker &> /dev/null ; then
if [[ -z $(which docker) ]] ; then
echo "docker executable not found. Is docker installed?"
exit 1
fi
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
if [[ -z $(which docker-compose) ]] ; then
echo "docker-compose executable not found. Is docker-compose installed?"
exit 1
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 &> /dev/null ; then
if [ ! "$(docker stats --no-stream 2>/dev/null 1>&2)" ] ; 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."
@@ -92,14 +87,6 @@ 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."
@@ -286,7 +273,6 @@ if [[ "$DATABASE_BACKEND" == "postgres" ]] ; then
fi
fi
echo ""
echo "URL: $URL"
echo "Port: $PORT"
echo "Database: $DATABASE_BACKEND"
echo "Tika enabled: $TIKA_ENABLED"
@@ -322,9 +308,6 @@ 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
@@ -368,8 +351,8 @@ if [ "$l1" -eq "$l2" ] ; then
fi
${DOCKER_COMPOSE_CMD} pull
docker-compose pull
${DOCKER_COMPOSE_CMD} run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
docker-compose run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
${DOCKER_COMPOSE_CMD} up -d
docker-compose up -d

View File

@@ -27,10 +27,8 @@
# Security and hosting
#PAPERLESS_SECRET_KEY=change-me
#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_ALLOWED_HOSTS=example.com,www.example.com
#PAPERLESS_CORS_ALLOWED_HOSTS=http://example.com,http://localhost:8000
#PAPERLESS_FORCE_SCRIPT_NAME=
#PAPERLESS_STATIC_URL=/static/
#PAPERLESS_AUTO_LOGIN_USERNAME=
@@ -60,10 +58,8 @@
#PAPERLESS_CONSUMER_POLLING=10
#PAPERLESS_CONSUMER_DELETE_DUPLICATES=false
#PAPERLESS_CONSUMER_RECURSIVE=false
#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]
#PAPERLESS_CONSUMER_IGNORE_PATTERNS=[".DS_STORE/*", "._*", ".stfolder/*"]
#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
@@ -71,7 +67,6 @@
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
#PAPERLESS_THUMBNAIL_FONT_NAME=
#PAPERLESS_IGNORE_DATES=
#PAPERLESS_ENABLE_UPDATE_CHECK=
# Tika settings

View File

@@ -5,15 +5,15 @@
# pipenv lock --requirements
#
-i https://pypi.python.org/simple/
--extra-index-url https://www.piwheels.org/simple/
-i https://pypi.python.org/simple
--extra-index-url https://www.piwheels.org/simple
aioredis==1.3.1
anyio==3.5.0; python_full_version >= '3.6.2'
arrow==1.2.2; python_version >= '3.6'
asgiref==3.5.0; python_version >= '3.7'
async-timeout==4.0.2; python_version >= '3.6'
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
autobahn==22.3.2; python_version >= '3.7'
autobahn==22.2.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.1.2; python_version >= '3.7'
click==8.0.4; python_version >= '3.6'
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.4
django==4.0.3
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.4
img2pdf==0.4.3
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,13 +57,12 @@ 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.2
ocrmypdf==13.4.1
packaging==21.3; python_version >= '3.6'
pathvalidate==2.5.0
pdf2image==1.16.0
pdfminer.six==20220319
pikepdf==5.1.1
pillow==9.1.0
pdfminer.six==20211012
pikepdf==5.1.0
pillow==9.0.1
pluggy==1.0.0; python_version >= '3.6'
portalocker==2.4.0; python_version >= '3'
psycopg2==2.9.3
@@ -71,7 +70,7 @@ pyasn1-modules==0.2.8
pyasn1==0.4.8
pycparser==2.21
pyopenssl==22.0.0
pyparsing==3.0.8; python_full_version >= '3.6.8'
pyparsing==3.0.7; python_version >= '3.6'
python-dateutil==2.8.2
python-dotenv==0.20.0
python-gnupg==0.4.8
@@ -80,7 +79,6 @@ 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'
@@ -88,26 +86,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==62.1.0; python_version >= '3.7'
setuptools==61.1.1; 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.64.0
twisted[tls]==22.4.0; python_full_version >= '3.6.7'
tqdm==4.63.1
twisted[tls]==22.2.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.2; python_version >= '3.6'
tzlocal==4.1; 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.2
watchgod==0.8.1
wcwidth==0.2.5
websockets==10.2
whitenoise==6.0.0
whoosh==2.7.4
zipp==3.8.0; python_version < '3.9'
zipp==3.7.0; python_version < '3.9'
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'

View File

@@ -1,13 +1,3 @@
<app-toasts></app-toasts>
<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>
<router-outlet></router-outlet>

View File

@@ -4,8 +4,6 @@ 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',
@@ -17,16 +15,11 @@ 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 uploadDocumentsService: UploadDocumentsService
private router: Router
) {
let anyWindow = window as any
anyWindow.pdfWorkerSrc = 'assets/js/pdf.worker.min.js'
@@ -107,36 +100,4 @@ export class AppComponent implements OnInit, OnDestroy {
}
})
}
public get dragDropEnabled(): boolean {
return !this.router.url.includes('dashboard')
}
public fileOver() {
// allows transition
setTimeout(() => {
this.fileIsOver = true
}, 1)
this.hidden = false
// stop fileLeave timeout
clearTimeout(this.fileLeaveTimeoutID)
}
public fileLeave(immediate: boolean = false) {
const ms = immediate ? 0 : 500
this.fileLeaveTimeoutID = setTimeout(() => {
this.fileIsOver = false
// await transition completed
setTimeout(() => {
this.hidden = true
}, 150)
}, ms)
}
public dropped(files: NgxFileDropEntry[]) {
this.fileLeave(true)
this.uploadDocumentsService.uploadFiles(files)
this.toastService.showInfo($localize`Initiating upload...`, 3000)
}
}

View File

@@ -12,7 +12,7 @@
</a>
<div class="search-form-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1">
<form (ngSubmit)="search()" class="form-inline flex-grow-1">
<svg width="1em" height="1em" fill="currentColor">
<svg width="1em" height="1em">
<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" fill="currentColor">
<svg width="1.3em" height="1.3em">
<use xlink:href="assets/bootstrap-icons.svg#person-circle"/>
</svg>
</button>
@@ -170,46 +170,21 @@
<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" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#github" />
<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>&nbsp;<ng-container i18n>GitHub</ng-container>
</a>
<a class="nav-link-additional small text-muted ms-3" target="_blank" rel="noopener noreferrer" href="https://github.com/paperless-ngx/paperless-ngx/discussions/categories/feature-requests" title="Suggest an idea">
<svg xmlns="http://www.w3.org/2000/svg" width="1.1em" height="1.1em" fill="currentColor" class="me-1" viewBox="0 0 16 16">
<use xlink:href="assets/bootstrap-icons.svg#lightbulb" />
<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>
<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 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 class="px-3 py-2 text-muted small">
{{versionString}}
</div>
</li>
</ul>

View File

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

View File

@@ -18,10 +18,6 @@ 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',
@@ -36,18 +32,10 @@ export class AppFrameComponent {
private searchService: SearchService,
public savedViewService: SavedViewService,
private list: DocumentListViewService,
private meta: Meta,
private remoteVersionService: RemoteVersionService
) {
this.remoteVersionService
.checkForUpdates()
.subscribe((appRemoteVersion: AppRemoteVersion) => {
this.appRemoteVersion = appRemoteVersion
})
}
private meta: Meta
) {}
versionString = `${environment.appTitle} ${environment.version}`
appRemoteVersion
isMenuCollapsed: boolean = true

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import {
FileStatus,
FileStatusPhase,
} from 'src/app/services/consumer-status.service'
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
import { DocumentService } from 'src/app/services/rest/document.service'
const MAX_ALERTS = 5
@@ -19,8 +19,8 @@ export class UploadFileWidgetComponent implements OnInit {
alertsExpanded = false
constructor(
private consumerStatusService: ConsumerStatusService,
private uploadDocumentsService: UploadDocumentsService
private documentService: DocumentService,
private consumerStatusService: ConsumerStatusService
) {}
getStatus() {
@@ -116,6 +116,48 @@ export class UploadFileWidgetComponent implements OnInit {
public fileLeave(event) {}
public dropped(files: NgxFileDropEntry[]) {
this.uploadDocumentsService.uploadFiles(files)
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
}
}
}
)
})
}
}
}
}

View File

@@ -135,27 +135,20 @@
</li>
<li [ngbNavItem]="4" class="d-md-none">
<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>
<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>
</div>
</ng-template>
<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>
</li>
</ul>
@@ -167,10 +160,10 @@
</form>
</div>
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block position-relative" #pdfPreview>
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block" #pdfPreview>
<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>
<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>
</div>
<ng-template #nativePdfViewer>
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
@@ -179,11 +172,5 @@
<ng-container *ngIf="getContentType() == 'text/plain'">
<object [data]="previewUrl | safeUrl" type="text/plain" class="preview-sticky bg-white" width="100%"></object>
</ng-container>
<div *ngIf="requiresPassword" class="password-prompt">
<form>
<input autocomplete="" class="form-control" i18n-placeholder placeholder="Enter Password" type="password" (keyup)="onPasswordKeyUp($event)" />
</form>
</div>
</div>
</div>

View File

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

View File

@@ -1,4 +1,10 @@
import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'
import {
Component,
OnInit,
OnDestroy,
ViewChild,
ElementRef,
} from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { ActivatedRoute, Router } from '@angular/router'
import { NgbModal, NgbNav } from '@ng-bootstrap/ng-bootstrap'
@@ -84,11 +90,6 @@ 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
@@ -144,21 +145,7 @@ export class DocumentDetailComponent
ngOnInit(): void {
this.documentForm.valueChanges
.pipe(takeUntil(this.unsubscribeNotifier))
.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 }
)
}
.subscribe((wow) => {
Object.assign(this.document, this.documentForm.value)
})
@@ -199,25 +186,17 @@ 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: this.formatDate(this.ogDate),
created: doc.created,
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()
@@ -471,22 +450,5 @@ export class DocumentDetailComponent
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
this.previewNumPages = pdf.numPages
if (this.password) this.requiresPassword = false
}
onError(event) {
if (event.name == 'PasswordException') {
this.requiresPassword = true
}
}
onPasswordKeyUp(event: KeyboardEvent) {
if ('Enter' == event.key) {
this.password = (event.target as HTMLInputElement).value
}
}
formatDate(date: Date): string {
return date.toISOString().split('.')[0] + 'Z'
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,11 @@
@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(--pngx-primary-faded);
background-color: var(--ngx-primary-faded);
}
$paperless-card-breakpoints: (

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ import {
import { PaperlessDocument } from '../data/paperless-document'
import { PaperlessSavedView } from '../data/paperless-saved-view'
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
import { DocumentService, DOCUMENT_SORT_FIELDS } from './rest/document.service'
import { DocumentService } from './rest/document.service'
import { SettingsService, SETTINGS_KEYS } from './settings.service'
/**
@@ -160,24 +160,7 @@ export class DocumentListViewService {
activeListViewState.currentPage = 1
this.reload()
} else {
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
this.error = error.error
}
},
})

View File

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

View File

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

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 861 KiB

After

Width:  |  Height:  |  Size: 579 KiB

View File

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

View File

@@ -7,112 +7,24 @@ $enable-negative-margins: true;
@import "theme_dark";
@import "print";
// Paperless-ngx styles
.toolbaricon {
width: 1.2em;
height: 1.2em;
}
.buttonicon {
width: 1.2em;
height: 1.2em;
}
.sidebaricon {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
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 {
@@ -204,245 +116,6 @@ a, a:hover, .btn-link, .btn-link:hover {
}
}
.form-control:not(.btn),
input,
select,
textarea,
.form-select:not(.is-invalid):not(:disabled),
.form-check-input {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
&:focus {
background-color: var(--pngx-bg-darker);
color: var(--bs-body-color);
}
}
.form-check-input:checked {
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
.form-check-input:focus {
border-color: var(--bs-primary);
}
.page-link {
color: var(--bs-secondary);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color) !important;
&:hover, &:focus {
background-color: var(--bs-primary) !important;
color: var(--bs-light) !important;
}
}
.page-item.active .page-link {
background-color: var(--bs-primary);
border-color: var(--bs-primary) !important;
color: var(--bs-light);
}
.page-item.disabled .page-link {
background-color: var(--pngx-bg-darker);
}
.nav-tabs {
border-bottom: 1px solid var(--bs-border-color);
.nav-link {
color: var(--bs-primary);
&.active, &:hover {
border-color: var(--bs-border-color);
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
border-bottom: 1px solid transparent;
}
&:focus {
border-color: var(--bs-border-color);
}
&.active:focus, &:active {
border-bottom: 1px solid transparent;
}
}
}
.ng-select-container,
.ng-select.ng-select-opened > .ng-select-container,
.ng-dropdown-panel,
.ng-dropdown-panel .ng-dropdown-panel-items .ng-option {
background-color: var(--bs-body-bg) !important;
color: var(--bs-body-color) !important;
border-color: var(--bs-border-color) !important;
input:focus {
background-color: transparent !important;
}
}
.input-group-text {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
}
.list-group-item {
color: var(--bs-body-color);
background-color: var(--bs-body-bg);
border-color: var(--bs-border-color);
&:hover, &:focus {
background-color: var(--bs-body-bg);
}
}
.dropdown-menu {
background-color: var(--bs-body-bg);
.dropdown-divider {
border-color: var(--bs-border-color);
}
.dropdown-item {
color: var(--bs-body-color);
&:hover, &:focus {
background-color: var(--pngx-bg-darker);
color: var(--bs-body-color);
}
&.active {
background-color: var(--bs-primary);
color: var(--pngx-primary-text-contrast);
}
}
}
// icons
.toolbaricon {
width: 1.2em;
height: 1.2em;
}
.buttonicon {
width: 1.2em;
height: 1.2em;
}
.sidebaricon {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}
table.table {
color: var(--bs-body-color);
.des,.asc {
background-color: var(--bs-body-bg) !important;
}
}
.close {
color: var(--bs-body-color);
}
.modal .btn-close {
color: var(--bs-body-color);
}
.main-dropzone {
height: 100%;
width: 100%;
&.ngx-file-drop__drop-zone--over {
background-color: transparent !important;
}
}
.global-dropzone-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(23, 84, 31, .8);
z-index: 1055; // $zindex-modal
pointer-events: none !important;
user-select: none !important;
text-align: center;
padding-top: 25%;
&.show {
opacity: 1 !important;
}
&.hide {
display: none;
}
}
.ngx-file-drop__drop-zone--over .global-dropzone-overlay {
opacity: 0;
}
.inert {
pointer-events: none !important;
user-select: none !important;
}
.alert-danger {
color: var(--bs-body-color);
background-color: var(--bs-danger);
border-color: var(--bs-danger);
}
.alert-secondary {
background-color: var(--pngx-primary-darken-18);
border-color: var(--pngx-primary-darken-15);
color: var(--bs-body-color);
}
.ngb-dp-header,
.ngb-dp-weekdays,
.ngb-dp-month {
background-color: var(--bs-body-bg);
}
.popover {
.popover-header,
.popover-body {
background-color: var(--pngx-bg-alt);
border-color: var(--bs-border-color);
color: var(--bs-body-color);
}
}
// fix popover carat colors
.bs-popover-start > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="left"] {
border-left-color: var(--pngx-bg-alt);
}
.bs-popover-end > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="right"] {
border-right-color: var(--pngx-bg-alt);
}
.bs-popover-top > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="top"] {
border-top-color: var(--pngx-bg-alt);
}
.bs-popover-bottom > .popover-arrow::after, .bs-popover-auto[data-popper-placement^="bottom"] {
border-bottom-color: var(--pngx-bg-alt);
}
.bs-popover-bottom .popover-header::before,
.bs-popover-auto[x-placement^=bottom] .popover-header::before {
border-bottom-color: var(--pngx-bg-alt);
}
// Bootstrap 5 tweaks
a.badge {
text-decoration: none;

View File

@@ -2,16 +2,293 @@
// 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);
--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;
--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);
}

View File

@@ -13,6 +13,10 @@ $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};
@@ -23,12 +27,11 @@ $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};
--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);
--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);
.navbar.bg-primary{
--bs-primary: hsl(var(--pngx-primary),var(--pngx-primary-lightness));
@@ -61,23 +64,17 @@ $border-color-dark-mode: #47494f;
.btn-outline-primary, .btn-primary {
&:hover, &:focus, &.active, &:active {
color: var(--bs-light) !important;
color: var(--ngx-body-color-accent) !important;
}
}
.btn-outline-secondary {
&:hover, &:focus, &.active, &:active {
background-color: var(--pngx-bg-darker);
background-color: var(--ngx-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);
@@ -144,11 +141,11 @@ $border-color-dark-mode: #47494f;
color: $text-color-dark-mode-accent;
}
.close, .modal .btn-close, .alert .btn-close {
.close, .modal .btn-close {
text-shadow: 0 1px 0 #666;
}
.modal .btn-close, .alert .btn-close {
.modal .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}

View File

@@ -3,9 +3,7 @@ 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
@@ -55,25 +53,6 @@ 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:
@@ -102,23 +81,19 @@ 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:
stat_data = os.stat(file)
new_mtime = stat_data.st_mtime
new_size = stat_data.st_size
new_mtime = os.stat(file).st_mtime
except FileNotFoundError:
logger.debug(
f"File {file} moved while waiting for it to remain " f"unmodified.",
)
return
if new_mtime == mtime and new_size == size:
if new_mtime == mtime:
_consume(file)
return
mtime = new_mtime
size = new_size
sleep(settings.CONSUMER_POLLING_DELAY)
current_try += 1
@@ -207,32 +182,14 @@ 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)
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
_consume(filepath)
except KeyboardInterrupt:
pass

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,6 @@ 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?
@@ -32,8 +31,7 @@ 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|(?!=([_-])))(\b[0-9]{1,2}[ \.\/-][A-Z]{3}[ \.\/-][0-9]{4})(\b|(?=([_-])))", # noqa: E501
r"(\b|(?!=([_-])))([^\W\d_]{3,9} [0-9]{4})(\b|(?=([_-])))",
)

View File

@@ -1,12 +1,6 @@
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
@@ -20,12 +14,8 @@ 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")
@@ -72,115 +62,6 @@ 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,
@@ -191,48 +72,6 @@ def consume_file(
task_id=None,
):
# check for separators in current document
if settings.CONSUMER_ENABLE_BARCODES:
separators = []
document_list = []
separators = scan_file_for_separating_barcodes(path)
if separators:
logger.debug(f"Pages with separators found in: {str(path)}")
document_list = separate_pages(path, separators)
if document_list:
for n, document in enumerate(document_list):
# save to consumption dir
# rename it to the original filename with number prefix
if override_filename:
newname = f"{str(n)}_" + override_filename
else:
newname = None
save_to_dir(document, newname=newname)
# if we got here, the document was successfully split
# and can safely be deleted
logger.debug("Deleting file {}".format(path))
os.unlink(path)
# notify the sender, otherwise the progress bar
# in the UI stays stuck
payload = {
"filename": override_filename,
"task_id": task_id,
"current_progress": 100,
"max_progress": 100,
"status": "SUCCESS",
"message": "finished",
}
try:
async_to_sync(get_channel_layer().group_send)(
"status_updates",
{"type": "status_update", "data": payload},
)
except OSError as e:
logger.warning("OSError. It could be, the broker cannot be reached.")
logger.warning(str(e))
return "File successfully split"
# continue with consumption if no barcode was found
document = Consumer().try_consume_file(
path,
override_filename=override_filename,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 836 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 891 B

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,14 +1,12 @@
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
from urllib.parse import quote_plus
from django.conf import settings
from django.db.models import Case
@@ -26,8 +24,6 @@ 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
@@ -248,7 +244,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(filename)
filename_encoded = quote_plus(filename)
content_disposition = (
f"{disposition}; "
f'filename="{filename_normalized}"; '
@@ -670,40 +666,3 @@ class BulkDownloadView(GenericAPIView):
)
return response
class RemoteVersionView(GenericAPIView):
def get(self, request, format=None):
remote_version = "0.0.0"
is_greater_than_current = False
# TODO: this can likely be removed when frontend settings are saved to DB
feature_is_set = settings.ENABLE_UPDATE_CHECK != "default"
if feature_is_set and settings.ENABLE_UPDATE_CHECK:
try:
with urllib.request.urlopen(
"https://api.github.com/repos/"
+ "paperless-ngx/paperless-ngx/releases/latest",
) as response:
remote = response.read().decode("utf-8")
try:
remote_json = json.loads(remote)
remote_version = remote_json["tag_name"].replace("ngx-", "")
except ValueError:
logger.debug("An error occured parsing remote version json")
except urllib.error.URLError:
logger.debug("An error occured checking for available updates")
current_version = ".".join([str(_) for _ in version.__version__[:3]])
is_greater_than_current = packaging_version.parse(
remote_version,
) > packaging_version.parse(
current_version,
)
return Response(
{
"version": remote_version,
"update_available": is_greater_than_current,
"feature_is_set": feature_is_set,
},
)

View File

@@ -3,8 +3,6 @@ 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 _
@@ -31,7 +29,7 @@ elif os.path.exists("/usr/local/etc/paperless.conf"):
os.environ["OMP_THREAD_LIMIT"] = "1"
def __get_boolean(key: str, default: str = "NO") -> bool:
def __get_boolean(key, default="NO"):
"""
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.
@@ -39,13 +37,6 @@ def __get_boolean(key: str, default: str = "NO") -> bool:
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")
@@ -220,15 +211,7 @@ if DEBUG:
else:
X_FRAME_OPTIONS = "SAMEORIGIN"
# 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
# We allow CORS from localhost:8080
CORS_ALLOWED_ORIGINS = tuple(
os.getenv("PAPERLESS_CORS_ALLOWED_HOSTS", "http://localhost:8000").split(","),
)
@@ -237,22 +220,6 @@ 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.
@@ -261,6 +228,12 @@ 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",
@@ -423,7 +396,7 @@ LOGGING = {
# in total.
def default_task_workers() -> int:
def default_task_workers():
# always leave one core open
available_cores = max(multiprocessing.cpu_count(), 1)
try:
@@ -434,29 +407,20 @@ def default_task_workers() -> int:
return 1
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,
)
TASK_WORKERS = int(os.getenv("PAPERLESS_TASK_WORKERS", default_task_workers()))
Q_CLUSTER = {
"name": "paperless",
"catch_up": False,
"recycle": 1,
"retry": PAPERLESS_WORKER_RETRY,
"timeout": PAPERLESS_WORKER_TIMEOUT,
"retry": 1800,
"timeout": int(os.getenv("PAPERLESS_WORKER_TIMEOUT", 1800)),
"workers": TASK_WORKERS,
"redis": os.getenv("PAPERLESS_REDIS", "redis://localhost:6379"),
}
def default_threads_per_worker(task_workers) -> int:
def default_threads_per_worker(task_workers):
# always leave one core open
available_cores = max(multiprocessing.cpu_count(), 1)
try:
@@ -491,19 +455,13 @@ CONSUMER_IGNORE_PATTERNS = list(
json.loads(
os.getenv(
"PAPERLESS_CONSUMER_IGNORE_PATTERNS",
'[".DS_STORE/*", "._*", ".stfolder/*", ".stversions/*", ".localized/*", "desktop.ini"]',
'[".DS_STORE/*", "._*", ".stfolder/*"]',
),
),
)
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))
@@ -608,7 +566,3 @@ if os.getenv("PAPERLESS_IGNORE_DATES", ""):
d = dateparser.parse(s)
if d:
IGNORE_DATES.add(d.date())
ENABLE_UPDATE_CHECK = os.getenv("PAPERLESS_ENABLE_UPDATE_CHECK", "default")
if ENABLE_UPDATE_CHECK != "default":
ENABLE_UPDATE_CHECK = __get_boolean("PAPERLESS_ENABLE_UPDATE_CHECK")

View File

@@ -14,7 +14,6 @@ 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
@@ -73,11 +72,6 @@ urlpatterns = [
BulkDownloadView.as_view(),
name="bulk_download",
),
re_path(
r"^remote_version/",
RemoteVersionView.as_view(),
name="remoteversion",
),
path("token/", views.obtain_auth_token),
]
+ api_router.urls,

View File

@@ -61,13 +61,13 @@ class FlagMailAction(BaseMailAction):
def get_rule_action(rule):
if rule.action == MailRule.AttachmentAction.FLAG:
if rule.action == MailRule.ACTION_FLAG:
return FlagMailAction()
elif rule.action == MailRule.AttachmentAction.DELETE:
elif rule.action == MailRule.ACTION_DELETE:
return DeleteMailAction()
elif rule.action == MailRule.AttachmentAction.MOVE:
elif rule.action == MailRule.ACTION_MOVE:
return MoveMailAction()
elif rule.action == MailRule.AttachmentAction.MARK_READ:
elif rule.action == MailRule.ACTION_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.ImapSecurity.NONE:
if security == MailAccount.IMAP_SECURITY_NONE:
mailbox = MailBoxUnencrypted(server, port)
elif security == MailAccount.ImapSecurity.STARTTLS:
elif security == MailAccount.IMAP_SECURITY_STARTTLS:
mailbox = MailBox(server, port, starttls=True)
elif security == MailAccount.ImapSecurity.SSL:
elif security == MailAccount.IMAP_SECURITY_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.TitleSource.FROM_SUBJECT:
if rule.assign_title_from == MailRule.TITLE_FROM_SUBJECT:
return message.subject
elif rule.assign_title_from == MailRule.TitleSource.FROM_FILENAME:
elif rule.assign_title_from == MailRule.TITLE_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.CorrespondentSource.FROM_NOTHING:
if c_from == MailRule.CORRESPONDENT_FROM_NOTHING:
return None
elif c_from == MailRule.CorrespondentSource.FROM_EMAIL:
elif c_from == MailRule.CORRESPONDENT_FROM_EMAIL:
return self._correspondent_from_name(message.from_)
elif c_from == MailRule.CorrespondentSource.FROM_NAME:
elif c_from == MailRule.CORRESPONDENT_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.CorrespondentSource.FROM_CUSTOM:
elif c_from == MailRule.CORRESPONDENT_FROM_CUSTOM:
return rule.assign_correspondent
else:
@@ -253,7 +253,7 @@ class MailAccountHandler(LoggingMixin):
return total_processed_files
def handle_message(self, message, rule) -> int:
def handle_message(self, message, rule):
if not message.attachments:
return 0
@@ -274,8 +274,7 @@ class MailAccountHandler(LoggingMixin):
if (
not att.content_disposition == "attachment"
and rule.attachment_type
== MailRule.AttachmentProcessing.ATTACHMENTS_ONLY
and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY
):
self.log(
"debug",
@@ -286,12 +285,7 @@ class MailAccountHandler(LoggingMixin):
continue
if 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(),
):
if not fnmatch(att.filename, rule.filter_attachment_filename):
continue
title = self.get_title(message, att, rule)

View File

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

View File

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

View File

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

View File

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