mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06: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
 | 
				
			||||||
@@ -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
 | 
				
			||||||
########
 | 
					########
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -180,6 +180,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
 | 
				
			||||||
    source archive.
 | 
					    source archive.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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) {
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (event.direction && event.direction.length > 0) {
 | 
					 | 
				
			||||||
    this.sortField = event.column
 | 
					    this.sortField = event.column
 | 
				
			||||||
      this.sortDirection = event.direction
 | 
					    this.sortReverse = event.reverse
 | 
				
			||||||
    } 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,9 +519,17 @@ class PostConsumeTestCase(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        m.assert_not_called()
 | 
					        m.assert_not_called()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @override_settings(POST_CONSUME_SCRIPT="does-not-exist")
 | 
				
			||||||
 | 
					    def test_post_consume_script_not_found(self):
 | 
				
			||||||
 | 
					        doc = Document.objects.create(title="Test", mime_type="application/pdf")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertRaises(ConsumerError, Consumer().run_post_consume_script, doc)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @mock.patch("documents.consumer.Popen")
 | 
					    @mock.patch("documents.consumer.Popen")
 | 
				
			||||||
    @override_settings(POST_CONSUME_SCRIPT="script")
 | 
					 | 
				
			||||||
    def test_post_consume_script_simple(self, m):
 | 
					    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")
 | 
					                doc = Document.objects.create(title="Test", mime_type="application/pdf")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                Consumer().run_post_consume_script(doc)
 | 
					                Consumer().run_post_consume_script(doc)
 | 
				
			||||||
@@ -493,8 +537,9 @@ class PostConsumeTestCase(TestCase):
 | 
				
			|||||||
                m.assert_called_once()
 | 
					                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):
 | 
				
			||||||
 | 
					        with tempfile.NamedTemporaryFile() as script:
 | 
				
			||||||
 | 
					            with override_settings(POST_CONSUME_SCRIPT=script.name):
 | 
				
			||||||
                c = Correspondent.objects.create(name="my_bank")
 | 
					                c = Correspondent.objects.create(name="my_bank")
 | 
				
			||||||
                doc = Document.objects.create(title="Test", mime_type="application/pdf", correspondent=c)
 | 
					                doc = Document.objects.create(title="Test", mime_type="application/pdf", correspondent=c)
 | 
				
			||||||
                tag1 = Tag.objects.create(name="a")
 | 
					                tag1 = Tag.objects.create(name="a")
 | 
				
			||||||
@@ -510,7 +555,7 @@ class PostConsumeTestCase(TestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                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/")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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