Merge branch 'dev' of github.com:paperless-ngx/paperless-ngx into patch-1
This commit is contained in:
commit
16ad7dd792
@ -47,7 +47,7 @@ repos:
|
|||||||
exclude: "(^Pipfile\\.lock$)"
|
exclude: "(^Pipfile\\.lock$)"
|
||||||
# Python hooks
|
# Python hooks
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: 'v0.2.2'
|
rev: 'v0.3.0'
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- repo: https://github.com/psf/black-pre-commit-mirror
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
|
@ -59,7 +59,8 @@ ARG GS_VERSION=10.02.1
|
|||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
# Ignore warning from Whitenoise
|
# Ignore warning from Whitenoise
|
||||||
PYTHONWARNINGS="ignore:::django.http.response:517"
|
PYTHONWARNINGS="ignore:::django.http.response:517" \
|
||||||
|
PNGX_CONTAINERIZED=1
|
||||||
|
|
||||||
#
|
#
|
||||||
# Begin installation and configuration
|
# Begin installation and configuration
|
||||||
|
2
Pipfile
2
Pipfile
@ -7,7 +7,7 @@ name = "pypi"
|
|||||||
dateparser = "~=1.2"
|
dateparser = "~=1.2"
|
||||||
# WARNING: django does not use semver.
|
# WARNING: django does not use semver.
|
||||||
# Only patch versions are guaranteed to not introduce breaking changes.
|
# Only patch versions are guaranteed to not introduce breaking changes.
|
||||||
django = "~=4.2.10"
|
django = "~=4.2.11"
|
||||||
django-allauth = "*"
|
django-allauth = "*"
|
||||||
django-auditlog = "*"
|
django-auditlog = "*"
|
||||||
django-celery-results = "*"
|
django-celery-results = "*"
|
||||||
|
653
Pipfile.lock
generated
653
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "cdd620926150607295aa42447d26c89d2a372309e3229c4f8f711ebe0ed19670"
|
"sha256": "7bc15a3bbd521f85a8cdcc85be8adf7c942acb53c6d461199d7f8b1ef63ac651"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
@ -24,11 +24,11 @@
|
|||||||
},
|
},
|
||||||
"anyio": {
|
"anyio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee",
|
"sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8",
|
||||||
"sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"
|
"sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==4.2.0"
|
"version": "==4.3.0"
|
||||||
},
|
},
|
||||||
"asgiref": {
|
"asgiref": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -43,7 +43,7 @@
|
|||||||
"sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f",
|
"sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f",
|
||||||
"sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"
|
"sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version <= '3.11.2'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==4.0.3"
|
"version": "==4.0.3"
|
||||||
},
|
},
|
||||||
"billiard": {
|
"billiard": {
|
||||||
@ -392,42 +392,41 @@
|
|||||||
},
|
},
|
||||||
"cryptography": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:01911714117642a3f1792c7f376db572aadadbafcd8d75bb527166009c9f1d1b",
|
"sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee",
|
||||||
"sha256:0e89f7b84f421c56e7ff69f11c441ebda73b8a8e6488d322ef71746224c20fce",
|
"sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576",
|
||||||
"sha256:12d341bd42cdb7d4937b0cabbdf2a94f949413ac4504904d0cdbdce4a22cbf88",
|
"sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d",
|
||||||
"sha256:15a1fb843c48b4a604663fa30af60818cd28f895572386e5f9b8a665874c26e7",
|
"sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30",
|
||||||
"sha256:1cdcdbd117681c88d717437ada72bdd5be9de117f96e3f4d50dab3f59fd9ab20",
|
"sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413",
|
||||||
"sha256:1df6fcbf60560d2113b5ed90f072dc0b108d64750d4cbd46a21ec882c7aefce9",
|
"sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb",
|
||||||
"sha256:3c6048f217533d89f2f8f4f0fe3044bf0b2090453b7b73d0b77db47b80af8dff",
|
"sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da",
|
||||||
"sha256:3e970a2119507d0b104f0a8e281521ad28fc26f2820687b3436b8c9a5fcf20d1",
|
"sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4",
|
||||||
"sha256:44a64043f743485925d3bcac548d05df0f9bb445c5fcca6681889c7c3ab12764",
|
"sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd",
|
||||||
"sha256:4e36685cb634af55e0677d435d425043967ac2f3790ec652b2b88ad03b85c27b",
|
"sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc",
|
||||||
"sha256:5f8907fcf57392cd917892ae83708761c6ff3c37a8e835d7246ff0ad251d9298",
|
"sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8",
|
||||||
"sha256:69b22ab6506a3fe483d67d1ed878e1602bdd5912a134e6202c1ec672233241c1",
|
"sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1",
|
||||||
"sha256:6bfadd884e7280df24d26f2186e4e07556a05d37393b0f220a840b083dc6a824",
|
"sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc",
|
||||||
"sha256:6d0fbe73728c44ca3a241eff9aefe6496ab2656d6e7a4ea2459865f2e8613257",
|
"sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e",
|
||||||
"sha256:6ffb03d419edcab93b4b19c22ee80c007fb2d708429cecebf1dd3258956a563a",
|
"sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8",
|
||||||
"sha256:810bcf151caefc03e51a3d61e53335cd5c7316c0a105cc695f0959f2c638b129",
|
"sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940",
|
||||||
"sha256:831a4b37accef30cccd34fcb916a5d7b5be3cbbe27268a02832c3e450aea39cb",
|
"sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400",
|
||||||
"sha256:887623fe0d70f48ab3f5e4dbf234986b1329a64c066d719432d0698522749929",
|
"sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7",
|
||||||
"sha256:a0298bdc6e98ca21382afe914c642620370ce0470a01e1bef6dd9b5354c36854",
|
"sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16",
|
||||||
"sha256:a1327f280c824ff7885bdeef8578f74690e9079267c1c8bd7dc5cc5aa065ae52",
|
"sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278",
|
||||||
"sha256:c1f25b252d2c87088abc8bbc4f1ecbf7c919e05508a7e8628e6875c40bc70923",
|
"sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74",
|
||||||
"sha256:c3a5cbc620e1e17009f30dd34cb0d85c987afd21c41a74352d1719be33380885",
|
"sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec",
|
||||||
"sha256:ce8613beaffc7c14f091497346ef117c1798c202b01153a8cc7b8e2ebaaf41c0",
|
"sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1",
|
||||||
"sha256:d2a27aca5597c8a71abbe10209184e1a8e91c1fd470b5070a2ea60cafec35bcd",
|
"sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2",
|
||||||
"sha256:dad9c385ba8ee025bb0d856714f71d7840020fe176ae0229de618f14dae7a6e2",
|
"sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c",
|
||||||
"sha256:db4b65b02f59035037fde0998974d84244a64c3265bdef32a827ab9b63d61b18",
|
"sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922",
|
||||||
"sha256:e09469a2cec88fb7b078e16d4adec594414397e8879a4341c6ace96013463d5b",
|
"sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a",
|
||||||
"sha256:e53dc41cda40b248ebc40b83b31516487f7db95ab8ceac1f042626bc43a2f992",
|
"sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6",
|
||||||
"sha256:f1e85a178384bf19e36779d91ff35c7617c885da487d689b05c1366f9933ad74",
|
"sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1",
|
||||||
"sha256:f47be41843200f7faec0683ad751e5ef11b9a56a220d57f300376cd8aba81660",
|
"sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e",
|
||||||
"sha256:fb0cef872d8193e487fc6bdb08559c3aa41b659a7d9be48b2e10747f47863925",
|
"sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac",
|
||||||
"sha256:ffc73996c4fca3d2b6c1c8c12bfd3ad00def8621da24f547626bf06441400449"
|
"sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==42.0.4"
|
"version": "==42.0.5"
|
||||||
},
|
},
|
||||||
"dateparser": {
|
"dateparser": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -463,12 +462,12 @@
|
|||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1",
|
"sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4",
|
||||||
"sha256:b1260ed381b10a11753c73444408e19869f3241fc45c985cd55a30177c789d13"
|
"sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==4.2.10"
|
"version": "==4.2.11"
|
||||||
},
|
},
|
||||||
"django-allauth": {
|
"django-allauth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -755,11 +754,11 @@
|
|||||||
},
|
},
|
||||||
"httpcore": {
|
"httpcore": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7",
|
"sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73",
|
||||||
"sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"
|
"sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.0.2"
|
"version": "==1.0.4"
|
||||||
},
|
},
|
||||||
"httptools": {
|
"httptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -807,11 +806,11 @@
|
|||||||
"http2"
|
"http2"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf",
|
"sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5",
|
||||||
"sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"
|
"sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.9'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==0.26.0"
|
"version": "==0.27.0"
|
||||||
},
|
},
|
||||||
"humanize": {
|
"humanize": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1007,65 +1006,66 @@
|
|||||||
},
|
},
|
||||||
"msgpack": {
|
"msgpack": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:04ad6069c86e531682f9e1e71b71c1c3937d6014a7c3e9edd2aa81ad58842862",
|
"sha256:00e073efcba9ea99db5acef3959efa45b52bc67b61b00823d2a1a6944bf45982",
|
||||||
"sha256:0bfdd914e55e0d2c9e1526de210f6fe8ffe9705f2b1dfcc4aecc92a4cb4b533d",
|
"sha256:0726c282d188e204281ebd8de31724b7d749adebc086873a59efb8cf7ae27df3",
|
||||||
"sha256:1dc93e8e4653bdb5910aed79f11e165c85732067614f180f70534f056da97db3",
|
"sha256:0ceea77719d45c839fd73abcb190b8390412a890df2f83fb8cf49b2a4b5c2f40",
|
||||||
"sha256:1e2d69948e4132813b8d1131f29f9101bc2c915f26089a6d632001a5c1349672",
|
"sha256:114be227f5213ef8b215c22dde19532f5da9652e56e8ce969bf0a26d7c419fee",
|
||||||
"sha256:235a31ec7db685f5c82233bddf9858748b89b8119bf4538d514536c485c15fe0",
|
"sha256:13577ec9e247f8741c84d06b9ece5f654920d8365a4b636ce0e44f15e07ec693",
|
||||||
"sha256:27dcd6f46a21c18fa5e5deed92a43d4554e3df8d8ca5a47bf0615d6a5f39dbc9",
|
"sha256:1876b0b653a808fcd50123b953af170c535027bf1d053b59790eebb0aeb38950",
|
||||||
"sha256:28efb066cde83c479dfe5a48141a53bc7e5f13f785b92ddde336c716663039ee",
|
"sha256:1ab0bbcd4d1f7b6991ee7c753655b481c50084294218de69365f8f1970d4c151",
|
||||||
"sha256:3476fae43db72bd11f29a5147ae2f3cb22e2f1a91d575ef130d2bf49afd21c46",
|
"sha256:1cce488457370ffd1f953846f82323cb6b2ad2190987cd4d70b2713e17268d24",
|
||||||
"sha256:36e17c4592231a7dbd2ed09027823ab295d2791b3b1efb2aee874b10548b7524",
|
"sha256:24f727df1e20b9876fa6e95f840a2a2651e34c0ad147676356f4bf5fbb0206ca",
|
||||||
"sha256:384d779f0d6f1b110eae74cb0659d9aa6ff35aaf547b3955abf2ab4c901c4819",
|
"sha256:26ee97a8261e6e35885c2ecd2fd4a6d38252246f94a2aec23665a4e66d066305",
|
||||||
"sha256:38949d30b11ae5f95c3c91917ee7a6b239f5ec276f271f28638dec9156f82cfc",
|
"sha256:3528807cbbb7f315bb81959d5961855e7ba52aa60a3097151cb21956fbc7502b",
|
||||||
"sha256:3967e4ad1aa9da62fd53e346ed17d7b2e922cba5ab93bdd46febcac39be636fc",
|
"sha256:374a8e88ddab84b9ada695d255679fb99c53513c0a51778796fcf0944d6c789c",
|
||||||
"sha256:3e7bf4442b310ff154b7bb9d81eb2c016b7d597e364f97d72b1acc3817a0fdc1",
|
"sha256:376081f471a2ef24828b83a641a02c575d6103a3ad7fd7dade5486cad10ea659",
|
||||||
"sha256:3f0c8c6dfa6605ab8ff0611995ee30d4f9fcff89966cf562733b4008a3d60d82",
|
"sha256:3923a1778f7e5ef31865893fdca12a8d7dc03a44b33e2a5f3295416314c09f5d",
|
||||||
"sha256:484ae3240666ad34cfa31eea7b8c6cd2f1fdaae21d73ce2974211df099a95d81",
|
"sha256:4916727e31c28be8beaf11cf117d6f6f188dcc36daae4e851fee88646f5b6b18",
|
||||||
"sha256:4a7b4f35de6a304b5533c238bee86b670b75b03d31b7797929caa7a624b5dda6",
|
"sha256:493c5c5e44b06d6c9268ce21b302c9ca055c1fd3484c25ba41d34476c76ee746",
|
||||||
"sha256:4cb14ce54d9b857be9591ac364cb08dc2d6a5c4318c1182cb1d02274029d590d",
|
"sha256:505fe3d03856ac7d215dbe005414bc28505d26f0c128906037e66d98c4e95868",
|
||||||
"sha256:4e71bc4416de195d6e9b4ee93ad3f2f6b2ce11d042b4d7a7ee00bbe0358bd0c2",
|
"sha256:5845fdf5e5d5b78a49b826fcdc0eb2e2aa7191980e3d2cfd2a30303a74f212e2",
|
||||||
"sha256:52700dc63a4676669b341ba33520f4d6e43d3ca58d422e22ba66d1736b0a6e4c",
|
"sha256:5c330eace3dd100bdb54b5653b966de7f51c26ec4a7d4e87132d9b4f738220ba",
|
||||||
"sha256:572efc93db7a4d27e404501975ca6d2d9775705c2d922390d878fcf768d92c87",
|
"sha256:5dbf059fb4b7c240c873c1245ee112505be27497e90f7c6591261c7d3c3a8228",
|
||||||
"sha256:576eb384292b139821c41995523654ad82d1916da6a60cff129c715a6223ea84",
|
"sha256:5e390971d082dba073c05dbd56322427d3280b7cc8b53484c9377adfbae67dc2",
|
||||||
"sha256:5b0bf0effb196ed76b7ad883848143427a73c355ae8e569fa538365064188b8e",
|
"sha256:5fbb160554e319f7b22ecf530a80a3ff496d38e8e07ae763b9e82fadfe96f273",
|
||||||
"sha256:5b6ccc0c85916998d788b295765ea0e9cb9aac7e4a8ed71d12e7d8ac31c23c95",
|
"sha256:64d0fcd436c5683fdd7c907eeae5e2cbb5eb872fafbc03a43609d7941840995c",
|
||||||
"sha256:5ed82f5a7af3697b1c4786053736f24a0efd0a1b8a130d4c7bfee4b9ded0f08f",
|
"sha256:69284049d07fce531c17404fcba2bb1df472bc2dcdac642ae71a2d079d950653",
|
||||||
"sha256:6d4c80667de2e36970ebf74f42d1088cc9ee7ef5f4e8c35eee1b40eafd33ca5b",
|
"sha256:6a0e76621f6e1f908ae52860bdcb58e1ca85231a9b0545e64509c931dd34275a",
|
||||||
"sha256:730076207cb816138cf1af7f7237b208340a2c5e749707457d70705715c93b93",
|
"sha256:73ee792784d48aa338bba28063e19a27e8d989344f34aad14ea6e1b9bd83f596",
|
||||||
"sha256:7687e22a31e976a0e7fc99c2f4d11ca45eff652a81eb8c8085e9609298916dcf",
|
"sha256:74398a4cf19de42e1498368c36eed45d9528f5fd0155241e82c4082b7e16cffd",
|
||||||
"sha256:822ea70dc4018c7e6223f13affd1c5c30c0f5c12ac1f96cd8e9949acddb48a61",
|
"sha256:7938111ed1358f536daf311be244f34df7bf3cdedb3ed883787aca97778b28d8",
|
||||||
"sha256:84b0daf226913133f899ea9b30618722d45feffa67e4fe867b0b5ae83a34060c",
|
"sha256:82d92c773fbc6942a7a8b520d22c11cfc8fd83bba86116bfcf962c2f5c2ecdaa",
|
||||||
"sha256:85765fdf4b27eb5086f05ac0491090fc76f4f2b28e09d9350c31aac25a5aaff8",
|
"sha256:83b5c044f3eff2a6534768ccfd50425939e7a8b5cf9a7261c385de1e20dcfc85",
|
||||||
"sha256:8dd178c4c80706546702c59529ffc005681bd6dc2ea234c450661b205445a34d",
|
"sha256:8db8e423192303ed77cff4dce3a4b88dbfaf43979d280181558af5e2c3c71afc",
|
||||||
"sha256:8f5b234f567cf76ee489502ceb7165c2a5cecec081db2b37e35332b537f8157c",
|
"sha256:9517004e21664f2b5a5fd6333b0731b9cf0817403a941b393d89a2f1dc2bd836",
|
||||||
"sha256:98bbd754a422a0b123c66a4c341de0474cad4a5c10c164ceed6ea090f3563db4",
|
"sha256:95c02b0e27e706e48d0e5426d1710ca78e0f0628d6e89d5b5a5b91a5f12274f3",
|
||||||
"sha256:993584fc821c58d5993521bfdcd31a4adf025c7d745bbd4d12ccfecf695af5ba",
|
"sha256:99881222f4a8c2f641f25703963a5cefb076adffd959e0558dc9f803a52d6a58",
|
||||||
"sha256:a40821a89dc373d6427e2b44b572efc36a2778d3f543299e2f24eb1a5de65415",
|
"sha256:9ee32dcb8e531adae1f1ca568822e9b3a738369b3b686d1477cbc643c4a9c128",
|
||||||
"sha256:b291f0ee7961a597cbbcc77709374087fa2a9afe7bdb6a40dbbd9b127e79afee",
|
"sha256:a22e47578b30a3e199ab067a4d43d790249b3c0587d9a771921f86250c8435db",
|
||||||
"sha256:b573a43ef7c368ba4ea06050a957c2a7550f729c31f11dd616d2ac4aba99888d",
|
"sha256:b5505774ea2a73a86ea176e8a9a4a7c8bf5d521050f0f6f8426afe798689243f",
|
||||||
"sha256:b610ff0f24e9f11c9ae653c67ff8cc03c075131401b3e5ef4b82570d1728f8a9",
|
"sha256:bd739c9251d01e0279ce729e37b39d49a08c0420d3fee7f2a4968c0576678f77",
|
||||||
"sha256:bdf38ba2d393c7911ae989c3bbba510ebbcdf4ecbdbfec36272abe350c454075",
|
"sha256:d16a786905034e7e34098634b184a7d81f91d4c3d246edc6bd7aefb2fd8ea6ad",
|
||||||
"sha256:bfef2bb6ef068827bbd021017a107194956918ab43ce4d6dc945ffa13efbc25f",
|
"sha256:d3420522057ebab1728b21ad473aa950026d07cb09da41103f8e597dfbfaeb13",
|
||||||
"sha256:cab3db8bab4b7e635c1c97270d7a4b2a90c070b33cbc00c99ef3f9be03d3e1f7",
|
"sha256:d56fd9f1f1cdc8227d7b7918f55091349741904d9520c65f0139a9755952c9e8",
|
||||||
"sha256:cb70766519500281815dfd7a87d3a178acf7ce95390544b8c90587d76b227681",
|
"sha256:d661dc4785affa9d0edfdd1e59ec056a58b3dbb9f196fa43587f3ddac654ac7b",
|
||||||
"sha256:cca1b62fe70d761a282496b96a5e51c44c213e410a964bdffe0928e611368329",
|
"sha256:dfe1f0f0ed5785c187144c46a292b8c34c1295c01da12e10ccddfc16def4448a",
|
||||||
"sha256:ccf9a39706b604d884d2cb1e27fe973bc55f2890c52f38df742bc1d79ab9f5e1",
|
"sha256:e1dd7839443592d00e96db831eddb4111a2a81a46b028f0facd60a09ebbdd543",
|
||||||
"sha256:dc43f1ec66eb8440567186ae2f8c447d91e0372d793dfe8c222aec857b81a8cf",
|
"sha256:e2872993e209f7ed04d963e4b4fbae72d034844ec66bc4ca403329db2074377b",
|
||||||
"sha256:dd632777ff3beaaf629f1ab4396caf7ba0bdd075d948a69460d13d44357aca4c",
|
"sha256:e2f879ab92ce502a1e65fce390eab619774dda6a6ff719718069ac94084098ce",
|
||||||
"sha256:e45ae4927759289c30ccba8d9fdce62bb414977ba158286b5ddaf8df2cddb5c5",
|
"sha256:e3aa7e51d738e0ec0afbed661261513b38b3014754c9459508399baf14ae0c9d",
|
||||||
"sha256:e50ebce52f41370707f1e21a59514e3375e3edd6e1832f5e5235237db933c98b",
|
"sha256:e532dbd6ddfe13946de050d7474e3f5fb6ec774fbb1a188aaf469b08cf04189a",
|
||||||
"sha256:ebbbba226f0a108a7366bf4b59bf0f30a12fd5e75100c630267d94d7f0ad20e5",
|
"sha256:e6b7842518a63a9f17107eb176320960ec095a8ee3b4420b5f688e24bf50c53c",
|
||||||
"sha256:ec79ff6159dffcc30853b2ad612ed572af86c92b5168aa3fc01a67b0fa40665e",
|
"sha256:e75753aeda0ddc4c28dce4c32ba2f6ec30b1b02f6c0b14e547841ba5b24f753f",
|
||||||
"sha256:f0936e08e0003f66bfd97e74ee530427707297b0d0361247e9b4f59ab78ddc8b",
|
"sha256:eadb9f826c138e6cf3c49d6f8de88225a3c0ab181a9b4ba792e006e5292d150e",
|
||||||
"sha256:f26a07a6e877c76a88e3cecac8531908d980d3d5067ff69213653649ec0f60ad",
|
"sha256:ed59dd52075f8fc91da6053b12e8c89e37aa043f8986efd89e61fae69dc1b011",
|
||||||
"sha256:f64e376cd20d3f030190e8c32e1c64582eba56ac6dc7d5b0b49a9d44021b52fd",
|
"sha256:ef254a06bcea461e65ff0373d8a0dd1ed3aa004af48839f002a0c994a6f72d04",
|
||||||
"sha256:f6ffbc252eb0d229aeb2f9ad051200668fc3a9aaa8994e49f0cb2ffe2b7867e7",
|
"sha256:f3709997b228685fe53e8c433e2df9f0cdb5f4542bd5114ed17ac3c0129b0480",
|
||||||
"sha256:f9a7c509542db4eceed3dcf21ee5267ab565a83555c9b88a8109dcecc4709002",
|
"sha256:f51bab98d52739c50c56658cc303f190785f9a2cd97b823357e7aeae54c8f68a",
|
||||||
"sha256:ff1d0899f104f3921d94579a5638847f783c9b04f2d5f229392ca77fba5b82fc"
|
"sha256:f9904e24646570539a8950400602d66d2b2c492b9010ea7e965025cb71d0c86d",
|
||||||
|
"sha256:f9af38a89b6a5c04b7d18c492c8ccf2aee7048aff1ce8437c4683bb5a1df893d"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==1.0.7"
|
"version": "==1.0.8"
|
||||||
},
|
},
|
||||||
"mysqlclient": {
|
"mysqlclient": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1186,41 +1186,44 @@
|
|||||||
},
|
},
|
||||||
"pikepdf": {
|
"pikepdf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0586b9aec4f78a4f6ab2497b47639744ce47b7ad9b96e149194b7ada2d34b848",
|
"sha256:0cad56309f0ca9702682be2bb2403e34d02edbb08a669c5a655622d40bb8ca0a",
|
||||||
"sha256:0718f822dd75574e8c015983d870d8336fe7e62d20f1f726e86fc3156d62c921",
|
"sha256:0f342b02f03bc18e0fd24be90be8d54801d1cff8178be214620d8220e6a17e59",
|
||||||
"sha256:0c7b1404f9c7714377c3df7c756cff03ac237b2bf3273c1193d0a1849e1b83f0",
|
"sha256:157ecf18c43a8448bfbe4736d3e337f270e7deabf024497cbd087cccf06daf8f",
|
||||||
"sha256:119c18de07040cbbdf9f06a3a06fe89c8f321d3386fb96b2f68455bb2f7cab46",
|
"sha256:22ce91e8fbb3ab7eb066a560db407215abe1f486a07843ff8e84e1585236aa85",
|
||||||
"sha256:19867ff1c4db1da7a74aad0fc9455a6ca1b7a45ee460c1e7fce1dd1962c0251d",
|
"sha256:26378e4a9d61bf00d839b637957e16d187f1207c23fa2ed1e2873f6d2d0e9d58",
|
||||||
"sha256:238cce9d9bbda606eb7050096251753b32f84c758633212d62ed5f1ecf8f5091",
|
"sha256:2a4c22fe09b39c58fc0669794e4ea27fabe1470db2fdffd62ed9fbedf9bb4ad0",
|
||||||
"sha256:32b033345d0979e8a0dfd7c3af76013b3a3632da50d419545d012535f0cfd3e1",
|
"sha256:2bd4435bfc757c515dc6932900d5d2cce7d911d034eff797440c3d78fa36fe46",
|
||||||
"sha256:4802e48046a059b26949b16c53adb9ce8c0137a265cb8b1083957a3fadc0418b",
|
"sha256:32ad5105e84a8b5798bb41a05fb4dee033f10e31a074d6b1d095a5ca5985d05b",
|
||||||
"sha256:480c69fabe958b56843dc38953c8568a4da2cc819d88b3b1080a470c114e622a",
|
"sha256:32c4ba17e4792d2505ae21bd27d99b728eba2c6ad16a1a9cfc0b77b52f9b413d",
|
||||||
"sha256:485760bb22bcf25f338ab7dc003e6b5f522783f5f00bab0079ea3383152b2d98",
|
"sha256:3344d99381ea75cb6b5cff5a7c6544f5d3bfac6ea784b4c493467b3577449ad1",
|
||||||
"sha256:54eeaeaa4bdca7320511f00f403ea1ae33ef2e006fd5b5c2b92d92ab7eb77641",
|
"sha256:3bbd79c7cd6630361d83e75132aeaf3a64ceb837f82870bafdc210a31e3d917a",
|
||||||
"sha256:5f09c4263bfcad2ba9836b578c42218d6d8cf751331f5cc9639b68d60ab7a05d",
|
"sha256:3c24e413b7e9cc16d219c7892fed067894fc296c4408222f2fe015d04256bc10",
|
||||||
"sha256:711064313c00a5dccc5306877844f4e12b33355a9f496d870158d7f906330fd2",
|
"sha256:44cb27be26883d604193c49219185d2488cb2c270e61f785091bcebd887785d3",
|
||||||
"sha256:7209d3cb07d238ef81d438644363a19974697ab6c0d3360292c3c1cb4f2915e6",
|
"sha256:4c70b6f864f8b2a8cc248c395e7420a338fffd39a5fbd72f4497edc80958100e",
|
||||||
"sha256:73fdaa359262feda7f62aa87bfb97c66f9a30d80bcb592d2c4c5e7714f2ab30e",
|
"sha256:55e0c6f5fdd2cb5ca9dc0ada5064a02404bc03ff504b70a0686aedd74ee41d41",
|
||||||
"sha256:744903f2a34f8feeb781e3e5ca86b3ea60aae92246c9085392666eaa878ca7fc",
|
"sha256:61fd4c2204836ae04dbc1b1439a47083fe4b31c6e29622cc5b84f57426dd4a57",
|
||||||
"sha256:75de10328d6558e7821aadabb63cadbb40eec3cdc407b01ef990760343ad4305",
|
"sha256:6504db23601710ab7e80b5ade84d97440532c156ce0f8bdbfafcbb0a806018df",
|
||||||
"sha256:78f0d79548e56636deb4749fb3fa73458dcdf86e634a1ce1b6e5155f97c4155f",
|
"sha256:65d89d269c4e43e00d4de1db7313cb6813a0c88ce89a4b60d84c414cb8cd17d2",
|
||||||
"sha256:7fd20e259c2192ca1b718d8f78bbeff1b37340b6a2c9764068c2112449895d85",
|
"sha256:6ac4d03a0f7f7b982169af828ab702914a5f6fd947ead1cb400c9091c384c2c2",
|
||||||
"sha256:8bc82d766f09fafd986d9410c7d53345b3e2963fe3253a9ba09b50812b60e9c4",
|
"sha256:76cc1941d0df4f70e41db5548d557be9131161bd29833b86158e7ba725e592c1",
|
||||||
"sha256:93ba53c05d511a5e8a1d5ccf00b71a3c89e9285ec92e8f9a86995b022e667346",
|
"sha256:80eb3fa37bc18d8481bc41840cfb7dcf2aa588d648205cc4b539796d7bd5a543",
|
||||||
"sha256:98c04283649b09c8c8d5f18389787dfe48224a3b99758d984ace9c1f1940dd33",
|
"sha256:88f354c4495dff95f99af284d00af4aa12060bc5db334946deb969ee99a4f9a6",
|
||||||
"sha256:9b2f8f518d7d4ee374deccc27cb65494c33cbc600885253e4454ab31dc466a04",
|
"sha256:8cc9bbcd13d99a3cd59a58c4f40f601124e62ada32914be89d4aa96b36475779",
|
||||||
"sha256:9f2cff2ed901b5dd6ea704a24e88dfb9e3303019473ffc224d4d72a294e7c43b",
|
"sha256:99e99d638466258e97de4cac07ab7f624fbf9b06b52dda23829e2aea07e449f3",
|
||||||
"sha256:b2d9957e188785a5913deedf6f12dc7eafb5764a3cc3996e044f0e9044b28889",
|
"sha256:9d3741bc7f325a445045894379a6450535eb906ccdb2875671318832a98f4792",
|
||||||
"sha256:b879e240b4d4b34822a0390fe4f30e6388d0f776e12376f043dc102fe11e9499",
|
"sha256:a3aab6e35819a8242ee81c4e62d1b39cfa085ace533ab921f45865fb02bed3c8",
|
||||||
"sha256:b8a91610cf07d29ef06992cdfae902fecef3712100f25796c553e1e3a0553a33",
|
"sha256:a3e1454a8346a98b8296a7e91cf23cb8f0947bcc1ff50ec6c086ec31225cfb46",
|
||||||
"sha256:c1e13a4f4c7a596ad4157b93453de4dfcb7abc14c58ce9ee110b0e2ee7a0813e",
|
"sha256:a54f2e40054ae8d6931552b41f721909b6c2efbc9f04e09ba549f65374919d57",
|
||||||
"sha256:c60e8b28b9e96137adc79eade42a764ff508291270e99ac84290808a85a12d94",
|
"sha256:bde6a1ad51b82edd2e627e50a83caf3b8e280cfdfa4f35ffc740eaad76f66772",
|
||||||
"sha256:d9ee83f20559c63413fed0059ea159a11116af098f976b8480e82018d954bba9",
|
"sha256:bf07f10682d1636a55230c01e1a7f8d6ab3ad189df1341fd5554fc978c219f0e",
|
||||||
"sha256:ea368bb11c1b57b773f349e8a1283617a66227a628f94023bb4a16ed53a1adf3",
|
"sha256:c40f5ab91151fe29020d0ad9ceabe194fea35276276c6ee7ee73038d55b2af77",
|
||||||
"sha256:fc8dcbdb7ba95ef1e8318eb2ebd2869d53e9e9f8b81d9d7b7b5f9acbc4e6557b"
|
"sha256:c8a21a0f96e0071651de90bf5c751513c88b6d77bc5a097a3eefa3ad5283d5ee",
|
||||||
|
"sha256:e83d4c4d1a537a77537db471f718a18901e58830638878e329848f2c796717af",
|
||||||
|
"sha256:ee3f79165fea8e443609925b457fc1eec438e7cac317a57e5c1d7409e2db92a3",
|
||||||
|
"sha256:f933dc6343c8a2aa01a8ed61f9961428e1958ada13c828418010c0b3e763663c"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==8.12.0"
|
"version": "==8.13.0"
|
||||||
},
|
},
|
||||||
"pillow": {
|
"pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1314,11 +1317,11 @@
|
|||||||
},
|
},
|
||||||
"prometheus-client": {
|
"prometheus-client": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4585b0d1223148c27a225b10dbec5ae9bc4c81a99a3fa80774fa6209935324e1",
|
"sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89",
|
||||||
"sha256:c88b1e6ecf6b41cd8fb5731c7ae919bf66df6ec6fafa555cd6c0e16ca169ae92"
|
"sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==0.19.0"
|
"version": "==0.20.0"
|
||||||
},
|
},
|
||||||
"prompt-toolkit": {
|
"prompt-toolkit": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1376,12 +1379,12 @@
|
|||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
|
||||||
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.8.2"
|
"version": "==2.9.0.post0"
|
||||||
},
|
},
|
||||||
"python-dotenv": {
|
"python-dotenv": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1402,12 +1405,12 @@
|
|||||||
},
|
},
|
||||||
"python-ipware": {
|
"python-ipware": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1992920ef553165dfa35e6ea5a90762f64f3b943cc22ee6b4ec02e2c86d31178",
|
"sha256:3a94fd073b93e12b13617e291f13eda3495d3ba68b580e3e30174ea84ac63041",
|
||||||
"sha256:9ba4805152ebb85ad5b53797185cd1ce6231e1db60155834f326c8cd61e8af34"
|
"sha256:86e30cc3af62cec42284dedd49c8a14e436e73c96433c8645ec0b476ff4ad7ec"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==2.0.1"
|
"version": "==2.0.2"
|
||||||
},
|
},
|
||||||
"python-magic": {
|
"python-magic": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1599,11 +1602,11 @@
|
|||||||
"hiredis"
|
"hiredis"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0dab495cd5753069d3bc650a0dde8a8f9edde16fc5691b689a566eda58100d0f",
|
"sha256:3f82cc80d350e93042c8e6e7a5d0596e4dd68715babffba79492733e1f367037",
|
||||||
"sha256:ed4802971884ae19d640775ba3b03aa2e7bd5e8fb8dfaed2decce4d0fc48391f"
|
"sha256:4caa8e1fcb6f3c0ef28dba99535101d80934b7d4cd541bbb47f4a3826ee472d1"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==5.0.1"
|
"version": "==5.0.2"
|
||||||
},
|
},
|
||||||
"regex": {
|
"regex": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1706,11 +1709,11 @@
|
|||||||
},
|
},
|
||||||
"reportlab": {
|
"reportlab": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c9656216321897486e323be138f7aea67851cedc116b8cc35f8ec7f8cc763538",
|
"sha256:28a40d5000afbd8ccae15a47f7abe2841768461354bede1a9d42841132997c98",
|
||||||
"sha256:f32bff66a0fda234202e1e33eaf77f25008871a61cb01cd91584a521a04c0047"
|
"sha256:3a99faf412691159c068b3ff01c15307ce2fd2cf6b860199434874e002040a84"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7' and python_version < '4'",
|
"markers": "python_version >= '3.7' and python_version < '4'",
|
||||||
"version": "==4.0.9"
|
"version": "==4.1.0"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1730,11 +1733,11 @@
|
|||||||
},
|
},
|
||||||
"rich": {
|
"rich": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa",
|
"sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222",
|
||||||
"sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"
|
"sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.7.0'",
|
"markers": "python_full_version >= '3.7.0'",
|
||||||
"version": "==13.7.0"
|
"version": "==13.7.1"
|
||||||
},
|
},
|
||||||
"scikit-learn": {
|
"scikit-learn": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1900,11 +1903,11 @@
|
|||||||
},
|
},
|
||||||
"sniffio": {
|
"sniffio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101",
|
"sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2",
|
||||||
"sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"
|
"sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==1.3.0"
|
"version": "==1.3.1"
|
||||||
},
|
},
|
||||||
"sqlparse": {
|
"sqlparse": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1959,19 +1962,19 @@
|
|||||||
},
|
},
|
||||||
"typing-extensions": {
|
"typing-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783",
|
"sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
|
||||||
"sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"
|
"sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.11'",
|
"markers": "python_version < '3.11'",
|
||||||
"version": "==4.9.0"
|
"version": "==4.10.0"
|
||||||
},
|
},
|
||||||
"tzdata": {
|
"tzdata": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3",
|
"sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd",
|
||||||
"sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"
|
"sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '2'",
|
"markers": "python_version >= '2'",
|
||||||
"version": "==2023.4"
|
"version": "==2024.1"
|
||||||
},
|
},
|
||||||
"tzlocal": {
|
"tzlocal": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -1983,11 +1986,11 @@
|
|||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20",
|
"sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
|
||||||
"sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"
|
"sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.2.0"
|
"version": "==2.2.1"
|
||||||
},
|
},
|
||||||
"uvicorn": {
|
"uvicorn": {
|
||||||
"extras": [
|
"extras": [
|
||||||
@ -2705,61 +2708,61 @@
|
|||||||
"toml"
|
"toml"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0193657651f5399d433c92f8ae264aff31fc1d066deee4b831549526433f3f61",
|
"sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa",
|
||||||
"sha256:02f2edb575d62172aa28fe00efe821ae31f25dc3d589055b3fb64d51e52e4ab1",
|
"sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003",
|
||||||
"sha256:0491275c3b9971cdbd28a4595c2cb5838f08036bca31765bad5e17edf900b2c7",
|
"sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f",
|
||||||
"sha256:077d366e724f24fc02dbfe9d946534357fda71af9764ff99d73c3c596001bbd7",
|
"sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c",
|
||||||
"sha256:10e88e7f41e6197ea0429ae18f21ff521d4f4490aa33048f6c6f94c6045a6a75",
|
"sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e",
|
||||||
"sha256:18e961aa13b6d47f758cc5879383d27b5b3f3dcd9ce8cdbfdc2571fe86feb4dd",
|
"sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0",
|
||||||
"sha256:1a78b656a4d12b0490ca72651fe4d9f5e07e3c6461063a9b6265ee45eb2bdd35",
|
"sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9",
|
||||||
"sha256:1ed4b95480952b1a26d863e546fa5094564aa0065e1e5f0d4d0041f293251d04",
|
"sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52",
|
||||||
"sha256:23b27b8a698e749b61809fb637eb98ebf0e505710ec46a8aa6f1be7dc0dc43a6",
|
"sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e",
|
||||||
"sha256:23f5881362dcb0e1a92b84b3c2809bdc90db892332daab81ad8f642d8ed55042",
|
"sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454",
|
||||||
"sha256:32a8d985462e37cfdab611a6f95b09d7c091d07668fdc26e47a725ee575fe166",
|
"sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0",
|
||||||
"sha256:3468cc8720402af37b6c6e7e2a9cdb9f6c16c728638a2ebc768ba1ef6f26c3a1",
|
"sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079",
|
||||||
"sha256:379d4c7abad5afbe9d88cc31ea8ca262296480a86af945b08214eb1a556a3e4d",
|
"sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352",
|
||||||
"sha256:3cacfaefe6089d477264001f90f55b7881ba615953414999c46cc9713ff93c8c",
|
"sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f",
|
||||||
"sha256:3e3424c554391dc9ef4a92ad28665756566a28fecf47308f91841f6c49288e66",
|
"sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30",
|
||||||
"sha256:46342fed0fff72efcda77040b14728049200cbba1279e0bf1188f1f2078c1d70",
|
"sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe",
|
||||||
"sha256:536d609c6963c50055bab766d9951b6c394759190d03311f3e9fcf194ca909e1",
|
"sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113",
|
||||||
"sha256:5d6850e6e36e332d5511a48a251790ddc545e16e8beaf046c03985c69ccb2676",
|
"sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765",
|
||||||
"sha256:6008adeca04a445ea6ef31b2cbaf1d01d02986047606f7da266629afee982630",
|
"sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc",
|
||||||
"sha256:64e723ca82a84053dd7bfcc986bdb34af8d9da83c521c19d6b472bc6880e191a",
|
"sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e",
|
||||||
"sha256:6b00e21f86598b6330f0019b40fb397e705135040dbedc2ca9a93c7441178e74",
|
"sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501",
|
||||||
"sha256:6d224f0c4c9c98290a6990259073f496fcec1b5cc613eecbd22786d398ded3ad",
|
"sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7",
|
||||||
"sha256:6dceb61d40cbfcf45f51e59933c784a50846dc03211054bd76b421a713dcdf19",
|
"sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2",
|
||||||
"sha256:7ac8f8eb153724f84885a1374999b7e45734bf93a87d8df1e7ce2146860edef6",
|
"sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f",
|
||||||
"sha256:85ccc5fa54c2ed64bd91ed3b4a627b9cce04646a659512a051fa82a92c04a448",
|
"sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4",
|
||||||
"sha256:869b5046d41abfea3e381dd143407b0d29b8282a904a19cb908fa24d090cc018",
|
"sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524",
|
||||||
"sha256:8bdb0285a0202888d19ec6b6d23d5990410decb932b709f2b0dfe216d031d218",
|
"sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c",
|
||||||
"sha256:8dfc5e195bbef80aabd81596ef52a1277ee7143fe419efc3c4d8ba2754671756",
|
"sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51",
|
||||||
"sha256:8e738a492b6221f8dcf281b67129510835461132b03024830ac0e554311a5c54",
|
"sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840",
|
||||||
"sha256:918440dea04521f499721c039863ef95433314b1db00ff826a02580c1f503e45",
|
"sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6",
|
||||||
"sha256:9641e21670c68c7e57d2053ddf6c443e4f0a6e18e547e86af3fad0795414a628",
|
"sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee",
|
||||||
"sha256:9d2f9d4cc2a53b38cabc2d6d80f7f9b7e3da26b2f53d48f05876fef7956b6968",
|
"sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e",
|
||||||
"sha256:a07f61fc452c43cd5328b392e52555f7d1952400a1ad09086c4a8addccbd138d",
|
"sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45",
|
||||||
"sha256:a3277f5fa7483c927fe3a7b017b39351610265308f5267ac6d4c2b64cc1d8d25",
|
"sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba",
|
||||||
"sha256:a4a3907011d39dbc3e37bdc5df0a8c93853c369039b59efa33a7b6669de04c60",
|
"sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d",
|
||||||
"sha256:aeb2c2688ed93b027eb0d26aa188ada34acb22dceea256d76390eea135083950",
|
"sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3",
|
||||||
"sha256:b094116f0b6155e36a304ff912f89bbb5067157aff5f94060ff20bbabdc8da06",
|
"sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10",
|
||||||
"sha256:b8ffb498a83d7e0305968289441914154fb0ef5d8b3157df02a90c6695978295",
|
"sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e",
|
||||||
"sha256:b9bb62fac84d5f2ff523304e59e5c439955fb3b7f44e3d7b2085184db74d733b",
|
"sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb",
|
||||||
"sha256:c61f66d93d712f6e03369b6a7769233bfda880b12f417eefdd4f16d1deb2fc4c",
|
"sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9",
|
||||||
"sha256:ca6e61dc52f601d1d224526360cdeab0d0712ec104a2ce6cc5ccef6ed9a233bc",
|
"sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a",
|
||||||
"sha256:ca7b26a5e456a843b9b6683eada193fc1f65c761b3a473941efe5a291f604c74",
|
"sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47",
|
||||||
"sha256:d12c923757de24e4e2110cf8832d83a886a4cf215c6e61ed506006872b43a6d1",
|
"sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1",
|
||||||
"sha256:d17bbc946f52ca67adf72a5ee783cd7cd3477f8f8796f59b4974a9b59cacc9ee",
|
"sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3",
|
||||||
"sha256:dfd1e1b9f0898817babf840b77ce9fe655ecbe8b1b327983df485b30df8cc011",
|
"sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914",
|
||||||
"sha256:e0860a348bf7004c812c8368d1fc7f77fe8e4c095d661a579196a9533778e156",
|
"sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328",
|
||||||
"sha256:f2f5968608b1fe2a1d00d01ad1017ee27efd99b3437e08b83ded9b7af3f6f766",
|
"sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6",
|
||||||
"sha256:f3771b23bb3675a06f5d885c3630b1d01ea6cac9e84a01aaf5508706dba546c5",
|
"sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d",
|
||||||
"sha256:f68ef3660677e6624c8cace943e4765545f8191313a07288a53d3da188bd8581",
|
"sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0",
|
||||||
"sha256:f86f368e1c7ce897bf2457b9eb61169a44e2ef797099fb5728482b8d69f3f016",
|
"sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94",
|
||||||
"sha256:f90515974b39f4dea2f27c0959688621b46d96d5a626cf9c53dbc653a895c05c",
|
"sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc",
|
||||||
"sha256:fe558371c1bdf3b8fa03e097c523fb9645b8730399c14fe7721ee9c9e2a545d3"
|
"sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==7.4.1"
|
"version": "==7.4.3"
|
||||||
},
|
},
|
||||||
"cryptography": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -2842,17 +2845,18 @@
|
|||||||
},
|
},
|
||||||
"faker": {
|
"faker": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:60e89e5c0b584e285a7db05eceba35011a241954afdab2853cb246c8a56700a2",
|
"sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1",
|
||||||
"sha256:b7f76bb1b2ac4cdc54442d955e36e477c387000f31ce46887fb9722a041be60b"
|
"sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==23.1.0"
|
"version": "==24.0.0"
|
||||||
},
|
},
|
||||||
"filelock": {
|
"filelock": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
|
"sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e",
|
||||||
"sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
|
"sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"
|
||||||
],
|
],
|
||||||
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==3.13.1"
|
"version": "==3.13.1"
|
||||||
},
|
},
|
||||||
@ -2887,7 +2891,7 @@
|
|||||||
"sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5",
|
"sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5",
|
||||||
"sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"
|
"sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.9'",
|
||||||
"version": "==0.27.0"
|
"version": "==0.27.0"
|
||||||
},
|
},
|
||||||
"hyperlink": {
|
"hyperlink": {
|
||||||
@ -3052,12 +3056,12 @@
|
|||||||
},
|
},
|
||||||
"mkdocs-material": {
|
"mkdocs-material": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:788ee0f3e036dca2dc20298d65e480297d348a44c9d7b2ee05c5262983e66072",
|
"sha256:5f69cef6a8aaa4050b812f72b1094fda3d079b9a51cf27a247244c03ec455e97",
|
||||||
"sha256:7af7f8af0dea16175558f3fb9245d26c83a17199baa5f157755e63d7437bf971"
|
"sha256:d6f0c269f015e48c76291cdc79efb70f7b33bbbf42d649cfe475522ebee61b1f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==9.5.11"
|
"version": "==9.5.12"
|
||||||
},
|
},
|
||||||
"mkdocs-material-extensions": {
|
"mkdocs-material-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3365,11 +3369,12 @@
|
|||||||
},
|
},
|
||||||
"python-dateutil": {
|
"python-dateutil": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
|
"sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
|
||||||
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
|
"sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
|
||||||
],
|
],
|
||||||
|
"index": "pypi",
|
||||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.8.2"
|
"version": "==2.9.0.post0"
|
||||||
},
|
},
|
||||||
"pywavelets": {
|
"pywavelets": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3456,7 +3461,6 @@
|
|||||||
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
|
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
|
||||||
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
|
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
|
||||||
"version": "==6.0.1"
|
"version": "==6.0.1"
|
||||||
},
|
},
|
||||||
"pyyaml-env-tag": {
|
"pyyaml-env-tag": {
|
||||||
@ -3576,27 +3580,27 @@
|
|||||||
},
|
},
|
||||||
"ruff": {
|
"ruff": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6",
|
"sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a",
|
||||||
"sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e",
|
"sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f",
|
||||||
"sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c",
|
"sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b",
|
||||||
"sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9",
|
"sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77",
|
||||||
"sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e",
|
"sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb",
|
||||||
"sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3",
|
"sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932",
|
||||||
"sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba",
|
"sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933",
|
||||||
"sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001",
|
"sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2",
|
||||||
"sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726",
|
"sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944",
|
||||||
"sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e",
|
"sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a",
|
||||||
"sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd",
|
"sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49",
|
||||||
"sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d",
|
"sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4",
|
||||||
"sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39",
|
"sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e",
|
||||||
"sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325",
|
"sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e",
|
||||||
"sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d",
|
"sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f",
|
||||||
"sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73",
|
"sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19",
|
||||||
"sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"
|
"sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==0.2.2"
|
"version": "==0.3.0"
|
||||||
},
|
},
|
||||||
"scipy": {
|
"scipy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3681,11 +3685,11 @@
|
|||||||
"tls"
|
"tls"
|
||||||
],
|
],
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4ae8bce12999a35f7fe6443e7f1893e6fe09588c8d2bed9c35cdce8ff2d5b444",
|
"sha256:039f2e6a49ab5108abd94de187fa92377abe5985c7a72d68d0ad266ba19eae63",
|
||||||
"sha256:987847a0790a2c597197613686e2784fd54167df3a55d0fb17c8412305d76ce5"
|
"sha256:6b38b6ece7296b5e122c9eb17da2eeab3d98a198f50ca9efd00fb03e5b4fd4ae"
|
||||||
],
|
],
|
||||||
"markers": "python_full_version >= '3.8.0'",
|
"markers": "python_full_version >= '3.8.0'",
|
||||||
"version": "==23.10.0"
|
"version": "==24.3.0"
|
||||||
},
|
},
|
||||||
"txaio": {
|
"txaio": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -3751,6 +3755,7 @@
|
|||||||
"sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245",
|
"sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245",
|
||||||
"sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"
|
"sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"
|
||||||
],
|
],
|
||||||
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==4.0.0"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
@ -3987,50 +3992,50 @@
|
|||||||
},
|
},
|
||||||
"cryptography": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380",
|
"sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee",
|
||||||
"sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589",
|
"sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576",
|
||||||
"sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea",
|
"sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d",
|
||||||
"sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65",
|
"sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30",
|
||||||
"sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a",
|
"sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413",
|
||||||
"sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3",
|
"sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb",
|
||||||
"sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008",
|
"sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da",
|
||||||
"sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1",
|
"sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4",
|
||||||
"sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2",
|
"sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd",
|
||||||
"sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635",
|
"sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc",
|
||||||
"sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2",
|
"sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8",
|
||||||
"sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90",
|
"sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1",
|
||||||
"sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee",
|
"sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc",
|
||||||
"sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a",
|
"sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e",
|
||||||
"sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242",
|
"sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8",
|
||||||
"sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12",
|
"sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940",
|
||||||
"sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2",
|
"sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400",
|
||||||
"sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d",
|
"sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7",
|
||||||
"sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be",
|
"sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16",
|
||||||
"sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee",
|
"sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278",
|
||||||
"sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6",
|
"sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74",
|
||||||
"sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529",
|
"sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec",
|
||||||
"sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929",
|
"sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1",
|
||||||
"sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1",
|
"sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2",
|
||||||
"sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6",
|
"sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c",
|
||||||
"sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a",
|
"sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922",
|
||||||
"sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446",
|
"sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a",
|
||||||
"sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9",
|
"sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6",
|
||||||
"sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888",
|
"sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1",
|
||||||
"sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4",
|
"sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e",
|
||||||
"sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33",
|
"sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac",
|
||||||
"sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"
|
"sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==42.0.2"
|
"version": "==42.0.5"
|
||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1",
|
"sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4",
|
||||||
"sha256:b1260ed381b10a11753c73444408e19869f3241fc45c985cd55a30177c789d13"
|
"sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==4.2.10"
|
"version": "==4.2.11"
|
||||||
},
|
},
|
||||||
"django-filter-stubs": {
|
"django-filter-stubs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -4153,12 +4158,12 @@
|
|||||||
},
|
},
|
||||||
"types-bleach": {
|
"types-bleach": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e43c437e734a90efe4f40ebfe831057599568d3b275939ffbd6094848a18a27",
|
"sha256:1299f06b5ef80e2d4f25fac11f613033c9bc35bad116413cb320d0c0c1188466",
|
||||||
"sha256:f83f80e0709f13d809a9c79b958a1089df9b99e68059287beb196e38967e4ddf"
|
"sha256:78a1c39484c8949030a0931076eeb69b03a0e08a4fafec5b3764e142929859d4"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==6.1.0.1"
|
"version": "==6.1.0.20240222"
|
||||||
},
|
},
|
||||||
"types-colorama": {
|
"types-colorama": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -4180,11 +4185,19 @@
|
|||||||
},
|
},
|
||||||
"types-docutils": {
|
"types-docutils": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:79d3bcef235f7c81a63f4f3dcf1d0b138985079bb32d02f5a7d266e1f9f361ba",
|
"sha256:c35ae35ca835a5aeead758df411cd46cfb7e7f19f2b223c413dae7e069d5b0be",
|
||||||
"sha256:ba4bfd4ff6dd19640ba7ab5d93900393a65897880f3650997964a943f4e79a6b"
|
"sha256:ef02f9d05f2b61500638b1358cdf3fbf975cc5dedaa825a2eb5ea71b7318a760"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==0.20.0.20240201"
|
"version": "==0.20.0.20240304"
|
||||||
|
},
|
||||||
|
"types-html5lib": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:22736b7299e605ec4ba539d48691e905fd0c61c3ea610acc59922232dc84cede",
|
||||||
|
"sha256:af5de0125cb0fe5667543b158db83849b22e25c0e36c9149836b095548bf1020"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.8'",
|
||||||
|
"version": "==1.1.11.20240228"
|
||||||
},
|
},
|
||||||
"types-markdown": {
|
"types-markdown": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -4197,21 +4210,21 @@
|
|||||||
},
|
},
|
||||||
"types-pillow": {
|
"types-pillow": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:abc339ae28af5916146a7729261480d68ac902cd4ff57e0bdd402eee7962644d",
|
"sha256:062c5a0f20301a30f2df4db583f15b3c2a1283a12518d1f9d81396154e12c1af",
|
||||||
"sha256:f0de5107ff8362ffdbbd53ec896202ac905e6ab22ae784b46bcdad160ea143b9"
|
"sha256:4800b61bf7eabdae2f1b17ade0d080709ed33e9f26a2e900e470e8b56ebe2387"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==10.2.0.20240206"
|
"version": "==10.2.0.20240213"
|
||||||
},
|
},
|
||||||
"types-psycopg2": {
|
"types-psycopg2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:75a92735f62ba36397409ae4758f0241cbc4a9bb30f1f95d3b4a660fea7e7711",
|
"sha256:3084cd807038a62c80fb5be78b41d855b48a060316101ea59fd85c302efb57d4",
|
||||||
"sha256:b05cf5d7ce0bd460319f6399fb358307e5af109bfcb6ed7b3d63c9ebec6b9be6"
|
"sha256:cac96264e063cbce28dee337a973d39e6df4ca671252343cb4f8e5ef6db5e67d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.9.21.20240201"
|
"version": "==2.9.21.20240218"
|
||||||
},
|
},
|
||||||
"types-pygments": {
|
"types-pygments": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -4224,11 +4237,11 @@
|
|||||||
},
|
},
|
||||||
"types-pyopenssl": {
|
"types-pyopenssl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:24a255458b5b8a7fca8139cf56f2a8ad5a4f1a5f711b73a5bb9cb50dc688fab5",
|
"sha256:a472cf877a873549175e81972f153f44e975302a3cf17381eb5f3d41ccfb75a4",
|
||||||
"sha256:c812e5c1c35249f75ef5935708b2a997d62abf9745be222e5f94b9595472ab25"
|
"sha256:cd990717d8aa3743ef0e73e0f462e64b54d90c304249232d48fece4f0f7c3c6a"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==24.0.0.20240130"
|
"version": "==24.0.0.20240228"
|
||||||
},
|
},
|
||||||
"types-python-dateutil": {
|
"types-python-dateutil": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -4256,29 +4269,29 @@
|
|||||||
},
|
},
|
||||||
"types-redis": {
|
"types-redis": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2b2fa3a78f84559616242d23f86de5f4130dfd6c3b83fb2d8ce3329e503f756e",
|
"sha256:5103d7e690e5c74c974a161317b2d59ac2303cf8bef24175b04c2a4c3486cb39",
|
||||||
"sha256:912de6507b631934bd225cdac310b04a58def94391003ba83939e5a10e99568d"
|
"sha256:dc9c45a068240e33a04302aec5655cf41e80f91eecffccbb2df215b2f6fc375d"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==4.6.0.20240106"
|
"version": "==4.6.0.20240218"
|
||||||
},
|
},
|
||||||
"types-requests": {
|
"types-requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5",
|
"sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b",
|
||||||
"sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"
|
"sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.31.0.20240125"
|
"version": "==2.31.0.20240218"
|
||||||
},
|
},
|
||||||
"types-setuptools": {
|
"types-setuptools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00835f959ff24ebc32c55da8df9d46e8df25e3c4bfacb43e98b61fde51a4bc41",
|
"sha256:99c1053920a6fa542b734c9ad61849c3993062f80963a4034771626528e192a0",
|
||||||
"sha256:22ad498cb585b22ce8c97ada1fccdf294a2e0dd7dc984a28535a84ea82f45b3f"
|
"sha256:ed5462cf8470831d1bdbf300e1eeea876040643bfc40b785109a5857fa7d3c3f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==69.0.0.20240125"
|
"version": "==69.1.0.20240302"
|
||||||
},
|
},
|
||||||
"types-tqdm": {
|
"types-tqdm": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -4291,19 +4304,19 @@
|
|||||||
},
|
},
|
||||||
"typing-extensions": {
|
"typing-extensions": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783",
|
"sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475",
|
||||||
"sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"
|
"sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '3.11'",
|
"markers": "python_version < '3.11'",
|
||||||
"version": "==4.9.0"
|
"version": "==4.10.0"
|
||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20",
|
"sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d",
|
||||||
"sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"
|
"sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.8'",
|
"markers": "python_version >= '3.8'",
|
||||||
"version": "==2.2.0"
|
"version": "==2.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -437,7 +437,7 @@ with Prometheus, as it exports metrics. For details on its capabilities,
|
|||||||
refer to the [Flower](https://flower.readthedocs.io/en/latest/index.html)
|
refer to the [Flower](https://flower.readthedocs.io/en/latest/index.html)
|
||||||
documentation.
|
documentation.
|
||||||
|
|
||||||
Flower can be enabled with the setting [PAPERLESS_ENABLE_FLOWER](configuration/#PAPERLESS_ENABLE_FLOWER).
|
Flower can be enabled with the setting [PAPERLESS_ENABLE_FLOWER](configuration.md#PAPERLESS_ENABLE_FLOWER).
|
||||||
To configure Flower further, create a `flowerconfig.py` and
|
To configure Flower further, create a `flowerconfig.py` and
|
||||||
place it into the `src/paperless` directory. For a Docker
|
place it into the `src/paperless` directory. For a Docker
|
||||||
installation, you can use volumes to accomplish this:
|
installation, you can use volumes to accomplish this:
|
||||||
|
@ -1,5 +1,98 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## paperless-ngx 2.6.0
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Feature: Allow user to control PIL image pixel limit [@stumpylog](https://github.com/stumpylog) ([#5997](https://github.com/paperless-ngx/paperless-ngx/pull/5997))
|
||||||
|
- Feature: Allow a user to disable the pixel limit for OCR entirely [@stumpylog](https://github.com/stumpylog) ([#5996](https://github.com/paperless-ngx/paperless-ngx/pull/5996))
|
||||||
|
- Feature: workflow removal action [@shamoon](https://github.com/shamoon) ([#5928](https://github.com/paperless-ngx/paperless-ngx/pull/5928))
|
||||||
|
- Feature: system status [@shamoon](https://github.com/shamoon) ([#5743](https://github.com/paperless-ngx/paperless-ngx/pull/5743))
|
||||||
|
- Enhancement: better monetary field with currency code [@shamoon](https://github.com/shamoon) ([#5858](https://github.com/paperless-ngx/paperless-ngx/pull/5858))
|
||||||
|
- Enhancement: support disabling regular login [@shamoon](https://github.com/shamoon) ([#5816](https://github.com/paperless-ngx/paperless-ngx/pull/5816))
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Fix: refactor base path settings, correct logout redirect [@shamoon](https://github.com/shamoon) ([#5976](https://github.com/paperless-ngx/paperless-ngx/pull/5976))
|
||||||
|
- Fix: always pass from UI, dont require in API [@shamoon](https://github.com/shamoon) ([#5962](https://github.com/paperless-ngx/paperless-ngx/pull/5962))
|
||||||
|
- Fix: Clear metadata cache when the filename(s) change [@stumpylog](https://github.com/stumpylog) ([#5957](https://github.com/paperless-ngx/paperless-ngx/pull/5957))
|
||||||
|
- Fix: include monetary, float and doc link values in search filters [@shamoon](https://github.com/shamoon) ([#5951](https://github.com/paperless-ngx/paperless-ngx/pull/5951))
|
||||||
|
- Fix: Better handling of a corrupted index [@stumpylog](https://github.com/stumpylog) ([#5950](https://github.com/paperless-ngx/paperless-ngx/pull/5950))
|
||||||
|
- Fix: Don't assume the location of scratch directory in Docker [@stumpylog](https://github.com/stumpylog) ([#5948](https://github.com/paperless-ngx/paperless-ngx/pull/5948))
|
||||||
|
- Fix: ensure document title always limited to 128 chars [@shamoon](https://github.com/shamoon) ([#5934](https://github.com/paperless-ngx/paperless-ngx/pull/5934))
|
||||||
|
- Fix: use for password reset emails, if set [@shamoon](https://github.com/shamoon) ([#5902](https://github.com/paperless-ngx/paperless-ngx/pull/5902))
|
||||||
|
- Fix: Correct docker compose check in install script [@ShanSanear](https://github.com/ShanSanear) ([#5917](https://github.com/paperless-ngx/paperless-ngx/pull/5917))
|
||||||
|
- Fix: respect global permissions for UI settings [@shamoon](https://github.com/shamoon) ([#5919](https://github.com/paperless-ngx/paperless-ngx/pull/5919))
|
||||||
|
- Fix: allow disable email verification during signup [@shamoon](https://github.com/shamoon) ([#5895](https://github.com/paperless-ngx/paperless-ngx/pull/5895))
|
||||||
|
- Fix: refactor accounts templates and create signup template [@shamoon](https://github.com/shamoon) ([#5899](https://github.com/paperless-ngx/paperless-ngx/pull/5899))
|
||||||
|
|
||||||
|
### Maintenance
|
||||||
|
|
||||||
|
- Chore(deps): Bump the actions group with 3 updates [@dependabot](https://github.com/dependabot) ([#5907](https://github.com/paperless-ngx/paperless-ngx/pull/5907))
|
||||||
|
- Chore: Ignores uvicorn updates in dependabot [@stumpylog](https://github.com/stumpylog) ([#5906](https://github.com/paperless-ngx/paperless-ngx/pull/5906))
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>15 changes</summary>
|
||||||
|
|
||||||
|
- Chore(deps): Bump the small-changes group with 3 updates [@dependabot](https://github.com/dependabot) ([#6001](https://github.com/paperless-ngx/paperless-ngx/pull/6001))
|
||||||
|
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5998](https://github.com/paperless-ngx/paperless-ngx/pull/5998))
|
||||||
|
- Chore(deps): Bump the django group with 1 update [@dependabot](https://github.com/dependabot) ([#6000](https://github.com/paperless-ngx/paperless-ngx/pull/6000))
|
||||||
|
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.41.2 to 1.42.0 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.41.2 to 1.42.0 in /src-ui @dependabot) ([#5964](https://github.com/paperless-ngx/paperless-ngx/pull/5964))
|
||||||
|
- Chore(deps-dev): Bump [@<!---->types/node from 20.11.20 to 20.11.24 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.11.20 to 20.11.24 in /src-ui @dependabot) ([#5965](https://github.com/paperless-ngx/paperless-ngx/pull/5965))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 11 updates [@dependabot](https://github.com/dependabot) ([#5963](https://github.com/paperless-ngx/paperless-ngx/pull/5963))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5918](https://github.com/paperless-ngx/paperless-ngx/pull/5918))
|
||||||
|
- Chore(deps-dev): Bump [@<!---->types/node from 20.11.16 to 20.11.20 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.11.16 to 20.11.20 in /src-ui @dependabot) ([#5912](https://github.com/paperless-ngx/paperless-ngx/pull/5912))
|
||||||
|
- Chore(deps): Bump zone.js from 0.14.3 to 0.14.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#5913](https://github.com/paperless-ngx/paperless-ngx/pull/5913))
|
||||||
|
- Chore(deps): Bump bootstrap from 5.3.2 to 5.3.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#5911](https://github.com/paperless-ngx/paperless-ngx/pull/5911))
|
||||||
|
- Chore(deps-dev): Bump typescript from 5.2.2 to 5.3.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#5915](https://github.com/paperless-ngx/paperless-ngx/pull/5915))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 15 updates [@dependabot](https://github.com/dependabot) ([#5908](https://github.com/paperless-ngx/paperless-ngx/pull/5908))
|
||||||
|
- Chore(deps): Bump the small-changes group with 4 updates [@dependabot](https://github.com/dependabot) ([#5916](https://github.com/paperless-ngx/paperless-ngx/pull/5916))
|
||||||
|
- Chore(deps-dev): Bump the development group with 4 updates [@dependabot](https://github.com/dependabot) ([#5914](https://github.com/paperless-ngx/paperless-ngx/pull/5914))
|
||||||
|
- Chore(deps): Bump the actions group with 3 updates [@dependabot](https://github.com/dependabot) ([#5907](https://github.com/paperless-ngx/paperless-ngx/pull/5907))
|
||||||
|
</details>
|
||||||
|
|
||||||
|
### All App Changes
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>33 changes</summary>
|
||||||
|
|
||||||
|
- Feature: Allow user to control PIL image pixel limit [@stumpylog](https://github.com/stumpylog) ([#5997](https://github.com/paperless-ngx/paperless-ngx/pull/5997))
|
||||||
|
- Enhancement: show ID when editing objects [@shamoon](https://github.com/shamoon) ([#6003](https://github.com/paperless-ngx/paperless-ngx/pull/6003))
|
||||||
|
- Feature: Allow a user to disable the pixel limit for OCR entirely [@stumpylog](https://github.com/stumpylog) ([#5996](https://github.com/paperless-ngx/paperless-ngx/pull/5996))
|
||||||
|
- Chore(deps): Bump the small-changes group with 3 updates [@dependabot](https://github.com/dependabot) ([#6001](https://github.com/paperless-ngx/paperless-ngx/pull/6001))
|
||||||
|
- Chore(deps-dev): Bump the development group with 2 updates [@dependabot](https://github.com/dependabot) ([#5998](https://github.com/paperless-ngx/paperless-ngx/pull/5998))
|
||||||
|
- Chore(deps): Bump the django group with 1 update [@dependabot](https://github.com/dependabot) ([#6000](https://github.com/paperless-ngx/paperless-ngx/pull/6000))
|
||||||
|
- Feature: workflow removal action [@shamoon](https://github.com/shamoon) ([#5928](https://github.com/paperless-ngx/paperless-ngx/pull/5928))
|
||||||
|
- Feature: system status [@shamoon](https://github.com/shamoon) ([#5743](https://github.com/paperless-ngx/paperless-ngx/pull/5743))
|
||||||
|
- Fix: refactor base path settings, correct logout redirect [@shamoon](https://github.com/shamoon) ([#5976](https://github.com/paperless-ngx/paperless-ngx/pull/5976))
|
||||||
|
- Chore(deps-dev): Bump [@<!---->playwright/test from 1.41.2 to 1.42.0 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.41.2 to 1.42.0 in /src-ui @dependabot) ([#5964](https://github.com/paperless-ngx/paperless-ngx/pull/5964))
|
||||||
|
- Chore(deps-dev): Bump [@<!---->types/node from 20.11.20 to 20.11.24 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.11.20 to 20.11.24 in /src-ui @dependabot) ([#5965](https://github.com/paperless-ngx/paperless-ngx/pull/5965))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 11 updates [@dependabot](https://github.com/dependabot) ([#5963](https://github.com/paperless-ngx/paperless-ngx/pull/5963))
|
||||||
|
- Fix: always pass from UI, dont require in API [@shamoon](https://github.com/shamoon) ([#5962](https://github.com/paperless-ngx/paperless-ngx/pull/5962))
|
||||||
|
- Fix: Clear metadata cache when the filename(s) change [@stumpylog](https://github.com/stumpylog) ([#5957](https://github.com/paperless-ngx/paperless-ngx/pull/5957))
|
||||||
|
- Fix: include monetary, float and doc link values in search filters [@shamoon](https://github.com/shamoon) ([#5951](https://github.com/paperless-ngx/paperless-ngx/pull/5951))
|
||||||
|
- Fix: Better handling of a corrupted index [@stumpylog](https://github.com/stumpylog) ([#5950](https://github.com/paperless-ngx/paperless-ngx/pull/5950))
|
||||||
|
- Chore: Includes OCRMyPdf logging into the log file [@stumpylog](https://github.com/stumpylog) ([#5947](https://github.com/paperless-ngx/paperless-ngx/pull/5947))
|
||||||
|
- Fix: ensure document title always limited to 128 chars [@shamoon](https://github.com/shamoon) ([#5934](https://github.com/paperless-ngx/paperless-ngx/pull/5934))
|
||||||
|
- Enhancement: better monetary field with currency code [@shamoon](https://github.com/shamoon) ([#5858](https://github.com/paperless-ngx/paperless-ngx/pull/5858))
|
||||||
|
- Change: add Thumbs.db to default ignores [@DennisGaida](https://github.com/DennisGaida) ([#5924](https://github.com/paperless-ngx/paperless-ngx/pull/5924))
|
||||||
|
- Fix: use for password reset emails, if set [@shamoon](https://github.com/shamoon) ([#5902](https://github.com/paperless-ngx/paperless-ngx/pull/5902))
|
||||||
|
- Fix: respect global permissions for UI settings [@shamoon](https://github.com/shamoon) ([#5919](https://github.com/paperless-ngx/paperless-ngx/pull/5919))
|
||||||
|
- Chore(deps-dev): Bump the frontend-eslint-dependencies group in /src-ui with 3 updates [@dependabot](https://github.com/dependabot) ([#5918](https://github.com/paperless-ngx/paperless-ngx/pull/5918))
|
||||||
|
- Chore(deps-dev): Bump [@<!---->types/node from 20.11.16 to 20.11.20 in /src-ui @dependabot](https://github.com/<!---->types/node from 20.11.16 to 20.11.20 in /src-ui @dependabot) ([#5912](https://github.com/paperless-ngx/paperless-ngx/pull/5912))
|
||||||
|
- Chore(deps): Bump zone.js from 0.14.3 to 0.14.4 in /src-ui [@dependabot](https://github.com/dependabot) ([#5913](https://github.com/paperless-ngx/paperless-ngx/pull/5913))
|
||||||
|
- Chore(deps): Bump bootstrap from 5.3.2 to 5.3.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#5911](https://github.com/paperless-ngx/paperless-ngx/pull/5911))
|
||||||
|
- Chore(deps-dev): Bump typescript from 5.2.2 to 5.3.3 in /src-ui [@dependabot](https://github.com/dependabot) ([#5915](https://github.com/paperless-ngx/paperless-ngx/pull/5915))
|
||||||
|
- Chore(deps): Bump the frontend-angular-dependencies group in /src-ui with 15 updates [@dependabot](https://github.com/dependabot) ([#5908](https://github.com/paperless-ngx/paperless-ngx/pull/5908))
|
||||||
|
- Fix: allow disable email verification during signup [@shamoon](https://github.com/shamoon) ([#5895](https://github.com/paperless-ngx/paperless-ngx/pull/5895))
|
||||||
|
- Fix: refactor accounts templates and create signup template [@shamoon](https://github.com/shamoon) ([#5899](https://github.com/paperless-ngx/paperless-ngx/pull/5899))
|
||||||
|
- Chore(deps): Bump the small-changes group with 4 updates [@dependabot](https://github.com/dependabot) ([#5916](https://github.com/paperless-ngx/paperless-ngx/pull/5916))
|
||||||
|
- Chore(deps-dev): Bump the development group with 4 updates [@dependabot](https://github.com/dependabot) ([#5914](https://github.com/paperless-ngx/paperless-ngx/pull/5914))
|
||||||
|
- Enhancement: support disabling regular login [@shamoon](https://github.com/shamoon) ([#5816](https://github.com/paperless-ngx/paperless-ngx/pull/5816))
|
||||||
|
</details>
|
||||||
|
|
||||||
## paperless-ngx 2.5.4
|
## paperless-ngx 2.5.4
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
@ -766,6 +766,8 @@ but could result in missing text content.
|
|||||||
If unset, will default to the value determined by
|
If unset, will default to the value determined by
|
||||||
[Pillow](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.MAX_IMAGE_PIXELS).
|
[Pillow](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.MAX_IMAGE_PIXELS).
|
||||||
|
|
||||||
|
Setting this value to 0 will entirely disable the limit. See the below warning.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
||||||
Increasing this limit could cause Paperless to consume additional
|
Increasing this limit could cause Paperless to consume additional
|
||||||
@ -775,7 +777,7 @@ but could result in missing text content.
|
|||||||
!!! warning
|
!!! warning
|
||||||
|
|
||||||
The limit is intended to prevent malicious files from consuming
|
The limit is intended to prevent malicious files from consuming
|
||||||
system resources and causing crashes and other errors. Only increase
|
system resources and causing crashes and other errors. Only change
|
||||||
this value if you are certain your documents are not malicious and
|
this value if you are certain your documents are not malicious and
|
||||||
you need the text which was not OCRed
|
you need the text which was not OCRed
|
||||||
|
|
||||||
@ -967,6 +969,20 @@ be used with caution!
|
|||||||
|
|
||||||
Defaults to None, which does not add any additional apps.
|
Defaults to None, which does not add any additional apps.
|
||||||
|
|
||||||
|
#### [`PAPERLESS_MAX_IMAGE_PIXELS=<number>`](#PAPERLESS_MAX_IMAGE_PIXELS) {#PAPERLESS_MAX_IMAGE_PIXELS}
|
||||||
|
|
||||||
|
: Configures the maximum size of an image PIL will allow to load without warning or error.
|
||||||
|
|
||||||
|
: If unset, will default to the value determined by
|
||||||
|
[Pillow](https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.MAX_IMAGE_PIXELS).
|
||||||
|
|
||||||
|
Defaults to None, which does change the limit
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
This limit is designed to prevent denial of service from malicious files.
|
||||||
|
It should only be raised or disabled in certain circumstances and with great care.
|
||||||
|
|
||||||
## Document Consumption {#consume_config}
|
## Document Consumption {#consume_config}
|
||||||
|
|
||||||
#### [`PAPERLESS_CONSUMER_DELETE_DUPLICATES=<bool>`](#PAPERLESS_CONSUMER_DELETE_DUPLICATES) {#PAPERLESS_CONSUMER_DELETE_DUPLICATES}
|
#### [`PAPERLESS_CONSUMER_DELETE_DUPLICATES=<bool>`](#PAPERLESS_CONSUMER_DELETE_DUPLICATES) {#PAPERLESS_CONSUMER_DELETE_DUPLICATES}
|
||||||
|
@ -329,7 +329,7 @@ Workflows allow you to filter by:
|
|||||||
|
|
||||||
### Workflow Actions
|
### Workflow Actions
|
||||||
|
|
||||||
There is currently one type of workflow action, "Assignment", which can assign:
|
There are currently two types of workflow actions, "Assignment", which can assign:
|
||||||
|
|
||||||
- Title, see [title placeholders](usage.md#title-placeholders) below
|
- Title, see [title placeholders](usage.md#title-placeholders) below
|
||||||
- Tags, correspondent, document type and storage path
|
- Tags, correspondent, document type and storage path
|
||||||
@ -337,6 +337,13 @@ There is currently one type of workflow action, "Assignment", which can assign:
|
|||||||
- View and / or edit permissions to users or groups
|
- View and / or edit permissions to users or groups
|
||||||
- Custom fields. Note that no value for the field will be set
|
- Custom fields. Note that no value for the field will be set
|
||||||
|
|
||||||
|
and "Removal" actions, which can remove either all of or specific sets of the following:
|
||||||
|
|
||||||
|
- Tags, correspondents, document types or storage paths
|
||||||
|
- Document owner
|
||||||
|
- View and / or edit permissions
|
||||||
|
- Custom fields
|
||||||
|
|
||||||
#### Title placeholders
|
#### Title placeholders
|
||||||
|
|
||||||
Workflow titles can include placeholders but the available options differ depending on the type of
|
Workflow titles can include placeholders but the available options differ depending on the type of
|
||||||
|
@ -77,7 +77,9 @@
|
|||||||
"scripts": [],
|
"scripts": [],
|
||||||
"allowedCommonJsDependencies": [
|
"allowedCommonJsDependencies": [
|
||||||
"pdfjs-dist",
|
"pdfjs-dist",
|
||||||
"pdfjs-dist/web/pdf_viewer"
|
"pdfjs-dist/web/pdf_viewer",
|
||||||
|
"filesize",
|
||||||
|
"file-saver"
|
||||||
],
|
],
|
||||||
"vendorChunk": true,
|
"vendorChunk": true,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
|
1069
src-ui/messages.xlf
1069
src-ui/messages.xlf
File diff suppressed because it is too large
Load Diff
23
src-ui/package-lock.json
generated
23
src-ui/package-lock.json
generated
@ -29,6 +29,7 @@
|
|||||||
"ngx-color": "^9.0.0",
|
"ngx-color": "^9.0.0",
|
||||||
"ngx-cookie-service": "^17.1.0",
|
"ngx-cookie-service": "^17.1.0",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
|
"ngx-filesize": "^3.0.3",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^14.0.2",
|
"ngx-ui-tour-ng-bootstrap": "^14.0.2",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
@ -9844,6 +9845,15 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/filesize": {
|
||||||
|
"version": "9.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/filesize/-/filesize-9.0.11.tgz",
|
||||||
|
"integrity": "sha512-gTAiTtI0STpKa5xesyTA9hA3LX4ga8sm2nWRcffEa1L/5vQwb4mj2MdzMkoHoGv4QzfDshQZuYscQSf8c4TKOA==",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@ -14105,6 +14115,19 @@
|
|||||||
"@angular/core": ">=14.0.0"
|
"@angular/core": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ngx-filesize": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ngx-filesize/-/ngx-filesize-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-qqP2p4WbbF7R+NXC9NqRQdAfWfMAYJ2Ijf4ezRCq7j3tPY6ybSP9AZ3FY1U7/95n1hmOJ2U5oY+oFb7LhHQRBw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": ">= 14.2.0 < 18.0.0",
|
||||||
|
"@angular/core": ">= 14.2.0 < 18.0.0",
|
||||||
|
"filesize": ">= 6.0.0 < 10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ngx-ui-tour-core": {
|
"node_modules/ngx-ui-tour-core": {
|
||||||
"version": "12.0.1",
|
"version": "12.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ngx-ui-tour-core/-/ngx-ui-tour-core-12.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ngx-ui-tour-core/-/ngx-ui-tour-core-12.0.1.tgz",
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
"ngx-color": "^9.0.0",
|
"ngx-color": "^9.0.0",
|
||||||
"ngx-cookie-service": "^17.1.0",
|
"ngx-cookie-service": "^17.1.0",
|
||||||
"ngx-file-drop": "^16.0.0",
|
"ngx-file-drop": "^16.0.0",
|
||||||
|
"ngx-filesize": "^3.0.3",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^14.0.2",
|
"ngx-ui-tour-ng-bootstrap": "^14.0.2",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
@ -114,7 +114,10 @@ import { FileComponent } from './components/common/input/file/file.component'
|
|||||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||||
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
|
import { ConfirmButtonComponent } from './components/common/confirm-button/confirm-button.component'
|
||||||
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
|
import { MonetaryComponent } from './components/common/input/monetary/monetary.component'
|
||||||
|
import { SystemStatusDialogComponent } from './components/common/system-status-dialog/system-status-dialog.component'
|
||||||
|
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||||
import {
|
import {
|
||||||
|
airplane,
|
||||||
archive,
|
archive,
|
||||||
arrowCounterclockwise,
|
arrowCounterclockwise,
|
||||||
arrowDown,
|
arrowDown,
|
||||||
@ -129,12 +132,14 @@ import {
|
|||||||
boxes,
|
boxes,
|
||||||
calendar,
|
calendar,
|
||||||
calendarEvent,
|
calendarEvent,
|
||||||
|
cardChecklist,
|
||||||
caretDown,
|
caretDown,
|
||||||
caretUp,
|
caretUp,
|
||||||
chatLeftText,
|
chatLeftText,
|
||||||
check,
|
check,
|
||||||
check2All,
|
check2All,
|
||||||
checkAll,
|
checkAll,
|
||||||
|
checkCircleFill,
|
||||||
checkLg,
|
checkLg,
|
||||||
chevronDoubleLeft,
|
chevronDoubleLeft,
|
||||||
chevronDoubleRight,
|
chevronDoubleRight,
|
||||||
@ -148,7 +153,9 @@ import {
|
|||||||
doorOpen,
|
doorOpen,
|
||||||
download,
|
download,
|
||||||
envelope,
|
envelope,
|
||||||
|
exclamationCircleFill,
|
||||||
exclamationTriangle,
|
exclamationTriangle,
|
||||||
|
exclamationTriangleFill,
|
||||||
eye,
|
eye,
|
||||||
fileEarmark,
|
fileEarmark,
|
||||||
fileEarmarkCheck,
|
fileEarmarkCheck,
|
||||||
@ -200,6 +207,7 @@ import {
|
|||||||
} from 'ngx-bootstrap-icons'
|
} from 'ngx-bootstrap-icons'
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
|
airplane,
|
||||||
archive,
|
archive,
|
||||||
arrowCounterclockwise,
|
arrowCounterclockwise,
|
||||||
arrowDown,
|
arrowDown,
|
||||||
@ -214,12 +222,14 @@ const icons = {
|
|||||||
boxes,
|
boxes,
|
||||||
calendar,
|
calendar,
|
||||||
calendarEvent,
|
calendarEvent,
|
||||||
|
cardChecklist,
|
||||||
caretDown,
|
caretDown,
|
||||||
caretUp,
|
caretUp,
|
||||||
chatLeftText,
|
chatLeftText,
|
||||||
check,
|
check,
|
||||||
check2All,
|
check2All,
|
||||||
checkAll,
|
checkAll,
|
||||||
|
checkCircleFill,
|
||||||
checkLg,
|
checkLg,
|
||||||
chevronDoubleLeft,
|
chevronDoubleLeft,
|
||||||
chevronDoubleRight,
|
chevronDoubleRight,
|
||||||
@ -233,7 +243,9 @@ const icons = {
|
|||||||
doorOpen,
|
doorOpen,
|
||||||
download,
|
download,
|
||||||
envelope,
|
envelope,
|
||||||
|
exclamationCircleFill,
|
||||||
exclamationTriangle,
|
exclamationTriangle,
|
||||||
|
exclamationTriangleFill,
|
||||||
eye,
|
eye,
|
||||||
fileEarmark,
|
fileEarmark,
|
||||||
fileEarmarkCheck,
|
fileEarmarkCheck,
|
||||||
@ -445,6 +457,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
FileComponent,
|
FileComponent,
|
||||||
ConfirmButtonComponent,
|
ConfirmButtonComponent,
|
||||||
MonetaryComponent,
|
MonetaryComponent,
|
||||||
|
SystemStatusDialogComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
@ -459,6 +472,7 @@ function initializeApp(settings: SettingsService) {
|
|||||||
TourNgBootstrapModule,
|
TourNgBootstrapModule,
|
||||||
DragDropModule,
|
DragDropModule,
|
||||||
NgxBootstrapIconsModule.pick(icons),
|
NgxBootstrapIconsModule.pick(icons),
|
||||||
|
NgxFilesizeModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -4,10 +4,31 @@
|
|||||||
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
|
info="Options to customize appearance, notifications, saved views and more. Settings apply to the <strong>current user only</strong>."
|
||||||
i18n-info
|
i18n-info
|
||||||
>
|
>
|
||||||
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button>
|
<button class="btn btn-sm btn-outline-primary" (click)="tourService.start()">
|
||||||
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank">
|
<i-bs class="me-1" name="airplane"></i-bs> <ng-container i18n>Start tour</ng-container>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
||||||
|
[disabled]="!systemStatus"
|
||||||
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }">
|
||||||
|
@if (!systemStatus) {
|
||||||
|
<div class="spinner-border spinner-border-sm me-1 h-75" role="status"></div>
|
||||||
|
} @else {
|
||||||
|
<i-bs class="me-2" name="card-checklist"></i-bs>
|
||||||
|
@if (systemStatusHasErrors) {
|
||||||
|
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||||
|
<i-bs name="exclamation-circle-fill" class="text-danger" width="1.75em" height="1.75em"></i-bs>
|
||||||
|
</span>
|
||||||
|
} @else {
|
||||||
|
<span class="badge bg-body position-absolute top-0 start-100 translate-middle rounded-pill p-0">
|
||||||
|
<i-bs name="check-circle-fill" class="text-primary" width="1.75em" height="1.75em"></i-bs>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<ng-container i18n>System Status</ng-container>
|
||||||
|
</button>
|
||||||
|
<a *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Admin }" class="btn btn-sm btn-primary" href="admin/" target="_blank">
|
||||||
<ng-container i18n>Open Django Admin</ng-container>
|
<ng-container i18n>Open Django Admin</ng-container>
|
||||||
<i-bs name="arrow-up-right"></i-bs>
|
<i-bs name="arrow-up-right"></i-bs>
|
||||||
</a>
|
</a>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
|
@ -9,6 +9,8 @@ import {
|
|||||||
NgbModule,
|
NgbModule,
|
||||||
NgbAlertModule,
|
NgbAlertModule,
|
||||||
NgbNavLink,
|
NgbNavLink,
|
||||||
|
NgbModal,
|
||||||
|
NgbModalModule,
|
||||||
} from '@ng-bootstrap/ng-bootstrap'
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { NgSelectModule } from '@ng-select/ng-select'
|
import { NgSelectModule } from '@ng-select/ng-select'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
@ -39,6 +41,13 @@ import { SettingsComponent } from './settings.component'
|
|||||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||||
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
|
||||||
|
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||||
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
|
import {
|
||||||
|
SystemStatus,
|
||||||
|
InstallType,
|
||||||
|
SystemStatusItemStatus,
|
||||||
|
} from 'src/app/data/system-status'
|
||||||
|
|
||||||
const savedViews = [
|
const savedViews = [
|
||||||
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
{ id: 1, name: 'view1', show_in_sidebar: true, show_on_dashboard: true },
|
||||||
@ -65,6 +74,8 @@ describe('SettingsComponent', () => {
|
|||||||
let userService: UserService
|
let userService: UserService
|
||||||
let permissionsService: PermissionsService
|
let permissionsService: PermissionsService
|
||||||
let groupService: GroupService
|
let groupService: GroupService
|
||||||
|
let modalService: NgbModal
|
||||||
|
let systemStatusService: SystemStatusService
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -96,6 +107,7 @@ describe('SettingsComponent', () => {
|
|||||||
NgbAlertModule,
|
NgbAlertModule,
|
||||||
NgSelectModule,
|
NgSelectModule,
|
||||||
NgxBootstrapIconsModule.pick(allIcons),
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
NgbModalModule,
|
||||||
],
|
],
|
||||||
}).compileComponents()
|
}).compileComponents()
|
||||||
|
|
||||||
@ -107,6 +119,8 @@ describe('SettingsComponent', () => {
|
|||||||
settingsService.currentUser = users[0]
|
settingsService.currentUser = users[0]
|
||||||
userService = TestBed.inject(UserService)
|
userService = TestBed.inject(UserService)
|
||||||
permissionsService = TestBed.inject(PermissionsService)
|
permissionsService = TestBed.inject(PermissionsService)
|
||||||
|
modalService = TestBed.inject(NgbModal)
|
||||||
|
systemStatusService = TestBed.inject(SystemStatusService)
|
||||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||||
jest
|
jest
|
||||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||||
@ -372,4 +386,54 @@ describe('SettingsComponent', () => {
|
|||||||
fixture.detectChanges()
|
fixture.detectChanges()
|
||||||
expect(toastErrorSpy).toBeCalled()
|
expect(toastErrorSpy).toBeCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should load system status on initialize, show errors if needed', () => {
|
||||||
|
const status: SystemStatus = {
|
||||||
|
pngx_version: '2.4.3',
|
||||||
|
server_os: 'macOS-14.1.1-arm64-arm-64bit',
|
||||||
|
install_type: InstallType.BareMetal,
|
||||||
|
storage: { total: 494384795648, available: 13573525504 },
|
||||||
|
database: {
|
||||||
|
type: 'sqlite',
|
||||||
|
url: '/paperless-ngx/data/db.sqlite3',
|
||||||
|
status: SystemStatusItemStatus.ERROR,
|
||||||
|
error: null,
|
||||||
|
migration_status: {
|
||||||
|
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
|
||||||
|
unapplied_migrations: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
redis_url: 'redis://localhost:6379',
|
||||||
|
redis_status: SystemStatusItemStatus.ERROR,
|
||||||
|
redis_error:
|
||||||
|
'Error 61 connecting to localhost:6379. Connection refused.',
|
||||||
|
celery_status: SystemStatusItemStatus.ERROR,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jest.spyOn(systemStatusService, 'get').mockReturnValue(of(status))
|
||||||
|
completeSetup()
|
||||||
|
expect(component['systemStatus']).toEqual(status) // private
|
||||||
|
expect(component.systemStatusHasErrors).toBeTruthy()
|
||||||
|
// coverage
|
||||||
|
component['systemStatus'].database.status = SystemStatusItemStatus.OK
|
||||||
|
component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
|
||||||
|
component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
|
||||||
|
expect(component.systemStatusHasErrors).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should open system status dialog', () => {
|
||||||
|
const modalOpenSpy = jest.spyOn(modalService, 'open')
|
||||||
|
completeSetup()
|
||||||
|
component.showSystemStatus()
|
||||||
|
expect(modalOpenSpy).toHaveBeenCalledWith(SystemStatusDialogComponent, {
|
||||||
|
size: 'xl',
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -9,7 +9,11 @@ import {
|
|||||||
} from '@angular/core'
|
} from '@angular/core'
|
||||||
import { FormGroup, FormControl } from '@angular/forms'
|
import { FormGroup, FormControl } from '@angular/forms'
|
||||||
import { ActivatedRoute, Router } from '@angular/router'
|
import { ActivatedRoute, Router } from '@angular/router'
|
||||||
import { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap'
|
import {
|
||||||
|
NgbModal,
|
||||||
|
NgbModalRef,
|
||||||
|
NgbNavChangeEvent,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
import { DirtyComponent, dirtyCheck } from '@ngneat/dirty-check-forms'
|
||||||
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
import { TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||||
import {
|
import {
|
||||||
@ -40,6 +44,12 @@ import {
|
|||||||
} from 'src/app/services/settings.service'
|
} from 'src/app/services/settings.service'
|
||||||
import { ToastService, Toast } from 'src/app/services/toast.service'
|
import { ToastService, Toast } from 'src/app/services/toast.service'
|
||||||
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
|
||||||
|
import { SystemStatusDialogComponent } from '../../common/system-status-dialog/system-status-dialog.component'
|
||||||
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
|
import {
|
||||||
|
SystemStatusItemStatus,
|
||||||
|
SystemStatus,
|
||||||
|
} from 'src/app/data/system-status'
|
||||||
|
|
||||||
enum SettingsNavIDs {
|
enum SettingsNavIDs {
|
||||||
General = 1,
|
General = 1,
|
||||||
@ -111,6 +121,18 @@ export class SettingsComponent
|
|||||||
users: User[]
|
users: User[]
|
||||||
groups: Group[]
|
groups: Group[]
|
||||||
|
|
||||||
|
private systemStatus: SystemStatus
|
||||||
|
|
||||||
|
get systemStatusHasErrors(): boolean {
|
||||||
|
return (
|
||||||
|
this.systemStatus.database.status === SystemStatusItemStatus.ERROR ||
|
||||||
|
this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
|
||||||
|
this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
|
||||||
|
this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
|
||||||
|
this.systemStatus.tasks.classifier_status === SystemStatusItemStatus.ERROR
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
get computedDateLocale(): string {
|
get computedDateLocale(): string {
|
||||||
return (
|
return (
|
||||||
this.settingsForm.value.dateLocale ||
|
this.settingsForm.value.dateLocale ||
|
||||||
@ -131,7 +153,9 @@ export class SettingsComponent
|
|||||||
private usersService: UserService,
|
private usersService: UserService,
|
||||||
private groupsService: GroupService,
|
private groupsService: GroupService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
public permissionsService: PermissionsService
|
public permissionsService: PermissionsService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private systemStatusService: SystemStatusService
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
this.settings.settingsSaved.subscribe(() => {
|
this.settings.settingsSaved.subscribe(() => {
|
||||||
@ -360,6 +384,17 @@ export class SettingsComponent
|
|||||||
// prevents loss of unsaved changes
|
// prevents loss of unsaved changes
|
||||||
this.settingsForm.patchValue(currentFormValue)
|
this.settingsForm.patchValue(currentFormValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.permissionsService.currentUserCan(
|
||||||
|
PermissionAction.View,
|
||||||
|
PermissionType.Admin
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
this.systemStatusService.get().subscribe((status) => {
|
||||||
|
this.systemStatus = status
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private emptyGroup(group: FormGroup) {
|
private emptyGroup(group: FormGroup) {
|
||||||
@ -565,4 +600,14 @@ export class SettingsComponent
|
|||||||
clearThemeColor() {
|
clearThemeColor() {
|
||||||
this.settingsForm.get('themeColor').patchValue('')
|
this.settingsForm.get('themeColor').patchValue('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showSystemStatus() {
|
||||||
|
const modal: NgbModalRef = this.modalService.open(
|
||||||
|
SystemStatusDialogComponent,
|
||||||
|
{
|
||||||
|
size: 'xl',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
modal.componentInstance.status = this.systemStatus
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@
|
|||||||
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
|
[formControl]="searchField" [ngbTypeahead]="searchAutoComplete" (keyup)="searchFieldKeyup($event)"
|
||||||
(selectItem)="itemSelected($event)" i18n-placeholder>
|
(selectItem)="itemSelected($event)" i18n-placeholder>
|
||||||
@if (!searchFieldEmpty) {
|
@if (!searchFieldEmpty) {
|
||||||
<button type="button" class="btn btn-link btn-sm px-0 position-absolute top-0 end-0" (click)="resetSearchField()">
|
<button type="button" class="btn btn-link btn-sm ps-0 pe-1 position-absolute top-0 end-0" (click)="resetSearchField()">
|
||||||
<i-bs width="1em" height="1em" name="x"></i-bs>
|
<i-bs width="1em" height="1em" name="x"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -29,7 +32,6 @@
|
|||||||
@if (showActionParamField) {
|
@if (showActionParamField) {
|
||||||
<pngx-input-text i18n-title title="Action parameter" formControlName="action_parameter" [error]="error?.action_parameter"></pngx-input-text>
|
<pngx-input-text i18n-title title="Action parameter" formControlName="action_parameter" [error]="error?.action_parameter"></pngx-input-text>
|
||||||
}
|
}
|
||||||
<p class="small fst-italic mt-5" i18n>Assignments specified here will supersede any consumption templates.</p>
|
|
||||||
<pngx-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></pngx-input-select>
|
<pngx-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></pngx-input-select>
|
||||||
<pngx-input-tags [allowCreate]="false" formControlName="assign_tags"></pngx-input-tags>
|
<pngx-input-tags [allowCreate]="false" formControlName="assign_tags"></pngx-input-tags>
|
||||||
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
<pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
|
||||||
|
@if (object?.id) {
|
||||||
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{object.id}}</span>
|
||||||
|
}
|
||||||
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -35,7 +38,7 @@
|
|||||||
<div ngbAccordionItem>
|
<div ngbAccordionItem>
|
||||||
<div ngbAccordionHeader>
|
<div ngbAccordionHeader>
|
||||||
<button ngbAccordionButton>{{i + 1}}. {{getTriggerTypeOptionName(triggerFields.controls[i].value.type)}}
|
<button ngbAccordionButton>{{i + 1}}. {{getTriggerTypeOptionName(triggerFields.controls[i].value.type)}}
|
||||||
@if(trigger.id > -1) {
|
@if(trigger.id) {
|
||||||
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{trigger.id}}</span>
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{trigger.id}}</span>
|
||||||
}
|
}
|
||||||
<pngx-confirm-button
|
<pngx-confirm-button
|
||||||
@ -77,7 +80,7 @@
|
|||||||
<div ngbAccordionItem cdkDrag [formGroup]="actionFields.controls[i]">
|
<div ngbAccordionItem cdkDrag [formGroup]="actionFields.controls[i]">
|
||||||
<div ngbAccordionHeader>
|
<div ngbAccordionHeader>
|
||||||
<button ngbAccordionButton>{{i + 1}}. {{getActionTypeOptionName(actionFields.controls[i].value.type)}}
|
<button ngbAccordionButton>{{i + 1}}. {{getActionTypeOptionName(actionFields.controls[i].value.type)}}
|
||||||
@if(action.id > -1) {
|
@if(action.id) {
|
||||||
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
|
<span class="badge bg-primary text-primary-text-contrast ms-2">ID: {{action.id}}</span>
|
||||||
}
|
}
|
||||||
<pngx-confirm-button
|
<pngx-confirm-button
|
||||||
@ -91,8 +94,67 @@
|
|||||||
</div>
|
</div>
|
||||||
<div ngbAccordionCollapse>
|
<div ngbAccordionCollapse>
|
||||||
<div ngbAccordionBody>
|
<div ngbAccordionBody>
|
||||||
<pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
|
<ng-template [ngTemplateOutlet]="actionForm" [ngTemplateOutletContext]="{ formGroup: actionFields.controls[i], action: action }"></ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
@if (error?.non_field_errors) {
|
||||||
|
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
||||||
|
}
|
||||||
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ng-template #triggerForm let-formGroup="formGroup" let-trigger="trigger">
|
||||||
|
<div [formGroup]="formGroup">
|
||||||
<input type="hidden" formControlName="id" />
|
<input type="hidden" formControlName="id" />
|
||||||
|
<pngx-input-select i18n-title title="Trigger type" [horizontal]="true" [items]="triggerTypeOptions" formControlName="type"></pngx-input-select>
|
||||||
|
<p class="small" i18n>Trigger for documents that match <em>all</em> filters specified below.</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
||||||
|
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
|
||||||
|
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
||||||
|
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a>" [error]="error?.filter_path"></pngx-input-text>
|
||||||
|
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
||||||
|
}
|
||||||
|
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
||||||
|
<pngx-input-select i18n-title title="Content matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
||||||
|
@if (patternRequired) {
|
||||||
|
<pngx-input-text i18n-title title="Content matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
||||||
|
}
|
||||||
|
@if (patternRequired) {
|
||||||
|
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
||||||
|
<div class="col-md-6">
|
||||||
|
<pngx-input-tags [allowCreate]="false" i18n-title title="Has tags" formControlName="filter_has_tags"></pngx-input-tags>
|
||||||
|
<pngx-input-select i18n-title title="Has correspondent" [items]="correspondents" [allowNull]="true" formControlName="filter_has_correspondent"></pngx-input-select>
|
||||||
|
<pngx-input-select i18n-title title="Has document type" [items]="documentTypes" [allowNull]="true" formControlName="filter_has_document_type"></pngx-input-select>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #actionForm let-formGroup="formGroup" let action="action">
|
||||||
|
<div [formGroup]="formGroup">
|
||||||
|
<input type="hidden" formControlName="id" />
|
||||||
|
<pngx-input-select i18n-title title="Action type" [horizontal]="true" [items]="actionTypeOptions" formControlName="type"></pngx-input-select>
|
||||||
|
@switch(formGroup.get('type').value) {
|
||||||
|
@case ( WorkflowActionType.Assignment) {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
|
<pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#workflows'>documentation</a>." [error]="error?.actions?.[i]?.assign_title"></pngx-input-text>
|
||||||
@ -147,57 +209,93 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
@case (WorkflowActionType.Removal) {
|
||||||
</ng-template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
@if (error?.non_field_errors) {
|
|
||||||
<span class="text-danger"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span>
|
|
||||||
}
|
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
|
||||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<ng-template #triggerForm let-formGroup="formGroup" let-trigger="trigger">
|
|
||||||
<div [formGroup]="formGroup">
|
|
||||||
<input type="hidden" formControlName="id" />
|
|
||||||
<pngx-input-select i18n-title title="Trigger type" [horizontal]="true" [items]="triggerTypeOptions" formControlName="type"></pngx-input-select>
|
|
||||||
<p class="small" i18n>Trigger for documents that match <em>all</em> filters specified below.</p>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text>
|
<h6 class="form-label" i18n>Remove tags</h6>
|
||||||
@if (formGroup.get('type').value === WorkflowTriggerType.Consumption) {
|
<pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_tags"></pngx-input-switch>
|
||||||
<pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.sources"></pngx-input-select>
|
<div class="mt-n3">
|
||||||
<pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case-normalized.</a>" [error]="error?.filter_path"></pngx-input-text>
|
<pngx-input-tags [allowCreate]="false" title="" formControlName="remove_tags"></pngx-input-tags>
|
||||||
<pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select>
|
</div>
|
||||||
}
|
|
||||||
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
<h6 class="form-label" i18n>Remove correspondents</h6>
|
||||||
<pngx-input-select i18n-title title="Content matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
|
<pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_correspondents"></pngx-input-switch>
|
||||||
@if (patternRequired) {
|
<div class="mt-n3">
|
||||||
<pngx-input-text i18n-title title="Content matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
|
<pngx-input-select i18n-title title="" multiple="true" [items]="correspondents" formControlName="remove_correspondents"></pngx-input-select>
|
||||||
}
|
</div>
|
||||||
@if (patternRequired) {
|
|
||||||
<pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive"></pngx-input-check>
|
<h6 class="form-label" i18n>Remove document types</h6>
|
||||||
}
|
<pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_document_types"></pngx-input-switch>
|
||||||
}
|
<div class="mt-n3">
|
||||||
|
<pngx-input-select i18n-title title="" multiple="true" [items]="documentTypes" formControlName="remove_document_types"></pngx-input-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="form-label" i18n>Remove storage paths</h6>
|
||||||
|
<pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_storage_paths"></pngx-input-switch>
|
||||||
|
<div class="mt-n3">
|
||||||
|
<pngx-input-select i18n-title title="" multiple="true" [items]="storagePaths" formControlName="remove_storage_paths"></pngx-input-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="form-label" i18n>Remove custom fields</h6>
|
||||||
|
<pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_custom_fields"></pngx-input-switch>
|
||||||
|
<div class="mt-n3">
|
||||||
|
<pngx-input-select i18n-title title="" multiple="true" [items]="customFields" formControlName="remove_custom_fields"></pngx-input-select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<h6 class="form-label" i18n>Remove owners</h6>
|
||||||
|
<pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_owners"></pngx-input-switch>
|
||||||
|
<div class="mt-n3">
|
||||||
|
<pngx-input-select i18n-title title="" multiple="true" [items]="users" bindLabel="username" formControlName="remove_owners"></pngx-input-select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h6 class="form-label" i18n>Remove permissions</h6>
|
||||||
|
<pngx-input-switch i18n-title title="Remove all" [horizontal]="true" formControlName="remove_all_permissions"></pngx-input-switch>
|
||||||
|
<div>
|
||||||
|
<label class="form-label" i18n>View permissions</label>
|
||||||
|
<div class="mb-2">
|
||||||
|
<div class="row mb-1">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<pngx-permissions-user type="view" formControlName="remove_view_users"></pngx-permissions-user>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<pngx-permissions-group type="view" formControlName="remove_view_groups"></pngx-permissions-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<label class="form-label" i18n>Edit permissions</label>
|
||||||
|
<div>
|
||||||
|
<div class="row mb-1">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2 text-nowrap" i18n>Users:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<pngx-permissions-user type="change" formControlName="remove_change_users"></pngx-permissions-user>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
<label class="form-label d-block my-2 text-nowrap" i18n>Groups:</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-9">
|
||||||
|
<pngx-permissions-group type="change" formControlName="remove_change_groups"></pngx-permissions-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (formGroup.get('type').value === WorkflowTriggerType.DocumentAdded || formGroup.get('type').value === WorkflowTriggerType.DocumentUpdated) {
|
|
||||||
<div class="col-md-6">
|
|
||||||
<pngx-input-tags [allowCreate]="false" i18n-title title="Has tags" formControlName="filter_has_tags"></pngx-input-tags>
|
|
||||||
<pngx-input-select i18n-title title="Has correspondent" [items]="correspondents" [allowNull]="true" formControlName="filter_has_correspondent"></pngx-input-select>
|
|
||||||
<pngx-input-select i18n-title title="Has document type" [items]="documentTypes" [allowNull]="true" formControlName="filter_has_document_type"></pngx-input-select>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
}
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -235,4 +235,103 @@ describe('WorkflowEditDialogComponent', () => {
|
|||||||
MATCHING_ALGORITHMS.find((a) => a.id === MATCH_AUTO)
|
MATCHING_ALGORITHMS.find((a) => a.id === MATCH_AUTO)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should disable or enable action fields based on removal action type', () => {
|
||||||
|
const workflow: Workflow = {
|
||||||
|
name: 'Workflow 1',
|
||||||
|
id: 1,
|
||||||
|
order: 1,
|
||||||
|
enabled: true,
|
||||||
|
triggers: [],
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
type: WorkflowActionType.Removal,
|
||||||
|
remove_all_tags: true,
|
||||||
|
remove_all_document_types: true,
|
||||||
|
remove_all_correspondents: true,
|
||||||
|
remove_all_storage_paths: true,
|
||||||
|
remove_all_custom_fields: true,
|
||||||
|
remove_all_owners: true,
|
||||||
|
remove_all_permissions: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
component.object = workflow
|
||||||
|
component.ngOnInit()
|
||||||
|
|
||||||
|
component['checkRemovalActionFields'](workflow)
|
||||||
|
|
||||||
|
// Assert that the action fields are disabled or enabled correctly
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_tags').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_document_types').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_correspondents').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_storage_paths').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_custom_fields').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_owners').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_view_users').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_view_groups').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_change_users').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_change_groups').disabled
|
||||||
|
).toBeTruthy()
|
||||||
|
|
||||||
|
workflow.actions[0].remove_all_tags = false
|
||||||
|
workflow.actions[0].remove_all_document_types = false
|
||||||
|
workflow.actions[0].remove_all_correspondents = false
|
||||||
|
workflow.actions[0].remove_all_storage_paths = false
|
||||||
|
workflow.actions[0].remove_all_custom_fields = false
|
||||||
|
workflow.actions[0].remove_all_owners = false
|
||||||
|
workflow.actions[0].remove_all_permissions = false
|
||||||
|
|
||||||
|
component['checkRemovalActionFields'](workflow)
|
||||||
|
|
||||||
|
// Assert that the action fields are disabled or enabled correctly
|
||||||
|
expect(component.actionFields.at(0).get('remove_tags').disabled).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_document_types').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_correspondents').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_storage_paths').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_custom_fields').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_owners').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_view_users').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_view_groups').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_change_users').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
component.actionFields.at(0).get('remove_change_groups').disabled
|
||||||
|
).toBeFalsy()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -68,6 +68,10 @@ export const WORKFLOW_ACTION_OPTIONS = [
|
|||||||
id: WorkflowActionType.Assignment,
|
id: WorkflowActionType.Assignment,
|
||||||
name: $localize`Assignment`,
|
name: $localize`Assignment`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: WorkflowActionType.Removal,
|
||||||
|
name: $localize`Removal`,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
|
const TRIGGER_MATCHING_ALGORITHMS = MATCHING_ALGORITHMS.filter(
|
||||||
@ -84,6 +88,7 @@ export class WorkflowEditDialogComponent
|
|||||||
implements OnInit
|
implements OnInit
|
||||||
{
|
{
|
||||||
public WorkflowTriggerType = WorkflowTriggerType
|
public WorkflowTriggerType = WorkflowTriggerType
|
||||||
|
public WorkflowActionType = WorkflowActionType
|
||||||
|
|
||||||
templates: Workflow[]
|
templates: Workflow[]
|
||||||
correspondents: Correspondent[]
|
correspondents: Correspondent[]
|
||||||
@ -159,6 +164,124 @@ export class WorkflowEditDialogComponent
|
|||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit()
|
super.ngOnInit()
|
||||||
this.updateAllTriggerActionFields()
|
this.updateAllTriggerActionFields()
|
||||||
|
this.objectForm.valueChanges.subscribe(
|
||||||
|
this.checkRemovalActionFields.bind(this)
|
||||||
|
)
|
||||||
|
this.checkRemovalActionFields(this.objectForm.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkRemovalActionFields(formWorkflow: Workflow) {
|
||||||
|
formWorkflow.actions
|
||||||
|
.filter((action) => action.type === WorkflowActionType.Removal)
|
||||||
|
.forEach((action, i) => {
|
||||||
|
if (action.remove_all_tags) {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_tags')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
} else {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_tags')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.remove_all_document_types) {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_document_types')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
} else {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_document_types')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.remove_all_correspondents) {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_correspondents')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
} else {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_correspondents')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.remove_all_storage_paths) {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_storage_paths')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
} else {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_storage_paths')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.remove_all_custom_fields) {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_custom_fields')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
} else {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_custom_fields')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.remove_all_owners) {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_owners')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
} else {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_owners')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action.remove_all_permissions) {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_view_users')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_view_groups')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_change_users')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_change_groups')
|
||||||
|
.disable({ emitEvent: false })
|
||||||
|
} else {
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_view_users')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_view_groups')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_change_users')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
this.actionFields
|
||||||
|
.at(i)
|
||||||
|
.get('remove_change_groups')
|
||||||
|
.enable({ emitEvent: false })
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get triggerFields(): FormArray {
|
get triggerFields(): FormArray {
|
||||||
@ -215,6 +338,31 @@ export class WorkflowEditDialogComponent
|
|||||||
assign_change_users: new FormControl(action.assign_change_users),
|
assign_change_users: new FormControl(action.assign_change_users),
|
||||||
assign_change_groups: new FormControl(action.assign_change_groups),
|
assign_change_groups: new FormControl(action.assign_change_groups),
|
||||||
assign_custom_fields: new FormControl(action.assign_custom_fields),
|
assign_custom_fields: new FormControl(action.assign_custom_fields),
|
||||||
|
remove_tags: new FormControl(action.remove_tags),
|
||||||
|
remove_all_tags: new FormControl(action.remove_all_tags),
|
||||||
|
remove_document_types: new FormControl(action.remove_document_types),
|
||||||
|
remove_all_document_types: new FormControl(
|
||||||
|
action.remove_all_document_types
|
||||||
|
),
|
||||||
|
remove_correspondents: new FormControl(action.remove_correspondents),
|
||||||
|
remove_all_correspondents: new FormControl(
|
||||||
|
action.remove_all_correspondents
|
||||||
|
),
|
||||||
|
remove_storage_paths: new FormControl(action.remove_storage_paths),
|
||||||
|
remove_all_storage_paths: new FormControl(
|
||||||
|
action.remove_all_storage_paths
|
||||||
|
),
|
||||||
|
remove_owners: new FormControl(action.remove_owners),
|
||||||
|
remove_all_owners: new FormControl(action.remove_all_owners),
|
||||||
|
remove_view_users: new FormControl(action.remove_view_users),
|
||||||
|
remove_view_groups: new FormControl(action.remove_view_groups),
|
||||||
|
remove_change_users: new FormControl(action.remove_change_users),
|
||||||
|
remove_change_groups: new FormControl(action.remove_change_groups),
|
||||||
|
remove_all_permissions: new FormControl(action.remove_all_permissions),
|
||||||
|
remove_custom_fields: new FormControl(action.remove_custom_fields),
|
||||||
|
remove_all_custom_fields: new FormControl(
|
||||||
|
action.remove_all_custom_fields
|
||||||
|
),
|
||||||
}),
|
}),
|
||||||
{ emitEvent }
|
{ emitEvent }
|
||||||
)
|
)
|
||||||
@ -290,6 +438,23 @@ export class WorkflowEditDialogComponent
|
|||||||
assign_change_users: [],
|
assign_change_users: [],
|
||||||
assign_change_groups: [],
|
assign_change_groups: [],
|
||||||
assign_custom_fields: [],
|
assign_custom_fields: [],
|
||||||
|
remove_tags: [],
|
||||||
|
remove_all_tags: false,
|
||||||
|
remove_document_types: [],
|
||||||
|
remove_all_document_types: false,
|
||||||
|
remove_correspondents: [],
|
||||||
|
remove_all_correspondents: false,
|
||||||
|
remove_storage_paths: [],
|
||||||
|
remove_all_storage_paths: false,
|
||||||
|
remove_owners: [],
|
||||||
|
remove_all_owners: false,
|
||||||
|
remove_view_users: [],
|
||||||
|
remove_view_groups: [],
|
||||||
|
remove_change_users: [],
|
||||||
|
remove_change_groups: [],
|
||||||
|
remove_all_permissions: false,
|
||||||
|
remove_custom_fields: [],
|
||||||
|
remove_all_custom_fields: false,
|
||||||
}
|
}
|
||||||
this.object.actions.push(action)
|
this.object.actions.push(action)
|
||||||
this.createActionField(action)
|
this.createActionField(action)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="paperless-input-select">
|
<div class="paperless-input-select" [class.disabled]="disabled">
|
||||||
<div>
|
<div>
|
||||||
<ng-select name="inputId" [(ngModel)]="value"
|
<ng-select name="inputId" [(ngModel)]="value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
.paperless-input-select.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
::ng-deep ng-select {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.ng-select-container {
|
||||||
|
background-color: var(--pngx-bg-alt) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
<div class="paperless-input-select">
|
<div class="paperless-input-select" [class.disabled]="disabled">
|
||||||
<div>
|
<div>
|
||||||
<ng-select name="inputId" [(ngModel)]="value"
|
<ng-select name="inputId" [(ngModel)]="value"
|
||||||
[disabled]="disabled"
|
[disabled]="disabled"
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
.paperless-input-select.disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
::ng-deep ng-select {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.ng-select-container {
|
||||||
|
background-color: var(--pngx-bg-alt) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
// styles for ng-select child are in styles.scss
|
// styles for ng-select child are in styles.scss
|
||||||
.paperless-input-select.disabled {
|
.paperless-input-select.disabled {
|
||||||
.input-group {
|
.input-group,
|
||||||
|
div > div {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
}
|
}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-toolbar col col-md-auto">
|
<div class="btn-toolbar col col-md-auto gap-2">
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,158 @@
|
|||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="modal-basic-title" i18n>System Status</h5>
|
||||||
|
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
@if (!status) {
|
||||||
|
<div class="w-100 h-100 d-flex align-items-center justify-content-center">
|
||||||
|
<div>
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||||
|
<ng-container i18n>Loading...</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="row row-cols-1 row-cols-md-3 g-3">
|
||||||
|
<div class="col">
|
||||||
|
<div class="card bg-light h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0" i18n>Environment</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="card-text">
|
||||||
|
<dt i18n>Paperless-ngx Version</dt>
|
||||||
|
<dd>{{status.pngx_version}}</dd>
|
||||||
|
<dt i18n>Install Type</dt>
|
||||||
|
<dd>{{status.install_type}}</dd>
|
||||||
|
<dt i18n>Server OS</dt>
|
||||||
|
<dd>{{status.server_os}}</dd>
|
||||||
|
<dt i18n>Media Storage</dt>
|
||||||
|
<dd>
|
||||||
|
<ngb-progressbar style="height: 4px;" class="mt-2 mb-1" type="primary" [max]="status.storage.total" [value]="status.storage.total - status.storage.available"></ngb-progressbar>
|
||||||
|
<span class="small">{{status.storage.available | filesize}} <ng-container i18n>available</ng-container> ({{status.storage.total | filesize}} <ng-container i18n>total</ng-container>)</span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="card bg-light h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0" i18n>Database</h5>
|
||||||
|
</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>
|
||||||
|
<dt i18n>Migration Status</dt>
|
||||||
|
<dd class="d-flex align-items-center">
|
||||||
|
@if (status.database.migration_status.unapplied_migrations.length === 0) {
|
||||||
|
<ng-container>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>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<div class="card bg-light h-100">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0" i18n>Tasks</h5>
|
||||||
|
</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>
|
||||||
|
} @else {
|
||||||
|
<i-bs name="check-circle-fill" class="text-primary ms-2 lh-1" [ngbPopover]="indexStatus" triggers="mouseenter:mouseleave"></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>
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
</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>
|
||||||
|
} @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>
|
||||||
|
}
|
||||||
|
</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>
|
||||||
|
</ng-template>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" (click)="copy()">
|
||||||
|
@if (!copied) {
|
||||||
|
<i-bs name="clipboard-fill"></i-bs>
|
||||||
|
}
|
||||||
|
@if (copied) {
|
||||||
|
<i-bs name="clipboard-check-fill"></i-bs>
|
||||||
|
}
|
||||||
|
<ng-container i18n>Copy</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,103 @@
|
|||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
fakeAsync,
|
||||||
|
tick,
|
||||||
|
} from '@angular/core/testing'
|
||||||
|
import {
|
||||||
|
NgbActiveModal,
|
||||||
|
NgbModalModule,
|
||||||
|
NgbPopoverModule,
|
||||||
|
NgbProgressbarModule,
|
||||||
|
} from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard'
|
||||||
|
import { SystemStatusDialogComponent } from './system-status-dialog.component'
|
||||||
|
import {
|
||||||
|
SystemStatusItemStatus,
|
||||||
|
InstallType,
|
||||||
|
SystemStatus,
|
||||||
|
} from 'src/app/data/system-status'
|
||||||
|
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||||
|
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
|
||||||
|
import { NgxFilesizeModule } from 'ngx-filesize'
|
||||||
|
|
||||||
|
const status: SystemStatus = {
|
||||||
|
pngx_version: '2.4.3',
|
||||||
|
server_os: 'macOS-14.1.1-arm64-arm-64bit',
|
||||||
|
install_type: InstallType.BareMetal,
|
||||||
|
storage: { total: 494384795648, available: 13573525504 },
|
||||||
|
database: {
|
||||||
|
type: 'sqlite',
|
||||||
|
url: '/paperless-ngx/data/db.sqlite3',
|
||||||
|
status: SystemStatusItemStatus.ERROR,
|
||||||
|
error: null,
|
||||||
|
migration_status: {
|
||||||
|
latest_migration: 'socialaccount.0006_alter_socialaccount_extra_data',
|
||||||
|
unapplied_migrations: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tasks: {
|
||||||
|
redis_url: 'redis://localhost:6379',
|
||||||
|
redis_status: SystemStatusItemStatus.ERROR,
|
||||||
|
redis_error: 'Error 61 connecting to localhost:6379. Connection refused.',
|
||||||
|
celery_status: SystemStatusItemStatus.ERROR,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SystemStatusDialogComponent', () => {
|
||||||
|
let component: SystemStatusDialogComponent
|
||||||
|
let fixture: ComponentFixture<SystemStatusDialogComponent>
|
||||||
|
let clipboard: Clipboard
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [SystemStatusDialogComponent],
|
||||||
|
providers: [NgbActiveModal],
|
||||||
|
imports: [
|
||||||
|
NgbModalModule,
|
||||||
|
ClipboardModule,
|
||||||
|
HttpClientTestingModule,
|
||||||
|
NgxBootstrapIconsModule.pick(allIcons),
|
||||||
|
NgxFilesizeModule,
|
||||||
|
NgbPopoverModule,
|
||||||
|
NgbProgressbarModule,
|
||||||
|
],
|
||||||
|
}).compileComponents()
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(SystemStatusDialogComponent)
|
||||||
|
component = fixture.componentInstance
|
||||||
|
component.status = status
|
||||||
|
clipboard = TestBed.inject(Clipboard)
|
||||||
|
fixture.detectChanges()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should close the active modal', () => {
|
||||||
|
const closeSpy = jest.spyOn(component.activeModal, 'close')
|
||||||
|
component.close()
|
||||||
|
expect(closeSpy).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should copy the system status to clipboard', fakeAsync(() => {
|
||||||
|
jest.spyOn(clipboard, 'copy')
|
||||||
|
component.copy()
|
||||||
|
expect(clipboard.copy).toHaveBeenCalledWith(
|
||||||
|
JSON.stringify(component.status)
|
||||||
|
)
|
||||||
|
expect(component.copied).toBeTruthy()
|
||||||
|
tick(3000)
|
||||||
|
expect(component.copied).toBeFalsy()
|
||||||
|
}))
|
||||||
|
|
||||||
|
it('should calculate if date is stale', () => {
|
||||||
|
const date = new Date()
|
||||||
|
date.setHours(date.getHours() - 25)
|
||||||
|
expect(component.isStale(date.toISOString())).toBeTruthy()
|
||||||
|
expect(component.isStale(date.toISOString(), 26)).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,43 @@
|
|||||||
|
import { Component, Input } from '@angular/core'
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
|
import {
|
||||||
|
SystemStatus,
|
||||||
|
SystemStatusItemStatus,
|
||||||
|
} from 'src/app/data/system-status'
|
||||||
|
import { SystemStatusService } from 'src/app/services/system-status.service'
|
||||||
|
import { Clipboard } from '@angular/cdk/clipboard'
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pngx-system-status-dialog',
|
||||||
|
templateUrl: './system-status-dialog.component.html',
|
||||||
|
styleUrl: './system-status-dialog.component.scss',
|
||||||
|
})
|
||||||
|
export class SystemStatusDialogComponent {
|
||||||
|
public SystemStatusItemStatus = SystemStatusItemStatus
|
||||||
|
public status: SystemStatus
|
||||||
|
|
||||||
|
public copied: boolean = false
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public activeModal: NgbActiveModal,
|
||||||
|
private clipboard: Clipboard
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public close() {
|
||||||
|
this.activeModal.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
public copy() {
|
||||||
|
this.clipboard.copy(JSON.stringify(this.status))
|
||||||
|
this.copied = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.copied = false
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
|
||||||
|
public isStale(dateStr: string, hours: number = 24): boolean {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
const now = new Date()
|
||||||
|
return now.getTime() - date.getTime() > hours * 60 * 60 * 1000
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
<pngx-page-header [(title)]="title">
|
<pngx-page-header [(title)]="title">
|
||||||
@if (contentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
|
@if (contentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
|
||||||
<div class="input-group input-group-sm me-2 d-none d-md-flex">
|
<div class="input-group input-group-sm d-none d-md-flex">
|
||||||
<div class="input-group-text" i18n>Page</div>
|
<div class="input-group-text" i18n>Page</div>
|
||||||
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
|
||||||
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group input-group-sm me-5 d-none d-md-flex">
|
<div class="input-group input-group-sm me-md-5 d-none d-md-flex">
|
||||||
<button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button>
|
<button class="btn btn-outline-secondary" (click)="decreaseZoom()" i18n>-</button>
|
||||||
<select class="form-select" (change)="onZoomSelect($event)">
|
<select class="form-select" (change)="onZoomSelect($event)">
|
||||||
@for (setting of zoomSettings; track setting) {
|
@for (setting of zoomSettings; track setting) {
|
||||||
@ -18,11 +18,11 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger me-4" (click)="delete()" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
|
<button type="button" class="btn btn-sm btn-outline-danger me-md-4" (click)="delete()" [disabled]="!userIsOwner" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.Document }">
|
||||||
<i-bs width="1.2em" height="1.2em" name="trash"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
|
<i-bs width="1.2em" height="1.2em" name="trash"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Delete</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="btn-group me-2">
|
<div class="btn-group">
|
||||||
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
|
||||||
<i-bs width="1.2em" height="1.2em" name="download"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
<i-bs width="1.2em" height="1.2em" name="download"></i-bs><span class="d-none d-lg-inline ps-1" i18n>Download</span>
|
||||||
</a>
|
</a>
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ms-auto" ngbDropdown>
|
<div class="ms-auto" ngbDropdown>
|
||||||
<button class="btn btn-sm btn-outline-primary me-2" id="actionsDropdown" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary" id="actionsDropdown" ngbDropdownToggle>
|
||||||
<i-bs name="three-dots"></i-bs>
|
<i-bs name="three-dots"></i-bs>
|
||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
<div class="d-none d-sm-inline"> <ng-container i18n>Actions</ng-container></div>
|
||||||
</button>
|
</button>
|
||||||
@ -55,7 +55,6 @@
|
|||||||
|
|
||||||
<pngx-custom-fields-dropdown
|
<pngx-custom-fields-dropdown
|
||||||
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"
|
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"
|
||||||
class="me-2"
|
|
||||||
[documentId]="documentId"
|
[documentId]="documentId"
|
||||||
[disabled]="!userIsOwner"
|
[disabled]="!userIsOwner"
|
||||||
[existingFields]="document?.custom_fields"
|
[existingFields]="document?.custom_fields"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<div class="card mb-3 shadow-sm" [class.card-selected]="selected" [class.document-card]="selectable" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()">
|
<div class="card mb-3 shadow-sm bg-light" [class.card-selected]="selected" [class.document-card]="selectable" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()">
|
||||||
<div class="row g-0">
|
<div class="row g-0">
|
||||||
<div class="col-md-2 doc-img-background rounded-start" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()">
|
<div class="col-md-2 doc-img-background rounded-start" [class.doc-img-background-selected]="selected" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()">
|
||||||
<img [src]="getThumbUrl()" class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
|
<img [src]="getThumbUrl()" class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
|
||||||
@ -12,8 +12,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="card-body bg-light">
|
<div class="card-body">
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">
|
||||||
@if (document.correspondent) {
|
@if (document.correspondent) {
|
||||||
@ -45,7 +44,6 @@
|
|||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-md-row align-items-md-center">
|
<div class="d-flex flex-column flex-md-row align-items-md-center">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
|
<a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
|
||||||
@ -67,28 +65,27 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
|
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
|
||||||
@if (notesEnabled && document.notes.length) {
|
@if (notesEnabled && document.notes.length) {
|
||||||
<button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
|
<button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
|
||||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="chat-left-text"></i-bs><small i18n>{{document.notes.length}} Notes</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="chat-left-text"></i-bs><small i18n>{{document.notes.length}} Notes</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (document.document_type) {
|
@if (document.document_type) {
|
||||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by document type" i18n-title
|
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by document type" i18n-title
|
||||||
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
|
||||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{(document.document_type$ | async)?.name}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{(document.document_type$ | async)?.name}}</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (document.storage_path) {
|
@if (document.storage_path) {
|
||||||
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by storage path" i18n-title
|
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by storage path" i18n-title
|
||||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{(document.storage_path$ | async)?.name}}</small>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (document.archive_serial_number | isNumber) {
|
@if (document.archive_serial_number | isNumber) {
|
||||||
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
|
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<ng-template #dateTooltip>
|
<ng-template #dateTooltip>
|
||||||
@ -99,27 +96,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
|
||||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created_date | customDate:'mediumDate'}}</small>
|
||||||
</div>
|
</div>
|
||||||
@if (document.owner && document.owner !== settingsService.currentUser.id) {
|
@if (document.owner && document.owner !== settingsService.currentUser.id) {
|
||||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username}}</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username}}</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (document.is_shared_by_requester) {
|
@if (document.is_shared_by_requester) {
|
||||||
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
|
||||||
<i-bs width="09.rem" height="0.9rem" class="me-2 text-muted" name="people-fill"></i-bs><small i18n>Shared</small>
|
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="people-fill"></i-bs><small i18n>Shared</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@if (document.__search_hit__?.score) {
|
@if (document.__search_hit__?.score) {
|
||||||
<div class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
|
<div class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
|
||||||
<small class="text-muted" i18n>Score:</small>
|
<small class="me-2 text-muted" i18n>Score:</small>
|
||||||
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
|
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<pngx-page-header [title]="getTitle()">
|
<pngx-page-header [title]="getTitle()">
|
||||||
|
|
||||||
<div ngbDropdown class="me-2 d-flex">
|
<div ngbDropdown class="d-flex">
|
||||||
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary" id="dropdownSelect" ngbDropdownToggle>
|
||||||
<i-bs name="text-indent-left"></i-bs>
|
<i-bs name="text-indent-left"></i-bs>
|
||||||
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
<div class="d-none d-sm-inline"> <ng-container i18n>Select</ng-container></div>
|
||||||
@ -26,7 +26,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ngbDropdown class="btn-group ms-2 flex-fill">
|
<div ngbDropdown class="btn-group flex-fill">
|
||||||
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button>
|
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle i18n>Sort</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right">
|
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow dropdown-menu-right">
|
||||||
<div class="w-100 d-flex pb-2 mb-1 border-bottom">
|
<div class="w-100 d-flex pb-2 mb-1 border-bottom">
|
||||||
@ -49,7 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="btn-group ms-2 flex-fill" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" ngbDropdown role="group">
|
<div class="btn-group flex-fill" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }" ngbDropdown role="group">
|
||||||
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
<button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle>
|
||||||
<ng-container i18n>Views</ng-container>
|
<ng-container i18n>Views</ng-container>
|
||||||
@if (savedViewIsModified) {
|
@if (savedViewIsModified) {
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
i18n-info
|
i18n-info
|
||||||
infoLink="usage/#custom-fields"
|
infoLink="usage/#custom-fields"
|
||||||
>
|
>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Field</ng-container>
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Field</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<pngx-page-header title="{{ typeNamePlural | titlecase }}">
|
<pngx-page-header title="{{ typeNamePlural | titlecase }}">
|
||||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
|
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
|
||||||
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary me-2" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
|
||||||
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger me-5" (click)="delete()" [disabled]="!userCanBulkEdit(PermissionAction.Delete) || selectedObjects.size === 0">
|
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete()" [disabled]="!userCanBulkEdit(PermissionAction.Delete) || selectedObjects.size === 0">
|
||||||
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
<i-bs name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }">
|
<button type="button" class="btn btn-sm btn-outline-primary ms-md-5" (click)="openCreateDialog()" *pngxIfPermissions="{ action: PermissionAction.Add, type: permissionType }">
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Create</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
@ -60,7 +60,7 @@
|
|||||||
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
<label class="form-check-label" for="{{typeName}}{{object.id}}"></label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td scope="row"><button class="btn btn-link ms-0 ps-0 text-start" (click)="userCanEdit(object) ? openEditDialog(object) : null">{{ object.name }}</button> </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" class="d-none d-sm-table-cell">{{ getMatching(object) }}</td>
|
||||||
<td scope="row">{{ object.document_count }}</td>
|
<td scope="row">{{ object.document_count }}</td>
|
||||||
@for (column of extraColumns; track column) {
|
@for (column of extraColumns; track column) {
|
||||||
@ -75,7 +75,7 @@
|
|||||||
<td scope="row">
|
<td scope="row">
|
||||||
<div class="btn-group d-block d-sm-none">
|
<div class="btn-group d-block d-sm-none">
|
||||||
<div ngbDropdown class="d-inline-block">
|
<div ngbDropdown class="d-inline-block">
|
||||||
<button type="button" class="btn btn-link" id="actionsMenuMobile" ngbDropdownToggle>
|
<button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
|
||||||
<i-bs name="three-dots-vertical"></i-bs>
|
<i-bs name="three-dots-vertical"></i-bs>
|
||||||
</button>
|
</button>
|
||||||
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
i18n-info
|
i18n-info
|
||||||
infoLink="usage/#workflows"
|
infoLink="usage/#workflows"
|
||||||
>
|
>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editWorkflow()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Workflow }">
|
<button type="button" class="btn btn-sm btn-outline-primary" (click)="editWorkflow()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Workflow }">
|
||||||
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Workflow</ng-container>
|
<i-bs name="plus-circle"></i-bs> <ng-container i18n>Add Workflow</ng-container>
|
||||||
</button>
|
</button>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
42
src-ui/src/app/data/system-status.ts
Normal file
42
src-ui/src/app/data/system-status.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
export enum InstallType {
|
||||||
|
Containerized = 'containerized',
|
||||||
|
BareMetal = 'bare-metal',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SystemStatusItemStatus {
|
||||||
|
OK = 'OK',
|
||||||
|
ERROR = 'ERROR',
|
||||||
|
WARNING = 'WARNING',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SystemStatus {
|
||||||
|
pngx_version: string
|
||||||
|
server_os: string
|
||||||
|
install_type: InstallType
|
||||||
|
storage: {
|
||||||
|
total: number
|
||||||
|
available: number
|
||||||
|
}
|
||||||
|
database: {
|
||||||
|
type: string
|
||||||
|
url: string
|
||||||
|
status: SystemStatusItemStatus
|
||||||
|
error?: string
|
||||||
|
migration_status: {
|
||||||
|
latest_migration: string
|
||||||
|
unapplied_migrations: string[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasks: {
|
||||||
|
redis_url: string
|
||||||
|
redis_status: SystemStatusItemStatus
|
||||||
|
redis_error: string
|
||||||
|
celery_status: SystemStatusItemStatus
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { ObjectWithId } from './object-with-id'
|
|||||||
|
|
||||||
export enum WorkflowActionType {
|
export enum WorkflowActionType {
|
||||||
Assignment = 1,
|
Assignment = 1,
|
||||||
|
Removal = 2,
|
||||||
}
|
}
|
||||||
export interface WorkflowAction extends ObjectWithId {
|
export interface WorkflowAction extends ObjectWithId {
|
||||||
type: WorkflowActionType
|
type: WorkflowActionType
|
||||||
@ -27,4 +28,38 @@ export interface WorkflowAction extends ObjectWithId {
|
|||||||
assign_change_groups?: number[] // [Group.id]
|
assign_change_groups?: number[] // [Group.id]
|
||||||
|
|
||||||
assign_custom_fields?: number[] // [CustomField.id]
|
assign_custom_fields?: number[] // [CustomField.id]
|
||||||
|
|
||||||
|
remove_tags?: number[] // Tag.id
|
||||||
|
|
||||||
|
remove_all_tags?: boolean
|
||||||
|
|
||||||
|
remove_document_types?: number[] // [DocumentType.id]
|
||||||
|
|
||||||
|
remove_all_document_types?: boolean
|
||||||
|
|
||||||
|
remove_correspondents?: number[] // [Correspondent.id]
|
||||||
|
|
||||||
|
remove_all_correspondents?: boolean
|
||||||
|
|
||||||
|
remove_storage_paths?: number[] // [StoragePath.id]
|
||||||
|
|
||||||
|
remove_all_storage_paths?: boolean
|
||||||
|
|
||||||
|
remove_owners?: number[] // [User.id]
|
||||||
|
|
||||||
|
remove_all_owners?: boolean
|
||||||
|
|
||||||
|
remove_view_users?: number[] // [User.id]
|
||||||
|
|
||||||
|
remove_view_groups?: number[] // [Group.id]
|
||||||
|
|
||||||
|
remove_change_users?: number[] // [User.id]
|
||||||
|
|
||||||
|
remove_change_groups?: number[] // [Group.id]
|
||||||
|
|
||||||
|
remove_all_permissions?: boolean
|
||||||
|
|
||||||
|
remove_custom_fields?: number[] // [CustomField.id]
|
||||||
|
|
||||||
|
remove_all_custom_fields?: boolean
|
||||||
}
|
}
|
||||||
|
35
src-ui/src/app/services/system-status.service.spec.ts
Normal file
35
src-ui/src/app/services/system-status.service.spec.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing'
|
||||||
|
|
||||||
|
import { SystemStatusService } from './system-status.service'
|
||||||
|
import {
|
||||||
|
HttpClientTestingModule,
|
||||||
|
HttpTestingController,
|
||||||
|
} from '@angular/common/http/testing'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
|
describe('SystemStatusService', () => {
|
||||||
|
let httpTestingController: HttpTestingController
|
||||||
|
let service: SystemStatusService
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [SystemStatusService],
|
||||||
|
imports: [HttpClientTestingModule],
|
||||||
|
})
|
||||||
|
|
||||||
|
httpTestingController = TestBed.inject(HttpTestingController)
|
||||||
|
service = TestBed.inject(SystemStatusService)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpTestingController.verify()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls get status endpoint', () => {
|
||||||
|
service.get().subscribe()
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}status/`
|
||||||
|
)
|
||||||
|
expect(req.request.method).toEqual('GET')
|
||||||
|
})
|
||||||
|
})
|
20
src-ui/src/app/services/system-status.service.ts
Normal file
20
src-ui/src/app/services/system-status.service.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { HttpClient } from '@angular/common/http'
|
||||||
|
import { Injectable } from '@angular/core'
|
||||||
|
import { Observable } from 'rxjs'
|
||||||
|
import { SystemStatus } from '../data/system-status'
|
||||||
|
import { environment } from 'src/environments/environment'
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class SystemStatusService {
|
||||||
|
private endpoint = 'status'
|
||||||
|
|
||||||
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
|
get(): Observable<SystemStatus> {
|
||||||
|
return this.http.get<SystemStatus>(
|
||||||
|
`${environment.apiBaseUrl}${this.endpoint}/`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ export const environment = {
|
|||||||
apiBaseUrl: document.baseURI + 'api/',
|
apiBaseUrl: document.baseURI + 'api/',
|
||||||
apiVersion: '5',
|
apiVersion: '5',
|
||||||
appTitle: 'Paperless-ngx',
|
appTitle: 'Paperless-ngx',
|
||||||
version: '2.5.4-dev',
|
version: '2.6.1-dev',
|
||||||
webSocketHost: window.location.host,
|
webSocketHost: window.location.host,
|
||||||
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
|
||||||
webSocketBaseUrl: base_url.pathname + 'ws/',
|
webSocketBaseUrl: base_url.pathname + 'ws/',
|
||||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -155,7 +155,7 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,<svg xmlns='htt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-cols-paperless-cards .card {
|
.row-cols-paperless-cards .card, pngx-document-card-large .card {
|
||||||
--bs-border-color-translucent: rgba(0, 0, 0, .3);
|
--bs-border-color-translucent: rgba(0, 0, 0, .3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from documents.plugins.base import StopConsumeTaskError
|
|||||||
from documents.plugins.helpers import ProgressStatusOptions
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
from documents.utils import copy_basic_file_stats
|
from documents.utils import copy_basic_file_stats
|
||||||
from documents.utils import copy_file_with_basic_stats
|
from documents.utils import copy_file_with_basic_stats
|
||||||
|
from documents.utils import maybe_override_pixel_limit
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.barcodes")
|
logger = logging.getLogger("paperless.barcodes")
|
||||||
|
|
||||||
@ -81,6 +82,9 @@ class BarcodePlugin(ConsumeTaskPlugin):
|
|||||||
self.barcodes: list[Barcode] = []
|
self.barcodes: list[Barcode] = []
|
||||||
|
|
||||||
def run(self) -> Optional[str]:
|
def run(self) -> Optional[str]:
|
||||||
|
# Some operations may use PIL, override pixel setting if needed
|
||||||
|
maybe_override_pixel_limit()
|
||||||
|
|
||||||
# Maybe do the conversion of TIFF to PDF
|
# Maybe do the conversion of TIFF to PDF
|
||||||
self.convert_from_tiff_to_pdf()
|
self.convert_from_tiff_to_pdf()
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from enum import Enum
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from subprocess import CompletedProcess
|
from subprocess import CompletedProcess
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import magic
|
import magic
|
||||||
@ -35,6 +36,7 @@ from documents.models import FileInfo
|
|||||||
from documents.models import StoragePath
|
from documents.models import StoragePath
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.models import Workflow
|
from documents.models import Workflow
|
||||||
|
from documents.models import WorkflowAction
|
||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
from documents.parsers import DocumentParser
|
from documents.parsers import DocumentParser
|
||||||
from documents.parsers import ParseError
|
from documents.parsers import ParseError
|
||||||
@ -63,9 +65,26 @@ class WorkflowTriggerPlugin(
|
|||||||
"""
|
"""
|
||||||
Get overrides from matching workflows
|
Get overrides from matching workflows
|
||||||
"""
|
"""
|
||||||
|
msg = ""
|
||||||
overrides = DocumentMetadataOverrides()
|
overrides = DocumentMetadataOverrides()
|
||||||
for workflow in Workflow.objects.filter(enabled=True).order_by("order"):
|
for workflow in (
|
||||||
template_overrides = DocumentMetadataOverrides()
|
Workflow.objects.filter(enabled=True)
|
||||||
|
.prefetch_related("actions")
|
||||||
|
.prefetch_related("actions__assign_view_users")
|
||||||
|
.prefetch_related("actions__assign_view_groups")
|
||||||
|
.prefetch_related("actions__assign_change_users")
|
||||||
|
.prefetch_related("actions__assign_change_groups")
|
||||||
|
.prefetch_related("actions__assign_custom_fields")
|
||||||
|
.prefetch_related("actions__remove_tags")
|
||||||
|
.prefetch_related("actions__remove_correspondents")
|
||||||
|
.prefetch_related("actions__remove_document_types")
|
||||||
|
.prefetch_related("actions__remove_storage_paths")
|
||||||
|
.prefetch_related("actions__remove_custom_fields")
|
||||||
|
.prefetch_related("actions__remove_owners")
|
||||||
|
.prefetch_related("triggers")
|
||||||
|
.order_by("order")
|
||||||
|
):
|
||||||
|
action_overrides = DocumentMetadataOverrides()
|
||||||
|
|
||||||
if document_matches_workflow(
|
if document_matches_workflow(
|
||||||
self.input_doc,
|
self.input_doc,
|
||||||
@ -73,49 +92,137 @@ class WorkflowTriggerPlugin(
|
|||||||
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
):
|
):
|
||||||
for action in workflow.actions.all():
|
for action in workflow.actions.all():
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(action, WorkflowAction)
|
||||||
|
msg += f"Applying {action} from {workflow}\n"
|
||||||
|
if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT:
|
||||||
if action.assign_title is not None:
|
if action.assign_title is not None:
|
||||||
template_overrides.title = action.assign_title
|
action_overrides.title = action.assign_title
|
||||||
if action.assign_tags is not None:
|
if action.assign_tags is not None:
|
||||||
template_overrides.tag_ids = [
|
action_overrides.tag_ids = list(
|
||||||
tag.pk for tag in action.assign_tags.all()
|
action.assign_tags.values_list("pk", flat=True),
|
||||||
]
|
)
|
||||||
|
|
||||||
if action.assign_correspondent is not None:
|
if action.assign_correspondent is not None:
|
||||||
template_overrides.correspondent_id = (
|
action_overrides.correspondent_id = (
|
||||||
action.assign_correspondent.pk
|
action.assign_correspondent.pk
|
||||||
)
|
)
|
||||||
if action.assign_document_type is not None:
|
if action.assign_document_type is not None:
|
||||||
template_overrides.document_type_id = (
|
action_overrides.document_type_id = (
|
||||||
action.assign_document_type.pk
|
action.assign_document_type.pk
|
||||||
)
|
)
|
||||||
if action.assign_storage_path is not None:
|
if action.assign_storage_path is not None:
|
||||||
template_overrides.storage_path_id = (
|
action_overrides.storage_path_id = (
|
||||||
action.assign_storage_path.pk
|
action.assign_storage_path.pk
|
||||||
)
|
)
|
||||||
if action.assign_owner is not None:
|
if action.assign_owner is not None:
|
||||||
template_overrides.owner_id = action.assign_owner.pk
|
action_overrides.owner_id = action.assign_owner.pk
|
||||||
if action.assign_view_users is not None:
|
if action.assign_view_users is not None:
|
||||||
template_overrides.view_users = [
|
action_overrides.view_users = list(
|
||||||
user.pk for user in action.assign_view_users.all()
|
action.assign_view_users.values_list("pk", flat=True),
|
||||||
]
|
)
|
||||||
if action.assign_view_groups is not None:
|
if action.assign_view_groups is not None:
|
||||||
template_overrides.view_groups = [
|
action_overrides.view_groups = list(
|
||||||
group.pk for group in action.assign_view_groups.all()
|
action.assign_view_groups.values_list("pk", flat=True),
|
||||||
]
|
)
|
||||||
if action.assign_change_users is not None:
|
if action.assign_change_users is not None:
|
||||||
template_overrides.change_users = [
|
action_overrides.change_users = list(
|
||||||
user.pk for user in action.assign_change_users.all()
|
action.assign_change_users.values_list("pk", flat=True),
|
||||||
]
|
)
|
||||||
if action.assign_change_groups is not None:
|
if action.assign_change_groups is not None:
|
||||||
template_overrides.change_groups = [
|
action_overrides.change_groups = list(
|
||||||
group.pk for group in action.assign_change_groups.all()
|
action.assign_change_groups.values_list(
|
||||||
]
|
"pk",
|
||||||
|
flat=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
if action.assign_custom_fields is not None:
|
if action.assign_custom_fields is not None:
|
||||||
template_overrides.custom_field_ids = [
|
action_overrides.custom_field_ids = list(
|
||||||
field.pk for field in action.assign_custom_fields.all()
|
action.assign_custom_fields.values_list(
|
||||||
]
|
"pk",
|
||||||
|
flat=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
overrides.update(action_overrides)
|
||||||
|
elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
|
||||||
|
# Removal actions overwrite the current overrides
|
||||||
|
if action.remove_all_tags:
|
||||||
|
overrides.tag_ids = []
|
||||||
|
elif overrides.tag_ids:
|
||||||
|
for tag in action.remove_custom_fields.filter(
|
||||||
|
pk__in=overrides.tag_ids,
|
||||||
|
):
|
||||||
|
overrides.tag_ids.remove(tag.pk)
|
||||||
|
|
||||||
|
if action.remove_all_correspondents or (
|
||||||
|
overrides.correspondent_id is not None
|
||||||
|
and action.remove_correspondents.filter(
|
||||||
|
pk=overrides.correspondent_id,
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
|
overrides.correspondent_id = None
|
||||||
|
|
||||||
|
if action.remove_all_document_types or (
|
||||||
|
overrides.document_type_id is not None
|
||||||
|
and action.remove_document_types.filter(
|
||||||
|
pk=overrides.document_type_id,
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
|
overrides.document_type_id = None
|
||||||
|
|
||||||
|
if action.remove_all_storage_paths or (
|
||||||
|
overrides.storage_path_id is not None
|
||||||
|
and action.remove_storage_paths.filter(
|
||||||
|
pk=overrides.storage_path_id,
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
|
overrides.storage_path_id = None
|
||||||
|
|
||||||
|
if action.remove_all_custom_fields:
|
||||||
|
overrides.custom_field_ids = []
|
||||||
|
elif overrides.custom_field_ids:
|
||||||
|
for field in action.remove_custom_fields.filter(
|
||||||
|
pk__in=overrides.custom_field_ids,
|
||||||
|
):
|
||||||
|
overrides.custom_field_ids.remove(field.pk)
|
||||||
|
|
||||||
|
if action.remove_all_owners or (
|
||||||
|
overrides.owner_id is not None
|
||||||
|
and action.remove_owners.filter(
|
||||||
|
pk=overrides.owner_id,
|
||||||
|
).exists()
|
||||||
|
):
|
||||||
|
overrides.owner_id = None
|
||||||
|
|
||||||
|
if action.remove_all_permissions:
|
||||||
|
overrides.view_users = []
|
||||||
|
overrides.view_groups = []
|
||||||
|
overrides.change_users = []
|
||||||
|
overrides.change_groups = []
|
||||||
|
else:
|
||||||
|
if overrides.view_users:
|
||||||
|
for user in action.remove_view_users.filter(
|
||||||
|
pk__in=overrides.view_users,
|
||||||
|
):
|
||||||
|
overrides.view_users.remove(user.pk)
|
||||||
|
if overrides.change_users:
|
||||||
|
for user in action.remove_change_users.filter(
|
||||||
|
pk__in=overrides.change_users,
|
||||||
|
):
|
||||||
|
overrides.change_users.remove(user.pk)
|
||||||
|
if overrides.view_groups:
|
||||||
|
for user in action.remove_view_groups.filter(
|
||||||
|
pk__in=overrides.view_groups,
|
||||||
|
):
|
||||||
|
overrides.view_groups.remove(user.pk)
|
||||||
|
if overrides.change_groups:
|
||||||
|
for user in action.remove_change_groups.filter(
|
||||||
|
pk__in=overrides.change_groups,
|
||||||
|
):
|
||||||
|
overrides.change_groups.remove(user.pk)
|
||||||
|
|
||||||
overrides.update(template_overrides)
|
|
||||||
self.metadata.update(overrides)
|
self.metadata.update(overrides)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
|
||||||
class ConsumerError(Exception):
|
class ConsumerError(Exception):
|
||||||
|
@ -6,6 +6,7 @@ from django.conf import settings
|
|||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from documents.utils import copy_basic_file_stats
|
from documents.utils import copy_basic_file_stats
|
||||||
|
from documents.utils import maybe_override_pixel_limit
|
||||||
|
|
||||||
|
|
||||||
def convert_from_tiff_to_pdf(tiff_path: Path, target_directory: Path) -> Path:
|
def convert_from_tiff_to_pdf(tiff_path: Path, target_directory: Path) -> Path:
|
||||||
@ -17,6 +18,9 @@ def convert_from_tiff_to_pdf(tiff_path: Path, target_directory: Path) -> Path:
|
|||||||
|
|
||||||
Returns the path of the PDF created.
|
Returns the path of the PDF created.
|
||||||
"""
|
"""
|
||||||
|
# override pixel setting if needed
|
||||||
|
maybe_override_pixel_limit()
|
||||||
|
|
||||||
with Image.open(tiff_path) as im:
|
with Image.open(tiff_path) as im:
|
||||||
has_alpha_layer = im.mode in ("RGBA", "LA")
|
has_alpha_layer = im.mode in ("RGBA", "LA")
|
||||||
if has_alpha_layer:
|
if has_alpha_layer:
|
||||||
|
@ -0,0 +1,223 @@
|
|||||||
|
# Generated by Django 4.2.10 on 2024-02-21 21:19
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("auth", "0012_alter_user_first_name_max_length"),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("documents", "1045_alter_customfieldinstance_value_monetary"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_all_correspondents",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="remove all correspondents",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_all_custom_fields",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="remove all custom fields",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_all_document_types",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="remove all document types",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_all_owners",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="remove all owners"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_all_permissions",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="remove all permissions",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_all_storage_paths",
|
||||||
|
field=models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="remove all storage paths",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_all_tags",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="remove all tags"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_change_groups",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="auth.group",
|
||||||
|
verbose_name="remove change permissions for these groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_change_users",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="remove change permissions for these users",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_correspondents",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.correspondent",
|
||||||
|
verbose_name="remove these correspondent(s)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_custom_fields",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.customfield",
|
||||||
|
verbose_name="remove these custom fields",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_document_types",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.documenttype",
|
||||||
|
verbose_name="remove these document type(s)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_owners",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="remove these owner(s)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_storage_paths",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.storagepath",
|
||||||
|
verbose_name="remove these storage path(s)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_tags",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.tag",
|
||||||
|
verbose_name="remove these tag(s)",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_view_groups",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="auth.group",
|
||||||
|
verbose_name="remove view permissions for these groups",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="remove_view_users",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="remove view permissions for these users",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="assign_correspondent",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.correspondent",
|
||||||
|
verbose_name="assign this correspondent",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="assign_document_type",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.documenttype",
|
||||||
|
verbose_name="assign this document type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="assign_storage_path",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.storagepath",
|
||||||
|
verbose_name="assign this storage path",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="assign_tags",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
to="documents.tag",
|
||||||
|
verbose_name="assign this tag",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="workflowaction",
|
||||||
|
name="type",
|
||||||
|
field=models.PositiveIntegerField(
|
||||||
|
choices=[(1, "Assignment"), (2, "Removal")],
|
||||||
|
default=1,
|
||||||
|
verbose_name="Workflow Action Type",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -997,7 +997,14 @@ class WorkflowTrigger(models.Model):
|
|||||||
|
|
||||||
class WorkflowAction(models.Model):
|
class WorkflowAction(models.Model):
|
||||||
class WorkflowActionType(models.IntegerChoices):
|
class WorkflowActionType(models.IntegerChoices):
|
||||||
ASSIGNMENT = 1, _("Assignment")
|
ASSIGNMENT = (
|
||||||
|
1,
|
||||||
|
_("Assignment"),
|
||||||
|
)
|
||||||
|
REMOVAL = (
|
||||||
|
2,
|
||||||
|
_("Removal"),
|
||||||
|
)
|
||||||
|
|
||||||
type = models.PositiveIntegerField(
|
type = models.PositiveIntegerField(
|
||||||
_("Workflow Action Type"),
|
_("Workflow Action Type"),
|
||||||
@ -1019,6 +1026,7 @@ class WorkflowAction(models.Model):
|
|||||||
assign_tags = models.ManyToManyField(
|
assign_tags = models.ManyToManyField(
|
||||||
Tag,
|
Tag,
|
||||||
blank=True,
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
verbose_name=_("assign this tag"),
|
verbose_name=_("assign this tag"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1027,6 +1035,7 @@ class WorkflowAction(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
verbose_name=_("assign this document type"),
|
verbose_name=_("assign this document type"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1035,6 +1044,7 @@ class WorkflowAction(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
verbose_name=_("assign this correspondent"),
|
verbose_name=_("assign this correspondent"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1043,6 +1053,7 @@ class WorkflowAction(models.Model):
|
|||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="+",
|
||||||
verbose_name=_("assign this storage path"),
|
verbose_name=_("assign this storage path"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1090,6 +1101,111 @@ class WorkflowAction(models.Model):
|
|||||||
verbose_name=_("assign these custom fields"),
|
verbose_name=_("assign these custom fields"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
remove_tags = models.ManyToManyField(
|
||||||
|
Tag,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove these tag(s)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_all_tags = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("remove all tags"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_document_types = models.ManyToManyField(
|
||||||
|
DocumentType,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove these document type(s)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_all_document_types = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("remove all document types"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_correspondents = models.ManyToManyField(
|
||||||
|
Correspondent,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove these correspondent(s)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_all_correspondents = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("remove all correspondents"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_storage_paths = models.ManyToManyField(
|
||||||
|
StoragePath,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove these storage path(s)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_all_storage_paths = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("remove all storage paths"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_owners = models.ManyToManyField(
|
||||||
|
User,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove these owner(s)"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_all_owners = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("remove all owners"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_view_users = models.ManyToManyField(
|
||||||
|
User,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove view permissions for these users"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_view_groups = models.ManyToManyField(
|
||||||
|
Group,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove view permissions for these groups"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_change_users = models.ManyToManyField(
|
||||||
|
User,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove change permissions for these users"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_change_groups = models.ManyToManyField(
|
||||||
|
Group,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove change permissions for these groups"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_all_permissions = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("remove all permissions"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_custom_fields = models.ManyToManyField(
|
||||||
|
CustomField,
|
||||||
|
blank=True,
|
||||||
|
related_name="+",
|
||||||
|
verbose_name=_("remove these custom fields"),
|
||||||
|
)
|
||||||
|
|
||||||
|
remove_all_custom_fields = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name=_("remove all custom fields"),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("workflow action")
|
verbose_name = _("workflow action")
|
||||||
verbose_name_plural = _("workflow actions")
|
verbose_name_plural = _("workflow actions")
|
||||||
|
@ -1471,6 +1471,23 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
|
|||||||
"assign_change_users",
|
"assign_change_users",
|
||||||
"assign_change_groups",
|
"assign_change_groups",
|
||||||
"assign_custom_fields",
|
"assign_custom_fields",
|
||||||
|
"remove_all_tags",
|
||||||
|
"remove_tags",
|
||||||
|
"remove_all_correspondents",
|
||||||
|
"remove_correspondents",
|
||||||
|
"remove_all_document_types",
|
||||||
|
"remove_document_types",
|
||||||
|
"remove_all_storage_paths",
|
||||||
|
"remove_storage_paths",
|
||||||
|
"remove_custom_fields",
|
||||||
|
"remove_all_custom_fields",
|
||||||
|
"remove_all_owners",
|
||||||
|
"remove_owners",
|
||||||
|
"remove_all_permissions",
|
||||||
|
"remove_view_users",
|
||||||
|
"remove_view_groups",
|
||||||
|
"remove_change_users",
|
||||||
|
"remove_change_groups",
|
||||||
]
|
]
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
@ -1551,10 +1568,22 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
|||||||
assign_change_users = action.pop("assign_change_users", None)
|
assign_change_users = action.pop("assign_change_users", None)
|
||||||
assign_change_groups = action.pop("assign_change_groups", None)
|
assign_change_groups = action.pop("assign_change_groups", None)
|
||||||
assign_custom_fields = action.pop("assign_custom_fields", None)
|
assign_custom_fields = action.pop("assign_custom_fields", None)
|
||||||
|
remove_tags = action.pop("remove_tags", None)
|
||||||
|
remove_correspondents = action.pop("remove_correspondents", None)
|
||||||
|
remove_document_types = action.pop("remove_document_types", None)
|
||||||
|
remove_storage_paths = action.pop("remove_storage_paths", None)
|
||||||
|
remove_custom_fields = action.pop("remove_custom_fields", None)
|
||||||
|
remove_owners = action.pop("remove_owners", None)
|
||||||
|
remove_view_users = action.pop("remove_view_users", None)
|
||||||
|
remove_view_groups = action.pop("remove_view_groups", None)
|
||||||
|
remove_change_users = action.pop("remove_change_users", None)
|
||||||
|
remove_change_groups = action.pop("remove_change_groups", None)
|
||||||
|
|
||||||
action_instance, _ = WorkflowAction.objects.update_or_create(
|
action_instance, _ = WorkflowAction.objects.update_or_create(
|
||||||
id=action.get("id"),
|
id=action.get("id"),
|
||||||
defaults=action,
|
defaults=action,
|
||||||
)
|
)
|
||||||
|
|
||||||
if assign_tags is not None:
|
if assign_tags is not None:
|
||||||
action_instance.assign_tags.set(assign_tags)
|
action_instance.assign_tags.set(assign_tags)
|
||||||
if assign_view_users is not None:
|
if assign_view_users is not None:
|
||||||
@ -1567,6 +1596,27 @@ class WorkflowSerializer(serializers.ModelSerializer):
|
|||||||
action_instance.assign_change_groups.set(assign_change_groups)
|
action_instance.assign_change_groups.set(assign_change_groups)
|
||||||
if assign_custom_fields is not None:
|
if assign_custom_fields is not None:
|
||||||
action_instance.assign_custom_fields.set(assign_custom_fields)
|
action_instance.assign_custom_fields.set(assign_custom_fields)
|
||||||
|
if remove_tags is not None:
|
||||||
|
action_instance.remove_tags.set(remove_tags)
|
||||||
|
if remove_correspondents is not None:
|
||||||
|
action_instance.remove_correspondents.set(remove_correspondents)
|
||||||
|
if remove_document_types is not None:
|
||||||
|
action_instance.remove_document_types.set(remove_document_types)
|
||||||
|
if remove_storage_paths is not None:
|
||||||
|
action_instance.remove_storage_paths.set(remove_storage_paths)
|
||||||
|
if remove_custom_fields is not None:
|
||||||
|
action_instance.remove_custom_fields.set(remove_custom_fields)
|
||||||
|
if remove_owners is not None:
|
||||||
|
action_instance.remove_owners.set(remove_owners)
|
||||||
|
if remove_view_users is not None:
|
||||||
|
action_instance.remove_view_users.set(remove_view_users)
|
||||||
|
if remove_view_groups is not None:
|
||||||
|
action_instance.remove_view_groups.set(remove_view_groups)
|
||||||
|
if remove_change_users is not None:
|
||||||
|
action_instance.remove_change_users.set(remove_change_users)
|
||||||
|
if remove_change_groups is not None:
|
||||||
|
action_instance.remove_change_groups.set(remove_change_groups)
|
||||||
|
|
||||||
set_actions.append(action_instance)
|
set_actions.append(action_instance)
|
||||||
|
|
||||||
instance.triggers.set(set_triggers)
|
instance.triggers.set(set_triggers)
|
||||||
|
@ -20,6 +20,7 @@ from django.db.models import Q
|
|||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from filelock import FileLock
|
from filelock import FileLock
|
||||||
|
from guardian.shortcuts import remove_perm
|
||||||
|
|
||||||
from documents import matching
|
from documents import matching
|
||||||
from documents.caching import clear_metadata_cache
|
from documents.caching import clear_metadata_cache
|
||||||
@ -34,6 +35,7 @@ from documents.models import MatchingModel
|
|||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
from documents.models import Tag
|
from documents.models import Tag
|
||||||
from documents.models import Workflow
|
from documents.models import Workflow
|
||||||
|
from documents.models import WorkflowAction
|
||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
from documents.permissions import get_objects_for_user_owner_aware
|
from documents.permissions import get_objects_for_user_owner_aware
|
||||||
from documents.permissions import set_permissions_for_object
|
from documents.permissions import set_permissions_for_object
|
||||||
@ -529,20 +531,39 @@ def run_workflow(
|
|||||||
document: Document,
|
document: Document,
|
||||||
logging_group=None,
|
logging_group=None,
|
||||||
):
|
):
|
||||||
for workflow in Workflow.objects.filter(
|
for workflow in (
|
||||||
|
Workflow.objects.filter(
|
||||||
enabled=True,
|
enabled=True,
|
||||||
triggers__type=trigger_type,
|
triggers__type=trigger_type,
|
||||||
).order_by("order"):
|
)
|
||||||
|
.prefetch_related("actions")
|
||||||
|
.prefetch_related("actions__assign_view_users")
|
||||||
|
.prefetch_related("actions__assign_view_groups")
|
||||||
|
.prefetch_related("actions__assign_change_users")
|
||||||
|
.prefetch_related("actions__assign_change_groups")
|
||||||
|
.prefetch_related("actions__assign_custom_fields")
|
||||||
|
.prefetch_related("actions__remove_tags")
|
||||||
|
.prefetch_related("actions__remove_correspondents")
|
||||||
|
.prefetch_related("actions__remove_document_types")
|
||||||
|
.prefetch_related("actions__remove_storage_paths")
|
||||||
|
.prefetch_related("actions__remove_custom_fields")
|
||||||
|
.prefetch_related("actions__remove_owners")
|
||||||
|
.prefetch_related("triggers")
|
||||||
|
.order_by("order")
|
||||||
|
):
|
||||||
if matching.document_matches_workflow(
|
if matching.document_matches_workflow(
|
||||||
document,
|
document,
|
||||||
workflow,
|
workflow,
|
||||||
trigger_type,
|
trigger_type,
|
||||||
):
|
):
|
||||||
|
action: WorkflowAction
|
||||||
for action in workflow.actions.all():
|
for action in workflow.actions.all():
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Applying {action} from {workflow}",
|
f"Applying {action} from {workflow}",
|
||||||
extra={"group": logging_group},
|
extra={"group": logging_group},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if action.type == WorkflowAction.WorkflowActionType.ASSIGNMENT:
|
||||||
if action.assign_tags.all().count() > 0:
|
if action.assign_tags.all().count() > 0:
|
||||||
document.tags.add(*action.assign_tags.all())
|
document.tags.add(*action.assign_tags.all())
|
||||||
|
|
||||||
@ -611,13 +632,19 @@ def run_workflow(
|
|||||||
):
|
):
|
||||||
permissions = {
|
permissions = {
|
||||||
"view": {
|
"view": {
|
||||||
"users": action.assign_view_users.all().values_list("id")
|
"users": action.assign_view_users.all().values_list(
|
||||||
|
"id",
|
||||||
|
)
|
||||||
or [],
|
or [],
|
||||||
"groups": action.assign_view_groups.all().values_list("id")
|
"groups": action.assign_view_groups.all().values_list(
|
||||||
|
"id",
|
||||||
|
)
|
||||||
or [],
|
or [],
|
||||||
},
|
},
|
||||||
"change": {
|
"change": {
|
||||||
"users": action.assign_change_users.all().values_list("id")
|
"users": action.assign_change_users.all().values_list(
|
||||||
|
"id",
|
||||||
|
)
|
||||||
or [],
|
or [],
|
||||||
"groups": action.assign_change_groups.all().values_list(
|
"groups": action.assign_change_groups.all().values_list(
|
||||||
"id",
|
"id",
|
||||||
@ -646,6 +673,90 @@ def run_workflow(
|
|||||||
document=document,
|
document=document,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
elif action.type == WorkflowAction.WorkflowActionType.REMOVAL:
|
||||||
|
if action.remove_all_tags:
|
||||||
|
document.tags.clear()
|
||||||
|
else:
|
||||||
|
for tag in action.remove_tags.filter(
|
||||||
|
pk__in=list(document.tags.values_list("pk", flat=True)),
|
||||||
|
).all():
|
||||||
|
document.tags.remove(tag.pk)
|
||||||
|
|
||||||
|
if action.remove_all_correspondents or (
|
||||||
|
document.correspondent
|
||||||
|
and (
|
||||||
|
action.remove_correspondents.filter(
|
||||||
|
pk=document.correspondent.pk,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
):
|
||||||
|
document.correspondent = None
|
||||||
|
|
||||||
|
if action.remove_all_document_types or (
|
||||||
|
document.document_type
|
||||||
|
and (
|
||||||
|
action.remove_document_types.filter(
|
||||||
|
pk=document.document_type.pk,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
):
|
||||||
|
document.document_type = None
|
||||||
|
|
||||||
|
if action.remove_all_storage_paths or (
|
||||||
|
document.storage_path
|
||||||
|
and (
|
||||||
|
action.remove_storage_paths.filter(
|
||||||
|
pk=document.storage_path.pk,
|
||||||
|
).exists()
|
||||||
|
)
|
||||||
|
):
|
||||||
|
document.storage_path = None
|
||||||
|
|
||||||
|
if action.remove_all_owners or (
|
||||||
|
document.owner
|
||||||
|
and (action.remove_owners.filter(pk=document.owner.pk).exists())
|
||||||
|
):
|
||||||
|
document.owner = None
|
||||||
|
|
||||||
|
if action.remove_all_permissions:
|
||||||
|
permissions = {
|
||||||
|
"view": {
|
||||||
|
"users": [],
|
||||||
|
"groups": [],
|
||||||
|
},
|
||||||
|
"change": {
|
||||||
|
"users": [],
|
||||||
|
"groups": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
set_permissions_for_object(
|
||||||
|
permissions=permissions,
|
||||||
|
object=document,
|
||||||
|
merge=False,
|
||||||
|
)
|
||||||
|
elif (
|
||||||
|
(action.remove_view_users.all().count() > 0)
|
||||||
|
or (action.remove_view_groups.all().count() > 0)
|
||||||
|
or (action.remove_change_users.all().count() > 0)
|
||||||
|
or (action.remove_change_groups.all().count() > 0)
|
||||||
|
):
|
||||||
|
for user in action.remove_view_users.all():
|
||||||
|
remove_perm("view_document", user, document)
|
||||||
|
for user in action.remove_change_users.all():
|
||||||
|
remove_perm("change_document", user, document)
|
||||||
|
for group in action.remove_view_groups.all():
|
||||||
|
remove_perm("view_document", group, document)
|
||||||
|
for group in action.remove_change_groups.all():
|
||||||
|
remove_perm("change_document", group, document)
|
||||||
|
|
||||||
|
if action.remove_all_custom_fields:
|
||||||
|
CustomFieldInstance.objects.filter(document=document).delete()
|
||||||
|
elif action.remove_custom_fields.all().count() > 0:
|
||||||
|
CustomFieldInstance.objects.filter(
|
||||||
|
field__in=action.remove_custom_fields.all(),
|
||||||
|
document=document,
|
||||||
|
).delete()
|
||||||
|
|
||||||
document.save()
|
document.save()
|
||||||
|
|
||||||
|
|
||||||
|
238
src/documents/tests/test_api_status.py
Normal file
238
src/documents/tests/test_api_status.py
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
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 paperless import version
|
||||||
|
|
||||||
|
|
||||||
|
class TestSystemStatus(APITestCase):
|
||||||
|
ENDPOINT = "/api/status/"
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create_superuser(
|
||||||
|
username="temp_admin",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_system_status(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A user is logged in
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains relevant system status information
|
||||||
|
"""
|
||||||
|
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["pngx_version"], version.__full_version_str__)
|
||||||
|
self.assertIsNotNone(response.data["server_os"])
|
||||||
|
self.assertEqual(response.data["install_type"], "bare-metal")
|
||||||
|
self.assertIsNotNone(response.data["storage"]["total"])
|
||||||
|
self.assertIsNotNone(response.data["storage"]["available"])
|
||||||
|
self.assertEqual(response.data["database"]["type"], "sqlite")
|
||||||
|
self.assertIsNotNone(response.data["database"]["url"])
|
||||||
|
self.assertEqual(response.data["database"]["status"], "OK")
|
||||||
|
self.assertIsNone(response.data["database"]["error"])
|
||||||
|
self.assertIsNotNone(response.data["database"]["migration_status"])
|
||||||
|
self.assertEqual(response.data["tasks"]["redis_url"], "redis://localhost:6379")
|
||||||
|
self.assertEqual(response.data["tasks"]["redis_status"], "ERROR")
|
||||||
|
self.assertIsNotNone(response.data["tasks"]["redis_error"])
|
||||||
|
|
||||||
|
def test_system_status_insufficient_permissions(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A user is not logged in or does not have permissions
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains a 401 status code or a 403 status code
|
||||||
|
"""
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
||||||
|
normal_user = User.objects.create_user(username="normal_user")
|
||||||
|
self.client.force_login(normal_user)
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||||
|
|
||||||
|
def test_system_status_container_detection(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The application is running in a containerized environment
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains the correct install type
|
||||||
|
"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
os.environ["PNGX_CONTAINERIZED"] = "1"
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data["install_type"], "docker")
|
||||||
|
os.environ["KUBERNETES_SERVICE_HOST"] = "http://localhost"
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
self.assertEqual(response.data["install_type"], "kubernetes")
|
||||||
|
|
||||||
|
@mock.patch("redis.Redis.execute_command")
|
||||||
|
def test_system_status_redis_ping(self, mock_ping):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Redies ping returns True
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains the correct redis status
|
||||||
|
"""
|
||||||
|
mock_ping.return_value = True
|
||||||
|
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"]["redis_status"], "OK")
|
||||||
|
|
||||||
|
@mock.patch("celery.app.control.Inspect.ping")
|
||||||
|
def test_system_status_celery_ping(self, mock_ping):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Celery ping returns pong
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains the correct celery status
|
||||||
|
"""
|
||||||
|
mock_ping.return_value = {"hostname": {"ok": "pong"}}
|
||||||
|
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"]["celery_status"], "OK")
|
||||||
|
|
||||||
|
@override_settings(INDEX_DIR=Path("/tmp/index"))
|
||||||
|
@mock.patch("whoosh.index.FileIndex.last_modified")
|
||||||
|
def test_system_status_index_ok(self, mock_last_modified):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The index last modified time is set
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains the correct index status
|
||||||
|
"""
|
||||||
|
mock_last_modified.return_value = 1707839087
|
||||||
|
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"]["index_status"], "OK")
|
||||||
|
self.assertIsNotNone(response.data["tasks"]["index_last_modified"])
|
||||||
|
|
||||||
|
@override_settings(INDEX_DIR="/tmp/index/")
|
||||||
|
@mock.patch("documents.index.open_index", autospec=True)
|
||||||
|
def test_system_status_index_error(self, mock_open_index):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The index is not found
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains the correct index status
|
||||||
|
"""
|
||||||
|
mock_open_index.return_value = None
|
||||||
|
mock_open_index.side_effect = Exception("Index error")
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
mock_open_index.assert_called_once()
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
self.assertEqual(response.data["tasks"]["index_status"], "ERROR")
|
||||||
|
self.assertIsNotNone(response.data["tasks"]["index_error"])
|
||||||
|
|
||||||
|
@override_settings(DATA_DIR="/tmp/does_not_exist/data/")
|
||||||
|
def test_system_status_classifier_ok(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The classifier is found
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains an OK classifier status
|
||||||
|
"""
|
||||||
|
load_classifier()
|
||||||
|
test_classifier = DocumentClassifier()
|
||||||
|
test_classifier.save()
|
||||||
|
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")
|
||||||
|
self.assertIsNone(response.data["tasks"]["classifier_error"])
|
||||||
|
|
||||||
|
def test_system_status_classifier_warning(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The classifier does not exist yet
|
||||||
|
- > 0 documents and tags with auto matching exist
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains an WARNING classifier status
|
||||||
|
"""
|
||||||
|
with override_settings(MODEL_FILE="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"])
|
||||||
|
|
||||||
|
def test_system_status_classifier_error(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The classifier does exist but is corrupt
|
||||||
|
- > 0 documents and tags with auto matching exist
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains an ERROR classifier status
|
||||||
|
"""
|
||||||
|
does_exist = tempfile.NamedTemporaryFile(
|
||||||
|
dir="/tmp",
|
||||||
|
delete=False,
|
||||||
|
)
|
||||||
|
with override_settings(MODEL_FILE=does_exist):
|
||||||
|
with mock.patch("documents.classifier.load_classifier") as mock_load:
|
||||||
|
mock_load.side_effect = ClassifierModelCorruptError()
|
||||||
|
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"])
|
||||||
|
|
||||||
|
def test_system_status_classifier_ok_no_objects(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The classifier does not exist (and should not)
|
||||||
|
- No documents nor objects with auto matching exist
|
||||||
|
WHEN:
|
||||||
|
- The user requests the system status
|
||||||
|
THEN:
|
||||||
|
- The response contains an OK classifier status
|
||||||
|
"""
|
||||||
|
with override_settings(MODEL_FILE="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")
|
@ -202,6 +202,19 @@ class TestApiWorkflows(DirectoriesMixin, APITestCase):
|
|||||||
"assign_change_groups": [self.group1.id],
|
"assign_change_groups": [self.group1.id],
|
||||||
"assign_custom_fields": [self.cf2.id],
|
"assign_custom_fields": [self.cf2.id],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": WorkflowAction.WorkflowActionType.REMOVAL,
|
||||||
|
"remove_tags": [self.t3.id],
|
||||||
|
"remove_document_types": [self.dt.id],
|
||||||
|
"remove_correspondents": [self.c.id],
|
||||||
|
"remove_storage_paths": [self.sp.id],
|
||||||
|
"remove_custom_fields": [self.cf1.id],
|
||||||
|
"remove_owners": [self.user2.id],
|
||||||
|
"remove_view_users": [self.user3.id],
|
||||||
|
"remove_change_users": [self.user3.id],
|
||||||
|
"remove_view_groups": [self.group1.id],
|
||||||
|
"remove_change_groups": [self.group1.id],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -1223,3 +1223,332 @@ class TestWorkflows(DirectoriesMixin, FileSystemAssertsMixin, APITestCase):
|
|||||||
title="test",
|
title="test",
|
||||||
)
|
)
|
||||||
self.assertRaises(Exception, document_matches_workflow, doc, w, 4)
|
self.assertRaises(Exception, document_matches_workflow, doc, w, 4)
|
||||||
|
|
||||||
|
def test_removal_action_document_updated_workflow(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow with removal action
|
||||||
|
WHEN:
|
||||||
|
- File that matches is updated
|
||||||
|
THEN:
|
||||||
|
- Action removals are applied
|
||||||
|
"""
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||||
|
filter_path="*",
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.REMOVAL,
|
||||||
|
)
|
||||||
|
action.remove_correspondents.add(self.c)
|
||||||
|
action.remove_tags.add(self.t1)
|
||||||
|
action.remove_document_types.add(self.dt)
|
||||||
|
action.remove_storage_paths.add(self.sp)
|
||||||
|
action.remove_owners.add(self.user2)
|
||||||
|
action.remove_custom_fields.add(self.cf1)
|
||||||
|
action.remove_view_users.add(self.user3)
|
||||||
|
action.remove_view_groups.add(self.group1)
|
||||||
|
action.remove_change_users.add(self.user3)
|
||||||
|
action.remove_change_groups.add(self.group1)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="sample test",
|
||||||
|
correspondent=self.c,
|
||||||
|
document_type=self.dt,
|
||||||
|
storage_path=self.sp,
|
||||||
|
owner=self.user2,
|
||||||
|
original_filename="sample.pdf",
|
||||||
|
)
|
||||||
|
doc.tags.set([self.t1, self.t2])
|
||||||
|
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
|
||||||
|
doc.save()
|
||||||
|
assign_perm("documents.view_document", self.user3, doc)
|
||||||
|
assign_perm("documents.change_document", self.user3, doc)
|
||||||
|
assign_perm("documents.view_document", self.group1, doc)
|
||||||
|
assign_perm("documents.change_document", self.group1, doc)
|
||||||
|
|
||||||
|
superuser = User.objects.create_superuser("superuser")
|
||||||
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
|
self.client.patch(
|
||||||
|
f"/api/documents/{doc.id}/",
|
||||||
|
{"title": "new title"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
doc.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsNone(doc.document_type)
|
||||||
|
self.assertIsNone(doc.correspondent)
|
||||||
|
self.assertIsNone(doc.storage_path)
|
||||||
|
self.assertEqual(doc.tags.all().count(), 1)
|
||||||
|
self.assertIn(self.t2, doc.tags.all())
|
||||||
|
self.assertIsNone(doc.owner)
|
||||||
|
self.assertEqual(doc.custom_fields.all().count(), 0)
|
||||||
|
self.assertFalse(self.user3.has_perm("documents.view_document", doc))
|
||||||
|
self.assertFalse(self.user3.has_perm("documents.change_document", doc))
|
||||||
|
group_perms: QuerySet = get_groups_with_perms(doc)
|
||||||
|
self.assertNotIn(self.group1, group_perms)
|
||||||
|
|
||||||
|
def test_removal_action_document_updated_removeall(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow with removal action with remove all fields set
|
||||||
|
WHEN:
|
||||||
|
- File that matches is updated
|
||||||
|
THEN:
|
||||||
|
- Action removals are applied
|
||||||
|
"""
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED,
|
||||||
|
filter_path="*",
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.REMOVAL,
|
||||||
|
remove_all_correspondents=True,
|
||||||
|
remove_all_tags=True,
|
||||||
|
remove_all_document_types=True,
|
||||||
|
remove_all_storage_paths=True,
|
||||||
|
remove_all_custom_fields=True,
|
||||||
|
remove_all_owners=True,
|
||||||
|
remove_all_permissions=True,
|
||||||
|
)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title="sample test",
|
||||||
|
correspondent=self.c,
|
||||||
|
document_type=self.dt,
|
||||||
|
storage_path=self.sp,
|
||||||
|
owner=self.user2,
|
||||||
|
original_filename="sample.pdf",
|
||||||
|
)
|
||||||
|
doc.tags.set([self.t1, self.t2])
|
||||||
|
CustomFieldInstance.objects.create(document=doc, field=self.cf1)
|
||||||
|
doc.save()
|
||||||
|
assign_perm("documents.view_document", self.user3, doc)
|
||||||
|
assign_perm("documents.change_document", self.user3, doc)
|
||||||
|
assign_perm("documents.view_document", self.group1, doc)
|
||||||
|
assign_perm("documents.change_document", self.group1, doc)
|
||||||
|
|
||||||
|
superuser = User.objects.create_superuser("superuser")
|
||||||
|
self.client.force_authenticate(user=superuser)
|
||||||
|
|
||||||
|
self.client.patch(
|
||||||
|
f"/api/documents/{doc.id}/",
|
||||||
|
{"title": "new title"},
|
||||||
|
format="json",
|
||||||
|
)
|
||||||
|
doc.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertIsNone(doc.document_type)
|
||||||
|
self.assertIsNone(doc.correspondent)
|
||||||
|
self.assertIsNone(doc.storage_path)
|
||||||
|
self.assertEqual(doc.tags.all().count(), 0)
|
||||||
|
self.assertEqual(doc.tags.all().count(), 0)
|
||||||
|
self.assertIsNone(doc.owner)
|
||||||
|
self.assertEqual(doc.custom_fields.all().count(), 0)
|
||||||
|
self.assertFalse(self.user3.has_perm("documents.view_document", doc))
|
||||||
|
self.assertFalse(self.user3.has_perm("documents.change_document", doc))
|
||||||
|
group_perms: QuerySet = get_groups_with_perms(doc)
|
||||||
|
self.assertNotIn(self.group1, group_perms)
|
||||||
|
|
||||||
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
|
def test_removal_action_document_consumed(self, m):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow with assignment and removal actions
|
||||||
|
WHEN:
|
||||||
|
- File that matches is consumed
|
||||||
|
THEN:
|
||||||
|
- Action removals are applied
|
||||||
|
"""
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
|
filter_filename="*simple*",
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
assign_title="Doc from {correspondent}",
|
||||||
|
assign_correspondent=self.c,
|
||||||
|
assign_document_type=self.dt,
|
||||||
|
assign_storage_path=self.sp,
|
||||||
|
assign_owner=self.user2,
|
||||||
|
)
|
||||||
|
action.assign_tags.add(self.t1)
|
||||||
|
action.assign_tags.add(self.t2)
|
||||||
|
action.assign_tags.add(self.t3)
|
||||||
|
action.assign_view_users.add(self.user2)
|
||||||
|
action.assign_view_users.add(self.user3)
|
||||||
|
action.assign_view_groups.add(self.group1)
|
||||||
|
action.assign_view_groups.add(self.group2)
|
||||||
|
action.assign_change_users.add(self.user2)
|
||||||
|
action.assign_change_users.add(self.user3)
|
||||||
|
action.assign_change_groups.add(self.group1)
|
||||||
|
action.assign_change_groups.add(self.group2)
|
||||||
|
action.assign_custom_fields.add(self.cf1)
|
||||||
|
action.assign_custom_fields.add(self.cf2)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
action2 = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.REMOVAL,
|
||||||
|
)
|
||||||
|
action2.remove_correspondents.add(self.c)
|
||||||
|
action2.remove_tags.add(self.t1)
|
||||||
|
action2.remove_document_types.add(self.dt)
|
||||||
|
action2.remove_storage_paths.add(self.sp)
|
||||||
|
action2.remove_owners.add(self.user2)
|
||||||
|
action2.remove_custom_fields.add(self.cf1)
|
||||||
|
action2.remove_view_users.add(self.user3)
|
||||||
|
action2.remove_change_users.add(self.user3)
|
||||||
|
action2.remove_view_groups.add(self.group1)
|
||||||
|
action2.remove_change_groups.add(self.group1)
|
||||||
|
action2.save()
|
||||||
|
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.actions.add(action2)
|
||||||
|
w.save()
|
||||||
|
|
||||||
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
|
tasks.consume_file(
|
||||||
|
ConsumableDocument(
|
||||||
|
source=DocumentSource.ConsumeFolder,
|
||||||
|
original_file=test_file,
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
m.assert_called_once()
|
||||||
|
_, overrides = m.call_args
|
||||||
|
self.assertIsNone(overrides["override_correspondent_id"])
|
||||||
|
self.assertIsNone(overrides["override_document_type_id"])
|
||||||
|
self.assertEqual(
|
||||||
|
overrides["override_tag_ids"],
|
||||||
|
[self.t2.pk, self.t3.pk],
|
||||||
|
)
|
||||||
|
self.assertIsNone(overrides["override_storage_path_id"])
|
||||||
|
self.assertIsNone(overrides["override_owner_id"])
|
||||||
|
self.assertEqual(overrides["override_view_users"], [self.user2.pk])
|
||||||
|
self.assertEqual(overrides["override_view_groups"], [self.group2.pk])
|
||||||
|
self.assertEqual(overrides["override_change_users"], [self.user2.pk])
|
||||||
|
self.assertEqual(overrides["override_change_groups"], [self.group2.pk])
|
||||||
|
self.assertEqual(
|
||||||
|
overrides["override_title"],
|
||||||
|
"Doc from {correspondent}",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
overrides["override_custom_field_ids"],
|
||||||
|
[self.cf2.pk],
|
||||||
|
)
|
||||||
|
|
||||||
|
info = cm.output[0]
|
||||||
|
expected_str = f"Document matched {trigger} from {w}"
|
||||||
|
self.assertIn(expected_str, info)
|
||||||
|
|
||||||
|
@mock.patch("documents.consumer.Consumer.try_consume_file")
|
||||||
|
def test_removal_action_document_consumed_removeall(self, m):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Workflow with assignment and removal actions with remove all fields set
|
||||||
|
WHEN:
|
||||||
|
- File that matches is consumed
|
||||||
|
THEN:
|
||||||
|
- Action removals are applied
|
||||||
|
"""
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.CONSUMPTION,
|
||||||
|
filter_filename="*simple*",
|
||||||
|
)
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
assign_title="Doc from {correspondent}",
|
||||||
|
assign_correspondent=self.c,
|
||||||
|
assign_document_type=self.dt,
|
||||||
|
assign_storage_path=self.sp,
|
||||||
|
assign_owner=self.user2,
|
||||||
|
)
|
||||||
|
action.assign_tags.add(self.t1)
|
||||||
|
action.assign_tags.add(self.t2)
|
||||||
|
action.assign_tags.add(self.t3)
|
||||||
|
action.assign_view_users.add(self.user3.pk)
|
||||||
|
action.assign_view_groups.add(self.group1.pk)
|
||||||
|
action.assign_change_users.add(self.user3.pk)
|
||||||
|
action.assign_change_groups.add(self.group1.pk)
|
||||||
|
action.assign_custom_fields.add(self.cf1.pk)
|
||||||
|
action.assign_custom_fields.add(self.cf2.pk)
|
||||||
|
action.save()
|
||||||
|
|
||||||
|
action2 = WorkflowAction.objects.create(
|
||||||
|
type=WorkflowAction.WorkflowActionType.REMOVAL,
|
||||||
|
remove_all_correspondents=True,
|
||||||
|
remove_all_tags=True,
|
||||||
|
remove_all_document_types=True,
|
||||||
|
remove_all_storage_paths=True,
|
||||||
|
remove_all_custom_fields=True,
|
||||||
|
remove_all_owners=True,
|
||||||
|
remove_all_permissions=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.actions.add(action2)
|
||||||
|
w.save()
|
||||||
|
|
||||||
|
test_file = self.SAMPLE_DIR / "simple.pdf"
|
||||||
|
|
||||||
|
with mock.patch("documents.tasks.ProgressManager", DummyProgressManager):
|
||||||
|
with self.assertLogs("paperless.matching", level="INFO") as cm:
|
||||||
|
tasks.consume_file(
|
||||||
|
ConsumableDocument(
|
||||||
|
source=DocumentSource.ConsumeFolder,
|
||||||
|
original_file=test_file,
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
m.assert_called_once()
|
||||||
|
_, overrides = m.call_args
|
||||||
|
self.assertIsNone(overrides["override_correspondent_id"])
|
||||||
|
self.assertIsNone(overrides["override_document_type_id"])
|
||||||
|
self.assertEqual(
|
||||||
|
overrides["override_tag_ids"],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
self.assertIsNone(overrides["override_storage_path_id"])
|
||||||
|
self.assertIsNone(overrides["override_owner_id"])
|
||||||
|
self.assertEqual(overrides["override_view_users"], [])
|
||||||
|
self.assertEqual(overrides["override_view_groups"], [])
|
||||||
|
self.assertEqual(overrides["override_change_users"], [])
|
||||||
|
self.assertEqual(overrides["override_change_groups"], [])
|
||||||
|
self.assertEqual(
|
||||||
|
overrides["override_custom_field_ids"],
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
|
||||||
|
info = cm.output[0]
|
||||||
|
expected_str = f"Document matched {trigger} from {w}"
|
||||||
|
self.assertIn(expected_str, info)
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import shutil
|
import shutil
|
||||||
from os import utime
|
from os import utime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
def _coerce_to_path(
|
def _coerce_to_path(
|
||||||
source: Union[Path, str],
|
source: Union[Path, str],
|
||||||
@ -40,3 +44,15 @@ def copy_file_with_basic_stats(
|
|||||||
|
|
||||||
shutil.copy(source, dest)
|
shutil.copy(source, dest)
|
||||||
copy_basic_file_stats(source, dest)
|
copy_basic_file_stats(source, dest)
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_override_pixel_limit() -> None:
|
||||||
|
"""
|
||||||
|
Maybe overrides the PIL limit on pixel count, if configured to allow it
|
||||||
|
"""
|
||||||
|
limit: Optional[Union[float, int]] = settings.MAX_IMAGE_PIXELS
|
||||||
|
if limit is not None and limit >= 0:
|
||||||
|
pixel_count = limit
|
||||||
|
if pixel_count == 0:
|
||||||
|
pixel_count = None
|
||||||
|
Image.MAX_IMAGE_PIXELS = pixel_count
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user