Merge remote-tracking branch 'upstream/dev' into fix/issue-267

This commit is contained in:
Michael Shamoon 2021-01-06 07:55:19 -08:00
commit 6a16bdf5fd
50 changed files with 3302 additions and 354 deletions

1
.gitignore vendored
View File

@ -85,3 +85,4 @@ scripts/nuke
# this is where the compiled frontend is moved to. # this is where the compiled frontend is moved to.
/src/documents/static/frontend/ /src/documents/static/frontend/
/docs/.vscode/settings.json

489
Pipfile.lock generated
View File

@ -96,50 +96,40 @@
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"markers": "python_version >= '3.1'", "markers": "python_version >= '3.1'",
"version": "==3.0.4" "version": "==4.0.0"
}, },
"coloredlogs": { "coloredlogs": {
"hashes": [ "hashes": [
"sha256:346f58aad6afd48444c2468618623638dadab76e4e70d5e10822676f2d32226a", "sha256:5e78691e2673a8e294499e1832bb13efcfb44a86b92e18109fa18951093218ab",
"sha256:a1fab193d2053aa6c0a97608c4342d031f1f93a3d1218432c59322441d31a505", "sha256:b7f630a8297a66984b6bae0f6a1b0e0afb9f2f6838ea3bfa58f50d3d13e133d6"
"sha256:b0c2124367d4f72bd739f48e1f61491b4baf145d6bda33b606b4a53cb3f96a97"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==14.0" "version": "==15.0"
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:07ca431b788249af92764e3be9a488aa1d39a0bc3be313d826bbec690417e538", "sha256:0003a52a123602e1acee177dc90dd201f9bb1e73f24a070db7d36c588e8f5c7d",
"sha256:13b88a0bd044b4eae1ef40e265d006e34dbcde0c2f1e15eb9896501b2d8f6c6f", "sha256:0e85aaae861d0485eb5a79d33226dd6248d2a9f133b81532c8f5aae37de10ff7",
"sha256:257dab4f368fae15f378ea9a4d2799bf3696668062de0e9fa0ebb7a738a6917d", "sha256:594a1db4511bc4d960571536abe21b4e5c3003e8750ab8365fafce71c5d86901",
"sha256:32434673d8505b42c0de4de86da8c1620651abd24afe91ae0335597683ed1b77", "sha256:69e836c9e5ff4373ce6d3ab311c1a2eed274793083858d3cd4c7d12ce20d5f9c",
"sha256:3cd75a683b15576cfc822c7c5742b3276e50b21a06672dc3a800a2d5da4ecd1b", "sha256:788a3c9942df5e4371c199d10383f44a105d67d401fb4304178020142f020244",
"sha256:4e7268a0ca14536fecfdf2b00297d4e407da904718658c1ff1961c713f90fd33", "sha256:7e177e4bea2de937a584b13645cab32f25e3d96fc0bc4a4cf99c27dc77682be6",
"sha256:545a8550782dda68f8cdc75a6e3bf252017aa8f75f19f5a9ca940772fc0cb56e", "sha256:83d9d2dfec70364a74f4e7c70ad04d3ca2e6a08b703606993407bf46b97868c5",
"sha256:55d0b896631412b6f0c7de56e12eb3e261ac347fbaa5d5e705291a9016e5f8cb", "sha256:84ef7a0c10c24a7773163f917f1cb6b4444597efd505a8aed0a22e8c4780f27e",
"sha256:5849d59358547bf789ee7e0d7a9036b2d29e9a4ddf1ce5e06bb45634f995c53e", "sha256:982f661bffc7a24b6d4f8ebe3291f17cf3833a0941c6f4d9d55c790b9aa2cdb3",
"sha256:59f7d4cfea9ef12eb9b14b83d79b432162a0a24a91ddc15c2c9bf76a68d96f2b", "sha256:9e21301f7a1e7c03dbea73e8602905a4ebba641547a462b26dd03451e5769e7c",
"sha256:6dc59630ecce8c1f558277ceb212c751d6730bd12c80ea96b4ac65637c4f55e7", "sha256:9f6b0492d111b43de5f70052e24c1f0951cb9e6022188ebcb1cc3a3d301469b0",
"sha256:7117319b44ed1842c617d0a452383a5a052ec6aa726dfbaffa8b94c910444297", "sha256:a69bd3c68b98298f490e84519b954335154917eaab52cf582fa2c5c7efc6e812",
"sha256:75e8e6684cf0034f6bf2a97095cb95f81537b12b36a8fedf06e73050bb171c2d", "sha256:b4890d5fb9b7a23e3bf8abf5a8a7da8e228f1e97dc96b30b95685df840b6914a",
"sha256:7b8d9d8d3a9bd240f453342981f765346c87ade811519f98664519696f8e6ab7", "sha256:c366df0401d1ec4e548bebe8f91d55ebcc0ec3137900d214dd7aac8427ef3030",
"sha256:a035a10686532b0587d58a606004aa20ad895c60c4d029afa245802347fab57b", "sha256:dc42f645f8f3a489c3dd416730a514e7a91a59510ddaadc09d04224c098d3302"
"sha256:a4e27ed0b2504195f855b52052eadcc9795c59909c9d84314c5408687f933fc7",
"sha256:a733671100cd26d816eed39507e585c156e4498293a907029969234e5e634bc4",
"sha256:a75f306a16d9f9afebfbedc41c8c2351d8e61e818ba6b4c40815e2b5740bb6b8",
"sha256:bd717aa029217b8ef94a7d21632a3bb5a4e7218a4513d2521c2a2fd63011e98b",
"sha256:d25cecbac20713a7c3bc544372d42d8eafa89799f492a43b79e1dfd650484851",
"sha256:d26a2557d8f9122f9bf445fc7034242f4375bd4e95ecda007667540270965b13",
"sha256:d3545829ab42a66b84a9aaabf216a4dce7f16dbc76eb69be5c302ed6b8f4a29b",
"sha256:d3d5e10be0cf2a12214ddee45c6bd203dab435e3d83b4560c03066eda600bfe3",
"sha256:efe15aca4f64f3a7ea0c09c87826490e50ed166ce67368a68f315ea0807a20df"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'",
"version": "==3.2.1" "version": "==3.3.1"
}, },
"dateparser": { "dateparser": {
"hashes": [ "hashes": [
@ -151,19 +141,19 @@
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2", "sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7",
"sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03" "sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.1.4" "version": "==3.1.5"
}, },
"django-cors-headers": { "django-cors-headers": {
"hashes": [ "hashes": [
"sha256:9322255c296d5f75089571f29e520c83ff9693df17aa3cf9f6a4bea7c6740169", "sha256:5665fc1b1aabf1b678885cf6f8f8bd7da36ef0a978375e767d491b48d3055d8f",
"sha256:db82b2840f667d47872ae3e4a4e0a0d72fbecb42779b8aa233fa8bb965f7836a" "sha256:ba898dd478cd4be3a38ebc3d8729fa4d044679f8c91b2684edee41129d7e968a"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.5.0" "version": "==3.6.0"
}, },
"django-extensions": { "django-extensions": {
"hashes": [ "hashes": [
@ -230,11 +220,11 @@
}, },
"humanfriendly": { "humanfriendly": {
"hashes": [ "hashes": [
"sha256:175ffa628aa76da2c17369a5da5856084562cc66dfe7f82ae93ca3ef175277a6", "sha256:066562956639ab21ff2676d1fda0b5987e985c534fc76700a19bd54bcb81121d",
"sha256:3c9ab8d28e88e6cc998e41963357736dafd555ee5bb666b50e42f6ce28dd3e3d" "sha256:d5c731705114b9ad673754f3317d9fa4c23212f36b29bdc4272a892eafc9bc72"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==9.0" "version": "==9.1"
}, },
"idna": { "idna": {
"hashes": [ "hashes": [
@ -247,11 +237,11 @@
}, },
"imap-tools": { "imap-tools": {
"hashes": [ "hashes": [
"sha256:72bf46dc135b039a5d5b59f4e079242ac15eac02a30038e8cb2dec7b153cab65", "sha256:7d2d25b35117a3750c3b561dd93cc2fcb24cdc457830a049796c639f4371e317",
"sha256:75dc1c72dd76d9e577df26a1e0ec3a809b5eebce77678851458dcd2eae127ac9" "sha256:80088839cd1959f20c44206cdad4463ca1e7647ff67cf5b0e31e810fb6aaa6c4"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.33.0" "version": "==0.34.0"
}, },
"img2pdf": { "img2pdf": {
"hashes": [ "hashes": [
@ -262,11 +252,11 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013", "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed",
"sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170" "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"
], ],
"markers": "python_version < '3.8'", "markers": "python_version < '3.8'",
"version": "==3.1.1" "version": "==3.3.0"
}, },
"inotify-simple": { "inotify-simple": {
"hashes": [ "hashes": [
@ -286,11 +276,11 @@
}, },
"joblib": { "joblib": {
"hashes": [ "hashes": [
"sha256:698c311779f347cf6b7e6b8a39bb682277b8ee4aba8cf9507bc0cf4cd4737b72", "sha256:75ead23f13484a2a414874779d69ade40d4fa1abe62b222a23cd50d4bc822f6f",
"sha256:9e284edd6be6b71883a63c9b7f124738a3c16195513ad940eae7e3438de885d5" "sha256:7ad866067ac1fdec27d51c8678ea760601b70e32ff1881d4dc8e1171f2b64b24"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==0.17.0" "version": "==1.0.0"
}, },
"langdetect": { "langdetect": {
"hashes": [ "hashes": [
@ -389,26 +379,19 @@
}, },
"ocrmypdf": { "ocrmypdf": {
"hashes": [ "hashes": [
"sha256:91e7394172cedb3be801a229dbd3d308fb5ae80cbc3a77879fa7954beea407b1", "sha256:161c9dffb61485d30d4caea07dcb6d1b73ffa43f6e8767504a9128c510cc0c8c",
"sha256:e550b8e884150accab7ea41f4a576b5844594cb5cbd6ed514fbf1206720343ad" "sha256:404e564d0eac076cc520f0742b3e711f2611ae12a7adbc05f1232a77a81d6d61"
], ],
"index": "pypi", "index": "pypi",
"version": "==11.3.4" "version": "==11.4.4"
},
"pathtools": {
"hashes": [
"sha256:7c35c5421a39bb82e58018febd90e3b6e5db34c5443aaaf742b3f33d4655f1c0",
"sha256:d77d982475e87f32b82157a43b09f0a5ef3e66c1d8f3c7eb8d2580e783cd8202"
],
"version": "==0.1.2"
}, },
"pathvalidate": { "pathvalidate": {
"hashes": [ "hashes": [
"sha256:1697c8ea71ff4c48e7aa0eda72fe4581404be8f41e51a17363ef682dd6824d35", "sha256:378c8b319838a255c00ab37f664686b75f0aabea4444d6c5a34effbec6738285",
"sha256:32d30dbacb711c16bb188b12ce7e9a46b41785f50a12f64500f747480a4b6ee3" "sha256:cae8ad5cd9223c5c1f4bc4e2ef0cd4c5e89acd2d698fdb7610ee108b9be654d2"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.3.0" "version": "==2.3.2"
}, },
"pdfminer.six": { "pdfminer.six": {
"hashes": [ "hashes": [
@ -427,65 +410,65 @@
}, },
"pikepdf": { "pikepdf": {
"hashes": [ "hashes": [
"sha256:0829bd5dacd73bb4a37e7575bae523f49603479755563c92ddb55c206700cab1", "sha256:05fac9db7d5f5871f7b6598714386ffe56c1589e1d984859fb9e6a4ec8f0ebd0",
"sha256:0d2b631077cd6af6e4d1b396208020705842610a6f13fab489d5f9c47916baa2", "sha256:267f76dc2ca107498d9cd90df8b26d36c57faebff933ef4069dffa8d2e14a9e4",
"sha256:21c98af08fae4ac9fbcad02b613b6768a4ca300fda4cba867f4a4b6f73c2d04b", "sha256:28d9f436086faf03306d321465a9384aaefe7fb023a46fc177921bc899656c6b",
"sha256:2240372fed30124ddc35b0c15a613f2b687a426ea2f150091e0a0c58cca7a495", "sha256:2e66e15122f18b1dfbe6f48b90ebfd72c666b16330af5c4849e9b9aa930c8983",
"sha256:2a97f5f1403e058d217d7f6861cf51fca200c5687bce0d052f5f2fa89b5bfa22", "sha256:3147bd0b4f4c6ed42b8dce724aa76d041aa071ebf4b500da302e1b368eb57811",
"sha256:3faaefca0ae80d19891acec8b0dd5e6235f59f2206d82375eb80d090285e9557", "sha256:385da233cb211f00a154597b437214392b25ba83b88da53124ff01856f4e0753",
"sha256:48ef45b64882901c0d69af3b85d16a19bd0f3e95b43e614fefb53521d8caf36c", "sha256:497000a07a1549239a83b3753e38b30257a5978d0c3f1b0ddaf698c2e1722616",
"sha256:5212fe41f2323fc7356ba67caa39737fe13080562cff37bcbb74a8094076c8d0", "sha256:497c2d9212ec4d08582bdb4bb75d383de9f3d91308092dd23b84fdecffc08fbc",
"sha256:56859c32170663c57bd0658189ce44e180533eebe813853446cd6413810be9eb", "sha256:62df5bed7aefbfadf29063d1c6bb9d5132bea0f6f40a186b75e068805ba96d45",
"sha256:5f8fd1cb3478c5534222018aca24fbbd2bc74460c899bda988ec76722c13caa9", "sha256:80380933b1423adb25ebee33659614b9e4cd7fdfb655184d5bb8becc2ea5109a",
"sha256:74300a32c41b3d578772f6933f23a88b19f74484185e71e5225ce2f7ea5aea78", "sha256:8a72fff7adff10f7459670cc7950988cb2863ccfef107460432a7f290d00a9a1",
"sha256:8cbc946bdd217148f4a9c029fcea62f4ae0f67d5346de4c865f4718cd0ddc37f", "sha256:a59fe04e67db87a63bc9f3722210e672c0b0577707e51dd121d1480afdec0c28",
"sha256:9ceefd30076f732530cf84a1be2ecb2fa9931af932706ded760a6d37c73b96ad", "sha256:ac163f12a1e07a441976261367e2dfd374e050ec81a199099b9ef01143d3b01b",
"sha256:ad69c170fda41b07a4c6b668a3128e7a759f50d9aebcfcde0ccff1358abe0423", "sha256:b63b0f6a73df3533181c310af48a5acc6acdb64deb3a36e4082264a7e98f3ca2",
"sha256:b715fe182189fb6870fab5b0383bb2fb278c88c46eade346b0f4c1ed8818c09d", "sha256:c3bba19636181cbe9b20dd382eec2c64c1df7ae410089c63ee20aa1d5d14dfa4",
"sha256:bb01ecf95083ffcb9ad542dc5342ccc1059e46f1395fd966629d36d9cc766b4a", "sha256:c8f70fb7453825bcbbe77da56132a22567d4ffbfe8ab8cb801d06fb56b624f6a",
"sha256:bd6328547219cf48cefb4e0a1bc54442910594de1c5a5feae847d9ff3c629031", "sha256:dd6dd1c15f770da01c03531095b8fbd1932df225297dc13f4987ca1260c2d723",
"sha256:edb128379bb1dea76b5bdbdacf5657a6e4754bacc2049640762725590d8ed905", "sha256:e6f5dc7e2a969e73134f7fd7876a7bd2a186e6284e0ed56745d7836626abed15",
"sha256:f8e687900557fcd4c51b4e72b9e337fdae9e2c81049d1d80b624bb2e88b5769d", "sha256:ef8f2935b4380b3ed797bfbb12d143cf01fe62bdec14018813fd4cb029495999",
"sha256:fe0ca120e3347c851c34a91041d574f3c588d832023906d8ae18d66d042e8a52", "sha256:f2a75b290f2740ccaad077240ec8d5f963991efd63369b2e4b5d2d046b22632e",
"sha256:fe8e0152672f24d8bfdecc725f97e9013f2de1b41849150959526ca3562bd3ef" "sha256:f81ea51e868f075515bc9f805710105ca759fc01c29ee3cd500186a2d17e21c2"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.2.0" "version": "==2.2.4"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
"sha256:006de60d7580d81f4a1a7e9f0173dc90a932e3905cc4d47ea909bc946302311a", "sha256:165c88bc9d8dba670110c689e3cc5c71dbe4bfb984ffa7cbebf1fac9554071d6",
"sha256:0a2e8d03787ec7ad71dc18aec9367c946ef8ef50e1e78c71f743bc3a770f9fae", "sha256:22d070ca2e60c99929ef274cfced04294d2368193e935c5d6febfd8b601bf865",
"sha256:0eeeae397e5a79dc088d8297a4c2c6f901f8fb30db47795113a4a605d0f1e5ce", "sha256:2353834b2c49b95e1313fb34edf18fca4d57446675d05298bb694bca4b194174",
"sha256:11c5c6e9b02c9dac08af04f093eb5a2f84857df70a7d4a6a6ad461aca803fb9e", "sha256:39725acf2d2e9c17356e6835dccebe7a697db55f25a09207e38b835d5e1bc032",
"sha256:2fb113757a369a6cdb189f8df3226e995acfed0a8919a72416626af1a0a71140", "sha256:3de6b2ee4f78c6b3d89d184ade5d8fa68af0848f9b6b6da2b9ab7943ec46971a",
"sha256:4b0ef2470c4979e345e4e0cc1bbac65fda11d0d7b789dbac035e4c6ce3f98adb", "sha256:47c0d93ee9c8b181f353dbead6530b26980fe4f5485aa18be8f1fd3c3cbc685e",
"sha256:59e903ca800c8cfd1ebe482349ec7c35687b95e98cefae213e271c8c7fffa021", "sha256:5e2fe3bb2363b862671eba632537cd3a823847db4d98be95690b7e382f3d6378",
"sha256:5a3342d34289715928c914ee7f389351eb37fa4857caa9297fc7948f2ed3e53d", "sha256:604815c55fd92e735f9738f65dabf4edc3e79f88541c221d292faec1904a4b17",
"sha256:5abd653a23c35d980b332bc0431d39663b1709d64142e3652890df4c9b6970f6", "sha256:6c5275bd82711cd3dcd0af8ce0bb99113ae8911fc2952805f1d012de7d600a4c",
"sha256:5f9403af9c790cc18411ea398a6950ee2def2a830ad0cfe6dc9122e6d528b302", "sha256:731ca5aabe9085160cf68b2dbef95fc1991015bc0a3a6ea46a371ab88f3d0913",
"sha256:6b4a8fd632b4ebee28282a9fef4c341835a1aa8671e2770b6f89adc8e8c2703c", "sha256:7612520e5e1a371d77e1d1ca3a3ee6227eef00d0a9cddb4ef7ecb0b7396eddf7",
"sha256:6c1aca8231625115104a06e4389fcd9ec88f0c9befbabd80dc206c35561be271", "sha256:7916cbc94f1c6b1301ac04510d0881b9e9feb20ae34094d3615a8a7c3db0dcc0",
"sha256:795e91a60f291e75de2e20e6bdd67770f793c8605b553cb6e4387ce0cb302e09", "sha256:81c3fa9a75d9f1afafdb916d5995633f319db09bd773cb56b8e39f1e98d90820",
"sha256:7ba0ba61252ab23052e642abdb17fd08fdcfdbbf3b74c969a30c58ac1ade7cd3", "sha256:887668e792b7edbfb1d3c9d8b5d8c859269a0f0eba4dda562adb95500f60dbba",
"sha256:7c9401e68730d6c4245b8e361d3d13e1035cbc94db86b49dc7da8bec235d0015", "sha256:8c183b5c60544b49e0a66f924b18c526dfd37774811b627f70836fe01711abd3",
"sha256:81f812d8f5e8a09b246515fac141e9d10113229bc33ea073fec11403b016bcf3", "sha256:93a473b53cc6e0b3ce6bf51b1b95b7b1e7e6084be3a07e40f79b42e83503fbf2",
"sha256:895d54c0ddc78a478c80f9c438579ac15f3e27bf442c2a9aa74d41d0e4d12544", "sha256:96d4dc103d1a0fa6d47c6c55a47de5f5dafd5ef0114fa10c85a1fd8e0216284b",
"sha256:8de332053707c80963b589b22f8e0229f1be1f3ca862a932c1bcd48dafb18dd8", "sha256:a3d3e086474ef12ef13d42e5f9b7bbf09d39cf6bd4940f982263d6954b13f6a9",
"sha256:92c882b70a40c79de9f5294dc99390671e07fc0b0113d472cbea3fde15db1792", "sha256:b02a0b9f332086657852b1f7cb380f6a42403a6d9c42a4c34a561aa4530d5234",
"sha256:95edb1ed513e68bddc2aee3de66ceaf743590bf16c023fb9977adc4be15bd3f0", "sha256:b09e10ec453de97f9a23a5aa5e30b334195e8d2ddd1ce76cc32e52ba63c8b31d",
"sha256:b63d4ff734263ae4ce6593798bcfee6dbfb00523c82753a3a03cbc05555a9cc3", "sha256:b6f00ad5ebe846cc91763b1d0c6d30a8042e02b2316e27b05de04fa6ec831ec5",
"sha256:bd7bf289e05470b1bc74889d1466d9ad4a56d201f24397557b6f65c24a6844b8", "sha256:bba80df38cfc17f490ec651c73bb37cd896bc2400cfba27d078c2135223c1206",
"sha256:cc3ea6b23954da84dbee8025c616040d9aa5eaf34ea6895a0a762ee9d3e12e11", "sha256:c3d911614b008e8a576b8e5303e3db29224b455d3d66d1b2848ba6ca83f9ece9",
"sha256:cc9ec588c6ef3a1325fa032ec14d97b7309db493782ea8c304666fb10c3bd9a7", "sha256:ca20739e303254287138234485579b28cb0d524401f83d5129b5ff9d606cb0a8",
"sha256:d3d07c86d4efa1facdf32aa878bd508c0dc4f87c48125cc16b937baa4e5b5e11", "sha256:cb192176b477d49b0a327b2a5a4979552b7a58cd42037034316b8018ac3ebb59",
"sha256:d8a96747df78cda35980905bf26e72960cba6d355ace4780d4bdde3b217cdf1e", "sha256:cdbbe7dff4a677fb555a54f9bc0450f2a21a93c5ba2b44e09e54fcb72d2bd13d",
"sha256:e38d58d9138ef972fceb7aeec4be02e3f01d383723965bfcef14d174c8ccd039", "sha256:d355502dce85ade85a2511b40b4c61a128902f246504f7de29bbeec1ae27933a",
"sha256:eb472586374dc66b31e36e14720747595c2b265ae962987261f044e5cce644b5", "sha256:dc577f4cfdda354db3ae37a572428a90ffdbe4e51eda7849bf442fb803f09c9b",
"sha256:fbd922f702582cb0d71ef94442bfca57624352622d75e3be7a1e7e9360b07e72" "sha256:dd9eef866c70d2cbbea1ae58134eaffda0d4bfea403025f4db6859724b18ab3d"
], ],
"index": "pypi", "index": "pypi",
"version": "==8.0.1" "version": "==8.1.0"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
@ -590,10 +573,10 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
], ],
"version": "==2020.4" "version": "==2020.5"
}, },
"redis": { "redis": {
"hashes": [ "hashes": [
@ -654,50 +637,50 @@
}, },
"reportlab": { "reportlab": {
"hashes": [ "hashes": [
"sha256:0008b5baa39d7e3a8132c4b47ecae88d6858ad386518e754e5e7b8025ee4722b", "sha256:009fa61710647cdc62eb373345248d8ebb93583a058990f7c4f9be46d90aa5b1",
"sha256:0ad5a540c336941272fe161ef3a9830da3d4b3a65a195531cebd3cad5db58b2a", "sha256:04a08d284da86882ec3a41a7c719833362ef891b09ee8e2fbb47cee352aa684a",
"sha256:0c965a5691686d746f558ee1c52aa9c63a01a0e13cba61ffc661573948e32f61", "sha256:07bff6742fba612da8d1b1f783c436338c6fdc6962828159827d5ca7d2b67935",
"sha256:0fd568fa5615ae99f76289c52ff230207852ee942d4934f6c893c93d2a79544e", "sha256:09fb11ab1500e679fc1b01199d2fed24435499856e75043a9ac0d31dd48fd881",
"sha256:1117d905a3404c696869c7aabec9454b43ed6acbbc73f9256c6fcea23e7ae93e", "sha256:18a876449c9000c391dd3415ebc8454cd7bb9e488977b894886a2d7d018f16cd",
"sha256:1ea7c388e91ad9d823655ad6a13751ff67e8a0e7cf4065cf051b4c931cdd9450", "sha256:18eec161411026dde49767bee4e5e8eeb8014879554811a62581dc7433628d5b",
"sha256:26c0ee8f62652cc7fcdc47a1cb3b34775a4d625738025c1a7edb8718bda5a315", "sha256:19353aead39fc115a4d6c598d6fb9fa26da7e69160a0443ebb49b02903e704e8",
"sha256:368c5b3fc3d5a541cb9dcacefa563fdb445365f517e3cbf64b4326631d1cf13c", "sha256:1b85c20e89c22ae902ca973df2afdd2d64d27dc4ffd2b29ebad8c805a213756b",
"sha256:451d42fdcdd7d84587d6d9c8f5d9a7d0e997305efb606705063ca1fe8bcca551", "sha256:1da3d7a35f918cee905facfa94bd00ae6091cadc06dca1b0b31b69ae02d41d1d",
"sha256:47394acba4da8e56ef8e55d8eb483b868521696ba49ab0f0fcf8a1a4a5ac6e49", "sha256:1e484ce83dae26cb40fcbd312d45b8ba921de7856a00339d867dd4ecf145a1e7",
"sha256:51b16e297f7b937fc530dd151e4b38f1d305b01c9aa10657bc32a5d2901b8ad7", "sha256:33f3cfdc492575f8af3225701301a7e62fc478358729820c9e0091aff5831378",
"sha256:51c0cdcf606ded0a7b4b50050400f25125ea797fbfc3c817135993b38f8b764e", "sha256:3b0026c1129147befd4e5a8cf25da8dea1096fce371e7b2412e36d7254019c06",
"sha256:55c672c579618843e0fd00140fb71f1ffebc4f1c542ac385c4f4999f2f5398d9", "sha256:3d7713dddaa8081ed709a1fa2456a43f6a74b0f07d605da8441fd53fef334f69",
"sha256:5c34a96ecfbf595caf16178a06abcd26a5f8720e01fe1285d4c97333382cfaeb", "sha256:3e2b4d69763103b9dc9b54c0952dc3cee05cedd06e28c0987fad7f84705b12c0",
"sha256:61aa89a00754b18c4f2956b8bff831f1fd3affef6476dc63462d92211941605e", "sha256:4ca5233a19a5ceca23546290f43addec2345789c7d65bb32f8b2668aa148351f",
"sha256:62234d29c97279917903e4587faf240a5dea4617be250db55386ff268eb5a7c5", "sha256:5214a289cf01ebbd65e49bae83709671dd9edb601891cf0ae8abf85f3c0b392f",
"sha256:670f2a8dcc23bf798c39b95c64bf76ee387549b962f76783670821978a226663", "sha256:52f8237654acbc78ea2fa6fb4a6a06e5b023b6da93f7889adfe2deba09473fad",
"sha256:69387f171f6c7b55109caa6d061b17a18f2f9e724a0212c07cd692aeb369dd19", "sha256:5ed00894e0f8281c0b7c0494b4d3067c641fd90c8e5cf933089ec4cc9a48e491",
"sha256:6c5c8871b659f7c2975382d7b61f3c182701fa9eb62cf649c3c73ba8fc5e2595", "sha256:6191961533d49c9d860964d42bada4d7ac3bb28502d984feb8034093f2012fa8",
"sha256:80139ceb3a568f5be908094f1701fd05391b71425e8b69aaed0d30db647ca2aa", "sha256:6f3ad2b1afe99c436563cd436d8693d4a12e2c4bd45f70c7705759ff7837fe53",
"sha256:80661a76d0019b5e2c315ccd3bc7093d754067d6142b36a3a0ec4f416073d23b", "sha256:739b743b7ca1ba4b4d64c321de6fccb49b562d0507ea06c817d9cc4faed5cd22",
"sha256:85a2236f324ae336da7f4b183fa99bed261bcc00ac1255ee91a504e68b086d00", "sha256:792efba0c0c6e4ee94f6dc95f305451733ee9230a1c7d51cb8e5301a549e0dfb",
"sha256:89a3acd98bd4478d6bbc5cb32e0665ea546c98bff8b58d5e1014659daa6ef75a", "sha256:79d63ca40231ca3860859b39a92daa5219035ba9553da89a5e1b218550744121",
"sha256:8a39119fcab146bde41fd1c6d148f9ee1e2cca10c6f9c2b7eb4dd710a3a2c6ac", "sha256:83b28104edd58ad65748d2d0e60e0d97e3b91b3e90b4573ea6fe60de6811972c",
"sha256:9c31c2526401da6cc92018f68483f2aac0a731cb98435445ea4b72d46b438c84", "sha256:85650446538cd2f606ca234634142a7ccd74cb6db7cfec250f76a4242e0f2431",
"sha256:9e8ae1c3b8a1697147c5c97f00d66ab1c54d88c4615b0cdd9b1a667d7baf3eb7", "sha256:8850eba6de6eb813036eb8dce353e40d60c8af48bbce107de82770b10d3aa525",
"sha256:a479c38ab2b997ce05d3bef906783ac20cf4cb224a154e80c9018c5e4d943a35", "sha256:9da445cb79e3f740756924c053edc952cde11a65ff5af8acfda3c0a1317136ef",
"sha256:a79aab8d069543d5085d58260f18705a08acd92a4501a41261913fddc2137d46", "sha256:9fabd5fbd24f5971085ffe53150d663f158f7d3050b25c95736e29ebf676d454",
"sha256:b0a8314383de853599ca531dfe55eaa49bb8d6b0bb663b2f8479b7a0f3385ea2", "sha256:a0c377bc45e73c3f15f55d7de69fab270d174749d5b454ab0de502b15430ec2a",
"sha256:b3d9926e64bd8008007b2d9819d7b30179b069ce95431d5060f71afc36885389", "sha256:a1d3f7022a920d4a5e165d264581f1862e1c1b877ceeabb96fe98cec98125ae5",
"sha256:c2a9a77ce4f25ffb52d705be82a9f41b47f6b0da23870ebc3587709e7242da30", "sha256:a315edef5c5610b0c75790142f49487e89ea34397fc247ae8aa890fe6d6dd057",
"sha256:c578dd0799f70fb577474cd383f035c6e1057e4fe837278113f9cfa6eee4b076", "sha256:a755cca2dcf023130b03bb671670301a992157d5c3151d838c0b68ef89894536",
"sha256:c5abd9d0023ad20030524ab0d5fa39d77aed025519b1fa426304ab2dd0328b89", "sha256:b1b20208ecdfffd7ca027955c4fe8972b28b30a4b3b80cf25099a08d3b20ed7c",
"sha256:ced96125525ba21311e9512adf391170b9e149f89e27e45b06ff07b70f97a0b2", "sha256:b26d6f416891cef93411d6d478a25db275766081a5fb66368248293ef459f3be",
"sha256:d692fb88d6ef5e75242b00009b54953a0425eaa8bd3a36db9db8b396785e1f57", "sha256:b4ba4c30af7044ee987e61c88a5ffb76031ca0c53666bc85d823b7de55ddbc75",
"sha256:d70c2104286459658e61388af9eee838b612986bd8a36e1d21ba36152983ac15", "sha256:b71faf3b6e4d7058e1af1b8afedaf39a962db4a219affc8177009d8244ec10d4",
"sha256:de47c65c10ac6f0d2addb28f1b1657b1c707aca014d09d01b3b728cf19e8f791", "sha256:cfa854bea525f8c913cb77e2bda724d94b965a0eb3bcfc4a645a9baa29bb86e2",
"sha256:e6e7592527791841db0820a72c6afae52655a05b0b6d4df184fd2bafe82ee1ee", "sha256:dd9687359e466086b9f6fe6d8069034017f8b6ca3080944fae5709767ca6814e",
"sha256:e8a7e95ee6ea5566291b59ede5b9fadce809dca43ebfbfe11e3ff3d6492c6f0e", "sha256:de0c675fc2998a7eaa929c356ba49c84f53a892e9ab25e8ee7d8ebbbdcb2ac16",
"sha256:f041759138b3a95508c4281b3db3bf9bb28636d84c554272a58a5ca7c9f9bbf4", "sha256:e2b4e33fea2ce9d3a14ea39191b169e41eb2ac995274f54ac8fd27519974bce8",
"sha256:f39c7fc1fa2e4a1d9747a3effd70731a9d0e9eb5738247fa089c059eff19d43e", "sha256:f3d4a1a273dc141e03b72a553c11bc14dd7a27ec7654a071edcf83eb04f004bc",
"sha256:f65ac89ee0ba569f5279360eae08783f7f2e95c9810a9846c957fbd5950f4896" "sha256:ff547cf4c1de7e104cad1a378431ff81efcb03e90e40871ee686107da5b91442"
], ],
"version": "==3.5.56" "version": "==3.5.59"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
@ -803,11 +786,11 @@
}, },
"tqdm": { "tqdm": {
"hashes": [ "hashes": [
"sha256:38b658a3e4ecf9b4f6f8ff75ca16221ae3378b2e175d846b6b33ea3a20852cf5", "sha256:556c55b081bd9aa746d34125d024b73f0e2a0e62d5927ff0e400e20ee0a03b9a",
"sha256:d4f413aecb61c9779888c64ddf0c62910ad56dcbe857d8922bb505d4dbff0df1" "sha256:b8b46036fd00176d0870307123ef06bb851096964fa7fc578d789f90ce82c3e4"
], ],
"index": "pypi", "index": "pypi",
"version": "==4.54.1" "version": "==4.55.1"
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
@ -835,11 +818,26 @@
}, },
"watchdog": { "watchdog": {
"hashes": [ "hashes": [
"sha256:3caefdcc8f06a57fdc5ef2d22aa7c0bfda4f55e71a0bee74cbf3176d97536ef3", "sha256:016b01495b9c55b5d4126ed8ae75d93ea0d99377084107c33162df52887cee18",
"sha256:e38bffc89b15bafe2a131f0e1c74924cf07dcec020c2e0a26cccd208831fcd43" "sha256:101532b8db506559e52a9b5d75a308729b3f68264d930670e6155c976d0e52a0",
"sha256:27d9b4666938d5d40afdcdf2c751781e9ce36320788b70208d0f87f7401caf93",
"sha256:2f1ade0d0802503fda4340374d333408831cff23da66d7e711e279ba50fe6c4a",
"sha256:376cbc2a35c0392b0fe7ff16fbc1b303fd99d4dd9911ab5581ee9d69adc88982",
"sha256:57f05e55aa603c3b053eed7e679f0a83873c540255b88d58c6223c7493833bac",
"sha256:5f1f3b65142175366ba94c64d8d4c8f4015825e0beaacee1c301823266b47b9b",
"sha256:602dbd9498592eacc42e0632c19781c3df1728ef9cbab555fab6778effc29eeb",
"sha256:68744de2003a5ea2dfbb104f9a74192cf381334a9e2c0ed2bbe1581828d50b61",
"sha256:85e6574395aa6c1e14e0f030d9d7f35c2340a6cf95d5671354ce876ac3ffdd4d",
"sha256:b1d723852ce90a14abf0ec0ca9e80689d9509ee4c9ee27163118d87b564a12ac",
"sha256:d948ad9ab9aba705f9836625b32e965b9ae607284811cd98334423f659ea537a",
"sha256:e2a531e71be7b5cc3499ae2d1494d51b6a26684bcc7c3146f63c810c00e8a3cc",
"sha256:e7c73edef48f4ceeebb987317a67e0080e5c9228601ff67b3c4062fa020403c7",
"sha256:ee21aeebe6b3e51e4ba64564c94cee8dbe7438b9cb60f0bb350c4fa70d1b52c2",
"sha256:f1d0e878fd69129d0d68b87cee5d9543f20d8018e82998efb79f7e412d42154a",
"sha256:f84146f7864339c8addf2c2b9903271df21d18d2c721e9a77f779493234a82b5"
], ],
"index": "pypi", "index": "pypi",
"version": "==0.10.4" "version": "==1.0.2"
}, },
"wcwidth": { "wcwidth": {
"hashes": [ "hashes": [
@ -922,53 +920,68 @@
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"markers": "python_version >= '3.1'", "markers": "python_version >= '3.1'",
"version": "==3.0.4" "version": "==4.0.0"
}, },
"coverage": { "coverage": {
"hashes": [ "hashes": [
"sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
"sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", "sha256:262066798d786ad67a13c7ba869e3ce0e39609f99f6d6c80160ad602c4808e32",
"sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
"sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
"sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
"sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
"sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
"sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
"sha256:3188a7dfd96f734a7498f37cde6598b1e9c084f1ca68bc1aa04e88db31168ab6", "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
"sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
"sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
"sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
"sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
"sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
"sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
"sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
"sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
"sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
"sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
"sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
"sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
"sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
"sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
"sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
"sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
"sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
"sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
"sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
"sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
"sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
"sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
"sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
"sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
"sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
"sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8", "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
"sha256:ef221855191457fffeb909d5787d1807800ab4d0111f089e6c93ee68f577634d" "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4",
"sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879",
"sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f",
"sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4",
"sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044",
"sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e",
"sha256:eb33c4c858d06bd8d79713c7628d3f2b50fb1c62071e2e88cb44876be03eabe1",
"sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899",
"sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f",
"sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448",
"sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714",
"sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2",
"sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d",
"sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd",
"sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7",
"sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==5.3" "version": "==5.3.1"
}, },
"coveralls": { "coveralls": {
"hashes": [ "hashes": [
@ -1010,19 +1023,19 @@
}, },
"factory-boy": { "factory-boy": {
"hashes": [ "hashes": [
"sha256:d8626622550c8ba31392f9e19fdbcef9f139cf1ad643c5923f20490a7b3e2e3d", "sha256:1d3db4b44b8c8c54cdd8b83ae4bdb9aeb121e464400035f1f03ae0e1eade56a4",
"sha256:ded73e49135c24bd4d3f45bf1eb168f8d290090f5cf4566b8df3698317dc9c08" "sha256:401cc00ff339a022f84d64a4339503d1689e8263a4478d876e58a3295b155c5b"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.1.0" "version": "==3.2.0"
}, },
"faker": { "faker": {
"hashes": [ "hashes": [
"sha256:1fcb415562ee6e2395b041e85fa6901d4708d30b84d54015226fa754ed0822c3", "sha256:7b0c4bb678be21a68640007f254259c73d18f7996a3448267716423360519732",
"sha256:e8beccb398ee9b8cc1a91d9295121d66512b6753b4846eb1e7370545d46b3311" "sha256:7e98483fc273ec5cfe1c9efa9b99adaa2de4c6b610fbc62d3767088e4974b0ce"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==5.0.1" "version": "==5.3.0"
}, },
"filelock": { "filelock": {
"hashes": [ "hashes": [
@ -1051,19 +1064,19 @@
}, },
"importlib-metadata": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:6112e21359ef8f344e7178aa5b72dc6e62b38b0d008e6d3cb212c5b84df72013", "sha256:5c5a2720817414a6c41f0a49993908068243ae02c1635a228126519b509c8aed",
"sha256:b0c2d3b226157ae4517d9625decf63591461c66b3a808c2666d538946519d170" "sha256:bf792d480abbd5eda85794e4afb09dd538393f7d6e6ffef6e9f03d2014cf9450"
], ],
"markers": "python_version < '3.8'", "markers": "python_version < '3.8'",
"version": "==3.1.1" "version": "==3.3.0"
}, },
"importlib-resources": { "importlib-resources": {
"hashes": [ "hashes": [
"sha256:7b51f0106c8ec564b1bef3d9c588bc694ce2b92125bbb6278f4f2f5b54ec3592", "sha256:0a948d0c8c3f9344de62997e3f73444dbba233b1eaf24352933c2d264b9e4182",
"sha256:a3d34a8464ce1d5d7c92b0ea4e921e696d86f2aa212e684451cb1482c8d84ed5" "sha256:6b45007a479c4ec21165ae3ffbe37faf35404e2041fac6ae1da684f38530ca73"
], ],
"markers": "python_version < '3.7'", "markers": "python_version < '3.7'",
"version": "==3.3.0" "version": "==4.1.1"
}, },
"iniconfig": { "iniconfig": {
"hashes": [ "hashes": [
@ -1126,11 +1139,11 @@
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
"sha256:05af3bb85d320377db281cf254ab050e1a7ebcbf5410685a9a407e18a1f81236", "sha256:24e0da08660a87484d1602c30bb4902d74816b6985b93de36926f5bc95741858",
"sha256:eb41423378682dadb7166144a4926e443093863024de508ca5c9737d6bc08376" "sha256:78598185a7008a470d64526a8059de9aaa449238f280fc9eb6b13ba6c4109093"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.7" "version": "==20.8"
}, },
"pluggy": { "pluggy": {
"hashes": [ "hashes": [
@ -1142,11 +1155,11 @@
}, },
"py": { "py": {
"hashes": [ "hashes": [
"sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2", "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342" "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.9.0" "version": "==1.10.0"
}, },
"pycodestyle": { "pycodestyle": {
"hashes": [ "hashes": [
@ -1174,11 +1187,11 @@
}, },
"pytest": { "pytest": {
"hashes": [ "hashes": [
"sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe", "sha256:1969f797a1a0dbd8ccf0fecc80262312729afea9c17f1d70ebf85c5e76c6f7c8",
"sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e" "sha256:66e419b1899bc27346cb2c993e12c5e5e8daba9073c1fbce33b9807abc95c306"
], ],
"index": "pypi", "index": "pypi",
"version": "==6.1.2" "version": "==6.2.1"
}, },
"pytest-cov": { "pytest-cov": {
"hashes": [ "hashes": [
@ -1223,11 +1236,11 @@
}, },
"pytest-xdist": { "pytest-xdist": {
"hashes": [ "hashes": [
"sha256:7c629016b3bb006b88ac68e2b31551e7becf173c76b977768848e2bbed594d90", "sha256:1d8edbb1a45e8e1f8e44b1260583107fc23f8bc8da6d18cb331ff61d41258ecf",
"sha256:82d938f1a24186520e2d9d3a64ef7d9ac7ecdf1a0659e095d18e596b8cbd0672" "sha256:f127e11e84ad37cc1de1088cb2990f3c354630d428af3f71282de589c5bb779b"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.1.0" "version": "==2.2.0"
}, },
"python-dateutil": { "python-dateutil": {
"hashes": [ "hashes": [
@ -1239,10 +1252,10 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4",
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5"
], ],
"version": "==2020.4" "version": "==2020.5"
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
@ -1269,11 +1282,11 @@
}, },
"sphinx": { "sphinx": {
"hashes": [ "hashes": [
"sha256:1e8d592225447104d1172be415bc2972bd1357e3e12fdc76edf2261105db4300", "sha256:77dec5ac77ca46eee54f59cf477780f4fb23327b3339ef39c8471abb829c1285",
"sha256:d4e59ad4ea55efbb3c05cde3bfc83bfc14f0c95aa95c3d75346fcce186a47960" "sha256:b8aa4eb5502c53d3b5ca13a07abeedacd887f7770c198952fd5b9530d973e767"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.3.1" "version": "==3.4.2"
}, },
"sphinx-rtd-theme": { "sphinx-rtd-theme": {
"hashes": [ "hashes": [

View File

@ -11,6 +11,7 @@ RUN apt-get update \
curl \ curl \
file \ file \
fonts-liberation \ fonts-liberation \
gettext \
ghostscript \ ghostscript \
gnupg \ gnupg \
icc-profiles-free \ icc-profiles-free \

View File

@ -5,6 +5,44 @@
Changelog Changelog
********* *********
paperless-ng 0.9.12
###################
* Paperless localization
* Thanks to the combined efforts of many users, Paperless is now available in English, Dutch, French and German.
* Thanks to `Jo Vandeginste`_, Paperless has optional support for Office documents such as .docx, .doc, .odt and more.
* See the :ref:`configuration<configuration-tika>` on how to enable this feature. This feature requires two additional services
(one for parsing Office documents and metadata extraction and another for converting Office documents to PDF), and is therefore
not enabled on default installations.
* As with all other documents, paperless converts Office documents to PDF and stores both the original as well as the archived PDF.
* Dark mode
* Thanks to `Michael Shamoon`_, paperless now has a dark mode. Configuration is available in the settings.
* Other changes and additions
* The PDF viewer now uses a local copy of some dependencies instead of fetching them from the internet. Thanks to `slorenz`_.
* Revamped search bar styling thanks to `Michael Shamoon`_.
* Sorting in the document list by clicking on table headers.
* A button was added to the document detail page that assigns a new ASN to a document.
* Form field validation: When providing invalid input in a form (such as a duplicate ASN or no name), paperless now has visual
indicators and clearer error messages about what's wrong.
* Paperless disables buttons with network actions (such as save and delete) when a network action is active. This indicates that
something is happening and prevents double clicking.
* When using "Save & next", the title field is focussed automatically to better support keyboard editing.
* E-Mail: Added filter rule parameters to allow inline attachments (watch out for mails with inlined images!) and attachment filename filters
with wildcards.
* Fixes
* Paperless was unable to save views when "Not assigned" was chosen in one of the filter dropdowns.
* Clearer error messages when pre and post consumption scripts do not exist.
* The post consumption script is executed later in the consumption process. Before the change, an ID was passed to the script referring to
a document that did not yet exist in the database.
paperless-ng 0.9.11 paperless-ng 0.9.11
################### ###################
@ -966,6 +1004,8 @@ bulk of the work on this big change.
* Initial release * Initial release
.. _slorenz: https://github.com/sisao
.. _Jo Vandeginste: https://github.com/jovandeginste
.. _zjean: https://github.com/zjean .. _zjean: https://github.com/zjean
.. _rYR79435: https://github.com/rYR79435 .. _rYR79435: https://github.com/rYR79435
.. _Michael Shamoon: https://github.com/shamoon .. _Michael Shamoon: https://github.com/shamoon

View File

@ -162,6 +162,12 @@ PAPERLESS_COOKIE_PREFIX=<str>
Defaults to ``""``, which does not alter the cookie names. Defaults to ``""``, which does not alter the cookie names.
PAPERLESS_ENABLE_HTTP_REMOTE_USER=<bool>
Allows authentication via HTTP_REMOTE_USER which is used by some SSO
applications.
Defaults to `false` which disables this feature.
.. _configuration-ocr: .. _configuration-ocr:
OCR settings OCR settings
@ -210,20 +216,20 @@ PAPERLESS_OCR_MODE=<mode>
into images and puts the OCRed text on top. This works for all documents, into images and puts the OCRed text on top. This works for all documents,
however, the resulting document may be significantly larger and text however, the resulting document may be significantly larger and text
won't appear as sharp when zoomed in. won't appear as sharp when zoomed in.
The default is ``skip``, which only performs OCR when necessary and always The default is ``skip``, which only performs OCR when necessary and always
creates archived documents. creates archived documents.
PAPERLESS_OCR_OUTPUT_TYPE=<type> PAPERLESS_OCR_OUTPUT_TYPE=<type>
Specify the the type of PDF documents that paperless should produce. Specify the the type of PDF documents that paperless should produce.
* ``pdf``: Modify the PDF document as little as possible. * ``pdf``: Modify the PDF document as little as possible.
* ``pdfa``: Convert PDF documents into PDF/A-2b documents, which is a * ``pdfa``: Convert PDF documents into PDF/A-2b documents, which is a
subset of the entire PDF specification and meant for storing subset of the entire PDF specification and meant for storing
documents long term. documents long term.
* ``pdfa-1``, ``pdfa-2``, ``pdfa-3`` to specify the exact version of * ``pdfa-1``, ``pdfa-2``, ``pdfa-3`` to specify the exact version of
PDF/A you wish to use. PDF/A you wish to use.
If not specified, ``pdfa`` is used. Remember that paperless also keeps If not specified, ``pdfa`` is used. Remember that paperless also keeps
the original input file as well as the archived version. the original input file as well as the archived version.
@ -275,14 +281,14 @@ PAPERLESS_OCR_USER_ARG=<json>
.. code:: json .. code:: json
{"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"} {"deskew": true, "optimize": 3, "unpaper_args": "--pre-rotate 90"}
.. _configuration-tika: .. _configuration-tika:
Tika settings Tika settings
############# #############
Paperless can make use of `Tika <https://tika.apache.org/>`_ and Paperless can make use of `Tika <https://tika.apache.org/>`_ and
`Gotenberg <https://thecodingmachine.github.io/gotenberg/>`_ for parsing and `Gotenberg <https://thecodingmachine.github.io/gotenberg/>`_ for parsing and
converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). If you converting "Office" documents (such as ".doc", ".xlsx" and ".odt"). If you
wish to use this, you must provide a Tika server and a Gotenberg server, wish to use this, you must provide a Tika server and a Gotenberg server,
@ -306,7 +312,7 @@ PAPERLESS_TIKA_GOTENBERG_ENDPOINT=<url>
Defaults to "http://localhost:3000". Defaults to "http://localhost:3000".
Software tweaks Software tweaks
############### ###############
@ -348,11 +354,14 @@ PAPERLESS_TIME_ZONE=<timezone>
Defaults to UTC. Defaults to UTC.
.. _configuration-polling:
PAPERLESS_CONSUMER_POLLING=<num> PAPERLESS_CONSUMER_POLLING=<num>
If paperless won't find documents added to your consume folder, it might If paperless won't find documents added to your consume folder, it might
not be able to automatically detect filesystem changes. In that case, not be able to automatically detect filesystem changes. In that case,
specify a polling interval in seconds here, which will then cause paperless specify a polling interval in seconds here, which will then cause paperless
to periodically check your consumption directory for changes. to periodically check your consumption directory for changes. This will also
disable listening for file system changes with ``inotify``.
Defaults to 0, which disables polling and uses filesystem notifications. Defaults to 0, which disables polling and uses filesystem notifications.
@ -438,6 +447,19 @@ PAPERLESS_THUMBNAIL_FONT_NAME=<filename>
Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``. Defaults to ``/usr/share/fonts/liberation/LiberationSerif-Regular.ttf``.
PAPERLESS_IGNORE_DATES=<string>
Paperless parses a documents creation date from filename and file content.
You may specify a comma separated list of dates that should be ignored during
this process. This is useful for special dates (like date of birth) that appear
in documents regularly but are very unlikely to be the documents creation date.
You may specify dates in a multitude of formats supported by dateparser (see
https://dateparser.readthedocs.io/en/latest/#popular-formats) but as the dates
need to be comma separated, the options are limited.
Example: "2020-12-02,22.04.1999"
Defaults to an empty string to not ignore any dates.
Binaries Binaries
######## ########

View File

@ -179,6 +179,14 @@ Docker Route
You can use any settings from the file ``paperless.conf`` in this file. You can use any settings from the file ``paperless.conf`` in this file.
Have a look at :ref:`configuration` to see whats available. Have a look at :ref:`configuration` to see whats available.
.. caution::
Certain file systems such as NFS network shares don't support file system
notifications with ``inotify``. When storing the consumption directory
on such a file system, paperless will be unable to pick up new files
with the default configuration. You will need to use ``PAPERLESS_CONSUMER_POLLING``,
which will disable inotify. See :ref:`here <configuration-polling>`.
4. Run ``docker-compose up -d``. This will create and start the necessary 4. Run ``docker-compose up -d``. This will create and start the necessary
containers. This will also build the image of paperless if you grabbed the containers. This will also build the image of paperless if you grabbed the

View File

@ -34,6 +34,9 @@ directory at startup, but won't find any other files added later, check out
the configuration file and enable filesystem polling with the setting the configuration file and enable filesystem polling with the setting
``PAPERLESS_CONSUMER_POLLING``. ``PAPERLESS_CONSUMER_POLLING``.
This will disable listening to filesystem changes with inotify and paperless will
manually check the consumption directory for changes instead.
Operation not permitted Operation not permitted
####################### #######################

View File

@ -31,6 +31,7 @@
#PAPERLESS_STATIC_URL=/static/ #PAPERLESS_STATIC_URL=/static/
#PAPERLESS_AUTO_LOGIN_USERNAME= #PAPERLESS_AUTO_LOGIN_USERNAME=
#PAPERLESS_COOKIE_PREFIX= #PAPERLESS_COOKIE_PREFIX=
#PAPERLESS_ENABLE_HTTP_REMOTE_USER=false
# OCR settings # OCR settings
@ -50,11 +51,14 @@
#PAPERLESS_TIME_ZONE=UTC #PAPERLESS_TIME_ZONE=UTC
#PAPERLESS_CONSUMER_POLLING=10 #PAPERLESS_CONSUMER_POLLING=10
#PAPERLESS_CONSUMER_DELETE_DUPLICATES=false #PAPERLESS_CONSUMER_DELETE_DUPLICATES=false
#PAPERLESS_CONSUMER_RECURSIVE=false
#PAPERLESS_CONSUMER_SUBDIRS_AS_TAGS=false
#PAPERLESS_OPTIMIZE_THUMBNAILS=true #PAPERLESS_OPTIMIZE_THUMBNAILS=true
#PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh #PAPERLESS_POST_CONSUME_SCRIPT=/path/to/an/arbitrary/script.sh
#PAPERLESS_FILENAME_DATE_ORDER=YMD #PAPERLESS_FILENAME_DATE_ORDER=YMD
#PAPERLESS_FILENAME_PARSE_TRANSFORMS=[] #PAPERLESS_FILENAME_PARSE_TRANSFORMS=[]
#PAPERLESS_THUMBNAIL_FONT_NAME= #PAPERLESS_THUMBNAIL_FONT_NAME=
#PAPERLESS_IGNORE_DATES=
# Tika settings # Tika settings

View File

@ -57,8 +57,8 @@ pipenv lock --keep-outdated -r > "$PAPERLESS_DIST_APP/requirements.txt"
# test if the application works. # test if the application works.
cd "$PAPERLESS_ROOT/src" cd "$PAPERLESS_ROOT/src"
pipenv run pytest --cov #pipenv run pytest --cov
pipenv run pycodestyle #pipenv run pycodestyle
# make the documentation. # make the documentation.
@ -81,7 +81,7 @@ cp "$PAPERLESS_ROOT/paperless.conf.example" "$PAPERLESS_DIST_APP/paperless.conf"
# copy python source, templates and static files. # copy python source, templates and static files.
cd "$PAPERLESS_ROOT" cd "$PAPERLESS_ROOT"
find src -wholename '*/templates/*' -o -wholename '*/static/*' -o -name '*.py' | cpio -pdm "$PAPERLESS_DIST_APP" find src -wholename '*/locale/*' -o -wholename '*/templates/*' -o -wholename '*/static/*' -o -name '*.py' | cpio -pdm "$PAPERLESS_DIST_APP"
# build the front end. # build the front end.

View File

@ -17,7 +17,8 @@
"sourceLocale": "en-US", "sourceLocale": "en-US",
"locales": { "locales": {
"de": "src/locale/messages.de.xlf", "de": "src/locale/messages.de.xlf",
"nl-NL": "src/locale/messages.nl_NL.xlf" "nl-NL": "src/locale/messages.nl_NL.xlf",
"fr": "src/locale/messages.fr.xlf"
} }
}, },
"architect": { "architect": {

View File

@ -140,6 +140,13 @@
</svg>&nbsp;<ng-container i18n>Logs</ng-container> </svg>&nbsp;<ng-container i18n>Logs</ng-container>
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
</svg>&nbsp;<ng-container i18n>Settings</ng-container>
</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="admin/"> <a class="nav-link" href="admin/">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">

View File

@ -9,7 +9,7 @@
<p *ngIf="message">{{message}}</p> <p *ngIf="message">{{message}}</p>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" [disabled]="!buttonsEnabled">Cancel</button> <button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()" [disabled]="!confirmButtonEnabled || !buttonsEnabled"> <button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}} {{btnCaption}}
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span> <span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>

View File

@ -1,10 +1,13 @@
import { Directive, Input, OnInit } from '@angular/core'; import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms'; import { ControlValueAccessor } from '@angular/forms';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@Directive() @Directive()
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor { export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
@ViewChild("inputField")
inputField: ElementRef
constructor() { } constructor() { }
onChange = (newValue: T) => {}; onChange = (newValue: T) => {};
@ -24,6 +27,12 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
this.disabled = isDisabled; this.disabled = isDisabled;
} }
focus() {
if (this.inputField && this.inputField.nativeElement) {
this.inputField.nativeElement.focus()
}
}
@Input() @Input()
title: string title: string

View File

@ -1,8 +1,14 @@
<div class="form-group"> <div class="form-group">
<label [for]="inputId">{{title}}</label> <label [for]="inputId">{{title}}</label>
<input type="number" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)"> <div class="input-group" [class.is-invalid]="error">
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> <input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
</div>
</div>
<div class="invalid-feedback"> <div class="invalid-feedback">
{{error}} {{error}}
</div> </div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
</div> </div>

View File

@ -1,5 +1,7 @@
import { Component, forwardRef } from '@angular/core'; import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms'; import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type';
import { DocumentService } from 'src/app/services/rest/document.service';
import { AbstractInputComponent } from '../abstract-input'; import { AbstractInputComponent } from '../abstract-input';
@Component({ @Component({
@ -14,8 +16,24 @@ import { AbstractInputComponent } from '../abstract-input';
}) })
export class NumberComponent extends AbstractInputComponent<number> { export class NumberComponent extends AbstractInputComponent<number> {
constructor() { constructor(private documentService: DocumentService) {
super() super()
} }
nextAsn() {
if (this.value) {
return
}
this.documentService.listFiltered(1, 1, "archive_serial_number", true, [{rule_type: FILTER_ASN_ISNULL, value: "false"}]).subscribe(
results => {
if (results.count > 0) {
this.value = results.results[0].archive_serial_number + 1
} else {
this.value + 1
}
this.onChange(this.value)
}
)
}
} }

View File

@ -1,5 +1,5 @@
<div class="form-group paperless-input-select paperless-input-tags"> <div class="form-group paperless-input-select paperless-input-tags">
<label for="tags">Tags</label> <label for="tags" i18n>Tags</label>
<div class="input-group flex-nowrap"> <div class="input-group flex-nowrap">
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue" <ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"

View File

@ -1,6 +1,6 @@
<div class="form-group"> <div class="form-group">
<label [for]="inputId">{{title}}</label> <label [for]="inputId">{{title}}</label>
<input type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)"> <input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small> <small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<div class="invalid-feedback"> <div class="invalid-feedback">
{{error}} {{error}}

View File

@ -56,14 +56,14 @@
<a ngbNavLink i18n>Details</a> <a ngbNavLink i18n>Details</a>
<ng-template ngbNavContent> <ng-template ngbNavContent>
<app-input-text i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text> <app-input-text #inputTitle i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text>
<app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number> <app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
<app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time> <app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time>
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" <app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
(createNew)="createCorrespondent()"></app-input-select> (createNew)="createCorrespondent()"></app-input-select>
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" <app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
(createNew)="createDocumentType()"></app-input-select> (createNew)="createDocumentType()"></app-input-select>
<app-input-tags formControlName="tags" i18n-title title="Tags"></app-input-tags> <app-input-tags formControlName="tags"></app-input-tags>
</ng-template> </ng-template>
</li> </li>

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@ -17,6 +17,7 @@ import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/c
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component'; import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
import { PDFDocumentProxy } from 'ng2-pdf-viewer'; import { PDFDocumentProxy } from 'ng2-pdf-viewer';
import { ToastService } from 'src/app/services/toast.service'; import { ToastService } from 'src/app/services/toast.service';
import { TextComponent } from '../common/input/text/text.component';
@Component({ @Component({
selector: 'app-document-detail', selector: 'app-document-detail',
@ -25,6 +26,9 @@ import { ToastService } from 'src/app/services/toast.service';
}) })
export class DocumentDetailComponent implements OnInit { export class DocumentDetailComponent implements OnInit {
@ViewChild("inputTitle")
titleInput: TextComponent
expandOriginalMetadata = false expandOriginalMetadata = false
expandArchivedMetadata = false expandArchivedMetadata = false
@ -157,6 +161,7 @@ export class DocumentDetailComponent implements OnInit {
if (nextDocId) { if (nextDocId) {
this.openDocumentService.closeDocument(this.document) this.openDocumentService.closeDocument(this.document)
this.router.navigate(['documents', nextDocId]) this.router.navigate(['documents', nextDocId])
this.titleInput.focus()
} }
}, error => { }, error => {
this.networkActive = false this.networkActive = false

View File

@ -83,8 +83,10 @@
</div> </div>
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<p i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</p> <p>
<p i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</p> <span i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
<span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>&nbsp;<span i18n *ngIf="isFiltered">(filtered)</span>
</p>
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" <ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination> [rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
</div> </div>
@ -97,12 +99,42 @@
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'"> <table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
<thead> <thead>
<th></th> <th></th>
<th class="d-none d-lg-table-cell" i18n>ASN</th> <th class="d-none d-lg-table-cell"
<th class="d-none d-md-table-cell" i18n>Correspondent</th> sortable="archive_serial_number"
<th i18n>Title</th> [currentSortField]="list.sortField"
<th class="d-none d-xl-table-cell" i18n>Document type</th> [currentSortReverse]="list.sortReverse"
<th i18n>Created</th> (sort)="onSort($event)"
<th class="d-none d-xl-table-cell" i18n>Added</th> i18n>ASN</th>
<th class="d-none d-md-table-cell"
sortable="correspondent__name"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
<th
sortable="title"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Title</th>
<th class="d-none d-xl-table-cell"
sortable="document_type__name"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Document type</th>
<th
sortable="created"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Created</th>
<th class="d-none d-xl-table-cell"
sortable="added"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''"> <tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">

View File

@ -1,8 +1,9 @@
import { Component, OnInit, ViewChild } from '@angular/core'; import { AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessDocument } from 'src/app/data/paperless-document'; import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
import { DocumentListViewService } from 'src/app/services/document-list-view.service'; import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service'; import { SavedViewService } from 'src/app/services/rest/saved-view.service';
@ -28,6 +29,8 @@ export class DocumentListComponent implements OnInit {
@ViewChild("filterEditor") @ViewChild("filterEditor")
private filterEditor: FilterEditorComponent private filterEditor: FilterEditorComponent
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>;
displayMode = 'smallCards' // largeCards, smallCards, details displayMode = 'smallCards' // largeCards, smallCards, details
getTitle() { getTitle() {
@ -38,6 +41,10 @@ export class DocumentListComponent implements OnInit {
return DOCUMENT_SORT_FIELDS return DOCUMENT_SORT_FIELDS
} }
onSort(event: SortEvent) {
this.list.setSort(event.column, event.reverse)
}
get isBulkEditing(): boolean { get isBulkEditing(): boolean {
return this.list.selected.size > 0 return this.list.selected.size > 0
} }

View File

@ -9,10 +9,10 @@
<table class="table table-striped border shadow"> <table class="table table-striped border shadow">
<thead> <thead>
<tr> <tr>
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th> <th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th> <th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th> <th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)" i18n>Last correspondence</th> <th scope="col" sortable="last_correspondence" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Last correspondence</th>
<th scope="col" i18n>Actions</th> <th scope="col" i18n>Actions</th>
</tr> </tr>
</thead> </thead>

View File

@ -10,9 +10,9 @@
<table class="table table-striped border shadow"> <table class="table table-striped border shadow">
<thead> <thead>
<tr> <tr>
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th> <th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th> <th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th> <th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" i18n>Actions</th> <th scope="col" i18n>Actions</th>
</tr> </tr>
</thead> </thead>

View File

@ -26,7 +26,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
public collectionSize = 0 public collectionSize = 0
public sortField: string public sortField: string
public sortDirection: string public sortReverse: boolean
getMatching(o: MatchingModel) { getMatching(o: MatchingModel) {
if (o.matching_algorithm == MATCH_AUTO) { if (o.matching_algorithm == MATCH_AUTO) {
@ -39,21 +39,8 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
} }
onSort(event: SortEvent) { onSort(event: SortEvent) {
this.sortField = event.column
if (event.direction && event.direction.length > 0) { this.sortReverse = event.reverse
this.sortField = event.column
this.sortDirection = event.direction
} else {
this.sortField = null
this.sortDirection = null
}
this.headers.forEach(header => {
if (header.sortable !== this.sortField) {
header.direction = '';
}
});
this.reloadData() this.reloadData()
} }
@ -62,8 +49,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
} }
reloadData() { reloadData() {
// TODO: this is a hack this.service.list(this.page, null, this.sortField, this.sortReverse).subscribe(c => {
this.service.list(this.page, null, this.sortField, this.sortDirection == 'des').subscribe(c => {
this.data = c.results this.data = c.results
this.collectionSize = c.count this.collectionSize = c.count
}); });

View File

@ -36,7 +36,7 @@
<app-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem" (change)="toggleDarkModeSetting()"></app-input-check> <app-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem" (change)="toggleDarkModeSetting()"></app-input-check>
<div class="custom-control custom-switch" *ngIf="!settingsForm.value.darkModeUseSystem"> <div class="custom-control custom-switch" *ngIf="!settingsForm.value.darkModeUseSystem">
<input type="checkbox" class="custom-control-input" id="darkModeEnabled" formControlName="darkModeEnabled" [checked]="settingsForm.value.darkModeEnabled"> <input type="checkbox" class="custom-control-input" id="darkModeEnabled" formControlName="darkModeEnabled" [checked]="settingsForm.value.darkModeEnabled">
<label class="custom-control-label" for="darkModeEnabled">Enabled</label> <label class="custom-control-label" for="darkModeEnabled" i18n>Enable dark mode</label>
</div> </div>
</div> </div>
</div> </div>
@ -92,5 +92,5 @@
<div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3 shadow"></div> <div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3 shadow"></div>
<button type="submit" class="btn btn-primary">Save</button> <button type="submit" class="btn btn-primary" i18n>Save</button>
</form> </form>

View File

@ -6,7 +6,7 @@
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<app-input-text title="Name" formControlName="name" [error]="error?.name"></app-input-text> <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
<div class="form-group paperless-input-select"> <div class="form-group paperless-input-select">

View File

@ -10,10 +10,10 @@
<table class="table table-striped border shadow-sm"> <table class="table table-striped border shadow-sm">
<thead> <thead>
<tr> <tr>
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th> <th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" i18n>Color</th> <th scope="col" i18n>Color</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th> <th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th> <th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" i18n>Actions</th> <th scope="col" i18n>Actions</th>
</tr> </tr>
</thead> </thead>

View File

@ -18,6 +18,8 @@ export const FILTER_MODIFIED_AFTER = 16
export const FILTER_DOES_NOT_HAVE_TAG = 17 export const FILTER_DOES_NOT_HAVE_TAG = 17
export const FILTER_ASN_ISNULL = 18
export const FILTER_RULE_TYPES: FilterRuleType[] = [ export const FILTER_RULE_TYPES: FilterRuleType[] = [
{id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false, default: ""}, {id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false, default: ""},
@ -45,6 +47,7 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
{id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false},
{id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false}, {id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false},
{id: FILTER_ASN_ISNULL, name: "ASN is null", filtervar: "archive_serial_number__isnull", datatype: "boolean", multi: false}
] ]
export interface FilterRuleType { export interface FilterRuleType {

View File

@ -1,17 +1,15 @@
import { Directive, EventEmitter, Input, Output } from '@angular/core'; import { Directive, EventEmitter, Input, Output } from '@angular/core';
export interface SortEvent { export interface SortEvent {
column: string; column: string
direction: string; reverse: boolean
} }
const rotate: {[key: string]: string} = { 'asc': 'des', 'des': '', '': 'asc' };
@Directive({ @Directive({
selector: 'th[sortable]', selector: 'th[sortable]',
host: { host: {
'[class.asc]': 'direction === "asc"', '[class.asc]': 'currentSortField == sortable && !currentSortReverse',
'[class.des]': 'direction === "des"', '[class.des]': 'currentSortField == sortable && currentSortReverse',
'(click)': 'rotate()' '(click)': 'rotate()'
} }
}) })
@ -19,12 +17,24 @@ export class SortableDirective {
constructor() { } constructor() { }
@Input() sortable: string = ''; @Input()
@Input() direction: string = ''; sortable: string = '';
@Input()
currentSortReverse: boolean = false
@Input()
currentSortField: string
@Output() sort = new EventEmitter<SortEvent>(); @Output() sort = new EventEmitter<SortEvent>();
rotate() { rotate() {
this.direction = rotate[this.direction]; if (this.currentSortField != this.sortable) {
this.sort.emit({column: this.sortable, direction: this.direction}); this.sort.emit({column: this.sortable, reverse: false});
} else if (this.currentSortField == this.sortable && !this.currentSortReverse) {
this.sort.emit({column: this.currentSortField, reverse: true});
} else {
this.sort.emit({column: null, reverse: false});
}
} }
} }

View File

@ -111,7 +111,8 @@ export class DocumentListViewService {
this.isReloading = false this.isReloading = false
}, },
error => { error => {
if (error.error['detail'] == 'Invalid page.') { if (this.currentPage != 1 && error.status == 404) {
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
this.currentPage = 1 this.currentPage = 1
this.reload() this.reload()
} }
@ -152,6 +153,13 @@ export class DocumentListViewService {
return this.view.sort_reverse return this.view.sort_reverse
} }
setSort(field: string, reverse: boolean) {
this.view.sort_field = field
this.view.sort_reverse = reverse
this.saveDocumentListView()
this.reload()
}
private saveDocumentListView() { private saveDocumentListView() {
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView)) sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
} }
@ -259,7 +267,7 @@ export class DocumentListViewService {
this.documentListView = null this.documentListView = null
} }
} }
if (!this.documentListView || !this.documentListView.filter_rules || !this.documentListView.sort_reverse || !this.documentListView.sort_field) { if (!this.documentListView || this.documentListView.filter_rules == null || this.documentListView.sort_reverse == null || this.documentListView.sort_field == null) {
this.documentListView = { this.documentListView = {
filter_rules: [], filter_rules: [],
sort_reverse: true, sort_reverse: true,

View File

@ -13,10 +13,10 @@ import { TagService } from './tag.service';
import { FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; import { FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
export const DOCUMENT_SORT_FIELDS = [ export const DOCUMENT_SORT_FIELDS = [
{ field: "correspondent__name", name: $localize`Correspondent` },
{ field: "document_type__name", name: $localize`Document type` },
{ field: 'title', name: $localize`Title` },
{ field: 'archive_serial_number', name: $localize`ASN` }, { field: 'archive_serial_number', name: $localize`ASN` },
{ field: "correspondent__name", name: $localize`Correspondent` },
{ field: 'title', name: $localize`Title` },
{ field: "document_type__name", name: $localize`Document type` },
{ field: 'created', name: $localize`Created` }, { field: 'created', name: $localize`Created` },
{ field: 'added', name: $localize`Added` }, { field: 'added', name: $localize`Added` },
{ field: 'modified', name: $localize`Modified` } { field: 'modified', name: $localize`Modified` }

File diff suppressed because it is too large Load Diff

View File

@ -174,6 +174,17 @@ $border-color-dark-mode: #47494f;
color: $text-color-dark-mode; color: $text-color-dark-mode;
border-color: $border-color-dark-mode; border-color: $border-color-dark-mode;
.des,
.asc {
background-color: transparent !important;
color: $text-color-dark-mode;
border-color: $border-color-dark-mode;
&::after {
filter: invert(0.8); /* arrow is a black inline png bkgd image (!) so use filter */
}
}
tr:hover { tr:hover {
background-color: $bg-light-dark-mode; background-color: $bg-light-dark-mode;
color: $text-color-dark-mode-accent; color: $text-color-dark-mode-accent;
@ -250,13 +261,18 @@ $border-color-dark-mode: #47494f;
background-color: $bg-dark-mode !important; background-color: $bg-dark-mode !important;
} }
.form-control, .form-control:not(.is-invalid):not(.btn),
input:not(.is-invalid),
textarea:not(.is-invalid) {
border-color: $border-color-dark-mode; /* we dont want to override controls that get highlighting for errors */
}
.form-control:not(.btn),
input, input,
select, select,
textarea { textarea {
background-color: $bg-dark-mode; background-color: $bg-dark-mode;
color: $text-color-dark-mode; color: $text-color-dark-mode;
border-color: $border-color-dark-mode;
&::placeholder { &::placeholder {
color: $text-color-dark-mode; color: $text-color-dark-mode;
@ -325,6 +341,12 @@ $border-color-dark-mode: #47494f;
.progress { .progress {
background-color: $border-color-dark-mode; background-color: $border-color-dark-mode;
} }
.alert-danger {
color: $text-color-dark-mode-accent;
background-color: darken($danger-dark-mode, 20%);
border-color: darken($danger-dark-mode, 20%);
}
} }
body.color-scheme-dark { body.color-scheme-dark {

View File

@ -71,6 +71,11 @@ class Consumer(LoggingMixin):
if not settings.PRE_CONSUME_SCRIPT: if not settings.PRE_CONSUME_SCRIPT:
return return
if not os.path.isfile(settings.PRE_CONSUME_SCRIPT):
raise ConsumerError(
f"Configured pre-consume script "
f"{settings.PRE_CONSUME_SCRIPT} does not exist.")
try: try:
Popen((settings.PRE_CONSUME_SCRIPT, self.path)).wait() Popen((settings.PRE_CONSUME_SCRIPT, self.path)).wait()
except Exception as e: except Exception as e:
@ -82,6 +87,11 @@ class Consumer(LoggingMixin):
if not settings.POST_CONSUME_SCRIPT: if not settings.POST_CONSUME_SCRIPT:
return return
if not os.path.isfile(settings.POST_CONSUME_SCRIPT):
raise ConsumerError(
f"Configured post-consume script "
f"{settings.POST_CONSUME_SCRIPT} does not exist.")
try: try:
Popen(( Popen((
settings.POST_CONSUME_SCRIPT, settings.POST_CONSUME_SCRIPT,
@ -252,8 +262,6 @@ class Consumer(LoggingMixin):
self.log("debug", "Deleting file {}".format(self.path)) self.log("debug", "Deleting file {}".format(self.path))
os.unlink(self.path) os.unlink(self.path)
self.run_post_consume_script(document)
except Exception as e: except Exception as e:
self.log( self.log(
"error", "error",
@ -264,6 +272,8 @@ class Consumer(LoggingMixin):
finally: finally:
document_parser.cleanup() document_parser.cleanup()
self.run_post_consume_script(document)
self.log( self.log(
"info", "info",
"Document {} consumption finished".format(document) "Document {} consumption finished".format(document)

View File

@ -4,7 +4,7 @@ from .models import Correspondent, Document, Tag, DocumentType, Log
CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"] CHAR_KWARGS = ["istartswith", "iendswith", "icontains", "iexact"]
ID_KWARGS = ["in", "exact"] ID_KWARGS = ["in", "exact"]
INT_KWARGS = ["exact", "gt", "gte", "lt", "lte"] INT_KWARGS = ["exact", "gt", "gte", "lt", "lte", "isnull"]
DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"] DATE_KWARGS = ["year", "month", "day", "date__gt", "gt", "date__lt", "lt"]

View File

@ -13,8 +13,14 @@ from ...parsers import get_parser_class_for_mime_type
def _process_document(doc_in): def _process_document(doc_in):
document = Document.objects.get(id=doc_in) document = Document.objects.get(id=doc_in)
parser = get_parser_class_for_mime_type(document.mime_type)( parser_class = get_parser_class_for_mime_type(document.mime_type)
logging_group=None)
if parser_class:
parser = parser_class(logging_group=None)
else:
print(f"{document} No parser for mime type {document.mime_type}")
return
try: try:
thumb = parser.get_optimised_thumbnail( thumb = parser.get_optimised_thumbnail(
document.source_path, document.mime_type) document.source_path, document.mime_type)

View File

@ -210,6 +210,13 @@ def parse_date(filename, text):
} }
) )
def __filter(date):
if date and date.year > 1900 and \
date <= timezone.now() and \
date.date() not in settings.IGNORE_DATES:
return date
return None
date = None date = None
# if filename date parsing is enabled, search there first: # if filename date parsing is enabled, search there first:
@ -223,7 +230,8 @@ def parse_date(filename, text):
# Skip all matches that do not parse to a proper date # Skip all matches that do not parse to a proper date
continue continue
if date and date.year > 1900 and date <= timezone.now(): date = __filter(date)
if date is not None:
return date return date
# Iterate through all regex matches in text and try to parse the date # Iterate through all regex matches in text and try to parse the date
@ -236,10 +244,9 @@ def parse_date(filename, text):
# Skip all matches that do not parse to a proper date # Skip all matches that do not parse to a proper date
continue continue
if date and date.year > 1900 and date <= timezone.now(): date = __filter(date)
if date is not None:
break break
else:
date = None
return date return date

View File

@ -36,7 +36,7 @@
<body class="text-center"> <body class="text-center">
<div class="form-signin"> <div class="form-signin">
<img class="mb-4" src="{% static 'frontend/assets/logo.svg' %}" alt="" width="300"> <img class="mb-4" src="{% static 'frontend/en-US/assets/logo.svg' %}" alt="" width="300">
<p>You have been successfully logged out. Bye!</p> <p>You have been successfully logged out. Bye!</p>
<a href="/">Sign in again</a> <a href="/">Sign in again</a>
</div> </div>

View File

@ -37,7 +37,7 @@
<body class="text-center"> <body class="text-center">
<form class="form-signin" method="post"> <form class="form-signin" method="post">
{% csrf_token %} {% csrf_token %}
<img class="mb-4" src="{% static 'frontend/assets/logo.svg' %}" alt="" width="300"> <img class="mb-4" src="{% static 'frontend/en-US/assets/logo.svg' %}" alt="" width="300">
<p>Please sign in.</p> <p>Please sign in.</p>
{% if form.errors %} {% if form.errors %}
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">

View File

@ -468,6 +468,42 @@ class TestConsumer(DirectoriesMixin, TestCase):
self.assertTrue(os.path.isfile(dst)) self.assertTrue(os.path.isfile(dst))
class PreConsumeTestCase(TestCase):
@mock.patch("documents.consumer.Popen")
@override_settings(PRE_CONSUME_SCRIPT=None)
def test_no_pre_consume_script(self, m):
c = Consumer()
c.path = "path-to-file"
c.run_pre_consume_script()
m.assert_not_called()
@mock.patch("documents.consumer.Popen")
@override_settings(PRE_CONSUME_SCRIPT="does-not-exist")
def test_pre_consume_script_not_found(self, m):
c = Consumer()
c.path = "path-to-file"
self.assertRaises(ConsumerError, c.run_pre_consume_script)
@mock.patch("documents.consumer.Popen")
def test_pre_consume_script(self, m):
with tempfile.NamedTemporaryFile() as script:
with override_settings(PRE_CONSUME_SCRIPT=script.name):
c = Consumer()
c.path = "path-to-file"
c.run_pre_consume_script()
m.assert_called_once()
args, kwargs = m.call_args
command = args[0]
self.assertEqual(command[0], script.name)
self.assertEqual(command[1], "path-to-file")
class PostConsumeTestCase(TestCase): class PostConsumeTestCase(TestCase):
@mock.patch("documents.consumer.Popen") @mock.patch("documents.consumer.Popen")
@ -483,36 +519,45 @@ class PostConsumeTestCase(TestCase):
m.assert_not_called() m.assert_not_called()
@mock.patch("documents.consumer.Popen")
@override_settings(POST_CONSUME_SCRIPT="script") @override_settings(POST_CONSUME_SCRIPT="does-not-exist")
def test_post_consume_script_simple(self, m): def test_post_consume_script_not_found(self):
doc = Document.objects.create(title="Test", mime_type="application/pdf") doc = Document.objects.create(title="Test", mime_type="application/pdf")
Consumer().run_post_consume_script(doc) self.assertRaises(ConsumerError, Consumer().run_post_consume_script, doc)
m.assert_called_once() @mock.patch("documents.consumer.Popen")
def test_post_consume_script_simple(self, m):
with tempfile.NamedTemporaryFile() as script:
with override_settings(POST_CONSUME_SCRIPT=script.name):
doc = Document.objects.create(title="Test", mime_type="application/pdf")
Consumer().run_post_consume_script(doc)
m.assert_called_once()
@mock.patch("documents.consumer.Popen") @mock.patch("documents.consumer.Popen")
@override_settings(POST_CONSUME_SCRIPT="script")
def test_post_consume_script_with_correspondent(self, m): def test_post_consume_script_with_correspondent(self, m):
c = Correspondent.objects.create(name="my_bank") with tempfile.NamedTemporaryFile() as script:
doc = Document.objects.create(title="Test", mime_type="application/pdf", correspondent=c) with override_settings(POST_CONSUME_SCRIPT=script.name):
tag1 = Tag.objects.create(name="a") c = Correspondent.objects.create(name="my_bank")
tag2 = Tag.objects.create(name="b") doc = Document.objects.create(title="Test", mime_type="application/pdf", correspondent=c)
doc.tags.add(tag1) tag1 = Tag.objects.create(name="a")
doc.tags.add(tag2) tag2 = Tag.objects.create(name="b")
doc.tags.add(tag1)
doc.tags.add(tag2)
Consumer().run_post_consume_script(doc) Consumer().run_post_consume_script(doc)
m.assert_called_once() m.assert_called_once()
args, kwargs = m.call_args args, kwargs = m.call_args
command = args[0] command = args[0]
self.assertEqual(command[0], "script") self.assertEqual(command[0], script.name)
self.assertEqual(command[1], str(doc.pk)) self.assertEqual(command[1], str(doc.pk))
self.assertEqual(command[5], f"/api/documents/{doc.pk}/download/") self.assertEqual(command[5], f"/api/documents/{doc.pk}/download/")
self.assertEqual(command[6], f"/api/documents/{doc.pk}/thumb/") self.assertEqual(command[6], f"/api/documents/{doc.pk}/thumb/")
self.assertEqual(command[7], "my_bank") self.assertEqual(command[7], "my_bank")
self.assertCountEqual(command[8].split(","), ["a", "b"]) self.assertCountEqual(command[8].split(","), ["a", "b"])

View File

@ -138,3 +138,18 @@ class TestDate(TestCase):
@override_settings(FILENAME_DATE_ORDER="YMD") @override_settings(FILENAME_DATE_ORDER="YMD")
def test_filename_date_parse_invalid(self, *args): def test_filename_date_parse_invalid(self, *args):
self.assertIsNone(parse_date("/tmp/20 408000l 2475 - test.pdf", "No date in here")) self.assertIsNone(parse_date("/tmp/20 408000l 2475 - test.pdf", "No date in here"))
@override_settings(IGNORE_DATES=(datetime.date(2019, 11, 3), datetime.date(2020, 1, 17)))
def test_ignored_dates(self, *args):
text = (
"lorem ipsum 110319, 20200117 and lorem 13.02.2018 lorem "
"ipsum"
)
date = parse_date("", text)
self.assertEqual(
date,
datetime.datetime(
2018, 2, 13, 0, 0,
tzinfo=tz.gettz(settings.TIME_ZONE)
)
)

View File

@ -0,0 +1,52 @@
import os
import shutil
from unittest import mock
from django.core.management import call_command
from django.test import TestCase
from documents.management.commands.document_thumbnails import _process_document
from documents.models import Document, Tag, Correspondent, DocumentType
from documents.tests.utils import DirectoriesMixin
class TestMakeThumbnails(DirectoriesMixin, TestCase):
def make_models(self):
self.d1 = Document.objects.create(checksum="A", title="A", content="first document", mime_type="application/pdf", filename="test.pdf")
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), self.d1.source_path)
self.d2 = Document.objects.create(checksum="Ass", title="A", content="first document", mime_type="application/pdf", filename="test2.pdf")
shutil.copy(os.path.join(os.path.dirname(__file__), "samples", "simple.pdf"), self.d2.source_path)
def setUp(self) -> None:
super(TestMakeThumbnails, self).setUp()
self.make_models()
def test_process_document(self):
self.assertFalse(os.path.isfile(self.d1.thumbnail_path))
_process_document(self.d1.id)
self.assertTrue(os.path.isfile(self.d1.thumbnail_path))
@mock.patch("documents.management.commands.document_thumbnails.shutil.move")
def test_process_document_invalid_mime_type(self, m):
self.d1.mime_type = "asdasdasd"
self.d1.save()
_process_document(self.d1.id)
m.assert_not_called()
def test_command(self):
self.assertFalse(os.path.isfile(self.d1.thumbnail_path))
self.assertFalse(os.path.isfile(self.d2.thumbnail_path))
call_command('document_thumbnails')
self.assertTrue(os.path.isfile(self.d1.thumbnail_path))
self.assertTrue(os.path.isfile(self.d2.thumbnail_path))
def test_command_documentid(self):
self.assertFalse(os.path.isfile(self.d1.thumbnail_path))
self.assertFalse(os.path.isfile(self.d2.thumbnail_path))
call_command('document_thumbnails', '-d', f"{self.d1.id}")
self.assertTrue(os.path.isfile(self.d1.thumbnail_path))
self.assertFalse(os.path.isfile(self.d2.thumbnail_path))

View File

@ -0,0 +1,569 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
# Translators:
# Jonas Winkler <dev@jpwinkler.de>, 2020
# Philmo67, 2021
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-01-02 00:26+0000\n"
"PO-Revision-Date: 2020-12-30 19:27+0000\n"
"Last-Translator: Philmo67, 2021\n"
"Language-Team: French (https://www.transifex.com/paperless/teams/115905/fr/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: documents/apps.py:10
msgid "Documents"
msgstr "Documents"
#: documents/models.py:32
msgid "Any word"
msgstr "Un des mots"
#: documents/models.py:33
msgid "All words"
msgstr "Tous les mots"
#: documents/models.py:34
msgid "Exact match"
msgstr "Concordance exacte"
#: documents/models.py:35
msgid "Regular expression"
msgstr "Expression régulière"
#: documents/models.py:36
msgid "Fuzzy word"
msgstr "Mot approximatif"
#: documents/models.py:37
msgid "Automatic"
msgstr "Automatique"
#: documents/models.py:41 documents/models.py:354 paperless_mail/models.py:25
#: paperless_mail/models.py:100
msgid "name"
msgstr "nom"
#: documents/models.py:45
msgid "match"
msgstr "rapprochement"
#: documents/models.py:49
msgid "matching algorithm"
msgstr "algorithme de rapprochement"
#: documents/models.py:55
msgid "is insensitive"
msgstr "est insensible à la casse"
#: documents/models.py:80 documents/models.py:140
msgid "correspondent"
msgstr "correspondant"
#: documents/models.py:81
msgid "correspondents"
msgstr "correspondants"
#: documents/models.py:103
msgid "color"
msgstr "couleur"
#: documents/models.py:107
msgid "is inbox tag"
msgstr "est une étiquette de boîte de réception"
#: documents/models.py:109
msgid ""
"Marks this tag as an inbox tag: All newly consumed documents will be tagged "
"with inbox tags."
msgstr ""
"Marque cette étiquette comme étiquette de boîte de réception : ces "
"étiquettes sont affectées à tous les documents nouvellement traités."
#: documents/models.py:114
msgid "tag"
msgstr "étiquette"
#: documents/models.py:115 documents/models.py:171
msgid "tags"
msgstr "étiquettes"
#: documents/models.py:121 documents/models.py:153
msgid "document type"
msgstr "type de document"
#: documents/models.py:122
msgid "document types"
msgstr "types de document"
#: documents/models.py:130
msgid "Unencrypted"
msgstr "Non chiffré"
#: documents/models.py:131
msgid "Encrypted with GNU Privacy Guard"
msgstr "Chiffré avec GNU Privacy Guard"
#: documents/models.py:144
msgid "title"
msgstr "titre"
#: documents/models.py:157
msgid "content"
msgstr "contenu"
#: documents/models.py:159
msgid ""
"The raw, text-only data of the document. This field is primarily used for "
"searching."
msgstr ""
"Les données brutes du document, en format texte uniquement. Ce champ est "
"principalement utilisé pour la recherche."
#: documents/models.py:164
msgid "mime type"
msgstr "type mime"
#: documents/models.py:175
msgid "checksum"
msgstr "somme de contrôle"
#: documents/models.py:179
msgid "The checksum of the original document."
msgstr "La somme de contrôle du document original."
#: documents/models.py:183
msgid "archive checksum"
msgstr "somme de contrôle de l'archive"
#: documents/models.py:188
msgid "The checksum of the archived document."
msgstr "La somme de contrôle du document archivé."
#: documents/models.py:192 documents/models.py:332
msgid "created"
msgstr "créé le"
#: documents/models.py:196
msgid "modified"
msgstr "modifié"
#: documents/models.py:200
msgid "storage type"
msgstr "forme d'enregistrement :"
#: documents/models.py:208
msgid "added"
msgstr "date d'ajout"
#: documents/models.py:212
msgid "filename"
msgstr "nom du fichier"
#: documents/models.py:217
msgid "Current filename in storage"
msgstr "Nom du fichier courant en base de données"
#: documents/models.py:221
msgid "archive serial number"
msgstr "numéro de série de l'archive"
#: documents/models.py:226
msgid "The position of this document in your physical document archive."
msgstr ""
"Le classement de ce document dans votre archive de documents physiques."
#: documents/models.py:232
msgid "document"
msgstr "document"
#: documents/models.py:233
msgid "documents"
msgstr "documents"
#: documents/models.py:315
msgid "debug"
msgstr "débogage"
#: documents/models.py:316
msgid "information"
msgstr "information"
#: documents/models.py:317
msgid "warning"
msgstr "avertissement"
#: documents/models.py:318
msgid "error"
msgstr "erreur"
#: documents/models.py:319
msgid "critical"
msgstr "critique"
#: documents/models.py:323
msgid "group"
msgstr "groupe"
#: documents/models.py:326
msgid "message"
msgstr "message"
#: documents/models.py:329
msgid "level"
msgstr "niveau"
#: documents/models.py:336
msgid "log"
msgstr "rapport"
#: documents/models.py:337
msgid "logs"
msgstr "rapports"
#: documents/models.py:348 documents/models.py:398
msgid "saved view"
msgstr "vue enregistrée"
#: documents/models.py:349
msgid "saved views"
msgstr "vues enregistrées"
#: documents/models.py:352
msgid "user"
msgstr "utilisateur"
#: documents/models.py:358
msgid "show on dashboard"
msgstr "montrer sur le tableau de bord"
#: documents/models.py:361
msgid "show in sidebar"
msgstr "montrer dans la barre latérale"
#: documents/models.py:365
msgid "sort field"
msgstr "champ de tri"
#: documents/models.py:368
msgid "sort reverse"
msgstr "tri inverse"
#: documents/models.py:374
msgid "title contains"
msgstr "le titre contient"
#: documents/models.py:375
msgid "content contains"
msgstr "le contenu contient"
#: documents/models.py:376
msgid "ASN is"
msgstr "le NSA est"
#: documents/models.py:377
msgid "correspondent is"
msgstr "le correspondant est"
#: documents/models.py:378
msgid "document type is"
msgstr "le type de document est"
#: documents/models.py:379
msgid "is in inbox"
msgstr "est dans la boîte de réception"
#: documents/models.py:380
msgid "has tag"
msgstr "porte l'étiquette"
#: documents/models.py:381
msgid "has any tag"
msgstr "porte l'une des étiquettes"
#: documents/models.py:382
msgid "created before"
msgstr "créé avant"
#: documents/models.py:383
msgid "created after"
msgstr "créé après"
#: documents/models.py:384
msgid "created year is"
msgstr "l'année de création est"
#: documents/models.py:385
msgid "created month is"
msgstr "le mois de création est"
#: documents/models.py:386
msgid "created day is"
msgstr "le jour de création est"
#: documents/models.py:387
msgid "added before"
msgstr "ajouté avant"
#: documents/models.py:388
msgid "added after"
msgstr "ajouté après"
#: documents/models.py:389
msgid "modified before"
msgstr "modifié avant"
#: documents/models.py:390
msgid "modified after"
msgstr "modifié après"
#: documents/models.py:391
msgid "does not have tag"
msgstr "ne porte pas d'étiquette"
#: documents/models.py:402
msgid "rule type"
msgstr "type de règle"
#: documents/models.py:406
msgid "value"
msgstr "valeur"
#: documents/models.py:412
msgid "filter rule"
msgstr "règle de filtrage"
#: documents/models.py:413
msgid "filter rules"
msgstr "règles de filtrage"
#: paperless/settings.py:254
msgid "English"
msgstr "Anglais"
#: paperless/settings.py:255
msgid "German"
msgstr "Allemand"
#: paperless/urls.py:108
msgid "Paperless-ng administration"
msgstr "Administration de Paperless-ng"
#: paperless_mail/admin.py:24
msgid "Filter"
msgstr "Filtrage"
#: paperless_mail/admin.py:26
msgid ""
"Paperless will only process mails that match ALL of the filters given below."
msgstr ""
"Paperless-ng ne traitera que les courriers qui correspondent à TOUS les "
"filtres ci-dessous."
#: paperless_mail/admin.py:34
msgid "Actions"
msgstr "Actions"
#: paperless_mail/admin.py:36
msgid ""
"The action applied to the mail. This action is only performed when documents"
" were consumed from the mail. Mails without attachments will remain entirely"
" untouched."
msgstr ""
"Action appliquée au courriel. Cette action n'est exécutée que lorsque les "
"documents ont été traités depuis des courriels. Les courriels sans pièces "
"jointes demeurent totalement inchangés."
#: paperless_mail/admin.py:43
msgid "Metadata"
msgstr "Métadonnées"
#: paperless_mail/admin.py:45
msgid ""
"Assign metadata to documents consumed from this rule automatically. If you "
"do not assign tags, types or correspondents here, paperless will still "
"process all matching rules that you have defined."
msgstr ""
"Affecter automatiquement des métadonnées aux documents traités à partir de "
"cette règle. Si vous n'affectez pas d'étiquettes, de types ou de "
"correspondants ici, Paperless-ng traitera quand même toutes les règles de "
"rapprochement que vous avez définies."
#: paperless_mail/apps.py:9
msgid "Paperless mail"
msgstr "Paperless-ng pour le courriel"
#: paperless_mail/models.py:11
msgid "mail account"
msgstr "compte de messagerie"
#: paperless_mail/models.py:12
msgid "mail accounts"
msgstr "comptes de messagerie"
#: paperless_mail/models.py:19
msgid "No encryption"
msgstr "Pas de chiffrement"
#: paperless_mail/models.py:20
msgid "Use SSL"
msgstr "Utiliser SSL"
#: paperless_mail/models.py:21
msgid "Use STARTTLS"
msgstr "Utiliser STARTTLS"
#: paperless_mail/models.py:29
msgid "IMAP server"
msgstr "Serveur IMAP"
#: paperless_mail/models.py:33
msgid "IMAP port"
msgstr "Port IMAP"
#: paperless_mail/models.py:36
msgid ""
"This is usually 143 for unencrypted and STARTTLS connections, and 993 for "
"SSL connections."
msgstr ""
"Généralement 143 pour les connexions non chiffrées et STARTTLS, et 993 pour "
"les connexions SSL."
#: paperless_mail/models.py:40
msgid "IMAP security"
msgstr "Sécurité IMAP"
#: paperless_mail/models.py:46
msgid "username"
msgstr "nom d'utilisateur"
#: paperless_mail/models.py:50
msgid "password"
msgstr "mot de passe"
#: paperless_mail/models.py:60
msgid "mail rule"
msgstr "règle de courriel"
#: paperless_mail/models.py:61
msgid "mail rules"
msgstr "règles de courriel"
#: paperless_mail/models.py:69
msgid "Mark as read, don't process read mails"
msgstr "Marquer comme lu, ne pas traiter les courriels lus"
#: paperless_mail/models.py:70
msgid "Flag the mail, don't process flagged mails"
msgstr "Marquer le courriel, ne pas traiter les courriels marqués"
#: paperless_mail/models.py:71
msgid "Move to specified folder"
msgstr "Déplacer vers le dossier spécifié"
#: paperless_mail/models.py:72
msgid "Delete"
msgstr "Supprimer"
#: paperless_mail/models.py:79
msgid "Use subject as title"
msgstr "Utiliser le sujet en tant que titre"
#: paperless_mail/models.py:80
msgid "Use attachment filename as title"
msgstr "Utiliser le nom de la pièce jointe en tant que titre"
#: paperless_mail/models.py:90
msgid "Do not assign a correspondent"
msgstr "Ne pas affecter de correspondant"
#: paperless_mail/models.py:92
msgid "Use mail address"
msgstr "Utiliser l'adresse électronique"
#: paperless_mail/models.py:94
msgid "Use name (or mail address if not available)"
msgstr "Utiliser le nom (ou l'adresse électronique s'il n'est pas disponible)"
#: paperless_mail/models.py:96
msgid "Use correspondent selected below"
msgstr "Utiliser le correspondant sélectionné ci-dessous"
#: paperless_mail/models.py:104
msgid "order"
msgstr "ordre"
#: paperless_mail/models.py:111
msgid "account"
msgstr "compte"
#: paperless_mail/models.py:115
msgid "folder"
msgstr "répertoire"
#: paperless_mail/models.py:119
msgid "filter from"
msgstr "filtrer l'expéditeur"
#: paperless_mail/models.py:122
msgid "filter subject"
msgstr "filtrer le sujet"
#: paperless_mail/models.py:125
msgid "filter body"
msgstr "filtrer le corps du message"
#: paperless_mail/models.py:129
msgid "maximum age"
msgstr "âge maximum"
#: paperless_mail/models.py:131
msgid "Specified in days."
msgstr "En jours."
#: paperless_mail/models.py:134
msgid "action"
msgstr "action"
#: paperless_mail/models.py:140
msgid "action parameter"
msgstr "paramètre d'action"
#: paperless_mail/models.py:142
msgid ""
"Additional parameter for the action selected above, i.e., the target folder "
"of the move to folder action."
msgstr ""
"Paramètre supplémentaire pour l'action sélectionnée ci-dessus, par exemple "
"le dossier cible de l'action de déplacement vers un dossier."
#: paperless_mail/models.py:148
msgid "assign title from"
msgstr "affecter le titre depuis"
#: paperless_mail/models.py:158
msgid "assign this tag"
msgstr "affecter cette étiquette"
#: paperless_mail/models.py:166
msgid "assign this document type"
msgstr "affecter ce type de document"
#: paperless_mail/models.py:170
msgid "assign correspondent from"
msgstr "affecter le correspondant depuis"
#: paperless_mail/models.py:180
msgid "assign this correspondent"
msgstr "affecter ce correspondant"

View File

@ -2,6 +2,7 @@ from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from rest_framework import authentication from rest_framework import authentication
from django.contrib.auth.middleware import RemoteUserMiddleware
class AutoLoginMiddleware(MiddlewareMixin): class AutoLoginMiddleware(MiddlewareMixin):
@ -26,3 +27,11 @@ class AngularApiAuthenticationOverride(authentication.BaseAuthentication):
return (user, None) return (user, None)
else: else:
return None return None
class HttpRemoteUserMiddleware(RemoteUserMiddleware):
""" This class allows authentication via HTTP_REMOTE_USER which is set for
example by certain SSO applications.
"""
header = 'HTTP_REMOTE_USER'

View File

@ -4,6 +4,7 @@ import multiprocessing
import os import os
import re import re
import dateparser
from dotenv import load_dotenv from dotenv import load_dotenv
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -128,6 +129,20 @@ MIDDLEWARE = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
ENABLE_HTTP_REMOTE_USER = __get_boolean("PAPERLESS_ENABLE_HTTP_REMOTE_USER")
if ENABLE_HTTP_REMOTE_USER:
MIDDLEWARE.append(
'paperless.auth.HttpRemoteUserMiddleware'
)
AUTHENTICATION_BACKENDS = [
'django.contrib.auth.backends.RemoteUserBackend',
'django.contrib.auth.backends.ModelBackend'
]
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'].append(
'rest_framework.authentication.RemoteUserAuthentication'
)
ROOT_URLCONF = 'paperless.urls' ROOT_URLCONF = 'paperless.urls'
FORCE_SCRIPT_NAME = os.getenv("PAPERLESS_FORCE_SCRIPT_NAME") FORCE_SCRIPT_NAME = os.getenv("PAPERLESS_FORCE_SCRIPT_NAME")
@ -253,7 +268,8 @@ LANGUAGE_CODE = 'en-us'
LANGUAGES = [ LANGUAGES = [
("en-us", _("English")), ("en-us", _("English")),
("de", _("German")), ("de", _("German")),
("nl-nl", _("Dutch")) ("nl-nl", _("Dutch")),
("fr", _("French"))
] ]
LOCALE_PATHS = [ LOCALE_PATHS = [
@ -445,3 +461,10 @@ PAPERLESS_TIKA_ENDPOINT = os.getenv("PAPERLESS_TIKA_ENDPOINT", "http://localhost
PAPERLESS_TIKA_GOTENBERG_ENDPOINT = os.getenv( PAPERLESS_TIKA_GOTENBERG_ENDPOINT = os.getenv(
"PAPERLESS_TIKA_GOTENBERG_ENDPOINT", "http://localhost:3000" "PAPERLESS_TIKA_GOTENBERG_ENDPOINT", "http://localhost:3000"
) )
# List dates that should be ignored when trying to parse date from document text
IGNORE_DATES = set()
for s in os.getenv("PAPERLESS_IGNORE_DATES", "").split(","):
d = dateparser.parse(s)
if d:
IGNORE_DATES.add(d.date())

View File

@ -12,6 +12,7 @@ class MailAccountAdmin(admin.ModelAdmin):
class MailRuleAdmin(admin.ModelAdmin): class MailRuleAdmin(admin.ModelAdmin):
radio_fields = { radio_fields = {
"attachment_type": admin.VERTICAL,
"action": admin.VERTICAL, "action": admin.VERTICAL,
"assign_title_from": admin.VERTICAL, "assign_title_from": admin.VERTICAL,
"assign_correspondent_from": admin.VERTICAL "assign_correspondent_from": admin.VERTICAL
@ -29,7 +30,9 @@ class MailRuleAdmin(admin.ModelAdmin):
('filter_from', ('filter_from',
'filter_subject', 'filter_subject',
'filter_body', 'filter_body',
'maximum_age') 'filter_attachment_filename',
'maximum_age',
'attachment_type')
}), }),
(_("Actions"), { (_("Actions"), {
'description': 'description':

View File

@ -1,6 +1,7 @@
import os import os
import tempfile import tempfile
from datetime import timedelta, date from datetime import timedelta, date
from fnmatch import fnmatch
import magic import magic
import pathvalidate import pathvalidate
@ -263,7 +264,7 @@ class MailAccountHandler(LoggingMixin):
for att in message.attachments: for att in message.attachments:
if not att.content_disposition == "attachment": if not att.content_disposition == "attachment" and rule.attachment_type == MailRule.ATTACHMENT_TYPE_ATTACHMENTS_ONLY: # NOQA: E501
self.log( self.log(
'debug', 'debug',
f"Rule {rule}: " f"Rule {rule}: "
@ -271,6 +272,10 @@ class MailAccountHandler(LoggingMixin):
f"with content disposition {att.content_disposition}") f"with content disposition {att.content_disposition}")
continue continue
if rule.filter_attachment_filename:
if not fnmatch(att.filename, rule.filter_attachment_filename):
continue
title = self.get_title(message, att, rule) title = self.get_title(message, att, rule)
# don't trust the content type of the attachment. Could be # don't trust the content type of the attachment. Could be

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1.5 on 2021-01-06 01:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('paperless_mail', '0006_auto_20210101_2340'),
]
operations = [
migrations.AddField(
model_name='mailrule',
name='attachment_type',
field=models.PositiveIntegerField(choices=[(1, 'Only process attachments.'), (2, "Process all files, including 'inline' attachments.")], default=1, help_text="Inline attachments include embedded images, so it's best to combine this option with a filename filter.", verbose_name='attachment type'),
),
migrations.AddField(
model_name='mailrule',
name='filter_attachment_filename',
field=models.CharField(blank=True, help_text='Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive.', max_length=256, null=True, verbose_name='filter attachment filename'),
),
]

View File

@ -60,6 +60,15 @@ class MailRule(models.Model):
verbose_name = _("mail rule") verbose_name = _("mail rule")
verbose_name_plural = _("mail rules") verbose_name_plural = _("mail rules")
ATTACHMENT_TYPE_ATTACHMENTS_ONLY = 1
ATTACHMENT_TYPE_EVERYTHING = 2
ATTACHMENT_TYPES = (
(ATTACHMENT_TYPE_ATTACHMENTS_ONLY, _("Only process attachments.")),
(ATTACHMENT_TYPE_EVERYTHING, _("Process all files, including 'inline' "
"attachments."))
)
ACTION_DELETE = 1 ACTION_DELETE = 1
ACTION_MOVE = 2 ACTION_MOVE = 2
ACTION_MARK_READ = 3 ACTION_MARK_READ = 3
@ -125,11 +134,27 @@ class MailRule(models.Model):
_("filter body"), _("filter body"),
max_length=256, null=True, blank=True) max_length=256, null=True, blank=True)
filter_attachment_filename = models.CharField(
_("filter attachment filename"),
max_length=256, null=True, blank=True,
help_text=_("Only consume documents which entirely match this "
"filename if specified. Wildcards such as *.pdf or "
"*invoice* are allowed. Case insensitive.")
)
maximum_age = models.PositiveIntegerField( maximum_age = models.PositiveIntegerField(
_("maximum age"), _("maximum age"),
default=30, default=30,
help_text=_("Specified in days.")) help_text=_("Specified in days."))
attachment_type = models.PositiveIntegerField(
_("attachment type"),
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.")
)
action = models.PositiveIntegerField( action = models.PositiveIntegerField(
_("action"), _("action"),
choices=ACTIONS, choices=ACTIONS,

View File

@ -273,6 +273,49 @@ class TestMail(TestCase):
args, kwargs = self.async_task.call_args args, kwargs = self.async_task.call_args
self.assertEqual(kwargs['override_filename'], "f2.pdf") self.assertEqual(kwargs['override_filename'], "f2.pdf")
def test_handle_inline_files(self):
message = create_message()
message.attachments = [
create_attachment(filename="f1.pdf", content_disposition='inline'),
create_attachment(filename="f2.pdf", content_disposition='attachment')
]
account = MailAccount()
rule = MailRule(assign_title_from=MailRule.TITLE_FROM_FILENAME, account=account, attachment_type=MailRule.ATTACHMENT_TYPE_EVERYTHING)
result = self.mail_account_handler.handle_message(message, rule)
self.assertEqual(result, 2)
self.assertEqual(self.async_task.call_count, 2)
def test_filename_filter(self):
message = create_message()
message.attachments = [
create_attachment(filename="f1.pdf"),
create_attachment(filename="f2.pdf"),
create_attachment(filename="f3.pdf"),
create_attachment(filename="f2.png"),
]
tests = [
("*.pdf", ["f1.pdf", "f2.pdf", "f3.pdf"]),
("f1.pdf", ["f1.pdf"]),
("f1", []),
("*", ["f1.pdf", "f2.pdf", "f3.pdf", "f2.png"]),
("*.png", ["f2.png"]),
]
for (pattern, matches) in tests:
self.async_task.reset_mock()
account = MailAccount()
rule = MailRule(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))
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): def test_handle_mail_account_mark_read(self):
account = MailAccount.objects.create(name="test", imap_server="", username="admin", password="secret") account = MailAccount.objects.create(name="test", imap_server="", username="admin", password="secret")