Merge branch 'dev' into feature-notification-wf-action

This commit is contained in:
shamoon 2024-12-01 16:52:08 -08:00
commit e68e8bb9b9
No known key found for this signature in database
36 changed files with 905 additions and 522 deletions

View File

@ -23,7 +23,7 @@ djangorestframework-guardian = "*"
drf-writable-nested = "*" drf-writable-nested = "*"
bleach = "*" bleach = "*"
celery = {extras = ["redis"], version = "*"} celery = {extras = ["redis"], version = "*"}
channels = "~=4.1" channels = "~=4.2"
channels-redis = "*" channels-redis = "*"
concurrent-log-handler = "*" concurrent-log-handler = "*"
filelock = "*" filelock = "*"

633
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "584249cbeaf29659c975000b5e02b12e45d768d795e4a8ac36118e73bd7c0b8a" "sha256": "a194c6834fba6a14712ba36eb0b896f18d7ef4393523e5d55ccb103104e99ddb"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@ -282,27 +282,27 @@
"sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87",
"sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"
], ],
"markers": "python_version >= '3.8'", "markers": "platform_python_implementation != 'PyPy'",
"version": "==1.17.1" "version": "==1.17.1"
}, },
"channels": { "channels": {
"hashes": [ "hashes": [
"sha256:a3c4419307f582c3f71d67bfb6eff748ae819c2f360b9b141694d84f242baa48", "sha256:6b75bc8d6888fb7236e7e7bf1948520b72d296ad08216a242fc56b1db0ffde1a",
"sha256:e0ed375719f5c1851861f05ed4ce78b0166f9245ca0ecd836cb77d4bb531489d" "sha256:d9e707487431ba5dbce9af982970dab3b0efd786580fadb99e45dca5e39fdd59"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.1.0"
},
"channels-redis": {
"hashes": [
"sha256:01c26c4d5d3a203f104bba9e5585c0305a70df390d21792386586068162027fd",
"sha256:2c5b944a39bd984b72aa8005a3ae11637bf29b5092adeb91c9aad4ab819a8ac4"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==4.2.0" "version": "==4.2.0"
}, },
"channels-redis": {
"hashes": [
"sha256:2ca33105b3a04b5a327a9c47dd762b546f30b76a0cd3f3f593a23d91d346b6f4",
"sha256:8375e81493e684792efe6e6eca60ef3d7782ef76c6664057d2e5c31e80d636dd"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.2.1"
},
"charset-normalizer": { "charset-normalizer": {
"hashes": [ "hashes": [
"sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621",
@ -456,36 +456,38 @@
}, },
"cryptography": { "cryptography": {
"hashes": [ "hashes": [
"sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362", "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7",
"sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4", "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731",
"sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa", "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b",
"sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83", "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc",
"sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff", "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543",
"sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805", "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385",
"sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6", "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c",
"sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664", "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591",
"sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08", "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede",
"sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e", "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb",
"sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18", "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f",
"sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f", "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123",
"sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73", "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c",
"sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5", "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba",
"sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984", "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c",
"sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd", "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285",
"sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3", "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd",
"sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e", "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092",
"sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405", "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa",
"sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2", "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289",
"sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c", "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02",
"sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995", "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64",
"sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73", "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053",
"sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16", "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417",
"sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7", "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e",
"sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd", "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e",
"sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7" "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7",
"sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756",
"sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"
], ],
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7' and python_full_version not in '3.9.0, 3.9.1'",
"version": "==43.0.3" "version": "==44.0.0"
}, },
"dateparser": { "dateparser": {
"hashes": [ "hashes": [
@ -498,11 +500,11 @@
}, },
"deprecated": { "deprecated": {
"hashes": [ "hashes": [
"sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320",
"sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3" "sha256:683e561a90de76239796e6b6feac66b99030d2dd3fcf61ef996330f14bbb9b0d"
], ],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.2.14" "version": "==1.2.15"
}, },
"deprecation": { "deprecation": {
"hashes": [ "hashes": [
@ -526,10 +528,11 @@
"socialaccount" "socialaccount"
], ],
"hashes": [ "hashes": [
"sha256:0a3d7baf7beefd6fe8027316302c26ece7433cf4331a3b245d15fc9a7be68b6f" "sha256:92e0242724af03458b05b88c5fa798b01112ab22a86d873a8a9fd8f0ec57bbbf"
], ],
"index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==65.2.0" "version": "==65.3.0"
}, },
"django-auditlog": { "django-auditlog": {
"hashes": [ "hashes": [
@ -1272,18 +1275,18 @@
}, },
"mysqlclient": { "mysqlclient": {
"hashes": [ "hashes": [
"sha256:1d2e2ca0fe8405d8d6464edd01bf059951279e4bc27284d39341bd4737b2bc64", "sha256:3da70a07753ba6be881f7d75e795e254f6a0c12795778034acc69769b0649d37",
"sha256:3f9625bea2b9bcde0ace76b32708762d44597491092c819fd1bff5b4e27f709b", "sha256:43c5b30be0675080b9c815f457d73397f0442173e7be83d089b126835e2617ae",
"sha256:8012c633aab8c91ea8172ac479807135b171501b9cad1a7cd9b58c4dc8dcdab5", "sha256:794857bce4f9a1903a99786dd29ad7887f45a870b3d11585b8c51c4a753c4174",
"sha256:add8643c32f738014d252d2bdebb478623b04802e8396d5903905db36474d3ff", "sha256:b0a5cddf1d3488b254605041070086cac743401d876a659a72d706a0d89c8ebb",
"sha256:aee14f1872114865679fcb09aac3772de4595fa7dcf2f83a4c7afee15e508854", "sha256:c0b46d9b78b461dbb62482089ca8040fa916595b1b30f831ebbd1b0a82b43d53",
"sha256:b54511648c1455b43ac28f8b4c1f732c5b0c343e87f7a3bd6fc9f9fe0f91934e", "sha256:e940b41d85dfd7b190fa47d52f525f878cfa203d4653bf6a35b271b3c3be125b",
"sha256:b78438314199504c64f69e1e3521f2c9b419f19fcd85158b44c997b64409a6af", "sha256:e94a92858203d97fd584bdb6d7ee8c56f2590db8d77fd44215c0dcf5e739bc37",
"sha256:e871ede4261d0d42b8ed20a2459db411c7deafedd8e77b7e4ba760be4a6a752b" "sha256:f3efb849d6f7ef4b9788a0eda2e896b975e0ebf1d6bf3dcabea63fd698e5b0b5"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==2.2.5" "version": "==2.2.6"
}, },
"nltk": { "nltk": {
"hashes": [ "hashes": [
@ -1365,12 +1368,12 @@
}, },
"ocrmypdf": { "ocrmypdf": {
"hashes": [ "hashes": [
"sha256:362fb80043ef8df9cd2356a9713e4edd47623c90ae3ce430c193c6fd80cb3697", "sha256:24b4d7454bbcbec3b017e55fcc5827c3da24c8f0abbb1efdf695020bbf7ca47f",
"sha256:c5c86223aa7f860734ce5db84975bbca223251357e1a6160291ff64019aee185" "sha256:dd08d32b3d989ba6d7b9f0dcebe3c1bcc049d06609916ac476c1f985b7605111"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.10'", "markers": "python_version >= '3.10'",
"version": "==16.6.1" "version": "==16.6.2"
}, },
"packaging": { "packaging": {
"hashes": [ "hashes": [
@ -1407,105 +1410,105 @@
}, },
"pi-heif": { "pi-heif": {
"hashes": [ "hashes": [
"sha256:0305ea6e108979eee370fed0486df8b90ff25c619db9d9d539b00603e139626f", "sha256:06e57bebaeadc3d708744bc3d2e4216d86907a2154c7e64ff4198a965e7f1a16",
"sha256:030df62463a03053d68af01fb3fafb3dae3bc578b8da24280f0787469c22bdd0", "sha256:09c58325a8148b62a37c1973dc6aff7c5ccd0421bb711ff2697fccdea3e1a9d5",
"sha256:05b938736a4712fbc1224d4207ceb226670568f7db329b61895876c1ae7fe241", "sha256:0c77bf122504eafc12841cfe03413048ad0af3e5f17b43ac8cfd9930830160aa",
"sha256:086d101461f0580b621de1cf24e6cf9c136015e6ff9408fec7f254c2c4f49243", "sha256:1077487878d2fd7a63fa3c65c96de6213c6c349f74b8c8625164e8edf4617441",
"sha256:13146a8d4873198cf614c70ea344b69932637c28fb506427caffe9b951cc5694", "sha256:1733631b37ee01a4d0b2bf4f727e5a13815ccf5d02b12a1c00edb258899645b5",
"sha256:1371a3a34bffdc98b5bacf48b01c8bba0fd49183fd425a6f152390fda792e8da", "sha256:2295ae1ed8e03fd96e988dba5bf4b179496093cb89c84bc326fcb8cf17c45b28",
"sha256:137af16fc31f8862775a82287e0c1ea0627fb0e9ffa53fccf84cacd1f4e0a90a", "sha256:235ca775c4500c30dc046352bdfddf97f909645cc187e5f382cd2ab4bf630d81",
"sha256:18b8c3269d1e07f85d146217616b117f5f2c8142328c6669a46d8d762699aadb", "sha256:274495e3a8495899ee8a8cfce5d1d3123217aa1093bf096077cdbf78d99ce712",
"sha256:1f482ac86090c0d8adf65bd3a6b97f485ceaa0876123225913e3e2ac687e5bfe", "sha256:2f4f034aed9443ce811f0700dfaa0bbaae55ea331371ff85a18a66432764d353",
"sha256:2484c8b5f447bc0018d9045faea9880e177c818c89eb333239aa02b9ed4a9ffb", "sha256:32648f66b95cec8bada6e3928d6eeed1f6cf9c4b73c7e1924cf84eec1ee8cda8",
"sha256:2a38a50b33cec5261037e625c35270656ec16c2febdcbbb529a9a96857b16a5b", "sha256:35995f4ecf73425d71c68c24f54d7945316667b0b6f2f56928f926d94f198b58",
"sha256:323b7d1ed4a702fe7b8ca2f959f01f33879e38c413542408d522383b65857492", "sha256:35a72089673bf0c9d7d0de994808718a91b2240e493a474c99ebcc3b7d536d96",
"sha256:33ab67a27bcbe24840bb26bf71a097e66c1ac63e1c5c155456913b8f933c9582", "sha256:3b623724c75b3049fd10e268d3283f98be643b0e421264d405aa7c4bc310383a",
"sha256:33cf72ec7d344ae681046455ab7b2d3210e8369ed46f921038fd980ba1b5b27f", "sha256:3ecd96cf9dad1ac9f4126771d164e901e668a8f28293ebccc3178b2c6f2bceb0",
"sha256:393868800055271ae7ad876055e7b3f2eae5ab2fe1980ebe28a5761725c2c189", "sha256:4902cdb84e75505e1d9abdd5aff1e6dcfebe569ec825162d68a4a399a43689a4",
"sha256:397e7839682771a21e7756a4822b4bc45251805d3188bb6275eb51fd63e6c1b2", "sha256:4b6a4fb3e73a7a859ba9ecf6d564e0445e961779145fff2282440915fe55310f",
"sha256:3bc6d86cdc42c22fe0821db2771eefa7e9651922668dd008c1cf8d786c1b13cc", "sha256:504c6912fb60aeb031644f37ac0b7edfdc02bc0023cf1eec2995cdc1486a6c43",
"sha256:3e00bab005d97e6a60b2a52e9fc14fb8d50d9a7ff290d52b0e6491848e853550", "sha256:59effc063d4715daa983ab8492e6d4bb104134a44821677185dfb99e9d16314d",
"sha256:4908b6e6be10062bf06dd5512184b1ad495291b73aa8f0b82f0fc9d66f2bcc1b", "sha256:671a57cb82430aa0c43f51f881888d69a6274236ee6f8e084270549feec7bb56",
"sha256:52436771e1e9198145a3db25885ec9fda4eda11f5753e1317ae9459b190cd6a4", "sha256:6a82187ac503ca57b5cbfce481e3ec9b18752b8dd3c259434d020de6adc9dcc2",
"sha256:5ca9ccfda3b114229c92b5470f8fe451b5d07f4c8677edc3525403ef379b53ba", "sha256:75da9aaf1b4a4c0bf56b0e3f75bbfa116c07b93be3f669b621d3a5b2ae5b4b44",
"sha256:627cc28de13eab9eabef9931c0ae260bdeb61200f6ce3d2cb8444a6979053ef9", "sha256:7c14255ccfdfa7719b664b7a47493412212acd6c075d3af7036adff34be29364",
"sha256:695417ff1050cf79bca5c47bead807d7eb30fcf76fd64457f686a6342712284f", "sha256:8c7808526530d0c534b925b1b9e9477ffb1aefa5aa4a356487f0c839e74933a2",
"sha256:6edf3cf270838d26329e1f0d93a3a85963e4f811de8e80173e18a2f137020deb", "sha256:8c83b87e1ca0950b5046963edfe450e04c6c8cf61ad21647898d563e45c555cb",
"sha256:81e0f769d62f7af250518a99c75c3cc357147ab78ad673b5c0e96b3c1e3ef21b", "sha256:918f863cd86e3550aa1caa3f2f6c176c922a7180025b582d5e20bd2a1e8c64d4",
"sha256:834b188f4963ac29b2ea92153e7d437993d8dd9e5adbff1640695563832f2157", "sha256:988619231bdd89bd39093e8d942397a07b832197d197e3c89d39a8614b051645",
"sha256:849d6ff186128d563b494cd463abc156a73ad4598b38dcaed0d52c0e78ec8cc4", "sha256:9c9558a511f7455230daf7fd36f024cc54c79315abdd272e73df2d655504088c",
"sha256:89c8e89b7615a0aa717f366169c4be31451af72a937314104cc88cbe40e1da11", "sha256:9e64cabc54900210b2e6ca8e7f6f0e496cd4e94e533f6a5bdf658c9204c7bc1a",
"sha256:8b71571e792389151a679f4cb477a54ee1f3834d4b21c2f9e272068a4166964a", "sha256:9ed397da8d10743d8f3499f636c8c15db11311d8d73b8ef120b364896b229f56",
"sha256:9050f675bdc43333c1ac6aab734e253a3ba92a0efc8014d3431953add906ead5", "sha256:a36d29a5ebce2541e11a104fbe2dacac25aa2b9f801fae5e28a13da6557bc694",
"sha256:90e1e4ccd08e7256d1a19519b546efd7d5baf75337d522cbaa813fd6155675ec", "sha256:a48ed9c2cca45d464c09d56f71bfaa85ec3e30c8c8bcb43e5f51b71301b13352",
"sha256:b23d891b4b788a00fb8e0c170e4744d86708bc682b3bf095143b10074bdd694c", "sha256:aa6d929305b62bb391066c2c3ee2ed219bdc5ea564f36091b50f942faf524815",
"sha256:b6bfcc917d3b11f6bcf80db8c64f721ce1ab7335459c0f109e933ca92e6dfb1a", "sha256:b3c111a39a08a56cc6b3c5f381a352d635fbe161d3aa9307a35e14a372bbbb9c",
"sha256:b8cd2a83ee8cc597ddf7e13d2adec60b7206357ff55649c4aea2f4c76bdb11c2", "sha256:b8617e40bba3b654496138ff6a0c99a14f58be012c41b2fdde9c1ba780944f14",
"sha256:d002ca0b10e39b3f034385f1d453f4effd94bf83bbb4464ee72a386dcdbb1c4c", "sha256:bec08ae26a3f73a62844fc7969e6903af7c13dfe3eab34846ffaaff245894c2a",
"sha256:d57f48a08b0cdc492e17fe33312b47356a17fcf6a93c508a9e503e497e03ef4f", "sha256:c37a4e0f4ef417d6bed1854e8176cacb7d9522003a9892ef0872c92909127f8f",
"sha256:d6a4d350ac12fb5f45bc83eb35747156a4faa12d001dcdd9525a3912ae5b4d7a", "sha256:c6a5bca23e86449b8bc7821701013fd4c4cd0b9083caac72eeefaf5e3de022cc",
"sha256:d7866ea2735d8cbc7501a98735a940ea22098303e340f2218caad3427f261abb", "sha256:c9c21dec9b0f0d0bd659687f53f02cbc59b86f1cdff5e14da979e370e185fac8",
"sha256:e81e28f0c7330133b5b1ce7e1ad118663eb9eac4cad6638bfca07c0eb761129a", "sha256:cbb0a2bc1c332664dd3bcbf96dd2f290ecabd1c9088b174412c29fafb667cd54",
"sha256:eadcc3cec45d3c021e4c41c5a85e56f4c2b8b8942fa9a1720d9376b29c2e39cb", "sha256:cd29361e0c156bc5bf0adeb58081a1955b2f02f9caa8bed30afdd595ff9a4745",
"sha256:ee28de31da7d1a8cc08e4fa1b5a1c095b3e1c8be5edd5cf26c8ac4c365b806b3", "sha256:d2b57c7c8b7f126970e2d655ac7b6b480a27a022b619e7463c071963571e498b",
"sha256:f082f12fb3954ac0140e9eead2d3a2be4aa60aa834a3998665175fde67f50793", "sha256:e0e8c432b43cb982f8816218e6997a4e27eec414da42eade5b7585a68776dea7",
"sha256:f3f1e59b802d96c0dcf1b91ed2cc7d196a94b9e3d985ef80a86700e16e45bb28", "sha256:e471c29555bf8c9567d4176eabe85aedc27ec1e0d3af1616fdf8a5b4c45b1757",
"sha256:f4b94f5ea91cfe180ead680528a197da8f77ff27b339aceb169bd7e2a51b53b1", "sha256:e47cda8937cb2cdac2b9071e6380ec0c15bd6fd5f871d3d69bc25f9a523d4916",
"sha256:f63b5ab190697c91dec482112bfa90751e48070899f4888317e943075dccf0be", "sha256:f51a518f659ff79db74b9044a52bf5a45ed5f0d7f2eca5afdfe55ae1bafcd7a8",
"sha256:ff50c2c090c2af6347e9caa8f2e3998c29f9ff8d4fac625bd48e0072f2133464" "sha256:f91a088b2b20d988c98aef6c85ca84ebf4829a4b65d800e72775b4759fa3310c"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==0.20.0" "version": "==0.21.0"
}, },
"pikepdf": { "pikepdf": {
"hashes": [ "hashes": [
"sha256:14cc769aafecedb25a112da54cbf3176533cd87dcf05f45308a0a28cfca55ce7", "sha256:008f207d7efcf8f5369e79f23638e50540a17c616517747738090b7cfddad8a6",
"sha256:17ab91fca8a8a1e5dd52483d647352ec8875f4bb9eef8b3fa186378c5c7815b1", "sha256:009e2aa4074faa60b5f107615616132e4635dfd7579dabf21f2862de0f84e216",
"sha256:1c3472e5570fef3a7714a41b09a81e3dcbbce3f2a0d6c43c4f197b40288a4aed", "sha256:0108c063bc56dc2dbfc87f20533a728342a938f4c85e39773866b71255aa8388",
"sha256:1d0b983ef7481502ce0439867280b00846ba2eecd37fa5906a4ff3e445895b85", "sha256:0240a27efa519fa534a543394f17d3a51d6468be43f218fe810506a106f131b1",
"sha256:1f1f53368c8a25f8c5e96dc19e57c0ad3d8bd7f259ca730ac5b0bd9d21747d5a", "sha256:0942201f49a08d877f9759cd83e7bcd12c6a4355b85f41086b2791ba75671382",
"sha256:1f6a9da26ae2472bdaaa038720778a1ac15af908c2d4b3670b90318ab00eaed1", "sha256:0bdac215f90b16ce05e854c71eba34021dd4226e054a02ff555dd95f5424f40d",
"sha256:20b7e5c5f93b674214a8ebff79c9793cab07261930e35d60110404fa7cdbcb56", "sha256:0c365bafc5c0fcf88fee07818fe2234069a193349ec79efb127b9037b7cfc8af",
"sha256:24e90a325f6e9a3eb0d043deddf1dbb5f5aba20c5ca31430f49397e9c5888b1e", "sha256:0d49b16c3e09de600c5baa7f903ab68e31e1210ceb523185552b173e68371141",
"sha256:30467da3000850a83da3e17c37e1fd6cc41005d6232f162725da15736996a39f", "sha256:0ee528d252b34e8760f8f8ec1517b35fb4bc4457c8cab6bcd7f0c128f28609ba",
"sha256:343328acc542aee0d5284fb0cb904cec6bfbedb860d3fa24dfeab306b05847cc", "sha256:1583c7b43a6c8986ec6e9b2833f1b01877338727a73630e533aad739ddc14501",
"sha256:3cdfc4f054efd75eaad3582e4ef94ff4ebc93c58de21f2769f071af984500a9a", "sha256:17817e739a0e9507b5f85463e90f376f54a1832667c6c315ad9fb0086bb1edf7",
"sha256:3d11f89276e7ae48c763b75fd3322cf6ebf0d45580778ad9423e56dca799dd9d", "sha256:277eef7c6ea5ca3b9f8a00005c4ad19ff3ad166800c96e1a9f866bd2ae853f4b",
"sha256:3d5e2bf5e376968ddbe1f5e409174d1d0fde04ddbcda6509b1ad63664f4a46aa", "sha256:2935d152612cc0af7ed619d39ad56d34e6fe56d11efa36d6eae0a7e3674ca6aa",
"sha256:3d85ecf8761f7fa8f4270176b7f2ebe92909d652867d684526e3d67a34497c41", "sha256:2b0a1e066e57d01ccef2fb46fc046731c9bd60d268a99b808fbd1807eb81fb84",
"sha256:45856be7fd9793090b258990896540e77d19908f26666286476c601ac65813d6", "sha256:3be7016a36b7099338f4290f9f3b5de7dadf4a446ad49d89f96b95bd89167d94",
"sha256:564e0f1a2299f4a8a5ae6ed3b29b683dac331bc3c1a5a701e579a1e6517ced77", "sha256:473589dd7ccd0e4b11f82a7fb366984d85fa1fc998ab0f8695f1eb6eb1cdce85",
"sha256:56bbbc4a8811fa440be22c7434e2e00267e363fc95a2f77aee62dcb6561ce119", "sha256:497f45bbe50f0031920af28da5e950cf577894236bd1f3feda1bad3858a9dd38",
"sha256:5cd676aaa5dfd2dde9dcc24950e23ea1a834ece3419cc54aaf91cfa0bb604632", "sha256:50db5731f3ec853980863e096789bbe1027425fa90ce36fe9af34dec3e8b98fe",
"sha256:60d540f8b7eef97410b29adb10ef1afd52e4107a49c019bf580c54d03e2f39d4", "sha256:5dadf9c8db722fbcd694445e8e6f6501475cf66773783aeaacd054c128898356",
"sha256:61bd67cf5f10f3fa20a1586f4210427d3878030062d861b37e20d662d214ec22", "sha256:701390e3a718a9588599cafc34ca2cb9a5a96109209088b46f44b787430e547b",
"sha256:64ed1993998bcb4da10663c98398c92c135b951696baba3f9e7d93b86b1c6ac8", "sha256:7bc3f951c8646f7bcd55f72b678801f3e0de17f7fb9329b4550ee64f9a566343",
"sha256:6b02fec9fbacf2bea1fc25a8a5dcdce27a6ee598c3a67166b371a792b42e45a8", "sha256:8390abb7beb80e53c92b587efe7609476a017dcd67430bd7f5c4698108f21b97",
"sha256:6c458976fb0540ea9ec79b0ebc6641df982e5b0ff93301d6935d70c7b051dbd1", "sha256:8a2204695f270fed41cc872c215a0dd9deac510845f9879aaed001303ecd3d88",
"sha256:703d72dac7ebc0d342afba57f5b91ad5043f67dd351f4d95a7c8e5d360759e69", "sha256:90cbb667fec4693834eeb9b5e58e62b5ccb53face183a54eaf5a157edbc6a9b8",
"sha256:84bb53011e01e976af699da897df290681c45e37a63330e3b05ec8eb08d3a9a9", "sha256:9200ce41c3c22bbd2c292666f7c27f8274f79ea1f99f929ff8cfde8ee3560c96",
"sha256:868aa504e07804dac3dc7f8a20befc6eaa2576baf7f3a7013826f10de6da74ce", "sha256:9470c1782143a62005d0b3dac59bbe394f65d9122c8f5966afa63b5c44aa51b3",
"sha256:8b4903e409a1f57f8c24bd6821cdaf26031ae0262feb6214662d9d1ecad27386", "sha256:97f71f87601c9931bea157d268acd1c1f5a78cf48bad0916403fbbc3ccce78cb",
"sha256:9fa695ede603884612a62494820669662af0d42fc37b6030a65990d20fe62cc8", "sha256:9a9bca70ba4bad72082f443f120064a1fde95d288e59c880d60d78386e99df2d",
"sha256:9fa773e3d9c5c6cc8f65b7043e56556e7e9534f5d0851c645a2942511c385081", "sha256:9b683f32681b62be7c772bcd84168c0fe616562aac99ca9258ce1b16fe5c3580",
"sha256:a6383bbc6feb14b42f0e9e7a85586e4ea566831786decc7b39863290687044de", "sha256:9d0337a8c68a0939a448b55e78cc267b2eebc8a711bbd9471bf497e2f1265828",
"sha256:bc5e435f4ffd640afc2acdd01f84e106623868558656bf038c2f0627e27b9b0a", "sha256:a2826183f5d4811ace4049fc23163a6b8540dca8bbe5083f5ac7020a629531a0",
"sha256:bc637626551f9ca98140d4fa84964ff07aa89faa3af91aca990041c11db10a9d", "sha256:af863cbb8aaafd2f9a15e41ade19d0a85d85d51b2d167f29a2b11985c8def154",
"sha256:c7bf91e24b93c8cf5e9441e86b04edc21779e64d8d48a9311a7fc678d396787f", "sha256:b42c9c8d9459e22ebeefc727f6a817ff0c5f23fbcd1bdb33cc12d00c943fb801",
"sha256:cfa901b74045623c7e3929c3ca0d5ceab01bdbc48888375cf98217509147bcd6", "sha256:b4a699cc88e4b19df68f1d62765368e274488949abf7c08709757207254e4c5b",
"sha256:d366f75d9bcefb666e208c8aa1e602de93f87f89515c9aaea65167d8261ef3fe", "sha256:b6513a119904cddd00c54cf8d65ed5183e416b06096bb90145c250b560075755",
"sha256:da0cb1272571916b025c3954a2f4f690784d3da41a485ffd8156fa3cd8b88019", "sha256:cd83787d151b58c127cadee01bd51e019cc772d337541ef91937c24d130392bc",
"sha256:db04a42b8d5b3d5422b17c0422e03b1bb9349910f8ce5d336df200e2f9ce6ecd", "sha256:d1bf723a35e6f54a0e662c3f65f5d133320fb23346e50db6feb528c71c688d6c",
"sha256:dd2de2a28da411713d169a1dcab805239f2fe3d5ab3e070d5e309977f92596df", "sha256:dc6eea6c6137d2a1d5866158fe54f66baa04d3fa954b3d880a8e4b1a1776426c",
"sha256:ddeaae67bb59d60538f3fc64459b83ff12eea34c399780527cf8e6d867254c2c", "sha256:dda896ca38d065e2aefe6160a776211c84c44c33677126b05eb75dbd369bb8cd",
"sha256:e42da36b2f449200a9b4dfbb5d4c661f5b118dd763799c077e9edb1b86df8cd1", "sha256:deccfb769dae0d0746d2bc373fe0bfa1b2c5b06c473dd126be37f80cfd7ec99d",
"sha256:f8ab6172825b95fa313162d199c073ea9515fbe72d8e8cfd824586c8537e3c08", "sha256:e893aa12673d1e3a3daf3cc76b652104dee27006d745d83dbd7a12183ca83bfd",
"sha256:f9360958e96f7979355b8f5af5409332f284fdcbf3d4985649635f5592ee05fc", "sha256:e9e004efab21fe220bf2d2c05234e1a743349aec7fa1b4069ed6eb6b05898856",
"sha256:f961e15db95d6fc2bf4d9c8fe3e7a843366ba956c6d3fcdf98613f0eb0a1bc46", "sha256:edddada6df766f571146ea0f902124c8c2953b79c2108918245f344466df0db1",
"sha256:fb95b6e251e90788391326ba27bbf021ae2ca3613573bcc887f8e6f65e2a433f" "sha256:fe705959bc7188c355f8fdbaf93dcac90cc5fe1c5762c55e59c27f6ce23c398c"
], ],
"markers": "python_version >= '3.9'", "markers": "python_version >= '3.9'",
"version": "==9.4.0" "version": "==9.4.2"
}, },
"pillow": { "pillow": {
"hashes": [ "hashes": [
@ -2095,98 +2098,95 @@
}, },
"setproctitle": { "setproctitle": {
"hashes": [ "hashes": [
"sha256:00e6e7adff74796ef12753ff399491b8827f84f6c77659d71bd0b35870a17d8f", "sha256:020ea47a79b2bbd7bd7b94b85ca956ba7cb026e82f41b20d2e1dac4008cead25",
"sha256:059f4ce86f8cc92e5860abfc43a1dceb21137b26a02373618d88f6b4b86ba9b2", "sha256:02ca3802902d91a89957f79da3ec44b25b5804c88026362cb85eea7c1fbdefd1",
"sha256:088b9efc62d5aa5d6edf6cba1cf0c81f4488b5ce1c0342a8b67ae39d64001120", "sha256:0361428e6378911a378841509c56ba472d991cbed1a7e3078ec0cacc103da44a",
"sha256:0d3a953c50776751e80fe755a380a64cb14d61e8762bd43041ab3f8cc436092f", "sha256:04d6ba8b816dbb0bfd62000b0c3e583160893e6e8c4233e1dca1a9ae4d95d924",
"sha256:1342f4fdb37f89d3e3c1c0a59d6ddbedbde838fff5c51178a7982993d238fe4f", "sha256:06c16b7a91cdc5d700271899e4383384a61aae83a3d53d0e2e5a266376083342",
"sha256:184239903bbc6b813b1a8fc86394dc6ca7d20e2ebe6f69f716bec301e4b0199d", "sha256:0855006261635e8669646c7c304b494b6df0a194d2626683520103153ad63cc9",
"sha256:195c961f54a09eb2acabbfc90c413955cf16c6e2f8caa2adbf2237d1019c7dd8", "sha256:091f682809a4d12291cf0205517619d2e7014986b7b00ebecfde3d76f8ae5a8f",
"sha256:1f5d9027eeda64d353cf21a3ceb74bb1760bd534526c9214e19f052424b37e42", "sha256:0ad212ae2b03951367a69584af034579b34e1e4199a75d377ef9f8e08ee299b1",
"sha256:200620c3b15388d7f3f97e0ae26599c0c378fdf07ae9ac5a13616e933cbd2086", "sha256:0b19813c852566fa031902124336fa1f080c51e262fc90266a8c3d65ca47b74c",
"sha256:200ede6fd11233085ba9b764eb055a2a191fb4ffb950c68675ac53c874c22e20", "sha256:0b6a4cbabf024cb263a45bdef425760f14470247ff223f0ec51699ca9046c0fe",
"sha256:21112fcd2195d48f25760f0eafa7a76510871bbb3b750219310cf88b04456ae3", "sha256:0baadeb27f9e97e65922b4151f818b19c311d30b9efdb62af0e53b3db4006ce2",
"sha256:224602f0939e6fb9d5dd881be1229d485f3257b540f8a900d4271a2c2aa4e5f4", "sha256:0f6661a69c68349172ba7b4d5dd65fec2b0917abc99002425ad78c3e58cf7595",
"sha256:287490eb90e7a0ddd22e74c89a92cc922389daa95babc833c08cf80c84c4df0a", "sha256:10a78fce9018cc3e9a772b6537bbe3fe92380acf656c9f86db2f45e685af376e",
"sha256:2982efe7640c4835f7355fdb4da313ad37fb3b40f5c69069912f8048f77b28c8", "sha256:122c2e05697fa91f5d23f00bbe98a9da1bd457b32529192e934095fadb0853f1",
"sha256:2df2b67e4b1d7498632e18c56722851ba4db5d6a0c91aaf0fd395111e51cdcf4", "sha256:149fdfb8a26a555780c4ce53c92e6d3c990ef7b30f90a675eca02e83c6d5f76d",
"sha256:2e4a8104db15d3462e29d9946f26bed817a5b1d7a47eabca2d9dc2b995991503", "sha256:1a2041b5788ce52f218b5be94af458e04470f997ab46fdebd57cf0b8374cc20e",
"sha256:2e71f6365744bf53714e8bd2522b3c9c1d83f52ffa6324bd7cbb4da707312cd8", "sha256:1a88e466fcaee659679c1d64dcb2eddbcb4bfadffeb68ba834d9c173a25b6184",
"sha256:334f7ed39895d692f753a443102dd5fed180c571eb6a48b2a5b7f5b3564908c8", "sha256:1bba0a866f5895d5b769d8c36b161271c7fd407e5065862ab80ff91c29fbe554",
"sha256:33c5609ad51cd99d388e55651b19148ea99727516132fb44680e1f28dd0d1de9", "sha256:1d2a154b79d5fb42d1eff06e05e22f0e8091261d877dd47b37d31352b74ecc37",
"sha256:37a62cbe16d4c6294e84670b59cf7adcc73faafe6af07f8cb9adaf1f0e775b19", "sha256:1eb115d53dc2a1299ae72f1119c96a556db36073bacb6da40c47ece5db0d9587",
"sha256:38ae9a02766dad331deb06855fb7a6ca15daea333b3967e214de12cfae8f0ef5", "sha256:202eae632815571297833876a0f407d0d9c7ad9d843b38adbe687fe68c5192ee",
"sha256:38da436a0aaace9add67b999eb6abe4b84397edf4a78ec28f264e5b4c9d53cd5", "sha256:24f3c8be826a7d44181eac2269b15b748b76d98cd9a539d4c69f09321dcb5c12",
"sha256:415bfcfd01d1fbf5cbd75004599ef167a533395955305f42220a585f64036081", "sha256:28b8614de08679ae95bc4e8d6daaef6b61afdf027fa0d23bf13d619000286b3c",
"sha256:417de6b2e214e837827067048f61841f5d7fc27926f2e43954567094051aff18", "sha256:2b0080819859e80a7776ac47cf6accb4b7ad313baf55fabac89c000480dcd103",
"sha256:477d3da48e216d7fc04bddab67b0dcde633e19f484a146fd2a34bb0e9dbb4a1e", "sha256:2b2ef636a6a25fe7f3d5a064bea0116b74a4c8c7df9646b17dc7386c439a26cf",
"sha256:4a6ba2494a6449b1f477bd3e67935c2b7b0274f2f6dcd0f7c6aceae10c6c6ba3", "sha256:2c3b1ce68746557aa6e6f4547e76883925cdc7f8d7c7a9f518acd203f1265ca5",
"sha256:4fe1c49486109f72d502f8be569972e27f385fe632bd8895f4730df3c87d5ac8", "sha256:3058a1bb0c767b3a6ccbb38b27ef870af819923eb732e21e44a3f300370fe159",
"sha256:507e8dc2891021350eaea40a44ddd887c9f006e6b599af8d64a505c0f718f170", "sha256:30bb223e6c3f95ad9e9bb2a113292759e947d1cfd60dbd4adb55851c370006b2",
"sha256:53bc0d2358507596c22b02db079618451f3bd720755d88e3cccd840bafb4c41c", "sha256:317218c9d8b17a010ab2d2f0851e8ef584077a38b1ba2b7c55c9e44e79a61e73",
"sha256:554eae5a5b28f02705b83a230e9d163d645c9a08914c0ad921df363a07cf39b1", "sha256:342570716e2647a51ea859b8a9126da9dc1a96a0153c9c0a3514effd60ab57ad",
"sha256:59335d000c6250c35989394661eb6287187854e94ac79ea22315469ee4f4c244", "sha256:3b40d32a3e1f04e94231ed6dfee0da9e43b4f9c6b5450d53e6dd7754c34e0c50",
"sha256:5a740f05d0968a5a17da3d676ce6afefebeeeb5ce137510901bf6306ba8ee002", "sha256:3e55d7ecc68bdc80de5a553691a3ed260395d5362c19a266cf83cbb4e046551f",
"sha256:5bc94cf128676e8fac6503b37763adb378e2b6be1249d207630f83fc325d9b11", "sha256:475986ddf6df65d619acd52188336a20f616589403f5a5ceb3fc70cdc137037a",
"sha256:64286f8a995f2cd934082b398fc63fca7d5ffe31f0e27e75b3ca6b4efda4e353", "sha256:47669fc8ed8b27baa2d698104732234b5389f6a59c37c046f6bcbf9150f7a94e",
"sha256:664698ae0013f986118064b6676d7dcd28fefd0d7d5a5ae9497cbc10cba48fa5", "sha256:4afcb38e22122465013f4621b7e9ff8d42a7a48ae0ffeb94133a806cb91b4aad",
"sha256:68f960bc22d8d8e4ac886d1e2e21ccbd283adcf3c43136161c1ba0fa509088e0", "sha256:4ee5b19a2d794463bcc19153dfceede7beec784b4cf7967dec0bc0fc212ab3a3",
"sha256:69d565d20efe527bd8a9b92e7f299ae5e73b6c0470f3719bd66f3cd821e0d5bd", "sha256:5519f2a7b8c535b0f1f77b30441476571373add72008230c81211ee17b423b57",
"sha256:6a143b31d758296dc2f440175f6c8e0b5301ced3b0f477b84ca43cdcf7f2f476", "sha256:59e0dda9ad245921af0328035a961767026e1fa94bb65957ab0db0a0491325d6",
"sha256:6a249415f5bb88b5e9e8c4db47f609e0bf0e20a75e8d744ea787f3092ba1f2d0", "sha256:5a97d37ee4fe0d1c6e87d2a97229c27a88787a8f4ebfbdeee95f91b818e52efe",
"sha256:6b9e62ddb3db4b5205c0321dd69a406d8af9ee1693529d144e86bd43bcb4b6c0", "sha256:5d758e2eed2643afac5f2881542fbb5aa97640b54be20d0a5ed0691d02f0867d",
"sha256:7f1d36a1e15a46e8ede4e953abb104fdbc0845a266ec0e99cc0492a4364f8c44", "sha256:5edd01909348f3b0b2da329836d6b5419cd4869fec2e118e8ff3275b38af6267",
"sha256:816330675e3504ae4d9a2185c46b573105d2310c20b19ea2b4596a9460a4f674", "sha256:5f0521ed3bb9f02e9486573ea95e2062cd6bf036fa44e640bd54a06f22d85f35",
"sha256:87e668f9561fd3a457ba189edfc9e37709261287b52293c115ae3487a24b92f6", "sha256:62d66e0423e3bd520b4c897063506b309843a8d07343fbfad04197e91a4edd28",
"sha256:897a73208da48db41e687225f355ce993167079eda1260ba5e13c4e53be7f754", "sha256:66821fada6426998762a3650a37fba77e814a249a95b1183011070744aff47f6",
"sha256:8c331e91a14ba4076f88c29c777ad6b58639530ed5b24b5564b5ed2fd7a95452", "sha256:6b17655a5f245b416e127e02087ea6347a48821cc4626bc0fd57101bfcd88afc",
"sha256:950f6476d56ff7817a8fed4ab207727fc5260af83481b2a4b125f32844df513a", "sha256:6dc3d656702791565994e64035a208be56b065675a5bc87b644c657d6d9e2232",
"sha256:9617b676b95adb412bb69645d5b077d664b6882bb0d37bfdafbbb1b999568d85", "sha256:6e61dd7d05da11fc69bb86d51f1e0ee08f74dccf3ecf884c94de41135ffdc75d",
"sha256:9e3b99b338598de0bd6b2643bf8c343cf5ff70db3627af3ca427a5e1a1a90dd9", "sha256:726aee40357d4bdb70115442cb85ccc8e8bc554fc0bbbaa3a57cbe81df42287d",
"sha256:a1fcac43918b836ace25f69b1dca8c9395253ad8152b625064415b1d2f9be4fb", "sha256:743836d484151334ebba1490d6907ca9e718fe815dcd5756f2a01bc3067d099c",
"sha256:a680d62c399fa4b44899094027ec9a1bdaf6f31c650e44183b50d4c4d0ccc085", "sha256:754bac5e470adac7f7ec2239c485cd0b75f8197ca8a5b86ffb20eb3a3676cc42",
"sha256:a6d50252377db62d6a0bb82cc898089916457f2db2041e1d03ce7fadd4a07381", "sha256:779006f9e1aade9522a40e8d9635115ab15dd82b7af8e655967162e9c01e2573",
"sha256:a83ca086fbb017f0d87f240a8f9bbcf0809f3b754ee01cec928fff926542c450", "sha256:8ab9f5b7f2bbc1754bc6292d9a7312071058e5a891b0391e6d13b226133f36aa",
"sha256:a911b26264dbe9e8066c7531c0591cfab27b464459c74385b276fe487ca91c12", "sha256:8c52b12b10e4057fc302bd09cb3e3f28bb382c30c044eb3396e805179a8260e4",
"sha256:ab2900d111e93aff5df9fddc64cf51ca4ef2c9f98702ce26524f1acc5a786ae7", "sha256:90ea8d302a5d30b948451d146e94674a3c5b020cc0ced9a1c28f8ddb0f203a5d",
"sha256:ab92e51cd4a218208efee4c6d37db7368fdf182f6e7ff148fb295ecddf264287", "sha256:939d364a187b2adfbf6ae488664277e717d56c7951a4ddeb4f23b281bc50bfe5",
"sha256:accb66d7b3ccb00d5cd11d8c6e07055a4568a24c95cf86109894dcc0c134cc89", "sha256:97f1f861998e326e640708488c442519ad69046374b2c3fe9bcc9869b387f23c",
"sha256:ad6d20f9541f5f6ac63df553b6d7a04f313947f550eab6a61aa758b45f0d5657", "sha256:9c76e43cb351ba8887371240b599925cdf3ecececc5dfb7125c71678e7722c55",
"sha256:aeaa71fb9568ebe9b911ddb490c644fbd2006e8c940f21cb9a1e9425bd709574", "sha256:9c9d7d1267dee8c6627963d9376efa068858cfc8f573c083b1b6a2d297a8710f",
"sha256:af2c67ae4c795d1674a8d3ac1988676fa306bcfa1e23fddb5e0bd5f5635309ca", "sha256:9f9732e59863eaeedd3feef94b2b216cb86d40dda4fad2d0f0aaec3b31592716",
"sha256:af4061f67fd7ec01624c5e3c21f6b7af2ef0e6bab7fbb43f209e6506c9ce0092", "sha256:a166251b8fbc6f2755e2ce9d3c11e9edb0c0c7d2ed723658ff0161fbce26ac1c",
"sha256:b1067647ac7aba0b44b591936118a22847bda3c507b0a42d74272256a7a798e9", "sha256:a46ef3ecf61e4840fbc1145fdd38acf158d0da7543eda7b773ed2b30f75c2830",
"sha256:b5901a31012a40ec913265b64e48c2a4059278d9f4e6be628441482dd13fb8b5", "sha256:a65a147f545f3fac86f11acb2d0b316d3e78139a9372317b7eb50561b2817ba0",
"sha256:bbbd6c7de0771c84b4aa30e70b409565eb1fc13627a723ca6be774ed6b9d9fa3", "sha256:abda20aff8d1751e48d7967fa8945fef38536b82366c49be39b83678d4be3893",
"sha256:bdfd7254745bb737ca1384dee57e6523651892f0ea2a7344490e9caefcc35e64", "sha256:acf41cf91bbc5a36d1fa4455a818bb02bf2a4ccfed2f892ba166ba2fcbb0ec8a",
"sha256:c05ac48ef16ee013b8a326c63e4610e2430dbec037ec5c5b58fcced550382b74", "sha256:adcd6ba863a315702184d92d3d3bbff290514f24a14695d310f02ae5e28bd1f7",
"sha256:c1c84beab776b0becaa368254801e57692ed749d935469ac10e2b9b825dbdd8e", "sha256:b3afa5a0ed08a477ded239c05db14c19af585975194a00adf594d48533b23701",
"sha256:c32c41ace41f344d317399efff4cffb133e709cec2ef09c99e7a13e9f3b9483c", "sha256:b669aaac70bd9f03c070270b953f78d9ee56c4af6f0ff9f9cd3e6d1878c10b40",
"sha256:c3ba57029c9c50ecaf0c92bb127224cc2ea9fda057b5d99d3f348c9ec2855ad3", "sha256:bdaaa81a6e95a0a19fba0285f10577377f3503ae4e9988b403feba79da3e2f80",
"sha256:c7951820b77abe03d88b114b998867c0f99da03859e5ab2623d94690848d3e45", "sha256:cb5fefb53b9d9f334a5d9ec518a36b92a10b936011ac8a6b6dffd60135f16459",
"sha256:c913e151e7ea01567837ff037a23ca8740192880198b7fbb90b16d181607caae", "sha256:cb8a6a19be0cbf6da6fcbf3698b76c8af03fe83e4bd77c96c3922be3b88bf7da",
"sha256:c9a402881ec269d0cc9c354b149fc29f9ec1a1939a777f1c858cdb09c7a261df", "sha256:ceb3ce3262b0e8e088e4117175591b7a82b3bdc5e52e33b1e74778b5fb53fd38",
"sha256:cbf16381c7bf7f963b58fb4daaa65684e10966ee14d26f5cc90f07049bfd8c1e", "sha256:d06990dcfcd41bb3543c18dd25c8476fbfe1f236757f42fef560f6aa03ac8dfc",
"sha256:d4460795a8a7a391e3567b902ec5bdf6c60a47d791c3b1d27080fc203d11c9dc", "sha256:d6e3b177e634aa6bbbfbf66d097b6d1cdb80fc60e912c7d8bace2e45699c07dd",
"sha256:d7f27e0268af2d7503386e0e6be87fb9b6657afd96f5726b733837121146750d", "sha256:db78b645dc63c0ccffca367a498f3b13492fb106a2243a1e998303ba79c996e2",
"sha256:d876d355c53d975c2ef9c4f2487c8f83dad6aeaaee1b6571453cb0ee992f55f6", "sha256:ded03546938a987f463c68ab98d683af87a83db7ac8093bbc179e77680be5ba2",
"sha256:da0d57edd4c95bf221b2ebbaa061e65b1788f1544977288bdf95831b6e44e44d", "sha256:e152f4ab9ea1632b5fecdd87cee354f2b2eb6e2dfc3aceb0eb36a01c1e12f94c",
"sha256:ddedd300cd690a3b06e7eac90ed4452348b1348635777ce23d460d913b5b63c3", "sha256:ef133a1a2ee378d549048a12d56f4ef0e2b9113b0b25b6b77821e9af94d50634",
"sha256:df3f4274b80709d8bcab2f9a862973d453b308b97a0b423a501bcd93582852e3", "sha256:f0f749f07002c2d6fecf37cedc43207a88e6c651926a470a5f229070cf791879",
"sha256:e18b7bd0898398cc97ce2dfc83bb192a13a087ef6b2d5a8a36460311cb09e775", "sha256:f7bc7088c15150745baf66db62a4ced4507d44419eb66207b609f91b64a682af",
"sha256:e5119a211c2e98ff18b9908ba62a3bd0e3fabb02a29277a7232a6fb4b2560aa0", "sha256:f859c88193ed466bee4eb9d45fbc29d2253e6aa3ccd9119c9a1d8d95f409a60d",
"sha256:e5e08e232b78ba3ac6bc0d23ce9e2bee8fad2be391b7e2da834fc9a45129eb87", "sha256:f963b6ed8ba33eda374a98d979e8a0eaf21f891b6e334701693a2c9510613c4c",
"sha256:eae8988e78192fd1a3245a6f4f382390b61bce6cfcc93f3809726e4c885fa68d", "sha256:fa5057a86df920faab8ee83960b724bace01a3231eb8e3f2c93d78283504d598",
"sha256:f05e66746bf9fe6a3397ec246fe481096664a9c97eb3fea6004735a4daf867fd", "sha256:fb693000b65842c85356b667d057ae0d0bac6519feca7e1c437cc2cfeb0afc59",
"sha256:f1da82c3e11284da4fcbf54957dafbf0655d2389cd3d54e4eaba636faf6d117a", "sha256:fc9d79b1bf833af63b7c720a6604eb16453ac1ad4e718eb8b59d1f97d986b98c",
"sha256:f38d48abc121263f3b62943f84cbaede05749047e428409c2c199664feb6abc7", "sha256:ffcb09d5c0ffa043254ec9a734a73f3791fec8bf6333592f906bb2e91ed2af1a"
"sha256:f5e7266498cd31a4572378c61920af9f6b4676a73c299fce8ba93afd694f8ae7",
"sha256:fc74e84fdfa96821580fb5e9c0b0777c1c4779434ce16d3d62a9c4d8c710df39",
"sha256:ff814dea1e5c492a4980e3e7d094286077054e7ea116cbeda138819db194b2cd"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.8'",
"version": "==1.3.3" "version": "==1.3.4"
}, },
"six": { "six": {
"hashes": [ "hashes": [
@ -2206,11 +2206,11 @@
}, },
"sqlparse": { "sqlparse": {
"hashes": [ "hashes": [
"sha256:773dcbf9a5ab44a090f3441e2180efe2560220203dc2f8c0b0fa141e18b505e4", "sha256:9e37b35e16d1cc652a2545f0997c1deb23ea28fa1f3eefe609eee3063c3b105f",
"sha256:bb6b4df465655ef332548e24f08e205afc81b9ab86cb1c45657a7ff173a3a00e" "sha256:e99bc85c78160918c3e1d9230834ab8d80fc06c59d03f8db2618f65f65dda55e"
], ],
"markers": "python_version >= '3.8'", "markers": "python_version >= '3.8'",
"version": "==0.5.1" "version": "==0.5.2"
}, },
"threadpoolctl": { "threadpoolctl": {
"hashes": [ "hashes": [
@ -2249,12 +2249,12 @@
}, },
"tqdm": { "tqdm": {
"hashes": [ "hashes": [
"sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be", "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2",
"sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a" "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"
], ],
"index": "pypi", "index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==4.67.0" "version": "==4.67.1"
}, },
"typing-extensions": { "typing-extensions": {
"hashes": [ "hashes": [
@ -2585,79 +2585,74 @@
}, },
"wrapt": { "wrapt": {
"hashes": [ "hashes": [
"sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", "sha256:0229b247b0fc7dee0d36176cbb79dbaf2a9eb7ecc50ec3121f40ef443155fb1d",
"sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", "sha256:0698d3a86f68abc894d537887b9bbf84d29bcfbc759e23f4644be27acf6da301",
"sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", "sha256:0a0a1a1ec28b641f2a3a2c35cbe86c00051c04fffcfcc577ffcdd707df3f8635",
"sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e", "sha256:0b48554952f0f387984da81ccfa73b62e52817a4386d070c75e4db7d43a28c4a",
"sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca", "sha256:0f2a28eb35cf99d5f5bd12f5dd44a0f41d206db226535b37b0c60e9da162c3ed",
"sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0", "sha256:140ea00c87fafc42739bd74a94a5a9003f8e72c27c47cd4f61d8e05e6dec8721",
"sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb", "sha256:16187aa2317c731170a88ef35e8937ae0f533c402872c1ee5e6d079fcf320801",
"sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", "sha256:17fcf043d0b4724858f25b8826c36e08f9fb2e475410bece0ec44a22d533da9b",
"sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40", "sha256:18b956061b8db634120b58f668592a772e87e2e78bc1f6a906cfcaa0cc7991c1",
"sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", "sha256:2399408ac33ffd5b200480ee858baa58d77dd30e0dd0cab6a8a9547135f30a88",
"sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", "sha256:2a0c23b8319848426f305f9cb0c98a6e32ee68a36264f45948ccf8e7d2b941f8",
"sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202", "sha256:2dfb7cff84e72e7bf975b06b4989477873dcf160b2fd89959c629535df53d4e0",
"sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41", "sha256:2f495b6754358979379f84534f8dd7a43ff8cff2558dcdea4a148a6e713a758f",
"sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", "sha256:33539c6f5b96cf0b1105a0ff4cf5db9332e773bb521cc804a90e58dc49b10578",
"sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", "sha256:3c34f6896a01b84bab196f7119770fd8466c8ae3dfa73c59c0bb281e7b588ce7",
"sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664", "sha256:498fec8da10e3e62edd1e7368f4b24aa362ac0ad931e678332d1b209aec93045",
"sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", "sha256:4d63f4d446e10ad19ed01188d6c1e1bb134cde8c18b0aa2acfd973d41fcc5ada",
"sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", "sha256:4e4b4385363de9052dac1a67bfb535c376f3d19c238b5f36bddc95efae15e12d",
"sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00", "sha256:4e547b447073fc0dbfcbff15154c1be8823d10dab4ad401bdb1575e3fdedff1b",
"sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", "sha256:4f643df3d4419ea3f856c5c3f40fec1d65ea2e89ec812c83f7767c8730f9827a",
"sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", "sha256:4f763a29ee6a20c529496a20a7bcb16a73de27f5da6a843249c7047daf135977",
"sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267", "sha256:5ae271862b2142f4bc687bdbfcc942e2473a89999a54231aa1c2c676e28f29ea",
"sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", "sha256:5d8fd17635b262448ab8f99230fe4dac991af1dabdbb92f7a70a6afac8a7e346",
"sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966", "sha256:69c40d4655e078ede067a7095544bcec5a963566e17503e75a3a3e0fe2803b13",
"sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", "sha256:69d093792dc34a9c4c8a70e4973a3361c7a7578e9cd86961b2bbf38ca71e4e22",
"sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228", "sha256:6a9653131bda68a1f029c52157fd81e11f07d485df55410401f745007bd6d339",
"sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", "sha256:6ff02a91c4fc9b6a94e1c9c20f62ea06a7e375f42fe57587f004d1078ac86ca9",
"sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", "sha256:714c12485aa52efbc0fc0ade1e9ab3a70343db82627f90f2ecbc898fdf0bb181",
"sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292", "sha256:7264cbb4a18dc4acfd73b63e4bcfec9c9802614572025bdd44d0721983fc1d9c",
"sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", "sha256:73a96fd11d2b2e77d623a7f26e004cc31f131a365add1ce1ce9a19e55a1eef90",
"sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0", "sha256:74bf625b1b4caaa7bad51d9003f8b07a468a704e0644a700e936c357c17dd45a",
"sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", "sha256:81b1289e99cf4bad07c23393ab447e5e96db0ab50974a280f7954b071d41b489",
"sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c", "sha256:8425cfce27b8b20c9b89d77fb50e368d8306a90bf2b6eef2cdf5cd5083adf83f",
"sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5", "sha256:875d240fdbdbe9e11f9831901fb8719da0bd4e6131f83aa9f69b96d18fae7504",
"sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f", "sha256:879591c2b5ab0a7184258274c42a126b74a2c3d5a329df16d69f9cee07bba6ea",
"sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", "sha256:89fc28495896097622c3fc238915c79365dd0ede02f9a82ce436b13bd0ab7569",
"sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", "sha256:8a5e7cc39a45fc430af1aefc4d77ee6bad72c5bcdb1322cfde852c15192b8bd4",
"sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2", "sha256:8f8909cdb9f1b237786c09a810e24ee5e15ef17019f7cecb207ce205b9b5fcce",
"sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593", "sha256:914f66f3b6fc7b915d46c1cc424bc2441841083de01b90f9e81109c9759e43ab",
"sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39", "sha256:92a3d214d5e53cb1db8b015f30d544bc9d3f7179a05feb8f16df713cecc2620a",
"sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", "sha256:948a9bd0fb2c5120457b07e59c8d7210cbc8703243225dbd78f4dfc13c8d2d1f",
"sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf", "sha256:9c900108df470060174108012de06d45f514aa4ec21a191e7ab42988ff42a86c",
"sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", "sha256:9f2939cd4a2a52ca32bc0b359015718472d7f6de870760342e7ba295be9ebaf9",
"sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", "sha256:a4192b45dff127c7d69b3bdfb4d3e47b64179a0b9900b6351859f3001397dabf",
"sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c", "sha256:a8fc931382e56627ec4acb01e09ce66e5c03c384ca52606111cee50d931a342d",
"sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c", "sha256:ad47b095f0bdc5585bced35bd088cbfe4177236c7df9984b3cc46b391cc60627",
"sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f", "sha256:b1ca5f060e205f72bec57faae5bd817a1560fcfc4af03f414b08fa29106b7e2d",
"sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", "sha256:ba1739fb38441a27a676f4de4123d3e858e494fac05868b7a281c0a383c098f4",
"sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465", "sha256:baa7ef4e0886a6f482e00d1d5bcd37c201b383f1d314643dfb0367169f94f04c",
"sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", "sha256:bb90765dd91aed05b53cd7a87bd7f5c188fcd95960914bae0d32c5e7f899719d",
"sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b", "sha256:bc7f729a72b16ee21795a943f85c6244971724819819a41ddbaeb691b2dd85ad",
"sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8", "sha256:bdf62d25234290db1837875d4dceb2151e4ea7f9fff2ed41c0fde23ed542eb5b",
"sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", "sha256:c30970bdee1cad6a8da2044febd824ef6dc4cc0b19e39af3085c763fdec7de33",
"sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8", "sha256:d2c63b93548eda58abf5188e505ffed0229bf675f7c3090f8e36ad55b8cbc371",
"sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6", "sha256:d751300b94e35b6016d4b1e7d0e7bbc3b5e1751e2405ef908316c2a9024008a1",
"sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e", "sha256:da427d311782324a376cacb47c1a4adc43f99fd9d996ffc1b3e8529c4074d393",
"sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f", "sha256:daba396199399ccabafbfc509037ac635a6bc18510ad1add8fd16d4739cdd106",
"sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c", "sha256:e185ec6060e301a7e5f8461c86fb3640a7beb1a0f0208ffde7a65ec4074931df",
"sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e", "sha256:e4a557d97f12813dc5e18dad9fa765ae44ddd56a672bb5de4825527c847d6379",
"sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", "sha256:e5ed16d95fd142e9c72b6c10b06514ad30e846a0d0917ab406186541fe68b451",
"sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", "sha256:e711fc1acc7468463bc084d1b68561e40d1eaa135d8c509a65dd534403d83d7b",
"sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", "sha256:f28b29dc158ca5d6ac396c8e0a2ef45c4e97bb7e65522bfc04c989e6fe814575",
"sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35", "sha256:f335579a1b485c834849e9075191c9898e0731af45705c2ebf70e0cd5d58beed",
"sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", "sha256:fce6fee67c318fdfb7f285c29a82d84782ae2579c0e1b385b7f36c6e8074fffb",
"sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3", "sha256:fd136bb85f4568fffca995bd3c8d52080b1e5b225dbf1c2b17b66b4c5fa02838"
"sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537",
"sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809",
"sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d",
"sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a",
"sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"
], ],
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.8'",
"version": "==1.16.0" "version": "==1.17.0"
}, },
"zstandard": { "zstandard": {
"hashes": [ "hashes": [

View File

@ -33,6 +33,7 @@
"ngx-ui-tour-ng-bootstrap": "^15.0.0", "ngx-ui-tour-ng-bootstrap": "^15.0.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"utif": "^3.1.0",
"uuid": "^11.0.2", "uuid": "^11.0.2",
"zone.js": "^0.14.8" "zone.js": "^0.14.8"
}, },
@ -13758,6 +13759,12 @@
"node": "^16.14.0 || >=18.0.0" "node": "^16.14.0 || >=18.0.0"
} }
}, },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -16563,6 +16570,15 @@
"requires-port": "^1.0.0" "requires-port": "^1.0.0"
} }
}, },
"node_modules/utif": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/utif/-/utif-3.1.0.tgz",
"integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==",
"license": "MIT",
"dependencies": {
"pako": "^1.0.5"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -35,6 +35,7 @@
"ngx-ui-tour-ng-bootstrap": "^15.0.0", "ngx-ui-tour-ng-bootstrap": "^15.0.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"utif": "^3.1.0",
"uuid": "^11.0.2", "uuid": "^11.0.2",
"zone.js": "^0.14.8" "zone.js": "^0.14.8"
}, },

View File

@ -47,14 +47,19 @@
</tr> </tr>
} }
@for (document of documentsInTrash; track document.id) { @for (document of documentsInTrash; track document.id) {
<tr (click)="toggleSelected(document); $event.stopPropagation();"> <tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()">
<td> <td>
<div class="form-check m-0 ms-2 me-n2"> <div class="form-check m-0 ms-2 me-n2">
<input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();"> <input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
<label class="form-check-label" for="{{document.id}}"></label> <label class="form-check-label" for="{{document.id}}"></label>
</div> </div>
</td> </td>
<td scope="row">{{ document.title }}</td> <td scope="row">
{{ document.title }}
<pngx-preview-popup [document]="document" linkClasses="btn btn-sm btn-link" #popupPreview>
<i-bs name="eye"></i-bs>
</pngx-preview-popup>
</td>
<td scope="row" i18n>{{ getDaysRemaining(document) }} days</td> <td scope="row" i18n>{{ getDaysRemaining(document) }} days</td>
<td scope="row"> <td scope="row">
<div class="btn-group d-block d-sm-none"> <div class="btn-group d-block d-sm-none">

View File

@ -1,6 +1,6 @@
.pdf-viewer-container { .pdf-viewer-container {
background-color: gray; background-color: gray;
height: 350px; height: 550px;
pdf-viewer { pdf-viewer {
width: 100%; width: 100%;

View File

@ -6,7 +6,7 @@
<div class="modal-body"> <div class="modal-body">
<p>{{message}}</p> <p>{{message}}</p>
<div class="row mb-2"> <div class="row mb-2">
<div class="col-8"> <div class="col-7">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<div class="input-group-text" i18n>Page</div> <div class="input-group-text" i18n>Page</div>
<input class="form-control" type="number" min="1" [(ngModel)]="page" /> <input class="form-control" type="number" min="1" [(ngModel)]="page" />
@ -21,7 +21,7 @@
</pdf-viewer> </pdf-viewer>
</div> </div>
</div> </div>
<div class="col-4"> <div class="col-5">
<div class="d-grid"> <div class="d-grid">
<button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit"> <button class="btn btn-sm btn-primary" (click)="addSplit()" [disabled]="!canSplit">
<i-bs name="plus-circle"></i-bs>&nbsp; <i-bs name="plus-circle"></i-bs>&nbsp;
@ -44,12 +44,12 @@
</ul> </ul>
</div> </div>
</div> </div>
<div class="form-check form-switch mt-4"> </div>
<div class="modal-footer">
<div class="form-check form-switch me-auto">
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalSwitch" [(ngModel)]="deleteOriginal" [disabled]="!userOwnsDocument"> <input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalSwitch" [(ngModel)]="deleteOriginal" [disabled]="!userOwnsDocument">
<label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label> <label class="form-check-label" for="deleteOriginalSwitch" i18n>Delete original document after successful split</label>
</div> </div>
</div>
<div class="modal-footer">
<button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled"> <button type="button" class="btn" [class]="cancelBtnClass" (click)="cancel()" [disabled]="!buttonsEnabled">
<span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span> <span class="d-inline-block" style="padding-bottom: 1px;">{{cancelBtnCaption}}</span>
</button> </button>

View File

@ -1,6 +1,6 @@
.pdf-viewer-container { .pdf-viewer-container {
background-color: gray; background-color: gray;
height: 350px; height: 500px;
pdf-viewer { pdf-viewer {
width: 100%; width: 100%;

View File

@ -38,7 +38,15 @@
@for (item of selectionModel.items | filter: filterText:'name'; track item; let i = $index) { @for (item of selectionModel.items | filter: filterText:'name'; track item; let i = $index) {
@if (allowSelectNone || item.id) { @if (allowSelectNone || item.id) {
<pngx-toggleable-dropdown-button <pngx-toggleable-dropdown-button
[item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggled)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled"> [item]="item"
[hideCount]="hideCount(item)"
[opacifyCount]="!editing"
[state]="selectionModel.get(item.id)"
[count]="getUpdatedDocumentCount(item.id)"
(toggled)="selectionModel.toggle(item.id)"
(exclude)="excludeClicked(item.id)"
(click)="setButtonItemIndex(i - 1)"
[disabled]="disabled">
</pngx-toggleable-dropdown-button> </pngx-toggleable-dropdown-button>
} }
} }

View File

@ -509,6 +509,37 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () =>
]) ])
}) })
it('selection model should sort items by state and document counts, if set', () => {
component.items = items.concat([{ id: 4, name: 'Item D' }])
component.selectionModel = selectionModel
component.documentCounts = [
{ id: 1, document_count: 0 }, // Tag1
{ id: 2, document_count: 1 }, // Tag2
{ id: 4, document_count: 2 },
]
component.selectionModel.apply()
expect(selectionModel.items).toEqual([
nullItem,
{ id: 4, name: 'Item D' },
items[1], // Tag2
items[0], // Tag1
])
selectionModel.toggle(items[1].id)
component.documentCounts = [
{ id: 1, document_count: 0 },
{ id: 2, document_count: 1 },
{ id: 4, document_count: 0 },
]
selectionModel.apply()
expect(selectionModel.items).toEqual([
nullItem,
items[1], // Tag2
{ id: 4, name: 'Item D' },
items[0], // Tag1
])
})
it('should set support create, keep open model and call createRef method', fakeAsync(() => { it('should set support create, keep open model and call createRef method', fakeAsync(() => {
component.items = items component.items = items
component.icon = 'tag-fill' component.icon = 'tag-fill'

View File

@ -43,6 +43,11 @@ export class FilterableDropdownSelectionModel {
private _intersection: Intersection = Intersection.Include private _intersection: Intersection = Intersection.Include
temporaryIntersection: Intersection = this._intersection temporaryIntersection: Intersection = this._intersection
private _documentCounts: SelectionDataItem[] = []
public set documentCounts(counts: SelectionDataItem[]) {
this._documentCounts = counts
}
private _items: MatchingModel[] = [] private _items: MatchingModel[] = []
get items(): MatchingModel[] { get items(): MatchingModel[] {
return this._items return this._items
@ -69,6 +74,16 @@ export class FilterableDropdownSelectionModel {
this.getNonTemporary(b.id) == ToggleableItemState.NotSelected this.getNonTemporary(b.id) == ToggleableItemState.NotSelected
) { ) {
return -1 return -1
} else if (
this._documentCounts.length &&
this.getDocumentCount(a.id) > this.getDocumentCount(b.id)
) {
return -1
} else if (
this._documentCounts.length &&
this.getDocumentCount(a.id) < this.getDocumentCount(b.id)
) {
return 1
} else { } else {
return a.name.localeCompare(b.name) return a.name.localeCompare(b.name)
} }
@ -286,6 +301,10 @@ export class FilterableDropdownSelectionModel {
) )
} }
getDocumentCount(id: number) {
return this._documentCounts.find((c) => c.id === id)?.document_count
}
init(map: Map<number, ToggleableItemState>) { init(map: Map<number, ToggleableItemState>) {
this.temporarySelectionStates = map this.temporarySelectionStates = map
this.apply() this.apply()
@ -431,7 +450,11 @@ export class FilterableDropdownComponent implements OnDestroy, OnInit {
} }
@Input() @Input()
documentCounts: SelectionDataItem[] set documentCounts(counts: SelectionDataItem[]) {
if (counts) {
this.selectionModel.documentCounts = counts
}
}
@Input() @Input()
shortcutKey: string shortcutKey: string
@ -544,9 +567,7 @@ export class FilterableDropdownComponent implements OnDestroy, OnInit {
} }
getUpdatedDocumentCount(id: number) { getUpdatedDocumentCount(id: number) {
if (this.documentCounts) { return this.selectionModel.getDocumentCount(id)
return this.documentCounts.find((c) => c.id === id)?.document_count
}
} }
listKeyDown(event: KeyboardEvent) { listKeyDown(event: KeyboardEvent) {

View File

@ -1,4 +1,9 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom" role="menuitem" (click)="toggleItem($event)" [disabled]="disabled"> <button
class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-start-0 border-end-0 border-bottom"
[class.opacity-50]="opacifyCount && !hideCount && currentCount === 0"
role="menuitem"
(click)="toggleItem($event)"
[disabled]="disabled">
<div class="selected-icon me-1"> <div class="selected-icon me-1">
@if (isChecked()) { @if (isChecked()) {
<i-bs width="1em" height="1em" name="check"></i-bs> <i-bs width="1em" height="1em" name="check"></i-bs>
@ -18,6 +23,6 @@
} }
</div> </div>
@if (!hideCount) { @if (!hideCount) {
<div class="badge bg-light text-dark rounded-pill ms-auto me-1">{{count ?? item.document_count}}</div> <div class="badge bg-light text-dark rounded-pill ms-auto me-1">{{currentCount}}</div>
} }
</button> </button>

View File

@ -29,6 +29,9 @@ export class ToggleableDropdownButtonComponent {
@Input() @Input()
hideCount: boolean = false hideCount: boolean = false
@Input()
opacifyCount: boolean = true
@Output() @Output()
toggled = new EventEmitter() toggled = new EventEmitter()
@ -39,6 +42,10 @@ export class ToggleableDropdownButtonComponent {
return 'is_inbox_tag' in this.item return 'is_inbox_tag' in this.item
} }
get currentCount(): number {
return this.count ?? this.item.document_count
}
toggleItem(event: MouseEvent): void { toggleItem(event: MouseEvent): void {
if (this.state == ToggleableItemState.Selected) { if (this.state == ToggleableItemState.Selected) {
this.exclude.emit() this.exclude.emit()

View File

@ -1,3 +1,9 @@
<a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle"
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body"
autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<ng-content></ng-content>
</a>
<ng-template #previewContent>
<div class="preview-popup-container"> <div class="preview-popup-container">
@if (error) { @if (error) {
<div class="w-100 h-100 position-relative"> <div class="w-100 h-100 position-relative">
@ -28,3 +34,4 @@
} }
} }
</div> </div>
</ng-template>

View File

@ -1,4 +1,9 @@
import { ComponentFixture, TestBed } from '@angular/core/testing' import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing'
import { PreviewPopupComponent } from './preview-popup.component' import { PreviewPopupComponent } from './preview-popup.component'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
@ -15,6 +20,8 @@ import {
withInterceptorsFromDi, withInterceptorsFromDi,
} from '@angular/common/http' } from '@angular/common/http'
import { of, throwError } from 'rxjs' import { of, throwError } from 'rxjs'
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
const doc = { const doc = {
id: 10, id: 10,
@ -34,8 +41,12 @@ describe('PreviewPopupComponent', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [PreviewPopupComponent, SafeUrlPipe], declarations: [PreviewPopupComponent, SafeUrlPipe, DocumentTitlePipe],
imports: [NgxBootstrapIconsModule.pick(allIcons), PdfViewerModule], imports: [
NgxBootstrapIconsModule.pick(allIcons),
PdfViewerModule,
NgbPopoverModule,
],
providers: [ providers: [
provideHttpClient(withInterceptorsFromDi()), provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(), provideHttpClientTesting(),
@ -70,12 +81,14 @@ describe('PreviewPopupComponent', () => {
it('should render object if native PDF viewer enabled', () => { it('should render object if native PDF viewer enabled', () => {
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true) settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true)
component.popover.open()
fixture.detectChanges() fixture.detectChanges()
expect(fixture.debugElement.query(By.css('object'))).not.toBeNull() expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
}) })
it('should render pngx viewer if native PDF viewer disabled', () => { it('should render pngx viewer if native PDF viewer disabled', () => {
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false) settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
component.popover.open()
fixture.detectChanges() fixture.detectChanges()
expect(fixture.debugElement.query(By.css('object'))).toBeNull() expect(fixture.debugElement.query(By.css('object'))).toBeNull()
expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull() expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
@ -83,6 +96,7 @@ describe('PreviewPopupComponent', () => {
it('should show lock icon on password error', () => { it('should show lock icon on password error', () => {
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false) settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
component.popover.open()
component.onError({ name: 'PasswordException' }) component.onError({ name: 'PasswordException' })
fixture.detectChanges() fixture.detectChanges()
expect(component.requiresPassword).toBeTruthy() expect(component.requiresPassword).toBeTruthy()
@ -93,16 +107,18 @@ describe('PreviewPopupComponent', () => {
component.document.original_file_name = 'sample.png' component.document.original_file_name = 'sample.png'
component.document.mime_type = 'image/png' component.document.mime_type = 'image/png'
component.document.archived_file_name = undefined component.document.archived_file_name = undefined
component.popover.open()
fixture.detectChanges() fixture.detectChanges()
expect(fixture.debugElement.query(By.css('object'))).not.toBeNull() expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
}) })
it('should show message on error', () => { it('should show message on error', () => {
component.popover.open()
component.onError({}) component.onError({})
fixture.detectChanges() fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).toContain( expect(
'Error loading preview' fixture.debugElement.query(By.css('.popover')).nativeElement.textContent
) ).toContain('Error loading preview')
}) })
it('should get text content from http if appropriate', () => { it('should get text content from http if appropriate', () => {
@ -122,4 +138,17 @@ describe('PreviewPopupComponent', () => {
component.init() component.init()
expect(component.previewText).toEqual('Preview text') expect(component.previewText).toEqual('Preview text')
}) })
it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
component.mouseEnterPreview()
expect(component.popover.isOpen()).toBeTruthy()
tick(600)
component.close()
component.mouseEnterPreview()
tick(100)
component.mouseLeavePreview()
tick(600)
expect(component.popover.isOpen()).toBeFalsy()
}))
}) })

View File

@ -1,5 +1,6 @@
import { HttpClient } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { Component, Input, OnDestroy } from '@angular/core' import { Component, Input, OnDestroy, ViewChild } from '@angular/core'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
import { first, Subject, takeUntil } from 'rxjs' import { first, Subject, takeUntil } from 'rxjs'
import { Document } from 'src/app/data/document' import { Document } from 'src/app/data/document'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
@ -23,6 +24,18 @@ export class PreviewPopupComponent implements OnDestroy {
return this._document return this._document
} }
@Input()
link: string
@Input()
linkClasses: string = 'btn btn-sm btn-outline-secondary'
@Input()
linkTarget: string = '_blank'
@Input()
linkTitle: string = $localize`Open preview`
unsubscribeNotifier: Subject<any> = new Subject() unsubscribeNotifier: Subject<any> = new Subject()
error = false error = false
@ -31,6 +44,12 @@ export class PreviewPopupComponent implements OnDestroy {
previewText: string previewText: string
@ViewChild('popover') popover: NgbPopover
mouseOnPreview: boolean
popoverClass: string = 'shadow popover-preview'
get renderAsObject(): boolean { get renderAsObject(): boolean {
return (this.isPdf && this.useNativePdfViewer) || !this.isPdf return (this.isPdf && this.useNativePdfViewer) || !this.isPdf
} }
@ -83,4 +102,33 @@ export class PreviewPopupComponent implements OnDestroy {
this.error = true this.error = true
} }
} }
get previewUrl() {
return this.documentService.getPreviewUrl(this.document.id)
}
mouseEnterPreview() {
this.mouseOnPreview = true
if (!this.popover.isOpen()) {
// we're going to open but hide to pre-load content during hover delay
this.popover.open()
this.popoverClass = 'shadow popover-preview pe-none opacity-0'
setTimeout(() => {
if (this.mouseOnPreview) {
// show popover
this.popoverClass = this.popoverClass.replace('pe-none opacity-0', '')
} else {
this.popover.close()
}
}, 600)
}
}
mouseLeavePreview() {
this.mouseOnPreview = false
}
public close() {
this.popover.close(false)
}
} }

View File

@ -388,6 +388,15 @@
<img [src]="previewUrl | safeUrl" width="100%" height="100%" alt="{{title}}" /> <img [src]="previewUrl | safeUrl" width="100%" height="100%" alt="{{title}}" />
</div> </div>
} }
@case (ContentRenderType.TIFF) {
@if (!tiffError) {
<div class="preview-sticky">
<img [src]="tiffURL" width="100%" height="100%" alt="{{title}}" />
</div>
} @else {
<div class="preview-sticky bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{tiffError}}</div>
}
}
@case (ContentRenderType.Other) { @case (ContentRenderType.Other) {
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
} }

View File

@ -61,6 +61,7 @@ textarea.rtl {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
object-position: top;
} }
.thumb-preview { .thumb-preview {

View File

@ -1270,4 +1270,46 @@ describe('DocumentDetailComponent', () => {
expect(component.createDisabled(DataType.StoragePath)).toBeFalsy() expect(component.createDisabled(DataType.StoragePath)).toBeFalsy()
expect(component.createDisabled(DataType.Tag)).toBeFalsy() expect(component.createDisabled(DataType.Tag)).toBeFalsy()
}) })
it('should call tryRenderTiff when no archive and file is tiff', () => {
initNormally()
const tiffRenderSpy = jest.spyOn(
DocumentDetailComponent.prototype as any,
'tryRenderTiff'
)
const doc = Object.assign({}, component.document)
doc.archived_file_name = null
doc.mime_type = 'image/tiff'
jest
.spyOn(documentService, 'getMetadata')
.mockReturnValue(
of({ has_archive_version: false, original_mime_type: 'image/tiff' })
)
component.updateComponent(doc)
fixture.detectChanges()
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.TIFF
)
expect(tiffRenderSpy).toHaveBeenCalled()
})
it('should try to render tiff and show error if failed', () => {
initNormally()
// just the text request
httpTestingController.expectOne(component.previewUrl)
// invalid tiff
component['tryRenderTiff']()
httpTestingController
.expectOne(component.previewUrl)
.flush(new ArrayBuffer(100)) // arraybuffer
expect(component.tiffError).not.toBeUndefined()
// http error
component['tryRenderTiff']()
httpTestingController
.expectOne(component.previewUrl)
.error(new ErrorEvent('failed'))
expect(component.tiffError).not.toBeUndefined()
})
}) })

View File

@ -72,6 +72,7 @@ import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/dele
import { HotKeyService } from 'src/app/services/hot-key.service' import { HotKeyService } from 'src/app/services/hot-key.service'
import { PDFDocumentProxy } from 'ng2-pdf-viewer' import { PDFDocumentProxy } from 'ng2-pdf-viewer'
import { DataType } from 'src/app/data/datatype' import { DataType } from 'src/app/data/datatype'
import * as UTIF from 'utif'
enum DocumentDetailNavIDs { enum DocumentDetailNavIDs {
Details = 1, Details = 1,
@ -89,6 +90,7 @@ enum ContentRenderType {
Text = 'text', Text = 'text',
Other = 'other', Other = 'other',
Unknown = 'unknown', Unknown = 'unknown',
TIFF = 'tiff',
} }
enum ZoomSetting { enum ZoomSetting {
@ -136,6 +138,8 @@ export class DocumentDetailComponent
downloadUrl: string downloadUrl: string
downloadOriginalUrl: string downloadOriginalUrl: string
previewLoaded: boolean = false previewLoaded: boolean = false
tiffURL: string
tiffError: string
correspondents: Correspondent[] correspondents: Correspondent[]
documentTypes: DocumentType[] documentTypes: DocumentType[]
@ -244,6 +248,8 @@ export class DocumentDetailComponent
['text/plain', 'application/csv', 'text/csv'].includes(mimeType) ['text/plain', 'application/csv', 'text/csv'].includes(mimeType)
) { ) {
return ContentRenderType.Text return ContentRenderType.Text
} else if (mimeType.indexOf('tiff') >= 0) {
return ContentRenderType.TIFF
} else if (mimeType?.indexOf('image/') === 0) { } else if (mimeType?.indexOf('image/') === 0) {
return ContentRenderType.Image return ContentRenderType.Image
} }
@ -542,6 +548,9 @@ export class DocumentDetailComponent
this.document = doc this.document = doc
this.requiresPassword = false this.requiresPassword = false
this.updateFormForCustomFields() this.updateFormForCustomFields()
if (this.archiveContentRenderType === ContentRenderType.TIFF) {
this.tryRenderTiff()
}
this.documentsService this.documentsService
.getMetadata(doc.id) .getMetadata(doc.id)
.pipe( .pipe(
@ -721,6 +730,7 @@ export class DocumentDetailComponent
save(close: boolean = false) { save(close: boolean = false) {
this.networkActive = true this.networkActive = true
;(document.activeElement as HTMLElement)?.dispatchEvent(new Event('change'))
this.documentsService this.documentsService
.update(this.document) .update(this.document)
.pipe(first()) .pipe(first())
@ -1163,6 +1173,7 @@ export class DocumentDetailComponent
splitDocument() { splitDocument() {
let modal = this.modalService.open(SplitConfirmDialogComponent, { let modal = this.modalService.open(SplitConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'lg',
}) })
modal.componentInstance.title = $localize`Split confirm` modal.componentInstance.title = $localize`Split confirm`
modal.componentInstance.messageBold = $localize`This operation will split the selected document(s) into new documents.` modal.componentInstance.messageBold = $localize`This operation will split the selected document(s) into new documents.`
@ -1201,6 +1212,7 @@ export class DocumentDetailComponent
rotateDocument() { rotateDocument() {
let modal = this.modalService.open(RotateConfirmDialogComponent, { let modal = this.modalService.open(RotateConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'lg',
}) })
modal.componentInstance.title = $localize`Rotate confirm` modal.componentInstance.title = $localize`Rotate confirm`
modal.componentInstance.messageBold = $localize`This operation will permanently rotate the original version of the current document.` modal.componentInstance.messageBold = $localize`This operation will permanently rotate the original version of the current document.`
@ -1275,4 +1287,45 @@ export class DocumentDetailComponent
}) })
}) })
} }
private tryRenderTiff() {
this.http.get(this.previewUrl, { responseType: 'arraybuffer' }).subscribe({
next: (res) => {
/* istanbul ignore next */
try {
// See UTIF.js > _imgLoaded
const tiffIfds: any[] = UTIF.decode(res)
var vsns = tiffIfds,
ma = 0,
page = vsns[0]
if (tiffIfds[0].subIFD) vsns = vsns.concat(tiffIfds[0].subIFD)
for (var i = 0; i < vsns.length; i++) {
var img = vsns[i]
if (img['t258'] == null || img['t258'].length < 3) continue
var ar = img['t256'] * img['t257']
if (ar > ma) {
ma = ar
page = img
}
}
UTIF.decodeImage(res, page, tiffIfds)
const rgba = UTIF.toRGBA8(page)
const { width: w, height: h } = page
var cnv = document.createElement('canvas')
cnv.width = w
cnv.height = h
var ctx = cnv.getContext('2d'),
imgd = ctx.createImageData(w, h)
for (var i = 0; i < rgba.length; i++) imgd.data[i] = rgba[i]
ctx.putImageData(imgd, 0, 0)
this.tiffURL = cnv.toDataURL()
} catch (err) {
this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}`
}
},
error: (err) => {
this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}`
},
})
}
} }

View File

@ -782,11 +782,11 @@ export class BulkEditorComponent
rotateSelected() { rotateSelected() {
let modal = this.modalService.open(RotateConfirmDialogComponent, { let modal = this.modalService.open(RotateConfirmDialogComponent, {
backdrop: 'static', backdrop: 'static',
size: 'lg',
}) })
const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent
rotateDialog.title = $localize`Rotate confirm` rotateDialog.title = $localize`Rotate confirm`
rotateDialog.messageBold = $localize`This operation will permanently rotate the original version of ${this.list.selected.size} document(s).` rotateDialog.messageBold = $localize`This operation will permanently rotate the original version of ${this.list.selected.size} document(s).`
rotateDialog.message = $localize`This will alter the original copy.`
rotateDialog.btnClass = 'btn-danger' rotateDialog.btnClass = 'btn-danger'
rotateDialog.btnCaption = $localize`Proceed` rotateDialog.btnCaption = $localize`Proceed`
rotateDialog.documentID = Array.from(this.list.selected)[0] rotateDialog.documentID = Array.from(this.list.selected)[0]

View File

@ -1,4 +1,4 @@
<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="card mb-3 shadow-sm bg-light" [class.card-selected]="selected" [class.document-card]="selectable" (mouseleave)="mouseLeaveCard()">
<div class="row g-0"> <div class="row g-0">
<div class="col-md-2 doc-img-container rounded-start" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()"> <div class="col-md-2 doc-img-container rounded-start" (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()">
@ -56,14 +56,9 @@
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"> <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<i-bs name="file-earmark-richtext"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>Open</span> <i-bs name="file-earmark-richtext"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>Open</span>
</a> </a>
<a class="btn btn-sm btn-outline-secondary" target="_blank" [href]="previewUrl" <pngx-preview-popup [document]="document" #popupPreview>
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<i-bs name="eye"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>View</span> <i-bs name="eye"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>View</span>
</a> </pngx-preview-popup>
<ng-template #previewContent>
<pngx-preview-popup [document]="document"></pngx-preview-popup>
</ng-template>
<a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()"> <a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
<i-bs name="download"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>Download</span> <i-bs name="download"></i-bs>&nbsp;<span class="d-none d-md-inline" i18n>Download</span>
</a> </a>

View File

@ -1,11 +1,6 @@
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { provideHttpClientTesting } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { import { ComponentFixture, TestBed } from '@angular/core/testing'
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { By } from '@angular/platform-browser' import { By } from '@angular/platform-browser'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { import {
@ -84,21 +79,6 @@ describe('DocumentCardLargeComponent', () => {
expect(fixture.nativeElement.textContent).toContain('8 pages') expect(fixture.nativeElement.textContent).toContain('8 pages')
}) })
it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
component.mouseEnterPreview()
expect(component.popover.isOpen()).toBeTruthy()
expect(component.popoverHidden).toBeTruthy()
tick(600)
expect(component.popoverHidden).toBeFalsy()
component.mouseLeaveCard()
component.mouseEnterPreview()
tick(100)
component.mouseLeavePreview()
tick(600)
expect(component.popover.isOpen()).toBeFalsy()
}))
it('should trim content', () => { it('should trim content', () => {
expect(component.contentTrimmed).toHaveLength(503) // includes ... expect(component.contentTrimmed).toHaveLength(503) // includes ...
}) })

View File

@ -12,9 +12,9 @@ import {
} from 'src/app/data/document' } from 'src/app/data/document'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
@Component({ @Component({
selector: 'pngx-document-card-large', selector: 'pngx-document-card-large',
@ -65,7 +65,7 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
@Output() @Output()
clickMoreLike = new EventEmitter() clickMoreLike = new EventEmitter()
@ViewChild('popover') popover: NgbPopover @ViewChild('popupPreview') popupPreview: PreviewPopupComponent
mouseOnPreview = false mouseOnPreview = false
popoverHidden = true popoverHidden = true
@ -112,29 +112,8 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
return this.documentService.getPreviewUrl(this.document.id) return this.documentService.getPreviewUrl(this.document.id)
} }
mouseEnterPreview() {
this.mouseOnPreview = true
if (!this.popover.isOpen()) {
// we're going to open but hide to pre-load content during hover delay
this.popover.open()
this.popoverHidden = true
setTimeout(() => {
if (this.mouseOnPreview) {
// show popover
this.popoverHidden = false
} else {
this.popover.close()
}
}, 600)
}
}
mouseLeavePreview() {
this.mouseOnPreview = false
}
mouseLeaveCard() { mouseLeaveCard() {
this.popover.close() this.popupPreview.close()
} }
get contentTrimmed() { get contentTrimmed() {

View File

@ -1,5 +1,5 @@
<div class="col p-2 h-100"> <div class="col p-2 h-100">
<div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" [class.popover-hidden]="popoverHidden" (mouseleave)="mouseLeaveCard()"> <div class="card h-100 shadow-sm document-card" [class.card-selected]="selected" (mouseleave)="mouseLeaveCard()">
<div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)"> <div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
<img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()"> <img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
@ -129,14 +129,9 @@
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title> <a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
<i-bs name="file-earmark-richtext"></i-bs> <i-bs name="file-earmark-richtext"></i-bs>
</a> </a>
<a [href]="previewUrl" target="_blank" class="btn btn-sm btn-outline-secondary" <pngx-preview-popup [document]="document" #popupPreview>
[ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle"
autoClose="true" popoverClass="shadow popover-preview" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover">
<i-bs name="eye"></i-bs> <i-bs name="eye"></i-bs>
</a> </pngx-preview-popup>
<ng-template #previewContent>
<pngx-preview-popup [document]="document"></pngx-preview-popup>
</ng-template>
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()"> <a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()">
<i-bs name="download"></i-bs> <i-bs name="download"></i-bs>
</a> </a>

View File

@ -1,11 +1,6 @@
import { DatePipe } from '@angular/common' import { DatePipe } from '@angular/common'
import { provideHttpClientTesting } from '@angular/common/http/testing' import { provideHttpClientTesting } from '@angular/common/http/testing'
import { import { ComponentFixture, TestBed } from '@angular/core/testing'
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { RouterTestingModule } from '@angular/router/testing' import { RouterTestingModule } from '@angular/router/testing'
import { import {
NgbPopoverModule, NgbPopoverModule,
@ -116,19 +111,4 @@ describe('DocumentCardSmallComponent', () => {
fixture.debugElement.queryAll(By.directive(TagComponent)) fixture.debugElement.queryAll(By.directive(TagComponent))
).toHaveLength(6) ).toHaveLength(6)
}) })
it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
component.mouseEnterPreview()
expect(component.popover.isOpen()).toBeTruthy()
expect(component.popoverHidden).toBeTruthy()
tick(600)
expect(component.popoverHidden).toBeFalsy()
component.mouseLeaveCard()
component.mouseEnterPreview()
tick(100)
component.mouseLeavePreview()
tick(600)
expect(component.popover.isOpen()).toBeFalsy()
}))
}) })

View File

@ -13,9 +13,9 @@ import {
} from 'src/app/data/document' } from 'src/app/data/document'
import { DocumentService } from 'src/app/services/rest/document.service' import { DocumentService } from 'src/app/services/rest/document.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
import { NgbPopover } from '@ng-bootstrap/ng-bootstrap'
import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component'
@Component({ @Component({
selector: 'pngx-document-card-small', selector: 'pngx-document-card-small',
@ -61,10 +61,7 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
moreTags: number = null moreTags: number = null
@ViewChild('popover') popover: NgbPopover @ViewChild('popupPreview') popupPreview: PreviewPopupComponent
mouseOnPreview = false
popoverHidden = true
getIsThumbInverted() { getIsThumbInverted() {
return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED) return this.settingsService.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)
@ -78,10 +75,6 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
return this.documentService.getDownloadUrl(this.document.id) return this.documentService.getDownloadUrl(this.document.id)
} }
get previewUrl() {
return this.documentService.getPreviewUrl(this.document.id)
}
get privateName() { get privateName() {
return $localize`Private` return $localize`Private`
} }
@ -100,29 +93,8 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions {
) )
} }
mouseEnterPreview() {
this.mouseOnPreview = true
if (!this.popover.isOpen()) {
// we're going to open but hide to pre-load content during hover delay
this.popover.open()
this.popoverHidden = true
setTimeout(() => {
if (this.mouseOnPreview) {
// show popover
this.popoverHidden = false
} else {
this.popover.close()
}
}, 600)
}
}
mouseLeavePreview() {
this.mouseOnPreview = false
}
mouseLeaveCard() { mouseLeaveCard() {
this.popover.close() this.popupPreview.close()
} }
get notesEnabled(): boolean { get notesEnabled(): boolean {

View File

@ -292,7 +292,12 @@
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) { @if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
<td width="30%"> <td width="30%">
@if (activeDisplayFields.includes(DisplayField.TITLE)) { @if (activeDisplayFields.includes(DisplayField.TITLE)) {
<div class="d-inline-block" (mouseleave)="popupPreview.close()">
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<pngx-preview-popup [document]="d" linkClasses="btn btn-sm btn-link text-secondary" linkTitle="Preview document" (click)="$event.stopPropagation()" i18n-linkTitle #popupPreview>
<i-bs name="eye"></i-bs>
</pngx-preview-popup>
</div>
} }
@if (activeDisplayFields.includes(DisplayField.TAGS)) { @if (activeDisplayFields.includes(DisplayField.TAGS)) {
@for (t of d.tags$ | async; track t) { @for (t of d.tags$ | async; track t) {

View File

@ -72,6 +72,7 @@ import { IsNumberPipe } from 'src/app/pipes/is-number.pipe'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { PermissionsService } from 'src/app/services/permissions.service' import { PermissionsService } from 'src/app/services/permissions.service'
import { NgSelectModule } from '@ng-select/ng-select' import { NgSelectModule } from '@ng-select/ng-select'
import { PreviewPopupComponent } from '../common/preview-popup/preview-popup.component'
const docs: Document[] = [ const docs: Document[] = [
{ {
@ -137,6 +138,7 @@ describe('DocumentListComponent', () => {
UsernamePipe, UsernamePipe,
SafeHtmlPipe, SafeHtmlPipe,
IsNumberPipe, IsNumberPipe,
PreviewPopupComponent,
], ],
imports: [ imports: [
RouterTestingModule.withRoutes(routes), RouterTestingModule.withRoutes(routes),

View File

@ -17,6 +17,8 @@ export enum GlobalSearchType {
TITLE_CONTENT = 'title-content', TITLE_CONTENT = 'title-content',
} }
export const PAPERLESS_GREEN_HEX = '#17541f'
export const SETTINGS_KEYS = { export const SETTINGS_KEYS = {
LANGUAGE: 'language', LANGUAGE: 'language',
APP_LOGO: 'app_logo', APP_LOGO: 'app_logo',

View File

@ -17,7 +17,12 @@ import {
hexToHsl, hexToHsl,
} from 'src/app/utils/color' } from 'src/app/utils/color'
import { environment } from 'src/environments/environment' import { environment } from 'src/environments/environment'
import { UiSettings, SETTINGS, SETTINGS_KEYS } from '../data/ui-settings' import {
UiSettings,
SETTINGS,
SETTINGS_KEYS,
PAPERLESS_GREEN_HEX,
} from '../data/ui-settings'
import { User } from '../data/user' import { User } from '../data/user'
import { import {
PermissionAction, PermissionAction,
@ -420,7 +425,7 @@ export class SettingsService {
) )
} }
if (themeColor) { if (themeColor?.length) {
const hsl = hexToHsl(themeColor) const hsl = hexToHsl(themeColor)
const bgBrightnessEstimate = estimateBrightnessForColor(themeColor) const bgBrightnessEstimate = estimateBrightnessForColor(themeColor)
@ -445,6 +450,11 @@ export class SettingsService {
document.documentElement.style.removeProperty('--pngx-primary') document.documentElement.style.removeProperty('--pngx-primary')
document.documentElement.style.removeProperty('--pngx-primary-lightness') document.documentElement.style.removeProperty('--pngx-primary-lightness')
} }
this.meta.updateTag({
name: 'theme-color',
content: themeColor?.length ? themeColor : PAPERLESS_GREEN_HEX,
})
} }
getLanguageOptions(): LanguageOption[] { getLanguageOptions(): LanguageOption[] {

View File

@ -564,11 +564,6 @@ table.table {
} }
} }
.popover-hidden .popover {
opacity: 0;
pointer-events: none;
}
// Tour // Tour
.tour-active .popover { .tour-active .popover {
min-width: 360px; min-width: 360px;
@ -728,3 +723,27 @@ i-bs svg {
vertical-align: middle; vertical-align: middle;
} }
} }
// fixes for buttons in preview popup
.btn-group pngx-preview-popup:not(:last-child) {
// Prevent double borders when buttons are next to each other
> .btn {
margin-left: calc(#{$btn-border-width} * -1);
}
> .btn {
@include border-end-radius(0);
}
}
.btn-group pngx-preview-popup:not(:first-child) {
> .btn {
@include border-start-radius(0);
}
}
.btn-group pngx-preview-popup {
position: relative;
flex: 1 1 auto;
> .btn {
display: block;
}
}

View File

@ -14,7 +14,7 @@ def settings(request):
app_logo = ( app_logo = (
django_settings.APP_LOGO django_settings.APP_LOGO
if general_config.app_logo is None or len(general_config.app_logo) == 0 if general_config.app_logo is None or len(general_config.app_logo) == 0
else general_config.app_logo else django_settings.BASE_URL + general_config.app_logo.lstrip("/")
) )
return { return {

View File

@ -1,7 +1,9 @@
import json import json
from unittest import mock from unittest import mock
from auditlog.models import LogEntry
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import override_settings
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
@ -51,8 +53,12 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.doc3.tags.add(self.t2) self.doc3.tags.add(self.t2)
self.doc4.tags.add(self.t1, self.t2) self.doc4.tags.add(self.t1, self.t2)
self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}") self.sp1 = StoragePath.objects.create(name="sp1", path="Something/{checksum}")
self.cf1 = CustomField.objects.create(name="cf1", data_type="text") self.cf1 = CustomField.objects.create(name="cf1", data_type="string")
self.cf2 = CustomField.objects.create(name="cf2", data_type="text") self.cf2 = CustomField.objects.create(name="cf2", data_type="string")
def setup_mock(self, m, method_name, return_value="OK"):
m.return_value = return_value
m.__name__ = method_name
@mock.patch("documents.bulk_edit.bulk_update_documents.delay") @mock.patch("documents.bulk_edit.bulk_update_documents.delay")
def test_api_set_correspondent(self, bulk_update_task_mock): def test_api_set_correspondent(self, bulk_update_task_mock):
@ -178,7 +184,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.modify_tags") @mock.patch("documents.serialisers.bulk_edit.modify_tags")
def test_api_modify_tags(self, m): def test_api_modify_tags(self, m):
m.return_value = "OK" self.setup_mock(m, "modify_tags")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -211,7 +217,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
- API returns HTTP 400 - API returns HTTP 400
- modify_tags is not called - modify_tags is not called
""" """
m.return_value = "OK" self.setup_mock(m, "modify_tags")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -230,7 +236,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.modify_custom_fields") @mock.patch("documents.serialisers.bulk_edit.modify_custom_fields")
def test_api_modify_custom_fields(self, m): def test_api_modify_custom_fields(self, m):
m.return_value = "OK" self.setup_mock(m, "modify_custom_fields")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -263,8 +269,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
- API returns HTTP 400 - API returns HTTP 400
- modify_custom_fields is not called - modify_custom_fields is not called
""" """
m.return_value = "OK" self.setup_mock(m, "modify_custom_fields")
# Missing add_custom_fields # Missing add_custom_fields
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
@ -359,7 +364,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.delete") @mock.patch("documents.serialisers.bulk_edit.delete")
def test_api_delete(self, m): def test_api_delete(self, m):
m.return_value = "OK" self.setup_mock(m, "delete")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -383,8 +388,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
THEN: THEN:
- set_storage_path is called with correct document IDs and storage_path ID - set_storage_path is called with correct document IDs and storage_path ID
""" """
m.return_value = "OK" self.setup_mock(m, "set_storage_path")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -414,8 +418,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
THEN: THEN:
- set_storage_path is called with correct document IDs and None storage_path - set_storage_path is called with correct document IDs and None storage_path
""" """
m.return_value = "OK" self.setup_mock(m, "set_storage_path")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -728,7 +731,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.set_permissions") @mock.patch("documents.serialisers.bulk_edit.set_permissions")
def test_set_permissions(self, m): def test_set_permissions(self, m):
m.return_value = "OK" self.setup_mock(m, "set_permissions")
user1 = User.objects.create(username="user1") user1 = User.objects.create(username="user1")
user2 = User.objects.create(username="user2") user2 = User.objects.create(username="user2")
permissions = { permissions = {
@ -763,7 +766,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.set_permissions") @mock.patch("documents.serialisers.bulk_edit.set_permissions")
def test_set_permissions_merge(self, m): def test_set_permissions_merge(self, m):
m.return_value = "OK" self.setup_mock(m, "set_permissions")
user1 = User.objects.create(username="user1") user1 = User.objects.create(username="user1")
user2 = User.objects.create(username="user2") user2 = User.objects.create(username="user2")
permissions = { permissions = {
@ -823,7 +826,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
THEN: THEN:
- User is not able to change permissions - User is not able to change permissions
""" """
m.return_value = "OK" self.setup_mock(m, "set_permissions")
self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.owner = User.objects.get(username="temp_admin")
self.doc1.save() self.doc1.save()
user1 = User.objects.create(username="user1") user1 = User.objects.create(username="user1")
@ -875,7 +878,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
THEN: THEN:
- set_storage_path only called if user can edit all docs - set_storage_path only called if user can edit all docs
""" """
m.return_value = "OK" self.setup_mock(m, "set_storage_path")
self.doc1.owner = User.objects.get(username="temp_admin") self.doc1.owner = User.objects.get(username="temp_admin")
self.doc1.save() self.doc1.save()
user1 = User.objects.create(username="user1") user1 = User.objects.create(username="user1")
@ -919,8 +922,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.rotate") @mock.patch("documents.serialisers.bulk_edit.rotate")
def test_rotate(self, m): def test_rotate(self, m):
m.return_value = "OK" self.setup_mock(m, "rotate")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -974,8 +976,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.merge") @mock.patch("documents.serialisers.bulk_edit.merge")
def test_merge(self, m): def test_merge(self, m):
m.return_value = "OK" self.setup_mock(m, "merge")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -1003,8 +1004,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
user1 = User.objects.create(username="user1") user1 = User.objects.create(username="user1")
self.client.force_authenticate(user=user1) self.client.force_authenticate(user=user1)
m.return_value = "OK" self.setup_mock(m, "merge")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -1053,8 +1053,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
THEN: THEN:
- The API fails with a correct error code - The API fails with a correct error code
""" """
m.return_value = "OK" self.setup_mock(m, "merge")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -1074,8 +1073,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.split") @mock.patch("documents.serialisers.bulk_edit.split")
def test_split(self, m): def test_split(self, m):
m.return_value = "OK" self.setup_mock(m, "split")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -1165,8 +1163,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
@mock.patch("documents.serialisers.bulk_edit.delete_pages") @mock.patch("documents.serialisers.bulk_edit.delete_pages")
def test_delete_pages(self, m): def test_delete_pages(self, m):
m.return_value = "OK" self.setup_mock(m, "delete_pages")
response = self.client.post( response = self.client.post(
"/api/documents/bulk_edit/", "/api/documents/bulk_edit/",
json.dumps( json.dumps(
@ -1254,3 +1251,87 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"pages must be a list of integers", response.content) self.assertIn(b"pages must be a list of integers", response.content)
@override_settings(AUDIT_LOG_ENABLED=True)
def test_bulk_edit_audit_log_enabled_simple_field(self):
"""
GIVEN:
- Audit log is enabled
WHEN:
- API to bulk edit documents is called
THEN:
- Audit log is created
"""
LogEntry.objects.all().delete()
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc1.id],
"method": "set_correspondent",
"parameters": {"correspondent": self.c2.id},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(LogEntry.objects.filter(object_pk=self.doc1.id).count(), 1)
@override_settings(AUDIT_LOG_ENABLED=True)
def test_bulk_edit_audit_log_enabled_tags(self):
"""
GIVEN:
- Audit log is enabled
WHEN:
- API to bulk edit tags is called
THEN:
- Audit log is created
"""
LogEntry.objects.all().delete()
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc1.id],
"method": "modify_tags",
"parameters": {
"add_tags": [self.t1.id],
"remove_tags": [self.t2.id],
},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(LogEntry.objects.filter(object_pk=self.doc1.id).count(), 1)
@override_settings(AUDIT_LOG_ENABLED=True)
def test_bulk_edit_audit_log_enabled_custom_fields(self):
"""
GIVEN:
- Audit log is enabled
WHEN:
- API to bulk edit custom fields is called
THEN:
- Audit log is created
"""
LogEntry.objects.all().delete()
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc1.id],
"method": "modify_custom_fields",
"parameters": {
"add_custom_fields": [self.cf1.id],
"remove_custom_fields": [],
},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(LogEntry.objects.filter(object_pk=self.doc1.id).count(), 2)

View File

@ -6,12 +6,14 @@ from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from django.test import override_settings
from django.utils import timezone from django.utils import timezone
from rest_framework import status from rest_framework import status
from documents.models import Document from documents.models import Document
from documents.models import ShareLink from documents.models import ShareLink
from documents.tests.utils import DirectoriesMixin from documents.tests.utils import DirectoriesMixin
from paperless.models import ApplicationConfiguration
class TestViews(DirectoriesMixin, TestCase): class TestViews(DirectoriesMixin, TestCase):
@ -67,6 +69,26 @@ class TestViews(DirectoriesMixin, TestCase):
f"frontend/{language_actual}/main.js", f"frontend/{language_actual}/main.js",
) )
@override_settings(BASE_URL="/paperless/")
def test_index_app_logo_with_base_url(self):
"""
GIVEN:
- Existing config with app_logo specified
WHEN:
- Index page is loaded
THEN:
- app_logo is prefixed with BASE_URL
"""
config = ApplicationConfiguration.objects.first()
config.app_logo = "/logo/example.jpg"
config.save()
self.client.force_login(self.user)
response = self.client.get("/")
self.assertEqual(
response.context["APP_LOGO"],
f"/paperless{config.app_logo}",
)
def test_share_link_views(self): def test_share_link_views(self):
""" """
GIVEN: GIVEN:

View File

@ -26,11 +26,13 @@ from django.db.models import Case
from django.db.models import Count from django.db.models import Count
from django.db.models import IntegerField from django.db.models import IntegerField
from django.db.models import Max from django.db.models import Max
from django.db.models import Model
from django.db.models import Q from django.db.models import Q
from django.db.models import Sum from django.db.models import Sum
from django.db.models import When from django.db.models import When
from django.db.models.functions import Length from django.db.models.functions import Length
from django.db.models.functions import Lower from django.db.models.functions import Lower
from django.db.models.manager import Manager
from django.http import Http404 from django.http import Http404
from django.http import HttpResponse from django.http import HttpResponse
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
@ -426,7 +428,7 @@ class DocumentViewSet(
) )
def file_response(self, pk, request, disposition): def file_response(self, pk, request, disposition):
doc = Document.objects.select_related("owner").get(id=pk) doc = Document.global_objects.select_related("owner").get(id=pk)
if request.user is not None and not has_perms_owner_aware( if request.user is not None and not has_perms_owner_aware(
request.user, request.user,
"view_document", "view_document",
@ -961,6 +963,22 @@ class SavedViewViewSet(ModelViewSet, PassUserMixin):
class BulkEditView(PassUserMixin): class BulkEditView(PassUserMixin):
MODIFIED_FIELD_BY_METHOD = {
"set_correspondent": "correspondent",
"set_document_type": "document_type",
"set_storage_path": "storage_path",
"add_tag": "tags",
"remove_tag": "tags",
"modify_tags": "tags",
"modify_custom_fields": "custom_fields",
"set_permissions": None,
"delete": "deleted_at",
"rotate": "checksum",
"delete_pages": "checksum",
"split": None,
"merge": None,
}
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = BulkEditSerializer serializer_class = BulkEditSerializer
parser_classes = (parsers.JSONParser,) parser_classes = (parsers.JSONParser,)
@ -1013,8 +1031,53 @@ class BulkEditView(PassUserMixin):
return HttpResponseForbidden("Insufficient permissions") return HttpResponseForbidden("Insufficient permissions")
try: try:
modified_field = self.MODIFIED_FIELD_BY_METHOD[method.__name__]
if settings.AUDIT_LOG_ENABLED and modified_field:
old_documents = {
obj["pk"]: obj
for obj in Document.objects.filter(pk__in=documents).values(
"pk",
"correspondent",
"document_type",
"storage_path",
"tags",
"custom_fields",
"deleted_at",
"checksum",
)
}
# TODO: parameter validation # TODO: parameter validation
result = method(documents, **parameters) result = method(documents, **parameters)
if settings.AUDIT_LOG_ENABLED and modified_field:
new_documents = Document.objects.filter(pk__in=documents)
for doc in new_documents:
old_value = old_documents[doc.pk][modified_field]
new_value = getattr(doc, modified_field)
if isinstance(new_value, Model):
# correspondent, document type, etc.
new_value = new_value.pk
elif isinstance(new_value, Manager):
# tags, custom fields
new_value = list(new_value.values_list("pk", flat=True))
LogEntry.objects.log_create(
instance=doc,
changes={
modified_field: [
old_value,
new_value,
],
},
action=LogEntry.Action.UPDATE,
actor=user,
additional_data={
"reason": f"Bulk edit: {method.__name__}",
},
)
return Response({"result": result}) return Response({"result": result})
except Exception as e: except Exception as e:
logger.warning(f"An error occurred performing bulk edit: {e!s}") logger.warning(f"An error occurred performing bulk edit: {e!s}")