Compare commits

..

20 Commits

Author SHA1 Message Date
shamoon
4070cd0e1b Just save this
[ci skip]
2025-01-22 12:13:35 -08:00
github-actions[bot]
2c28348b56 Changelog v2.14.5 - GHA (#8852)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-01-21 17:50:53 -08:00
shamoon
79e541244e Bump version to 2.14.5 2025-01-21 17:02:38 -08:00
shamoon
74afad5976 Merge branch 'dev' 2025-01-21 17:02:07 -08:00
dependabot[bot]
c694c9791b Chore(deps-dev): Bump undici from 5.28.4 to 5.28.5 in /src-ui (#8851)
Bumps [undici](https://github.com/nodejs/undici) from 5.28.4 to 5.28.5.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v5.28.4...v5.28.5)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-22 00:38:44 +00:00
github-actions[bot]
11ceb8bde5 New Crowdin translations by GitHub Action (#8804) 2025-01-21 16:28:36 -08:00
dependabot[bot]
20ec8cb57b Chore(deps-dev): Bump the development group with 2 updates (#8841)
* Chore(deps-dev): Bump the development group with 2 updates

Bumps the development group with 2 updates: [ruff](https://github.com/astral-sh/ruff) and [mkdocs-material](https://github.com/squidfunk/mkdocs-material).


Updates `ruff` from 0.8.6 to 0.9.2
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.8.6...0.9.2)

Updates `mkdocs-material` from 9.5.49 to 9.5.50
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.49...9.5.50)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: mkdocs-material
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update .pre-commit-config.yaml

* Run new ruff format

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
2025-01-21 19:22:25 +00:00
shamoon
bfc11a545b Change: use simpler method for attaching files (#8845) 2025-01-21 10:38:21 -08:00
shamoon
4866af31cb Documentation: update usage 2025-01-20 22:51:12 -08:00
shamoon
0ea4da03a7 Chore: fix coverage for superuser change 2025-01-20 12:02:20 -08:00
shamoon
41bcc12cc2 Change: restrict altering and creation of superusers to superusers only (#8837) 2025-01-20 11:57:22 -08:00
shamoon
475c231c6f Fix: fix long tag visual wrapping (#8833) 2025-01-20 07:43:39 -08:00
shamoon
e00dd46b22 Change: allow generate auth token without a usable password (#8824) 2025-01-19 13:49:16 -08:00
Trenton H
fd425aa618 Fix: Enforce classifier training ordering to prevent extra training (#8822) 2025-01-19 20:52:03 +00:00
shamoon
e1dde85c59 Fix failing test 2025-01-19 10:46:57 -08:00
shamoon
01207a284d Fix: import router module to not found component (#8821) 2025-01-19 10:37:31 -08:00
Thomas Hess
0f863ab378 Documentation: fix error in storage paths example in advanced_usage.md (#8817) 2025-01-19 18:04:55 +00:00
shamoon
258064b339 Fix: better reflect some mail account / rule permissions in UI (#8812) 2025-01-19 08:50:26 -08:00
kevin
2bcb37f3e9 Documentation: Add ISO number example (#8809) 2025-01-19 14:53:51 +00:00
github-actions[bot]
81f8c64b2c Changelog v2.14.4 - GHA (#8801)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
2025-01-18 13:05:36 -08:00
47 changed files with 1072 additions and 562 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

@@ -1,5 +1,64 @@
# Changelog
## paperless-ngx 2.14.5
### Features
- Change: restrict altering and creation of superusers to superusers only [@shamoon](https://github.com/shamoon) ([#8837](https://github.com/paperless-ngx/paperless-ngx/pull/8837))
### Bug Fixes
- Fix: fix long tag visual wrapping [@shamoon](https://github.com/shamoon) ([#8833](https://github.com/paperless-ngx/paperless-ngx/pull/8833))
- Fix: Enforce classifier training ordering to prevent extra training [@stumpylog](https://github.com/stumpylog) ([#8822](https://github.com/paperless-ngx/paperless-ngx/pull/8822))
- Fix: import router module to not found component [@shamoon](https://github.com/shamoon) ([#8821](https://github.com/paperless-ngx/paperless-ngx/pull/8821))
- Fix: better reflect some mail account / rule permissions in UI [@shamoon](https://github.com/shamoon) ([#8812](https://github.com/paperless-ngx/paperless-ngx/pull/8812))
### Dependencies
- Chore(deps-dev): Bump undici from 5.28.4 to 5.28.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8851](https://github.com/paperless-ngx/paperless-ngx/pull/8851))
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8841](https://github.com/paperless-ngx/paperless-ngx/pull/8841))
### All App Changes
<details>
<summary>9 changes</summary>
- Chore(deps-dev): Bump undici from 5.28.4 to 5.28.5 in /src-ui @[dependabot[bot]](https://github.com/apps/dependabot) ([#8851](https://github.com/paperless-ngx/paperless-ngx/pull/8851))
- Chore(deps-dev): Bump the development group with 2 updates @[dependabot[bot]](https://github.com/apps/dependabot) ([#8841](https://github.com/paperless-ngx/paperless-ngx/pull/8841))
- Chore: use simpler method for attaching files to emails [@shamoon](https://github.com/shamoon) ([#8845](https://github.com/paperless-ngx/paperless-ngx/pull/8845))
- Change: restrict altering and creation of superusers to superusers only [@shamoon](https://github.com/shamoon) ([#8837](https://github.com/paperless-ngx/paperless-ngx/pull/8837))
- Fix: fix long tag visual wrapping [@shamoon](https://github.com/shamoon) ([#8833](https://github.com/paperless-ngx/paperless-ngx/pull/8833))
- Change: allow generate auth token without a usable password [@shamoon](https://github.com/shamoon) ([#8824](https://github.com/paperless-ngx/paperless-ngx/pull/8824))
- Fix: Enforce classifier training ordering to prevent extra training [@stumpylog](https://github.com/stumpylog) ([#8822](https://github.com/paperless-ngx/paperless-ngx/pull/8822))
- Fix: import router module to not found component [@shamoon](https://github.com/shamoon) ([#8821](https://github.com/paperless-ngx/paperless-ngx/pull/8821))
- Fix: better reflect some mail account / rule permissions in UI [@shamoon](https://github.com/shamoon) ([#8812](https://github.com/paperless-ngx/paperless-ngx/pull/8812))
</details>
## paperless-ngx 2.14.4
### Features
- Enhancement: allow specifying JSON encoding for webhooks [@shamoon](https://github.com/shamoon) ([#8799](https://github.com/paperless-ngx/paperless-ngx/pull/8799))
- Change: disable API basic auth if MFA enabled [@shamoon](https://github.com/shamoon) ([#8792](https://github.com/paperless-ngx/paperless-ngx/pull/8792))
### Bug Fixes
- Fix: Include email and webhook objects in the export [@stumpylog](https://github.com/stumpylog) ([#8790](https://github.com/paperless-ngx/paperless-ngx/pull/8790))
- Fix: use MIMEBase for email attachments [@shamoon](https://github.com/shamoon) ([#8762](https://github.com/paperless-ngx/paperless-ngx/pull/8762))
- Fix: handle page out of range in mgmt lists after delete [@shamoon](https://github.com/shamoon) ([#8771](https://github.com/paperless-ngx/paperless-ngx/pull/8771))
### All App Changes
<details>
<summary>5 changes</summary>
- Enhancement: allow specifying JSON encoding for webhooks [@shamoon](https://github.com/shamoon) ([#8799](https://github.com/paperless-ngx/paperless-ngx/pull/8799))
- Change: disable API basic auth if MFA enabled [@shamoon](https://github.com/shamoon) ([#8792](https://github.com/paperless-ngx/paperless-ngx/pull/8792))
- Fix: Include email and webhook objects in the export [@stumpylog](https://github.com/stumpylog) ([#8790](https://github.com/paperless-ngx/paperless-ngx/pull/8790))
- Fix: use MIMEBase for email attachments [@shamoon](https://github.com/shamoon) ([#8762](https://github.com/paperless-ngx/paperless-ngx/pull/8762))
- Fix: handle page out of range in mgmt lists after delete [@shamoon](https://github.com/shamoon) ([#8771](https://github.com/paperless-ngx/paperless-ngx/pull/8771))
</details>
## paperless-ngx 2.14.3
### Bug Fixes

View File

@@ -1,9 +1,9 @@
# Usage Overview
Paperless is an application that manages your personal documents. With
the help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)),
paperless transforms your unwieldy physical document binders into a searchable archive
and provides many utilities for finding and managing your documents.
Paperless-ngx is an application that manages your personal documents. With
the (optional) help of a document scanner (see [the scanners wiki](https://github.com/paperless-ngx/paperless-ngx/wiki/Scanner-&-Software-Recommendations)), Paperless-ngx transforms your unwieldy
physical documents into a searchable archive and provides many utilities
for finding and managing your documents.
## Terms and definitions
@@ -12,10 +12,10 @@ documents:
- The _consumer_ watches a specified folder and adds all documents in
that folder to paperless.
- The _web server_ provides a UI that you use to manage and search for
your scanned documents.
- The _web server_ (web UI) provides a UI that you use to manage and
search documents.
Each document has a couple of fields that you can assign to them:
Each document has data fields that you can assign to them:
- A _Document_ is a piece of paper that sometimes contains valuable
information.
@@ -41,6 +41,53 @@ Each document has a couple of fields that you can assign to them:
- The _content_ of a document is the text that was OCR'ed from the
document. This text is fed into the search engine and is used for
matching tags, correspondents and document types.
- Paperless-ngx also supports _custom fields_ which can be used to
store additional metadata about a document.
## The Web UI
The web UI is the primary way to interact with Paperless-ngx. It is a
single-page application that is built with modern web technologies and
is designed to be fast and responsive. The web UI includes a robust
interface for filtering, viewing, searching and editing documents.
You can also manage tags, correspondents, document types, and other
settings from the web UI.
The web UI also includes a 'tour' feature that can be accessed from the
settings page or from the dashboard for new users. The tour highlights
some of the key features of the web UI and can be useful for new users.
### Dashboard
The dashboard is the first page you see when you log in. By default, it
does not show any documents, but you can add saved views to the dashboard
to show documents that match certain criteria. The dashboard also includes
a button to upload documents to Paperless-ngx but you can also drag and
drop files anywhere in the app to initiate the consumption process.
### Document List
The document list is the primary way to view and interact with your documents.
You can filter the list by tags, correspondents, document types, and other
criteria. You can also edit documents in bulk including assigning tags,
correspondents, document types, and custom fields. Selecting document(s) from
the list will allow you to perform the various bulk edit operations. The
document list also includes a search bar that allows you to search for documents
by title, ASN, and use advanced search syntax.
### Document Detail
The document detail page shows all the information about a single document.
You can view the document, edit its metadata, assign tags, correspondents,
document types, and custom fields. You can also view the document history,
download the document or share it via a share link.
### Management Lists
Paperless-ngx includes management lists for tags, correspondents, document types
and more. These areas allow you to view, add, edit, delete and manage permissions
for these objects. You can also manage saved views, mail accounts, mail rules,
workflows and more from the management sections.
## Adding documents to paperless
@@ -252,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

@@ -1,10 +1,14 @@
<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>
@if (!previewOnly) {
<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>
} @else {
<ng-container [ngTemplateOutlet]="previewContent" [ngTemplateOutletContext]="{ $implicit: document }"></ng-container>
}
<ng-template #previewContent>
<div class="preview-popup-container" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview(); close()">
<div class="preview-popup-container" [class.full-size]="previewOnly" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview(); close()">
@if (error) {
<div class="w-100 h-100 position-relative">
<p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p>

View File

@@ -4,6 +4,16 @@
overflow-y: scroll;
}
.preview-popup-container.full-size {
width: 100% !important;
height: 100% !important;
> * {
width: 100% !important;
height: 100% !important;
}
}
::ng-deep .popover.popover-preview {
max-width: 32rem;
}

View File

@@ -1,3 +1,4 @@
import { NgTemplateOutlet } from '@angular/common'
import { HttpClient } from '@angular/common/http'
import { Component, Input, OnDestroy, ViewChild } from '@angular/core'
import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
@@ -17,6 +18,7 @@ import { SettingsService } from 'src/app/services/settings.service'
styleUrls: ['./preview-popup.component.scss'],
imports: [
NgbPopoverModule,
NgTemplateOutlet,
DocumentTitlePipe,
PdfViewerModule,
SafeUrlPipe,
@@ -47,6 +49,9 @@ export class PreviewPopupComponent implements OnDestroy {
@Input()
linkTitle: string = $localize`Open preview`
@Input()
previewOnly: boolean = false
unsubscribeNotifier: Subject<any> = new Subject()
error = false
@@ -91,6 +96,8 @@ export class PreviewPopupComponent implements OnDestroy {
}
init() {
this.error = false
this.requiresPassword = false
if (this.document.mime_type?.includes('text')) {
this.http
.get(this.previewURL, { responseType: 'text' })
@@ -119,6 +126,7 @@ export class PreviewPopupComponent implements OnDestroy {
}
mouseEnterPreview() {
if (this.previewOnly) return
this.mouseOnPreview = true
if (!this.popover.isOpen()) {
// we're going to open but hide to pre-load content during hover delay
@@ -136,10 +144,12 @@ export class PreviewPopupComponent implements OnDestroy {
}
mouseLeavePreview() {
if (this.previewOnly) return
this.mouseOnPreview = false
}
public close(immediate: boolean = false) {
if (this.previewOnly) return
setTimeout(
() => {
if (!this.mouseOnPreview) this.popover.close()

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

@@ -27,6 +27,7 @@
</div>
</div>
</div>
<div class="btn-group flex-fill" role="group">
<input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="table" id="displayModeDetails" name="displayModeDetails">
<label for="displayModeDetails" class="btn btn-outline-primary btn-sm">
@@ -42,6 +43,13 @@
</label>
</div>
<div class="btn-group flex-fill" role="group">
<input type="checkbox" class="btn-check" [(ngModel)]="list.showPreviewPane" value="table" id="previewPane" name="previewPane">
<label for="previewPane" class="btn btn-outline-primary btn-sm">
<i-bs name="window-split"></i-bs>
</label>
</div>
<div ngbDropdown class="btn-group flex-fill">
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>
<i-bs name="arrow-down-up"></i-bs>
@@ -105,298 +113,335 @@
<pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor>
</div>
<ng-template #pagination>
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
@if (list.isReloading) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
}
@if (list.selected.size > 0) {
<span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
}
@if (!list.isReloading) {
@if (list.selected.size === 0) {
<span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>
}&nbsp;@if (isFiltered) {
&nbsp;<span i18n>(filtered)</span>
<div class="row">
<div [class.col-lg-6]="list.showPreviewPane" [class.col]="!list.showPreviewPane">
<ng-template #pagination>
<div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3">
<div class="d-flex align-items-center">
@if (list.isReloading) {
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
<ng-container i18n>Loading...</ng-container>
}
}
@if (!list.isReloading && isFiltered) {
<button class="btn btn-link py-0" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button>
@if (list.selected.size > 0) {
<span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
}
@if (!list.isReloading) {
@if (list.selected.size === 0) {
<span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>
}&nbsp;@if (isFiltered) {
&nbsp;<span i18n>(filtered)</span>
}
}
@if (!list.isReloading && isFiltered) {
<button class="btn btn-link py-0" (click)="resetFilters()">
<i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small>
</button>
}
</div>
@if (list.collectionSize) {
<ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
}
</div>
@if (list.collectionSize) {
<ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination>
}
</ng-template>
<div tourAnchor="tour.documents">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
</ng-template>
<div tourAnchor="tour.documents">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
@if (list.error ) {
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
} @else {
@if (list.displayMode === DisplayMode.LARGE_CARDS) {
<div>
@for (d of list.documents; track d.id) {
<pngx-document-card-large
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
[displayFields]="activeDisplayFields"
(clickTag)="clickTag($event)"
(clickCorrespondent)="clickCorrespondent($event)"
(clickDocumentType)="clickDocumentType($event)"
(clickStoragePath)="clickStoragePath($event)"
(clickMoreLike)="clickMoreLike(d.id)">
</pngx-document-card-large>
}
</div>
}
@if (list.displayMode === DisplayMode.TABLE) {
<div class="table-responsive">
<table class="table table-sm align-middle border shadow-sm">
<thead>
<tr>
<th></th>
@if (activeDisplayFields.includes(DisplayField.ASN)) {
<th class="cursor-pointer"
pngxSortable="archive_serial_number"
title="Sort by ASN" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>ASN</th>
}
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<th class="cursor-pointer"
pngxSortable="correspondent__name"
title="Sort by correspondent" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
}
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
<th class="cursor-pointer"
pngxSortable="title"
title="Sort by title" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
style="min-width: 150px;"
i18n>Title</th>
}
@if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) {
<th i18n>Tags</th>
}
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
<th class="cursor-pointer"
pngxSortable="owner"
title="Sort by owner" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Owner</th>
}
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
<th class="cursor-pointer"
pngxSortable="num_notes"
title="Sort by notes" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Notes</th>
}
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<th class="cursor-pointer"
pngxSortable="document_type__name"
title="Sort by document type" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Document type</th>
}
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<th class="cursor-pointer"
pngxSortable="storage_path__name"
title="Sort by storage path" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Storage path</th>
}
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
<th class="cursor-pointer"
pngxSortable="created"
title="Sort by created date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Created</th>
}
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
<th class="cursor-pointer"
pngxSortable="added"
title="Sort by added date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
}
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
@if (list.error ) {
<div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div>
} @else {
@if (list.displayMode === DisplayMode.LARGE_CARDS) {
<div>
@for (d of list.documents; track d.id) {
<pngx-document-card-large
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
[displayFields]="activeDisplayFields"
(clickTag)="clickTag($event)"
(clickCorrespondent)="clickCorrespondent($event)"
(clickDocumentType)="clickDocumentType($event)"
(clickStoragePath)="clickStoragePath($event)"
(clickMoreLike)="clickMoreLike(d.id)">
</pngx-document-card-large>
}
</div>
}
@if (list.displayMode === DisplayMode.TABLE) {
<div class="table-responsive">
<table class="table table-sm align-middle border shadow-sm">
<thead>
<tr>
<th></th>
@if (activeDisplayFields.includes(DisplayField.ASN)) {
<th class="cursor-pointer"
pngxSortable="page_count"
title="Sort by number of pages" i18n-title
pngxSortable="archive_serial_number"
title="Sort by ASN" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Pages</th>
}
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
<th i18n>
Shared
</th>
}
@for (field_id of activeDisplayCustomFields; track field_id) {
<th class="cursor-pointer"
pngxSortable="{{field_id}}"
title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)">
{{getDisplayCustomFieldTitle(field_id)}}
</th>
}
</tr>
</thead>
<tbody>
@for (d of list.documents; track d.id) {
<tr (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();">
<label class="form-check-label" for="docCheck{{d.id}}"></label>
</div>
</td>
@if (activeDisplayFields.includes(DisplayField.ASN)) {
<td class="">
{{d.archive_serial_number}}
</td>
i18n>ASN</th>
}
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<td class="">
@if (d.correspondent) {
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
}
</td>
<th class="cursor-pointer"
pngxSortable="correspondent__name"
title="Sort by correspondent" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
}
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
<td width="30%">
@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>
<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)) {
@for (t of d.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
}
}
</td>
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
<th class="cursor-pointer"
pngxSortable="title"
title="Sort by title" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
style="min-width: 150px;"
i18n>Title</th>
}
@if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) {
<th i18n>Tags</th>
}
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
<td>
{{d.owner | username}}
</td>
<th class="cursor-pointer"
pngxSortable="owner"
title="Sort by owner" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Owner</th>
}
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
<td class="">
@if (d.notes.length) {
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
<span class="badge rounded-pill bg-light border text-primary">
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
{{d.notes.length}}</span>
</a>
}
</td>
<th class="cursor-pointer"
pngxSortable="num_notes"
title="Sort by notes" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Notes</th>
}
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<td class="">
@if (d.document_type) {
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
}
</td>
<th class="cursor-pointer"
pngxSortable="document_type__name"
title="Sort by document type" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Document type</th>
}
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<td class="">
@if (d.storage_path) {
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
}
</td>
<th class="cursor-pointer"
pngxSortable="storage_path__name"
title="Sort by storage path" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Storage path</th>
}
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
<td>
{{d.created_date | customDate}}
</td>
<th class="cursor-pointer"
pngxSortable="created"
title="Sort by created date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Created</th>
}
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
<td>
{{d.added | customDate}}
</td>
<th class="cursor-pointer"
pngxSortable="added"
title="Sort by added date" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
}
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
<td>
{{ d.page_count }}
</td>
<th class="cursor-pointer"
pngxSortable="page_count"
title="Sort by number of pages" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Pages</th>
}
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
<td>
@if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }
</td>
<th i18n>
Shared
</th>
}
@for (field of activeDisplayCustomFields; track field) {
<td class="">
<pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display>
</td>
@for (field_id of activeDisplayCustomFields; track field_id) {
<th class="cursor-pointer"
pngxSortable="{{field_id}}"
title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)">
{{getDisplayCustomFieldTitle(field_id)}}
</th>
}
</tr>
}
</tbody>
</table>
</div>
</thead>
<tbody>
@for (d of list.documents; track d.id) {
<tr (click)="toggleSelected(d, $event); $event.stopPropagation();" (dblclick)="openDocumentDetail(d)" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">
<td>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="docCheck{{d.id}}" [checked]="list.isSelected(d)" (click)="toggleSelected(d, $event); $event.stopPropagation();">
<label class="form-check-label" for="docCheck{{d.id}}"></label>
</div>
</td>
@if (activeDisplayFields.includes(DisplayField.ASN)) {
<td class="">
{{d.archive_serial_number}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<td class="">
@if (d.correspondent) {
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
<td width="30%">
@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>
<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)) {
@for (t of d.tags$ | async; track t) {
<pngx-tag [tag]="t" class="ms-1" clickable="true" linkTitle="Filter by tag" i18n-linkTitle (click)="clickTag(t.id);$event.stopPropagation()"></pngx-tag>
}
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) {
<td>
{{d.owner | username}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) {
<td class="">
@if (d.notes.length) {
<a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0">
<span class="badge rounded-pill bg-light border text-primary">
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
{{d.notes.length}}</span>
</a>
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) {
<td class="">
@if (d.document_type) {
<a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a>
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) {
<td class="">
@if (d.storage_path) {
<a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a>
}
</td>
}
@if (activeDisplayFields.includes(DisplayField.CREATED)) {
<td>
{{d.created_date | customDate}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.ADDED)) {
<td>
{{d.added | customDate}}
</td>
}
@if (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) {
<td>
{{ d.page_count }}
</td>
}
@if (activeDisplayFields.includes(DisplayField.SHARED)) {
<td>
@if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> }
</td>
}
@for (field of activeDisplayCustomFields; track field) {
<td class="">
<pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display>
</td>
}
</tr>
}
</tbody>
</table>
</div>
}
@if (list.displayMode === DisplayMode.SMALL_CARDS) {
<div class="row row-cols-paperless-cards">
@for (d of list.documents; track d.id) {
<pngx-document-card-small class="p-0"
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
(clickTag)="clickTag($event)"
[displayFields]="activeDisplayFields"
(clickCorrespondent)="clickCorrespondent($event)"
(clickStoragePath)="clickStoragePath($event)"
(clickDocumentType)="clickDocumentType($event)">
</pngx-document-card-small>
}
</div>
}
@if (list.documents?.length > 15) {
<div class="mt-3">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
</div>
}
}
@if (list.displayMode === DisplayMode.SMALL_CARDS) {
<div class="row row-cols-paperless-cards">
@for (d of list.documents; track d.id) {
<pngx-document-card-small class="p-0"
[selected]="list.isSelected(d)"
(toggleSelected)="toggleSelected(d, $event)"
(dblClickDocument)="openDocumentDetail(d)"
[document]="d"
(clickTag)="clickTag($event)"
[displayFields]="activeDisplayFields"
(clickCorrespondent)="clickCorrespondent($event)"
(clickStoragePath)="clickStoragePath($event)"
(clickDocumentType)="clickDocumentType($event)">
</pngx-document-card-small>
}
</div>
@if (list.showPreviewPane) {
<div class="col-lg-6">
<div class="row">
<div class="btn-toolbar mb-1 border-bottom align-items-center">
<div class="btn-group pb-3">
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="list.documents.length === 0 || !hasPrevious">
<i-bs width="1.2em" height="1.2em" name="arrow-left" class="me-1"></i-bs><ng-container i18n>Previous</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Next" (click)="nextDoc()" [disabled]="list.documents.length === 0 || !hasNext">
<ng-container i18n>Next</ng-container><i-bs width="1.2em" height="1.2em" name="arrow-right" class="ms-1"></i-bs>
</button>
</div>
<div class="input-group pb-3 ms-auto">
<h5 class="mb-0">
{{list.firstSelectedDocument?.title}}
</h5>
</div>
</div>
</div>
}
@if (list.documents?.length > 15) {
<div class="mt-3">
<ng-container *ngTemplateOutlet="pagination"></ng-container>
<div class="row">
<div class="col preview-pane">
@if (list.selected.size > 0) {
<pngx-preview-popup [document]="list.firstSelectedDocument" [previewOnly]="true"></pngx-preview-popup>
} @else {
<div class="w-100 h-100 position-relative">
<p class="fst-italic">
<ng-container i18n>No document selected</ng-container>
</p>
</div>
}
</div>
</div>
}
</div>
}
</div>

View File

@@ -80,3 +80,9 @@ a {
pngx-page-header .dropdown-menu {
--bs-dropdown-min-width: 12em;
}
.preview-pane {
height: 60rem;
top: 70px;
position: sticky;
}

View File

@@ -326,24 +326,36 @@ export class DocumentListComponent
this.hotKeyService
.addShortcut({
keys: 'control.arrowleft',
description: $localize`Previous page`,
description: $localize`Previous page / document`,
})
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.list.currentPage > 1) {
this.list.currentPage--
if (this.list.showPreviewPane) {
if (this.hasPrevious) {
this.previousDoc()
}
} else {
if (this.list.currentPage > 1) {
this.list.currentPage--
}
}
})
this.hotKeyService
.addShortcut({
keys: 'control.arrowright',
description: $localize`Next page`,
description: $localize`Next page / document`,
})
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
if (this.list.currentPage < this.list.getLastPage()) {
this.list.currentPage++
if (this.list.showPreviewPane) {
if (this.hasNext) {
this.nextDoc()
}
} else {
if (this.list.currentPage < this.list.getLastPage()) {
this.list.currentPage++
}
}
})
}
@@ -473,4 +485,45 @@ export class DocumentListComponent
resetFilters() {
this.filterEditor.resetSelected()
}
public get hasPrevious(): boolean {
return (
(this.list.selected.size > 0 &&
this.list.documents.indexOf(this.list.firstSelectedDocument) > 0) ||
(this.list.selected.size === 0 && this.list.documents.length > 0)
)
}
public get hasNext(): boolean {
return (
(this.list.selected.size > 0 &&
this.list.documents.indexOf(this.list.firstSelectedDocument) <
this.list.documents.length - 1) ||
(this.list.selected.size === 0 && this.list.documents.length > 0)
)
}
public nextDoc(): void {
const index =
this.list.selected.size === 0
? 0
: Math.min(
this.list.documents.indexOf(this.list.firstSelectedDocument) + 1,
this.list.documents.length - 1
)
this.list.selected.clear()
this.list.selected.add(this.list.documents[index].id)
}
public previousDoc(): void {
const index =
this.list.selected.size === 0
? 0
: Math.max(
this.list.documents.indexOf(this.list.firstSelectedDocument) - 1,
0
)
this.list.selected.clear()
this.list.selected.add(this.list.documents[index].id)
}
}

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

@@ -79,6 +79,11 @@ export interface ListViewState {
* The fields to display in the document list.
*/
displayFields?: DisplayField[]
/**
* Whether the preview pane is shown.
*/
showPreviewPane?: boolean
}
/**
@@ -165,6 +170,7 @@ export class DocumentListViewService {
sortReverse: true,
filterRules: [],
selected: new Set<number>(),
showPreviewPane: false,
}
}
@@ -451,6 +457,15 @@ export class DocumentListViewService {
this.saveDocumentListView()
}
get showPreviewPane(): boolean {
return this.activeListViewState.showPreviewPane
}
set showPreviewPane(show: boolean) {
this.activeListViewState.showPreviewPane = show
this.saveDocumentListView()
}
private saveDocumentListView() {
if (this._activeSavedViewId == null) {
let savedState: ListViewState = {
@@ -461,6 +476,7 @@ export class DocumentListViewService {
sortReverse: this.activeListViewState.sortReverse,
displayMode: this.activeListViewState.displayMode,
displayFields: this.activeListViewState.displayFields,
showPreviewPane: this.activeListViewState.showPreviewPane,
}
localStorage.setItem(
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
@@ -626,4 +642,8 @@ export class DocumentListViewService {
documentIndexInCurrentView(documentID: number): number {
return this.documents.map((d) => d.id).indexOf(documentID)
}
get firstSelectedDocument(): Document {
return this.documents.find((d) => this.selected.has(d.id))
}
}

View File

@@ -5,7 +5,7 @@ export const environment = {
apiBaseUrl: document.baseURI + 'api/',
apiVersion: '6',
appTitle: 'Paperless-ngx',
version: '2.14.4',
version: '2.14.5',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',

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

@@ -125,6 +125,7 @@ import {
trash,
uiRadios,
upcScan,
windowSplit,
windowStack,
x,
xCircle,
@@ -323,6 +324,7 @@ const icons = {
trash,
uiRadios,
upcScan,
windowSplit,
windowStack,
x,
xCircle,

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

@@ -1,6 +1,6 @@
from typing import Final
__version__: Final[tuple[int, int, int]] = (2, 14, 4)
__version__: Final[tuple[int, int, int]] = (2, 14, 5)
# Version string like X.Y.Z
__full_version_str__: Final[str] = ".".join(map(str, __version__))
# Version string like X.Y

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 = ""