Merge branch 'dev'

This commit is contained in:
shamoon 2025-01-21 17:02:07 -08:00
commit 74afad5976
36 changed files with 535 additions and 281 deletions

View File

@ -51,7 +51,7 @@ repos:
- 'prettier-plugin-organize-imports@4.1.0'
# Python hooks
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.6
rev: v0.9.2
hooks:
- id: ruff
- id: ruff-format

277
Pipfile.lock generated
View File

@ -2913,122 +2913,109 @@
},
"charset-normalizer": {
"hashes": [
"sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621",
"sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6",
"sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8",
"sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912",
"sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c",
"sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b",
"sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d",
"sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d",
"sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95",
"sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e",
"sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565",
"sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64",
"sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab",
"sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be",
"sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e",
"sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907",
"sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0",
"sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2",
"sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62",
"sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62",
"sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23",
"sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc",
"sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284",
"sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca",
"sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455",
"sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858",
"sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b",
"sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594",
"sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc",
"sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db",
"sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b",
"sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea",
"sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6",
"sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920",
"sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749",
"sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7",
"sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd",
"sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99",
"sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242",
"sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee",
"sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129",
"sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2",
"sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51",
"sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee",
"sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8",
"sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b",
"sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613",
"sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742",
"sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe",
"sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3",
"sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5",
"sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631",
"sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7",
"sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15",
"sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c",
"sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea",
"sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417",
"sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250",
"sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88",
"sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca",
"sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa",
"sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99",
"sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149",
"sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41",
"sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574",
"sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0",
"sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f",
"sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d",
"sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654",
"sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3",
"sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19",
"sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90",
"sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578",
"sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9",
"sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1",
"sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51",
"sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719",
"sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236",
"sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a",
"sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c",
"sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade",
"sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944",
"sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc",
"sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6",
"sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6",
"sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27",
"sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6",
"sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2",
"sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12",
"sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf",
"sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114",
"sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7",
"sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf",
"sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d",
"sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b",
"sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed",
"sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03",
"sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4",
"sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67",
"sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365",
"sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a",
"sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748",
"sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b",
"sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079",
"sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"
"sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537",
"sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa",
"sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a",
"sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294",
"sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b",
"sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd",
"sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601",
"sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd",
"sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4",
"sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d",
"sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2",
"sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313",
"sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd",
"sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa",
"sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8",
"sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1",
"sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2",
"sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496",
"sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d",
"sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b",
"sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e",
"sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a",
"sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4",
"sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca",
"sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78",
"sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408",
"sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5",
"sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3",
"sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f",
"sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a",
"sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765",
"sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6",
"sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146",
"sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6",
"sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9",
"sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd",
"sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c",
"sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f",
"sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545",
"sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176",
"sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770",
"sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824",
"sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f",
"sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf",
"sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487",
"sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d",
"sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd",
"sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b",
"sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534",
"sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f",
"sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b",
"sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9",
"sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd",
"sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125",
"sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9",
"sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de",
"sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11",
"sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d",
"sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35",
"sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f",
"sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda",
"sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7",
"sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a",
"sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971",
"sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8",
"sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41",
"sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d",
"sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f",
"sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757",
"sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a",
"sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886",
"sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77",
"sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76",
"sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247",
"sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85",
"sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb",
"sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7",
"sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e",
"sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6",
"sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037",
"sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1",
"sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e",
"sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807",
"sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407",
"sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c",
"sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12",
"sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3",
"sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089",
"sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd",
"sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e",
"sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00",
"sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==3.4.0"
"markers": "python_version >= '3.7'",
"version": "==3.4.1"
},
"click": {
"hashes": [
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
"sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
"sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2",
"sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
"version": "==8.1.8"
},
"colorama": {
"hashes": [
@ -3291,11 +3278,11 @@
},
"jinja2": {
"hashes": [
"sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369",
"sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"
"sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb",
"sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.4"
"version": "==3.1.5"
},
"markdown": {
"hashes": [
@ -3406,12 +3393,12 @@
},
"mkdocs-material": {
"hashes": [
"sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d",
"sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e"
"sha256:ae5fe16f3d7c9ccd05bb6916a7da7420cf99a9ce5e33debd9d40403a090d5825",
"sha256:f24100f234741f4d423a9d672a909d859668a4f404796be3cf035f10d6050385"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==9.5.49"
"version": "==9.5.50"
},
"mkdocs-material-extensions": {
"hashes": [
@ -3645,19 +3632,19 @@
},
"pygments": {
"hashes": [
"sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199",
"sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"
"sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f",
"sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"
],
"markers": "python_version >= '3.8'",
"version": "==2.18.0"
"version": "==2.19.1"
},
"pymdown-extensions": {
"hashes": [
"sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77",
"sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7"
"sha256:202481f716cc8250e4be8fce997781ebf7917701b59652458ee47f2401f818b5",
"sha256:741bd7c4ff961ba40b7528d32284c53bc436b8b1645e8e37c3e57770b8700a34"
],
"markers": "python_version >= '3.8'",
"version": "==10.12"
"version": "==10.14"
},
"pyopenssl": {
"hashes": [
@ -3974,28 +3961,28 @@
},
"ruff": {
"hashes": [
"sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25",
"sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe",
"sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75",
"sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a",
"sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76",
"sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188",
"sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1",
"sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf",
"sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117",
"sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162",
"sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d",
"sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d",
"sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315",
"sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5",
"sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3",
"sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764",
"sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807",
"sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905"
"sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df",
"sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d",
"sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb",
"sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145",
"sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347",
"sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d",
"sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c",
"sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684",
"sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f",
"sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6",
"sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a",
"sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe",
"sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0",
"sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00",
"sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247",
"sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5",
"sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e",
"sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==0.8.6"
"version": "==0.9.2"
},
"scipy": {
"hashes": [
@ -4113,11 +4100,11 @@
},
"urllib3": {
"hashes": [
"sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac",
"sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"
"sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df",
"sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"
],
"markers": "python_version >= '3.8'",
"version": "==2.2.3"
"markers": "python_version >= '3.9'",
"version": "==2.3.0"
},
"virtualenv": {
"hashes": [

View File

@ -308,7 +308,7 @@ Paperless provides the following variables for use within filenames:
- `{{ tag_list }}`: A comma separated list of all tags assigned to the
document.
- `{{ title }}`: The title of the document.
- `{{ created }}`: The full date (ISO format) the document was created.
- `{{ created }}`: The full date (ISO 8601 format, e.g. `2024-03-14`) the document was created.
- `{{ created_year }}`: Year created only, formatted as the year with
century.
- `{{ created_year_short }}`: Year created only, formatted as the year
@ -476,7 +476,7 @@ a document with an ASN of 355 would be placed in `somepath/asn-201-400/asn-3xx/T
/{{ title }}
```
For a PDF document, it would result in `pdfs/Title.pdf`, but for a PNG document, the path would be `pngs/Title.pdf`.
For a PDF document, it would result in `pdfs/Title.pdf`, but for a PNG document, the path would be `pngs/Title.png`.
To use custom fields:

View File

@ -299,7 +299,7 @@ permissions can be granted to limit access to certain parts of the UI (and corre
#### Superusers
Superusers can access all parts of the front and backend application as well as any and all objects.
Superusers can access all parts of the front and backend application as well as any and all objects. Superuser status can only be granted by another superuser.
#### Admin Status

View File

@ -3,7 +3,7 @@ import os
# See https://docs.gunicorn.org/en/stable/settings.html for
# explanations of settings
bind = f'{os.getenv("PAPERLESS_BIND_ADDR", "[::]")}:{os.getenv("PAPERLESS_PORT", 8000)}'
bind = f"{os.getenv('PAPERLESS_BIND_ADDR', '[::]')}:{os.getenv('PAPERLESS_PORT', 8000)}"
workers = int(os.getenv("PAPERLESS_WEBSERVER_WORKERS", 1))
worker_class = "paperless.workers.ConfigurableWorker"

View File

@ -18107,10 +18107,11 @@
}
},
"node_modules/undici": {
"version": "5.28.4",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz",
"integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==",
"version": "5.28.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},

View File

@ -160,4 +160,23 @@ describe('UserEditDialogComponent', () => {
})
expect(component.currentUserIsSuperUser).toBeTruthy()
})
it('should disable superuser option if current user is not superuser', () => {
const control: AbstractControl = component.objectForm.get('is_superuser')
permissionsService.initialize([], {
id: 99,
username: 'user99',
is_superuser: false,
})
component.ngOnInit()
expect(control.disabled).toBeTruthy()
permissionsService.initialize([], {
id: 99,
username: 'user99',
is_superuser: true,
})
component.ngOnInit()
expect(control.disabled).toBeFalsy()
})
})

View File

@ -60,6 +60,11 @@ export class UserEditDialogComponent
ngOnInit(): void {
super.ngOnInit()
this.onToggleSuperUser()
if (!this.currentUserIsSuperUser) {
this.objectForm.get('is_superuser').disable()
} else {
this.objectForm.get('is_superuser').enable()
}
}
getCreateTitle() {

View File

@ -17,7 +17,7 @@
(change)="onChange(value)">
<ng-template ng-label-tmp let-item="item">
<button class="tag-wrap btn p-0" (click)="removeTag($event, item.id)" title="Remove tag" i18n-title>
<button class="tag-wrap btn p-0 d-flex align-items-center" (click)="removeTag($event, item.id)" title="Remove tag" i18n-title>
<i-bs name="x" style="margin-inline-end: 1px;"></i-bs>
@if (item.id && tags) {
<pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag>

View File

@ -48,7 +48,6 @@
i18n-title
buttonClasses=" btn-outline-secondary"
iconName="arrow-repeat"
[disabled]="!hasUsablePassword"
(confirm)="generateAuthToken()">
</pngx-confirm-button>
</div>

View File

@ -1,8 +1,8 @@
a {
a, span {
cursor: pointer;
white-space: normal;
word-break: break-word;
text-align: end;
text-align: start;
}
.private {

View File

@ -45,7 +45,7 @@
<li class="list-group-item">
<div class="row fade" [class.show]="showAccounts">
<div class="col d-flex align-items-center">
<button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount)">
<button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount) || !userCanEdit(account)">
{{account.name}}@switch (account.account_type) {
@case (MailAccountType.IMAP) {<i-bs name="envelope-at-fill" class="ms-2"></i-bs>}
@case (MailAccountType.Gmail_OAuth) {<i-bs name="google" class="ms-2"></i-bs>}
@ -62,10 +62,10 @@
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="editMailAccount(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Edit</button>
<button (click)="editMailAccount(account)" [disabled]="!userCanEdit(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Edit</button>
<button (click)="editPermissions(account)" *pngxIfOwner="account" ngbDropdownItem i18n>Permissions</button>
<button (click)="deleteMailAccount(account)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Delete</button>
<button (click)="processAccount(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Process Mail</button>
<button (click)="deleteMailAccount(account)" [disabled]="!userIsOwner(account)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Delete</button>
<button (click)="processAccount(account)" [disabled]="!userIsOwner(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Process Mail</button>
</div>
</div>
</div>
@ -82,7 +82,7 @@
</button>
</div>
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" class="btn btn-sm btn-outline-secondary" type="button" (click)="processAccount(account)">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" [disabled]="!userIsOwner(account)" class="btn btn-sm btn-outline-secondary" type="button" (click)="processAccount(account)">
<i-bs width="1em" height="1em" name="arrow-clockwise"></i-bs>&nbsp;<ng-container i18n>Process Mail</ng-container>
</button>
</div>
@ -126,7 +126,7 @@
@for (rule of mailRules; track rule) {
<li class="list-group-item">
<div class="row fade" [class.show]="showRules">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule)">{{rule.name}}</button></div>
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule) || !userCanEdit(rule)">{{rule.name}}</button></div>
<div class="col d-flex align-items-center d-none d-sm-flex">{{rule.order}}</div>
<div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div>
<div class="col d-flex align-items-center d-none d-sm-flex">
@ -144,9 +144,9 @@
<i-bs name="three-dots-vertical"></i-bs>
</button>
<div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
<button (click)="editMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" ngbDropdownItem i18n>Edit</button>
<button (click)="editMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }" [disabled]="!userCanEdit(rule)" ngbDropdownItem i18n>Edit</button>
<button (click)="editPermissions(rule)" *pngxIfOwner="rule" ngbDropdownItem i18n>Permissions</button>
<button (click)="deleteMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" ngbDropdownItem i18n>Delete</button>
<button (click)="deleteMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.MailRule }" [disabled]="!userIsOwner(rule)" ngbDropdownItem i18n>Delete</button>
<button (click)="copyMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }" ngbDropdownItem i18n>Copy</button>
</div>
</div>

View File

@ -2,7 +2,9 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { RouterModule } from '@angular/router'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { routes } from 'src/app/app-routing.module'
import { LogoComponent } from '../common/logo/logo.component'
import { NotFoundComponent } from './not-found.component'
@ -16,6 +18,7 @@ describe('NotFoundComponent', () => {
NgxBootstrapIconsModule.pick(allIcons),
NotFoundComponent,
LogoComponent,
RouterModule.forRoot(routes),
],
providers: [
provideHttpClient(withInterceptorsFromDi()),

View File

@ -1,4 +1,5 @@
import { Component } from '@angular/core'
import { RouterModule } from '@angular/router'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { LogoComponent } from '../common/logo/logo.component'
@ -6,7 +7,7 @@ import { LogoComponent } from '../common/logo/logo.component'
selector: 'pngx-not-found',
templateUrl: './not-found.component.html',
styleUrls: ['./not-found.component.scss'],
imports: [LogoComponent, NgxBootstrapIconsModule],
imports: [LogoComponent, NgxBootstrapIconsModule, RouterModule],
})
export class NotFoundComponent {
constructor() {}

View File

@ -8381,7 +8381,7 @@
<context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context>
<context context-type="linenumber">285</context>
</context-group>
<target state="translated">Ripristina filtri / selezione</target>
<target state="translated">Azzera filtri / selezione</target>
</trans-unit>
<trans-unit id="4135055128446167640" datatype="html">
<source>Open first [selected] document</source>
@ -9349,7 +9349,7 @@
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<target state="needs-translation">Customize the views of your documents.</target>
<target state="translated">Personalizza la vista dei tuoi documenti.</target>
</trans-unit>
<trans-unit id="6338800642797811873" datatype="html">
<source>Documents page size</source>
@ -9421,7 +9421,7 @@
<context context-type="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context>
<context context-type="linenumber">163</context>
</context-group>
<target state="needs-translation">Error while saving views.</target>
<target state="translated">Errore durante il salvataggio delle viste.</target>
</trans-unit>
<trans-unit id="5101757640976222639" datatype="html">
<source>storage path</source>

View File

@ -452,7 +452,7 @@
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">183</context>
</context-group>
<target state="translated">Dra-og-slipp dokumenter hit for å laste dem opp, eller plasser dem i opplastningsmappen. Du kan også dra-og-slippe dokumenter hvor som helst på alle andre sider av nettsiden. Da vil Paperless-ngx starte å trene opp maskinlæringsalgoritmene sine.</target>
<target state="translated">Dra og slipp dokumenter hit for å laste dem opp, eller plasser dem i opplastningsmappen. Du kan også dra og slippe dokumenter hvor som helst på alle andre sider av nettsiden. Da vil Paperless-ngx starte å trene opp maskinlæringsalgoritmene sine.</target>
</trans-unit>
<trans-unit id="7495498057594070122" datatype="html">
<source>The documents list shows all of your documents and allows for filtering as well as bulk-editing. There are three different view styles: list, small cards and large cards. A list of documents currently opened for editing is shown in the sidebar.</source>
@ -524,7 +524,7 @@
<context context-type="sourcefile">src/app/app.component.ts</context>
<context context-type="linenumber">238</context>
</context-group>
<target state="needs-translation">Check out the settings for various tweaks to the web app.</target>
<target state="translated">Sjekk ut innstillingene for forskjellige endringer du kan gjøre på applikasjonen.</target>
</trans-unit>
<trans-unit id="7172877665285340082" datatype="html">
<source>Thank you! 🙏</source>
@ -572,7 +572,7 @@
<context context-type="sourcefile">src/app/components/admin/config/config.component.html</context>
<context context-type="linenumber">25</context>
</context-group>
<target state="needs-translation">Read the documentation about this setting</target>
<target state="translated">Les dokumentasjonen om denne innstillingen</target>
</trans-unit>
<trans-unit id="2180291763949669799" datatype="html">
<source>Enable</source>
@ -1108,7 +1108,7 @@
<context context-type="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<target state="needs-translation">What's this?</target>
<target state="translated">Hva er dette?</target>
</trans-unit>
<trans-unit id="6226301160429720843" datatype="html">
<source> Update checking works by pinging the public GitHub API for the latest release to determine whether a new version is available. Actual updating of the app must still be performed manually. </source>
@ -1724,7 +1724,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
<target state="needs-translation">Filter by</target>
<target state="translated">Filtrer etter</target>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html">
<source>Name</source>
@ -1968,7 +1968,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
<context context-type="linenumber">157</context>
</context-group>
<target state="translated">Avvis</target>
<target state="translated">Fjern varsel</target>
</trans-unit>
<trans-unit id="2134950584701094962" datatype="html">
<source>Open Document</source>
@ -2016,7 +2016,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">164,166</context>
</context-group>
<target state="needs-translation">Started<x id="START_BLOCK_IF" equiv-text="@if (tasksService.startedFileTasks.length &gt; 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge bg-secondary ms-2&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></target>
<target state="translated">Startet<x id="START_BLOCK_IF" equiv-text="@if (tasksService.startedFileTasks.length &gt; 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge bg-secondary ms-2&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></target>
</trans-unit>
<trans-unit id="2341807459308874922" datatype="html">
<source>Queued<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length &gt; 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge bg-secondary ms-2&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source>
@ -2024,7 +2024,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">172,174</context>
</context-group>
<target state="needs-translation">Queued<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length &gt; 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge bg-secondary ms-2&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></target>
<target state="translated">I kø:<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length &gt; 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="&lt;span class=&quot;badge bg-secondary ms-2&quot;&gt;"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="&lt;/span&gt;"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></target>
</trans-unit>
<trans-unit id="2525230676386818985" datatype="html">
<source>Result</source>
@ -2032,7 +2032,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
<context context-type="linenumber">45</context>
</context-group>
<target state="needs-translation">Result</target>
<target state="translated">Resultat</target>
</trans-unit>
<trans-unit id="5404910960991552159" datatype="html">
<source>Dismiss selected</source>
@ -2040,7 +2040,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
<context context-type="linenumber">104</context>
</context-group>
<target state="translated">Avvis valgte</target>
<target state="translated">Fjern valgte varsel</target>
</trans-unit>
<trans-unit id="8829078752502782653" datatype="html">
<source>Dismiss all</source>
@ -2048,7 +2048,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
<context context-type="linenumber">105</context>
</context-group>
<target state="translated">Avvis alle</target>
<target state="translated">Kvitter ut alle beskjeder</target>
</trans-unit>
<trans-unit id="1323591410517879795" datatype="html">
<source>Confirm Dismiss All</source>
@ -2056,7 +2056,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
<context context-type="linenumber">154</context>
</context-group>
<target state="translated">Bekreft avvisning av alle</target>
<target state="translated">Bekreft kvittering av alle</target>
</trans-unit>
<trans-unit id="4157200209636243740" datatype="html">
<source>Dismiss all <x id="PH" equiv-text="tasks.size"/> tasks?</source>
@ -2064,7 +2064,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
<context context-type="linenumber">155</context>
</context-group>
<target state="needs-translation">Dismiss all <x id="PH" equiv-text="tasks.size"/> tasks?</target>
<target state="translated">Fjern alle <x id="PH" equiv-text="tasks.size"/> oppgaver?</target>
</trans-unit>
<trans-unit id="9011556615675272238" datatype="html">
<source>queued</source>
@ -2120,7 +2120,7 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<target state="needs-translation">Manage trashed documents that are pending deletion.</target>
<target state="translated">Administrer forkastede dokumenter som venter på å bli slettet.</target>
</trans-unit>
<trans-unit id="3186604097120837257" datatype="html">
<source>Restore selected</source>
@ -2328,7 +2328,7 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
<context context-type="linenumber">94</context>
</context-group>
<target state="needs-translation">{VAR_PLURAL, plural, =1 {One document in trash} other {<x id="INTERPOLATION"/> total documents in trash}}</target>
<target state="translated">{VAR_PLURAL, plural, one {}=1 {Ett dokument i papirkurven} other {<x id="INTERPOLATION"/> totalt antall dokumenter i papirkurven}}</target>
</trans-unit>
<trans-unit id="9021887951960049161" datatype="html">
<source>Confirm delete</source>
@ -3912,7 +3912,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">37</context>
</context-group>
<target state="needs-translation">Use locale</target>
<target state="translated">Bruk nasjonale innstillinger</target>
</trans-unit>
<trans-unit id="528950215505228201" datatype="html">
<source>Create new custom field</source>
@ -6462,7 +6462,7 @@
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.ts</context>
<context context-type="linenumber">57</context>
</context-group>
<target state="needs-translation">Hello <x id="PH" equiv-text="this.settingsService.displayName"/>, welcome to <x id="PH_1" equiv-text="environment.appTitle"/></target>
<target state="translated">Hei <x id="PH" equiv-text="this.settingsService.displayName"/> og velkommen til <x id="PH_1" equiv-text="environment.appTitle"/></target>
</trans-unit>
<trans-unit id="2901300640157872718" datatype="html">
<source>Welcome to <x id="PH" equiv-text="environment.appTitle"/></source>
@ -6634,7 +6634,7 @@
<context context-type="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.html</context>
<context context-type="linenumber">4</context>
</context-group>
<target state="needs-translation">Drop documents anywhere or</target>
<target state="translated">Slipp dokumenter her, eller</target>
</trans-unit>
<trans-unit id="8133800334834354642" datatype="html">
<source>Browse files</source>
@ -6651,7 +6651,7 @@
<context context-type="linenumber">20</context>
</context-group>
<note priority="1" from="description">This button dismisses all status messages about processed documents on the dashboard (failed and successful)</note>
<target state="translated">Avvis fullført</target>
<target state="translated">Kvittert ut alle varsler</target>
</trans-unit>
<trans-unit id="2330646618997399019" datatype="html">
<source>{VAR_PLURAL, plural, =1 {One more document} other {<x id="INTERPOLATION"/> more documents}}</source>

View File

@ -1724,7 +1724,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
<context context-type="linenumber">16</context>
</context-group>
<target state="needs-translation">Filter by</target>
<target state="translated">Фильтровать по</target>
</trans-unit>
<trans-unit id="8953033926734869941" datatype="html" approved="yes">
<source>Name</source>
@ -2032,7 +2032,7 @@
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
<context context-type="linenumber">45</context>
</context-group>
<target state="needs-translation">Result</target>
<target state="translated">Результат</target>
</trans-unit>
<trans-unit id="5404910960991552159" datatype="html">
<source>Dismiss selected</source>
@ -2152,7 +2152,7 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
<context context-type="linenumber">36</context>
</context-group>
<target state="needs-translation">Remaining</target>
<target state="translated">Осталось</target>
</trans-unit>
<trans-unit id="7494361412465596264" datatype="html">
<source><x id="INTERPOLATION" equiv-text="{{ getDaysRemaining(document) }}"/> days</source>
@ -2440,7 +2440,7 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">119</context>
</context-group>
<target state="needs-translation">Document(s) deleted</target>
<target state="translated">Документ(ы) удален(ы)</target>
</trans-unit>
<trans-unit id="6962724852893361467" datatype="html">
<source>Error deleting document(s)</source>
@ -2448,7 +2448,7 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">126</context>
</context-group>
<target state="needs-translation">Error deleting document(s)</target>
<target state="translated">Ошибка при удалении документам(ов)</target>
</trans-unit>
<trans-unit id="7534569062269274401" datatype="html">
<source>Document restored</source>
@ -2472,7 +2472,7 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">159</context>
</context-group>
<target state="needs-translation">Document(s) restored</target>
<target state="translated">Документ(ы) восстановлены</target>
</trans-unit>
<trans-unit id="8405416976953346141" datatype="html">
<source>Error restoring document(s)</source>
@ -2480,7 +2480,7 @@
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context>
<context context-type="linenumber">165</context>
</context-group>
<target state="needs-translation">Error restoring document(s)</target>
<target state="translated">Ошибка при восстановлении документа(ов)</target>
</trans-unit>
<trans-unit id="8119815638230251386" datatype="html">
<source>Users &amp; Groups</source>
@ -3184,7 +3184,7 @@
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">62</context>
</context-group>
<target state="needs-translation">Filter documents</target>
<target state="translated">Отфильтровать документы</target>
</trans-unit>
<trans-unit id="3099741642167775297" datatype="html" approved="yes">
<source>Download</source>
@ -3228,7 +3228,7 @@
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">90</context>
</context-group>
<target state="needs-translation">Documents</target>
<target state="translated">Документы</target>
</trans-unit>
<trans-unit id="searchResults.saved_views" datatype="html">
<source>Saved Views</source>
@ -3244,7 +3244,7 @@
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
<context context-type="linenumber">103</context>
</context-group>
<target state="needs-translation">Tags</target>
<target state="translated">Теги</target>
</trans-unit>
<trans-unit id="searchResults.correspondents" datatype="html">
<source>Correspondents</source>
@ -3480,7 +3480,7 @@
<context context-type="sourcefile">src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.html</context>
<context context-type="linenumber">32</context>
</context-group>
<target state="needs-translation">Delete original documents after successful merge</target>
<target state="translated">Удалить оригиналы после успешного объединения</target>
</trans-unit>
<trans-unit id="5138283234724909648" datatype="html">
<source>Note that only PDFs will be included.</source>
@ -3488,7 +3488,7 @@
<context context-type="sourcefile">src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.html</context>
<context context-type="linenumber">34</context>
</context-group>
<target state="needs-translation">Note that only PDFs will be included.</target>
<target state="translated">Только PDF файлы могут быть добавлены.</target>
</trans-unit>
<trans-unit id="8157388568390631653" datatype="html">
<source>Note that only PDFs will be rotated.</source>
@ -4240,7 +4240,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
<context context-type="linenumber">46</context>
</context-group>
<target state="needs-translation">Include only files matching</target>
<target state="translated">Включить только соответствующие файлы</target>
</trans-unit>
<trans-unit id="7233407036155150477" datatype="html">
<source>Optional. Wildcards e.g. *.pdf or *invoice* allowed. Can be comma-separated list. Case insensitive.</source>
@ -4252,7 +4252,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<target state="needs-translation">Optional. Wildcards e.g. *.pdf or *invoice* allowed. Can be comma-separated list. Case insensitive.</target>
<target state="translated">Необязательно. Допускается использовать символ подстановки (wildcard), например *.pdf or *invoice*. Может быть список, разделенным через запятую. Нечувствителен к регистру.</target>
</trans-unit>
<trans-unit id="1546332577833742677" datatype="html">
<source>Exclude files matching</source>
@ -4260,7 +4260,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
<context context-type="linenumber">47</context>
</context-group>
<target state="needs-translation">Exclude files matching</target>
<target state="translated">Исключить соответствующие файлы</target>
</trans-unit>
<trans-unit id="9216117865911519658" datatype="html">
<source>Action</source>
@ -4276,7 +4276,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.html</context>
<context context-type="linenumber">53</context>
</context-group>
<target state="needs-translation">Only performed if the mail is processed.</target>
<target state="translated">Выполнять только если письмо обработано.</target>
</trans-unit>
<trans-unit id="1261794314435932203" datatype="html">
<source>Action parameter</source>
@ -4904,7 +4904,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">143</context>
</context-group>
<target state="needs-translation">Repeat the trigger every n days.</target>
<target state="translated">Повторять триггер каждые n дней.</target>
</trans-unit>
<trans-unit id="8727727835543352574" datatype="html">
<source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;em&gt;"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="&lt;/em&gt;"/> filters specified below.</source>
@ -4992,7 +4992,7 @@
<context context-type="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context>
<context context-type="linenumber">169</context>
</context-group>
<target state="needs-translation">Has any of tags</target>
<target state="translated">Содержит любой из тегов</target>
</trans-unit>
<trans-unit id="5281365940563983618" datatype="html">
<source>Has correspondent</source>

View File

@ -32,8 +32,7 @@ def changed_password_check(app_configs, **kwargs):
if not settings.PASSPHRASE:
return [
Error(
"The database contains encrypted documents but no password "
"is set.",
"The database contains encrypted documents but no password is set.",
),
]

View File

@ -170,6 +170,7 @@ class DocumentClassifier:
)
.select_related("document_type", "correspondent", "storage_path")
.prefetch_related("tags")
.order_by("pk")
)
# No documents exit to train against
@ -199,11 +200,10 @@ class DocumentClassifier:
hasher.update(y.to_bytes(4, "little", signed=True))
labels_correspondent.append(y)
tags: list[int] = sorted(
tag.pk
for tag in doc.tags.filter(
matching_algorithm=MatchingModel.MATCH_AUTO,
)
tags: list[int] = list(
doc.tags.filter(matching_algorithm=MatchingModel.MATCH_AUTO)
.order_by("pk")
.values_list("pk", flat=True),
)
for tag in tags:
hasher.update(tag.to_bytes(4, "little", signed=True))
@ -315,8 +315,7 @@ class DocumentClassifier:
else:
self.correspondent_classifier = None
logger.debug(
"There are no correspondents. Not training correspondent "
"classifier.",
"There are no correspondents. Not training correspondent classifier.",
)
if num_document_types > 0:
@ -326,8 +325,7 @@ class DocumentClassifier:
else:
self.document_type_classifier = None
logger.debug(
"There are no document types. Not training document type "
"classifier.",
"There are no document types. Not training document type classifier.",
)
if num_storage_paths > 0:

View File

@ -1,7 +1,5 @@
from email.encoders import encode_base64
from email.mime.base import MIMEBase
from email import message_from_bytes
from pathlib import Path
from urllib.parse import quote
from django.conf import settings
from django.core.mail import EmailMessage
@ -27,35 +25,14 @@ def send_email(
if attachment:
# Something could be renaming the file concurrently so it can't be attached
with FileLock(settings.MEDIA_LOCK), attachment.open("rb") as f:
file_content = f.read()
content = f.read()
if attachment_mime_type == "message/rfc822":
# See https://forum.djangoproject.com/t/using-emailmessage-with-an-attached-email-file-crashes-due-to-non-ascii/37981
content = message_from_bytes(f.read())
main_type, sub_type = (
attachment_mime_type.split("/", 1)
if attachment_mime_type
else ("application", "octet-stream")
email.attach(
filename=attachment.name,
content=content,
mimetype=attachment_mime_type,
)
mime_part = MIMEBase(main_type, sub_type)
mime_part.set_payload(file_content)
encode_base64(mime_part)
# see https://github.com/stumpylog/tika-client/blob/f65a2b792fc3cf15b9b119501bba9bddfac15fcc/src/tika_client/_base.py#L46-L57
try:
attachment.name.encode("ascii")
except UnicodeEncodeError:
filename_safed = attachment.name.encode("ascii", "ignore").decode(
"ascii",
)
filepath_quoted = quote(attachment.name, encoding="utf-8")
mime_part.add_header(
"Content-Disposition",
f"attachment; filename={filename_safed}; filename*=UTF-8''{filepath_quoted}",
)
else:
mime_part.add_header(
"Content-Disposition",
f"attachment; filename={attachment.name}",
)
email.attach(mime_part)
return email.send()

View File

@ -18,8 +18,7 @@ class Command(BaseCommand):
parser.add_argument(
"--passphrase",
help=(
"If PAPERLESS_PASSPHRASE isn't set already, you need to "
"specify it here"
"If PAPERLESS_PASSPHRASE isn't set already, you need to specify it here"
),
)

View File

@ -0,0 +1,63 @@
From: =?UTF-8?Q?My_Name=C3=B6er?= <myaddr@volkswagen.de>
Return-Path: <myaddr@volkswagen.de>
X-Original-To: rechnung@domain.de
Delivered-To: rechnung@domain.de
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=domainb.de; s=default;
t=1736973836; bh=bCUrrHd7c5mrvMbK20=;
h=Date:To:From:Subject:From;
b=QPaQKuzx2adfCr0S18KVgA5x01KXZknaaEpQW49Ock2ghScLAvv3ij8xfzUbZewCT
CuUAYBmCxbN5ygIztJXfgWpl1Cx5FsVQNpdZ/6Ns=
Received: by mail.domain.de (Postfix, from userid 121)
id 407BCE078A; Wed, 15 Jan 2025 21:43:56 +0100 (CET)
X-Spam-Checker-Version: SpamAssassin 4.0.0 (2022-12-13) on imail.domain.de
X-Spam-Level:
X-Spam-Status: No, score=-3.0 required=1.7 tests=ALL_TRUSTED,BAYES_00,
DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU autolearn=ham autolearn_force=no
version=4.0.0
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=domain.de; s=default;
t=1736973835; bh=bCUrrHvn+Hd7c5mrvMbK20=;
h=Date:To:From:Subject:From;
b=AjGxzFALRR0AixC1uRhFuQkb4MoBqju1NInlUzx9w+toniNx3ifgkXpGxiV7+JJsr
Z+jNZxck3D3M05ETYnrGInO+vDlosfFU2WqnZn+E=
Received: from [192.168.8.154] (unknown [1.1.1.1])
(using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits)
key-exchange X25519 server-signature ECDSA (prime256v1) server-digest
SHA256)
(No client certificate requested)
(Authenticated sender: myuser)
by mail.domain.de (Postfix) with ESMTPSA id C8BC6DF926
for <rechnung@domain.de>; Wed, 15 Jan 2025 21:43:55 +0100 (CET)
Message-ID: <da0c12c1-58c3-4f3d-ab89-9ae04@domain.de>
Date: Wed, 15 Jan 2025 21:43:52 +0100
MIME-Version: 1.0
User-Agent: Mozilla Thunderbird
Content-Language: de-DE
To: rechnung@domain.de
Subject: No Umlauts here
Autocrypt: addr=myaddr@domain.de; keydata=
xsDiBEK4/dERBACj7Kn2Skjnyq/Q69FKLSd9WJg/7Ta3aZwWiaizzAnB/avBoN9/NPkVCQbB
jeJ8G/uOtYDCgjmxeBNMVM3DOMTu4QfLnl0BoQz811bxiaPqQ6YLRA4MZrawZwerIOS2oSk2
FDGKsZvAYCG439QK102XPlSPC7c4/oQ+3fwkeqFpEwCg4XYOfTNzis6CZPgkQqyVrpaYR5kD
/j1HIDd1B75eeCb8ifoyWoWHB+cVHR+kEuMw1FMZt7UQ6Pb5nfQTcpEvrH9BTc0GKmTzj1N3
ExOPaNaGtsc7FAST+5dYflfL1+WVzsNJWgIp6PoAL1XoCZ6l63/qOrHtnp6l42IO8Rg2lDcc
25YdfiRSlTWuKvleT/okyc6jHioEA/9bUPbpdmUyR5kWRkdRBTjjCipl+o8rSlparnnk+7jh
1cvOHJlNJ/MYP9vcgDGYFIv+38sY4+UuBBoNmSS7yN5yKpT+XIsSgMEvyRPP6lr1GJ76aT2v
dIvcozHdC9g+nu6AlKgywdWW3hq5IjqRqnmVQfUN/1dL/D1ZImclEJoZR80lQ2hyaXN0aWFu
IFZvZWxrZXIgPGN2b2Vsa2VyQGtuZWJiLmRlPsJ3BBMRCAA3AhsjBgsJCAcDAgQVAggDBBYC
AwECHgECF4AWIQQl96acg1HmEUgEtrfRc0hiUBebOwUCXwQmvwAKCRDRc0hiUBebOxeiAJ43
bk2DCMuEVho3wRUqEyhNk0/mwQCaA60n1eTn+6bs2WXttTVGkBJGadzOwU0EQrj92RAIAKJz
rvwheohL4D327LEpy1AkIjUJotYUt9fPW+MVDSsoyj67HFTRz1WcK51+/8Fi6jedKxmR3hAi
GlZRvpsJ2chOuaynMac0Uv42rnSGHcLZf0KxLG+r7HOPSEAnSrbDAhWbuqyV994vCIfG9LDz
RDocaUEyJ7M+QV4VGS6Z3PPgxm78kCJ5TGHXRA96ponSptkyfIxvKHBa2TyrhMoLj4TmW4CO
SHmQD2e3EVIYlhERdPEQ5DmCljeO19ZopjNOLcAx4eOyguwvjpdeLUQJdaryWo56USWKbrmU
VrK4OodWkgcUvaagvey0MkABZkY0RMRKrfMuGb+Vw2nH9OGaRysAAwYH/AxC/+/m+OTA6tmA
AXd31vpMNUdVoPjyO+FQ7f8mwXa3SjPZeQLvpA1RfYFdDtSfr16RI8s41xtL12IYZr4nyRG/
wYPmM2WvcTUp3vWVizzHSERlarONc7aaCGXghg6Trpbz7+tv2MOpRLMfJd+6kyCz5pRSGeuX
z0iIxWSny1+Vc9uGgxyjJ21FFuvYPR8xmjfCGXvsnWLhKxTPNdhIG6/im/1/uTznzlfGUvgx
eNuzVphaVSPzP5DBVxJbKZzZYKOydQLx0Z79YF2xCGmz80EsSajpQNMvNYuNQXuH1ogFIP7e
PNOoaoakYuLE1YMhWL+AJzYRRevW8k/VLBgsYvbCRgQYEQIABgUCQrj92QAKCRDRc0hiUBeb
O/HXAJ0WAbB0sQ0SBVF+2Nlabw4HICAiKwCg4Fe9VjcfR4+ZJqq3Mx1c+IAE65c=
Content-Type: text/plain; charset=UTF-8; format=flowed
Content-Transfer-Encoding: 8bit
But here: üöäüäö

View File

@ -1,4 +1,7 @@
import types
from django.contrib.admin.sites import AdminSite
from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone
@ -6,6 +9,7 @@ from documents import index
from documents.admin import DocumentAdmin
from documents.models import Document
from documents.tests.utils import DirectoriesMixin
from paperless.admin import PaperlessUserAdmin
class TestDocumentAdmin(DirectoriesMixin, TestCase):
@ -64,3 +68,39 @@ class TestDocumentAdmin(DirectoriesMixin, TestCase):
created=timezone.make_aware(timezone.datetime(2020, 4, 12)),
)
self.assertEqual(self.doc_admin.created_(doc), "2020-04-12")
class TestPaperlessAdmin(DirectoriesMixin, TestCase):
def setUp(self) -> None:
super().setUp()
self.user_admin = PaperlessUserAdmin(model=User, admin_site=AdminSite())
def test_request_is_passed_to_form(self):
user = User.objects.create(username="test", is_superuser=False)
non_superuser = User.objects.create(username="requestuser")
request = types.SimpleNamespace(user=non_superuser)
formType = self.user_admin.get_form(request)
form = formType(data={}, instance=user)
self.assertEqual(form.request, request)
def test_only_superuser_can_change_superuser(self):
superuser = User.objects.create_superuser(username="superuser", password="test")
non_superuser = User.objects.create(username="requestuser")
user = User.objects.create(username="test", is_superuser=False)
data = {
"username": "test",
"is_superuser": True,
}
form = self.user_admin.form(data, instance=user)
form.request = types.SimpleNamespace(user=non_superuser)
self.assertFalse(form.is_valid())
self.assertEqual(
form.errors.get("__all__"),
["Superuser status can only be changed by a superuser"],
)
form = self.user_admin.form(data, instance=user)
form.request = types.SimpleNamespace(user=superuser)
self.assertTrue(form.is_valid())
self.assertEqual({}, form.errors)

View File

@ -681,6 +681,80 @@ class TestApiUser(DirectoriesMixin, APITestCase):
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_only_superusers_can_create_or_alter_superuser_status(self):
"""
GIVEN:
- Existing user account
WHEN:
- API request is made to add a user account with superuser status
- API request is made to change superuser status
THEN:
- Only superusers can change superuser status
"""
user1 = User.objects.create_user(username="user1")
user1.user_permissions.add(*Permission.objects.all())
user2 = User.objects.create_superuser(username="user2")
self.client.force_authenticate(user1)
response = self.client.patch(
f"{self.ENDPOINT}{user1.pk}/",
json.dumps(
{
"is_superuser": True,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self.client.post(
f"{self.ENDPOINT}",
json.dumps(
{
"username": "user3",
"is_superuser": True,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.client.force_authenticate(user2)
response = self.client.patch(
f"{self.ENDPOINT}{user1.pk}/",
json.dumps(
{
"is_superuser": True,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
returned_user1 = User.objects.get(pk=user1.pk)
self.assertEqual(returned_user1.is_superuser, True)
response = self.client.patch(
f"{self.ENDPOINT}{user1.pk}/",
json.dumps(
{
"is_superuser": False,
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
returned_user1 = User.objects.get(pk=user1.pk)
self.assertEqual(returned_user1.is_superuser, False)
class TestApiGroup(DirectoriesMixin, APITestCase):
ENDPOINT = "/api/groups/"

View File

@ -96,7 +96,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
doc = Document.objects.create(
checksum=str(i),
pk=i + 1,
title=f"Document {i+1}",
title=f"Document {i + 1}",
content="content",
)
index.update_document(writer, doc)
@ -131,7 +131,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
doc = Document.objects.create(
checksum=str(i),
pk=i + 1,
title=f"Document {i+1}",
title=f"Document {i + 1}",
content="content",
)
index.update_document(writer, doc)
@ -630,8 +630,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase):
doc = Document.objects.create(
checksum=str(i),
pk=i + 1,
title=f"Document {i+1}",
content=f"Things document {i+1}",
title=f"Document {i + 1}",
content=f"Things document {i + 1}",
)
index.update_document(writer, doc)

View File

@ -2149,9 +2149,8 @@ class TestWorkflows(
EMAIL_ENABLED=True,
PAPERLESS_URL="http://localhost:8000",
)
@mock.patch("httpx.post")
@mock.patch("django.core.mail.message.EmailMessage.send")
def test_workflow_email_include_file(self, mock_email_send, mock_post):
def test_workflow_email_include_file(self, mock_email_send):
"""
GIVEN:
- Document updated workflow with email action
@ -2199,6 +2198,24 @@ class TestWorkflows(
mock_email_send.assert_called_once()
mock_email_send.reset_mock()
# test with .eml file
test_file2 = shutil.copy(
self.SAMPLE_DIR / "eml_with_umlaut.eml",
self.dirs.scratch_dir / "eml_with_umlaut.eml",
)
doc2 = Document.objects.create(
title="sample eml",
checksum="123456",
filename=test_file2,
mime_type="message/rfc822",
)
run_workflows(WorkflowTrigger.WorkflowTriggerType.DOCUMENT_UPDATED, doc2)
mock_email_send.assert_called_once()
@override_settings(
EMAIL_ENABLED=False,
)

View File

@ -295,9 +295,9 @@ class TestMigrations(TransactionTestCase):
def setUp(self):
super().setUp()
assert (
self.migrate_from and self.migrate_to
), f"TestCase '{type(self).__name__}' must define migrate_from and migrate_to properties"
assert self.migrate_from and self.migrate_to, (
f"TestCase '{type(self).__name__}' must define migrate_from and migrate_to properties"
)
self.migrate_from = [(self.app, self.migrate_from)]
if self.dependencies is not None:
self.migrate_from.extend(self.dependencies)

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-02 23:04-0800\n"
"PO-Revision-Date: 2025-01-16 00:30\n"
"PO-Revision-Date: 2025-01-21 00:29\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Language: fr_FR\n"

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-02 23:04-0800\n"
"PO-Revision-Date: 2025-01-18 20:24\n"
"PO-Revision-Date: 2025-01-21 00:29\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Language: it_IT\n"
@ -31,7 +31,7 @@ msgstr "Campo personalizzato della query non valido"
#: documents/filters.py:365
msgid "Invalid expression list. Must be nonempty."
msgstr ""
msgstr "Elenco delle espressioni non valido. Deve essere non vuoto."
#: documents/filters.py:386
msgid "Invalid logical operator {op!r}"
@ -39,7 +39,7 @@ msgstr "Operatore logico non valido {op!r}"
#: documents/filters.py:400
msgid "Maximum number of query conditions exceeded."
msgstr ""
msgstr "Numero massimo delle condizioni della jQuery superato."
#: documents/filters.py:465
msgid "{name!r} is not a valid custom field."
@ -47,7 +47,7 @@ msgstr "{name!r} non è un campo personalizzato valido."
#: documents/filters.py:502
msgid "{data_type} does not support query expr {expr!r}."
msgstr ""
msgstr "{data_type} Non supporta la jQuery Expo {Expo!r}."
#: documents/filters.py:610
msgid "Maximum nesting depth exceeded."
@ -794,7 +794,7 @@ msgstr "Campo personalizzato"
#: documents/models.py:1037
msgid "Workflow Trigger Type"
msgstr ""
msgstr "Tipo Frigger Del Workshop"
#: documents/models.py:1049
msgid "filter path"

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-02 23:04-0800\n"
"PO-Revision-Date: 2024-12-03 07:05\n"
"PO-Revision-Date: 2025-01-19 12:10\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Language: no_NO\n"
@ -23,7 +23,7 @@ msgstr "Dokumenter"
#: documents/filters.py:336
msgid "Value must be valid JSON."
msgstr ""
msgstr "Verdien må være en gyldig JSON."
#: documents/filters.py:355
msgid "Invalid custom field query expression"
@ -1149,7 +1149,7 @@ msgstr "Ugyldig variabel oppdaget."
#: documents/templates/account/email/base_message.txt:1
#, python-format
msgid "Hello from %(site_name)s!"
msgstr ""
msgstr "Hei fra %(site_name)s!"
#: documents/templates/account/email/base_message.txt:5
#, python-format

View File

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: paperless-ngx\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-12-02 23:04-0800\n"
"PO-Revision-Date: 2024-12-31 00:30\n"
"PO-Revision-Date: 2025-01-19 00:32\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Language: ru_RU\n"
@ -374,7 +374,7 @@ msgstr "обратная сортировка"
#: documents/models.py:451
msgid "View page size"
msgstr ""
msgstr "Посмотреть размер страницы"
#: documents/models.py:459
msgid "View display mode"

53
src/paperless/admin.py Normal file
View File

@ -0,0 +1,53 @@
from django import forms
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
class PaperlessUserForm(forms.ModelForm):
"""
Custom form for the User model that adds validation to prevent non-superusers
from changing the superuser status of a user.
"""
class Meta:
model = User
fields = [
"username",
"first_name",
"last_name",
"email",
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
]
def clean(self):
cleaned_data = super().clean()
user_being_edited = self.instance
is_superuser = cleaned_data.get("is_superuser")
if (
not self.request.user.is_superuser
and is_superuser != user_being_edited.is_superuser
):
raise forms.ValidationError(
"Superuser status can only be changed by a superuser",
)
return cleaned_data
class PaperlessUserAdmin(UserAdmin):
form = PaperlessUserForm
def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
form.request = request
return form
admin.site.unregister(User)
admin.site.register(User, PaperlessUserAdmin)

View File

@ -109,6 +109,25 @@ class UserViewSet(ModelViewSet):
filterset_class = UserFilterSet
ordering_fields = ("username",)
def create(self, request, *args, **kwargs):
if not request.user.is_superuser and request.data.get("is_superuser") is True:
return HttpResponseForbidden(
"Superuser status can only be granted by a superuser",
)
return super().create(request, *args, **kwargs)
def update(self, request, *args, **kwargs):
user_to_update: User = self.get_object()
if (
not request.user.is_superuser
and request.data.get("is_superuser") is not None
and request.data.get("is_superuser") != user_to_update.is_superuser
):
return HttpResponseForbidden(
"Superuser status can only be changed by a superuser",
)
return super().update(request, *args, **kwargs)
@action(detail=True, methods=["post"])
def deactivate_totp(self, request, pk=None):
request_user = request.user

View File

@ -552,8 +552,7 @@ class MailAccountHandler(LoggingMixin):
mailbox_login(M, account)
self.log.debug(
f"Account {account}: Processing "
f"{account.rules.count()} rule(s)",
f"Account {account}: Processing {account.rules.count()} rule(s)",
)
for rule in account.rules.order_by("order"):

View File

@ -129,9 +129,11 @@ class TestParserLive:
assert thumb.exists()
assert thumb.is_file()
assert (
self.imagehash(thumb) == self.imagehash(simple_txt_email_thumbnail_file)
), f"Created Thumbnail {thumb} differs from expected file {simple_txt_email_thumbnail_file}"
assert self.imagehash(thumb) == self.imagehash(
simple_txt_email_thumbnail_file,
), (
f"Created Thumbnail {thumb} differs from expected file {simple_txt_email_thumbnail_file}"
)
def test_tika_parse_successful(self, mail_parser: MailDocumentParser):
"""
@ -226,6 +228,6 @@ class TestParserLive:
# The created pdf is not reproducible. But the converted image should always look the same.
expected_hash = self.imagehash(html_email_thumbnail_file)
assert (
generated_thumbnail_hash == expected_hash
), f"PDF looks different. Check if {generated_thumbnail} looks weird."
assert generated_thumbnail_hash == expected_hash, (
f"PDF looks different. Check if {generated_thumbnail} looks weird."
)

View File

@ -455,8 +455,7 @@ class RasterisedDocumentParser(DocumentParser):
self.text = text_original
else:
self.log.warning(
f"No text was found in {document_path}, the content will "
f"be empty.",
f"No text was found in {document_path}, the content will be empty.",
)
self.text = ""