mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-28 03:46:06 -05:00 
			
		
		
		
	Compare commits
	
		
			20 Commits
		
	
	
		
			v2.14.4
			...
			feature-pr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 4070cd0e1b | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 2c28348b56 | ||
|   | 79e541244e | ||
|   | 74afad5976 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | c694c9791b | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 11ceb8bde5 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 20ec8cb57b | ||
|   | bfc11a545b | ||
|   | 4866af31cb | ||
|   | 0ea4da03a7 | ||
|   | 41bcc12cc2 | ||
|   | 475c231c6f | ||
|   | e00dd46b22 | ||
|   | fd425aa618 | ||
|   | e1dde85c59 | ||
|   | 01207a284d | ||
|   | 0f863ab378 | ||
|   | 258064b339 | ||
|   | 2bcb37f3e9 | ||
| ![github-actions[bot]](/assets/img/avatar_default.png)  | 81f8c64b2c | 
| @@ -51,7 +51,7 @@ repos: | |||||||
|           - 'prettier-plugin-organize-imports@4.1.0' |           - 'prettier-plugin-organize-imports@4.1.0' | ||||||
|   # Python hooks |   # Python hooks | ||||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit |   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||||
|     rev: v0.8.6 |     rev: v0.9.2 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: ruff |       - id: ruff | ||||||
|       - id: ruff-format |       - id: ruff-format | ||||||
|   | |||||||
							
								
								
									
										277
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										277
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @@ -2913,122 +2913,109 @@ | |||||||
|         }, |         }, | ||||||
|         "charset-normalizer": { |         "charset-normalizer": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621", |                 "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537", | ||||||
|                 "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", |                 "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa", | ||||||
|                 "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", |                 "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a", | ||||||
|                 "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", |                 "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294", | ||||||
|                 "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", |                 "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b", | ||||||
|                 "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", |                 "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", | ||||||
|                 "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", |                 "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", | ||||||
|                 "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", |                 "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd", | ||||||
|                 "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", |                 "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4", | ||||||
|                 "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", |                 "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", | ||||||
|                 "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", |                 "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2", | ||||||
|                 "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", |                 "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", | ||||||
|                 "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab", |                 "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd", | ||||||
|                 "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", |                 "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", | ||||||
|                 "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", |                 "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8", | ||||||
|                 "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", |                 "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", | ||||||
|                 "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", |                 "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", | ||||||
|                 "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", |                 "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496", | ||||||
|                 "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62", |                 "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", | ||||||
|                 "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", |                 "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", | ||||||
|                 "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", |                 "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e", | ||||||
|                 "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", |                 "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a", | ||||||
|                 "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", |                 "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4", | ||||||
|                 "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", |                 "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca", | ||||||
|                 "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455", |                 "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78", | ||||||
|                 "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858", |                 "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", | ||||||
|                 "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", |                 "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5", | ||||||
|                 "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", |                 "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", | ||||||
|                 "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", |                 "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", | ||||||
|                 "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", |                 "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", | ||||||
|                 "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", |                 "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765", | ||||||
|                 "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea", |                 "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6", | ||||||
|                 "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", |                 "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146", | ||||||
|                 "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", |                 "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6", | ||||||
|                 "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", |                 "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", | ||||||
|                 "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", |                 "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd", | ||||||
|                 "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", |                 "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c", | ||||||
|                 "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", |                 "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", | ||||||
|                 "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", |                 "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", | ||||||
|                 "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee", |                 "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", | ||||||
|                 "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", |                 "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770", | ||||||
|                 "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", |                 "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824", | ||||||
|                 "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51", |                 "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f", | ||||||
|                 "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", |                 "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf", | ||||||
|                 "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8", |                 "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487", | ||||||
|                 "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", |                 "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d", | ||||||
|                 "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613", |                 "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd", | ||||||
|                 "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", |                 "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", | ||||||
|                 "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", |                 "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534", | ||||||
|                 "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", |                 "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", | ||||||
|                 "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", |                 "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b", | ||||||
|                 "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", |                 "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", | ||||||
|                 "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", |                 "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd", | ||||||
|                 "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", |                 "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", | ||||||
|                 "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", |                 "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9", | ||||||
|                 "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", |                 "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", | ||||||
|                 "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417", |                 "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", | ||||||
|                 "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", |                 "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d", | ||||||
|                 "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", |                 "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", | ||||||
|                 "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", |                 "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", | ||||||
|                 "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", |                 "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", | ||||||
|                 "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", |                 "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7", | ||||||
|                 "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149", |                 "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", | ||||||
|                 "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41", |                 "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", | ||||||
|                 "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574", |                 "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8", | ||||||
|                 "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", |                 "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41", | ||||||
|                 "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f", |                 "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", | ||||||
|                 "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", |                 "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", | ||||||
|                 "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654", |                 "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", | ||||||
|                 "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", |                 "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", | ||||||
|                 "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19", |                 "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", | ||||||
|                 "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", |                 "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77", | ||||||
|                 "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578", |                 "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76", | ||||||
|                 "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", |                 "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", | ||||||
|                 "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", |                 "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", | ||||||
|                 "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51", |                 "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", | ||||||
|                 "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", |                 "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", | ||||||
|                 "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", |                 "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e", | ||||||
|                 "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", |                 "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6", | ||||||
|                 "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", |                 "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", | ||||||
|                 "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade", |                 "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", | ||||||
|                 "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", |                 "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e", | ||||||
|                 "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", |                 "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", | ||||||
|                 "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6", |                 "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", | ||||||
|                 "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", |                 "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c", | ||||||
|                 "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", |                 "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12", | ||||||
|                 "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6", |                 "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", | ||||||
|                 "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2", |                 "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089", | ||||||
|                 "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12", |                 "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", | ||||||
|                 "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf", |                 "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e", | ||||||
|                 "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", |                 "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00", | ||||||
|                 "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7", |                 "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616" | ||||||
|                 "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" |  | ||||||
|             ], |             ], | ||||||
|             "markers": "python_full_version >= '3.7.0'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==3.4.0" |             "version": "==3.4.1" | ||||||
|         }, |         }, | ||||||
|         "click": { |         "click": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", |                 "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", | ||||||
|                 "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" |                 "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.7'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==8.1.7" |             "version": "==8.1.8" | ||||||
|         }, |         }, | ||||||
|         "colorama": { |         "colorama": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -3291,11 +3278,11 @@ | |||||||
|         }, |         }, | ||||||
|         "jinja2": { |         "jinja2": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369", |                 "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", | ||||||
|                 "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" |                 "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.7'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==3.1.4" |             "version": "==3.1.5" | ||||||
|         }, |         }, | ||||||
|         "markdown": { |         "markdown": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -3406,12 +3393,12 @@ | |||||||
|         }, |         }, | ||||||
|         "mkdocs-material": { |         "mkdocs-material": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:3671bb282b4f53a1c72e08adbe04d2481a98f85fed392530051f80ff94a9621d", |                 "sha256:ae5fe16f3d7c9ccd05bb6916a7da7420cf99a9ce5e33debd9d40403a090d5825", | ||||||
|                 "sha256:c3c2d8176b18198435d3a3e119011922f3e11424074645c24019c2dcf08a360e" |                 "sha256:f24100f234741f4d423a9d672a909d859668a4f404796be3cf035f10d6050385" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==9.5.49" |             "version": "==9.5.50" | ||||||
|         }, |         }, | ||||||
|         "mkdocs-material-extensions": { |         "mkdocs-material-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -3645,19 +3632,19 @@ | |||||||
|         }, |         }, | ||||||
|         "pygments": { |         "pygments": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", |                 "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", | ||||||
|                 "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" |                 "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==2.18.0" |             "version": "==2.19.1" | ||||||
|         }, |         }, | ||||||
|         "pymdown-extensions": { |         "pymdown-extensions": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:49f81412242d3527b8b4967b990df395c89563043bc51a3d2d7d500e52123b77", |                 "sha256:202481f716cc8250e4be8fce997781ebf7917701b59652458ee47f2401f818b5", | ||||||
|                 "sha256:b0ee1e0b2bef1071a47891ab17003bfe5bf824a398e13f49f8ed653b699369a7" |                 "sha256:741bd7c4ff961ba40b7528d32284c53bc436b8b1645e8e37c3e57770b8700a34" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.8'", | ||||||
|             "version": "==10.12" |             "version": "==10.14" | ||||||
|         }, |         }, | ||||||
|         "pyopenssl": { |         "pyopenssl": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -3974,28 +3961,28 @@ | |||||||
|         }, |         }, | ||||||
|         "ruff": { |         "ruff": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:0509e8da430228236a18a677fcdb0c1f102dd26d5520f71f79b094963322ed25", |                 "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df", | ||||||
|                 "sha256:0c000a471d519b3e6cfc9c6680025d923b4ca140ce3e4612d1a2ef58e11f11fe", |                 "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d", | ||||||
|                 "sha256:248b1fb3f739d01d528cc50b35ee9c4812aa58cc5935998e776bf8ed5b251e75", |                 "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb", | ||||||
|                 "sha256:45a56f61b24682f6f6709636949ae8cc82ae229d8d773b4c76c09ec83964a95a", |                 "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145", | ||||||
|                 "sha256:496dd38a53aa173481a7d8866bcd6451bd934d06976a2505028a50583e001b76", |                 "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347", | ||||||
|                 "sha256:52d587092ab8df308635762386f45f4638badb0866355b2b86760f6d3c076188", |                 "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d", | ||||||
|                 "sha256:54799ca3d67ae5e0b7a7ac234baa657a9c1784b48ec954a094da7c206e0365b1", |                 "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c", | ||||||
|                 "sha256:61323159cf21bc3897674e5adb27cd9e7700bab6b84de40d7be28c3d46dc67cf", |                 "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684", | ||||||
|                 "sha256:7ae4478b1471fc0c44ed52a6fb787e641a2ac58b1c1f91763bafbc2faddc5117", |                 "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f", | ||||||
|                 "sha256:7d7fc2377a04b6e04ffe588caad613d0c460eb2ecba4c0ccbbfe2bc973cbc162", |                 "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6", | ||||||
|                 "sha256:91a7ddb221779871cf226100e677b5ea38c2d54e9e2c8ed847450ebbdf99b32d", |                 "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a", | ||||||
|                 "sha256:9257aa841e9e8d9b727423086f0fa9a86b6b420fbf4bf9e1465d1250ce8e4d8d", |                 "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe", | ||||||
|                 "sha256:bc3c083c50390cf69e7e1b5a5a7303898966be973664ec0c4a4acea82c1d4315", |                 "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0", | ||||||
|                 "sha256:dcad24b81b62650b0eb8814f576fc65cfee8674772a6e24c9b747911801eeaa5", |                 "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00", | ||||||
|                 "sha256:defed167955d42c68b407e8f2e6f56ba52520e790aba4ca707a9c88619e580e3", |                 "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247", | ||||||
|                 "sha256:e169ea1b9eae61c99b257dc83b9ee6c76f89042752cb2d83486a7d6e48e8f764", |                 "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5", | ||||||
|                 "sha256:e88b8f6d901477c41559ba540beeb5a671e14cd29ebd5683903572f4b40a9807", |                 "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e", | ||||||
|                 "sha256:f1d70bef3d16fdc897ee290d7d20da3cbe4e26349f62e8a0274e7a3f4ce7a905" |                 "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4" | ||||||
|             ], |             ], | ||||||
|             "index": "pypi", |             "index": "pypi", | ||||||
|             "markers": "python_version >= '3.7'", |             "markers": "python_version >= '3.7'", | ||||||
|             "version": "==0.8.6" |             "version": "==0.9.2" | ||||||
|         }, |         }, | ||||||
|         "scipy": { |         "scipy": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
| @@ -4113,11 +4100,11 @@ | |||||||
|         }, |         }, | ||||||
|         "urllib3": { |         "urllib3": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|                 "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", |                 "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", | ||||||
|                 "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" |                 "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d" | ||||||
|             ], |             ], | ||||||
|             "markers": "python_version >= '3.8'", |             "markers": "python_version >= '3.9'", | ||||||
|             "version": "==2.2.3" |             "version": "==2.3.0" | ||||||
|         }, |         }, | ||||||
|         "virtualenv": { |         "virtualenv": { | ||||||
|             "hashes": [ |             "hashes": [ | ||||||
|   | |||||||
| @@ -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 | -   `{{ tag_list }}`: A comma separated list of all tags assigned to the | ||||||
|     document. |     document. | ||||||
| -   `{{ title }}`: The title of 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 | -   `{{ created_year }}`: Year created only, formatted as the year with | ||||||
|     century. |     century. | ||||||
| -   `{{ created_year_short }}`: Year created only, formatted as the year | -   `{{ 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 }} | /{{ 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: | To use custom fields: | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,64 @@ | |||||||
| # Changelog | # 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 | ## paperless-ngx 2.14.3 | ||||||
|  |  | ||||||
| ### Bug Fixes | ### Bug Fixes | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| # Usage Overview | # Usage Overview | ||||||
|  |  | ||||||
| Paperless is an application that manages your personal documents. With | Paperless-ngx 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)), | 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 | ||||||
| paperless transforms your unwieldy physical document binders into a searchable archive | physical documents into a searchable archive and provides many utilities | ||||||
| and provides many utilities for finding and managing your documents. | for finding and managing your documents. | ||||||
|  |  | ||||||
| ## Terms and definitions | ## Terms and definitions | ||||||
|  |  | ||||||
| @@ -12,10 +12,10 @@ documents: | |||||||
|  |  | ||||||
| -   The _consumer_ watches a specified folder and adds all documents in | -   The _consumer_ watches a specified folder and adds all documents in | ||||||
|     that folder to paperless. |     that folder to paperless. | ||||||
| -   The _web server_ provides a UI that you use to manage and search for | -   The _web server_ (web UI) provides a UI that you use to manage and | ||||||
|     your scanned documents. |     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 | -   A _Document_ is a piece of paper that sometimes contains valuable | ||||||
|     information. |     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 | -   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 |     document. This text is fed into the search engine and is used for | ||||||
|     matching tags, correspondents and document types. |     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 | ## 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 | ||||||
|  |  | ||||||
| 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 | #### Admin Status | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import os | |||||||
| # See https://docs.gunicorn.org/en/stable/settings.html for | # See https://docs.gunicorn.org/en/stable/settings.html for | ||||||
| # explanations of settings | # 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)) | workers = int(os.getenv("PAPERLESS_WEBSERVER_WORKERS", 1)) | ||||||
| worker_class = "paperless.workers.ConfigurableWorker" | worker_class = "paperless.workers.ConfigurableWorker" | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								src-ui/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -18107,10 +18107,11 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "node_modules/undici": { |     "node_modules/undici": { | ||||||
|       "version": "5.28.4", |       "version": "5.28.5", | ||||||
|       "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", |       "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", | ||||||
|       "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", |       "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|  |       "license": "MIT", | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@fastify/busboy": "^2.0.0" |         "@fastify/busboy": "^2.0.0" | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -160,4 +160,23 @@ describe('UserEditDialogComponent', () => { | |||||||
|     }) |     }) | ||||||
|     expect(component.currentUserIsSuperUser).toBeTruthy() |     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() | ||||||
|  |   }) | ||||||
| }) | }) | ||||||
|   | |||||||
| @@ -60,6 +60,11 @@ export class UserEditDialogComponent | |||||||
|   ngOnInit(): void { |   ngOnInit(): void { | ||||||
|     super.ngOnInit() |     super.ngOnInit() | ||||||
|     this.onToggleSuperUser() |     this.onToggleSuperUser() | ||||||
|  |     if (!this.currentUserIsSuperUser) { | ||||||
|  |       this.objectForm.get('is_superuser').disable() | ||||||
|  |     } else { | ||||||
|  |       this.objectForm.get('is_superuser').enable() | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   getCreateTitle() { |   getCreateTitle() { | ||||||
|   | |||||||
| @@ -17,7 +17,7 @@ | |||||||
|           (change)="onChange(value)"> |           (change)="onChange(value)"> | ||||||
|  |  | ||||||
|           <ng-template ng-label-tmp let-item="item"> |           <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> |               <i-bs name="x" style="margin-inline-end: 1px;"></i-bs> | ||||||
|               @if (item.id && tags) { |               @if (item.id && tags) { | ||||||
|                 <pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag> |                 <pngx-tag style="background-color: none;" [tag]="getTag(item.id)"></pngx-tag> | ||||||
|   | |||||||
| @@ -1,10 +1,14 @@ | |||||||
| <a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle" | @if (!previewOnly) { | ||||||
|   [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body" |   <a [href]="link ?? previewUrl" class="{{linkClasses}}" [target]="linkTarget" [title]="linkTitle" | ||||||
|   autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> |     [ngbPopover]="previewContent" [popoverTitle]="document.title | documentTitle" container="body" | ||||||
|   <ng-content></ng-content> |     autoClose="true" [popoverClass]="popoverClass" (mouseenter)="mouseEnterPreview()" (mouseleave)="mouseLeavePreview()" #popover="ngbPopover"> | ||||||
| </a> |     <ng-content></ng-content> | ||||||
|  |   </a> | ||||||
|  | } @else { | ||||||
|  |   <ng-container [ngTemplateOutlet]="previewContent" [ngTemplateOutletContext]="{ $implicit: document }"></ng-container> | ||||||
|  | } | ||||||
| <ng-template #previewContent> | <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) { |     @if (error) { | ||||||
|       <div class="w-100 h-100 position-relative"> |       <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> |         <p class="fst-italic position-absolute top-50 start-50 translate-middle" i18n>Error loading preview</p> | ||||||
|   | |||||||
| @@ -4,6 +4,16 @@ | |||||||
|     overflow-y: scroll; |     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 { | ::ng-deep .popover.popover-preview { | ||||||
|     max-width: 32rem; |     max-width: 32rem; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import { NgTemplateOutlet } from '@angular/common' | ||||||
| import { HttpClient } from '@angular/common/http' | import { HttpClient } from '@angular/common/http' | ||||||
| import { Component, Input, OnDestroy, ViewChild } from '@angular/core' | import { Component, Input, OnDestroy, ViewChild } from '@angular/core' | ||||||
| import { NgbPopover, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap' | 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'], |   styleUrls: ['./preview-popup.component.scss'], | ||||||
|   imports: [ |   imports: [ | ||||||
|     NgbPopoverModule, |     NgbPopoverModule, | ||||||
|  |     NgTemplateOutlet, | ||||||
|     DocumentTitlePipe, |     DocumentTitlePipe, | ||||||
|     PdfViewerModule, |     PdfViewerModule, | ||||||
|     SafeUrlPipe, |     SafeUrlPipe, | ||||||
| @@ -47,6 +49,9 @@ export class PreviewPopupComponent implements OnDestroy { | |||||||
|   @Input() |   @Input() | ||||||
|   linkTitle: string = $localize`Open preview` |   linkTitle: string = $localize`Open preview` | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   previewOnly: boolean = false | ||||||
|  |  | ||||||
|   unsubscribeNotifier: Subject<any> = new Subject() |   unsubscribeNotifier: Subject<any> = new Subject() | ||||||
|  |  | ||||||
|   error = false |   error = false | ||||||
| @@ -91,6 +96,8 @@ export class PreviewPopupComponent implements OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   init() { |   init() { | ||||||
|  |     this.error = false | ||||||
|  |     this.requiresPassword = false | ||||||
|     if (this.document.mime_type?.includes('text')) { |     if (this.document.mime_type?.includes('text')) { | ||||||
|       this.http |       this.http | ||||||
|         .get(this.previewURL, { responseType: 'text' }) |         .get(this.previewURL, { responseType: 'text' }) | ||||||
| @@ -119,6 +126,7 @@ export class PreviewPopupComponent implements OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   mouseEnterPreview() { |   mouseEnterPreview() { | ||||||
|  |     if (this.previewOnly) return | ||||||
|     this.mouseOnPreview = true |     this.mouseOnPreview = true | ||||||
|     if (!this.popover.isOpen()) { |     if (!this.popover.isOpen()) { | ||||||
|       // we're going to open but hide to pre-load content during hover delay |       // we're going to open but hide to pre-load content during hover delay | ||||||
| @@ -136,10 +144,12 @@ export class PreviewPopupComponent implements OnDestroy { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   mouseLeavePreview() { |   mouseLeavePreview() { | ||||||
|  |     if (this.previewOnly) return | ||||||
|     this.mouseOnPreview = false |     this.mouseOnPreview = false | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public close(immediate: boolean = false) { |   public close(immediate: boolean = false) { | ||||||
|  |     if (this.previewOnly) return | ||||||
|     setTimeout( |     setTimeout( | ||||||
|       () => { |       () => { | ||||||
|         if (!this.mouseOnPreview) this.popover.close() |         if (!this.mouseOnPreview) this.popover.close() | ||||||
|   | |||||||
| @@ -48,7 +48,6 @@ | |||||||
|                   i18n-title |                   i18n-title | ||||||
|                   buttonClasses=" btn-outline-secondary" |                   buttonClasses=" btn-outline-secondary" | ||||||
|                   iconName="arrow-repeat" |                   iconName="arrow-repeat" | ||||||
|                   [disabled]="!hasUsablePassword" |  | ||||||
|                   (confirm)="generateAuthToken()"> |                   (confirm)="generateAuthToken()"> | ||||||
|                 </pngx-confirm-button> |                 </pngx-confirm-button> | ||||||
|               </div> |               </div> | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| a { | a, span { | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|     white-space: normal; |     white-space: normal; | ||||||
|     word-break: break-word; |     word-break: break-word; | ||||||
|     text-align: end; |     text-align: start; | ||||||
| } | } | ||||||
|  |  | ||||||
| .private { | .private { | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ | |||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|   <div class="btn-group flex-fill" role="group"> |   <div class="btn-group flex-fill" role="group"> | ||||||
|     <input type="radio" class="btn-check" [(ngModel)]="list.displayMode" value="table" id="displayModeDetails" name="displayModeDetails"> |     <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"> |     <label for="displayModeDetails" class="btn btn-outline-primary btn-sm"> | ||||||
| @@ -42,6 +43,13 @@ | |||||||
|     </label> |     </label> | ||||||
|   </div> |   </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"> |   <div ngbDropdown class="btn-group flex-fill"> | ||||||
|     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle> |     <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle> | ||||||
|       <i-bs name="arrow-down-up"></i-bs> |       <i-bs name="arrow-down-up"></i-bs> | ||||||
| @@ -105,298 +113,335 @@ | |||||||
|   <pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor> |   <pngx-bulk-editor [hidden]="!isBulkEditing" [disabled]="!isBulkEditing"></pngx-bulk-editor> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|  | <div class="row"> | ||||||
| <ng-template #pagination> |   <div [class.col-lg-6]="list.showPreviewPane" [class.col]="!list.showPreviewPane"> | ||||||
|   <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> |   <ng-template #pagination> | ||||||
|     <div class="d-flex align-items-center"> |     <div class="d-flex flex-wrap gap-3 justify-content-between align-items-center mb-3"> | ||||||
|       @if (list.isReloading) { |       <div class="d-flex align-items-center"> | ||||||
|         <div class="spinner-border spinner-border-sm me-2" role="status"></div> |         @if (list.isReloading) { | ||||||
|         <ng-container i18n>Loading...</ng-container> |           <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> |  | ||||||
|           } @if (isFiltered) { |  | ||||||
|              <span i18n>(filtered)</span> |  | ||||||
|         } |         } | ||||||
|       } |         @if (list.selected.size > 0) { | ||||||
|       @if (!list.isReloading && isFiltered) { |           <span i18n>{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span> | ||||||
|         <button class="btn btn-link py-0" (click)="resetFilters()"> |         } | ||||||
|           <i-bs width="1em" height="1em" name="x"></i-bs><small i18n>Reset filters</small> |         @if (!list.isReloading) { | ||||||
|           </button> |           @if (list.selected.size === 0) { | ||||||
|  |             <span i18n>{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span> | ||||||
|  |             } @if (isFiltered) { | ||||||
|  |                <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> |       </div> | ||||||
|       @if (list.collectionSize) { |     </ng-template> | ||||||
|         <ngb-pagination [pageSize]="list.pageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5" |  | ||||||
|         [rotate]="true" aria-label="Default pagination" size="sm"></ngb-pagination> |     <div tourAnchor="tour.documents"> | ||||||
|       } |       <ng-container *ngTemplateOutlet="pagination"></ng-container> | ||||||
|     </div> |     </div> | ||||||
|   </ng-template> |  | ||||||
|  |  | ||||||
|   <div tourAnchor="tour.documents"> |     @if (list.error ) { | ||||||
|     <ng-container *ngTemplateOutlet="pagination"></ng-container> |       <div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div> | ||||||
|   </div> |     } @else { | ||||||
|  |       @if (list.displayMode === DisplayMode.LARGE_CARDS) { | ||||||
|   @if (list.error ) { |         <div> | ||||||
|     <div class="alert alert-danger" role="alert"><ng-container i18n>Error while loading documents</ng-container>: {{list.error}}</div> |           @for (d of list.documents; track d.id) { | ||||||
|   } @else { |             <pngx-document-card-large | ||||||
|     @if (list.displayMode === DisplayMode.LARGE_CARDS) { |               [selected]="list.isSelected(d)" | ||||||
|       <div> |               (toggleSelected)="toggleSelected(d, $event)" | ||||||
|         @for (d of list.documents; track d.id) { |               (dblClickDocument)="openDocumentDetail(d)" | ||||||
|           <pngx-document-card-large |               [document]="d" | ||||||
|             [selected]="list.isSelected(d)" |               [displayFields]="activeDisplayFields" | ||||||
|             (toggleSelected)="toggleSelected(d, $event)" |               (clickTag)="clickTag($event)" | ||||||
|             (dblClickDocument)="openDocumentDetail(d)" |               (clickCorrespondent)="clickCorrespondent($event)" | ||||||
|             [document]="d" |               (clickDocumentType)="clickDocumentType($event)" | ||||||
|             [displayFields]="activeDisplayFields" |               (clickStoragePath)="clickStoragePath($event)" | ||||||
|             (clickTag)="clickTag($event)" |               (clickMoreLike)="clickMoreLike(d.id)"> | ||||||
|             (clickCorrespondent)="clickCorrespondent($event)" |             </pngx-document-card-large> | ||||||
|             (clickDocumentType)="clickDocumentType($event)" |           } | ||||||
|             (clickStoragePath)="clickStoragePath($event)" |         </div> | ||||||
|             (clickMoreLike)="clickMoreLike(d.id)"> |       } | ||||||
|           </pngx-document-card-large> |       @if (list.displayMode === DisplayMode.TABLE) { | ||||||
|         } |         <div class="table-responsive"> | ||||||
|       </div> |           <table class="table table-sm align-middle border shadow-sm"> | ||||||
|     } |             <thead> | ||||||
|     @if (list.displayMode === DisplayMode.TABLE) { |               <tr> | ||||||
|       <div class="table-responsive"> |                 <th></th> | ||||||
|         <table class="table table-sm align-middle border shadow-sm"> |                 @if (activeDisplayFields.includes(DisplayField.ASN)) { | ||||||
|           <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)) { |  | ||||||
|                   <th class="cursor-pointer" |                   <th class="cursor-pointer" | ||||||
|                     pngxSortable="page_count" |                     pngxSortable="archive_serial_number" | ||||||
|                     title="Sort by number of pages" i18n-title |                     title="Sort by ASN" i18n-title | ||||||
|                     [currentSortField]="list.sortField" |                     [currentSortField]="list.sortField" | ||||||
|                     [currentSortReverse]="list.sortReverse" |                     [currentSortReverse]="list.sortReverse" | ||||||
|                     (sort)="onSort($event)" |                     (sort)="onSort($event)" | ||||||
|                     i18n>Pages</th> |                     i18n>ASN</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> |  | ||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { |                 @if (activeDisplayFields.includes(DisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { | ||||||
|                   <td class=""> |                   <th class="cursor-pointer" | ||||||
|                     @if (d.correspondent) { |                     pngxSortable="correspondent__name" | ||||||
|                       <a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a> |                     title="Sort by correspondent" i18n-title | ||||||
|                     } |                     [currentSortField]="list.sortField" | ||||||
|                   </td> |                     [currentSortReverse]="list.sortReverse" | ||||||
|  |                     (sort)="onSort($event)" | ||||||
|  |                     i18n>Correspondent</th> | ||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) { |                 @if (activeDisplayFields.includes(DisplayField.TITLE)) { | ||||||
|                   <td width="30%"> |                   <th class="cursor-pointer" | ||||||
|                     @if (activeDisplayFields.includes(DisplayField.TITLE)) { |                     pngxSortable="title" | ||||||
|                       <div class="d-inline-block" (mouseleave)="popupPreview.close()"> |                     title="Sort by title" i18n-title | ||||||
|                         <a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a> |                     [currentSortField]="list.sortField" | ||||||
|                         <pngx-preview-popup [document]="d" linkClasses="btn btn-sm btn-link text-secondary" linkTitle="Preview document" (click)="$event.stopPropagation()" i18n-linkTitle #popupPreview> |                     [currentSortReverse]="list.sortReverse" | ||||||
|                           <i-bs name="eye"></i-bs> |                     (sort)="onSort($event)" | ||||||
|                         </pngx-preview-popup> |                     style="min-width: 150px;" | ||||||
|                       </div> |                     i18n>Title</th> | ||||||
|                     } |                 } | ||||||
|                     @if (activeDisplayFields.includes(DisplayField.TAGS)) { |                 @if (activeDisplayFields.includes(DisplayField.TAGS) && !activeDisplayFields.includes(DisplayField.TITLE)) { | ||||||
|                       @for (t of d.tags$ | async; track t) { |                   <th i18n>Tags</th> | ||||||
|                         <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)) { |                 @if (activeDisplayFields.includes(DisplayField.OWNER) && permissionService.currentUserCan(PermissionAction.View, PermissionType.User)) { | ||||||
|                   <td> |                   <th class="cursor-pointer" | ||||||
|                     {{d.owner | username}} |                     pngxSortable="owner" | ||||||
|                   </td> |                     title="Sort by owner" i18n-title | ||||||
|  |                     [currentSortField]="list.sortField" | ||||||
|  |                     [currentSortReverse]="list.sortReverse" | ||||||
|  |                     (sort)="onSort($event)" | ||||||
|  |                     i18n>Owner</th> | ||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) { |                 @if (activeDisplayFields.includes(DisplayField.NOTES) && notesEnabled) { | ||||||
|                   <td class=""> |                   <th class="cursor-pointer" | ||||||
|                     @if (d.notes.length) { |                     pngxSortable="num_notes" | ||||||
|                       <a routerLink="/documents/{{d.id}}/notes" class="btn btn-sm p-0"> |                     title="Sort by notes" i18n-title | ||||||
|                         <span class="badge rounded-pill bg-light border text-primary"> |                     [currentSortField]="list.sortField" | ||||||
|                           <i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs> |                     [currentSortReverse]="list.sortReverse" | ||||||
|                         {{d.notes.length}}</span> |                     (sort)="onSort($event)" | ||||||
|                       </a> |                     i18n>Notes</th> | ||||||
|                     } |  | ||||||
|                   </td> |  | ||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { |                 @if (activeDisplayFields.includes(DisplayField.DOCUMENT_TYPE) && permissionService.currentUserCan(PermissionAction.View, PermissionType.DocumentType)) { | ||||||
|                   <td class=""> |                   <th class="cursor-pointer" | ||||||
|                     @if (d.document_type) { |                     pngxSortable="document_type__name" | ||||||
|                       <a (click)="clickDocumentType(d.document_type);$event.stopPropagation()" title="Filter by document type" i18n-title>{{(d.document_type$ | async)?.name}}</a> |                     title="Sort by document type" i18n-title | ||||||
|                     } |                     [currentSortField]="list.sortField" | ||||||
|                   </td> |                     [currentSortReverse]="list.sortReverse" | ||||||
|  |                     (sort)="onSort($event)" | ||||||
|  |                     i18n>Document type</th> | ||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) { |                 @if (activeDisplayFields.includes(DisplayField.STORAGE_PATH) && permissionService.currentUserCan(PermissionAction.View, PermissionType.StoragePath)) { | ||||||
|                   <td class=""> |                   <th class="cursor-pointer" | ||||||
|                     @if (d.storage_path) { |                     pngxSortable="storage_path__name" | ||||||
|                       <a (click)="clickStoragePath(d.storage_path);$event.stopPropagation()" title="Filter by storage path" i18n-title>{{(d.storage_path$ | async)?.name}}</a> |                     title="Sort by storage path" i18n-title | ||||||
|                     } |                     [currentSortField]="list.sortField" | ||||||
|                   </td> |                     [currentSortReverse]="list.sortReverse" | ||||||
|  |                     (sort)="onSort($event)" | ||||||
|  |                     i18n>Storage path</th> | ||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.CREATED)) { |                 @if (activeDisplayFields.includes(DisplayField.CREATED)) { | ||||||
|                   <td> |                   <th class="cursor-pointer" | ||||||
|                     {{d.created_date | customDate}} |                     pngxSortable="created" | ||||||
|                   </td> |                     title="Sort by created date" i18n-title | ||||||
|  |                     [currentSortField]="list.sortField" | ||||||
|  |                     [currentSortReverse]="list.sortReverse" | ||||||
|  |                     (sort)="onSort($event)" | ||||||
|  |                     i18n>Created</th> | ||||||
|                 } |                 } | ||||||
|                 @if (activeDisplayFields.includes(DisplayField.ADDED)) { |                 @if (activeDisplayFields.includes(DisplayField.ADDED)) { | ||||||
|                   <td> |                   <th class="cursor-pointer" | ||||||
|                     {{d.added | customDate}} |                     pngxSortable="added" | ||||||
|                   </td> |                     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 (activeDisplayFields.includes(DisplayField.PAGE_COUNT)) { | ||||||
|                     <td> |                     <th class="cursor-pointer" | ||||||
|                         {{ d.page_count }} |                       pngxSortable="page_count" | ||||||
|                     </td> |                       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)) { |                 @if (activeDisplayFields.includes(DisplayField.SHARED)) { | ||||||
|                   <td> |                   <th i18n> | ||||||
|                     @if (d.is_shared_by_requester) { <ng-container i18n>Yes</ng-container> } @else { <ng-container i18n>No</ng-container> } |                     Shared | ||||||
|                   </td> |                   </th> | ||||||
|                 } |                 } | ||||||
|                 @for (field of activeDisplayCustomFields; track field) { |                 @for (field_id of activeDisplayCustomFields; track field_id) { | ||||||
|                   <td class=""> |                   <th class="cursor-pointer" | ||||||
|                     <pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display> |                     pngxSortable="{{field_id}}" | ||||||
|                   </td> |                     title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title | ||||||
|  |                     [currentSortField]="list.sortField" | ||||||
|  |                     [currentSortReverse]="list.sortReverse" | ||||||
|  |                     (sort)="onSort($event)"> | ||||||
|  |                     {{getDisplayCustomFieldTitle(field_id)}} | ||||||
|  |                   </th> | ||||||
|                 } |                 } | ||||||
|               </tr> |               </tr> | ||||||
|             } |             </thead> | ||||||
|           </tbody> |             <tbody> | ||||||
|         </table> |               @for (d of list.documents; track d.id) { | ||||||
|       </div> |                 <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> | ||||||
|       <div class="row row-cols-paperless-cards"> |   @if (list.showPreviewPane) { | ||||||
|         @for (d of list.documents; track d.id) { |     <div class="col-lg-6"> | ||||||
|           <pngx-document-card-small class="p-0" |       <div class="row"> | ||||||
|             [selected]="list.isSelected(d)" |         <div class="btn-toolbar mb-1 border-bottom align-items-center"> | ||||||
|             (toggleSelected)="toggleSelected(d, $event)" |           <div class="btn-group pb-3"> | ||||||
|             (dblClickDocument)="openDocumentDetail(d)" |             <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Previous" (click)="previousDoc()" [disabled]="list.documents.length === 0 || !hasPrevious"> | ||||||
|             [document]="d" |               <i-bs width="1.2em" height="1.2em" name="arrow-left" class="me-1"></i-bs><ng-container i18n>Previous</ng-container> | ||||||
|             (clickTag)="clickTag($event)" |             </button> | ||||||
|             [displayFields]="activeDisplayFields" |             <button type="button" class="btn btn-sm btn-outline-secondary"  i18n-title title="Next" (click)="nextDoc()" [disabled]="list.documents.length === 0 || !hasNext"> | ||||||
|             (clickCorrespondent)="clickCorrespondent($event)" |               <ng-container i18n>Next</ng-container><i-bs width="1.2em" height="1.2em" name="arrow-right" class="ms-1"></i-bs> | ||||||
|             (clickStoragePath)="clickStoragePath($event)" |             </button> | ||||||
|             (clickDocumentType)="clickDocumentType($event)"> |           </div> | ||||||
|           </pngx-document-card-small> |           <div class="input-group pb-3 ms-auto"> | ||||||
|         } |             <h5 class="mb-0"> | ||||||
|  |               {{list.firstSelectedDocument?.title}} | ||||||
|  |             </h5> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|       </div> |       </div> | ||||||
|     } |       <div class="row"> | ||||||
|     @if (list.documents?.length > 15) { |         <div class="col preview-pane"> | ||||||
|       <div class="mt-3"> |           @if (list.selected.size > 0) { | ||||||
|         <ng-container *ngTemplateOutlet="pagination"></ng-container> |             <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> | ||||||
|   } |   } | ||||||
|  | </div> | ||||||
|   | |||||||
| @@ -80,3 +80,9 @@ a { | |||||||
| pngx-page-header .dropdown-menu { | pngx-page-header .dropdown-menu { | ||||||
|   --bs-dropdown-min-width: 12em; |   --bs-dropdown-min-width: 12em; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .preview-pane { | ||||||
|  |   height: 60rem; | ||||||
|  |   top: 70px; | ||||||
|  |   position: sticky; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -326,24 +326,36 @@ export class DocumentListComponent | |||||||
|     this.hotKeyService |     this.hotKeyService | ||||||
|       .addShortcut({ |       .addShortcut({ | ||||||
|         keys: 'control.arrowleft', |         keys: 'control.arrowleft', | ||||||
|         description: $localize`Previous page`, |         description: $localize`Previous page / document`, | ||||||
|       }) |       }) | ||||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|       .subscribe(() => { |       .subscribe(() => { | ||||||
|         if (this.list.currentPage > 1) { |         if (this.list.showPreviewPane) { | ||||||
|           this.list.currentPage-- |           if (this.hasPrevious) { | ||||||
|  |             this.previousDoc() | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           if (this.list.currentPage > 1) { | ||||||
|  |             this.list.currentPage-- | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|  |  | ||||||
|     this.hotKeyService |     this.hotKeyService | ||||||
|       .addShortcut({ |       .addShortcut({ | ||||||
|         keys: 'control.arrowright', |         keys: 'control.arrowright', | ||||||
|         description: $localize`Next page`, |         description: $localize`Next page / document`, | ||||||
|       }) |       }) | ||||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) |       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||||
|       .subscribe(() => { |       .subscribe(() => { | ||||||
|         if (this.list.currentPage < this.list.getLastPage()) { |         if (this.list.showPreviewPane) { | ||||||
|           this.list.currentPage++ |           if (this.hasNext) { | ||||||
|  |             this.nextDoc() | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           if (this.list.currentPage < this.list.getLastPage()) { | ||||||
|  |             this.list.currentPage++ | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       }) |       }) | ||||||
|   } |   } | ||||||
| @@ -473,4 +485,45 @@ export class DocumentListComponent | |||||||
|   resetFilters() { |   resetFilters() { | ||||||
|     this.filterEditor.resetSelected() |     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) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -45,7 +45,7 @@ | |||||||
|       <li class="list-group-item"> |       <li class="list-group-item"> | ||||||
|         <div class="row fade" [class.show]="showAccounts"> |         <div class="row fade" [class.show]="showAccounts"> | ||||||
|           <div class="col d-flex align-items-center"> |           <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) { |               {{account.name}}@switch (account.account_type) { | ||||||
|                 @case (MailAccountType.IMAP) {<i-bs name="envelope-at-fill" class="ms-2"></i-bs>} |                 @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>} |                 @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> |                   <i-bs name="three-dots-vertical"></i-bs> | ||||||
|                 </button> |                 </button> | ||||||
|                 <div ngbDropdownMenu aria-labelledby="actionsMenuMobile"> |                 <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)="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)="deleteMailAccount(account)" [disabled]="!userIsOwner(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)="processAccount(account)" [disabled]="!userIsOwner(account)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailAccount }" ngbDropdownItem i18n>Process Mail</button> | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
| @@ -82,7 +82,7 @@ | |||||||
|                 </button> |                 </button> | ||||||
|               </div> |               </div> | ||||||
|               <div class="btn-group"> |               <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> <ng-container i18n>Process Mail</ng-container> |                   <i-bs width="1em" height="1em" name="arrow-clockwise"></i-bs> <ng-container i18n>Process Mail</ng-container> | ||||||
|                 </button> |                 </button> | ||||||
|               </div> |               </div> | ||||||
| @@ -126,7 +126,7 @@ | |||||||
|     @for (rule of mailRules; track rule) { |     @for (rule of mailRules; track rule) { | ||||||
|       <li class="list-group-item"> |       <li class="list-group-item"> | ||||||
|         <div class="row fade" [class.show]="showRules"> |         <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 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">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div> | ||||||
|           <div class="col d-flex align-items-center d-none d-sm-flex"> |           <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> |                   <i-bs name="three-dots-vertical"></i-bs> | ||||||
|                 </button> |                 </button> | ||||||
|                 <div ngbDropdownMenu aria-labelledby="actionsMenuMobile"> |                 <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)="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> |                   <button (click)="copyMailRule(rule)" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.MailRule }" ngbDropdownItem i18n>Copy</button> | ||||||
|                 </div> |                 </div> | ||||||
|               </div> |               </div> | ||||||
|   | |||||||
| @@ -2,7 +2,9 @@ import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http' | |||||||
| import { provideHttpClientTesting } from '@angular/common/http/testing' | import { provideHttpClientTesting } from '@angular/common/http/testing' | ||||||
| import { ComponentFixture, TestBed } from '@angular/core/testing' | import { ComponentFixture, TestBed } from '@angular/core/testing' | ||||||
| import { By } from '@angular/platform-browser' | import { By } from '@angular/platform-browser' | ||||||
|  | import { RouterModule } from '@angular/router' | ||||||
| import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' | import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' | ||||||
|  | import { routes } from 'src/app/app-routing.module' | ||||||
| import { LogoComponent } from '../common/logo/logo.component' | import { LogoComponent } from '../common/logo/logo.component' | ||||||
| import { NotFoundComponent } from './not-found.component' | import { NotFoundComponent } from './not-found.component' | ||||||
|  |  | ||||||
| @@ -16,6 +18,7 @@ describe('NotFoundComponent', () => { | |||||||
|         NgxBootstrapIconsModule.pick(allIcons), |         NgxBootstrapIconsModule.pick(allIcons), | ||||||
|         NotFoundComponent, |         NotFoundComponent, | ||||||
|         LogoComponent, |         LogoComponent, | ||||||
|  |         RouterModule.forRoot(routes), | ||||||
|       ], |       ], | ||||||
|       providers: [ |       providers: [ | ||||||
|         provideHttpClient(withInterceptorsFromDi()), |         provideHttpClient(withInterceptorsFromDi()), | ||||||
|   | |||||||
| @@ -1,4 +1,5 @@ | |||||||
| import { Component } from '@angular/core' | import { Component } from '@angular/core' | ||||||
|  | import { RouterModule } from '@angular/router' | ||||||
| import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' | ||||||
| import { LogoComponent } from '../common/logo/logo.component' | import { LogoComponent } from '../common/logo/logo.component' | ||||||
|  |  | ||||||
| @@ -6,7 +7,7 @@ import { LogoComponent } from '../common/logo/logo.component' | |||||||
|   selector: 'pngx-not-found', |   selector: 'pngx-not-found', | ||||||
|   templateUrl: './not-found.component.html', |   templateUrl: './not-found.component.html', | ||||||
|   styleUrls: ['./not-found.component.scss'], |   styleUrls: ['./not-found.component.scss'], | ||||||
|   imports: [LogoComponent, NgxBootstrapIconsModule], |   imports: [LogoComponent, NgxBootstrapIconsModule, RouterModule], | ||||||
| }) | }) | ||||||
| export class NotFoundComponent { | export class NotFoundComponent { | ||||||
|   constructor() {} |   constructor() {} | ||||||
|   | |||||||
| @@ -79,6 +79,11 @@ export interface ListViewState { | |||||||
|    * The fields to display in the document list. |    * The fields to display in the document list. | ||||||
|    */ |    */ | ||||||
|   displayFields?: DisplayField[] |   displayFields?: DisplayField[] | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Whether the preview pane is shown. | ||||||
|  |    */ | ||||||
|  |   showPreviewPane?: boolean | ||||||
| } | } | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -165,6 +170,7 @@ export class DocumentListViewService { | |||||||
|       sortReverse: true, |       sortReverse: true, | ||||||
|       filterRules: [], |       filterRules: [], | ||||||
|       selected: new Set<number>(), |       selected: new Set<number>(), | ||||||
|  |       showPreviewPane: false, | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -451,6 +457,15 @@ export class DocumentListViewService { | |||||||
|     this.saveDocumentListView() |     this.saveDocumentListView() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get showPreviewPane(): boolean { | ||||||
|  |     return this.activeListViewState.showPreviewPane | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set showPreviewPane(show: boolean) { | ||||||
|  |     this.activeListViewState.showPreviewPane = show | ||||||
|  |     this.saveDocumentListView() | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private saveDocumentListView() { |   private saveDocumentListView() { | ||||||
|     if (this._activeSavedViewId == null) { |     if (this._activeSavedViewId == null) { | ||||||
|       let savedState: ListViewState = { |       let savedState: ListViewState = { | ||||||
| @@ -461,6 +476,7 @@ export class DocumentListViewService { | |||||||
|         sortReverse: this.activeListViewState.sortReverse, |         sortReverse: this.activeListViewState.sortReverse, | ||||||
|         displayMode: this.activeListViewState.displayMode, |         displayMode: this.activeListViewState.displayMode, | ||||||
|         displayFields: this.activeListViewState.displayFields, |         displayFields: this.activeListViewState.displayFields, | ||||||
|  |         showPreviewPane: this.activeListViewState.showPreviewPane, | ||||||
|       } |       } | ||||||
|       localStorage.setItem( |       localStorage.setItem( | ||||||
|         DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, |         DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, | ||||||
| @@ -626,4 +642,8 @@ export class DocumentListViewService { | |||||||
|   documentIndexInCurrentView(documentID: number): number { |   documentIndexInCurrentView(documentID: number): number { | ||||||
|     return this.documents.map((d) => d.id).indexOf(documentID) |     return this.documents.map((d) => d.id).indexOf(documentID) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   get firstSelectedDocument(): Document { | ||||||
|  |     return this.documents.find((d) => this.selected.has(d.id)) | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -5,7 +5,7 @@ export const environment = { | |||||||
|   apiBaseUrl: document.baseURI + 'api/', |   apiBaseUrl: document.baseURI + 'api/', | ||||||
|   apiVersion: '6', |   apiVersion: '6', | ||||||
|   appTitle: 'Paperless-ngx', |   appTitle: 'Paperless-ngx', | ||||||
|   version: '2.14.4', |   version: '2.14.5', | ||||||
|   webSocketHost: window.location.host, |   webSocketHost: window.location.host, | ||||||
|   webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', |   webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', | ||||||
|   webSocketBaseUrl: base_url.pathname + 'ws/', |   webSocketBaseUrl: base_url.pathname + 'ws/', | ||||||
|   | |||||||
| @@ -8381,7 +8381,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> |           <context context-type="sourcefile">src/app/components/document-list/document-list.component.ts</context> | ||||||
|           <context context-type="linenumber">285</context> |           <context context-type="linenumber">285</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Ripristina filtri / selezione</target> |         <target state="translated">Azzera filtri / selezione</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4135055128446167640" datatype="html"> |       <trans-unit id="4135055128446167640" datatype="html"> | ||||||
|         <source>Open first [selected] document</source> |         <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="sourcefile">src/app/components/manage/saved-views/saved-views.component.html</context> | ||||||
|           <context context-type="linenumber">4</context> |           <context context-type="linenumber">4</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="6338800642797811873" datatype="html"> |       <trans-unit id="6338800642797811873" datatype="html"> | ||||||
|         <source>Documents page size</source> |         <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="sourcefile">src/app/components/manage/saved-views/saved-views.component.ts</context> | ||||||
|           <context context-type="linenumber">163</context> |           <context context-type="linenumber">163</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="5101757640976222639" datatype="html"> |       <trans-unit id="5101757640976222639" datatype="html"> | ||||||
|         <source>storage path</source> |         <source>storage path</source> | ||||||
|   | |||||||
| @@ -452,7 +452,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/app.component.ts</context> |           <context context-type="sourcefile">src/app/app.component.ts</context> | ||||||
|           <context context-type="linenumber">183</context> |           <context context-type="linenumber">183</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="7495498057594070122" datatype="html"> |       <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> |         <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="sourcefile">src/app/app.component.ts</context> | ||||||
|           <context context-type="linenumber">238</context> |           <context context-type="linenumber">238</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="7172877665285340082" datatype="html"> |       <trans-unit id="7172877665285340082" datatype="html"> | ||||||
|         <source>Thank you! 🙏</source> |         <source>Thank you! 🙏</source> | ||||||
| @@ -572,7 +572,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/config/config.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/config/config.component.html</context> | ||||||
|           <context context-type="linenumber">25</context> |           <context context-type="linenumber">25</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="2180291763949669799" datatype="html"> |       <trans-unit id="2180291763949669799" datatype="html"> | ||||||
|         <source>Enable</source> |         <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="sourcefile">src/app/components/common/permissions-select/permissions-select.component.html</context> | ||||||
|           <context context-type="linenumber">4</context> |           <context context-type="linenumber">4</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">What's this?</target> |         <target state="translated">Hva er dette?</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6226301160429720843" datatype="html"> |       <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> |         <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="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> | ||||||
|           <context context-type="linenumber">16</context> |           <context context-type="linenumber">16</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Filter by</target> |         <target state="translated">Filtrer etter</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8953033926734869941" datatype="html"> |       <trans-unit id="8953033926734869941" datatype="html"> | ||||||
|         <source>Name</source> |         <source>Name</source> | ||||||
| @@ -1968,7 +1968,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">157</context> |           <context context-type="linenumber">157</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Avvis</target> |         <target state="translated">Fjern varsel</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2134950584701094962" datatype="html"> |       <trans-unit id="2134950584701094962" datatype="html"> | ||||||
|         <source>Open Document</source> |         <source>Open Document</source> | ||||||
| @@ -2016,7 +2016,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> | ||||||
|           <context context-type="linenumber">164,166</context> |           <context context-type="linenumber">164,166</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Started<x id="START_BLOCK_IF" equiv-text="@if (tasksService.startedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></target> |         <target state="translated">Startet<x id="START_BLOCK_IF" equiv-text="@if (tasksService.startedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.startedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2341807459308874922" datatype="html"> |       <trans-unit id="2341807459308874922" datatype="html"> | ||||||
|         <source>Queued<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></source> |         <source>Queued<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><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="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> | ||||||
|           <context context-type="linenumber">172,174</context> |           <context context-type="linenumber">172,174</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Queued<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></target> |         <target state="translated">I kø:<x id="START_BLOCK_IF" equiv-text="@if (tasksService.queuedFileTasks.length > 0) {"/><x id="START_TAG_SPAN" ctype="x-span" equiv-text="<span class="badge bg-secondary ms-2">"/><x id="INTERPOLATION" equiv-text="{{tasksService.queuedFileTasks.length}}"/><x id="CLOSE_TAG_SPAN" ctype="x-span" equiv-text="</span>"/><x id="CLOSE_BLOCK_IF" equiv-text="}"/></target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="2525230676386818985" datatype="html"> |       <trans-unit id="2525230676386818985" datatype="html"> | ||||||
|         <source>Result</source> |         <source>Result</source> | ||||||
| @@ -2032,7 +2032,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">45</context> |           <context context-type="linenumber">45</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Result</target> |         <target state="translated">Resultat</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5404910960991552159" datatype="html"> |       <trans-unit id="5404910960991552159" datatype="html"> | ||||||
|         <source>Dismiss selected</source> |         <source>Dismiss selected</source> | ||||||
| @@ -2040,7 +2040,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">104</context> |           <context context-type="linenumber">104</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Avvis valgte</target> |         <target state="translated">Fjern valgte varsel</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8829078752502782653" datatype="html"> |       <trans-unit id="8829078752502782653" datatype="html"> | ||||||
|         <source>Dismiss all</source> |         <source>Dismiss all</source> | ||||||
| @@ -2048,7 +2048,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">105</context> |           <context context-type="linenumber">105</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Avvis alle</target> |         <target state="translated">Kvitter ut alle beskjeder</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1323591410517879795" datatype="html"> |       <trans-unit id="1323591410517879795" datatype="html"> | ||||||
|         <source>Confirm Dismiss All</source> |         <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="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">154</context> |           <context context-type="linenumber">154</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="translated">Bekreft avvisning av alle</target> |         <target state="translated">Bekreft kvittering av alle</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="4157200209636243740" datatype="html"> |       <trans-unit id="4157200209636243740" datatype="html"> | ||||||
|         <source>Dismiss all <x id="PH" equiv-text="tasks.size"/> tasks?</source> |         <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="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">155</context> |           <context context-type="linenumber">155</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="9011556615675272238" datatype="html"> |       <trans-unit id="9011556615675272238" datatype="html"> | ||||||
|         <source>queued</source> |         <source>queued</source> | ||||||
| @@ -2120,7 +2120,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> | ||||||
|           <context context-type="linenumber">4</context> |           <context context-type="linenumber">4</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="3186604097120837257" datatype="html"> |       <trans-unit id="3186604097120837257" datatype="html"> | ||||||
|         <source>Restore selected</source> |         <source>Restore selected</source> | ||||||
| @@ -2328,7 +2328,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> | ||||||
|           <context context-type="linenumber">94</context> |           <context context-type="linenumber">94</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="9021887951960049161" datatype="html"> |       <trans-unit id="9021887951960049161" datatype="html"> | ||||||
|         <source>Confirm delete</source> |         <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="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 context-type="linenumber">37</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Use locale</target> |         <target state="translated">Bruk nasjonale innstillinger</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="528950215505228201" datatype="html"> |       <trans-unit id="528950215505228201" datatype="html"> | ||||||
|         <source>Create new custom field</source> |         <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="sourcefile">src/app/components/dashboard/dashboard.component.ts</context> | ||||||
|           <context context-type="linenumber">57</context> |           <context context-type="linenumber">57</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="2901300640157872718" datatype="html"> |       <trans-unit id="2901300640157872718" datatype="html"> | ||||||
|         <source>Welcome to <x id="PH" equiv-text="environment.appTitle"/></source> |         <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="sourcefile">src/app/components/dashboard/widgets/upload-file-widget/upload-file-widget.component.html</context> | ||||||
|           <context context-type="linenumber">4</context> |           <context context-type="linenumber">4</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Drop documents anywhere or</target> |         <target state="translated">Slipp dokumenter her, eller</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8133800334834354642" datatype="html"> |       <trans-unit id="8133800334834354642" datatype="html"> | ||||||
|         <source>Browse files</source> |         <source>Browse files</source> | ||||||
| @@ -6651,7 +6651,7 @@ | |||||||
|           <context context-type="linenumber">20</context> |           <context context-type="linenumber">20</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <note priority="1" from="description">This button dismisses all status messages about processed documents on the dashboard (failed and successful)</note> |         <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> | ||||||
|       <trans-unit id="2330646618997399019" datatype="html"> |       <trans-unit id="2330646618997399019" datatype="html"> | ||||||
|         <source>{VAR_PLURAL, plural, =1 {One more document} other {<x id="INTERPOLATION"/> more documents}}</source> |         <source>{VAR_PLURAL, plural, =1 {One more document} other {<x id="INTERPOLATION"/> more documents}}</source> | ||||||
|   | |||||||
| @@ -1724,7 +1724,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context> | ||||||
|           <context context-type="linenumber">16</context> |           <context context-type="linenumber">16</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Filter by</target> |         <target state="translated">Фильтровать по</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8953033926734869941" datatype="html" approved="yes"> |       <trans-unit id="8953033926734869941" datatype="html" approved="yes"> | ||||||
|         <source>Name</source> |         <source>Name</source> | ||||||
| @@ -2032,7 +2032,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context> | ||||||
|           <context context-type="linenumber">45</context> |           <context context-type="linenumber">45</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Result</target> |         <target state="translated">Результат</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5404910960991552159" datatype="html"> |       <trans-unit id="5404910960991552159" datatype="html"> | ||||||
|         <source>Dismiss selected</source> |         <source>Dismiss selected</source> | ||||||
| @@ -2152,7 +2152,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> |           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context> | ||||||
|           <context context-type="linenumber">36</context> |           <context context-type="linenumber">36</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Remaining</target> |         <target state="translated">Осталось</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7494361412465596264" datatype="html"> |       <trans-unit id="7494361412465596264" datatype="html"> | ||||||
|         <source><x id="INTERPOLATION" equiv-text="{{ getDaysRemaining(document) }}"/> days</source> |         <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="sourcefile">src/app/components/admin/trash/trash.component.ts</context> | ||||||
|           <context context-type="linenumber">119</context> |           <context context-type="linenumber">119</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Document(s) deleted</target> |         <target state="translated">Документ(ы) удален(ы)</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="6962724852893361467" datatype="html"> |       <trans-unit id="6962724852893361467" datatype="html"> | ||||||
|         <source>Error deleting document(s)</source> |         <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="sourcefile">src/app/components/admin/trash/trash.component.ts</context> | ||||||
|           <context context-type="linenumber">126</context> |           <context context-type="linenumber">126</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Error deleting document(s)</target> |         <target state="translated">Ошибка при удалении документам(ов)</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7534569062269274401" datatype="html"> |       <trans-unit id="7534569062269274401" datatype="html"> | ||||||
|         <source>Document restored</source> |         <source>Document restored</source> | ||||||
| @@ -2472,7 +2472,7 @@ | |||||||
|           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> |           <context context-type="sourcefile">src/app/components/admin/trash/trash.component.ts</context> | ||||||
|           <context context-type="linenumber">159</context> |           <context context-type="linenumber">159</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Document(s) restored</target> |         <target state="translated">Документ(ы) восстановлены</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8405416976953346141" datatype="html"> |       <trans-unit id="8405416976953346141" datatype="html"> | ||||||
|         <source>Error restoring document(s)</source> |         <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="sourcefile">src/app/components/admin/trash/trash.component.ts</context> | ||||||
|           <context context-type="linenumber">165</context> |           <context context-type="linenumber">165</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Error restoring document(s)</target> |         <target state="translated">Ошибка при восстановлении документа(ов)</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8119815638230251386" datatype="html"> |       <trans-unit id="8119815638230251386" datatype="html"> | ||||||
|         <source>Users & Groups</source> |         <source>Users & 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="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> | ||||||
|           <context context-type="linenumber">62</context> |           <context context-type="linenumber">62</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Filter documents</target> |         <target state="translated">Отфильтровать документы</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="3099741642167775297" datatype="html" approved="yes"> |       <trans-unit id="3099741642167775297" datatype="html" approved="yes"> | ||||||
|         <source>Download</source> |         <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="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> | ||||||
|           <context context-type="linenumber">90</context> |           <context context-type="linenumber">90</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Documents</target> |         <target state="translated">Документы</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="searchResults.saved_views" datatype="html"> |       <trans-unit id="searchResults.saved_views" datatype="html"> | ||||||
|         <source>Saved Views</source> |         <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="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context> | ||||||
|           <context context-type="linenumber">103</context> |           <context context-type="linenumber">103</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Tags</target> |         <target state="translated">Теги</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="searchResults.correspondents" datatype="html"> |       <trans-unit id="searchResults.correspondents" datatype="html"> | ||||||
|         <source>Correspondents</source> |         <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="sourcefile">src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">32</context> |           <context context-type="linenumber">32</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Delete original documents after successful merge</target> |         <target state="translated">Удалить оригиналы после успешного объединения</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5138283234724909648" datatype="html"> |       <trans-unit id="5138283234724909648" datatype="html"> | ||||||
|         <source>Note that only PDFs will be included.</source> |         <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="sourcefile">src/app/components/common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">34</context> |           <context context-type="linenumber">34</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Note that only PDFs will be included.</target> |         <target state="translated">Только PDF файлы могут быть добавлены.</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8157388568390631653" datatype="html"> |       <trans-unit id="8157388568390631653" datatype="html"> | ||||||
|         <source>Note that only PDFs will be rotated.</source> |         <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="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 context-type="linenumber">46</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Include only files matching</target> |         <target state="translated">Включить только соответствующие файлы</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="7233407036155150477" datatype="html"> |       <trans-unit id="7233407036155150477" datatype="html"> | ||||||
|         <source>Optional. Wildcards e.g. *.pdf or *invoice* allowed. Can be comma-separated list. Case insensitive.</source> |         <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="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 context-type="linenumber">47</context> | ||||||
|         </context-group> |         </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> | ||||||
|       <trans-unit id="1546332577833742677" datatype="html"> |       <trans-unit id="1546332577833742677" datatype="html"> | ||||||
|         <source>Exclude files matching</source> |         <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="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 context-type="linenumber">47</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Exclude files matching</target> |         <target state="translated">Исключить соответствующие файлы</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="9216117865911519658" datatype="html"> |       <trans-unit id="9216117865911519658" datatype="html"> | ||||||
|         <source>Action</source> |         <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="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 context-type="linenumber">53</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Only performed if the mail is processed.</target> |         <target state="translated">Выполнять только если письмо обработано.</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="1261794314435932203" datatype="html"> |       <trans-unit id="1261794314435932203" datatype="html"> | ||||||
|         <source>Action parameter</source> |         <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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">143</context> |           <context context-type="linenumber">143</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Repeat the trigger every n days.</target> |         <target state="translated">Повторять триггер каждые n дней.</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="8727727835543352574" datatype="html"> |       <trans-unit id="8727727835543352574" datatype="html"> | ||||||
|         <source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="<em>"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="</em>"/> filters specified below.</source> |         <source>Trigger for documents that match <x id="START_EMPHASISED_TEXT" ctype="x-em" equiv-text="<em>"/>all<x id="CLOSE_EMPHASISED_TEXT" ctype="x-em" equiv-text="</em>"/> 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="sourcefile">src/app/components/common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component.html</context> | ||||||
|           <context context-type="linenumber">169</context> |           <context context-type="linenumber">169</context> | ||||||
|         </context-group> |         </context-group> | ||||||
|         <target state="needs-translation">Has any of tags</target> |         <target state="translated">Содержит любой из тегов</target> | ||||||
|       </trans-unit> |       </trans-unit> | ||||||
|       <trans-unit id="5281365940563983618" datatype="html"> |       <trans-unit id="5281365940563983618" datatype="html"> | ||||||
|         <source>Has correspondent</source> |         <source>Has correspondent</source> | ||||||
|   | |||||||
| @@ -125,6 +125,7 @@ import { | |||||||
|   trash, |   trash, | ||||||
|   uiRadios, |   uiRadios, | ||||||
|   upcScan, |   upcScan, | ||||||
|  |   windowSplit, | ||||||
|   windowStack, |   windowStack, | ||||||
|   x, |   x, | ||||||
|   xCircle, |   xCircle, | ||||||
| @@ -323,6 +324,7 @@ const icons = { | |||||||
|   trash, |   trash, | ||||||
|   uiRadios, |   uiRadios, | ||||||
|   upcScan, |   upcScan, | ||||||
|  |   windowSplit, | ||||||
|   windowStack, |   windowStack, | ||||||
|   x, |   x, | ||||||
|   xCircle, |   xCircle, | ||||||
|   | |||||||
| @@ -32,8 +32,7 @@ def changed_password_check(app_configs, **kwargs): | |||||||
|         if not settings.PASSPHRASE: |         if not settings.PASSPHRASE: | ||||||
|             return [ |             return [ | ||||||
|                 Error( |                 Error( | ||||||
|                     "The database contains encrypted documents but no password " |                     "The database contains encrypted documents but no password is set.", | ||||||
|                     "is set.", |  | ||||||
|                 ), |                 ), | ||||||
|             ] |             ] | ||||||
|  |  | ||||||
|   | |||||||
| @@ -170,6 +170,7 @@ class DocumentClassifier: | |||||||
|             ) |             ) | ||||||
|             .select_related("document_type", "correspondent", "storage_path") |             .select_related("document_type", "correspondent", "storage_path") | ||||||
|             .prefetch_related("tags") |             .prefetch_related("tags") | ||||||
|  |             .order_by("pk") | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # No documents exit to train against |         # No documents exit to train against | ||||||
| @@ -199,11 +200,10 @@ class DocumentClassifier: | |||||||
|             hasher.update(y.to_bytes(4, "little", signed=True)) |             hasher.update(y.to_bytes(4, "little", signed=True)) | ||||||
|             labels_correspondent.append(y) |             labels_correspondent.append(y) | ||||||
|  |  | ||||||
|             tags: list[int] = sorted( |             tags: list[int] = list( | ||||||
|                 tag.pk |                 doc.tags.filter(matching_algorithm=MatchingModel.MATCH_AUTO) | ||||||
|                 for tag in doc.tags.filter( |                 .order_by("pk") | ||||||
|                     matching_algorithm=MatchingModel.MATCH_AUTO, |                 .values_list("pk", flat=True), | ||||||
|                 ) |  | ||||||
|             ) |             ) | ||||||
|             for tag in tags: |             for tag in tags: | ||||||
|                 hasher.update(tag.to_bytes(4, "little", signed=True)) |                 hasher.update(tag.to_bytes(4, "little", signed=True)) | ||||||
| @@ -315,8 +315,7 @@ class DocumentClassifier: | |||||||
|         else: |         else: | ||||||
|             self.correspondent_classifier = None |             self.correspondent_classifier = None | ||||||
|             logger.debug( |             logger.debug( | ||||||
|                 "There are no correspondents. Not training correspondent " |                 "There are no correspondents. Not training correspondent classifier.", | ||||||
|                 "classifier.", |  | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         if num_document_types > 0: |         if num_document_types > 0: | ||||||
| @@ -326,8 +325,7 @@ class DocumentClassifier: | |||||||
|         else: |         else: | ||||||
|             self.document_type_classifier = None |             self.document_type_classifier = None | ||||||
|             logger.debug( |             logger.debug( | ||||||
|                 "There are no document types. Not training document type " |                 "There are no document types. Not training document type classifier.", | ||||||
|                 "classifier.", |  | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|         if num_storage_paths > 0: |         if num_storage_paths > 0: | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| from email.encoders import encode_base64 | from email import message_from_bytes | ||||||
| from email.mime.base import MIMEBase |  | ||||||
| from pathlib import Path | from pathlib import Path | ||||||
| from urllib.parse import quote |  | ||||||
|  |  | ||||||
| from django.conf import settings | from django.conf import settings | ||||||
| from django.core.mail import EmailMessage | from django.core.mail import EmailMessage | ||||||
| @@ -27,35 +25,14 @@ def send_email( | |||||||
|     if attachment: |     if attachment: | ||||||
|         # Something could be renaming the file concurrently so it can't be attached |         # Something could be renaming the file concurrently so it can't be attached | ||||||
|         with FileLock(settings.MEDIA_LOCK), attachment.open("rb") as f: |         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 = ( |             email.attach( | ||||||
|                 attachment_mime_type.split("/", 1) |                 filename=attachment.name, | ||||||
|                 if attachment_mime_type |                 content=content, | ||||||
|                 else ("application", "octet-stream") |                 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() |     return email.send() | ||||||
|   | |||||||
| @@ -18,8 +18,7 @@ class Command(BaseCommand): | |||||||
|         parser.add_argument( |         parser.add_argument( | ||||||
|             "--passphrase", |             "--passphrase", | ||||||
|             help=( |             help=( | ||||||
|                 "If PAPERLESS_PASSPHRASE isn't set already, you need to " |                 "If PAPERLESS_PASSPHRASE isn't set already, you need to specify it here" | ||||||
|                 "specify it here" |  | ||||||
|             ), |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										63
									
								
								src/documents/tests/samples/eml_with_umlaut.eml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								src/documents/tests/samples/eml_with_umlaut.eml
									
									
									
									
									
										Normal 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: üöäüäö | ||||||
| @@ -1,4 +1,7 @@ | |||||||
|  | import types | ||||||
|  |  | ||||||
| from django.contrib.admin.sites import AdminSite | from django.contrib.admin.sites import AdminSite | ||||||
|  | from django.contrib.auth.models import User | ||||||
| from django.test import TestCase | from django.test import TestCase | ||||||
| from django.utils import timezone | from django.utils import timezone | ||||||
|  |  | ||||||
| @@ -6,6 +9,7 @@ from documents import index | |||||||
| from documents.admin import DocumentAdmin | from documents.admin import DocumentAdmin | ||||||
| from documents.models import Document | from documents.models import Document | ||||||
| from documents.tests.utils import DirectoriesMixin | from documents.tests.utils import DirectoriesMixin | ||||||
|  | from paperless.admin import PaperlessUserAdmin | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestDocumentAdmin(DirectoriesMixin, TestCase): | class TestDocumentAdmin(DirectoriesMixin, TestCase): | ||||||
| @@ -64,3 +68,39 @@ class TestDocumentAdmin(DirectoriesMixin, TestCase): | |||||||
|             created=timezone.make_aware(timezone.datetime(2020, 4, 12)), |             created=timezone.make_aware(timezone.datetime(2020, 4, 12)), | ||||||
|         ) |         ) | ||||||
|         self.assertEqual(self.doc_admin.created_(doc), "2020-04-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) | ||||||
|   | |||||||
| @@ -681,6 +681,80 @@ class TestApiUser(DirectoriesMixin, APITestCase): | |||||||
|         ) |         ) | ||||||
|         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) |         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): | class TestApiGroup(DirectoriesMixin, APITestCase): | ||||||
|     ENDPOINT = "/api/groups/" |     ENDPOINT = "/api/groups/" | ||||||
|   | |||||||
| @@ -96,7 +96,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): | |||||||
|                 doc = Document.objects.create( |                 doc = Document.objects.create( | ||||||
|                     checksum=str(i), |                     checksum=str(i), | ||||||
|                     pk=i + 1, |                     pk=i + 1, | ||||||
|                     title=f"Document {i+1}", |                     title=f"Document {i + 1}", | ||||||
|                     content="content", |                     content="content", | ||||||
|                 ) |                 ) | ||||||
|                 index.update_document(writer, doc) |                 index.update_document(writer, doc) | ||||||
| @@ -131,7 +131,7 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): | |||||||
|                 doc = Document.objects.create( |                 doc = Document.objects.create( | ||||||
|                     checksum=str(i), |                     checksum=str(i), | ||||||
|                     pk=i + 1, |                     pk=i + 1, | ||||||
|                     title=f"Document {i+1}", |                     title=f"Document {i + 1}", | ||||||
|                     content="content", |                     content="content", | ||||||
|                 ) |                 ) | ||||||
|                 index.update_document(writer, doc) |                 index.update_document(writer, doc) | ||||||
| @@ -630,8 +630,8 @@ class TestDocumentSearchApi(DirectoriesMixin, APITestCase): | |||||||
|                 doc = Document.objects.create( |                 doc = Document.objects.create( | ||||||
|                     checksum=str(i), |                     checksum=str(i), | ||||||
|                     pk=i + 1, |                     pk=i + 1, | ||||||
|                     title=f"Document {i+1}", |                     title=f"Document {i + 1}", | ||||||
|                     content=f"Things document {i+1}", |                     content=f"Things document {i + 1}", | ||||||
|                 ) |                 ) | ||||||
|                 index.update_document(writer, doc) |                 index.update_document(writer, doc) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2149,9 +2149,8 @@ class TestWorkflows( | |||||||
|         EMAIL_ENABLED=True, |         EMAIL_ENABLED=True, | ||||||
|         PAPERLESS_URL="http://localhost:8000", |         PAPERLESS_URL="http://localhost:8000", | ||||||
|     ) |     ) | ||||||
|     @mock.patch("httpx.post") |  | ||||||
|     @mock.patch("django.core.mail.message.EmailMessage.send") |     @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: |         GIVEN: | ||||||
|             - Document updated workflow with email action |             - Document updated workflow with email action | ||||||
| @@ -2199,6 +2198,24 @@ class TestWorkflows( | |||||||
|  |  | ||||||
|         mock_email_send.assert_called_once() |         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( |     @override_settings( | ||||||
|         EMAIL_ENABLED=False, |         EMAIL_ENABLED=False, | ||||||
|     ) |     ) | ||||||
|   | |||||||
| @@ -295,9 +295,9 @@ class TestMigrations(TransactionTestCase): | |||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super().setUp() |         super().setUp() | ||||||
|  |  | ||||||
|         assert ( |         assert self.migrate_from and self.migrate_to, ( | ||||||
|             self.migrate_from and self.migrate_to |             f"TestCase '{type(self).__name__}' must define migrate_from and migrate_to properties" | ||||||
|         ), f"TestCase '{type(self).__name__}' must define migrate_from and migrate_to properties" |         ) | ||||||
|         self.migrate_from = [(self.app, self.migrate_from)] |         self.migrate_from = [(self.app, self.migrate_from)] | ||||||
|         if self.dependencies is not None: |         if self.dependencies is not None: | ||||||
|             self.migrate_from.extend(self.dependencies) |             self.migrate_from.extend(self.dependencies) | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ msgstr "" | |||||||
| "Project-Id-Version: paperless-ngx\n" | "Project-Id-Version: paperless-ngx\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-12-02 23:04-0800\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" | "Last-Translator: \n" | ||||||
| "Language-Team: French\n" | "Language-Team: French\n" | ||||||
| "Language: fr_FR\n" | "Language: fr_FR\n" | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ msgstr "" | |||||||
| "Project-Id-Version: paperless-ngx\n" | "Project-Id-Version: paperless-ngx\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-12-02 23:04-0800\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" | "Last-Translator: \n" | ||||||
| "Language-Team: Italian\n" | "Language-Team: Italian\n" | ||||||
| "Language: it_IT\n" | "Language: it_IT\n" | ||||||
| @@ -31,7 +31,7 @@ msgstr "Campo personalizzato della query non valido" | |||||||
|  |  | ||||||
| #: documents/filters.py:365 | #: documents/filters.py:365 | ||||||
| msgid "Invalid expression list. Must be nonempty." | msgid "Invalid expression list. Must be nonempty." | ||||||
| msgstr "" | msgstr "Elenco delle espressioni non valido. Deve essere non vuoto." | ||||||
|  |  | ||||||
| #: documents/filters.py:386 | #: documents/filters.py:386 | ||||||
| msgid "Invalid logical operator {op!r}" | msgid "Invalid logical operator {op!r}" | ||||||
| @@ -39,7 +39,7 @@ msgstr "Operatore logico non valido {op!r}" | |||||||
|  |  | ||||||
| #: documents/filters.py:400 | #: documents/filters.py:400 | ||||||
| msgid "Maximum number of query conditions exceeded." | msgid "Maximum number of query conditions exceeded." | ||||||
| msgstr "" | msgstr "Numero massimo delle condizioni della jQuery superato." | ||||||
|  |  | ||||||
| #: documents/filters.py:465 | #: documents/filters.py:465 | ||||||
| msgid "{name!r} is not a valid custom field." | 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 | #: documents/filters.py:502 | ||||||
| msgid "{data_type} does not support query expr {expr!r}." | 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 | #: documents/filters.py:610 | ||||||
| msgid "Maximum nesting depth exceeded." | msgid "Maximum nesting depth exceeded." | ||||||
| @@ -794,7 +794,7 @@ msgstr "Campo personalizzato" | |||||||
|  |  | ||||||
| #: documents/models.py:1037 | #: documents/models.py:1037 | ||||||
| msgid "Workflow Trigger Type" | msgid "Workflow Trigger Type" | ||||||
| msgstr "" | msgstr "Tipo Frigger Del Workshop" | ||||||
|  |  | ||||||
| #: documents/models.py:1049 | #: documents/models.py:1049 | ||||||
| msgid "filter path" | msgid "filter path" | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ msgstr "" | |||||||
| "Project-Id-Version: paperless-ngx\n" | "Project-Id-Version: paperless-ngx\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-12-02 23:04-0800\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" | "Last-Translator: \n" | ||||||
| "Language-Team: Norwegian\n" | "Language-Team: Norwegian\n" | ||||||
| "Language: no_NO\n" | "Language: no_NO\n" | ||||||
| @@ -23,7 +23,7 @@ msgstr "Dokumenter" | |||||||
|  |  | ||||||
| #: documents/filters.py:336 | #: documents/filters.py:336 | ||||||
| msgid "Value must be valid JSON." | msgid "Value must be valid JSON." | ||||||
| msgstr "" | msgstr "Verdien må være en gyldig JSON." | ||||||
|  |  | ||||||
| #: documents/filters.py:355 | #: documents/filters.py:355 | ||||||
| msgid "Invalid custom field query expression" | msgid "Invalid custom field query expression" | ||||||
| @@ -1149,7 +1149,7 @@ msgstr "Ugyldig variabel oppdaget." | |||||||
| #: documents/templates/account/email/base_message.txt:1 | #: documents/templates/account/email/base_message.txt:1 | ||||||
| #, python-format | #, python-format | ||||||
| msgid "Hello from %(site_name)s!" | msgid "Hello from %(site_name)s!" | ||||||
| msgstr "" | msgstr "Hei fra %(site_name)s!" | ||||||
|  |  | ||||||
| #: documents/templates/account/email/base_message.txt:5 | #: documents/templates/account/email/base_message.txt:5 | ||||||
| #, python-format | #, python-format | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ msgstr "" | |||||||
| "Project-Id-Version: paperless-ngx\n" | "Project-Id-Version: paperless-ngx\n" | ||||||
| "Report-Msgid-Bugs-To: \n" | "Report-Msgid-Bugs-To: \n" | ||||||
| "POT-Creation-Date: 2024-12-02 23:04-0800\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" | "Last-Translator: \n" | ||||||
| "Language-Team: Russian\n" | "Language-Team: Russian\n" | ||||||
| "Language: ru_RU\n" | "Language: ru_RU\n" | ||||||
| @@ -374,7 +374,7 @@ msgstr "обратная сортировка" | |||||||
|  |  | ||||||
| #: documents/models.py:451 | #: documents/models.py:451 | ||||||
| msgid "View page size" | msgid "View page size" | ||||||
| msgstr "" | msgstr "Посмотреть размер страницы" | ||||||
|  |  | ||||||
| #: documents/models.py:459 | #: documents/models.py:459 | ||||||
| msgid "View display mode" | msgid "View display mode" | ||||||
|   | |||||||
							
								
								
									
										53
									
								
								src/paperless/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/paperless/admin.py
									
									
									
									
									
										Normal 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) | ||||||
| @@ -1,6 +1,6 @@ | |||||||
| from typing import Final | 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 | # Version string like X.Y.Z | ||||||
| __full_version_str__: Final[str] = ".".join(map(str, __version__)) | __full_version_str__: Final[str] = ".".join(map(str, __version__)) | ||||||
| # Version string like X.Y | # Version string like X.Y | ||||||
|   | |||||||
| @@ -109,6 +109,25 @@ class UserViewSet(ModelViewSet): | |||||||
|     filterset_class = UserFilterSet |     filterset_class = UserFilterSet | ||||||
|     ordering_fields = ("username",) |     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"]) |     @action(detail=True, methods=["post"]) | ||||||
|     def deactivate_totp(self, request, pk=None): |     def deactivate_totp(self, request, pk=None): | ||||||
|         request_user = request.user |         request_user = request.user | ||||||
|   | |||||||
| @@ -552,8 +552,7 @@ class MailAccountHandler(LoggingMixin): | |||||||
|                 mailbox_login(M, account) |                 mailbox_login(M, account) | ||||||
|  |  | ||||||
|                 self.log.debug( |                 self.log.debug( | ||||||
|                     f"Account {account}: Processing " |                     f"Account {account}: Processing {account.rules.count()} rule(s)", | ||||||
|                     f"{account.rules.count()} rule(s)", |  | ||||||
|                 ) |                 ) | ||||||
|  |  | ||||||
|                 for rule in account.rules.order_by("order"): |                 for rule in account.rules.order_by("order"): | ||||||
|   | |||||||
| @@ -129,9 +129,11 @@ class TestParserLive: | |||||||
|         assert thumb.exists() |         assert thumb.exists() | ||||||
|         assert thumb.is_file() |         assert thumb.is_file() | ||||||
|  |  | ||||||
|         assert ( |         assert self.imagehash(thumb) == self.imagehash( | ||||||
|             self.imagehash(thumb) == self.imagehash(simple_txt_email_thumbnail_file) |             simple_txt_email_thumbnail_file, | ||||||
|         ), f"Created Thumbnail {thumb} differs from expected file {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): |     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. |         # The created pdf is not reproducible. But the converted image should always look the same. | ||||||
|         expected_hash = self.imagehash(html_email_thumbnail_file) |         expected_hash = self.imagehash(html_email_thumbnail_file) | ||||||
|  |  | ||||||
|         assert ( |         assert generated_thumbnail_hash == expected_hash, ( | ||||||
|             generated_thumbnail_hash == expected_hash |             f"PDF looks different. Check if {generated_thumbnail} looks weird." | ||||||
|         ), f"PDF looks different. Check if {generated_thumbnail} looks weird." |         ) | ||||||
|   | |||||||
| @@ -455,8 +455,7 @@ class RasterisedDocumentParser(DocumentParser): | |||||||
|                 self.text = text_original |                 self.text = text_original | ||||||
|             else: |             else: | ||||||
|                 self.log.warning( |                 self.log.warning( | ||||||
|                     f"No text was found in {document_path}, the content will " |                     f"No text was found in {document_path}, the content will be empty.", | ||||||
|                     f"be empty.", |  | ||||||
|                 ) |                 ) | ||||||
|                 self.text = "" |                 self.text = "" | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user