mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge remote-tracking branch 'upstream/dev' into fix/issue-267
This commit is contained in:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
									
									
								
							
							
						
						
									
										489
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -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": [ | ||||||
|   | |||||||
| @@ -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 \ | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
| ######## | ######## | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
| ####################### | ####################### | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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": { | ||||||
|   | |||||||
| @@ -140,6 +140,13 @@ | |||||||
|               </svg> <ng-container i18n>Logs</ng-container> |               </svg> <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> <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"> | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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> | ||||||
| @@ -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) | ||||||
|  |       } | ||||||
|  |     ) | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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" | ||||||
|   | |||||||
| @@ -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}} | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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> <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' : ''"> | ||||||
|   | |||||||
| @@ -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 | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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 | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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"> | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -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}); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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` } | ||||||
|   | |||||||
							
								
								
									
										1907
									
								
								src-ui/src/locale/messages.fr.xlf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1907
									
								
								src-ui/src/locale/messages.fr.xlf
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -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 { | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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"] | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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> | ||||||
|   | |||||||
| @@ -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"> | ||||||
|   | |||||||
| @@ -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"]) | ||||||
|   | |||||||
| @@ -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) | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
							
								
								
									
										52
									
								
								src/documents/tests/test_management_thumbnails.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/documents/tests/test_management_thumbnails.py
									
									
									
									
									
										Normal 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)) | ||||||
							
								
								
									
										569
									
								
								src/locale/fr/LC_MESSAGES/django.po
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										569
									
								
								src/locale/fr/LC_MESSAGES/django.po
									
									
									
									
									
										Normal 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" | ||||||
| @@ -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' | ||||||
|   | |||||||
| @@ -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()) | ||||||
|   | |||||||
| @@ -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': | ||||||
|   | |||||||
| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								src/paperless_mail/migrations/0007_auto_20210106_0138.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/paperless_mail/migrations/0007_auto_20210106_0138.py
									
									
									
									
									
										Normal 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'), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @@ -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, | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon