mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-18 00:46:25 +00:00
Compare commits
43 Commits
feature-ne
...
feature-pa
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a0a9e0c6c8 | ||
![]() |
1c7c703e5f | ||
![]() |
53e9e910d8 | ||
![]() |
4f08b5fa20 | ||
![]() |
9fe611a24c | ||
![]() |
3bf64ae7da | ||
![]() |
822c2d2d56 | ||
![]() |
98e0a934ac | ||
![]() |
ceffcd6360 | ||
![]() |
37442ff829 | ||
![]() |
31e71aab83 | ||
![]() |
7e7ce97d10 | ||
![]() |
e06adc58c7 | ||
![]() |
7170ac31b7 | ||
![]() |
a0aa78c788 | ||
![]() |
f3438914cc | ||
![]() |
e1b944ce6b | ||
![]() |
0add5aab0e | ||
![]() |
c9adc74fa9 | ||
![]() |
32abfbfc0a | ||
![]() |
7f02f782f4 | ||
![]() |
7c3f011e84 | ||
![]() |
5c68177960 | ||
![]() |
7a4666783e | ||
![]() |
372825c271 | ||
![]() |
abfddd6931 | ||
![]() |
b3d49dbf12 | ||
![]() |
673839265d | ||
![]() |
f31df22ab6 | ||
![]() |
f897447a65 | ||
![]() |
de5f66b3a0 | ||
![]() |
e37096f66f | ||
![]() |
e49ecd4dfe | ||
![]() |
4718df271f | ||
![]() |
17bb3ebbf5 | ||
![]() |
5e00c1c676 | ||
![]() |
a9ef7ff58e | ||
![]() |
518091f856 | ||
![]() |
feb30f36df | ||
![]() |
bbad36717f | ||
![]() |
329ef7aef3 | ||
![]() |
2b2115e5f0 | ||
![]() |
ba5705a54f |
@@ -116,7 +116,7 @@ ARG DEBIAN_FRONTEND=noninteractive
|
||||
ARG TARGETARCH
|
||||
|
||||
# Can be workflow provided, defaults set for manual building
|
||||
ARG JBIG2ENC_VERSION=0.29
|
||||
ARG JBIG2ENC_VERSION=0.30
|
||||
ARG QPDF_VERSION=11.9.0
|
||||
ARG GS_VERSION=10.03.1
|
||||
|
||||
|
6
Pipfile
6
Pipfile
@@ -14,7 +14,7 @@ django-celery-results = "*"
|
||||
django-compression-middleware = "*"
|
||||
django-cors-headers = "*"
|
||||
django-extensions = "*"
|
||||
django-filter = "~=24.3"
|
||||
django-filter = "~=25.1"
|
||||
django-guardian = "*"
|
||||
django-multiselectfield = "*"
|
||||
django-soft-delete = "*"
|
||||
@@ -39,7 +39,7 @@ jinja2 = "~=3.1"
|
||||
langdetect = "*"
|
||||
mysqlclient = "*"
|
||||
nltk = "*"
|
||||
ocrmypdf = "~=16.8"
|
||||
ocrmypdf = "~=16.9"
|
||||
pathvalidate = "*"
|
||||
pdf2image = "*"
|
||||
psycopg = {version = "*", extras = ["c"]}
|
||||
@@ -58,7 +58,7 @@ tqdm = "*"
|
||||
# See https://github.com/paperless-ngx/paperless-ngx/issues/5494
|
||||
uvicorn = {extras = ["standard"], version = "==0.25.0"}
|
||||
watchdog = "~=6.0"
|
||||
whitenoise = "~=6.8"
|
||||
whitenoise = "~=6.9"
|
||||
whoosh = "~=2.7"
|
||||
zxing-cpp = "*"
|
||||
|
||||
|
687
Pipfile.lock
generated
687
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "b08210d0d72465f043a0e0e59cba4a93456f4f658a0c2433404b62138db447e0"
|
||||
"sha256": "4d54b43e6f093a817b2dc9b923f93b889bf7a42cd937ea971cd8773484fc4636"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -290,7 +290,7 @@
|
||||
"sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87",
|
||||
"sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"markers": "platform_python_implementation != 'PyPy'",
|
||||
"version": "==1.17.1"
|
||||
},
|
||||
"channels": {
|
||||
@@ -451,53 +451,58 @@
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7",
|
||||
"sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731",
|
||||
"sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b",
|
||||
"sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc",
|
||||
"sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543",
|
||||
"sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c",
|
||||
"sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591",
|
||||
"sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede",
|
||||
"sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb",
|
||||
"sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f",
|
||||
"sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123",
|
||||
"sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c",
|
||||
"sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c",
|
||||
"sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285",
|
||||
"sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd",
|
||||
"sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092",
|
||||
"sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa",
|
||||
"sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289",
|
||||
"sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02",
|
||||
"sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64",
|
||||
"sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053",
|
||||
"sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417",
|
||||
"sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e",
|
||||
"sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e",
|
||||
"sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7",
|
||||
"sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756",
|
||||
"sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"
|
||||
"sha256:00918d859aa4e57db8299607086f793fa7813ae2ff5a4637e318a25ef82730f7",
|
||||
"sha256:1e8d181e90a777b63f3f0caa836844a1182f1f265687fac2115fcf245f5fbec3",
|
||||
"sha256:1f9a92144fa0c877117e9748c74501bea842f93d21ee00b0cf922846d9d0b183",
|
||||
"sha256:21377472ca4ada2906bc313168c9dc7b1d7ca417b63c1c3011d0c74b7de9ae69",
|
||||
"sha256:24979e9f2040c953a94bf3c6782e67795a4c260734e5264dceea65c8f4bae64a",
|
||||
"sha256:2a46a89ad3e6176223b632056f321bc7de36b9f9b93b2cc1cccf935a3849dc62",
|
||||
"sha256:322eb03ecc62784536bc173f1483e76747aafeb69c8728df48537eb431cd1911",
|
||||
"sha256:436df4f203482f41aad60ed1813811ac4ab102765ecae7a2bbb1dbb66dcff5a7",
|
||||
"sha256:4f422e8c6a28cf8b7f883eb790695d6d45b0c385a2583073f3cec434cc705e1a",
|
||||
"sha256:53f23339864b617a3dfc2b0ac8d5c432625c80014c25caac9082314e9de56f41",
|
||||
"sha256:5fed5cd6102bb4eb843e3315d2bf25fede494509bddadb81e03a859c1bc17b83",
|
||||
"sha256:610a83540765a8d8ce0f351ce42e26e53e1f774a6efb71eb1b41eb01d01c3d12",
|
||||
"sha256:6c8acf6f3d1f47acb2248ec3ea261171a671f3d9428e34ad0357148d492c7864",
|
||||
"sha256:6f76fdd6fd048576a04c5210d53aa04ca34d2ed63336d4abd306d0cbe298fddf",
|
||||
"sha256:72198e2b5925155497a5a3e8c216c7fb3e64c16ccee11f0e7da272fa93b35c4c",
|
||||
"sha256:887143b9ff6bad2b7570da75a7fe8bbf5f65276365ac259a5d2d5147a73775f2",
|
||||
"sha256:888fcc3fce0c888785a4876ca55f9f43787f4c5c1cc1e2e0da71ad481ff82c5b",
|
||||
"sha256:8e6a85a93d0642bd774460a86513c5d9d80b5c002ca9693e63f6e540f1815ed0",
|
||||
"sha256:94f99f2b943b354a5b6307d7e8d19f5c423a794462bde2bf310c770ba052b1c4",
|
||||
"sha256:9b336599e2cb77b1008cb2ac264b290803ec5e8e89d618a5e978ff5eb6f715d9",
|
||||
"sha256:a2d8a7045e1ab9b9f803f0d9531ead85f90c5f2859e653b61497228b18452008",
|
||||
"sha256:b8272f257cf1cbd3f2e120f14c68bff2b6bdfcc157fafdee84a1b795efd72862",
|
||||
"sha256:bf688f615c29bfe9dfc44312ca470989279f0e94bb9f631f85e3459af8efc009",
|
||||
"sha256:d9c5b9f698a83c8bd71e0f4d3f9f839ef244798e5ffe96febfa9714717db7af7",
|
||||
"sha256:dd7c7e2d71d908dc0f8d2027e1604102140d84b155e658c20e8ad1304317691f",
|
||||
"sha256:df978682c1504fc93b3209de21aeabf2375cb1571d4e61907b3e7a2540e83026",
|
||||
"sha256:e403f7f766ded778ecdb790da786b418a9f2394f36e8cc8b796cc056ab05f44f",
|
||||
"sha256:eb3889330f2a4a148abead555399ec9a32b13b7c8ba969b72d8e500eb7ef84cd",
|
||||
"sha256:f4daefc971c2d1f82f03097dc6f216744a6cd2ac0f04c68fb935ea2ba2a0d420",
|
||||
"sha256:f51f5705ab27898afda1aaa430f34ad90dc117421057782022edf0600bec5f14",
|
||||
"sha256:fd0ee90072861e276b0ff08bd627abec29e32a53b2be44e41dbcdf87cbee2b00"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'",
|
||||
"version": "==44.0.0"
|
||||
"version": "==44.0.1"
|
||||
},
|
||||
"dateparser": {
|
||||
"hashes": [
|
||||
"sha256:0b21ad96534e562920a0083e97fd45fa959882d4162acc358705144520a35830",
|
||||
"sha256:7975b43a4222283e0ae15be7b4999d08c9a70e2d378ac87385b1ccf2cffbbb30"
|
||||
"sha256:7e4919aeb48481dbfc01ac9683c8e20bfe95bb715a38c1e9f6af889f4f30ccc3",
|
||||
"sha256:bdcac262a467e6260030040748ad7c10d6bacd4f3b9cdb4cfd2251939174508c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.2.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"deprecated": {
|
||||
"hashes": [
|
||||
"sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320",
|
||||
"sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"
|
||||
"sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d",
|
||||
"sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.2.15"
|
||||
"version": "==1.2.18"
|
||||
},
|
||||
"deprecation": {
|
||||
"hashes": [
|
||||
@@ -572,12 +577,12 @@
|
||||
},
|
||||
"django-filter": {
|
||||
"hashes": [
|
||||
"sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64",
|
||||
"sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3"
|
||||
"sha256:1ec9eef48fa8da1c0ac9b411744b16c3f4c31176c867886e4c48da369c407153",
|
||||
"sha256:4fa48677cf5857b9b1347fed23e355ea792464e0fe07244d1fdfb8a806215b80"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==24.3"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==25.1"
|
||||
},
|
||||
"django-guardian": {
|
||||
"hashes": [
|
||||
@@ -942,11 +947,11 @@
|
||||
},
|
||||
"imap-tools": {
|
||||
"hashes": [
|
||||
"sha256:b5f0611156ad7ab64ea2a7283312480f5787406275f11574e35b3190028062df",
|
||||
"sha256:c2a866ec8c875613b6306b5874bd82d126d94ff85fbc6d5180f038f25af336f1"
|
||||
"sha256:3d2bee8e2900a58a3bf91e09531e548453f91fae2e491965030a4d96c4a34557",
|
||||
"sha256:8b8794f0ffe4b3de1e72dea4e0b77ed744d9cd225ecaace81976a599eec0947b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.9.1"
|
||||
"version": "==1.10.0"
|
||||
},
|
||||
"img2pdf": {
|
||||
"hashes": [
|
||||
@@ -1029,147 +1034,147 @@
|
||||
},
|
||||
"lxml": {
|
||||
"hashes": [
|
||||
"sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e",
|
||||
"sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229",
|
||||
"sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3",
|
||||
"sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5",
|
||||
"sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70",
|
||||
"sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15",
|
||||
"sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002",
|
||||
"sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd",
|
||||
"sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22",
|
||||
"sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf",
|
||||
"sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22",
|
||||
"sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832",
|
||||
"sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727",
|
||||
"sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e",
|
||||
"sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30",
|
||||
"sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f",
|
||||
"sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f",
|
||||
"sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51",
|
||||
"sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4",
|
||||
"sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de",
|
||||
"sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875",
|
||||
"sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42",
|
||||
"sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e",
|
||||
"sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6",
|
||||
"sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391",
|
||||
"sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc",
|
||||
"sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b",
|
||||
"sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237",
|
||||
"sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4",
|
||||
"sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86",
|
||||
"sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f",
|
||||
"sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a",
|
||||
"sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8",
|
||||
"sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f",
|
||||
"sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903",
|
||||
"sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03",
|
||||
"sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e",
|
||||
"sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99",
|
||||
"sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7",
|
||||
"sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab",
|
||||
"sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d",
|
||||
"sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22",
|
||||
"sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492",
|
||||
"sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b",
|
||||
"sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3",
|
||||
"sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be",
|
||||
"sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469",
|
||||
"sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f",
|
||||
"sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a",
|
||||
"sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c",
|
||||
"sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a",
|
||||
"sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4",
|
||||
"sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94",
|
||||
"sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442",
|
||||
"sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b",
|
||||
"sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84",
|
||||
"sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c",
|
||||
"sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9",
|
||||
"sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1",
|
||||
"sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be",
|
||||
"sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367",
|
||||
"sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e",
|
||||
"sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21",
|
||||
"sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa",
|
||||
"sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16",
|
||||
"sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d",
|
||||
"sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe",
|
||||
"sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83",
|
||||
"sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba",
|
||||
"sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040",
|
||||
"sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763",
|
||||
"sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8",
|
||||
"sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff",
|
||||
"sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2",
|
||||
"sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a",
|
||||
"sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b",
|
||||
"sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce",
|
||||
"sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c",
|
||||
"sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577",
|
||||
"sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8",
|
||||
"sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71",
|
||||
"sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512",
|
||||
"sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540",
|
||||
"sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f",
|
||||
"sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2",
|
||||
"sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a",
|
||||
"sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce",
|
||||
"sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e",
|
||||
"sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2",
|
||||
"sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27",
|
||||
"sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1",
|
||||
"sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d",
|
||||
"sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1",
|
||||
"sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330",
|
||||
"sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920",
|
||||
"sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99",
|
||||
"sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff",
|
||||
"sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18",
|
||||
"sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff",
|
||||
"sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c",
|
||||
"sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179",
|
||||
"sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080",
|
||||
"sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19",
|
||||
"sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d",
|
||||
"sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70",
|
||||
"sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32",
|
||||
"sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a",
|
||||
"sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2",
|
||||
"sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79",
|
||||
"sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3",
|
||||
"sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5",
|
||||
"sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f",
|
||||
"sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d",
|
||||
"sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3",
|
||||
"sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b",
|
||||
"sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753",
|
||||
"sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9",
|
||||
"sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957",
|
||||
"sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033",
|
||||
"sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb",
|
||||
"sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656",
|
||||
"sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab",
|
||||
"sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b",
|
||||
"sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d",
|
||||
"sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd",
|
||||
"sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859",
|
||||
"sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11",
|
||||
"sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c",
|
||||
"sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a",
|
||||
"sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005",
|
||||
"sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654",
|
||||
"sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80",
|
||||
"sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e",
|
||||
"sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec",
|
||||
"sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7",
|
||||
"sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965",
|
||||
"sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945",
|
||||
"sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"
|
||||
"sha256:016b96c58e9a4528219bb563acf1aaaa8bc5452e7651004894a973f03b84ba81",
|
||||
"sha256:05123fad495a429f123307ac6d8fd6f977b71e9a0b6d9aeeb8f80c017cb17131",
|
||||
"sha256:057e30d0012439bc54ca427a83d458752ccda725c1c161cc283db07bcad43cf9",
|
||||
"sha256:06a20d607a86fccab2fc15a77aa445f2bdef7b49ec0520a842c5c5afd8381576",
|
||||
"sha256:094b28ed8a8a072b9e9e2113a81fda668d2053f2ca9f2d202c2c8c7c2d6516b1",
|
||||
"sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c",
|
||||
"sha256:0d6b2fa86becfa81f0a0271ccb9eb127ad45fb597733a77b92e8a35e53414914",
|
||||
"sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607",
|
||||
"sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8",
|
||||
"sha256:133f3493253a00db2c870d3740bc458ebb7d937bd0a6a4f9328373e0db305709",
|
||||
"sha256:136bf638d92848a939fd8f0e06fcf92d9f2e4b57969d94faae27c55f3d85c05b",
|
||||
"sha256:155e1a5693cf4b55af652f5c0f78ef36596c7f680ff3ec6eb4d7d85367259b2c",
|
||||
"sha256:1637fa31ec682cd5760092adfabe86d9b718a75d43e65e211d5931809bc111e7",
|
||||
"sha256:172d65f7c72a35a6879217bcdb4bb11bc88d55fb4879e7569f55616062d387c2",
|
||||
"sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c",
|
||||
"sha256:198bb4b4dd888e8390afa4f170d4fa28467a7eaf857f1952589f16cfbb67af27",
|
||||
"sha256:1b6f92e35e2658a5ed51c6634ceb5ddae32053182851d8cad2a5bc102a359b33",
|
||||
"sha256:1b92fe86e04f680b848fff594a908edfa72b31bfc3499ef7433790c11d4c8cd8",
|
||||
"sha256:1bcc211542f7af6f2dfb705f5f8b74e865592778e6cafdfd19c792c244ccce19",
|
||||
"sha256:1c93ed3c998ea8472be98fb55aed65b5198740bfceaec07b2eba551e55b7b9ae",
|
||||
"sha256:203b1d3eaebd34277be06a3eb880050f18a4e4d60861efba4fb946e31071a295",
|
||||
"sha256:22ec2b3c191f43ed21f9545e9df94c37c6b49a5af0a874008ddc9132d49a2d9c",
|
||||
"sha256:231cf4d140b22a923b1d0a0a4e0b4f972e5893efcdec188934cc65888fd0227b",
|
||||
"sha256:236610b77589faf462337b3305a1be91756c8abc5a45ff7ca8f245a71c5dab70",
|
||||
"sha256:29bfc8d3d88e56ea0a27e7c4897b642706840247f59f4377d81be8f32aa0cfbf",
|
||||
"sha256:2b8969dbc8d09d9cd2ae06362c3bad27d03f433252601ef658a49bd9f2b22d79",
|
||||
"sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84",
|
||||
"sha256:2df7ed5edeb6bd5590914cd61df76eb6cce9d590ed04ec7c183cf5509f73530d",
|
||||
"sha256:2e4a570f6a99e96c457f7bec5ad459c9c420ee80b99eb04cbfcfe3fc18ec6423",
|
||||
"sha256:2f1be45d4c15f237209bbf123a0e05b5d630c8717c42f59f31ea9eae2ad89394",
|
||||
"sha256:2f23cf50eccb3255b6e913188291af0150d89dab44137a69e14e4dcb7be981f1",
|
||||
"sha256:3031e4c16b59424e8d78522c69b062d301d951dc55ad8685736c3335a97fc270",
|
||||
"sha256:33e06717c00c788ab4e79bc4726ecc50c54b9bfb55355eae21473c145d83c2d2",
|
||||
"sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b",
|
||||
"sha256:3715cdf0dd31b836433af9ee9197af10e3df41d273c19bb249230043667a5dfd",
|
||||
"sha256:3bb8149840daf2c3f97cebf00e4ed4a65a0baff888bf2605a8d0135ff5cf764e",
|
||||
"sha256:3c3c8b55c7fc7b7e8877b9366568cc73d68b82da7fe33d8b98527b73857a225f",
|
||||
"sha256:3d68eeef7b4d08a25e51897dac29bcb62aba830e9ac6c4e3297ee7c6a0cf6439",
|
||||
"sha256:3dddf0fb832486cc1ea71d189cb92eb887826e8deebe128884e15020bb6e3f61",
|
||||
"sha256:3edbb9c9130bac05d8c3fe150c51c337a471cc7fdb6d2a0a7d3a88e88a829314",
|
||||
"sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0",
|
||||
"sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2",
|
||||
"sha256:42978a68d3825eaac55399eb37a4d52012a205c0c6262199b8b44fcc6fd686e8",
|
||||
"sha256:4399b4226c4785575fb20998dc571bc48125dc92c367ce2602d0d70e0c455eb0",
|
||||
"sha256:45fbb70ccbc8683f2fb58bea89498a7274af1d9ec7995e9f4af5604e028233fc",
|
||||
"sha256:4867361c049761a56bd21de507cab2c2a608c55102311d142ade7dab67b34f32",
|
||||
"sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499",
|
||||
"sha256:4b0d5cdba1b655d5b18042ac9c9ff50bda33568eb80feaaca4fc237b9c4fbfde",
|
||||
"sha256:4df0ec814b50275ad6a99bc82a38b59f90e10e47714ac9871e1b223895825468",
|
||||
"sha256:4e52e1b148867b01c05e21837586ee307a01e793b94072d7c7b91d2c2da02ffe",
|
||||
"sha256:514fe78fc4b87e7a7601c92492210b20a1b0c6ab20e71e81307d9c2e377c64de",
|
||||
"sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877",
|
||||
"sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5",
|
||||
"sha256:52d82b0d436edd6a1d22d94a344b9a58abd6c68c357ed44f22d4ba8179b37629",
|
||||
"sha256:5412500e0dc5481b1ee9cf6b38bb3b473f6e411eb62b83dc9b62699c3b7b79f7",
|
||||
"sha256:585c4dc429deebc4307187d2b71ebe914843185ae16a4d582ee030e6cfbb4d8a",
|
||||
"sha256:5865b270b420eda7b68928d70bb517ccbe045e53b1a428129bb44372bf3d7dd5",
|
||||
"sha256:5881aaa4bf3a2d086c5f20371d3a5856199a0d8ac72dd8d0dbd7a2ecfc26ab73",
|
||||
"sha256:5885bc586f1edb48e5d68e7a4b4757b5feb2a496b64f462b4d65950f5af3364f",
|
||||
"sha256:5a11b16a33656ffc43c92a5343a28dc71eefe460bcc2a4923a96f292692709f6",
|
||||
"sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac",
|
||||
"sha256:5be8f5e4044146a69c96077c7e08f0709c13a314aa5315981185c1f00235fe65",
|
||||
"sha256:63d57fc94eb0bbb4735e45517afc21ef262991d8758a8f2f05dd6e4174944519",
|
||||
"sha256:673b9d8e780f455091200bba8534d5f4f465944cbdd61f31dc832d70e29064a5",
|
||||
"sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8",
|
||||
"sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d",
|
||||
"sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51",
|
||||
"sha256:6c4dd3bfd0c82400060896717dd261137398edb7e524527438c54a8c34f736bf",
|
||||
"sha256:71f31eda4e370f46af42fc9f264fafa1b09f46ba07bdbee98f25689a04b81c20",
|
||||
"sha256:7512b4d0fc5339d5abbb14d1843f70499cab90d0b864f790e73f780f041615d7",
|
||||
"sha256:75fa3d6946d317ffc7016a6fcc44f42db6d514b7fdb8b4b28cbe058303cb6e53",
|
||||
"sha256:779e851fd0e19795ccc8a9bb4d705d6baa0ef475329fe44a13cf1e962f18ff1e",
|
||||
"sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5",
|
||||
"sha256:7aae7a3d63b935babfdc6864b31196afd5145878ddd22f5200729006366bc4d5",
|
||||
"sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9",
|
||||
"sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2",
|
||||
"sha256:7eda194dd46e40ec745bf76795a7cccb02a6a41f445ad49d3cf66518b0bd9cff",
|
||||
"sha256:82a4bb10b0beef1434fb23a09f001ab5ca87895596b4581fd53f1e5145a8934a",
|
||||
"sha256:85c4f11be9cf08917ac2a5a8b6e1ef63b2f8e3799cec194417e76826e5f1de9c",
|
||||
"sha256:88b72eb7222d918c967202024812c2bfb4048deeb69ca328363fb8e15254c549",
|
||||
"sha256:89934f9f791566e54c1d92cdc8f8fd0009447a5ecdb1ec6b810d5f8c4955f6be",
|
||||
"sha256:8b1942b3e4ed9ed551ed3083a2e6e0772de1e5e3aca872d955e2e86385fb7ff9",
|
||||
"sha256:8ffb141361108e864ab5f1813f66e4e1164181227f9b1f105b042729b6c15125",
|
||||
"sha256:8fffc08de02071c37865a155e5ea5fce0282e1546fd5bde7f6149fcaa32558ac",
|
||||
"sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332",
|
||||
"sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2",
|
||||
"sha256:96eef5b9f336f623ffc555ab47a775495e7e8846dde88de5f941e2906453a1ce",
|
||||
"sha256:a0611da6b07dd3720f492db1b463a4d1175b096b49438761cc9f35f0d9eaaef5",
|
||||
"sha256:a091026c3bf7519ab1e64655a3f52a59ad4a4e019a6f830c24d6430695b1cf6a",
|
||||
"sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23",
|
||||
"sha256:a243132767150a44e6a93cd1dde41010036e1cbc63cc3e9fe1712b277d926ce3",
|
||||
"sha256:a31fa7536ec1fb7155a0cd3a4e3d956c835ad0a43e3610ca32384d01f079ea1c",
|
||||
"sha256:a364e8e944d92dcbf33b6b494d4e0fb3499dcc3bd9485beb701aa4b4201fa414",
|
||||
"sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b",
|
||||
"sha256:a4b382e0e636ed54cd278791d93fe2c4f370772743f02bcbe431a160089025c9",
|
||||
"sha256:a83d3adea1e0ee36dac34627f78ddd7f093bb9cfc0a8e97f1572a949b695cb98",
|
||||
"sha256:a8ade0363f776f87f982572c2860cc43c65ace208db49c76df0a21dde4ddd16e",
|
||||
"sha256:aa59974880ab5ad8ef3afaa26f9bda148c5f39e06b11a8ada4660ecc9fb2feb3",
|
||||
"sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406",
|
||||
"sha256:aaca5a812f050ab55426c32177091130b1e49329b3f002a32934cd0245571307",
|
||||
"sha256:ae82fce1d964f065c32c9517309f0c7be588772352d2f40b1574a214bd6e6098",
|
||||
"sha256:aed57b541b589fa05ac248f4cb1c46cbb432ab82cbd467d1c4f6a2bdc18aecf9",
|
||||
"sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725",
|
||||
"sha256:b0884e3f22d87c30694e625b1e62e6f30d39782c806287450d9dc2fdf07692fd",
|
||||
"sha256:b2aca14c235c7a08558fe0a4786a1a05873a01e86b474dfa8f6df49101853a4e",
|
||||
"sha256:b450d7cabcd49aa7ab46a3c6aa3ac7e1593600a1a0605ba536ec0f1b99a04322",
|
||||
"sha256:b725e70d15906d24615201e650d5b0388b08a5187a55f119f25874d0103f90dd",
|
||||
"sha256:bfbbab9316330cf81656fed435311386610f78b6c93cc5db4bebbce8dd146675",
|
||||
"sha256:c093c7088b40d8266f57ed71d93112bd64c6724d31f0794c1e52cc4857c28e0e",
|
||||
"sha256:c2e49dc23a10a1296b04ca9db200c44d3eb32c8d8ec532e8c1fd24792276522a",
|
||||
"sha256:c4393600915c308e546dc7003d74371744234e8444a28622d76fe19b98fa59d1",
|
||||
"sha256:c5ae125276f254b01daa73e2c103363d3e99e3e10505686ac7d9d2442dd4627a",
|
||||
"sha256:c6aacf00d05b38a5069826e50ae72751cb5bc27bdc4d5746203988e429b385bb",
|
||||
"sha256:c76722b5ed4a31ba103e0dc77ab869222ec36efe1a614e42e9bcea88a36186fe",
|
||||
"sha256:c809eef167bf4a57af4b03007004896f5c60bd38dc3852fcd97a26eae3d4c9e6",
|
||||
"sha256:c92ea6d9dd84a750b2bae72ff5e8cf5fdd13e58dda79c33e057862c29a8d5b50",
|
||||
"sha256:cb659702a45136c743bc130760c6f137870d4df3a9e14386478b8a0511abcfca",
|
||||
"sha256:ce0930a963ff593e8bb6fda49a503911accc67dee7e5445eec972668e672a0f0",
|
||||
"sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78",
|
||||
"sha256:d184f85ad2bb1f261eac55cddfcf62a70dee89982c978e92b9a74a1bfef2e367",
|
||||
"sha256:d2a3e412ce1849be34b45922bfef03df32d1410a06d1cdeb793a343c2f1fd666",
|
||||
"sha256:d61ec60945d694df806a9aec88e8f29a27293c6e424f8ff91c80416e3c617645",
|
||||
"sha256:db0c742aad702fd5d0c6611a73f9602f20aec2007c102630c06d7633d9c8f09a",
|
||||
"sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3",
|
||||
"sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252",
|
||||
"sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79",
|
||||
"sha256:df7e5edac4778127f2bf452e0721a58a1cfa4d1d9eac63bdd650535eb8543615",
|
||||
"sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f",
|
||||
"sha256:e3c623923967f3e5961d272718655946e5322b8d058e094764180cdee7bab1af",
|
||||
"sha256:e69add9b6b7b08c60d7ff0152c7c9a6c45b4a71a919be5abde6f98f1ea16421c",
|
||||
"sha256:e8e0d177b1fe251c3b1b914ab64135475c5273c8cfd2857964b2e3bb0fe196a7",
|
||||
"sha256:ef45f31aec9be01379fc6c10f1d9c677f032f2bac9383c827d44f620e8a88407",
|
||||
"sha256:f1208c1c67ec9e151d78aa3435aa9b08a488b53d9cfac9b699f15255a3461ef2",
|
||||
"sha256:f12582b8d3b4c6be1d298c49cb7ae64a3a73efaf4c2ab4e37db182e3545815ac",
|
||||
"sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9",
|
||||
"sha256:f4eac0584cdc3285ef2e74eee1513a6001681fd9753b259e8159421ed28a72e5",
|
||||
"sha256:f7b64fcd670bca8800bc10ced36620c6bbb321e7bc1214b9c0c0df269c1dddc2",
|
||||
"sha256:fb7c61d4be18e930f75948705e9718618862e6fc2ed0d7159b2262be73f167a2"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.3.0"
|
||||
"version": "==5.3.1"
|
||||
},
|
||||
"markdown-it-py": {
|
||||
"hashes": [
|
||||
@@ -1419,12 +1424,12 @@
|
||||
},
|
||||
"ocrmypdf": {
|
||||
"hashes": [
|
||||
"sha256:007f2c536415ff570d43aabc01996578d3d07f277c585be446da771aff6d9a48",
|
||||
"sha256:28b7437a571610717de54d3074eaa3456721a6ea54c05cc15ad8301fdfdd4392"
|
||||
"sha256:33fec95450727b0d9482ee3851e45dd0219ff8d52a14fd45a8d3d0c71875584e",
|
||||
"sha256:d000a2294cd1478d4bbfe15df5172327f77f4139bb5307404bc53be9bd81f039"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==16.8.0"
|
||||
"version": "==16.9.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
@@ -1513,49 +1518,49 @@
|
||||
},
|
||||
"pikepdf": {
|
||||
"hashes": [
|
||||
"sha256:031347be6efe62f943712d0b94872a4bb907ffd7ad2740263429acbc60311b69",
|
||||
"sha256:106ac4976000481da8721c77e108a5afdd1d4d38bb229f728adc6c323b66a772",
|
||||
"sha256:10f5f0724ff2d5b2bf4ad33ca334d1053d7d7c8cd871abddf03df72e58f42aa4",
|
||||
"sha256:17d1f5667cf19093e7b4861a1cfe8a7c44b8cc74179c117da492bbc8c0843109",
|
||||
"sha256:1aab4342ede54879bb0966af41aca58f4d73a5d2ecf8a661161fffced6590a34",
|
||||
"sha256:1bbfc4f03f3355f08f525d8ef65c09f61a92b0e7b16da49ee40bedb9aa5f4a9b",
|
||||
"sha256:2a7adcf55e8b9f9b5e4797976b0c6dd2a9834a330139b38777892a4214c1c8cb",
|
||||
"sha256:2d3e5de71505aec66da5c0fde8b786fdec78e660b759a35f6dd9ff12eb0e153f",
|
||||
"sha256:3fcb8dcba75138b59285f3d492b9350c8b643689d7d8de83216a4d9576b91082",
|
||||
"sha256:44033a1908fc2bd2827e6b4f7e039eb8e9742488047112e4ca3991d636641761",
|
||||
"sha256:48a258dc8f3ba5381d3000082264f4bca93e00b640c267844140fac4cfe3ec79",
|
||||
"sha256:4a2e889b5365522ca88301617512c15e53bd64b48ad50f44f3d321ae47187b79",
|
||||
"sha256:5d8d2dd974cac31473adaa0dcad276adc18a53a1aebb05533be8fb90404d472e",
|
||||
"sha256:5fdf7aed55487d72c213e9224aef49f0370dc3a6501baaaf89d4eedfb57f3ef8",
|
||||
"sha256:6446470c47694be0b73d19fb1527f418356f05acc252d99c9f84eca98eadc1be",
|
||||
"sha256:656b801d10603a7bd6208b2796fa9d69756174130369d728de870db31b356b90",
|
||||
"sha256:6a4dfde19b5f3fb49c093059f6b1cb834309c7e2788f31f654ba4d99c6c2cd1d",
|
||||
"sha256:713664b861c572b47898d04a40293df13be79f7e2e5709939ce3512474a787fb",
|
||||
"sha256:7245302119d137651cd7585279c3c731960742e93162d9f4df5837f424dcdc8b",
|
||||
"sha256:7da459d7e57794bd3b4171c13d5bd642bb33327342e341c4c5e8451aa844f575",
|
||||
"sha256:81b16393bf28dd62d61fa1b8bd92e721adffcbf6a2cfaaaaab6cd634ff59efca",
|
||||
"sha256:8d8840939098956c348d5ff1e4105b988a1060932c72c5e996c103be5b21a390",
|
||||
"sha256:8f8caba01bc1eb989e13c99c9da884ae6d343e0e53c9987fa561788ba920590d",
|
||||
"sha256:91028bb5672b79de49c9e7c3fe75d9c80fb0af8a096dc731b7e4425243b72676",
|
||||
"sha256:91cb8ec804845a2614ff65539e5b417a469182adf2fafa32d62ef4723b9926e4",
|
||||
"sha256:99355e88ee8d69f148196ec8df5f2f16326698da6acd3601b97bb6497ed1f780",
|
||||
"sha256:9aa11fbfc9b27a722b4ea6b7e766725676530b6ad1a12cc95393d2fd234fb431",
|
||||
"sha256:9b82f2fd382ee2c18fe78824e76f0b1841ee77ae68d0e2f8a39f411925a3a4b6",
|
||||
"sha256:9d44a83537360c24b1f773a5ac00b7d1ab66685baecbbc055b3da8fc759cfb2a",
|
||||
"sha256:a1246c3706a2e14dd421570de0e4f562bbe6f2a4b3a30bcba5de7a596ff395bb",
|
||||
"sha256:aa7bcf2a46623e3f68892c8a365bf4986eb0a8c0c2996bdbd627c79e2c7c6abc",
|
||||
"sha256:ac14f0d99c996d437ac8f1e72f5e39c4534f8e341b0d84baf7e01ae154148a11",
|
||||
"sha256:b516a64185d83b3b0a7f3bc34a5f7124da9faab35748d5cb611cab41853cf569",
|
||||
"sha256:bb8d67c0098a6fd248a7ae1e03310c193706b82e7b39c6c1486f141f3697ccd1",
|
||||
"sha256:ca8e5608a482773cee054b7b9e63202faed9ee613fe59bcb4c712219eaef3981",
|
||||
"sha256:d2a1ba67ea285bb4c5af7623438748c0686db714eaa6c994ccf33c76d04d73b7",
|
||||
"sha256:dccdab8c176956ab049bf527cf4f47b4f678ac77d65659cc2575a27e3965ce3f",
|
||||
"sha256:e8e27be3253a09e01a21d5bc25c4f0ac78ca0732be292361a0d74f5fce180812",
|
||||
"sha256:f9a064f56803a36ba6c57b6e6f27ba849a813e3536d68b032167ef0f1a6a19cf",
|
||||
"sha256:fda775e99c1b2d541bdc1c21245fc1d595b25b654cc4d749f4dba32513bd1359"
|
||||
"sha256:0154a4b4d558ff488a662597128075f26956fa4b8682ce7f1bca7b383a7d6a6e",
|
||||
"sha256:0ec92b93d912ff8864c661f38b622062a5dc0e2296a86acc372dcf3828421095",
|
||||
"sha256:101b0718599b404e9235f51e024f4b555aec95fcbfa78d67e5edde8950e411f4",
|
||||
"sha256:103b73642c9ab175e93c771673bca565acad8b78d4a3af0f68319ab7ec6af990",
|
||||
"sha256:190b3bb4891a7a154315f505d7dcd557ef21e8130cea8b78eb9646f8d67072ed",
|
||||
"sha256:1ab0f76e376c2ccf247ee8ec6e7b2ecb4100a54ae2b9d0ed633f66d4425188cb",
|
||||
"sha256:1f27d95c7ce2dea03758af44f06edd1e6f8bf226aa804fa0ee7fa1d6ced21707",
|
||||
"sha256:20dc868d7e032afc614d2a7baea87fd45b025044313a9fd8b12add537a5b77a5",
|
||||
"sha256:217ab7fc70ce2c4befef39a07c8d0f1598f5d3e46d440514aab4a4655e954cfc",
|
||||
"sha256:24b5f7b4ffa17e3a0db232d68353ffc24dd4bd3658094e27dbdf6ee393753987",
|
||||
"sha256:353b23c5b75d7042c99bbb72dcc78063bc04599b5ec6516c301102e42afdff50",
|
||||
"sha256:3b8b47ca9a8fdad67edc802fb8435bed17cc164a3768f57c30c46ff428015c1d",
|
||||
"sha256:3f422e477c4408189e219ae27f2089e661e472877bacf99c3e077e03937f3cec",
|
||||
"sha256:406a98e2fc6fa2522d249921e261f3303a4e563e3ed9de6e924ad303df9aeb97",
|
||||
"sha256:412bd45c806ea3d7ef25a04b24a777c87efef725ac7f047d1fc71062049d2625",
|
||||
"sha256:4397d3dccda0f047b19d21fbcc50eae36b9745cf697d1eca4dd998bd5c12a952",
|
||||
"sha256:466f9936433dcc1e3c78ae6371e2ed6ac42fe23d8bb10e255fb9ef1aa36d82f3",
|
||||
"sha256:4aad61ce4b10239a7079e7553aed07620b049037cfeb5c972f4e5be56c9f875e",
|
||||
"sha256:4bced2322e060fc79b6b933428d63fa5d65e0de0060f8f661401b45f14194e62",
|
||||
"sha256:518fa733807d805930b1d122579806c43128cd9d298980a547f250f4cb8e0a4f",
|
||||
"sha256:52d8a6cb3d9e8ad5b9a5517fd6238203359c832b641fb1b254328708ae59e9e5",
|
||||
"sha256:63c564102cf39c6518a803e1653a820255d6283e27ad6517cbd60a224e56aba1",
|
||||
"sha256:65e395ed795b8eb2991b7838f96dd51ecab14c57ecdad82e07ad98560ae090d2",
|
||||
"sha256:8bec3ac14a56a9435b8ccd34c56b7a331caf0c960344f68570e551a899989a69",
|
||||
"sha256:a5ea94021603bd71d6a3ccc22b1d6799ff1ad4190224472550f73801b3beceed",
|
||||
"sha256:a7b191c782999fc7cfac01343db7096a30235385b80341f8eb2c704516fc432c",
|
||||
"sha256:ae4517cf8bc356609e1174c27309e128a78e155d6663e38346710bbe0c4373f4",
|
||||
"sha256:af5b7bbcaf80ef981fb3ff8a1ce9d8c4b4af96b35e71947525f70362235c784c",
|
||||
"sha256:b3aa2beec989035451d54a36443230995adda13677b2809a2d1f55767b040120",
|
||||
"sha256:b75b04ee87a216c94e75e947e1921ff6426f1589cb245ab3894f67ea10bf5e34",
|
||||
"sha256:bee6e7f93533c0d5ee66f65547963a56f85d0469b1134ddfc13439cf45b0e989",
|
||||
"sha256:dcc3d3bdcf3f63a0aece21afa9d517872cc375ec120d3e4143ff7ad5203cd9e4",
|
||||
"sha256:e771712910d47ae16d4b5f314922cb3f090ddab7ea06db4872ef519420c64ba8",
|
||||
"sha256:e89a0a74917feb0a9ab7450f83dc6b821d0c20ae28ee42cdee9b484ff3e114b7",
|
||||
"sha256:ee8ee014b10599e65c3edd38aaf100e1bb67d888ba52004e09eff830bf5ca845",
|
||||
"sha256:f0d19fb0646da6d69a86b28cb0a80ffde5a519f65cf79cc12451551977015fd2",
|
||||
"sha256:fadb3eac9a4c109d9a13a7f3687091333d160469983db319fcc7fd51bb74548b",
|
||||
"sha256:fc30dcf70fdce9a0bfd110e1afa0c21ef5f17ba8c743745651b1e3ac7c0ff122",
|
||||
"sha256:fc6a52ca6ad8bbd06ec84fb5c8ef5ed151d4fd360e2e6ffcabe2dc899cd87a76",
|
||||
"sha256:ff6044857ef3eef9eed61121e1189df816fbbe5363fa0dc1c446bd145f074ee3"
|
||||
],
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==9.5.1"
|
||||
"version": "==9.5.2"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
@@ -1717,7 +1722,7 @@
|
||||
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.9.0.post0"
|
||||
},
|
||||
"python-dotenv": {
|
||||
@@ -1757,10 +1762,10 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a",
|
||||
"sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"
|
||||
"sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57",
|
||||
"sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"
|
||||
],
|
||||
"version": "==2024.2"
|
||||
"version": "==2025.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
@@ -1840,98 +1845,98 @@
|
||||
},
|
||||
"rapidfuzz": {
|
||||
"hashes": [
|
||||
"sha256:0b488b244931d0291412917e6e46ee9f6a14376625e150056fe7c4426ef28225",
|
||||
"sha256:1315cd2a351144572e31fe3df68340d4b83ddec0af8b2e207cd32930c6acd037",
|
||||
"sha256:1bac4873f6186f5233b0084b266bfb459e997f4c21fc9f029918f44a9eccd304",
|
||||
"sha256:1cb1965a28b0fa64abdee130c788a0bc0bb3cf9ef7e3a70bf055c086c14a3d7e",
|
||||
"sha256:22033677982b9c4c49676f215b794b0404073f8974f98739cb7234e4a9ade9ad",
|
||||
"sha256:231c8b2efbd7f8d2ecd1ae900363ba168b8870644bb8f2b5aa96e4a7573bde19",
|
||||
"sha256:25398d9ac7294e99876a3027ffc52c6bebeb2d702b1895af6ae9c541ee676702",
|
||||
"sha256:2c87319b0ab9d269ab84f6453601fd49b35d9e4a601bbaef43743f26fabf496c",
|
||||
"sha256:3048c6ed29d693fba7d2a7caf165f5e0bb2b9743a0989012a98a47b975355cca",
|
||||
"sha256:339607394941801e6e3f6c1ecd413a36e18454e7136ed1161388de674f47f9d9",
|
||||
"sha256:3794df87313dfb56fafd679b962e0613c88a293fd9bd5dd5c2793d66bf06a101",
|
||||
"sha256:3857e335f97058c4b46fa39ca831290b70de554a5c5af0323d2f163b19c5f2a6",
|
||||
"sha256:3871fa7dfcef00bad3c7e8ae8d8fd58089bad6fb21f608d2bf42832267ca9663",
|
||||
"sha256:3f28952da055dbfe75828891cd3c9abf0984edc8640573c18b48c14c68ca5e06",
|
||||
"sha256:42f4dd264ada7a9aa0805ea0da776dc063533917773cf2df5217f14eb4429eae",
|
||||
"sha256:4416ca69af933d4a8ad30910149d3db6d084781d5c5fdedb713205389f535385",
|
||||
"sha256:4469307f464ae3089acf3210b8fc279110d26d10f79e576f385a98f4429f7d97",
|
||||
"sha256:4513dd01cee11e354c31b75f652d4d466c9440b6859f84e600bdebfccb17735a",
|
||||
"sha256:45b15b8a118856ac9caac6877f70f38b8a0d310475d50bc814698659eabc1cdb",
|
||||
"sha256:494eef2c68305ab75139034ea25328a04a548d297712d9cf887bf27c158c388b",
|
||||
"sha256:4d0d26c7172bdb64f86ee0765c5b26ea1dc45c52389175888ec073b9b28f4305",
|
||||
"sha256:4f9f12c2d0aa52b86206d2059916153876a9b1cf9dfb3cf2f344913167f1c3d4",
|
||||
"sha256:51f24cb39e64256221e6952f22545b8ce21cacd59c0d3e367225da8fc4b868d8",
|
||||
"sha256:54e7f442fb9cca81e9df32333fb075ef729052bcabe05b0afc0441f462299114",
|
||||
"sha256:5a167344c1d6db06915fb0225592afdc24d8bafaaf02de07d4788ddd37f4bc2f",
|
||||
"sha256:5b659e1e2ea2784a9a397075a7fc395bfa4fe66424042161c4bcaf6e4f637b38",
|
||||
"sha256:5bb636b0150daa6d3331b738f7c0f8b25eadc47f04a40e5c23c4bfb4c4e20ae3",
|
||||
"sha256:5e8ea35f2419c7d56b3e75fbde2698766daedb374f20eea28ac9b1f668ef4f74",
|
||||
"sha256:5e8f93bc736020351a6f8e71666e1f486bb8bd5ce8112c443a30c77bfde0eb68",
|
||||
"sha256:62171b270ecc4071be1c1f99960317db261d4c8c83c169e7f8ad119211fe7397",
|
||||
"sha256:6668321f90aa02a5a789d4e16058f2e4f2692c5230252425c3532a8a62bc3424",
|
||||
"sha256:6ad02bab756751c90fa27f3069d7b12146613061341459abf55f8190d899649f",
|
||||
"sha256:6b01c1ddbb054283797967ddc5433d5c108d680e8fa2684cf368be05407b07e4",
|
||||
"sha256:714a7ba31ba46b64d30fccfe95f8013ea41a2e6237ba11a805a27cdd3bce2573",
|
||||
"sha256:76a4a11ba8f678c9e5876a7d465ab86def047a4fcc043617578368755d63a1bc",
|
||||
"sha256:7864e80a0d4e23eb6194254a81ee1216abdc53f9dc85b7f4d56668eced022eb8",
|
||||
"sha256:82497f244aac10b20710448645f347d862364cc4f7d8b9ba14bd66b5ce4dec18",
|
||||
"sha256:84819390a36d6166cec706b9d8f0941f115f700b7faecab5a7e22fc367408bc3",
|
||||
"sha256:8724a978f8af7059c5323d523870bf272a097478e1471295511cf58b2642ff83",
|
||||
"sha256:8b63cb1f2eb371ef20fb155e95efd96e060147bdd4ab9fc400c97325dfee9fe1",
|
||||
"sha256:8c7af25bda96ac799378ac8aba54a8ece732835c7b74cfc201b688a87ed11152",
|
||||
"sha256:8dd501de6f7a8f83557d20613b58734d1cb5f0be78d794cde64fe43cfc63f5f2",
|
||||
"sha256:8ed59044aea9eb6c663112170f2399b040d5d7b162828b141f2673e822093fa8",
|
||||
"sha256:906f1f2a1b91c06599b3dd1be207449c5d4fc7bd1e1fa2f6aef161ea6223f165",
|
||||
"sha256:92ebb7c12f682b5906ed98429f48a3dd80dd0f9721de30c97a01473d1a346576",
|
||||
"sha256:99aebef8268f2bc0b445b5640fd3312e080bd17efd3fbae4486b20ac00466308",
|
||||
"sha256:9a1b3ebc62d4bcdfdeba110944a25ab40916d5383c5e57e7c4a8dc0b6c17211a",
|
||||
"sha256:9a52eea839e4bdc72c5e60a444d26004da00bb5bc6301e99b3dde18212e41465",
|
||||
"sha256:9c6d7fea39cb33e71de86397d38bf7ff1a6273e40367f31d05761662ffda49e4",
|
||||
"sha256:a53ca4d3f52f00b393fab9b5913c5bafb9afc27d030c8a1db1283da6917a860f",
|
||||
"sha256:a7743cca45b4684c54407e8638f6d07b910d8d811347b9d42ff21262c7c23245",
|
||||
"sha256:aaf391fb6715866bc14681c76dc0308f46877f7c06f61d62cc993b79fc3c4a2a",
|
||||
"sha256:ab9eab33ee3213f7751dc07a1a61b8d9a3d748ca4458fffddd9defa6f0493c16",
|
||||
"sha256:b04f29735bad9f06bb731c214f27253bd8bedb248ef9b8a1b4c5bde65b838454",
|
||||
"sha256:b1472986fd9c5d318399a01a0881f4a0bf4950264131bb8e2deba9df6d8c362b",
|
||||
"sha256:b1d67d67f89e4e013a5295e7523bc34a7a96f2dba5dd812c7c8cb65d113cbf28",
|
||||
"sha256:b1f7efdd7b7adb32102c2fa481ad6f11923e2deb191f651274be559d56fc913b",
|
||||
"sha256:b2669eafee38c5884a6e7cc9769d25c19428549dcdf57de8541cf9e82822e7db",
|
||||
"sha256:ba26d87fe7fcb56c4a53b549a9e0e9143f6b0df56d35fe6ad800c902447acd5b",
|
||||
"sha256:be15496e7244361ff0efcd86e52559bacda9cd975eccf19426a0025f9547c792",
|
||||
"sha256:c36539ed2c0173b053dafb221458812e178cfa3224ade0960599bec194637048",
|
||||
"sha256:c408f09649cbff8da76f8d3ad878b64ba7f7abdad1471efb293d2c075e80c822",
|
||||
"sha256:cd340bbd025302276b5aa221dccfe43040c7babfc32f107c36ad783f2ffd8775",
|
||||
"sha256:d0edecc3f90c2653298d380f6ea73b536944b767520c2179ec5d40b9145e47aa",
|
||||
"sha256:d2a0f7e17f33e7890257367a1662b05fecaf56625f7dbb6446227aaa2b86448b",
|
||||
"sha256:d71da0012face6f45432a11bc59af19e62fac5a41f8ce489e80c0add8153c3d1",
|
||||
"sha256:d895998fec712544c13cfe833890e0226585cf0391dd3948412441d5d68a2b8c",
|
||||
"sha256:d95f9e9f3777b96241d8a00d6377cc9c716981d828b5091082d0fe3a2924b43e",
|
||||
"sha256:d9727b85511b912571a76ce53c7640ba2c44c364e71cef6d7359b5412739c570",
|
||||
"sha256:d98a46cf07c0c875d27e8a7ed50f304d83063e49b9ab63f21c19c154b4c0d08d",
|
||||
"sha256:d994cf27e2f874069884d9bddf0864f9b90ad201fcc9cb2f5b82bacc17c8d5f2",
|
||||
"sha256:dc0e0d41ad8a056a9886bac91ff9d9978e54a244deb61c2972cc76b66752de9c",
|
||||
"sha256:dfaefe08af2a928e72344c800dcbaf6508e86a4ed481e28355e8d4b6a6a5230e",
|
||||
"sha256:e60814edd0c9b511b5f377d48b9782b88cfe8be07a98f99973669299c8bb318a",
|
||||
"sha256:eb8a54543d16ab1b69e2c5ed96cabbff16db044a50eddfc028000138ca9ddf33",
|
||||
"sha256:eb97c53112b593f89a90b4f6218635a9d1eea1d7f9521a3b7d24864228bbc0aa",
|
||||
"sha256:ebadd5b8624d8ad503e505a99b8eb26fe3ea9f8e9c2234e805a27b269e585842",
|
||||
"sha256:ec8d7d8567e14af34a7911c98f5ac74a3d4a743cd848643341fc92b12b3784ff",
|
||||
"sha256:ed78c8e94f57b44292c1a0350f580e18d3a3c5c0800e253f1583580c1b417ad2",
|
||||
"sha256:eea8d9e20632d68f653455265b18c35f90965e26f30d4d92f831899d6682149b",
|
||||
"sha256:ef8937dae823b889c0273dfa0f0f6c46a3658ac0d851349c464d1b00e7ff4252",
|
||||
"sha256:f06e3c4c0a8badfc4910b9fd15beb1ad8f3b8fafa8ea82c023e5e607b66a78e4",
|
||||
"sha256:f0821b9bdf18c5b7d51722b906b233a39b17f602501a966cfbd9b285f8ab83cd",
|
||||
"sha256:f0ba13557fec9d5ffc0a22826754a7457cc77f1b25145be10b7bb1d143ce84c6",
|
||||
"sha256:f382fec4a7891d66fb7163c90754454030bb9200a13f82ee7860b6359f3f2fa8",
|
||||
"sha256:fe7aaf5a54821d340d21412f7f6e6272a9b17a0cbafc1d68f77f2fc11009dcd5",
|
||||
"sha256:ff38378346b7018f42cbc1f6d1d3778e36e16d8595f79a312b31e7c25c50bd08",
|
||||
"sha256:ffa1bb0e26297b0f22881b219ffc82a33a3c84ce6174a9d69406239b14575bd5"
|
||||
"sha256:00ceb8ff3c44ab0d6014106c71709c85dee9feedd6890eff77c814aa3798952b",
|
||||
"sha256:018506a53c3b20dcbda8c93d4484b9eb1764c93d5ea16be103cf6b0d8b11d860",
|
||||
"sha256:04283c6f3e79f13a784f844cd5b1df4f518ad0f70c789aea733d106c26e1b4fb",
|
||||
"sha256:046fc67f3885d94693a2151dd913aaf08b10931639cbb953dfeef3151cb1027c",
|
||||
"sha256:0666ab4c52e500af7ba5cc17389f5d15c0cdad06412c80312088519fdc25686d",
|
||||
"sha256:0acbd27543b158cb915fde03877383816a9e83257832818f1e803bac9b394900",
|
||||
"sha256:0b31ab59e1a0df5afc21f3109b6cfd77b34040dbf54f1bad3989f885cfae1e60",
|
||||
"sha256:0d03ad14a26a477be221fddc002954ae68a9e2402b9d85433f2d0a6af01aa2bb",
|
||||
"sha256:12802e5c4d8ae104fb6efeeb436098325ce0dca33b461c46e8df015c84fbef26",
|
||||
"sha256:129d536740ab0048c1a06ccff73c683f282a2347c68069affae8dbc423a37c50",
|
||||
"sha256:165bcdecbfed9978962da1d3ec9c191b2ff9f1ccc2668fbaf0613a975b9aa326",
|
||||
"sha256:187cdb402e223264eebed2fe671e367e636a499a7a9c82090b8d4b75aa416c2a",
|
||||
"sha256:1ae41361de05762c1eaa3955e5355de7c4c6f30d1ef1ea23d29bf738a35809ab",
|
||||
"sha256:1b67e390261ffe98ec86c771b89425a78b60ccb610c3b5874660216fcdbded4b",
|
||||
"sha256:2477da227e266f9c712f11393182c69a99d3c8007ea27f68c5afc3faf401cc43",
|
||||
"sha256:27b4d440fa50b50c515a91a01ee17e8ede719dca06eef4c0cccf1a111a4cfad3",
|
||||
"sha256:2cf27e8e4bf7bf9d92ef04f3d2b769e91c3f30ba99208c29f5b41e77271a2614",
|
||||
"sha256:2d7d9e6a04d8344b0198c96394c28874086888d0a2b2f605f30d1b27b9377b7d",
|
||||
"sha256:2d844c0587d969ce36fbf4b7cbf0860380ffeafc9ac5e17a7cbe8abf528d07bb",
|
||||
"sha256:325c9c71b737fcd32e2a4e634c430c07dd3d374cfe134eded3fe46e4c6f9bf5d",
|
||||
"sha256:346a2d8f17224e99f9ef988606c83d809d5917d17ad00207237e0965e54f9730",
|
||||
"sha256:34dcbf5a7daecebc242f72e2500665f0bde9dd11b779246c6d64d106a7d57c99",
|
||||
"sha256:3a860d103bbb25c69c2e995fdf4fac8cb9f77fb69ec0a00469d7fd87ff148f46",
|
||||
"sha256:3c5ec360694ac14bfaeb6aea95737cf1a6cf805b5fe8ea7fd28814706c7fa838",
|
||||
"sha256:3ecf0e6de84c0bc2c0f48bc03ba23cef2c5f1245db7b26bc860c11c6fd7a097c",
|
||||
"sha256:3fe8da12ea77271097b303fa7624cfaf5afd90261002314e3b0047d36f4afd8d",
|
||||
"sha256:42149e6d13bd6d06437d2a954dae2184dadbbdec0fdb82dafe92860d99f80519",
|
||||
"sha256:43bb17056c5d1332f517b888c4e57846c4b5f936ed304917eeb5c9ac85d940d4",
|
||||
"sha256:4a4422e4f73a579755ab60abccb3ff148b5c224b3c7454a13ca217dfbad54da6",
|
||||
"sha256:4c26cd1b9969ea70dbf0dbda3d2b54ab4b2e683d0fd0f17282169a19563efeb1",
|
||||
"sha256:4dc2ebad4adb29d84a661f6a42494df48ad2b72993ff43fad2b9794804f91e45",
|
||||
"sha256:5620001fd4d6644a2f56880388179cc8f3767670f0670160fcb97c3b46c828af",
|
||||
"sha256:5942dc4460e5030c5f9e1d4c9383de2f3564a2503fe25e13e89021bcbfea2f44",
|
||||
"sha256:69f2520296f1ae1165b724a3aad28c56fd0ac7dd2e4cff101a5d986e840f02d4",
|
||||
"sha256:6a98bbca18b4a37adddf2d8201856441c26e9c981d8895491b5bc857b5f780eb",
|
||||
"sha256:6b5e176524653ac46f1802bdd273a4b44a5f8d0054ed5013a8e8a4b72f254599",
|
||||
"sha256:6d9afad7b16d01c9e8929b6a205a18163c7e61b6cd9bcf9c81be77d5afc1067a",
|
||||
"sha256:6f463c6f1c42ec90e45d12a6379e18eddd5cdf74138804d8215619b6f4d31cea",
|
||||
"sha256:6f7e92fc7d2a7f02e1e01fe4f539324dfab80f27cb70a30dd63a95445566946b",
|
||||
"sha256:760ac95d788f2964b73da01e0bdffbe1bf2ad8273d0437565ce9092ae6ad1fbc",
|
||||
"sha256:773ab37fccf6e0513891f8eb4393961ddd1053c6eb7e62eaa876e94668fc6d31",
|
||||
"sha256:7fa7b81fb52902d5f78dac42b3d6c835a6633b01ddf9b202a3ca8443be4b2d6a",
|
||||
"sha256:80ff9283c54d7d29b2d954181e137deee89bec62f4a54675d8b6dbb6b15d3e03",
|
||||
"sha256:82260b20bc7a76556cecb0c063c87dad19246a570425d38f8107b8404ca3ac97",
|
||||
"sha256:834f6113d538af358f39296604a1953e55f8eeffc20cb4caf82250edbb8bf679",
|
||||
"sha256:8389d98b9f54cb4f8a95f1fa34bf0ceee639e919807bb931ca479c7a5f2930bf",
|
||||
"sha256:83dccfd5a754f2a0e8555b23dde31f0f7920601bfa807aa76829391ea81e7c67",
|
||||
"sha256:841e0c2a5fbe8fc8b9b1a56e924c871899932c0ece7fbd970aa1c32bfd12d4bf",
|
||||
"sha256:8499c7d963ddea8adb6cffac2861ee39a1053e22ca8a5ee9de1197f8dc0275a5",
|
||||
"sha256:8dc1937198e7ff67e217e60bfa339f05da268d91bb15fec710452d11fe2fdf60",
|
||||
"sha256:920733a28c3af47870835d59ca9879579f66238f10de91d2b4b3f809d1ebfc5b",
|
||||
"sha256:930756639643e3aa02d3136b6fec74e5b9370a24f8796e1065cd8a857a6a6c50",
|
||||
"sha256:97c885a7a480b21164f57a706418c9bbc9a496ec6da087e554424358cadde445",
|
||||
"sha256:97f824c15bc6933a31d6e3cbfa90188ba0e5043cf2b6dd342c2b90ee8b3fd47c",
|
||||
"sha256:9c78582f50e75e6c2bc38c791ed291cb89cf26a3148c47860c1a04d6e5379c8e",
|
||||
"sha256:a66520180d3426b9dc2f8d312f38e19bc1fc5601f374bae5c916f53fa3534a7d",
|
||||
"sha256:a718f740553aad5f4daef790191511da9c6eae893ee1fc2677627e4b624ae2db",
|
||||
"sha256:a93c95dce8917bf428064c64024de43ffd34ec5949dd4425780c72bd41f9d969",
|
||||
"sha256:a940aa71a7f37d7f0daac186066bf6668d4d3b7e7ef464cb50bc7ba89eae1f51",
|
||||
"sha256:a973b3f5cabf931029a3ae4a0f72e3222e53d412ea85fc37ddc49e1774f00fbf",
|
||||
"sha256:af4585e5812632c357fee5ab781c29f00cd06bea58f8882ff244cc4906ba6c9e",
|
||||
"sha256:b1d4fbff980cb6baef4ee675963c081f7b5d6580a105d6a4962b20f1f880e1fb",
|
||||
"sha256:b4d2d39b2e76c17f92edd6d384dc21fa020871c73251cdfa017149358937a41d",
|
||||
"sha256:b572b634740e047c53743ed27a1bb3b4f93cf4abbac258cd7af377b2c4a9ba5b",
|
||||
"sha256:b79286738a43e8df8420c4b30a92712dec6247430b130f8e015c3a78b6d61ac2",
|
||||
"sha256:b7cba636c32a6fc3a402d1cb2c70c6c9f8e6319380aaf15559db09d868a23e56",
|
||||
"sha256:b85817a57cf8db32dd5d2d66ccfba656d299b09eaf86234295f89f91be1a0db2",
|
||||
"sha256:b894fa2b30cd6498a29e5c470cb01c6ea898540b7e048a0342775a5000531334",
|
||||
"sha256:b8b61c558574fbc093d85940c3264c08c2b857b8916f8e8f222e7b86b0bb7d12",
|
||||
"sha256:bb424ae7240f2d2f7d8dda66a61ebf603f74d92f109452c63b0dbf400204a437",
|
||||
"sha256:bd47dfb1bca9673a48b923b3d988b7668ee8efd0562027f58b0f2b7abf27144c",
|
||||
"sha256:bef5c91d5db776523530073cda5b2a276283258d2f86764be4a008c83caf7acd",
|
||||
"sha256:bf56ea4edd69005786e6c80a9049d95003aeb5798803e7a2906194e7a3cb6472",
|
||||
"sha256:c5857dda85165b986c26a474b22907db6b93932c99397c818bcdec96340a76d5",
|
||||
"sha256:c6e4ed63e204daa863a802eec09feea5448617981ba5d150f843ad8e3ae071a4",
|
||||
"sha256:cbdf145c7e4ebf2e81c794ed7a582c4acad19e886d5ad6676086369bd6760753",
|
||||
"sha256:d60d1db1b7e470e71ae096b6456e20ec56b52bde6198e2dbbc5e6769fa6797dc",
|
||||
"sha256:d6899b41bf6c30282179f77096c1939f1454836440a8ab05b48ebf7026a3b590",
|
||||
"sha256:dbb7ea2fd786e6d66f225ef6eef1728832314f47e82fee877cb2a793ebda9579",
|
||||
"sha256:dc3c39e0317e7f68ba01bac056e210dd13c7a0abf823e7b6a5fe7e451ddfc496",
|
||||
"sha256:df7880e012228722dec1be02b9ef3898ed023388b8a24d6fa8213d7581932510",
|
||||
"sha256:e1061311d07e7cdcffa92c9b50c2ab4192907e70ca01b2e8e1c0b6b4495faa37",
|
||||
"sha256:e31be53d7f4905a6a038296d8b773a79da9ee9f0cd19af9490c5c5a22e37d2e5",
|
||||
"sha256:ec9eaf73501c9a7de2c6938cb3050392e2ee0c5ca3921482acf01476b85a7226",
|
||||
"sha256:f1187aeae9c89e838d2a0a2b954b4052e4897e5f62e5794ef42527bf039d469e",
|
||||
"sha256:f6235b57ae3faa3f85cb3f90c9fee49b21bd671b76e90fc99e8ca2bdf0b5e4a3",
|
||||
"sha256:fbe7580b5fb2db8ebd53819171ff671124237a55ada3f64d20fc9a149d133960",
|
||||
"sha256:fd37e53f0ed239d0cec27b250cec958982a8ba252ce64aa5e6052de3a82fa8db"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==3.11.0"
|
||||
"version": "==3.12.1"
|
||||
},
|
||||
"redis": {
|
||||
"extras": [
|
||||
@@ -2352,7 +2357,7 @@
|
||||
"sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
|
||||
"sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.17.0"
|
||||
},
|
||||
"sniffio": {
|
||||
@@ -2721,12 +2726,12 @@
|
||||
},
|
||||
"whitenoise": {
|
||||
"hashes": [
|
||||
"sha256:486bd7267a375fa9650b136daaec156ac572971acc8bf99add90817a530dd1d4",
|
||||
"sha256:df12dce147a043d1956d81d288c6f0044147c6d2ab9726e5772ac50fb45d2280"
|
||||
"sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609",
|
||||
"sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==6.8.2"
|
||||
"version": "==6.9.0"
|
||||
},
|
||||
"whoosh": {
|
||||
"hashes": [
|
||||
@@ -3355,20 +3360,20 @@
|
||||
},
|
||||
"factory-boy": {
|
||||
"hashes": [
|
||||
"sha256:7b1113c49736e1e9995bc2a18f4dbf2c52cf0f841103517010b1d825712ce3ca",
|
||||
"sha256:8317aa5289cdfc45f9cae570feb07a6177316c82e34d14df3c2e1f22f26abef0"
|
||||
"sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc",
|
||||
"sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.3.1"
|
||||
"version": "==3.3.3"
|
||||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:42f2da8cf561e38c72b25e9891168b1e25fec42b6b0b5b0b6cd6041da54af885",
|
||||
"sha256:926d2301787220e0554c2e39afc4dc535ce4b0a8d0a089657137999f66334ef4"
|
||||
"sha256:aa0b93487d3adf7cd89953d172e3df896cb7b35d8a5222c0da873edbe2f7adf5",
|
||||
"sha256:f40510350aecfe006f45cb3f8879b35e861367cf347f51a7f2ca2c0571fdcc0b"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==35.0.0"
|
||||
"markers": "python_version >= '3.9'",
|
||||
"version": "==36.1.0"
|
||||
},
|
||||
"filelock": {
|
||||
"hashes": [
|
||||
@@ -3438,11 +3443,11 @@
|
||||
},
|
||||
"imagehash": {
|
||||
"hashes": [
|
||||
"sha256:5ad9a5cde14fe255745a8245677293ac0d67f09c330986a351f34b614ba62fb5",
|
||||
"sha256:7038d1b7f9e0585beb3dd8c0a956f02b95a346c0b5f24a9e8cc03ebadaf0aa70"
|
||||
"sha256:02b0f965f8c77cd813f61d7d39031ea27d4780e7ebcad56c6cd6a709acc06e5f",
|
||||
"sha256:e54a79805afb82a34acde4746a16540503a9636fd1ffb31d8e099b29bbbf8156"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.3.1"
|
||||
"version": "==4.3.2"
|
||||
},
|
||||
"incremental": {
|
||||
"hashes": [
|
||||
@@ -3577,12 +3582,12 @@
|
||||
},
|
||||
"mkdocs-material": {
|
||||
"hashes": [
|
||||
"sha256:1125622067e26940806701219303b27c0933e04533560725d97ec26fd16a39cf",
|
||||
"sha256:c87f7d1c39ce6326da5e10e232aed51bae46252e646755900f4b0fc9192fa832"
|
||||
"sha256:414e8376551def6d644b8e6f77226022868532a792eb2c9accf52199009f568f",
|
||||
"sha256:4d1d35e1c1d3e15294cb7fa5d02e0abaee70d408f75027dc7be6e30fb32e6867"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==9.6.3"
|
||||
"version": "==9.6.4"
|
||||
},
|
||||
"mkdocs-material-extensions": {
|
||||
"hashes": [
|
||||
@@ -4317,6 +4322,14 @@
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.12.2"
|
||||
},
|
||||
"tzdata": {
|
||||
"hashes": [
|
||||
"sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694",
|
||||
"sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"
|
||||
],
|
||||
"markers": "python_version >= '2'",
|
||||
"version": "==2025.1"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df",
|
||||
|
@@ -47,7 +47,8 @@
|
||||
"sv-SE": "src/locale/messages.sv_SE.xlf",
|
||||
"tr-TR": "src/locale/messages.tr_TR.xlf",
|
||||
"uk-UA": "src/locale/messages.uk_UA.xlf",
|
||||
"zh-CN": "src/locale/messages.zh_CN.xlf"
|
||||
"zh-CN": "src/locale/messages.zh_CN.xlf",
|
||||
"zh-TW": "src/locale/messages.zh_TW.xlf"
|
||||
}
|
||||
},
|
||||
"architect": {
|
||||
|
@@ -2230,7 +2230,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
|
||||
<context context-type="linenumber">103</context>
|
||||
<context context-type="linenumber">106</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
|
||||
@@ -2565,7 +2565,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
|
||||
<context context-type="linenumber">105</context>
|
||||
<context context-type="linenumber">108</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.ts</context>
|
||||
@@ -3322,7 +3322,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
|
||||
<context context-type="linenumber">85</context>
|
||||
<context context-type="linenumber">87</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1841172489943868696" datatype="html">
|
||||
@@ -3333,7 +3333,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
|
||||
<context context-type="linenumber">93</context>
|
||||
<context context-type="linenumber">96</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6048892649018070225" datatype="html">
|
||||
@@ -4099,6 +4099,18 @@
|
||||
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
|
||||
<context context-type="linenumber">111</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">165</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">189</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">213</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/toast/toast.component.html</context>
|
||||
<context context-type="linenumber">30</context>
|
||||
@@ -5548,7 +5560,7 @@
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">156</context>
|
||||
<context context-type="linenumber">231</context>
|
||||
</context-group>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/mail/mail.component.html</context>
|
||||
@@ -5943,77 +5955,98 @@
|
||||
<source>Migration Status</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">56</context>
|
||||
<context context-type="linenumber">65</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7489316373554112115" datatype="html">
|
||||
<source>Up to date</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">59</context>
|
||||
<context context-type="linenumber">69</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="7881311375431899727" datatype="html">
|
||||
<source>Latest Migration</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">64</context>
|
||||
<context context-type="linenumber">74</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4632965004151576238" datatype="html">
|
||||
<source>Pending Migrations</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">66</context>
|
||||
<context context-type="linenumber">76</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6904866445262015585" datatype="html">
|
||||
<source>Tasks</source>
|
||||
<trans-unit id="2790343143501919450" datatype="html">
|
||||
<source>Tasks Queue</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">83</context>
|
||||
<context context-type="linenumber">94</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6911698235105017958" datatype="html">
|
||||
<source>Redis Status</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">87</context>
|
||||
<context context-type="linenumber">98</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5349496739889768589" datatype="html">
|
||||
<source>Celery Status</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">96</context>
|
||||
<context context-type="linenumber">116</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2041675390931385838" datatype="html">
|
||||
<source>Health</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">142</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="31377277941774469" datatype="html">
|
||||
<source>Search Index</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">105</context>
|
||||
<context context-type="linenumber">146</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4089509911694721896" datatype="html">
|
||||
<source>Last Updated</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">119</context>
|
||||
<context context-type="linenumber">163</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="46628344485199198" datatype="html">
|
||||
<source>Classifier</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">121</context>
|
||||
<context context-type="linenumber">168</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6096684179126491743" datatype="html">
|
||||
<source>Last Trained</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">139</context>
|
||||
<context context-type="linenumber">187</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6427836860962380759" datatype="html">
|
||||
<source>Sanity Checker</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">192</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6578747070254776938" datatype="html">
|
||||
<source>Last Run</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/common/system-status-dialog/system-status-dialog.component.html</context>
|
||||
<context context-type="linenumber">211</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="6732151329960766506" datatype="html">
|
||||
@@ -8160,28 +8193,28 @@
|
||||
<source>Confirm delete field</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
|
||||
<context context-type="linenumber">101</context>
|
||||
<context context-type="linenumber">104</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="2939457975223185057" datatype="html">
|
||||
<source>This operation will permanently delete this field.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
|
||||
<context context-type="linenumber">102</context>
|
||||
<context context-type="linenumber">105</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4679555638382452936" datatype="html">
|
||||
<source>Deleted field "<x id="PH" equiv-text="field.name"/>"</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
|
||||
<context context-type="linenumber">111</context>
|
||||
<context context-type="linenumber">114</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4704551499967874824" datatype="html">
|
||||
<source>Error deleting field "<x id="PH" equiv-text="field.name"/>".</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/components/manage/custom-fields/custom-fields.component.ts</context>
|
||||
<context context-type="linenumber">118</context>
|
||||
<context context-type="linenumber">122</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8084492669582894778" datatype="html">
|
||||
@@ -9620,32 +9653,39 @@
|
||||
<context context-type="linenumber">243</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="8082606363137705994" datatype="html">
|
||||
<source>Chinese Traditional</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">249</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="4912706592792948707" datatype="html">
|
||||
<source>ISO 8601</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">251</context>
|
||||
<context context-type="linenumber">257</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="313643372755303297" datatype="html">
|
||||
<source>Successfully completed one-time migratration of settings to the database!</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">584</context>
|
||||
<context context-type="linenumber">590</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="5558341108007064934" datatype="html">
|
||||
<source>Unable to migrate settings to the database, please try saving manually.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">585</context>
|
||||
<context context-type="linenumber">591</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="1168781785897678748" datatype="html">
|
||||
<source>You can restart the tour from the settings page.</source>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||
<context context-type="linenumber">661</context>
|
||||
<context context-type="linenumber">667</context>
|
||||
</context-group>
|
||||
</trans-unit>
|
||||
<trans-unit id="3852289441366561594" datatype="html">
|
||||
|
@@ -40,6 +40,7 @@ import localeSv from '@angular/common/locales/sv'
|
||||
import localeTr from '@angular/common/locales/tr'
|
||||
import localeUk from '@angular/common/locales/uk'
|
||||
import localeZh from '@angular/common/locales/zh'
|
||||
import localeZhHant from '@angular/common/locales/zh-Hant'
|
||||
|
||||
registerLocaleData(localeAf)
|
||||
registerLocaleData(localeAr)
|
||||
@@ -73,6 +74,7 @@ registerLocaleData(localeSv)
|
||||
registerLocaleData(localeTr)
|
||||
registerLocaleData(localeUk)
|
||||
registerLocaleData(localeZh)
|
||||
registerLocaleData(localeZhHant)
|
||||
|
||||
/* global mocks for jsdom */
|
||||
const mock = () => {
|
||||
@@ -118,3 +120,20 @@ Object.defineProperty(window, 'location', {
|
||||
HTMLCanvasElement.prototype.getContext = <
|
||||
typeof HTMLCanvasElement.prototype.getContext
|
||||
>jest.fn()
|
||||
|
||||
// pdfjs
|
||||
jest.mock('pdfjs-dist', () => ({
|
||||
getDocument: jest.fn(() => ({
|
||||
promise: Promise.resolve({ numPages: 3 }),
|
||||
})),
|
||||
GlobalWorkerOptions: { workerSrc: '' },
|
||||
VerbosityLevel: { ERRORS: 0 },
|
||||
globalThis: {
|
||||
pdfjsLib: {
|
||||
GlobalWorkerOptions: {
|
||||
workerSrc: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
jest.mock('pdfjs-dist/web/pdf_viewer', () => ({}))
|
||||
|
@@ -303,12 +303,17 @@ describe('SettingsComponent', () => {
|
||||
redis_error:
|
||||
'Error 61 connecting to localhost:6379. Connection refused.',
|
||||
celery_status: SystemStatusItemStatus.ERROR,
|
||||
celery_url: 'celery@localhost',
|
||||
celery_error: 'Error connecting to celery@localhost',
|
||||
index_status: SystemStatusItemStatus.OK,
|
||||
index_last_modified: new Date().toISOString(),
|
||||
index_error: null,
|
||||
classifier_status: SystemStatusItemStatus.OK,
|
||||
classifier_last_trained: new Date().toISOString(),
|
||||
classifier_error: null,
|
||||
sanity_check_status: SystemStatusItemStatus.ERROR,
|
||||
sanity_check_last_run: new Date().toISOString(),
|
||||
sanity_check_error: 'Error running sanity check.',
|
||||
},
|
||||
}
|
||||
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
|
||||
|
@@ -19,6 +19,7 @@ import { allIcons, NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import {
|
||||
PaperlessTask,
|
||||
PaperlessTaskName,
|
||||
PaperlessTaskStatus,
|
||||
PaperlessTaskType,
|
||||
} from 'src/app/data/paperless-task'
|
||||
@@ -39,7 +40,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'test.pdf',
|
||||
date_created: new Date('2023-03-01T10:26:03.093116Z'),
|
||||
date_done: new Date('2023-03-01T10:26:07.223048Z'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Failed,
|
||||
result: 'test.pd: Not consuming test.pdf: It is a duplicate of test (#100)',
|
||||
acknowledged: false,
|
||||
@@ -51,7 +53,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: '191092.pdf',
|
||||
date_created: new Date('2023-03-01T09:26:03.093116Z'),
|
||||
date_done: new Date('2023-03-01T09:26:07.223048Z'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Failed,
|
||||
result:
|
||||
'191092.pd: Not consuming 191092.pdf: It is a duplicate of 191092 (#311)',
|
||||
@@ -64,7 +67,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'Scan Jun 6, 2023 at 3.19 PM.pdf',
|
||||
date_created: new Date('2023-06-06T15:22:05.722323-07:00'),
|
||||
date_done: new Date('2023-06-06T15:22:14.564305-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Pending,
|
||||
result: null,
|
||||
acknowledged: false,
|
||||
@@ -76,7 +80,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'paperless-mail-l4dkg8ir',
|
||||
date_created: new Date('2023-06-04T11:24:32.898089-07:00'),
|
||||
date_done: new Date('2023-06-04T11:24:44.678605-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Complete,
|
||||
result: 'Success. New document id 422 created',
|
||||
acknowledged: false,
|
||||
@@ -88,7 +93,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'onlinePaymentSummary.pdf',
|
||||
date_created: new Date('2023-06-01T13:49:51.631305-07:00'),
|
||||
date_done: new Date('2023-06-01T13:49:54.190220-07:00'),
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Complete,
|
||||
result: 'Success. New document id 421 created',
|
||||
acknowledged: false,
|
||||
@@ -100,7 +106,8 @@ const tasks: PaperlessTask[] = [
|
||||
task_file_name: 'paperless-mail-_rrpmqk6',
|
||||
date_created: new Date('2023-06-07T02:54:35.694916Z'),
|
||||
date_done: null,
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Started,
|
||||
result: null,
|
||||
acknowledged: false,
|
||||
@@ -155,7 +162,9 @@ describe('TasksComponent', () => {
|
||||
jest.useFakeTimers()
|
||||
fixture.detectChanges()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}tasks/`)
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||
)
|
||||
.flush(tasks)
|
||||
})
|
||||
|
||||
|
@@ -244,6 +244,13 @@ main {
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.navbar-toggler {
|
||||
// compensate for 2 buttons on the right
|
||||
margin-right: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.navbar-brand.slim {
|
||||
max-width: 50px;
|
||||
|
@@ -12,8 +12,6 @@
|
||||
|
||||
<pngx-input-color i18n-title title="Color" formControlName="color" [error]="error?.color"></pngx-input-color>
|
||||
|
||||
<pngx-input-select i18n-title title="Parent" formControlName="parent" [items]="tags" [allowNull]="true" [error]="error?.parent"></pngx-input-select>
|
||||
|
||||
<pngx-input-check i18n-title title="Inbox tag" formControlName="is_inbox_tag" i18n-hint hint="Inbox tags are automatically assigned to all consumed documents."></pngx-input-check>
|
||||
<pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||
@if (patternRequired) {
|
||||
|
@@ -36,8 +36,6 @@ import { TextComponent } from '../../input/text/text.component'
|
||||
],
|
||||
})
|
||||
export class TagEditDialogComponent extends EditDialogComponent<Tag> {
|
||||
tags: Tag[]
|
||||
|
||||
constructor(
|
||||
service: TagService,
|
||||
activeModal: NgbActiveModal,
|
||||
@@ -45,10 +43,6 @@ export class TagEditDialogComponent extends EditDialogComponent<Tag> {
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
|
||||
this.service.listAll().subscribe((result) => {
|
||||
this.tags = result.results
|
||||
})
|
||||
}
|
||||
|
||||
getCreateTitle() {
|
||||
@@ -64,7 +58,6 @@ export class TagEditDialogComponent extends EditDialogComponent<Tag> {
|
||||
name: new FormControl(''),
|
||||
color: new FormControl(randomColor()),
|
||||
is_inbox_tag: new FormControl(false),
|
||||
parent: new FormControl(null),
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
match: new FormControl(''),
|
||||
is_insensitive: new FormControl(true),
|
||||
|
@@ -71,6 +71,10 @@ export const DOCUMENT_SOURCE_OPTIONS = [
|
||||
id: DocumentSource.MailFetch,
|
||||
name: $localize`Mail Fetch`,
|
||||
},
|
||||
{
|
||||
id: DocumentSource.WebUI,
|
||||
name: $localize`Web UI`,
|
||||
},
|
||||
]
|
||||
|
||||
export const SCHEDULE_DATE_FIELD_OPTIONS = [
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<div class="d-flex flex-row mt-2 align-items-center">
|
||||
<span class="me-2">{{title}}:</span>
|
||||
<div class="d-flex flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
||||
<div class="d-flex flex-wrap flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
||||
cdkDropList #selectedList="cdkDropList"
|
||||
cdkDropListOrientation="horizontal"
|
||||
cdkDropListOrientation="mixed"
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
[cdkDropListConnectedTo]="[unselectedList]">
|
||||
@for (item of selectedItems; track item.id) {
|
||||
|
@@ -7,14 +7,13 @@
|
||||
<div class="input-group flex-nowrap">
|
||||
<ng-select #tagSelect name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value"
|
||||
[disabled]="disabled"
|
||||
[multiple]="multiple"
|
||||
[multiple]="true"
|
||||
[closeOnSelect]="false"
|
||||
[clearSearchOnAdd]="true"
|
||||
[hideSelected]="tags.length > 0"
|
||||
[addTag]="allowCreate ? createTagRef : false"
|
||||
addTagText="Add tag"
|
||||
i18n-addTagText
|
||||
(add)="onAdd($event)"
|
||||
(change)="onChange(value)">
|
||||
|
||||
<ng-template ng-label-tmp let-item="item">
|
||||
|
@@ -99,9 +99,6 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
@Input()
|
||||
horizontal: boolean = false
|
||||
|
||||
@Input()
|
||||
multiple: boolean = true
|
||||
|
||||
@Output()
|
||||
filterDocuments = new EventEmitter<Tag[]>()
|
||||
|
||||
@@ -129,40 +126,13 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
|
||||
let index = this.value.indexOf(id)
|
||||
if (index > -1) {
|
||||
const tag = this.getTag(id)
|
||||
|
||||
// remove tag
|
||||
let oldValue = this.value
|
||||
oldValue.splice(index, 1)
|
||||
|
||||
// remove children
|
||||
oldValue = this.removeChildren(oldValue, tag)
|
||||
|
||||
this.value = [...oldValue]
|
||||
this.onChange(this.value)
|
||||
}
|
||||
}
|
||||
|
||||
private removeChildren(tagIDs: number[], tag: Tag) {
|
||||
if (tag.children.length > 0) {
|
||||
const childIDs = tag.children.map((child) => child.id)
|
||||
tagIDs = tagIDs.filter((id) => !childIDs.includes(id))
|
||||
}
|
||||
for (const child of tag.children) {
|
||||
tagIDs = this.removeChildren(tagIDs, child)
|
||||
}
|
||||
return tagIDs
|
||||
}
|
||||
|
||||
public onAdd(tag: Tag) {
|
||||
if (tag.parent) {
|
||||
// add all parents recursively
|
||||
const parent = this.getTag(tag.parent)
|
||||
this.value = [...this.value, parent.id]
|
||||
this.onAdd(parent)
|
||||
}
|
||||
}
|
||||
|
||||
createTag(name: string = null) {
|
||||
var modal = this.modalService.open(TagEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
|
@@ -1,7 +1,6 @@
|
||||
.preview-popup-container > * {
|
||||
width: 30rem !important;
|
||||
height: 22rem !important;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
::ng-deep .popover.popover-preview {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="modal-basic-title" i18n>System Status</h5>
|
||||
<h6 class="modal-title" id="modal-basic-title" i18n>System Status</h6>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -11,11 +11,11 @@
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="row row-cols-1 row-cols-md-3 g-3">
|
||||
<div class="col">
|
||||
<div class="row row-cols-1 row-cols-md-4 g-3">
|
||||
<div class="col-4">
|
||||
<div class="card bg-light h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" i18n>Environment</h5>
|
||||
<h6 class="card-title mb-0" i18n>Environment</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="card-text">
|
||||
@@ -38,37 +38,96 @@
|
||||
<div class="col">
|
||||
<div class="card bg-light h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" i18n>Database</h5>
|
||||
<h6 class="card-title mb-0" i18n>Database</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="card-text">
|
||||
<dt i18n>Type</dt>
|
||||
<dd>{{status.database.type}}</dd>
|
||||
<dt i18n>Status</dt>
|
||||
<dd class="d-flex align-items-center">
|
||||
{{status.database.status}}
|
||||
@if (status.database.status === 'OK') {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" ngbPopover="{{status.database.url}}" triggers="mouseenter:mouseleave"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.database.url}}: {{status.database.error}}" triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
<dd>
|
||||
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="databaseStatus" triggers="mouseenter:mouseleave">
|
||||
{{status.database.status}}
|
||||
@if (status.database.status === 'OK') {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<ng-template #databaseStatus>
|
||||
@if (status.database.status === 'OK') {
|
||||
{{status.database.url}}
|
||||
} @else {
|
||||
{{status.database.url}}: {{status.database.error}}
|
||||
}
|
||||
</ng-template>
|
||||
</dd>
|
||||
<dt i18n>Migration Status</dt>
|
||||
<dd class="d-flex align-items-center">
|
||||
@if (status.database.migration_status.unapplied_migrations.length === 0) {
|
||||
<ng-container i18n>Up to date</ng-container><i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
|
||||
} @else {
|
||||
<ng-container>{{status.database.migration_status.unapplied_migrations.length}} Pending</ng-container><i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
<ng-template #migrationStatus>
|
||||
<h6><ng-container i18n>Latest Migration</ng-container>:</h6> <span class="font-monospace small">{{status.database.migration_status.latest_migration}}</span>
|
||||
@if (status.database.migration_status.unapplied_migrations.length > 0) {
|
||||
<h6 class="mt-3"><ng-container i18n>Pending Migrations</ng-container>:</h6>
|
||||
<ul>
|
||||
@for (migration of status.database.migration_status.unapplied_migrations; track migration) {
|
||||
<li class="font-monospace small">{{migration}}</li>
|
||||
}
|
||||
</ul>
|
||||
<dd>
|
||||
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="migrationStatus" triggers="mouseenter:mouseleave">
|
||||
@if (status.database.migration_status.unapplied_migrations.length === 0) {
|
||||
<ng-container i18n>Up to date</ng-container><i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<ng-container>{{status.database.migration_status.unapplied_migrations.length}} Pending</ng-container><i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||
}
|
||||
<ng-template #migrationStatus>
|
||||
<h6><ng-container i18n>Latest Migration</ng-container>:</h6> <span class="font-monospace small">{{status.database.migration_status.latest_migration}}</span>
|
||||
@if (status.database.migration_status.unapplied_migrations.length > 0) {
|
||||
<h6 class="mt-3"><ng-container i18n>Pending Migrations</ng-container>:</h6>
|
||||
<ul>
|
||||
@for (migration of status.database.migration_status.unapplied_migrations; track migration) {
|
||||
<li class="font-monospace small">{{migration}}</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</ng-template>
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card bg-light h-100">
|
||||
<div class="card-header">
|
||||
<h6 class="card-title mb-0" i18n>Tasks Queue</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="card-text">
|
||||
<dt i18n>Redis Status</dt>
|
||||
<dd>
|
||||
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="redisStatus" triggers="mouseenter:mouseleave">
|
||||
{{status.tasks.redis_status}}
|
||||
@if (status.tasks.redis_status === 'OK') {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<ng-template #redisStatus>
|
||||
@if (status.tasks.redis_status === 'OK') {
|
||||
{{status.tasks.redis_url}}
|
||||
} @else {
|
||||
{{status.tasks.redis_url}}: {{status.tasks.redis_error}}
|
||||
}
|
||||
</ng-template>
|
||||
</dd>
|
||||
<dt i18n>Celery Status</dt>
|
||||
<dd>
|
||||
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="celeryStatus" triggers="mouseenter:mouseleave">
|
||||
{{status.tasks.celery_status}}
|
||||
@if (status.tasks.celery_status === 'OK') {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||
}
|
||||
</div>
|
||||
<ng-template #celeryStatus>
|
||||
@if (status.tasks.celery_status === 'OK') {
|
||||
{{status.tasks.celery_url}}
|
||||
} @else {
|
||||
{{status.tasks.celery_error}}
|
||||
}
|
||||
</ng-template>
|
||||
</dd>
|
||||
@@ -80,63 +139,79 @@
|
||||
<div class="col">
|
||||
<div class="card bg-light h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0" i18n>Tasks</h5>
|
||||
<h6 class="card-title mb-0" i18n>Health</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="card-text">
|
||||
<dt i18n>Redis Status</dt>
|
||||
<dd class="d-flex align-items-center">
|
||||
{{status.tasks.redis_status}}
|
||||
@if (status.tasks.redis_status === 'OK') {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" ngbPopover="{{status.tasks.redis_url}}" triggers="mouseenter:mouseleave"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.redis_url}}: {{status.tasks.redis_error}}" triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
</dd>
|
||||
<dt i18n>Celery Status</dt>
|
||||
<dd class="d-flex align-items-center">
|
||||
{{status.tasks.celery_status}}
|
||||
@if (status.tasks.celery_status === 'OK') {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||
}
|
||||
</dd>
|
||||
<dt i18n>Search Index</dt>
|
||||
<dd class="d-flex align-items-center">
|
||||
{{status.tasks.index_status}}
|
||||
@if (status.tasks.index_status === 'OK') {
|
||||
@if (isStale(status.tasks.index_last_modified)) {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
|
||||
<dd>
|
||||
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave">
|
||||
{{status.tasks.index_status}}
|
||||
@if (status.tasks.index_status === 'OK') {
|
||||
@if (isStale(status.tasks.index_last_modified)) {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
}
|
||||
} @else {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></i-bs>
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1"></i-bs>
|
||||
}
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-danger ms-2 lh-1" ngbPopover="{{status.tasks.index_error}}" triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
</div>
|
||||
</dd>
|
||||
<ng-template #indexStatus>
|
||||
<h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_last_modified | customDate:'medium'}}</span>
|
||||
@if (status.tasks.index_status === 'OK') {
|
||||
<h6><ng-container i18n>Last Updated</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_last_modified | customDate:'medium'}}</span>
|
||||
} @else {
|
||||
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.index_error}}</span>
|
||||
}
|
||||
</ng-template>
|
||||
<dt i18n>Classifier</dt>
|
||||
<dd class="d-flex align-items-center">
|
||||
{{status.tasks.classifier_status}}
|
||||
@if (status.tasks.classifier_status === 'OK') {
|
||||
@if (isStale(status.tasks.classifier_last_trained)) {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
|
||||
<dd>
|
||||
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave">
|
||||
{{status.tasks.classifier_status}}
|
||||
@if (status.tasks.classifier_status === 'OK') {
|
||||
@if (isStale(status.tasks.classifier_last_trained)) {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
}
|
||||
} @else {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="classifierStatus" triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="ms-2 lh-1"
|
||||
[class.text-danger]="status.tasks.classifier_status === SystemStatusItemStatus.ERROR"
|
||||
[class.text-warning]="status.tasks.classifier_status === SystemStatusItemStatus.WARNING"
|
||||
ngbPopover="{{status.tasks.classifier_error}}"
|
||||
triggers="mouseenter:mouseleave"></i-bs>
|
||||
}
|
||||
[class.text-warning]="status.tasks.classifier_status === SystemStatusItemStatus.WARNING"></i-bs>
|
||||
}
|
||||
</div>
|
||||
</dd>
|
||||
<ng-template #classifierStatus>
|
||||
<h6><ng-container i18n>Last Trained</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_last_trained | customDate:'medium'}}</span>
|
||||
@if (status.tasks.classifier_status === 'OK') {
|
||||
<h6><ng-container i18n>Last Trained</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_last_trained | customDate:'medium'}}</span>
|
||||
} @else {
|
||||
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.classifier_error}}</span>
|
||||
}
|
||||
</ng-template>
|
||||
<dt i18n>Sanity Checker</dt>
|
||||
<dd>
|
||||
<div class="badge text-uppercase bg-info text-dark" [ngbPopover]="sanityCheckerStatus" triggers="mouseenter:mouseleave">
|
||||
{{status.tasks.sanity_check_status}}
|
||||
@if (status.tasks.sanity_check_status === 'OK') {
|
||||
@if (isStale(status.tasks.sanity_check_last_run)) {
|
||||
<i-bs name="exclamation-triangle-fill" class="text-warning ms-2 lh-1"></i-bs>
|
||||
} @else {
|
||||
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1"></i-bs>
|
||||
}
|
||||
} @else {
|
||||
<i-bs name="exclamation-triangle-fill" class="ms-2 lh-1"
|
||||
[class.text-danger]="status.tasks.sanity_check_status === SystemStatusItemStatus.ERROR"
|
||||
[class.text-warning]="status.tasks.sanity_check_status === SystemStatusItemStatus.WARNING"></i-bs>
|
||||
}
|
||||
</div>
|
||||
</dd>
|
||||
<ng-template #sanityCheckerStatus>
|
||||
@if (status.tasks.sanity_check_status === 'OK') {
|
||||
<h6><ng-container i18n>Last Run</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_last_run | customDate:'medium'}}</span>
|
||||
} @else {
|
||||
<h6><ng-container i18n>Error</ng-container>:</h6> <span class="font-monospace small">{{status.tasks.sanity_check_error}}</span>
|
||||
}
|
||||
</ng-template>
|
||||
</dl>
|
||||
</div>
|
||||
|
@@ -0,0 +1,3 @@
|
||||
.border-primary {
|
||||
--bs-border-color: var(--bs-primary);
|
||||
}
|
||||
|
@@ -36,12 +36,17 @@ const status: SystemStatus = {
|
||||
redis_status: SystemStatusItemStatus.ERROR,
|
||||
redis_error: 'Error 61 connecting to localhost:6379. Connection refused.',
|
||||
celery_status: SystemStatusItemStatus.ERROR,
|
||||
celery_url: 'celery@localhost',
|
||||
celery_error: 'Error connecting to celery@localhost',
|
||||
index_status: SystemStatusItemStatus.OK,
|
||||
index_last_modified: new Date().toISOString(),
|
||||
index_error: null,
|
||||
classifier_status: SystemStatusItemStatus.OK,
|
||||
classifier_last_trained: new Date().toISOString(),
|
||||
classifier_error: null,
|
||||
sanity_check_status: SystemStatusItemStatus.OK,
|
||||
sanity_check_last_run: new Date().toISOString(),
|
||||
sanity_check_error: null,
|
||||
},
|
||||
}
|
||||
|
||||
|
@@ -145,7 +145,10 @@ export class SavedViewWidgetComponent
|
||||
})
|
||||
}
|
||||
|
||||
if (this.savedView.display_fields) {
|
||||
if (
|
||||
this.savedView.display_fields &&
|
||||
this.savedView.display_fields.length > 0
|
||||
) {
|
||||
this.displayFields = this.savedView.display_fields
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ import { IfPermissionsDirective } from 'src/app/directives/if-permissions.direct
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
@@ -48,7 +49,8 @@ export class CustomFieldsComponent
|
||||
private modalService: NgbModal,
|
||||
private toastService: ToastService,
|
||||
private documentListViewService: DocumentListViewService,
|
||||
private settingsService: SettingsService
|
||||
private settingsService: SettingsService,
|
||||
private documentService: DocumentService
|
||||
) {
|
||||
super()
|
||||
}
|
||||
@@ -85,6 +87,7 @@ export class CustomFieldsComponent
|
||||
this.toastService.showInfo($localize`Saved field "${newField.name}".`)
|
||||
this.customFieldsService.clearCache()
|
||||
this.settingsService.initializeDisplayFields()
|
||||
this.documentService.reload()
|
||||
this.reload()
|
||||
})
|
||||
modal.componentInstance.failed
|
||||
@@ -111,6 +114,7 @@ export class CustomFieldsComponent
|
||||
this.toastService.showInfo($localize`Deleted field "${field.name}"`)
|
||||
this.customFieldsService.clearCache()
|
||||
this.settingsService.initializeDisplayFields()
|
||||
this.documentService.reload()
|
||||
this.reload()
|
||||
},
|
||||
error: (e) => {
|
||||
|
@@ -53,7 +53,59 @@
|
||||
</tr>
|
||||
}
|
||||
@for (object of data; track object) {
|
||||
<ng-container [ngTemplateOutlet]="objectRow" [ngTemplateOutletContext]="{ object: object, depth: 0 }"></ng-container>
|
||||
<tr (click)="toggleSelected(object); $event.stopPropagation();" class="data-row fade" [class.show]="show">
|
||||
<td>
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td scope="row"><button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button> </td>
|
||||
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
||||
<td scope="row">{{ object.document_count }}</td>
|
||||
@for (column of extraColumns; track column) {
|
||||
<td scope="row" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }">
|
||||
@if (column.rendersHtml) {
|
||||
<div [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div>
|
||||
} @else {
|
||||
{{ column.valueFn.call(null, object) }}
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td scope="row">
|
||||
<div class="btn-toolbar gap-2">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
|
||||
<button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
|
||||
@if (object.document_count > 0) {
|
||||
<button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ object.document_count }})</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
@if (object.document_count > 0) {
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ object.document_count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -74,70 +126,3 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-template #objectRow let-object="object" let-depth="depth">
|
||||
<tr (click)="toggleSelected(object); $event.stopPropagation();" class="data-row fade" [class.show]="show">
|
||||
<td>
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
<td scope="row" class="name-cell" style="--depth: {{depth}}">
|
||||
@if (depth > 0) {
|
||||
<div class="indicator"></div>
|
||||
}
|
||||
<button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null; $event.stopPropagation()">{{ object.name }}</button>
|
||||
</td>
|
||||
<td scope="row" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
||||
<td scope="row">{{ object.document_count }}</td>
|
||||
@for (column of extraColumns; track column) {
|
||||
<td scope="row" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }">
|
||||
@if (column.rendersHtml) {
|
||||
<div [innerHtml]="column.valueFn.call(null, object) | safeHtml"></div>
|
||||
} @else {
|
||||
{{ column.valueFn.call(null, object) }}
|
||||
}
|
||||
</td>
|
||||
}
|
||||
<td scope="row">
|
||||
<div class="btn-toolbar gap-2">
|
||||
<div class="btn-group d-block d-sm-none">
|
||||
<div ngbDropdown container="body" class="d-inline-block">
|
||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||
<i-bs name="three-dots-vertical"></i-bs>
|
||||
</button>
|
||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||
<button (click)="openEditDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" ngbDropdownItem i18n>Edit</button>
|
||||
<button class="text-danger" (click)="openDeleteDialog(object)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" ngbDropdownItem i18n>Delete</button>
|
||||
@if (object.document_count > 0) {
|
||||
<button (click)="filterDocuments(object)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ object.document_count }})</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Change, type: permissionType }" [disabled]="!userCanEdit(object)">
|
||||
<i-bs width="1em" height="1em" name="pencil"></i-bs> <ng-container i18n>Edit</ng-container>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.Delete, type: permissionType }" [disabled]="!userCanDelete(object)">
|
||||
<i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||
</button>
|
||||
</div>
|
||||
@if (object.document_count > 0) {
|
||||
<div class="btn-group d-none d-sm-inline-block">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="filterDocuments(object); $event.stopPropagation();" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }">
|
||||
<i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ object.document_count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@if (object.children && object.children.length > 0) {
|
||||
@for (child of object.children; track child) {
|
||||
<ng-container [ngTemplateOutlet]="objectRow" [ngTemplateOutletContext]="{ object: child, depth: depth + 1 }"></ng-container>
|
||||
}
|
||||
}
|
||||
</ng-template>
|
||||
|
@@ -10,17 +10,3 @@ tbody tr:last-child td {
|
||||
.form-check {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
td.name-cell {
|
||||
padding-left: calc(calc(var(--depth) - 1) * 1.1rem);
|
||||
|
||||
.indicator {
|
||||
display: inline-block;
|
||||
width: .8rem;
|
||||
height: .8rem;
|
||||
border-left: 1px solid var(--bs-secondary);
|
||||
border-bottom: 1px solid var(--bs-secondary);
|
||||
margin-right: .25rem;
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
@@ -131,10 +131,6 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
this.reloadData()
|
||||
}
|
||||
|
||||
protected filterData(data: T[]): T[] {
|
||||
return data
|
||||
}
|
||||
|
||||
reloadData(extraParams: { [key: string]: any } = null) {
|
||||
this.loading = true
|
||||
this.clearSelection()
|
||||
@@ -151,7 +147,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((c) => {
|
||||
this.data = this.filterData(c.results)
|
||||
this.data = c.results
|
||||
this.collectionSize = c.count
|
||||
}),
|
||||
delay(100)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { NgClass, NgTemplateOutlet, TitleCasePipe } from '@angular/common'
|
||||
import { NgClass, TitleCasePipe } from '@angular/common'
|
||||
import { Component } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
@@ -36,7 +36,6 @@ import { ManagementListComponent } from '../management-list/management-list.comp
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgClass,
|
||||
NgTemplateOutlet,
|
||||
NgbDropdownModule,
|
||||
NgbPaginationModule,
|
||||
NgxBootstrapIconsModule,
|
||||
@@ -77,8 +76,4 @@ export class TagListComponent extends ManagementListComponent<Tag> {
|
||||
getDeleteMessage(object: Tag) {
|
||||
return $localize`Do you really want to delete the tag "${object.name}"?`
|
||||
}
|
||||
|
||||
filterData(data: Tag[]) {
|
||||
return data.filter((tag) => !tag.parent)
|
||||
}
|
||||
}
|
||||
|
@@ -1,8 +1,15 @@
|
||||
import { ObjectWithId } from './object-with-id'
|
||||
|
||||
export enum PaperlessTaskType {
|
||||
// just file tasks, for now
|
||||
File = 'file',
|
||||
Auto = 'auto_task',
|
||||
ScheduledTask = 'scheduled_task',
|
||||
ManualTask = 'manual_task',
|
||||
}
|
||||
|
||||
export enum PaperlessTaskName {
|
||||
ConsumeFile = 'consume_file',
|
||||
TrainClassifier = 'train_classifier',
|
||||
SanityCheck = 'check_sanity',
|
||||
}
|
||||
|
||||
export enum PaperlessTaskStatus {
|
||||
@@ -23,6 +30,8 @@ export interface PaperlessTask extends ObjectWithId {
|
||||
|
||||
task_file_name: string
|
||||
|
||||
task_name: PaperlessTaskName
|
||||
|
||||
date_created: Date
|
||||
|
||||
date_done?: Date
|
||||
|
@@ -32,11 +32,16 @@ export interface SystemStatus {
|
||||
redis_status: SystemStatusItemStatus
|
||||
redis_error: string
|
||||
celery_status: SystemStatusItemStatus
|
||||
celery_url: string
|
||||
celery_error: string
|
||||
index_status: SystemStatusItemStatus
|
||||
index_last_modified: string // ISO date string
|
||||
index_error: string
|
||||
classifier_status: SystemStatusItemStatus
|
||||
classifier_last_trained: string // ISO date string
|
||||
classifier_error: string
|
||||
sanity_check_status: SystemStatusItemStatus
|
||||
sanity_check_last_run: string // ISO date string
|
||||
sanity_check_error: string
|
||||
}
|
||||
}
|
||||
|
@@ -6,8 +6,4 @@ export interface Tag extends MatchingModel {
|
||||
text_color?: string
|
||||
|
||||
is_inbox_tag?: boolean
|
||||
|
||||
parent?: number // Tag ID
|
||||
|
||||
children?: Tag[] // read-only
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ export enum DocumentSource {
|
||||
ConsumeFolder = 1,
|
||||
ApiUpload = 2,
|
||||
MailFetch = 3,
|
||||
WebUI = 4,
|
||||
}
|
||||
|
||||
export enum WorkflowTriggerType {
|
||||
|
@@ -195,11 +195,7 @@ describe('DocumentListViewService', () => {
|
||||
{ custom_field_999: ['Custom field not found'] },
|
||||
{ status: 400, statusText: 'Unexpected error' }
|
||||
)
|
||||
expect(documentListViewService.error).toEqual(
|
||||
'custom_field_999: Custom field not found'
|
||||
)
|
||||
// reset the list
|
||||
documentListViewService.sortField = 'created'
|
||||
// resets itself
|
||||
req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
)
|
||||
|
@@ -306,6 +306,14 @@ export class DocumentListViewService {
|
||||
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
|
||||
activeListViewState.currentPage = 1
|
||||
this.reload()
|
||||
} else if (
|
||||
activeListViewState.sortField.indexOf('custom_field') === 0 &&
|
||||
this.settings.allDisplayFields.find(
|
||||
(f) => f.id === activeListViewState.sortField
|
||||
) === undefined
|
||||
) {
|
||||
// e.g. field was deleted
|
||||
this.sortField = 'created'
|
||||
} else {
|
||||
this.selectionData = null
|
||||
let errorMessage
|
||||
|
@@ -62,6 +62,10 @@ export class DocumentService extends AbstractPaperlessService<Document> {
|
||||
private customFieldService: CustomFieldsService
|
||||
) {
|
||||
super(http, 'documents')
|
||||
this.reload()
|
||||
}
|
||||
|
||||
public reload() {
|
||||
if (
|
||||
this.permissionsService.currentUserCan(
|
||||
PermissionAction.View,
|
||||
|
@@ -114,6 +114,48 @@ describe(`Additional service tests for SavedViewService`, () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('should treat empty display_fields as null', () => {
|
||||
subscription = service
|
||||
.patch({
|
||||
id: 1,
|
||||
name: 'Saved View',
|
||||
show_on_dashboard: true,
|
||||
show_in_sidebar: true,
|
||||
sort_field: 'name',
|
||||
sort_reverse: true,
|
||||
filter_rules: [],
|
||||
display_fields: [],
|
||||
})
|
||||
.subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/1/`
|
||||
)
|
||||
expect(req.request.body.display_fields).toBeNull()
|
||||
})
|
||||
|
||||
it('should support patch without reload', () => {
|
||||
subscription = service
|
||||
.patch(
|
||||
{
|
||||
id: 1,
|
||||
name: 'Saved View',
|
||||
show_on_dashboard: true,
|
||||
show_in_sidebar: true,
|
||||
sort_field: 'name',
|
||||
sort_reverse: true,
|
||||
filter_rules: [],
|
||||
},
|
||||
false
|
||||
)
|
||||
.subscribe()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}${endpoint}/1/`
|
||||
)
|
||||
expect(req.request.method).toEqual('PATCH')
|
||||
req.flush({})
|
||||
httpTestingController.verify() // no reload
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Dont need to setup again
|
||||
|
||||
|
@@ -87,12 +87,21 @@ export class SavedViewService extends AbstractPaperlessService<SavedView> {
|
||||
return super.create(o).pipe(tap(() => this.reload()))
|
||||
}
|
||||
|
||||
update(o: SavedView) {
|
||||
return super.update(o).pipe(tap(() => this.reload()))
|
||||
patch(o: SavedView, reload: boolean = false): Observable<SavedView> {
|
||||
if (o.display_fields?.length === 0) {
|
||||
o.display_fields = null
|
||||
}
|
||||
return super.patch(o).pipe(
|
||||
tap(() => {
|
||||
if (reload) {
|
||||
this.reload()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
patchMany(objects: SavedView[]): Observable<SavedView[]> {
|
||||
return combineLatest(objects.map((o) => super.patch(o))).pipe(
|
||||
return combineLatest(objects.map((o) => this.patch(o, false))).pipe(
|
||||
tap(() => this.reload())
|
||||
)
|
||||
}
|
||||
|
@@ -244,6 +244,12 @@ const LANGUAGE_OPTIONS = [
|
||||
englishName: 'Chinese Simplified',
|
||||
dateInputFormat: 'yyyy-mm-dd',
|
||||
},
|
||||
{
|
||||
code: 'zh-tw',
|
||||
name: $localize`Chinese Traditional`,
|
||||
englishName: 'Chinese Traditional',
|
||||
dateInputFormat: 'yyyy/mm/dd',
|
||||
},
|
||||
]
|
||||
|
||||
const ISO_LANGUAGE_OPTION: LanguageOption = {
|
||||
|
@@ -5,7 +5,11 @@ import {
|
||||
} from '@angular/common/http/testing'
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { PaperlessTaskStatus, PaperlessTaskType } from '../data/paperless-task'
|
||||
import {
|
||||
PaperlessTaskName,
|
||||
PaperlessTaskStatus,
|
||||
PaperlessTaskType,
|
||||
} from '../data/paperless-task'
|
||||
import { TasksService } from './tasks.service'
|
||||
|
||||
describe('TasksService', () => {
|
||||
@@ -33,7 +37,7 @@ describe('TasksService', () => {
|
||||
it('calls tasks api endpoint on reload', () => {
|
||||
tasksService.reload()
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}tasks/`
|
||||
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
@@ -41,7 +45,9 @@ describe('TasksService', () => {
|
||||
it('does not call tasks api endpoint on reload if already loading', () => {
|
||||
tasksService.loading = true
|
||||
tasksService.reload()
|
||||
httpTestingController.expectNone(`${environment.apiBaseUrl}tasks/`)
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||
)
|
||||
})
|
||||
|
||||
it('calls acknowledge_tasks api endpoint on dismiss and reloads', () => {
|
||||
@@ -55,14 +61,19 @@ describe('TasksService', () => {
|
||||
})
|
||||
req.flush([])
|
||||
// reload is then called
|
||||
httpTestingController.expectOne(`${environment.apiBaseUrl}tasks/`).flush([])
|
||||
httpTestingController
|
||||
.expectOne(
|
||||
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||
)
|
||||
.flush([])
|
||||
})
|
||||
|
||||
it('sorts tasks returned from api', () => {
|
||||
expect(tasksService.total).toEqual(0)
|
||||
const mockTasks = [
|
||||
{
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Complete,
|
||||
acknowledged: false,
|
||||
task_id: '1234',
|
||||
@@ -70,7 +81,8 @@ describe('TasksService', () => {
|
||||
date_created: new Date(),
|
||||
},
|
||||
{
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Failed,
|
||||
acknowledged: false,
|
||||
task_id: '1235',
|
||||
@@ -78,7 +90,8 @@ describe('TasksService', () => {
|
||||
date_created: new Date(),
|
||||
},
|
||||
{
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Pending,
|
||||
acknowledged: false,
|
||||
task_id: '1236',
|
||||
@@ -86,7 +99,8 @@ describe('TasksService', () => {
|
||||
date_created: new Date(),
|
||||
},
|
||||
{
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Started,
|
||||
acknowledged: false,
|
||||
task_id: '1237',
|
||||
@@ -94,7 +108,8 @@ describe('TasksService', () => {
|
||||
date_created: new Date(),
|
||||
},
|
||||
{
|
||||
type: PaperlessTaskType.File,
|
||||
type: PaperlessTaskType.Auto,
|
||||
task_name: PaperlessTaskName.ConsumeFile,
|
||||
status: PaperlessTaskStatus.Complete,
|
||||
acknowledged: false,
|
||||
task_id: '1238',
|
||||
@@ -106,7 +121,7 @@ describe('TasksService', () => {
|
||||
tasksService.reload()
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}tasks/`
|
||||
`${environment.apiBaseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||
)
|
||||
|
||||
req.flush(mockTasks)
|
||||
|
@@ -4,8 +4,8 @@ import { Subject } from 'rxjs'
|
||||
import { first, takeUntil } from 'rxjs/operators'
|
||||
import {
|
||||
PaperlessTask,
|
||||
PaperlessTaskName,
|
||||
PaperlessTaskStatus,
|
||||
PaperlessTaskType,
|
||||
} from 'src/app/data/paperless-task'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
@@ -54,10 +54,14 @@ export class TasksService {
|
||||
this.loading = true
|
||||
|
||||
this.http
|
||||
.get<PaperlessTask[]>(`${this.baseUrl}tasks/`)
|
||||
.get<PaperlessTask[]>(
|
||||
`${this.baseUrl}tasks/?task_name=consume_file&acknowledged=false`
|
||||
)
|
||||
.pipe(takeUntil(this.unsubscribeNotifer), first())
|
||||
.subscribe((r) => {
|
||||
this.fileTasks = r.filter((t) => t.type == PaperlessTaskType.File) // they're all File tasks, for now
|
||||
this.fileTasks = r.filter(
|
||||
(t) => t.task_name == PaperlessTaskName.ConsumeFile
|
||||
)
|
||||
this.loading = false
|
||||
})
|
||||
}
|
||||
|
@@ -37,6 +37,7 @@ export class UploadDocumentsService {
|
||||
private uploadFile(file: File) {
|
||||
let formData = new FormData()
|
||||
formData.append('document', file, file.name)
|
||||
formData.append('from_webui', 'true')
|
||||
let status = this.websocketStatusService.newFileUpload(file.name)
|
||||
|
||||
status.message = $localize`Connecting...`
|
||||
|
@@ -181,6 +181,7 @@ import localeSv from '@angular/common/locales/sv'
|
||||
import localeTr from '@angular/common/locales/tr'
|
||||
import localeUk from '@angular/common/locales/uk'
|
||||
import localeZh from '@angular/common/locales/zh'
|
||||
import localeZhHant from '@angular/common/locales/zh-Hant'
|
||||
import { CorrespondentNamePipe } from './app/pipes/correspondent-name.pipe'
|
||||
import { DocumentTypeNamePipe } from './app/pipes/document-type-name.pipe'
|
||||
import { StoragePathNamePipe } from './app/pipes/storage-path-name.pipe'
|
||||
@@ -217,6 +218,7 @@ registerLocaleData(localeSv)
|
||||
registerLocaleData(localeTr)
|
||||
registerLocaleData(localeUk)
|
||||
registerLocaleData(localeZh)
|
||||
registerLocaleData(localeZhHant)
|
||||
|
||||
function initializeApp(settings: SettingsService) {
|
||||
return () => {
|
||||
|
@@ -21,10 +21,12 @@
|
||||
--pngx-success-darken-10: hsl(152, 69%, 11%); // based on success #198754
|
||||
--pngx-bg-alt: #fff;
|
||||
--pngx-bg-darker: var(--bs-gray-100);
|
||||
--pngx-bg-alt2: var(--bs-gray-200);
|
||||
--pngx-bg-alt2: var(--bs-gray-200); // #e9ecef
|
||||
--pngx-bg-disabled: #f7f7f7;
|
||||
--pngx-focus-alpha: 0.3;
|
||||
--pngx-toast-max-width: 360px;
|
||||
--bs-info: var(--pngx-bg-alt2);
|
||||
--bs-info-rgb: 233, 236, 239;
|
||||
@media screen and (min-width: 1024px) {
|
||||
--pngx-toast-max-width: 450px;
|
||||
}
|
||||
@@ -71,8 +73,15 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
||||
}
|
||||
|
||||
@mixin dark-mode {
|
||||
--bs-body-color: #{$text-color-dark-bg};
|
||||
--pngx-body-color-accent: #{$text-color-dark-bg-accent};
|
||||
--pngx-bg-alt: #242529;
|
||||
--pngx-bg-alt2: #232323;
|
||||
--pngx-bg-darker: #101216;
|
||||
--pngx-bg-disabled: var(--pngx-bg-alt);
|
||||
--pngx-focus-alpha: 0.6;
|
||||
--pngx-primary-faded: var(--pngx-primary-darken-15);
|
||||
--pngx-primary-text-contrast: var(--bs-body-color);
|
||||
--bs-body-color: #{$text-color-dark-bg};
|
||||
--bs-secondary-color: #6c757d;
|
||||
--bs-danger: #b71631;
|
||||
--bs-danger-rgb: 183, 22, 49;
|
||||
@@ -80,15 +89,10 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
||||
--bs-body-bg-rgb: 22, 22, 24;
|
||||
--bs-light: #1c1c1f;
|
||||
--bs-light-rgb: 28, 28, 31;
|
||||
--bs-info: var(--pngx-bg-alt);
|
||||
--bs-info-rgb: 36, 36, 39;
|
||||
--bs-border-color: #47494f;
|
||||
--pngx-bg-alt2: #232323;
|
||||
--pngx-bg-darker: #101216;
|
||||
--bs-tertiary-bg: var(--pngx-bg-darker);
|
||||
--pngx-bg-alt: #242529;
|
||||
--pngx-bg-disabled: var(--pngx-bg-alt);
|
||||
--pngx-focus-alpha: 0.6;
|
||||
--pngx-primary-faded: var(--pngx-primary-darken-15);
|
||||
--pngx-primary-text-contrast: var(--bs-body-color);
|
||||
--bs-dark-border-subtle: var(--pngx-bg-darker);
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, .175); // override bs
|
||||
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
import pickle
|
||||
import re
|
||||
import time
|
||||
import warnings
|
||||
from collections.abc import Iterator
|
||||
from hashlib import sha256
|
||||
@@ -142,19 +141,6 @@ class DocumentClassifier:
|
||||
):
|
||||
raise IncompatibleClassifierVersionError("sklearn version update")
|
||||
|
||||
def set_last_checked(self) -> None:
|
||||
# save a timestamp of the last time we checked for retraining to a file
|
||||
with Path(settings.MODEL_FILE.with_suffix(".last_checked")).open("w") as f:
|
||||
f.write(str(time.time()))
|
||||
|
||||
def get_last_checked(self) -> float | None:
|
||||
# load the timestamp of the last time we checked for retraining
|
||||
try:
|
||||
with Path(settings.MODEL_FILE.with_suffix(".last_checked")).open("r") as f:
|
||||
return float(f.read())
|
||||
except FileNotFoundError: # pragma: no cover
|
||||
return None
|
||||
|
||||
def save(self) -> None:
|
||||
target_file: Path = settings.MODEL_FILE
|
||||
target_file_temp: Path = target_file.with_suffix(".pickle.part")
|
||||
@@ -175,7 +161,6 @@ class DocumentClassifier:
|
||||
pickle.dump(self.storage_path_classifier, f)
|
||||
|
||||
target_file_temp.rename(target_file)
|
||||
self.set_last_checked()
|
||||
|
||||
def train(self) -> bool:
|
||||
# Get non-inbox documents
|
||||
@@ -244,7 +229,6 @@ class DocumentClassifier:
|
||||
and self.last_doc_change_time >= latest_doc_change
|
||||
) and self.last_auto_type_hash == hasher.digest():
|
||||
logger.info("No updates since last training")
|
||||
self.set_last_checked()
|
||||
# Set the classifier information into the cache
|
||||
# Caching for 50 minutes, so slightly less than the normal retrain time
|
||||
cache.set(
|
||||
|
@@ -775,7 +775,7 @@ class ConsumerPlugin(
|
||||
|
||||
if self.metadata.tag_ids:
|
||||
for tag_id in self.metadata.tag_ids:
|
||||
document.add_nested_tags(Tag.objects.get(pk=tag_id))
|
||||
document.tags.add(Tag.objects.get(pk=tag_id))
|
||||
|
||||
if self.metadata.storage_path_id:
|
||||
document.storage_path = StoragePath.objects.get(
|
||||
|
@@ -144,6 +144,7 @@ class DocumentSource(IntEnum):
|
||||
ConsumeFolder = 1
|
||||
ApiUpload = 2
|
||||
MailFetch = 3
|
||||
WebUI = 4
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
|
@@ -35,6 +35,7 @@ from documents.models import CustomFieldInstance
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import Log
|
||||
from documents.models import PaperlessTask
|
||||
from documents.models import ShareLink
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
@@ -770,6 +771,21 @@ class ShareLinkFilterSet(FilterSet):
|
||||
}
|
||||
|
||||
|
||||
class PaperlessTaskFilterSet(FilterSet):
|
||||
acknowledged = BooleanFilter(
|
||||
label="Acknowledged",
|
||||
field_name="acknowledged",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = PaperlessTask
|
||||
fields = {
|
||||
"type": ["exact"],
|
||||
"task_name": ["exact"],
|
||||
"status": ["exact"],
|
||||
}
|
||||
|
||||
|
||||
class ObjectOwnedOrGrantedPermissionsFilter(ObjectPermissionsFilter):
|
||||
"""
|
||||
A filter backend that limits results to those where the requesting user
|
||||
|
@@ -10,4 +10,4 @@ class Command(BaseCommand):
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
train_classifier()
|
||||
train_classifier(scheduled=False)
|
||||
|
@@ -12,6 +12,6 @@ class Command(ProgressBarMixin, BaseCommand):
|
||||
|
||||
def handle(self, *args, **options):
|
||||
self.handle_progress_bar_mixin(**options)
|
||||
messages = check_sanity(progress=self.use_progress_bar)
|
||||
messages = check_sanity(progress=self.use_progress_bar, scheduled=False)
|
||||
|
||||
messages.log_messages()
|
||||
|
@@ -0,0 +1,91 @@
|
||||
# Generated by Django 5.1.6 on 2025-02-21 16:34
|
||||
|
||||
import multiselectfield.db.fields
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
# WebUI source was added, so all existing APIUpload sources should be updated to include WebUI
|
||||
def update_workflow_sources(apps, schema_editor):
|
||||
WorkflowTrigger = apps.get_model("documents", "WorkflowTrigger")
|
||||
for trigger in WorkflowTrigger.objects.all():
|
||||
sources = list(trigger.sources)
|
||||
if 2 in sources:
|
||||
sources.append(4)
|
||||
trigger.sources = sources
|
||||
trigger.save()
|
||||
|
||||
|
||||
def make_existing_tasks_consume_auto(apps, schema_editor):
|
||||
PaperlessTask = apps.get_model("documents", "PaperlessTask")
|
||||
PaperlessTask.objects.all().update(type="auto_task", task_name="consume_file")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1062_alter_savedviewfilterrule_rule_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="paperlesstask",
|
||||
name="type",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("auto_task", "Auto Task"),
|
||||
("scheduled_task", "Scheduled Task"),
|
||||
("manual_task", "Manual Task"),
|
||||
],
|
||||
default="auto_task",
|
||||
help_text="The type of task that was run",
|
||||
max_length=30,
|
||||
verbose_name="Task Type",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="paperlesstask",
|
||||
name="task_name",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("consume_file", "Consume File"),
|
||||
("train_classifier", "Train Classifier"),
|
||||
("check_sanity", "Check Sanity"),
|
||||
],
|
||||
help_text="Name of the task that was run",
|
||||
max_length=255,
|
||||
null=True,
|
||||
verbose_name="Task Name",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=make_existing_tasks_consume_auto,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="workflowactionwebhook",
|
||||
name="url",
|
||||
field=models.CharField(
|
||||
help_text="The destination URL for the notification.",
|
||||
max_length=256,
|
||||
verbose_name="webhook url",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="workflowtrigger",
|
||||
name="sources",
|
||||
field=multiselectfield.db.fields.MultiSelectField(
|
||||
choices=[
|
||||
(1, "Consume Folder"),
|
||||
(2, "Api Upload"),
|
||||
(3, "Mail Fetch"),
|
||||
(4, "Web UI"),
|
||||
],
|
||||
default="1,2,3,4",
|
||||
max_length=7,
|
||||
),
|
||||
),
|
||||
migrations.RunPython(
|
||||
code=update_workflow_sources,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
),
|
||||
]
|
@@ -1,26 +0,0 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-10 06:02
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("documents", "1062_alter_savedviewfilterrule_rule_type"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="tag",
|
||||
name="parent",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="children",
|
||||
to="documents.tag",
|
||||
verbose_name="parent",
|
||||
),
|
||||
),
|
||||
]
|
@@ -12,7 +12,6 @@ from celery import states
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import MaxValueValidator
|
||||
from django.core.validators import MinValueValidator
|
||||
from django.db import models
|
||||
@@ -114,38 +113,10 @@ class Tag(MatchingModel):
|
||||
),
|
||||
)
|
||||
|
||||
parent = models.ForeignKey(
|
||||
"self",
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="children",
|
||||
verbose_name=_("parent"),
|
||||
)
|
||||
|
||||
class Meta(MatchingModel.Meta):
|
||||
verbose_name = _("tag")
|
||||
verbose_name_plural = _("tags")
|
||||
|
||||
def get_all_descendants(self):
|
||||
descendants = []
|
||||
for child in self.children.all():
|
||||
descendants.append(child)
|
||||
descendants.extend(child.get_all_descendants())
|
||||
return descendants
|
||||
|
||||
def get_all_ancestors(self):
|
||||
ancestors = []
|
||||
if self.parent:
|
||||
ancestors.append(self.parent)
|
||||
ancestors.extend(self.parent.get_all_ancestors())
|
||||
return ancestors
|
||||
|
||||
def clean(self):
|
||||
if self.parent == self:
|
||||
raise ValidationError("Cannot set itself as parent.")
|
||||
return super().clean()
|
||||
|
||||
|
||||
class DocumentType(MatchingModel):
|
||||
class Meta(MatchingModel.Meta):
|
||||
@@ -407,12 +378,6 @@ class Document(SoftDeleteModel, ModelWithOwner):
|
||||
def created_date(self):
|
||||
return timezone.localdate(self.created)
|
||||
|
||||
def add_nested_tags(self, tags):
|
||||
for tag in tags:
|
||||
self.tags.add(tag)
|
||||
if tag.parent:
|
||||
self.add_nested_tags([tag.parent])
|
||||
|
||||
|
||||
class Log(models.Model):
|
||||
LEVELS = (
|
||||
@@ -685,6 +650,16 @@ class PaperlessTask(ModelWithOwner):
|
||||
ALL_STATES = sorted(states.ALL_STATES)
|
||||
TASK_STATE_CHOICES = sorted(zip(ALL_STATES, ALL_STATES))
|
||||
|
||||
class TaskType(models.TextChoices):
|
||||
AUTO = ("auto_task", _("Auto Task"))
|
||||
SCHEDULED_TASK = ("scheduled_task", _("Scheduled Task"))
|
||||
MANUAL_TASK = ("manual_task", _("Manual Task"))
|
||||
|
||||
class TaskName(models.TextChoices):
|
||||
CONSUME_FILE = ("consume_file", _("Consume File"))
|
||||
TRAIN_CLASSIFIER = ("train_classifier", _("Train Classifier"))
|
||||
CHECK_SANITY = ("check_sanity", _("Check Sanity"))
|
||||
|
||||
task_id = models.CharField(
|
||||
max_length=255,
|
||||
unique=True,
|
||||
@@ -708,8 +683,9 @@ class PaperlessTask(ModelWithOwner):
|
||||
task_name = models.CharField(
|
||||
null=True,
|
||||
max_length=255,
|
||||
choices=TaskName.choices,
|
||||
verbose_name=_("Task Name"),
|
||||
help_text=_("Name of the Task which was run"),
|
||||
help_text=_("Name of the task that was run"),
|
||||
)
|
||||
|
||||
status = models.CharField(
|
||||
@@ -719,24 +695,28 @@ class PaperlessTask(ModelWithOwner):
|
||||
verbose_name=_("Task State"),
|
||||
help_text=_("Current state of the task being run"),
|
||||
)
|
||||
|
||||
date_created = models.DateTimeField(
|
||||
null=True,
|
||||
default=timezone.now,
|
||||
verbose_name=_("Created DateTime"),
|
||||
help_text=_("Datetime field when the task result was created in UTC"),
|
||||
)
|
||||
|
||||
date_started = models.DateTimeField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("Started DateTime"),
|
||||
help_text=_("Datetime field when the task was started in UTC"),
|
||||
)
|
||||
|
||||
date_done = models.DateTimeField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("Completed DateTime"),
|
||||
help_text=_("Datetime field when the task was completed in UTC"),
|
||||
)
|
||||
|
||||
result = models.TextField(
|
||||
null=True,
|
||||
default=None,
|
||||
@@ -746,6 +726,14 @@ class PaperlessTask(ModelWithOwner):
|
||||
),
|
||||
)
|
||||
|
||||
type = models.CharField(
|
||||
max_length=30,
|
||||
choices=TaskType.choices,
|
||||
default=TaskType.AUTO,
|
||||
verbose_name=_("Task Type"),
|
||||
help_text=_("The type of task that was run"),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Task {self.task_id}"
|
||||
|
||||
@@ -1066,6 +1054,7 @@ class WorkflowTrigger(models.Model):
|
||||
CONSUME_FOLDER = DocumentSource.ConsumeFolder.value, _("Consume Folder")
|
||||
API_UPLOAD = DocumentSource.ApiUpload.value, _("Api Upload")
|
||||
MAIL_FETCH = DocumentSource.MailFetch.value, _("Mail Fetch")
|
||||
WEB_UI = DocumentSource.WebUI.value, _("Web UI")
|
||||
|
||||
class ScheduleDateField(models.TextChoices):
|
||||
ADDED = "added", _("Added")
|
||||
@@ -1080,9 +1069,9 @@ class WorkflowTrigger(models.Model):
|
||||
)
|
||||
|
||||
sources = MultiSelectField(
|
||||
max_length=5,
|
||||
max_length=7,
|
||||
choices=DocumentSourceChoices.choices,
|
||||
default=f"{DocumentSource.ConsumeFolder},{DocumentSource.ApiUpload},{DocumentSource.MailFetch}",
|
||||
default=f"{DocumentSource.ConsumeFolder},{DocumentSource.ApiUpload},{DocumentSource.MailFetch},{DocumentSource.WebUI}",
|
||||
)
|
||||
|
||||
filter_path = models.CharField(
|
||||
@@ -1238,9 +1227,12 @@ class WorkflowActionEmail(models.Model):
|
||||
|
||||
|
||||
class WorkflowActionWebhook(models.Model):
|
||||
url = models.URLField(
|
||||
# We dont use the built-in URLField because it is not flexible enough
|
||||
# validation is handled in the serializer
|
||||
url = models.CharField(
|
||||
_("webhook url"),
|
||||
null=False,
|
||||
max_length=256,
|
||||
help_text=_("The destination URL for the notification."),
|
||||
)
|
||||
|
||||
|
@@ -1,13 +1,17 @@
|
||||
import hashlib
|
||||
import logging
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
from celery import states
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from tqdm import tqdm
|
||||
|
||||
from documents.models import Document
|
||||
from documents.models import PaperlessTask
|
||||
|
||||
|
||||
class SanityCheckMessages:
|
||||
@@ -57,7 +61,17 @@ class SanityCheckFailedException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def check_sanity(*, progress=False) -> SanityCheckMessages:
|
||||
def check_sanity(*, progress=False, scheduled=True) -> SanityCheckMessages:
|
||||
paperless_task = PaperlessTask.objects.create(
|
||||
task_id=uuid.uuid4(),
|
||||
type=PaperlessTask.TaskType.SCHEDULED_TASK
|
||||
if scheduled
|
||||
else PaperlessTask.TaskType.MANUAL_TASK,
|
||||
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||
status=states.STARTED,
|
||||
date_created=timezone.now(),
|
||||
date_started=timezone.now(),
|
||||
)
|
||||
messages = SanityCheckMessages()
|
||||
|
||||
present_files = {
|
||||
@@ -142,4 +156,11 @@ def check_sanity(*, progress=False) -> SanityCheckMessages:
|
||||
for extra_file in present_files:
|
||||
messages.warning(None, f"Orphaned file in media dir: {extra_file}")
|
||||
|
||||
paperless_task.status = states.SUCCESS if not messages.has_error else states.FAILURE
|
||||
# result is concatenated messages
|
||||
paperless_task.result = f"{len(messages)} issues found."
|
||||
if messages.has_error:
|
||||
paperless_task.result += " Check logs for details."
|
||||
paperless_task.date_done = timezone.now()
|
||||
paperless_task.save(update_fields=["status", "result", "date_done"])
|
||||
return messages
|
||||
|
@@ -58,6 +58,7 @@ from documents.permissions import set_permissions_for_object
|
||||
from documents.templating.filepath import validate_filepath_template_and_render
|
||||
from documents.templating.utils import convert_format_str_to_template_format
|
||||
from documents.validators import uri_validator
|
||||
from documents.validators import url_validator
|
||||
|
||||
logger = logging.getLogger("paperless.serializers")
|
||||
|
||||
@@ -528,11 +529,6 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
||||
|
||||
text_color = serializers.SerializerMethodField()
|
||||
|
||||
children = SerializerMethodField()
|
||||
|
||||
def get_children(self, obj):
|
||||
return TagSerializer(obj.children.all(), many=True).data
|
||||
|
||||
class Meta:
|
||||
model = Tag
|
||||
fields = (
|
||||
@@ -550,8 +546,6 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
|
||||
"permissions",
|
||||
"user_can_change",
|
||||
"set_permissions",
|
||||
"parent",
|
||||
"children",
|
||||
)
|
||||
|
||||
def validate_color(self, color):
|
||||
@@ -959,23 +953,6 @@ class DocumentSerializer(
|
||||
custom_field_instance.field,
|
||||
doc_id,
|
||||
)
|
||||
if "tags" in validated_data:
|
||||
# add all parent tags
|
||||
all_ancestor_tags = set(validated_data["tags"])
|
||||
for tag in validated_data["tags"]:
|
||||
all_ancestor_tags.update(tag.get_all_ancestors())
|
||||
validated_data["tags"] = list(all_ancestor_tags)
|
||||
# remove any children for parents that are being removed
|
||||
tag_parents_being_removed = [
|
||||
tag
|
||||
for tag in instance.tags.all()
|
||||
if tag not in validated_data["tags"] and tag.children.count() > 0
|
||||
]
|
||||
validated_data["tags"] = [
|
||||
tag
|
||||
for tag in validated_data["tags"]
|
||||
if tag not in tag_parents_being_removed
|
||||
]
|
||||
if validated_data.get("remove_inbox_tags"):
|
||||
tag_ids_being_added = (
|
||||
[
|
||||
@@ -1170,6 +1147,15 @@ class SavedViewSerializer(OwnedObjectSerializer):
|
||||
if "user" in validated_data:
|
||||
# backwards compatibility
|
||||
validated_data["owner"] = validated_data.pop("user")
|
||||
if (
|
||||
"display_fields" in validated_data
|
||||
and isinstance(
|
||||
validated_data["display_fields"],
|
||||
list,
|
||||
)
|
||||
and len(validated_data["display_fields"]) == 0
|
||||
):
|
||||
validated_data["display_fields"] = None
|
||||
super().update(instance, validated_data)
|
||||
if rules_data is not None:
|
||||
SavedViewFilterRule.objects.filter(saved_view=instance).delete()
|
||||
@@ -1560,6 +1546,12 @@ class PostDocumentSerializer(serializers.Serializer):
|
||||
required=False,
|
||||
)
|
||||
|
||||
from_webui = serializers.BooleanField(
|
||||
label="Documents are from Paperless-ngx WebUI",
|
||||
write_only=True,
|
||||
required=False,
|
||||
)
|
||||
|
||||
def validate_document(self, document):
|
||||
document_data = document.file.read()
|
||||
mime_type = magic.from_buffer(document_data, mime=True)
|
||||
@@ -1712,6 +1704,7 @@ class TasksViewSerializer(OwnedObjectSerializer):
|
||||
fields = (
|
||||
"id",
|
||||
"task_id",
|
||||
"task_name",
|
||||
"task_file_name",
|
||||
"date_created",
|
||||
"date_done",
|
||||
@@ -1723,12 +1716,6 @@ class TasksViewSerializer(OwnedObjectSerializer):
|
||||
"owner",
|
||||
)
|
||||
|
||||
type = serializers.SerializerMethodField()
|
||||
|
||||
def get_type(self, obj) -> str:
|
||||
# just file tasks, for now
|
||||
return "file"
|
||||
|
||||
related_document = serializers.SerializerMethodField()
|
||||
created_doc_re = re.compile(r"New document id (\d+) created")
|
||||
duplicate_doc_re = re.compile(r"It is a duplicate of .* \(#(\d+)\)")
|
||||
@@ -1736,20 +1723,21 @@ class TasksViewSerializer(OwnedObjectSerializer):
|
||||
def get_related_document(self, obj) -> str | None:
|
||||
result = None
|
||||
re = None
|
||||
match obj.status:
|
||||
case states.SUCCESS:
|
||||
re = self.created_doc_re
|
||||
case states.FAILURE:
|
||||
re = (
|
||||
self.duplicate_doc_re
|
||||
if "existing document is in the trash" not in obj.result
|
||||
else None
|
||||
)
|
||||
if re is not None:
|
||||
try:
|
||||
result = re.search(obj.result).group(1)
|
||||
except Exception:
|
||||
pass
|
||||
if obj.result:
|
||||
match obj.status:
|
||||
case states.SUCCESS:
|
||||
re = self.created_doc_re
|
||||
case states.FAILURE:
|
||||
re = (
|
||||
self.duplicate_doc_re
|
||||
if "existing document is in the trash" not in obj.result
|
||||
else None
|
||||
)
|
||||
if re is not None:
|
||||
try:
|
||||
result = re.search(obj.result).group(1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
@@ -1973,6 +1961,10 @@ class WorkflowActionEmailSerializer(serializers.ModelSerializer):
|
||||
class WorkflowActionWebhookSerializer(serializers.ModelSerializer):
|
||||
id = serializers.IntegerField(allow_null=True, required=False)
|
||||
|
||||
def validate_url(self, url):
|
||||
url_validator(url)
|
||||
return url
|
||||
|
||||
class Meta:
|
||||
model = WorkflowActionWebhook
|
||||
fields = [
|
||||
|
@@ -248,7 +248,7 @@ def set_tags(
|
||||
extra={"group": logging_group},
|
||||
)
|
||||
|
||||
document.add_nested_tags(relevant_tags)
|
||||
document.tags.add(*relevant_tags)
|
||||
|
||||
|
||||
def set_storage_path(
|
||||
@@ -525,19 +525,21 @@ def check_paths_and_prune_custom_fields(sender, instance: CustomField, **kwargs)
|
||||
"""
|
||||
if (
|
||||
instance.data_type == CustomField.FieldDataType.SELECT
|
||||
and instance.fields.count() > 0
|
||||
and instance.extra_data
|
||||
): # Only select fields, for now
|
||||
select_options = {
|
||||
option["id"]: option["label"]
|
||||
for option in instance.extra_data.get("select_options", [])
|
||||
}
|
||||
|
||||
for cf_instance in instance.fields.all():
|
||||
options = instance.extra_data.get("select_options", [])
|
||||
try:
|
||||
next(
|
||||
option["label"]
|
||||
for option in options
|
||||
if option["id"] == cf_instance.value
|
||||
)
|
||||
except StopIteration:
|
||||
# The value of this custom field instance is not in the select options anymore
|
||||
# Check if the current value is still a valid option
|
||||
if cf_instance.value not in select_options:
|
||||
cf_instance.value_select = None
|
||||
cf_instance.save()
|
||||
cf_instance.save(update_fields=["value_select"])
|
||||
|
||||
# Update the filename and move files if necessary
|
||||
update_filename_and_move_files(sender, cf_instance)
|
||||
|
||||
|
||||
@@ -1178,6 +1180,8 @@ def run_workflows(
|
||||
webhook_action()
|
||||
|
||||
if not use_overrides:
|
||||
# limit title to 128 characters
|
||||
document.title = document.title[:128]
|
||||
# save first before setting tags
|
||||
document.save()
|
||||
document.tags.set(doc_tag_ids)
|
||||
@@ -1217,10 +1221,11 @@ def before_task_publish_handler(sender=None, headers=None, body=None, **kwargs):
|
||||
user_id = overrides.owner_id if overrides else None
|
||||
|
||||
PaperlessTask.objects.create(
|
||||
type=PaperlessTask.TaskType.AUTO,
|
||||
task_id=headers["id"],
|
||||
status=states.PENDING,
|
||||
task_file_name=task_file_name,
|
||||
task_name=headers["task"],
|
||||
task_name=PaperlessTask.TaskName.CONSUME_FILE,
|
||||
result=None,
|
||||
date_created=timezone.now(),
|
||||
date_started=None,
|
||||
|
@@ -9,6 +9,7 @@ from tempfile import TemporaryDirectory
|
||||
import tqdm
|
||||
from celery import Task
|
||||
from celery import shared_task
|
||||
from celery import states
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
@@ -35,6 +36,7 @@ from documents.models import Correspondent
|
||||
from documents.models import CustomFieldInstance
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import PaperlessTask
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.models import Workflow
|
||||
@@ -74,19 +76,34 @@ def index_reindex(*, progress_bar_disable=False):
|
||||
|
||||
|
||||
@shared_task
|
||||
def train_classifier():
|
||||
def train_classifier(*, scheduled=True):
|
||||
task = PaperlessTask.objects.create(
|
||||
type=PaperlessTask.TaskType.SCHEDULED_TASK
|
||||
if scheduled
|
||||
else PaperlessTask.TaskType.MANUAL_TASK,
|
||||
task_id=uuid.uuid4(),
|
||||
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||
status=states.STARTED,
|
||||
date_created=timezone.now(),
|
||||
date_started=timezone.now(),
|
||||
)
|
||||
if (
|
||||
not Tag.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||
and not DocumentType.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||
and not Correspondent.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||
and not StoragePath.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||
):
|
||||
logger.info("No automatic matching items, not training")
|
||||
result = "No automatic matching items, not training"
|
||||
logger.info(result)
|
||||
# Special case, items were once auto and trained, so remove the model
|
||||
# and prevent its use again
|
||||
if settings.MODEL_FILE.exists():
|
||||
logger.info(f"Removing {settings.MODEL_FILE} so it won't be used")
|
||||
settings.MODEL_FILE.unlink()
|
||||
task.status = states.SUCCESS
|
||||
task.result = result
|
||||
task.date_done = timezone.now()
|
||||
task.save()
|
||||
return
|
||||
|
||||
classifier = load_classifier()
|
||||
@@ -100,11 +117,19 @@ def train_classifier():
|
||||
f"Saving updated classifier model to {settings.MODEL_FILE}...",
|
||||
)
|
||||
classifier.save()
|
||||
task.result = "Training completed successfully"
|
||||
else:
|
||||
logger.debug("Training data unchanged.")
|
||||
task.result = "Training data unchanged"
|
||||
|
||||
task.status = states.SUCCESS
|
||||
task.date_done = timezone.now()
|
||||
task.save(update_fields=["status", "result", "date_done"])
|
||||
|
||||
except Exception as e:
|
||||
logger.warning("Classifier error: " + str(e))
|
||||
task.status = states.FAILURE
|
||||
task.result = str(e)
|
||||
|
||||
|
||||
@shared_task(bind=True)
|
||||
@@ -335,7 +360,7 @@ def empty_trash(doc_ids=None):
|
||||
)
|
||||
|
||||
try:
|
||||
deleted_document_ids = documents.values_list("id", flat=True)
|
||||
deleted_document_ids = list(documents.values_list("id", flat=True))
|
||||
# Temporarily connect the cleanup handler
|
||||
models.signals.post_delete.connect(cleanup_document_deletion, sender=Document)
|
||||
documents.delete() # this is effectively a hard delete
|
||||
|
@@ -38,6 +38,7 @@ from documents.models import SavedView
|
||||
from documents.models import ShareLink
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.models import WorkflowTrigger
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
from documents.tests.utils import DocumentConsumeDelayMixin
|
||||
|
||||
@@ -1362,6 +1363,30 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
self.assertEqual(overrides.filename, "simple.pdf")
|
||||
self.assertEqual(overrides.custom_field_ids, [custom_field.id])
|
||||
|
||||
def test_upload_with_webui_source(self):
|
||||
"""
|
||||
GIVEN: A document with a source file
|
||||
WHEN: Upload the document with 'from_webui' flag
|
||||
THEN: Consume is called with the source set as WebUI
|
||||
"""
|
||||
self.consume_file_mock.return_value = celery.result.AsyncResult(
|
||||
id=str(uuid.uuid4()),
|
||||
)
|
||||
|
||||
with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f:
|
||||
response = self.client.post(
|
||||
"/api/documents/post_document/",
|
||||
{"document": f, "from_webui": True},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
self.consume_file_mock.assert_called_once()
|
||||
|
||||
input_doc, overrides = self.get_last_consume_delay_call_args()
|
||||
|
||||
self.assertEqual(input_doc.source, WorkflowTrigger.DocumentSourceChoices.WEB_UI)
|
||||
|
||||
def test_upload_invalid_pdf(self):
|
||||
"""
|
||||
GIVEN: Invalid PDF named "*.pdf" that mime_type is in settings.CONSUMER_PDF_RECOVERABLE_MIME_TYPES
|
||||
@@ -1815,6 +1840,19 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# empty display fields treated as none
|
||||
response = self.client.patch(
|
||||
f"/api/saved_views/{v1.id}/",
|
||||
{
|
||||
"display_fields": [],
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
v1.refresh_from_db()
|
||||
self.assertEqual(v1.display_fields, None)
|
||||
|
||||
def test_saved_view_display_customfields(self):
|
||||
"""
|
||||
GIVEN:
|
||||
|
@@ -1,18 +1,14 @@
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from celery import states
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.classifier import ClassifierModelCorruptError
|
||||
from documents.classifier import DocumentClassifier
|
||||
from documents.classifier import load_classifier
|
||||
from documents.models import Document
|
||||
from documents.models import Tag
|
||||
from documents.models import PaperlessTask
|
||||
from paperless import version
|
||||
|
||||
|
||||
@@ -193,7 +189,6 @@ class TestSystemStatus(APITestCase):
|
||||
self.assertEqual(response.data["tasks"]["index_status"], "ERROR")
|
||||
self.assertIsNotNone(response.data["tasks"]["index_error"])
|
||||
|
||||
@override_settings(DATA_DIR=Path("/tmp/does_not_exist/data/"))
|
||||
def test_system_status_classifier_ok(self):
|
||||
"""
|
||||
GIVEN:
|
||||
@@ -203,9 +198,11 @@ class TestSystemStatus(APITestCase):
|
||||
THEN:
|
||||
- The response contains an OK classifier status
|
||||
"""
|
||||
load_classifier()
|
||||
test_classifier = DocumentClassifier()
|
||||
test_classifier.save()
|
||||
PaperlessTask.objects.create(
|
||||
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||
status=states.SUCCESS,
|
||||
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -215,73 +212,101 @@ class TestSystemStatus(APITestCase):
|
||||
def test_system_status_classifier_warning(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- The classifier does not exist yet
|
||||
- > 0 documents and tags with auto matching exist
|
||||
- No classifier task is found
|
||||
WHEN:
|
||||
- The user requests the system status
|
||||
THEN:
|
||||
- The response contains an WARNING classifier status
|
||||
- The response contains a WARNING classifier status
|
||||
"""
|
||||
with override_settings(MODEL_FILE=Path("does_not_exist")):
|
||||
Document.objects.create(
|
||||
title="Test Document",
|
||||
)
|
||||
Tag.objects.create(name="Test Tag", matching_algorithm=Tag.MATCH_AUTO)
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["tasks"]["classifier_status"], "WARNING")
|
||||
self.assertIsNotNone(response.data["tasks"]["classifier_error"])
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data["tasks"]["classifier_status"],
|
||||
"WARNING",
|
||||
)
|
||||
|
||||
@mock.patch(
|
||||
"documents.classifier.load_classifier",
|
||||
side_effect=ClassifierModelCorruptError(),
|
||||
)
|
||||
def test_system_status_classifier_error(self, mock_load_classifier):
|
||||
def test_system_status_classifier_error(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- The classifier does exist but is corrupt
|
||||
- > 0 documents and tags with auto matching exist
|
||||
- An error occurred while loading the classifier
|
||||
WHEN:
|
||||
- The user requests the system status
|
||||
THEN:
|
||||
- The response contains an ERROR classifier status
|
||||
"""
|
||||
with (
|
||||
tempfile.NamedTemporaryFile(
|
||||
dir="/tmp",
|
||||
delete=False,
|
||||
) as does_exist,
|
||||
override_settings(MODEL_FILE=Path(does_exist.name)),
|
||||
):
|
||||
Document.objects.create(
|
||||
title="Test Document",
|
||||
)
|
||||
Tag.objects.create(
|
||||
name="Test Tag",
|
||||
matching_algorithm=Tag.MATCH_AUTO,
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data["tasks"]["classifier_status"],
|
||||
"ERROR",
|
||||
)
|
||||
self.assertIsNotNone(response.data["tasks"]["classifier_error"])
|
||||
PaperlessTask.objects.create(
|
||||
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||
status=states.FAILURE,
|
||||
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||
result="Classifier training failed",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data["tasks"]["classifier_status"],
|
||||
"ERROR",
|
||||
)
|
||||
self.assertIsNotNone(response.data["tasks"]["classifier_error"])
|
||||
|
||||
def test_system_status_classifier_ok_no_objects(self):
|
||||
def test_system_status_sanity_check_ok(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- The classifier does not exist (and should not)
|
||||
- No documents nor objects with auto matching exist
|
||||
- The sanity check is successful
|
||||
WHEN:
|
||||
- The user requests the system status
|
||||
THEN:
|
||||
- The response contains an OK classifier status
|
||||
- The response contains an OK sanity check status
|
||||
"""
|
||||
with override_settings(MODEL_FILE=Path("does_not_exist")):
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["tasks"]["classifier_status"], "OK")
|
||||
PaperlessTask.objects.create(
|
||||
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||
status=states.SUCCESS,
|
||||
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["tasks"]["sanity_check_status"], "OK")
|
||||
self.assertIsNone(response.data["tasks"]["sanity_check_error"])
|
||||
|
||||
def test_system_status_sanity_check_warning(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- No sanity check task is found
|
||||
WHEN:
|
||||
- The user requests the system status
|
||||
THEN:
|
||||
- The response contains a WARNING sanity check status
|
||||
"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data["tasks"]["sanity_check_status"],
|
||||
"WARNING",
|
||||
)
|
||||
|
||||
def test_system_status_sanity_check_error(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- The sanity check failed
|
||||
WHEN:
|
||||
- The user requests the system status
|
||||
THEN:
|
||||
- The response contains an ERROR sanity check status
|
||||
"""
|
||||
PaperlessTask.objects.create(
|
||||
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||
status=states.FAILURE,
|
||||
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||
result="5 issues found.",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data["tasks"]["sanity_check_status"],
|
||||
"ERROR",
|
||||
)
|
||||
self.assertIsNotNone(response.data["tasks"]["sanity_check_error"])
|
||||
|
@@ -130,7 +130,7 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
|
||||
response = self.client.get(self.ENDPOINT)
|
||||
response = self.client.get(self.ENDPOINT + "?acknowledged=false")
|
||||
self.assertEqual(len(response.data), 0)
|
||||
|
||||
def test_tasks_owner_aware(self):
|
||||
@@ -246,7 +246,7 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
||||
PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="test.pdf",
|
||||
task_name="documents.tasks.some_task",
|
||||
task_name=PaperlessTask.TaskName.CONSUME_FILE,
|
||||
status=celery.states.SUCCESS,
|
||||
)
|
||||
|
||||
@@ -272,7 +272,7 @@ class TestTasks(DirectoriesMixin, APITestCase):
|
||||
PaperlessTask.objects.create(
|
||||
task_id=str(uuid.uuid4()),
|
||||
task_file_name="anothertest.pdf",
|
||||
task_name="documents.tasks.some_task",
|
||||
task_name=PaperlessTask.TaskName.CONSUME_FILE,
|
||||
status=celery.states.SUCCESS,
|
||||
)
|
||||
|
||||
|
@@ -588,3 +588,45 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
|
||||
def test_webhook_action_url_validation(self):
|
||||
"""
|
||||
GIVEN:
|
||||
- API request to create a workflow with a notification action
|
||||
WHEN:
|
||||
- API is called
|
||||
THEN:
|
||||
- Correct HTTP response
|
||||
"""
|
||||
|
||||
for url, expected_resp_code in [
|
||||
("https://examplewithouttld:3000/path", status.HTTP_201_CREATED),
|
||||
("file:///etc/passwd/path", status.HTTP_400_BAD_REQUEST),
|
||||
]:
|
||||
response = self.client.post(
|
||||
self.ENDPOINT,
|
||||
json.dumps(
|
||||
{
|
||||
"name": "Workflow 2",
|
||||
"order": 1,
|
||||
"triggers": [
|
||||
{
|
||||
"type": WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||
"sources": [DocumentSource.ApiUpload],
|
||||
"filter_filename": "*",
|
||||
},
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"type": WorkflowAction.WorkflowActionType.WEBHOOK,
|
||||
"webhook": {
|
||||
"url": url,
|
||||
"include_document": False,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
self.assertEqual(response.status_code, expected_resp_code)
|
||||
|
@@ -68,7 +68,7 @@ class TestTaskSignalHandler(DirectoriesMixin, TestCase):
|
||||
self.assertIsNotNone(task)
|
||||
self.assertEqual(headers["id"], task.task_id)
|
||||
self.assertEqual("hello-999.pdf", task.task_file_name)
|
||||
self.assertEqual("documents.tasks.consume_file", task.task_name)
|
||||
self.assertEqual(PaperlessTask.TaskName.CONSUME_FILE, task.task_name)
|
||||
self.assertEqual(1, task.owner_id)
|
||||
self.assertEqual(celery.states.PENDING, task.status)
|
||||
|
||||
|
@@ -4,11 +4,18 @@ from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def uri_validator(value) -> None:
|
||||
def uri_validator(value: str, allowed_schemes: set[str] | None = None) -> None:
|
||||
"""
|
||||
Raises a ValidationError if the given value does not parse as an
|
||||
URI looking thing, which we're defining as a scheme and either network
|
||||
location or path value
|
||||
Validates that the given value parses as a URI with required components
|
||||
and optionally restricts to specific schemes.
|
||||
|
||||
Args:
|
||||
value: The URI string to validate
|
||||
allowed_schemes: Optional set/list of allowed schemes (e.g. {'http', 'https'}).
|
||||
If None, all schemes are allowed.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the URI is invalid or uses a disallowed scheme
|
||||
"""
|
||||
try:
|
||||
parts = urlparse(value)
|
||||
@@ -22,8 +29,32 @@ def uri_validator(value) -> None:
|
||||
_(f"Unable to parse URI {value}, missing net location or path"),
|
||||
params={"value": value},
|
||||
)
|
||||
|
||||
if allowed_schemes and parts.scheme not in allowed_schemes:
|
||||
raise ValidationError(
|
||||
_(
|
||||
f"URI scheme '{parts.scheme}' is not allowed. Allowed schemes: {', '.join(allowed_schemes)}",
|
||||
),
|
||||
params={"value": value, "scheme": parts.scheme},
|
||||
)
|
||||
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
_(f"Unable to parse URI {value}"),
|
||||
params={"value": value},
|
||||
) from e
|
||||
|
||||
|
||||
def url_validator(value) -> None:
|
||||
"""
|
||||
Validates that the given value is a valid HTTP or HTTPS URL.
|
||||
|
||||
Args:
|
||||
value: The URL string to validate
|
||||
|
||||
Raises:
|
||||
ValidationError: If the URL is invalid or not using http/https scheme
|
||||
"""
|
||||
uri_validator(value, allowed_schemes={"http", "https"})
|
||||
|
@@ -15,6 +15,7 @@ from urllib.parse import quote
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import pathvalidate
|
||||
from celery import states
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.models import User
|
||||
@@ -103,6 +104,7 @@ from documents.filters import DocumentsOrderingFilter
|
||||
from documents.filters import DocumentTypeFilterSet
|
||||
from documents.filters import ObjectOwnedOrGrantedPermissionsFilter
|
||||
from documents.filters import ObjectOwnedPermissionsFilter
|
||||
from documents.filters import PaperlessTaskFilterSet
|
||||
from documents.filters import ShareLinkFilterSet
|
||||
from documents.filters import StoragePathFilterSet
|
||||
from documents.filters import TagFilterSet
|
||||
@@ -1385,6 +1387,7 @@ class PostDocumentView(GenericAPIView):
|
||||
created = serializer.validated_data.get("created")
|
||||
archive_serial_number = serializer.validated_data.get("archive_serial_number")
|
||||
custom_field_ids = serializer.validated_data.get("custom_fields")
|
||||
from_webui = serializer.validated_data.get("from_webui")
|
||||
|
||||
t = int(mktime(datetime.now().timetuple()))
|
||||
|
||||
@@ -1399,7 +1402,7 @@ class PostDocumentView(GenericAPIView):
|
||||
os.utime(temp_file_path, times=(t, t))
|
||||
|
||||
input_doc = ConsumableDocument(
|
||||
source=DocumentSource.ApiUpload,
|
||||
source=DocumentSource.WebUI if from_webui else DocumentSource.ApiUpload,
|
||||
original_file=temp_file_path,
|
||||
)
|
||||
input_doc_overrides = DocumentMetadataOverrides(
|
||||
@@ -2223,16 +2226,15 @@ class RemoteVersionView(GenericAPIView):
|
||||
class TasksViewSet(ReadOnlyModelViewSet):
|
||||
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
|
||||
serializer_class = TasksViewSerializer
|
||||
filter_backends = (ObjectOwnedOrGrantedPermissionsFilter,)
|
||||
filter_backends = (
|
||||
DjangoFilterBackend,
|
||||
OrderingFilter,
|
||||
ObjectOwnedOrGrantedPermissionsFilter,
|
||||
)
|
||||
filterset_class = PaperlessTaskFilterSet
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = (
|
||||
PaperlessTask.objects.filter(
|
||||
acknowledged=False,
|
||||
)
|
||||
.order_by("date_created")
|
||||
.reverse()
|
||||
)
|
||||
queryset = PaperlessTask.objects.all().order_by("-date_created")
|
||||
task_id = self.request.query_params.get("task_id")
|
||||
if task_id is not None:
|
||||
queryset = PaperlessTask.objects.filter(task_id=task_id)
|
||||
@@ -2561,6 +2563,14 @@ class CustomFieldViewSet(ModelViewSet):
|
||||
"last_trained": serializers.DateTimeField(),
|
||||
},
|
||||
),
|
||||
"sanity_check": inline_serializer(
|
||||
name="SanityCheck",
|
||||
fields={
|
||||
"status": serializers.CharField(),
|
||||
"error": serializers.CharField(),
|
||||
"last_run": serializers.DateTimeField(),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -2569,6 +2579,17 @@ class CustomFieldViewSet(ModelViewSet):
|
||||
class SystemStatusView(PassUserMixin):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def _get_next_scheduled_task_schedule(
|
||||
self,
|
||||
schedule: dict,
|
||||
task_name: str,
|
||||
last_run,
|
||||
) -> datetime | None:
|
||||
# example: {'Check all e-mail accounts': {'task': 'paperless_mail.tasks.process_mail_accounts', 'schedule': <crontab: */10 * * * * (m/h/dM/MY/d)>, 'options': {'expires': 540.0}}, 'Train the classifier': {'task': 'documents.tasks.train_classifier', 'schedule': <crontab: 5 */1 * * * (m/h/dM/MY/d)>, 'options': {'expires': 3540.0}}, 'Optimize the index': {'task': 'documents.tasks.index_optimize', 'schedule': <crontab: 0 0 * * * (m/h/dM/MY/d)>, 'options': {'expires': 82800.0}}, 'Perform sanity check': {'task': 'documents.tasks.sanity_check', 'schedule': <crontab: 30 0 * * sun (m/h/dM/MY/d)>, 'options': {'expires': 601200.0}}, 'Empty trash': {'task': 'documents.tasks.empty_trash', 'schedule': <crontab: 0 1 * * * (m/h/dM/MY/d)>, 'options': {'expires': 82800.0}}, 'Check and run scheduled workflows': {'task': 'documents.tasks.check_scheduled_workflows', 'schedule': <crontab: 5 */1 * * * (m/h/dM/MY/d)>, 'options': {'expires': 3540.0}}}
|
||||
for _, task_data in schedule.items():
|
||||
if task_data["task"] and task_data["task"].find(task_name) != -1:
|
||||
return task_data["schedule"]
|
||||
|
||||
def get(self, request, format=None):
|
||||
if not request.user.is_staff:
|
||||
return HttpResponseForbidden("Insufficient permissions")
|
||||
@@ -2621,13 +2642,22 @@ class SystemStatusView(PassUserMixin):
|
||||
)
|
||||
redis_error = "Error connecting to redis, check logs for more detail."
|
||||
|
||||
celery_error = None
|
||||
celery_url = None
|
||||
schedule = None
|
||||
try:
|
||||
celery_ping = celery_app.control.inspect().ping()
|
||||
first_worker_ping = celery_ping[next(iter(celery_ping.keys()))]
|
||||
celery_url = next(iter(celery_ping.keys()))
|
||||
first_worker_ping = celery_ping[celery_url]
|
||||
schedule = celery_app.conf.beat_schedule
|
||||
if first_worker_ping["ok"] == "pong":
|
||||
celery_active = "OK"
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
celery_active = "ERROR"
|
||||
logger.exception(
|
||||
f"System status detected a possible problem while connecting to celery: {e}",
|
||||
)
|
||||
celery_error = "Error connecting to celery, check logs for more detail."
|
||||
|
||||
index_error = None
|
||||
try:
|
||||
@@ -2644,54 +2674,72 @@ class SystemStatusView(PassUserMixin):
|
||||
)
|
||||
index_last_modified = None
|
||||
|
||||
classifier_error = None
|
||||
classifier_status = None
|
||||
try:
|
||||
classifier = load_classifier(raise_exception=True)
|
||||
if classifier is None:
|
||||
# Make sure classifier should exist
|
||||
docs_queryset = Document.objects.exclude(
|
||||
tags__is_inbox_tag=True,
|
||||
)
|
||||
if (
|
||||
docs_queryset.count() > 0
|
||||
and (
|
||||
Tag.objects.filter(matching_algorithm=Tag.MATCH_AUTO).exists()
|
||||
or DocumentType.objects.filter(
|
||||
matching_algorithm=Tag.MATCH_AUTO,
|
||||
).exists()
|
||||
or Correspondent.objects.filter(
|
||||
matching_algorithm=Tag.MATCH_AUTO,
|
||||
).exists()
|
||||
or StoragePath.objects.filter(
|
||||
matching_algorithm=Tag.MATCH_AUTO,
|
||||
).exists()
|
||||
)
|
||||
and not settings.MODEL_FILE.exists()
|
||||
):
|
||||
# if classifier file doesn't exist just classify as a warning
|
||||
classifier_error = "Classifier file does not exist (yet). Re-training may be pending."
|
||||
classifier_status = "WARNING"
|
||||
raise FileNotFoundError(classifier_error)
|
||||
classifier_status = "OK"
|
||||
classifier_last_trained = (
|
||||
make_aware(
|
||||
datetime.fromtimestamp(classifier.get_last_checked()),
|
||||
)
|
||||
if settings.MODEL_FILE.exists()
|
||||
and classifier.get_last_checked() is not None
|
||||
else None
|
||||
last_trained_task = (
|
||||
PaperlessTask.objects.filter(
|
||||
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||
)
|
||||
except Exception as e:
|
||||
if classifier_status is None:
|
||||
classifier_status = "ERROR"
|
||||
classifier_last_trained = None
|
||||
if classifier_error is None:
|
||||
classifier_error = (
|
||||
"Unable to load classifier, check logs for more detail."
|
||||
)
|
||||
logger.exception(
|
||||
f"System status detected a possible problem while loading the classifier: {e}",
|
||||
.order_by("-date_done")
|
||||
.first()
|
||||
)
|
||||
classifier_status = "OK"
|
||||
classifier_error = None
|
||||
classifier_next_training = None
|
||||
if last_trained_task is None:
|
||||
classifier_status = "WARNING"
|
||||
classifier_error = "No classifier training tasks found"
|
||||
elif last_trained_task and last_trained_task.status == states.FAILURE:
|
||||
classifier_status = "ERROR"
|
||||
classifier_error = last_trained_task.result
|
||||
classifier_last_trained = (
|
||||
last_trained_task.date_done if last_trained_task else None
|
||||
)
|
||||
last_scheduled_trained_task = (
|
||||
PaperlessTask.objects.filter(
|
||||
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||
)
|
||||
.order_by("-date_done")
|
||||
.first()
|
||||
)
|
||||
if last_scheduled_trained_task and schedule:
|
||||
classifier_next_training: datetime = self._get_next_scheduled_task_schedule(
|
||||
schedule=schedule,
|
||||
task_name=PaperlessTask.TaskName.TRAIN_CLASSIFIER,
|
||||
last_run=last_trained_task.date_done,
|
||||
)
|
||||
|
||||
last_sanity_check = (
|
||||
PaperlessTask.objects.filter(
|
||||
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||
)
|
||||
.order_by("-date_done")
|
||||
.first()
|
||||
)
|
||||
sanity_check_status = "OK"
|
||||
sanity_check_error = None
|
||||
sanity_check_next_run = None
|
||||
if last_sanity_check is None:
|
||||
sanity_check_status = "WARNING"
|
||||
sanity_check_error = "No sanity check tasks found"
|
||||
elif last_sanity_check and last_sanity_check.status == states.FAILURE:
|
||||
sanity_check_status = "ERROR"
|
||||
sanity_check_error = last_sanity_check.result
|
||||
sanity_check_last_run = (
|
||||
last_sanity_check.date_done if last_sanity_check else None
|
||||
)
|
||||
last_scheduled_sanity_check = (
|
||||
PaperlessTask.objects.filter(
|
||||
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||
type=PaperlessTask.TaskType.SCHEDULED_TASK,
|
||||
)
|
||||
.order_by("-date_done")
|
||||
.first()
|
||||
)
|
||||
if last_scheduled_sanity_check and schedule:
|
||||
sanity_check_next_run: datetime = self._get_next_scheduled_task_schedule(
|
||||
schedule=schedule,
|
||||
task_name=PaperlessTask.TaskName.CHECK_SANITY,
|
||||
last_run=last_sanity_check.date_done,
|
||||
)
|
||||
|
||||
return Response(
|
||||
@@ -2720,12 +2768,19 @@ class SystemStatusView(PassUserMixin):
|
||||
"redis_status": redis_status,
|
||||
"redis_error": redis_error,
|
||||
"celery_status": celery_active,
|
||||
"celery_url": celery_url,
|
||||
"celery_error": celery_error,
|
||||
"index_status": index_status,
|
||||
"index_last_modified": index_last_modified,
|
||||
"index_error": index_error,
|
||||
"classifier_status": classifier_status,
|
||||
"classifier_last_trained": classifier_last_trained,
|
||||
"classifier_next_training": classifier_next_training,
|
||||
"classifier_error": classifier_error,
|
||||
"sanity_check_status": sanity_check_status,
|
||||
"sanity_check_last_run": sanity_check_last_run,
|
||||
"sanity_check_next_run": sanity_check_next_run,
|
||||
"sanity_check_error": sanity_check_error,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -510,6 +510,8 @@ SESSION_EXPIRE_AT_BROWSER_CLOSE = not ACCOUNT_SESSION_REMEMBER
|
||||
SESSION_COOKIE_AGE = int(
|
||||
os.getenv("PAPERLESS_SESSION_COOKIE_AGE", 60 * 60 * 24 * 7 * 3),
|
||||
)
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-SESSION_ENGINE
|
||||
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
|
||||
|
||||
if AUTO_LOGIN_USERNAME:
|
||||
_index = MIDDLEWARE.index("django.contrib.auth.middleware.AuthenticationMiddleware")
|
||||
@@ -749,6 +751,7 @@ LANGUAGES = [
|
||||
("tr-tr", _("Turkish")),
|
||||
("uk-ua", _("Ukrainian")),
|
||||
("zh-cn", _("Chinese Simplified")),
|
||||
("zh-tw", _("Chinese Traditional")),
|
||||
]
|
||||
|
||||
LOCALE_PATHS = [os.path.join(BASE_DIR, "locale")]
|
||||
|
@@ -305,6 +305,11 @@ urlpatterns = [
|
||||
],
|
||||
),
|
||||
),
|
||||
re_path(
|
||||
r"^confirm-email/(?P<key>[-:\w]+)/$",
|
||||
allauth_account_views.ConfirmEmailView.as_view(),
|
||||
name="account_confirm_email",
|
||||
),
|
||||
re_path(
|
||||
r"^password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$",
|
||||
allauth_account_views.password_reset_from_key,
|
||||
|
Reference in New Issue
Block a user