mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-03-27 03:12:45 +00:00
Compare commits
10 Commits
chore/plug
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9383471fa0 | ||
|
|
0060b46c8b | ||
|
|
b153ec803b | ||
|
|
38dba60ceb | ||
|
|
ae0474450f | ||
|
|
8efb01010c | ||
|
|
d18bbfa9c3 | ||
|
|
ec76d3c762 | ||
|
|
bdc0a58242 | ||
|
|
b049ad9626 |
@@ -297,11 +297,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">87</context>
|
<context context-type="linenumber">88</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">89</context>
|
<context context-type="linenumber">90</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/dashboard.component.html</context>
|
||||||
@@ -324,11 +324,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">94</context>
|
<context context-type="linenumber">95</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">96</context>
|
<context context-type="linenumber">97</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/share-link-bundle-dialog/share-link-bundle-dialog.component.html</context>
|
||||||
@@ -375,15 +375,15 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">54</context>
|
<context context-type="linenumber">55</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">273</context>
|
<context context-type="linenumber">274</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">275</context>
|
<context context-type="linenumber">276</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5890330709052835856" datatype="html">
|
<trans-unit id="5890330709052835856" datatype="html">
|
||||||
@@ -728,11 +728,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">308</context>
|
<context context-type="linenumber">309</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">311</context>
|
<context context-type="linenumber">312</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2272120016352772836" datatype="html">
|
<trans-unit id="2272120016352772836" datatype="html">
|
||||||
@@ -1139,11 +1139,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">233</context>
|
<context context-type="linenumber">234</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">235</context>
|
<context context-type="linenumber">236</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<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>
|
||||||
@@ -1700,7 +1700,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">204</context>
|
<context context-type="linenumber">205</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context>
|
<context context-type="sourcefile">src/app/components/common/input/tags/tags.component.ts</context>
|
||||||
@@ -1782,15 +1782,15 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">156</context>
|
<context context-type="linenumber">164</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">230</context>
|
<context context-type="linenumber">238</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">255</context>
|
<context context-type="linenumber">263</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2991443309752293110" datatype="html">
|
<trans-unit id="2991443309752293110" datatype="html">
|
||||||
@@ -1801,11 +1801,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">296</context>
|
<context context-type="linenumber">297</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">298</context>
|
<context context-type="linenumber">299</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="103921551219467537" datatype="html">
|
<trans-unit id="103921551219467537" datatype="html">
|
||||||
@@ -2224,11 +2224,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">256</context>
|
<context context-type="linenumber">257</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">259</context>
|
<context context-type="linenumber">260</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3818027200170621545" datatype="html">
|
<trans-unit id="3818027200170621545" datatype="html">
|
||||||
@@ -2581,11 +2581,11 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">287</context>
|
<context context-type="linenumber">288</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">289</context>
|
<context context-type="linenumber">290</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4569276013106377105" datatype="html">
|
<trans-unit id="4569276013106377105" datatype="html">
|
||||||
@@ -2897,90 +2897,90 @@
|
|||||||
<source>Logged in as <x id="INTERPOLATION" equiv-text="{{this.settingsService.displayName}}"/></source>
|
<source>Logged in as <x id="INTERPOLATION" equiv-text="{{this.settingsService.displayName}}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">46</context>
|
<context context-type="linenumber">47</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2127032578120864096" datatype="html">
|
<trans-unit id="2127032578120864096" datatype="html">
|
||||||
<source>My Profile</source>
|
<source>My Profile</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">50</context>
|
<context context-type="linenumber">51</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3797778920049399855" datatype="html">
|
<trans-unit id="3797778920049399855" datatype="html">
|
||||||
<source>Logout</source>
|
<source>Logout</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">57</context>
|
<context context-type="linenumber">58</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4895326106573044490" datatype="html">
|
<trans-unit id="4895326106573044490" datatype="html">
|
||||||
<source>Documentation</source>
|
<source>Documentation</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">62</context>
|
<context context-type="linenumber">63</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">317</context>
|
<context context-type="linenumber">318</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">320</context>
|
<context context-type="linenumber">321</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="472206565520537964" datatype="html">
|
<trans-unit id="472206565520537964" datatype="html">
|
||||||
<source>Saved views</source>
|
<source>Saved views</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">104</context>
|
<context context-type="linenumber">105</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">134</context>
|
<context context-type="linenumber">135</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6988090220128974198" datatype="html">
|
<trans-unit id="6988090220128974198" datatype="html">
|
||||||
<source>Open documents</source>
|
<source>Open documents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">143</context>
|
<context context-type="linenumber">144</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5687256342387781369" datatype="html">
|
<trans-unit id="5687256342387781369" datatype="html">
|
||||||
<source>Close all</source>
|
<source>Close all</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">163</context>
|
<context context-type="linenumber">164</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">165</context>
|
<context context-type="linenumber">166</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3897348120591552265" datatype="html">
|
<trans-unit id="3897348120591552265" datatype="html">
|
||||||
<source>Manage</source>
|
<source>Manage</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">174</context>
|
<context context-type="linenumber">175</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8008131619909556709" datatype="html">
|
<trans-unit id="8008131619909556709" datatype="html">
|
||||||
<source>Attributes</source>
|
<source>Attributes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">181</context>
|
<context context-type="linenumber">182</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">183</context>
|
<context context-type="linenumber">184</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7437910965833684826" datatype="html">
|
<trans-unit id="7437910965833684826" datatype="html">
|
||||||
<source>Correspondents</source>
|
<source>Correspondents</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">209</context>
|
<context context-type="linenumber">210</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
<context context-type="sourcefile">src/app/components/dashboard/widgets/statistics-widget/statistics-widget.component.html</context>
|
||||||
@@ -2995,7 +2995,7 @@
|
|||||||
<source>Document types</source>
|
<source>Document types</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">214</context>
|
<context context-type="linenumber">215</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
|
||||||
@@ -3006,7 +3006,7 @@
|
|||||||
<source>Storage paths</source>
|
<source>Storage paths</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">219</context>
|
<context context-type="linenumber">220</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
|
<context context-type="sourcefile">src/app/components/manage/document-attributes/document-attributes.component.ts</context>
|
||||||
@@ -3017,7 +3017,7 @@
|
|||||||
<source>Custom fields</source>
|
<source>Custom fields</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">224</context>
|
<context context-type="linenumber">225</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.html</context>
|
||||||
@@ -3040,11 +3040,11 @@
|
|||||||
<source>Workflows</source>
|
<source>Workflows</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">242</context>
|
<context context-type="linenumber">243</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">244</context>
|
<context context-type="linenumber">245</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/workflows/workflows.component.html</context>
|
||||||
@@ -3055,92 +3055,92 @@
|
|||||||
<source>Mail</source>
|
<source>Mail</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">249</context>
|
<context context-type="linenumber">250</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">252</context>
|
<context context-type="linenumber">253</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7844706011418789951" datatype="html">
|
<trans-unit id="7844706011418789951" datatype="html">
|
||||||
<source>Administration</source>
|
<source>Administration</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">267</context>
|
<context context-type="linenumber">268</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3008420115644088420" datatype="html">
|
<trans-unit id="3008420115644088420" datatype="html">
|
||||||
<source>Configuration</source>
|
<source>Configuration</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">280</context>
|
<context context-type="linenumber">281</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">282</context>
|
<context context-type="linenumber">283</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1534029177398918729" datatype="html">
|
<trans-unit id="1534029177398918729" datatype="html">
|
||||||
<source>GitHub</source>
|
<source>GitHub</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">327</context>
|
<context context-type="linenumber">328</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4112664765954374539" datatype="html">
|
<trans-unit id="4112664765954374539" datatype="html">
|
||||||
<source>is available.</source>
|
<source>is available.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">336,337</context>
|
<context context-type="linenumber">337,338</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1175891574282637937" datatype="html">
|
<trans-unit id="1175891574282637937" datatype="html">
|
||||||
<source>Click to view.</source>
|
<source>Click to view.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">337</context>
|
<context context-type="linenumber">338</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="9811291095862612" datatype="html">
|
<trans-unit id="9811291095862612" datatype="html">
|
||||||
<source>Paperless-ngx can automatically check for updates</source>
|
<source>Paperless-ngx can automatically check for updates</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">341</context>
|
<context context-type="linenumber">342</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="894819944961861800" datatype="html">
|
<trans-unit id="894819944961861800" datatype="html">
|
||||||
<source> How does this work? </source>
|
<source> How does this work? </source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">348,350</context>
|
<context context-type="linenumber">349,351</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="509090351011426949" datatype="html">
|
<trans-unit id="509090351011426949" datatype="html">
|
||||||
<source>Update available</source>
|
<source>Update available</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.html</context>
|
||||||
<context context-type="linenumber">361</context>
|
<context context-type="linenumber">362</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1542489069631984294" datatype="html">
|
<trans-unit id="1542489069631984294" datatype="html">
|
||||||
<source>Sidebar views updated</source>
|
<source>Sidebar views updated</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">343</context>
|
<context context-type="linenumber">383</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3547923076537026828" datatype="html">
|
<trans-unit id="3547923076537026828" datatype="html">
|
||||||
<source>Error updating sidebar views</source>
|
<source>Error updating sidebar views</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">346</context>
|
<context context-type="linenumber">386</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2526035785704676448" datatype="html">
|
<trans-unit id="2526035785704676448" datatype="html">
|
||||||
<source>An error occurred while saving update checking settings.</source>
|
<source>An error occurred while saving update checking settings.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
<context context-type="sourcefile">src/app/components/app-frame/app-frame.component.ts</context>
|
||||||
<context context-type="linenumber">367</context>
|
<context context-type="linenumber">407</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4580988005648117665" datatype="html">
|
<trans-unit id="4580988005648117665" datatype="html">
|
||||||
@@ -11187,21 +11187,21 @@
|
|||||||
<source>Successfully completed one-time migratration of settings to the database!</source>
|
<source>Successfully completed one-time migratration of settings to the database!</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||||
<context context-type="linenumber">609</context>
|
<context context-type="linenumber">635</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5558341108007064934" datatype="html">
|
<trans-unit id="5558341108007064934" datatype="html">
|
||||||
<source>Unable to migrate settings to the database, please try saving manually.</source>
|
<source>Unable to migrate settings to the database, please try saving manually.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||||
<context context-type="linenumber">610</context>
|
<context context-type="linenumber">636</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1168781785897678748" datatype="html">
|
<trans-unit id="1168781785897678748" datatype="html">
|
||||||
<source>You can restart the tour from the settings page.</source>
|
<source>You can restart the tour from the settings page.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
<context context-type="sourcefile">src/app/services/settings.service.ts</context>
|
||||||
<context context-type="linenumber">683</context>
|
<context context-type="linenumber">708</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3852289441366561594" datatype="html">
|
<trans-unit id="3852289441366561594" datatype="html">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<nav class="navbar navbar-dark fixed-top bg-primary flex-md-nowrap p-0 shadow-sm">
|
<nav class="navbar navbar-dark fixed-top bg-primary flex-md-nowrap p-0 shadow-sm">
|
||||||
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
<button class="navbar-toggler d-md-none collapsed border-0" type="button" data-toggle="collapse"
|
||||||
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation"
|
||||||
(click)="isMenuCollapsed = !isMenuCollapsed">
|
(click)="mobileSearchHidden = false; isMenuCollapsed = !isMenuCollapsed">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand d-flex align-items-center me-0 px-3 py-3 order-sm-0"
|
<a class="navbar-brand d-flex align-items-center me-0 px-3 py-3 order-sm-0"
|
||||||
@@ -24,7 +24,8 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="search-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1">
|
<div class="search-container flex-grow-1 py-2 pb-3 pb-sm-2 px-3 ps-md-4 me-sm-auto order-3 order-sm-1"
|
||||||
|
[class.mobile-hidden]="mobileSearchHidden">
|
||||||
<div class="col-12 col-md-7">
|
<div class="col-12 col-md-7">
|
||||||
<pngx-global-search></pngx-global-search>
|
<pngx-global-search></pngx-global-search>
|
||||||
</div>
|
</div>
|
||||||
@@ -378,7 +379,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main role="main" class="ms-sm-auto px-md-4"
|
<main role="main" class="ms-sm-auto px-md-4" [class.mobile-search-hidden]="mobileSearchHidden"
|
||||||
[ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10 col-xxxl-11'">
|
[ngClass]="slimSidebarEnabled ? 'col-slim' : 'col-md-9 col-lg-10 col-xxxl-11'">
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -44,6 +44,23 @@
|
|||||||
.sidebar {
|
.sidebar {
|
||||||
top: 3.5rem;
|
top: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
max-height: 4.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height .2s ease, opacity .2s ease, padding-top .2s ease, padding-bottom .2s ease;
|
||||||
|
|
||||||
|
&.mobile-hidden {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main.mobile-search-hidden {
|
||||||
|
padding-top: 56px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
|
|||||||
@@ -293,6 +293,59 @@ describe('AppFrameComponent', () => {
|
|||||||
expect(component.isMenuCollapsed).toBeTruthy()
|
expect(component.isMenuCollapsed).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should hide mobile search when scrolling down and show it when scrolling up', () => {
|
||||||
|
Object.defineProperty(globalThis, 'innerWidth', {
|
||||||
|
value: 767,
|
||||||
|
})
|
||||||
|
|
||||||
|
component.ngOnInit()
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'scrollY', {
|
||||||
|
configurable: true,
|
||||||
|
value: 40,
|
||||||
|
})
|
||||||
|
component.onWindowScroll()
|
||||||
|
expect(component.mobileSearchHidden).toBe(true)
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'scrollY', {
|
||||||
|
configurable: true,
|
||||||
|
value: 0,
|
||||||
|
})
|
||||||
|
component.onWindowScroll()
|
||||||
|
expect(component.mobileSearchHidden).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep mobile search visible on desktop scroll or resize', () => {
|
||||||
|
Object.defineProperty(globalThis, 'innerWidth', {
|
||||||
|
value: 1024,
|
||||||
|
})
|
||||||
|
component.ngOnInit()
|
||||||
|
component.mobileSearchHidden = true
|
||||||
|
|
||||||
|
component.onWindowScroll()
|
||||||
|
|
||||||
|
expect(component.mobileSearchHidden).toBe(false)
|
||||||
|
|
||||||
|
component.mobileSearchHidden = true
|
||||||
|
component.onWindowResize()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should keep mobile search visible while the mobile menu is expanded', () => {
|
||||||
|
Object.defineProperty(globalThis, 'innerWidth', {
|
||||||
|
value: 767,
|
||||||
|
})
|
||||||
|
component.ngOnInit()
|
||||||
|
component.isMenuCollapsed = false
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'scrollY', {
|
||||||
|
configurable: true,
|
||||||
|
value: 40,
|
||||||
|
})
|
||||||
|
component.onWindowScroll()
|
||||||
|
|
||||||
|
expect(component.mobileSearchHidden).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('should support close document & navigate on close current doc', () => {
|
it('should support close document & navigate on close current doc', () => {
|
||||||
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
|
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
|
||||||
closeSpy.mockReturnValue(of(true))
|
closeSpy.mockReturnValue(of(true))
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ import { ComponentWithPermissions } from '../with-permissions/with-permissions.c
|
|||||||
import { GlobalSearchComponent } from './global-search/global-search.component'
|
import { GlobalSearchComponent } from './global-search/global-search.component'
|
||||||
import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component'
|
import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component'
|
||||||
|
|
||||||
|
const SCROLL_THRESHOLD = 16
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-app-frame',
|
selector: 'pngx-app-frame',
|
||||||
templateUrl: './app-frame.component.html',
|
templateUrl: './app-frame.component.html',
|
||||||
@@ -94,6 +96,10 @@ export class AppFrameComponent
|
|||||||
|
|
||||||
slimSidebarAnimating: boolean = false
|
slimSidebarAnimating: boolean = false
|
||||||
|
|
||||||
|
public mobileSearchHidden: boolean = false
|
||||||
|
|
||||||
|
private lastScrollY: number = 0
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
const permissionsService = this.permissionsService
|
const permissionsService = this.permissionsService
|
||||||
@@ -111,6 +117,8 @@ export class AppFrameComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.lastScrollY = window.scrollY
|
||||||
|
|
||||||
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
if (this.settingsService.get(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED)) {
|
||||||
this.checkForUpdates()
|
this.checkForUpdates()
|
||||||
}
|
}
|
||||||
@@ -263,6 +271,38 @@ export class AppFrameComponent
|
|||||||
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
return this.settingsService.get(SETTINGS_KEYS.AI_ENABLED)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@HostListener('window:resize')
|
||||||
|
onWindowResize(): void {
|
||||||
|
if (!this.isMobileViewport()) {
|
||||||
|
this.mobileSearchHidden = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('window:scroll')
|
||||||
|
onWindowScroll(): void {
|
||||||
|
const currentScrollY = window.scrollY
|
||||||
|
|
||||||
|
if (!this.isMobileViewport() || this.isMenuCollapsed === false) {
|
||||||
|
this.mobileSearchHidden = false
|
||||||
|
this.lastScrollY = currentScrollY
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = currentScrollY - this.lastScrollY
|
||||||
|
|
||||||
|
if (currentScrollY <= 0 || delta < -SCROLL_THRESHOLD) {
|
||||||
|
this.mobileSearchHidden = false
|
||||||
|
} else if (currentScrollY > SCROLL_THRESHOLD && delta > SCROLL_THRESHOLD) {
|
||||||
|
this.mobileSearchHidden = true
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastScrollY = currentScrollY
|
||||||
|
}
|
||||||
|
|
||||||
|
private isMobileViewport(): boolean {
|
||||||
|
return window.innerWidth < 768
|
||||||
|
}
|
||||||
|
|
||||||
closeMenu() {
|
closeMenu() {
|
||||||
this.isMenuCollapsed = true
|
this.isMenuCollapsed = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,13 +56,20 @@ $paperless-card-breakpoints: (
|
|||||||
|
|
||||||
.sticky-top {
|
.sticky-top {
|
||||||
z-index: 990; // below main navbar
|
z-index: 990; // below main navbar
|
||||||
top: calc(7rem - 2px); // height of navbar (mobile)
|
top: calc(7rem - 2px); // height of navbar + search row (mobile)
|
||||||
|
transition: top 0.2s ease;
|
||||||
|
|
||||||
@media (min-width: 580px) {
|
@media (min-width: 580px) {
|
||||||
top: 3.5rem; // height of navbar
|
top: 3.5rem; // height of navbar
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 579.98px) {
|
||||||
|
:host-context(main.mobile-search-hidden) .sticky-top {
|
||||||
|
top: calc(3.5rem - 2px); // height of navbar only when search is hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.table .form-check {
|
.table .form-check {
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
FILTER_HAS_TAGS_ANY,
|
FILTER_HAS_TAGS_ANY,
|
||||||
} from '../data/filter-rule-type'
|
} from '../data/filter-rule-type'
|
||||||
import { SavedView } from '../data/saved-view'
|
import { SavedView } from '../data/saved-view'
|
||||||
|
import { DOCUMENT_LIST_SERVICE } from '../data/storage-keys'
|
||||||
import { SETTINGS_KEYS } from '../data/ui-settings'
|
import { SETTINGS_KEYS } from '../data/ui-settings'
|
||||||
import { PermissionsGuard } from '../guards/permissions.guard'
|
import { PermissionsGuard } from '../guards/permissions.guard'
|
||||||
import { DocumentListViewService } from './document-list-view.service'
|
import { DocumentListViewService } from './document-list-view.service'
|
||||||
@@ -248,6 +249,29 @@ describe('DocumentListViewService', () => {
|
|||||||
expect(documentListViewService.sortReverse).toBeTruthy()
|
expect(documentListViewService.sortReverse).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('restores only known list view state fields from local storage', () => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG,
|
||||||
|
'{"currentPage":3,"sortField":"title","sortReverse":false,"__proto__":{"polluted":true},"injected":"ignored"}'
|
||||||
|
)
|
||||||
|
|
||||||
|
const restoredService = TestBed.runInInjectionContext(
|
||||||
|
() => new DocumentListViewService()
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(restoredService.currentPage).toEqual(3)
|
||||||
|
expect(restoredService.sortField).toEqual('title')
|
||||||
|
expect(restoredService.sortReverse).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
(restoredService as any).activeListViewState.injected
|
||||||
|
).toBeUndefined()
|
||||||
|
expect(({} as any).polluted).toBeUndefined()
|
||||||
|
} finally {
|
||||||
|
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
it('should load from query params', () => {
|
it('should load from query params', () => {
|
||||||
expect(documentListViewService.currentPage).toEqual(1)
|
expect(documentListViewService.currentPage).toEqual(1)
|
||||||
const page = 2
|
const page = 2
|
||||||
|
|||||||
@@ -24,6 +24,20 @@ const LIST_DEFAULT_DISPLAY_FIELDS: DisplayField[] = DEFAULT_DISPLAY_FIELDS.map(
|
|||||||
(f) => f.id
|
(f) => f.id
|
||||||
).filter((f) => f !== DisplayField.ADDED)
|
).filter((f) => f !== DisplayField.ADDED)
|
||||||
|
|
||||||
|
const RESTORABLE_LIST_VIEW_STATE_KEYS: (keyof ListViewState)[] = [
|
||||||
|
'title',
|
||||||
|
'documents',
|
||||||
|
'currentPage',
|
||||||
|
'collectionSize',
|
||||||
|
'sortField',
|
||||||
|
'sortReverse',
|
||||||
|
'filterRules',
|
||||||
|
'selected',
|
||||||
|
'pageSize',
|
||||||
|
'displayMode',
|
||||||
|
'displayFields',
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Captures the current state of the list view.
|
* Captures the current state of the list view.
|
||||||
*/
|
*/
|
||||||
@@ -112,6 +126,32 @@ export class DocumentListViewService {
|
|||||||
|
|
||||||
private displayFieldsInitialized: boolean = false
|
private displayFieldsInitialized: boolean = false
|
||||||
|
|
||||||
|
private restoreListViewState(savedState: unknown): ListViewState {
|
||||||
|
const newState = this.defaultListViewState()
|
||||||
|
|
||||||
|
if (
|
||||||
|
!savedState ||
|
||||||
|
typeof savedState !== 'object' ||
|
||||||
|
Array.isArray(savedState)
|
||||||
|
) {
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedState = savedState as Partial<
|
||||||
|
Record<keyof ListViewState, unknown>
|
||||||
|
>
|
||||||
|
const mutableState = newState as Record<keyof ListViewState, unknown>
|
||||||
|
|
||||||
|
for (const key of RESTORABLE_LIST_VIEW_STATE_KEYS) {
|
||||||
|
const value = parsedState[key]
|
||||||
|
if (value != null) {
|
||||||
|
mutableState[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newState
|
||||||
|
}
|
||||||
|
|
||||||
get activeSavedViewId() {
|
get activeSavedViewId() {
|
||||||
return this._activeSavedViewId
|
return this._activeSavedViewId
|
||||||
}
|
}
|
||||||
@@ -127,14 +167,7 @@ export class DocumentListViewService {
|
|||||||
if (documentListViewConfigJson) {
|
if (documentListViewConfigJson) {
|
||||||
try {
|
try {
|
||||||
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
|
let savedState: ListViewState = JSON.parse(documentListViewConfigJson)
|
||||||
// Remove null elements from the restored state
|
let newState = this.restoreListViewState(savedState)
|
||||||
Object.keys(savedState).forEach((k) => {
|
|
||||||
if (savedState[k] == null) {
|
|
||||||
delete savedState[k]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// only use restored state attributes instead of defaults if they are not null
|
|
||||||
let newState = Object.assign(this.defaultListViewState(), savedState)
|
|
||||||
this.listViewStates.set(null, newState)
|
this.listViewStates.set(null, newState)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
localStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||||
|
|||||||
@@ -166,6 +166,23 @@ describe('SettingsService', () => {
|
|||||||
expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('#9fbf2f')
|
expect(settingsService.get(SETTINGS_KEYS.THEME_COLOR)).toEqual('#9fbf2f')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('ignores unsafe top-level keys from loaded settings', () => {
|
||||||
|
const req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}ui_settings/`
|
||||||
|
)
|
||||||
|
const payload = JSON.parse(
|
||||||
|
JSON.stringify(ui_settings).replace(
|
||||||
|
'"settings":{',
|
||||||
|
'"settings":{"__proto__":{"polluted":"yes"},'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
payload.settings.app_title = 'Safe Title'
|
||||||
|
req.flush(payload)
|
||||||
|
|
||||||
|
expect(settingsService.get(SETTINGS_KEYS.APP_TITLE)).toEqual('Safe Title')
|
||||||
|
expect(({} as any).polluted).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('correctly allows updating settings of various types', () => {
|
it('correctly allows updating settings of various types', () => {
|
||||||
const req = httpTestingController.expectOne(
|
const req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}ui_settings/`
|
`${environment.apiBaseUrl}ui_settings/`
|
||||||
|
|||||||
@@ -276,6 +276,8 @@ const ISO_LANGUAGE_OPTION: LanguageOption = {
|
|||||||
dateInputFormat: 'yyyy-mm-dd',
|
dateInputFormat: 'yyyy-mm-dd',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UNSAFE_OBJECT_KEYS = new Set(['__proto__', 'prototype', 'constructor'])
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
@@ -291,7 +293,7 @@ export class SettingsService {
|
|||||||
|
|
||||||
protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/'
|
protected baseUrl: string = environment.apiBaseUrl + 'ui_settings/'
|
||||||
|
|
||||||
private settings: Object = {}
|
private settings: Record<string, any> = {}
|
||||||
currentUser: User
|
currentUser: User
|
||||||
|
|
||||||
public settingsSaved: EventEmitter<any> = new EventEmitter()
|
public settingsSaved: EventEmitter<any> = new EventEmitter()
|
||||||
@@ -320,6 +322,21 @@ export class SettingsService {
|
|||||||
this._renderer = rendererFactory.createRenderer(null, null)
|
this._renderer = rendererFactory.createRenderer(null, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isSafeObjectKey(key: string): boolean {
|
||||||
|
return !UNSAFE_OBJECT_KEYS.has(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
private assignSafeSettings(source: Record<string, any>) {
|
||||||
|
if (!source || typeof source !== 'object' || Array.isArray(source)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(source)) {
|
||||||
|
if (!this.isSafeObjectKey(key)) continue
|
||||||
|
this.settings[key] = source[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// this is called by the app initializer in app.module
|
// this is called by the app initializer in app.module
|
||||||
public initializeSettings(): Observable<UiSettings> {
|
public initializeSettings(): Observable<UiSettings> {
|
||||||
return this.http.get<UiSettings>(this.baseUrl).pipe(
|
return this.http.get<UiSettings>(this.baseUrl).pipe(
|
||||||
@@ -338,7 +355,7 @@ export class SettingsService {
|
|||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
tap((uisettings) => {
|
tap((uisettings) => {
|
||||||
Object.assign(this.settings, uisettings.settings)
|
this.assignSafeSettings(uisettings.settings)
|
||||||
if (this.get(SETTINGS_KEYS.APP_TITLE)?.length) {
|
if (this.get(SETTINGS_KEYS.APP_TITLE)?.length) {
|
||||||
environment.appTitle = this.get(SETTINGS_KEYS.APP_TITLE)
|
environment.appTitle = this.get(SETTINGS_KEYS.APP_TITLE)
|
||||||
}
|
}
|
||||||
@@ -533,7 +550,11 @@ export class SettingsService {
|
|||||||
let settingObj = this.settings
|
let settingObj = this.settings
|
||||||
keys.forEach((keyPart, index) => {
|
keys.forEach((keyPart, index) => {
|
||||||
keyPart = keyPart.replace(/-/g, '_')
|
keyPart = keyPart.replace(/-/g, '_')
|
||||||
if (!settingObj.hasOwnProperty(keyPart)) return
|
if (
|
||||||
|
!this.isSafeObjectKey(keyPart) ||
|
||||||
|
!Object.prototype.hasOwnProperty.call(settingObj, keyPart)
|
||||||
|
)
|
||||||
|
return
|
||||||
if (index == keys.length - 1) value = settingObj[keyPart]
|
if (index == keys.length - 1) value = settingObj[keyPart]
|
||||||
else settingObj = settingObj[keyPart]
|
else settingObj = settingObj[keyPart]
|
||||||
})
|
})
|
||||||
@@ -579,7 +600,9 @@ export class SettingsService {
|
|||||||
const keys = key.replace('general-settings:', '').split(':')
|
const keys = key.replace('general-settings:', '').split(':')
|
||||||
keys.forEach((keyPart, index) => {
|
keys.forEach((keyPart, index) => {
|
||||||
keyPart = keyPart.replace(/-/g, '_')
|
keyPart = keyPart.replace(/-/g, '_')
|
||||||
if (!settingObj.hasOwnProperty(keyPart)) settingObj[keyPart] = {}
|
if (!this.isSafeObjectKey(keyPart)) return
|
||||||
|
if (!Object.prototype.hasOwnProperty.call(settingObj, keyPart))
|
||||||
|
settingObj[keyPart] = {}
|
||||||
if (index == keys.length - 1) settingObj[keyPart] = value
|
if (index == keys.length - 1) settingObj[keyPart] = value
|
||||||
else settingObj = settingObj[keyPart]
|
else settingObj = settingObj[keyPart]
|
||||||
})
|
})
|
||||||
@@ -602,7 +625,10 @@ export class SettingsService {
|
|||||||
|
|
||||||
maybeMigrateSettings() {
|
maybeMigrateSettings() {
|
||||||
if (
|
if (
|
||||||
!this.settings.hasOwnProperty('documentListSize') &&
|
!Object.prototype.hasOwnProperty.call(
|
||||||
|
this.settings,
|
||||||
|
'documentListSize'
|
||||||
|
) &&
|
||||||
localStorage.getItem(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
localStorage.getItem(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)
|
||||||
) {
|
) {
|
||||||
// lets migrate
|
// lets migrate
|
||||||
@@ -610,8 +636,7 @@ export class SettingsService {
|
|||||||
const errorMessage = $localize`Unable to migrate settings to the database, please try saving manually.`
|
const errorMessage = $localize`Unable to migrate settings to the database, please try saving manually.`
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (const setting in SETTINGS_KEYS) {
|
for (const key of Object.values(SETTINGS_KEYS)) {
|
||||||
const key = SETTINGS_KEYS[setting]
|
|
||||||
const value = localStorage.getItem(key)
|
const value = localStorage.getItem(key)
|
||||||
this.set(key, value)
|
this.set(key, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import tempfile
|
import tempfile
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -46,6 +46,7 @@ from documents.signals import document_consumption_started
|
|||||||
from documents.signals import document_updated
|
from documents.signals import document_updated
|
||||||
from documents.signals.handlers import run_workflows
|
from documents.signals.handlers import run_workflows
|
||||||
from documents.templating.workflows import parse_w_workflow_placeholders
|
from documents.templating.workflows import parse_w_workflow_placeholders
|
||||||
|
from documents.utils import compute_checksum
|
||||||
from documents.utils import copy_basic_file_stats
|
from documents.utils import copy_basic_file_stats
|
||||||
from documents.utils import copy_file_with_basic_stats
|
from documents.utils import copy_file_with_basic_stats
|
||||||
from documents.utils import run_subprocess
|
from documents.utils import run_subprocess
|
||||||
@@ -196,9 +197,7 @@ class ConsumerPlugin(
|
|||||||
version_doc = Document(
|
version_doc = Document(
|
||||||
root_document=root_doc_frozen,
|
root_document=root_doc_frozen,
|
||||||
version_index=next_version_index + 1,
|
version_index=next_version_index + 1,
|
||||||
checksum=hashlib.md5(
|
checksum=compute_checksum(file_for_checksum),
|
||||||
file_for_checksum.read_bytes(),
|
|
||||||
).hexdigest(),
|
|
||||||
content=text or "",
|
content=text or "",
|
||||||
page_count=page_count,
|
page_count=page_count,
|
||||||
mime_type=mime_type,
|
mime_type=mime_type,
|
||||||
@@ -338,18 +337,15 @@ class ConsumerPlugin(
|
|||||||
Return the document object if it was successfully created.
|
Return the document object if it was successfully created.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tempdir = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Preflight has already run including progress update to 0%
|
# Preflight has already run including progress update to 0%
|
||||||
self.log.info(f"Consuming {self.filename}")
|
self.log.info(f"Consuming {self.filename}")
|
||||||
|
|
||||||
# For the actual work, copy the file into a tempdir
|
# For the actual work, copy the file into a tempdir
|
||||||
tempdir = tempfile.TemporaryDirectory(
|
with tempfile.TemporaryDirectory(
|
||||||
prefix="paperless-ngx",
|
prefix="paperless-ngx",
|
||||||
dir=settings.SCRATCH_DIR,
|
dir=settings.SCRATCH_DIR,
|
||||||
)
|
) as tmpdir:
|
||||||
self.working_copy = Path(tempdir.name) / Path(self.filename)
|
self.working_copy = Path(tmpdir) / Path(self.filename)
|
||||||
copy_file_with_basic_stats(self.input_doc.original_file, self.working_copy)
|
copy_file_with_basic_stats(self.input_doc.original_file, self.working_copy)
|
||||||
self.unmodified_original = None
|
self.unmodified_original = None
|
||||||
|
|
||||||
@@ -381,7 +377,7 @@ class ConsumerPlugin(
|
|||||||
self.log.debug(f"Detected mime type after qpdf: {mime_type}")
|
self.log.debug(f"Detected mime type after qpdf: {mime_type}")
|
||||||
# Save the original file for later
|
# Save the original file for later
|
||||||
self.unmodified_original = (
|
self.unmodified_original = (
|
||||||
Path(tempdir.name) / Path("uo") / Path(self.filename)
|
Path(tmpdir) / Path("uo") / Path(self.filename)
|
||||||
)
|
)
|
||||||
self.unmodified_original.parent.mkdir(exist_ok=True)
|
self.unmodified_original.parent.mkdir(exist_ok=True)
|
||||||
copy_file_with_basic_stats(
|
copy_file_with_basic_stats(
|
||||||
@@ -400,7 +396,6 @@ class ConsumerPlugin(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if not parser_class:
|
if not parser_class:
|
||||||
tempdir.cleanup()
|
|
||||||
self._fail(
|
self._fail(
|
||||||
ConsumerStatusShortMessage.UNSUPPORTED_TYPE,
|
ConsumerStatusShortMessage.UNSUPPORTED_TYPE,
|
||||||
f"Unsupported mime type {mime_type}",
|
f"Unsupported mime type {mime_type}",
|
||||||
@@ -415,10 +410,6 @@ class ConsumerPlugin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.run_pre_consume_script()
|
self.run_pre_consume_script()
|
||||||
except:
|
|
||||||
if tempdir:
|
|
||||||
tempdir.cleanup()
|
|
||||||
raise
|
|
||||||
|
|
||||||
# This doesn't parse the document yet, but gives us a parser.
|
# This doesn't parse the document yet, but gives us a parser.
|
||||||
with parser_class() as document_parser:
|
with parser_class() as document_parser:
|
||||||
@@ -426,7 +417,9 @@ class ConsumerPlugin(
|
|||||||
ParserContext(mailrule_id=self.input_doc.mailrule_id),
|
ParserContext(mailrule_id=self.input_doc.mailrule_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log.debug(f"Parser: {document_parser.name} v{document_parser.version}")
|
self.log.debug(
|
||||||
|
f"Parser: {document_parser.name} v{document_parser.version}",
|
||||||
|
)
|
||||||
|
|
||||||
# Parse the document. This may take some time.
|
# Parse the document. This may take some time.
|
||||||
|
|
||||||
@@ -454,7 +447,10 @@ class ConsumerPlugin(
|
|||||||
ProgressStatusOptions.WORKING,
|
ProgressStatusOptions.WORKING,
|
||||||
ConsumerStatusShortMessage.GENERATING_THUMBNAIL,
|
ConsumerStatusShortMessage.GENERATING_THUMBNAIL,
|
||||||
)
|
)
|
||||||
thumbnail = document_parser.get_thumbnail(self.working_copy, mime_type)
|
thumbnail = document_parser.get_thumbnail(
|
||||||
|
self.working_copy,
|
||||||
|
mime_type,
|
||||||
|
)
|
||||||
|
|
||||||
text = document_parser.get_text()
|
text = document_parser.get_text()
|
||||||
date = document_parser.get_date()
|
date = document_parser.get_date()
|
||||||
@@ -474,8 +470,6 @@ class ConsumerPlugin(
|
|||||||
)
|
)
|
||||||
|
|
||||||
except ParseError as e:
|
except ParseError as e:
|
||||||
if tempdir:
|
|
||||||
tempdir.cleanup()
|
|
||||||
self._fail(
|
self._fail(
|
||||||
str(e),
|
str(e),
|
||||||
f"Error occurred while consuming document {self.filename}: {e}",
|
f"Error occurred while consuming document {self.filename}: {e}",
|
||||||
@@ -483,8 +477,6 @@ class ConsumerPlugin(
|
|||||||
exception=e,
|
exception=e,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if tempdir:
|
|
||||||
tempdir.cleanup()
|
|
||||||
self._fail(
|
self._fail(
|
||||||
str(e),
|
str(e),
|
||||||
f"Unexpected error while consuming document {self.filename}: {e}",
|
f"Unexpected error while consuming document {self.filename}: {e}",
|
||||||
@@ -640,10 +632,9 @@ class ConsumerPlugin(
|
|||||||
document.archive_path,
|
document.archive_path,
|
||||||
)
|
)
|
||||||
|
|
||||||
with Path(archive_path).open("rb") as f:
|
document.archive_checksum = compute_checksum(
|
||||||
document.archive_checksum = hashlib.md5(
|
document.archive_path,
|
||||||
f.read(),
|
)
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
# Don't save with the lock active. Saving will cause the file
|
# Don't save with the lock active. Saving will cause the file
|
||||||
# renaming logic to acquire the lock as well.
|
# renaming logic to acquire the lock as well.
|
||||||
@@ -687,8 +678,6 @@ class ConsumerPlugin(
|
|||||||
exc_info=True,
|
exc_info=True,
|
||||||
exception=e,
|
exception=e,
|
||||||
)
|
)
|
||||||
finally:
|
|
||||||
tempdir.cleanup()
|
|
||||||
|
|
||||||
self.run_post_consume_script(document)
|
self.run_post_consume_script(document)
|
||||||
|
|
||||||
@@ -785,7 +774,7 @@ class ConsumerPlugin(
|
|||||||
title=title[:127],
|
title=title[:127],
|
||||||
content=text,
|
content=text,
|
||||||
mime_type=mime_type,
|
mime_type=mime_type,
|
||||||
checksum=hashlib.md5(file_for_checksum.read_bytes()).hexdigest(),
|
checksum=compute_checksum(file_for_checksum),
|
||||||
created=create_date,
|
created=create_date,
|
||||||
modified=create_date,
|
modified=create_date,
|
||||||
page_count=page_count,
|
page_count=page_count,
|
||||||
@@ -833,7 +822,7 @@ class ConsumerPlugin(
|
|||||||
self.metadata.view_users is not None
|
self.metadata.view_users is not None
|
||||||
or self.metadata.view_groups is not None
|
or self.metadata.view_groups is not None
|
||||||
or self.metadata.change_users is not None
|
or self.metadata.change_users is not None
|
||||||
or self.metadata.change_users is not None
|
or self.metadata.change_groups is not None
|
||||||
):
|
):
|
||||||
permissions = {
|
permissions = {
|
||||||
"view": {
|
"view": {
|
||||||
@@ -866,7 +855,7 @@ class ConsumerPlugin(
|
|||||||
Path(source).open("rb") as read_file,
|
Path(source).open("rb") as read_file,
|
||||||
Path(target).open("wb") as write_file,
|
Path(target).open("wb") as write_file,
|
||||||
):
|
):
|
||||||
write_file.write(read_file.read())
|
shutil.copyfileobj(read_file, write_file)
|
||||||
|
|
||||||
# Attempt to copy file's original stats, but it's ok if we can't
|
# Attempt to copy file's original stats, but it's ok if we can't
|
||||||
try:
|
try:
|
||||||
@@ -902,10 +891,9 @@ class ConsumerPreflightPlugin(
|
|||||||
|
|
||||||
def pre_check_duplicate(self) -> None:
|
def pre_check_duplicate(self) -> None:
|
||||||
"""
|
"""
|
||||||
Using the MD5 of the file, check this exact file doesn't already exist
|
Using the SHA256 of the file, check this exact file doesn't already exist
|
||||||
"""
|
"""
|
||||||
with Path(self.input_doc.original_file).open("rb") as f:
|
checksum = compute_checksum(Path(self.input_doc.original_file))
|
||||||
checksum = hashlib.md5(f.read()).hexdigest()
|
|
||||||
existing_doc = Document.global_objects.filter(
|
existing_doc = Document.global_objects.filter(
|
||||||
Q(checksum=checksum) | Q(archive_checksum=checksum),
|
Q(checksum=checksum) | Q(archive_checksum=checksum),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ from documents.models import WorkflowTrigger
|
|||||||
from documents.settings import EXPORTER_ARCHIVE_NAME
|
from documents.settings import EXPORTER_ARCHIVE_NAME
|
||||||
from documents.settings import EXPORTER_FILE_NAME
|
from documents.settings import EXPORTER_FILE_NAME
|
||||||
from documents.settings import EXPORTER_THUMBNAIL_NAME
|
from documents.settings import EXPORTER_THUMBNAIL_NAME
|
||||||
|
from documents.utils import compute_checksum
|
||||||
from documents.utils import copy_file_with_basic_stats
|
from documents.utils import copy_file_with_basic_stats
|
||||||
from paperless import version
|
from paperless import version
|
||||||
from paperless.models import ApplicationConfiguration
|
from paperless.models import ApplicationConfiguration
|
||||||
@@ -693,7 +694,7 @@ class Command(CryptMixin, PaperlessCommand):
|
|||||||
source_stat = source.stat()
|
source_stat = source.stat()
|
||||||
target_stat = target.stat()
|
target_stat = target.stat()
|
||||||
if self.compare_checksums and source_checksum:
|
if self.compare_checksums and source_checksum:
|
||||||
target_checksum = hashlib.md5(target.read_bytes()).hexdigest()
|
target_checksum = compute_checksum(target)
|
||||||
perform_copy = target_checksum != source_checksum
|
perform_copy = target_checksum != source_checksum
|
||||||
elif (
|
elif (
|
||||||
source_stat.st_mtime != target_stat.st_mtime
|
source_stat.st_mtime != target_stat.st_mtime
|
||||||
|
|||||||
130
src/documents/migrations/0016_sha256_checksums.py
Normal file
130
src/documents/migrations/0016_sha256_checksums.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
logger = logging.getLogger("paperless.migrations")
|
||||||
|
|
||||||
|
_CHUNK_SIZE = 65536 # 64 KiB — avoids loading entire files into memory
|
||||||
|
_BATCH_SIZE = 500 # documents per bulk_update call
|
||||||
|
_PROGRESS_INTERVAL = 500 # log a progress line every N documents
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(path: Path) -> str:
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with path.open("rb") as fh:
|
||||||
|
while chunk := fh.read(_CHUNK_SIZE):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def recompute_checksums(apps, schema_editor):
|
||||||
|
"""Recompute all document checksums from MD5 to SHA256."""
|
||||||
|
Document = apps.get_model("documents", "Document")
|
||||||
|
|
||||||
|
total = Document.objects.count()
|
||||||
|
if total == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info("Recomputing SHA-256 checksums for %d document(s)...", total)
|
||||||
|
|
||||||
|
batch: list = []
|
||||||
|
processed = 0
|
||||||
|
|
||||||
|
for doc in Document.objects.only(
|
||||||
|
"pk",
|
||||||
|
"filename",
|
||||||
|
"checksum",
|
||||||
|
"archive_filename",
|
||||||
|
"archive_checksum",
|
||||||
|
).iterator(chunk_size=_BATCH_SIZE):
|
||||||
|
updated_fields: list[str] = []
|
||||||
|
|
||||||
|
# Reconstruct source path the same way Document.source_path does
|
||||||
|
fname = str(doc.filename) if doc.filename else f"{doc.pk:07}.pdf"
|
||||||
|
source_path = (settings.ORIGINALS_DIR / Path(fname)).resolve()
|
||||||
|
|
||||||
|
if source_path.exists():
|
||||||
|
doc.checksum = _sha256(source_path)
|
||||||
|
updated_fields.append("checksum")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Document %s: original file %s not found, checksum not updated.",
|
||||||
|
doc.pk,
|
||||||
|
source_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mirror Document.has_archive_version: archive_filename is not None
|
||||||
|
if doc.archive_filename is not None:
|
||||||
|
archive_path = (
|
||||||
|
settings.ARCHIVE_DIR / Path(str(doc.archive_filename))
|
||||||
|
).resolve()
|
||||||
|
if archive_path.exists():
|
||||||
|
doc.archive_checksum = _sha256(archive_path)
|
||||||
|
updated_fields.append("archive_checksum")
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Document %s: archive file %s not found, checksum not updated.",
|
||||||
|
doc.pk,
|
||||||
|
archive_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
if updated_fields:
|
||||||
|
batch.append(doc)
|
||||||
|
|
||||||
|
processed += 1
|
||||||
|
|
||||||
|
if len(batch) >= _BATCH_SIZE:
|
||||||
|
Document.objects.bulk_update(batch, ["checksum", "archive_checksum"])
|
||||||
|
batch.clear()
|
||||||
|
|
||||||
|
if processed % _PROGRESS_INTERVAL == 0:
|
||||||
|
logger.info(
|
||||||
|
"SHA-256 checksum progress: %d/%d (%d%%)",
|
||||||
|
processed,
|
||||||
|
total,
|
||||||
|
processed * 100 // total,
|
||||||
|
)
|
||||||
|
|
||||||
|
if batch:
|
||||||
|
Document.objects.bulk_update(batch, ["checksum", "archive_checksum"])
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"SHA-256 checksum recomputation complete: %d document(s) processed.",
|
||||||
|
total,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("documents", "0015_document_version_index_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="document",
|
||||||
|
name="checksum",
|
||||||
|
field=models.CharField(
|
||||||
|
editable=False,
|
||||||
|
help_text="The checksum of the original document.",
|
||||||
|
max_length=64,
|
||||||
|
verbose_name="checksum",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="document",
|
||||||
|
name="archive_checksum",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
editable=False,
|
||||||
|
help_text="The checksum of the archived document.",
|
||||||
|
max_length=64,
|
||||||
|
null=True,
|
||||||
|
verbose_name="archive checksum",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(recompute_checksums, migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@@ -216,14 +216,14 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
|
|||||||
|
|
||||||
checksum = models.CharField(
|
checksum = models.CharField(
|
||||||
_("checksum"),
|
_("checksum"),
|
||||||
max_length=32,
|
max_length=64,
|
||||||
editable=False,
|
editable=False,
|
||||||
help_text=_("The checksum of the original document."),
|
help_text=_("The checksum of the original document."),
|
||||||
)
|
)
|
||||||
|
|
||||||
archive_checksum = models.CharField(
|
archive_checksum = models.CharField(
|
||||||
_("archive checksum"),
|
_("archive checksum"),
|
||||||
max_length=32,
|
max_length=64,
|
||||||
editable=False,
|
editable=False,
|
||||||
blank=True,
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ is an identity function that adds no overhead.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
import uuid
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
@@ -30,6 +29,7 @@ from django.utils import timezone
|
|||||||
|
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
from documents.models import PaperlessTask
|
from documents.models import PaperlessTask
|
||||||
|
from documents.utils import compute_checksum
|
||||||
from paperless.config import GeneralConfig
|
from paperless.config import GeneralConfig
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.sanity_checker")
|
logger = logging.getLogger("paperless.sanity_checker")
|
||||||
@@ -218,7 +218,7 @@ def _check_original(
|
|||||||
|
|
||||||
present_files.discard(source_path)
|
present_files.discard(source_path)
|
||||||
try:
|
try:
|
||||||
checksum = hashlib.md5(source_path.read_bytes()).hexdigest()
|
checksum = compute_checksum(source_path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
messages.error(doc.pk, f"Cannot read original file of document: {e}")
|
messages.error(doc.pk, f"Cannot read original file of document: {e}")
|
||||||
else:
|
else:
|
||||||
@@ -255,7 +255,7 @@ def _check_archive(
|
|||||||
|
|
||||||
present_files.discard(archive_path)
|
present_files.discard(archive_path)
|
||||||
try:
|
try:
|
||||||
checksum = hashlib.md5(archive_path.read_bytes()).hexdigest()
|
checksum = compute_checksum(archive_path)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
messages.error(
|
messages.error(
|
||||||
doc.pk,
|
doc.pk,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import hashlib
|
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
import uuid
|
import uuid
|
||||||
@@ -53,14 +52,15 @@ from documents.models import Tag
|
|||||||
from documents.models import WorkflowRun
|
from documents.models import WorkflowRun
|
||||||
from documents.models import WorkflowTrigger
|
from documents.models import WorkflowTrigger
|
||||||
from documents.plugins.base import ConsumeTaskPlugin
|
from documents.plugins.base import ConsumeTaskPlugin
|
||||||
from documents.plugins.base import ProgressManager
|
|
||||||
from documents.plugins.base import StopConsumeTaskError
|
from documents.plugins.base import StopConsumeTaskError
|
||||||
|
from documents.plugins.helpers import ProgressManager
|
||||||
from documents.plugins.helpers import ProgressStatusOptions
|
from documents.plugins.helpers import ProgressStatusOptions
|
||||||
from documents.sanity_checker import SanityCheckFailedException
|
from documents.sanity_checker import SanityCheckFailedException
|
||||||
from documents.signals import document_updated
|
from documents.signals import document_updated
|
||||||
from documents.signals.handlers import cleanup_document_deletion
|
from documents.signals.handlers import cleanup_document_deletion
|
||||||
from documents.signals.handlers import run_workflows
|
from documents.signals.handlers import run_workflows
|
||||||
from documents.signals.handlers import send_websocket_document_updated
|
from documents.signals.handlers import send_websocket_document_updated
|
||||||
|
from documents.utils import compute_checksum
|
||||||
from documents.workflows.utils import get_workflows_for_trigger
|
from documents.workflows.utils import get_workflows_for_trigger
|
||||||
from paperless.config import AIConfig
|
from paperless.config import AIConfig
|
||||||
from paperless.parsers import ParserContext
|
from paperless.parsers import ParserContext
|
||||||
@@ -328,8 +328,7 @@ def update_document_content_maybe_archive_file(document_id) -> None:
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
oldDocument = Document.objects.get(pk=document.pk)
|
oldDocument = Document.objects.get(pk=document.pk)
|
||||||
if parser.get_archive_path():
|
if parser.get_archive_path():
|
||||||
with Path(parser.get_archive_path()).open("rb") as f:
|
checksum = compute_checksum(parser.get_archive_path())
|
||||||
checksum = hashlib.md5(f.read()).hexdigest()
|
|
||||||
# I'm going to save first so that in case the file move
|
# I'm going to save first so that in case the file move
|
||||||
# fails, the database is rolled back.
|
# fails, the database is rolled back.
|
||||||
# We also don't use save() since that triggers the filehandling
|
# We also don't use save() since that triggers the filehandling
|
||||||
@@ -533,13 +532,13 @@ def check_scheduled_workflows() -> None:
|
|||||||
id__in=matched_ids,
|
id__in=matched_ids,
|
||||||
)
|
)
|
||||||
|
|
||||||
if documents.count() > 0:
|
if documents.exists():
|
||||||
documents = prefilter_documents_by_workflowtrigger(
|
documents = prefilter_documents_by_workflowtrigger(
|
||||||
documents,
|
documents,
|
||||||
trigger,
|
trigger,
|
||||||
)
|
)
|
||||||
|
|
||||||
if documents.count() > 0:
|
if documents.exists():
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Found {documents.count()} documents for trigger {trigger}",
|
f"Found {documents.count()} documents for trigger {trigger}",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<p>
|
<p>
|
||||||
{% translate "Please sign in." %}
|
{% translate "Please sign in." %}
|
||||||
{% if ACCOUNT_ALLOW_SIGNUPS %}
|
{% if ACCOUNT_ALLOW_SIGNUPS %}
|
||||||
<br/>{% blocktrans %}Don't have an account yet? <a href="{{ signup_url }}">Sign up</a>{% endblocktrans %}
|
<br/>{% translate "Don't have an account yet?" %} <a href="{{ signup_url }}">{% translate "Sign up" %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
{% endblock form_top_content %}
|
{% endblock form_top_content %}
|
||||||
@@ -25,12 +25,12 @@
|
|||||||
{% translate "Username" as i18n_username %}
|
{% translate "Username" as i18n_username %}
|
||||||
{% translate "Password" as i18n_password %}
|
{% translate "Password" as i18n_password %}
|
||||||
<div class="form-floating form-stacked-top">
|
<div class="form-floating form-stacked-top">
|
||||||
<input type="text" name="login" id="inputUsername" placeholder="{{ i18n_username }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus>
|
<input type="text" name="login" id="inputUsername" placeholder="{{ i18n_username|force_escape }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus>
|
||||||
<label for="inputUsername">{{ i18n_username }}</label>
|
<label for="inputUsername">{{ i18n_username|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating form-stacked-bottom">
|
<div class="form-floating form-stacked-bottom">
|
||||||
<input type="password" name="password" id="inputPassword" placeholder="{{ i18n_password }}" class="form-control" required>
|
<input type="password" name="password" id="inputPassword" placeholder="{{ i18n_password|force_escape }}" class="form-control" required>
|
||||||
<label for="inputPassword">{{ i18n_password }}</label>
|
<label for="inputPassword">{{ i18n_password|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid mt-3">
|
<div class="d-grid mt-3">
|
||||||
<button class="btn btn-lg btn-primary" type="submit">{% translate "Sign in" %}</button>
|
<button class="btn btn-lg btn-primary" type="submit">{% translate "Sign in" %}</button>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% translate "Email" as i18n_email %}
|
{% translate "Email" as i18n_email %}
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="email" name="{{form.email.name}}" id="inputEmail" placeholder="{{ i18n_email }}" class="form-control" required>
|
<input type="email" name="{{form.email.name}}" id="inputEmail" placeholder="{{ i18n_email|force_escape }}" class="form-control" required>
|
||||||
<label for="inputEmail">{{ i18n_email }}</label>
|
<label for="inputEmail">{{ i18n_email|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid mt-3">
|
<div class="d-grid mt-3">
|
||||||
<button class="btn btn-lg btn-primary" type="submit">{% translate "Send me instructions!" %}</button>
|
<button class="btn btn-lg btn-primary" type="submit">{% translate "Send me instructions!" %}</button>
|
||||||
|
|||||||
@@ -17,12 +17,12 @@
|
|||||||
{% translate "New Password" as i18n_new_password1 %}
|
{% translate "New Password" as i18n_new_password1 %}
|
||||||
{% translate "Confirm Password" as i18n_new_password2 %}
|
{% translate "Confirm Password" as i18n_new_password2 %}
|
||||||
<div class="form-floating form-stacked-top">
|
<div class="form-floating form-stacked-top">
|
||||||
<input type="password" name="{{form.password1.name}}" id="inputPassword1" placeholder="{{ i18n_new_password1 }}" class="form-control" required>
|
<input type="password" name="{{form.password1.name}}" id="inputPassword1" placeholder="{{ i18n_new_password1|force_escape }}" class="form-control" required>
|
||||||
<label for="inputPassword1">{{ i18n_new_password1 }}</label>
|
<label for="inputPassword1">{{ i18n_new_password1|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating form-stacked-bottom">
|
<div class="form-floating form-stacked-bottom">
|
||||||
<input type="password" name="{{form.password2.name}}" id="inputPassword2" placeholder="{{ i18n_new_password2 }}" class="form-control" required>
|
<input type="password" name="{{form.password2.name}}" id="inputPassword2" placeholder="{{ i18n_new_password2|force_escape }}" class="form-control" required>
|
||||||
<label for="inputPassword2">{{ i18n_new_password2 }}</label>
|
<label for="inputPassword2">{{ i18n_new_password2|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid mt-3">
|
<div class="d-grid mt-3">
|
||||||
<button class="btn btn-lg btn-primary" type="submit">{% translate "Change my password" %}</button>
|
<button class="btn btn-lg btn-primary" type="submit">{% translate "Change my password" %}</button>
|
||||||
|
|||||||
@@ -11,5 +11,5 @@
|
|||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
{% url 'account_login' as login_url %}
|
{% url 'account_login' as login_url %}
|
||||||
<p>{% blocktranslate %}Your new password has been set. You can now <a href="{{ login_url }}">log in</a>{% endblocktranslate %}.</p>
|
<p>{% translate "Your new password has been set. You can now" %} <a href="{{ login_url }}">{% translate "log in" %}</a>.</p>
|
||||||
{% endblock form_content %}
|
{% endblock form_content %}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{% block form_top_content %}
|
{% block form_top_content %}
|
||||||
{% if not FIRST_INSTALL %}
|
{% if not FIRST_INSTALL %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans %}Already have an account? <a href="{{ login_url }}">Sign in</a>{% endblocktrans %}
|
{% translate "Already have an account?" %} <a href="{{ login_url }}">{% translate "Sign in" %}</a>
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock form_top_content %}
|
{% endblock form_top_content %}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
{% if FIRST_INSTALL %}
|
{% if FIRST_INSTALL %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans %}Note: This is the first user account for this installation and will be granted superuser privileges.{% endblocktrans %}
|
{% translate "Note: This is the first user account for this installation and will be granted superuser privileges." %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% translate "Username" as i18n_username %}
|
{% translate "Username" as i18n_username %}
|
||||||
@@ -24,20 +24,20 @@
|
|||||||
{% translate "Password" as i18n_password1 %}
|
{% translate "Password" as i18n_password1 %}
|
||||||
{% translate "Password (again)" as i18n_password2 %}
|
{% translate "Password (again)" as i18n_password2 %}
|
||||||
<div class="form-floating form-stacked-top">
|
<div class="form-floating form-stacked-top">
|
||||||
<input type="text" name="username" id="inputUsername" placeholder="{{ i18n_username }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus>
|
<input type="text" name="username" id="inputUsername" placeholder="{{ i18n_username|force_escape }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus>
|
||||||
<label for="inputUsername">{{ i18n_username }}</label>
|
<label for="inputUsername">{{ i18n_username|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating form-stacked-middle">
|
<div class="form-floating form-stacked-middle">
|
||||||
<input type="email" name="email" id="inputEmail" placeholder="{{ i18n_email }}" class="form-control">
|
<input type="email" name="email" id="inputEmail" placeholder="{{ i18n_email|force_escape }}" class="form-control">
|
||||||
<label for="inputEmail">{{ i18n_email }}</label>
|
<label for="inputEmail">{{ i18n_email|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating form-stacked-middle">
|
<div class="form-floating form-stacked-middle">
|
||||||
<input type="password" name="password1" id="inputPassword1" placeholder="{{ i18n_password1 }}" class="form-control" required>
|
<input type="password" name="password1" id="inputPassword1" placeholder="{{ i18n_password1|force_escape }}" class="form-control" required>
|
||||||
<label for="inputPassword1">{{ i18n_password1 }}</label>
|
<label for="inputPassword1">{{ i18n_password1|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating form-stacked-bottom">
|
<div class="form-floating form-stacked-bottom">
|
||||||
<input type="password" name="password2" id="inputPassword2" placeholder="{{ i18n_password2 }}" class="form-control" required>
|
<input type="password" name="password2" id="inputPassword2" placeholder="{{ i18n_password2|force_escape }}" class="form-control" required>
|
||||||
<label for="inputPassword2">{{ i18n_password2 }}</label>
|
<label for="inputPassword2">{{ i18n_password2|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid mt-3">
|
<div class="d-grid mt-3">
|
||||||
<button class="btn btn-lg btn-primary" type="submit">{% translate "Sign up" %}</button>
|
<button class="btn btn-lg btn-primary" type="submit">{% translate "Sign up" %}</button>
|
||||||
|
|||||||
@@ -9,15 +9,15 @@
|
|||||||
|
|
||||||
{% block form_top_content %}
|
{% block form_top_content %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktranslate %}Your account is protected by two-factor authentication. Please enter an authenticator code:{% endblocktranslate %}
|
{% translate "Your account is protected by two-factor authentication. Please enter an authenticator code:" %}
|
||||||
</p>
|
</p>
|
||||||
{% endblock form_top_content %}
|
{% endblock form_top_content %}
|
||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
{% translate "Code" as i18n_code %}
|
{% translate "Code" as i18n_code %}
|
||||||
<div class="form-floating">
|
<div class="form-floating">
|
||||||
<input type="code" name="code" id="inputCode" autocomplete="one-time-code" placeholder="{{ i18n_code }}" class="form-control" required autofocus>
|
<input type="code" name="code" id="inputCode" autocomplete="one-time-code" placeholder="{{ i18n_code|force_escape }}" class="form-control" required autofocus>
|
||||||
<label for="inputCode">{{ i18n_code }}</label>
|
<label for="inputCode">{{ i18n_code|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid mt-3">
|
<div class="d-grid mt-3">
|
||||||
<button class="btn btn-lg btn-primary" type="submit">{% translate "Sign in" %}</button>
|
<button class="btn btn-lg btn-primary" type="submit">{% translate "Sign in" %}</button>
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
{% url 'account_login' as login_url %}
|
{% url 'account_login' as login_url %}
|
||||||
<p>{% blocktranslate %}An error occurred while attempting to login via your social network account. Back to the <a href="{{ login_url }}">login page</a>{% endblocktranslate %}</p>
|
<p>{% translate "An error occurred while attempting to login via your social network account. Back to the" %} <a href="{{ login_url }}">{% translate "login page" %}</a></p>
|
||||||
{% endblock form_content %}
|
{% endblock form_content %}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
<p>
|
<p>
|
||||||
{% blocktrans with provider.name as provider %}You are about to connect a new third-party account from {{ provider }}.{% endblocktrans %}
|
{% filter force_escape %}
|
||||||
|
{% blocktrans with provider=provider.name %}You are about to connect a new third-party account from {{ provider }}.{% endblocktrans %}
|
||||||
|
{% endfilter %}
|
||||||
</p>
|
</p>
|
||||||
<div class="d-grid mt-3">
|
<div class="d-grid mt-3">
|
||||||
<button class="btn btn-lg btn-primary" type="submit">{% translate "Continue" %}</button>
|
<button class="btn btn-lg btn-primary" type="submit">{% translate "Continue" %}</button>
|
||||||
|
|||||||
@@ -7,18 +7,20 @@
|
|||||||
|
|
||||||
{% block form_content %}
|
{% block form_content %}
|
||||||
<p>
|
<p>
|
||||||
|
{% filter force_escape %}
|
||||||
{% blocktrans with provider_name=account.get_provider.name %}You are about to use your {{ provider_name }} account to login.{% endblocktrans %}
|
{% blocktrans with provider_name=account.get_provider.name %}You are about to use your {{ provider_name }} account to login.{% endblocktrans %}
|
||||||
{% blocktrans %}As a final step, please complete the following form:{% endblocktrans %}
|
{% endfilter %}
|
||||||
|
{% translate "As a final step, please complete the following form:" %}
|
||||||
</p>
|
</p>
|
||||||
{% translate "Username" as i18n_username %}
|
{% translate "Username" as i18n_username %}
|
||||||
{% translate "Email (optional)" as i18n_email %}
|
{% translate "Email (optional)" as i18n_email %}
|
||||||
<div class="form-floating form-stacked-top">
|
<div class="form-floating form-stacked-top">
|
||||||
<input type="text" name="{{ form.username.name }}" id="inputUsername" placeholder="{{ i18n_username }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus value="{{ form.username.value }}">
|
<input type="text" name="{{ form.username.name }}" id="inputUsername" placeholder="{{ i18n_username|force_escape }}" class="form-control" autocorrect="off" autocapitalize="none" required autofocus value="{{ form.username.value }}">
|
||||||
<label for="inputUsername">{{ i18n_username }}</label>
|
<label for="inputUsername">{{ i18n_username|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-floating form-stacked-bottom">
|
<div class="form-floating form-stacked-bottom">
|
||||||
<input type="email" name="{{ form.email.name }}" id="inputEmail" placeholder="{{ i18n_email }}" class="form-control" autocorrect="off" autocapitalize="none" autofocus value="{{ form.email.value }}">
|
<input type="email" name="{{ form.email.name }}" id="inputEmail" placeholder="{{ i18n_email|force_escape }}" class="form-control" autocorrect="off" autocapitalize="none" autofocus value="{{ form.email.value }}">
|
||||||
<label for="inputEmail">{{ i18n_email }}</label>
|
<label for="inputEmail">{{ i18n_email|force_escape }}</label>
|
||||||
</div>
|
</div>
|
||||||
{% if redirect_field_value %}
|
{% if redirect_field_value %}
|
||||||
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
|
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
|
||||||
|
|||||||
@@ -82,8 +82,8 @@ def sample_doc(
|
|||||||
|
|
||||||
return DocumentFactory(
|
return DocumentFactory(
|
||||||
title="test",
|
title="test",
|
||||||
checksum="42995833e01aea9b3edee44bbfdd7ce1",
|
checksum="1093cf6e32adbd16b06969df09215d42c4a3a8938cc18b39455953f08d1ff2ab",
|
||||||
archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b",
|
archive_checksum="706124ecde3c31616992fa979caed17a726b1c9ccdba70e82a4ff796cea97ccf",
|
||||||
content="test content",
|
content="test content",
|
||||||
pk=1,
|
pk=1,
|
||||||
filename="0000001.pdf",
|
filename="0000001.pdf",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class DocumentFactory(DjangoModelFactory):
|
|||||||
model = Document
|
model = Document
|
||||||
|
|
||||||
title = factory.Faker("sentence", nb_words=4)
|
title = factory.Faker("sentence", nb_words=4)
|
||||||
checksum = factory.Faker("md5")
|
checksum = factory.Faker("sha256")
|
||||||
content = factory.Faker("paragraph")
|
content = factory.Faker("paragraph")
|
||||||
correspondent = None
|
correspondent = None
|
||||||
document_type = None
|
document_type = None
|
||||||
|
|||||||
@@ -261,8 +261,14 @@ class TestConsumer(
|
|||||||
|
|
||||||
self.assertIsFile(document.archive_path)
|
self.assertIsFile(document.archive_path)
|
||||||
|
|
||||||
self.assertEqual(document.checksum, "42995833e01aea9b3edee44bbfdd7ce1")
|
self.assertEqual(
|
||||||
self.assertEqual(document.archive_checksum, "62acb0bcbfbcaa62ca6ad3668e4e404b")
|
document.checksum,
|
||||||
|
"1093cf6e32adbd16b06969df09215d42c4a3a8938cc18b39455953f08d1ff2ab",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
document.archive_checksum,
|
||||||
|
"706124ecde3c31616992fa979caed17a726b1c9ccdba70e82a4ff796cea97ccf",
|
||||||
|
)
|
||||||
|
|
||||||
self.assertIsNotFile(filename)
|
self.assertIsNotFile(filename)
|
||||||
|
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ class TestExportImport(
|
|||||||
|
|
||||||
self.d1 = Document.objects.create(
|
self.d1 = Document.objects.create(
|
||||||
content="Content",
|
content="Content",
|
||||||
checksum="42995833e01aea9b3edee44bbfdd7ce1",
|
checksum="1093cf6e32adbd16b06969df09215d42c4a3a8938cc18b39455953f08d1ff2ab",
|
||||||
archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b",
|
archive_checksum="706124ecde3c31616992fa979caed17a726b1c9ccdba70e82a4ff796cea97ccf",
|
||||||
title="wow1",
|
title="wow1",
|
||||||
filename="0000001.pdf",
|
filename="0000001.pdf",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
@@ -72,21 +72,21 @@ class TestExportImport(
|
|||||||
)
|
)
|
||||||
self.d2 = Document.objects.create(
|
self.d2 = Document.objects.create(
|
||||||
content="Content",
|
content="Content",
|
||||||
checksum="9c9691e51741c1f4f41a20896af31770",
|
checksum="550d1bae0f746d4f7c6be07054eb20cc2f11988a58ef64ceae45e98f85e92a5b",
|
||||||
title="wow2",
|
title="wow2",
|
||||||
filename="0000002.pdf",
|
filename="0000002.pdf",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
)
|
)
|
||||||
self.d3 = Document.objects.create(
|
self.d3 = Document.objects.create(
|
||||||
content="Content",
|
content="Content",
|
||||||
checksum="d38d7ed02e988e072caf924e0f3fcb76",
|
checksum="f1ba6b7ff8548214a75adec228f5468a14fe187f445bc0b9485cbf1c35b15915",
|
||||||
title="wow2",
|
title="wow2",
|
||||||
filename="0000003.pdf",
|
filename="0000003.pdf",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
)
|
)
|
||||||
self.d4 = Document.objects.create(
|
self.d4 = Document.objects.create(
|
||||||
content="Content",
|
content="Content",
|
||||||
checksum="82186aaa94f0b98697d704b90fd1c072",
|
checksum="a81b16b6b313cfd7e60eb7b12598d1343b58622b4030cfa19a2724a02e98db1b",
|
||||||
title="wow_dec",
|
title="wow_dec",
|
||||||
filename="0000004.pdf",
|
filename="0000004.pdf",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
@@ -239,7 +239,7 @@ class TestExportImport(
|
|||||||
)
|
)
|
||||||
|
|
||||||
with Path(fname).open("rb") as f:
|
with Path(fname).open("rb") as f:
|
||||||
checksum = hashlib.md5(f.read()).hexdigest()
|
checksum = hashlib.sha256(f.read()).hexdigest()
|
||||||
self.assertEqual(checksum, element["fields"]["checksum"])
|
self.assertEqual(checksum, element["fields"]["checksum"])
|
||||||
|
|
||||||
# Generated field "content_length" should not be exported,
|
# Generated field "content_length" should not be exported,
|
||||||
@@ -253,7 +253,7 @@ class TestExportImport(
|
|||||||
self.assertIsFile(fname)
|
self.assertIsFile(fname)
|
||||||
|
|
||||||
with Path(fname).open("rb") as f:
|
with Path(fname).open("rb") as f:
|
||||||
checksum = hashlib.md5(f.read()).hexdigest()
|
checksum = hashlib.sha256(f.read()).hexdigest()
|
||||||
self.assertEqual(checksum, element["fields"]["archive_checksum"])
|
self.assertEqual(checksum, element["fields"]["archive_checksum"])
|
||||||
|
|
||||||
elif element["model"] == "documents.note":
|
elif element["model"] == "documents.note":
|
||||||
|
|||||||
@@ -277,8 +277,8 @@ class TestCommandImport(
|
|||||||
|
|
||||||
Document.objects.create(
|
Document.objects.create(
|
||||||
content="Content",
|
content="Content",
|
||||||
checksum="42995833e01aea9b3edee44bbfdd7ce1",
|
checksum="1093cf6e32adbd16b06969df09215d42c4a3a8938cc18b39455953f08d1ff2ab",
|
||||||
archive_checksum="62acb0bcbfbcaa62ca6ad3668e4e404b",
|
archive_checksum="706124ecde3c31616992fa979caed17a726b1c9ccdba70e82a4ff796cea97ccf",
|
||||||
title="wow1",
|
title="wow1",
|
||||||
filename="0000001.pdf",
|
filename="0000001.pdf",
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
|
|||||||
132
src/documents/tests/test_migration_sha256_checksums.py
Normal file
132
src/documents/tests/test_migration_sha256_checksums.py
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import hashlib
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import connection
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from documents.tests.utils import TestMigrations
|
||||||
|
|
||||||
|
|
||||||
|
def _sha256(data: bytes) -> str:
|
||||||
|
return hashlib.sha256(data).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSha256ChecksumDataMigration(TestMigrations):
|
||||||
|
"""recompute_checksums correctly updates document checksums from MD5 to SHA256."""
|
||||||
|
|
||||||
|
migrate_from = "0015_document_version_index_and_more"
|
||||||
|
migrate_to = "0016_sha256_checksums"
|
||||||
|
reset_sequences = True
|
||||||
|
|
||||||
|
ORIGINAL_CONTENT = b"original file content for sha256 migration test"
|
||||||
|
ARCHIVE_CONTENT = b"archive file content for sha256 migration test"
|
||||||
|
|
||||||
|
def setUpBeforeMigration(self, apps) -> None:
|
||||||
|
self._originals_dir = Path(tempfile.mkdtemp())
|
||||||
|
self._archive_dir = Path(tempfile.mkdtemp())
|
||||||
|
self._settings_override = override_settings(
|
||||||
|
ORIGINALS_DIR=self._originals_dir,
|
||||||
|
ARCHIVE_DIR=self._archive_dir,
|
||||||
|
)
|
||||||
|
self._settings_override.enable()
|
||||||
|
Document = apps.get_model("documents", "Document")
|
||||||
|
|
||||||
|
# doc1: original file present, no archive
|
||||||
|
(settings.ORIGINALS_DIR / "doc1.txt").write_bytes(self.ORIGINAL_CONTENT)
|
||||||
|
self.doc1_id = Document.objects.create(
|
||||||
|
title="Doc 1",
|
||||||
|
mime_type="text/plain",
|
||||||
|
filename="doc1.txt",
|
||||||
|
checksum="a" * 32,
|
||||||
|
).pk
|
||||||
|
|
||||||
|
# doc2: original and archive both present
|
||||||
|
(settings.ORIGINALS_DIR / "doc2.txt").write_bytes(self.ORIGINAL_CONTENT)
|
||||||
|
(settings.ARCHIVE_DIR / "doc2.pdf").write_bytes(self.ARCHIVE_CONTENT)
|
||||||
|
self.doc2_id = Document.objects.create(
|
||||||
|
title="Doc 2",
|
||||||
|
mime_type="text/plain",
|
||||||
|
filename="doc2.txt",
|
||||||
|
checksum="b" * 32,
|
||||||
|
archive_filename="doc2.pdf",
|
||||||
|
archive_checksum="c" * 32,
|
||||||
|
).pk
|
||||||
|
|
||||||
|
# doc3: original file missing — checksum must stay unchanged
|
||||||
|
self.doc3_id = Document.objects.create(
|
||||||
|
title="Doc 3",
|
||||||
|
mime_type="text/plain",
|
||||||
|
filename="missing_original.txt",
|
||||||
|
checksum="d" * 32,
|
||||||
|
).pk
|
||||||
|
|
||||||
|
# doc4: original present, archive_filename set but archive file missing
|
||||||
|
(settings.ORIGINALS_DIR / "doc4.txt").write_bytes(self.ORIGINAL_CONTENT)
|
||||||
|
self.doc4_id = Document.objects.create(
|
||||||
|
title="Doc 4",
|
||||||
|
mime_type="text/plain",
|
||||||
|
filename="doc4.txt",
|
||||||
|
checksum="e" * 32,
|
||||||
|
archive_filename="missing_archive.pdf",
|
||||||
|
archive_checksum="f" * 32,
|
||||||
|
).pk
|
||||||
|
|
||||||
|
# doc5: original present, archive_filename is None — archive_checksum must stay null
|
||||||
|
(settings.ORIGINALS_DIR / "doc5.txt").write_bytes(self.ORIGINAL_CONTENT)
|
||||||
|
self.doc5_id = Document.objects.create(
|
||||||
|
title="Doc 5",
|
||||||
|
mime_type="text/plain",
|
||||||
|
filename="doc5.txt",
|
||||||
|
checksum="0" * 32,
|
||||||
|
archive_filename=None,
|
||||||
|
archive_checksum=None,
|
||||||
|
).pk
|
||||||
|
|
||||||
|
def _fixture_teardown(self) -> None:
|
||||||
|
super()._fixture_teardown()
|
||||||
|
# Django's SQLite backend returns [] from sequence_reset_sql(), so
|
||||||
|
# reset_sequences=True flushes rows but never clears sqlite_sequence.
|
||||||
|
# Explicitly delete the entry so subsequent tests start from pk=1.
|
||||||
|
if connection.vendor == "sqlite":
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
"DELETE FROM sqlite_sequence WHERE name='documents_document'",
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self) -> None:
|
||||||
|
super().tearDown()
|
||||||
|
self._settings_override.disable()
|
||||||
|
shutil.rmtree(self._originals_dir, ignore_errors=True)
|
||||||
|
shutil.rmtree(self._archive_dir, ignore_errors=True)
|
||||||
|
|
||||||
|
def test_original_checksum_updated_to_sha256_when_file_exists(self) -> None:
|
||||||
|
Document = self.apps.get_model("documents", "Document")
|
||||||
|
doc = Document.objects.get(pk=self.doc1_id)
|
||||||
|
self.assertEqual(doc.checksum, _sha256(self.ORIGINAL_CONTENT))
|
||||||
|
|
||||||
|
def test_both_checksums_updated_when_original_and_archive_exist(self) -> None:
|
||||||
|
Document = self.apps.get_model("documents", "Document")
|
||||||
|
doc = Document.objects.get(pk=self.doc2_id)
|
||||||
|
self.assertEqual(doc.checksum, _sha256(self.ORIGINAL_CONTENT))
|
||||||
|
self.assertEqual(doc.archive_checksum, _sha256(self.ARCHIVE_CONTENT))
|
||||||
|
|
||||||
|
def test_checksum_unchanged_when_original_file_missing(self) -> None:
|
||||||
|
Document = self.apps.get_model("documents", "Document")
|
||||||
|
doc = Document.objects.get(pk=self.doc3_id)
|
||||||
|
self.assertEqual(doc.checksum, "d" * 32)
|
||||||
|
|
||||||
|
def test_archive_checksum_unchanged_when_archive_file_missing(self) -> None:
|
||||||
|
Document = self.apps.get_model("documents", "Document")
|
||||||
|
doc = Document.objects.get(pk=self.doc4_id)
|
||||||
|
# Original was updated (file exists)
|
||||||
|
self.assertEqual(doc.checksum, _sha256(self.ORIGINAL_CONTENT))
|
||||||
|
# Archive was not updated (file missing)
|
||||||
|
self.assertEqual(doc.archive_checksum, "f" * 32)
|
||||||
|
|
||||||
|
def test_archive_checksum_stays_null_when_no_archive_filename(self) -> None:
|
||||||
|
Document = self.apps.get_model("documents", "Document")
|
||||||
|
doc = Document.objects.get(pk=self.doc5_id)
|
||||||
|
self.assertIsNone(doc.archive_checksum)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import shutil
|
import shutil
|
||||||
from os import utime
|
from os import utime
|
||||||
@@ -128,3 +129,28 @@ def get_boolean(boolstr: str) -> bool:
|
|||||||
Return a boolean value from a string representation.
|
Return a boolean value from a string representation.
|
||||||
"""
|
"""
|
||||||
return bool(boolstr.lower() in ("yes", "y", "1", "t", "true"))
|
return bool(boolstr.lower() in ("yes", "y", "1", "t", "true"))
|
||||||
|
|
||||||
|
|
||||||
|
def compute_checksum(path: Path, chunk_size: int = 65536) -> str:
|
||||||
|
"""
|
||||||
|
Compute the SHA-256 checksum of a file.
|
||||||
|
|
||||||
|
Reads the file in chunks to avoid loading the entire file into memory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (Path): Path to the file to hash.
|
||||||
|
chunk_size (int, optional): Number of bytes to read per chunk.
|
||||||
|
Defaults to 65536.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Hexadecimal SHA-256 digest of the file contents.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: If the file does not exist.
|
||||||
|
OSError: If the file cannot be read.
|
||||||
|
"""
|
||||||
|
h = hashlib.sha256()
|
||||||
|
with path.open("rb") as f:
|
||||||
|
while chunk := f.read(chunk_size):
|
||||||
|
h.update(chunk)
|
||||||
|
return h.hexdigest()
|
||||||
|
|||||||
@@ -2027,7 +2027,10 @@ class UnifiedSearchViewSet(DocumentViewSet):
|
|||||||
except NotFound:
|
except NotFound:
|
||||||
raise
|
raise
|
||||||
except PermissionDenied as e:
|
except PermissionDenied as e:
|
||||||
return HttpResponseForbidden(str(e.detail))
|
invalid_more_like_id_message = _("Invalid more_like_id")
|
||||||
|
if str(e.detail) == str(invalid_more_like_id_message):
|
||||||
|
return HttpResponseForbidden(invalid_more_like_id_message)
|
||||||
|
return HttpResponseForbidden(_("Insufficient permissions."))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"An error occurred listing search results: {e!s}")
|
logger.warning(f"An error occurred listing search results: {e!s}")
|
||||||
return HttpResponseBadRequest(
|
return HttpResponseBadRequest(
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ def execute_password_removal_action(
|
|||||||
passwords = action.passwords
|
passwords = action.passwords
|
||||||
if not passwords:
|
if not passwords:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Password removal action %s has no passwords configured",
|
"Workflow action %s has no configured unlock values",
|
||||||
action.pk,
|
action.pk,
|
||||||
extra={"group": logging_group},
|
extra={"group": logging_group},
|
||||||
)
|
)
|
||||||
@@ -321,22 +321,23 @@ def execute_password_removal_action(
|
|||||||
user=document.owner,
|
user=document.owner,
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Removed password from document %s using workflow action %s",
|
"Unlocked document %s using workflow action %s",
|
||||||
document.pk,
|
document.pk,
|
||||||
action.pk,
|
action.pk,
|
||||||
extra={"group": logging_group},
|
extra={"group": logging_group},
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Password removal failed for document %s with supplied password: %s",
|
"Workflow action %s could not unlock document %s with one configured value",
|
||||||
|
action.pk,
|
||||||
document.pk,
|
document.pk,
|
||||||
e,
|
|
||||||
extra={"group": logging_group},
|
extra={"group": logging_group},
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.error(
|
logger.error(
|
||||||
"Password removal failed for document %s after trying all provided passwords",
|
"Workflow action %s could not unlock document %s with any configured value",
|
||||||
|
action.pk,
|
||||||
document.pk,
|
document.pk,
|
||||||
extra={"group": logging_group},
|
extra={"group": logging_group},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ msgid ""
|
|||||||
msgstr ""
|
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: 2026-03-22 13:54+0000\n"
|
"POT-Creation-Date: 2026-03-26 14:37+0000\n"
|
||||||
"PO-Revision-Date: 2022-02-17 04:17\n"
|
"PO-Revision-Date: 2022-02-17 04:17\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: English\n"
|
"Language-Team: English\n"
|
||||||
@@ -1301,7 +1301,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: documents/serialisers.py:463 documents/serialisers.py:815
|
#: documents/serialisers.py:463 documents/serialisers.py:815
|
||||||
#: documents/serialisers.py:2501 documents/views.py:1990
|
#: documents/serialisers.py:2501 documents/views.py:1990
|
||||||
#: paperless_mail/serialisers.py:143
|
#: documents/views.py:2033 paperless_mail/serialisers.py:143
|
||||||
msgid "Insufficient permissions."
|
msgid "Insufficient permissions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1341,7 +1341,7 @@ msgstr ""
|
|||||||
msgid "Duplicate document identifiers are not allowed."
|
msgid "Duplicate document identifiers are not allowed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2587 documents/views.py:3596
|
#: documents/serialisers.py:2587 documents/views.py:3599
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Documents not found: %(ids)s"
|
msgid "Documents not found: %(ids)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1383,13 +1383,18 @@ msgid "Please sign in."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/login.html:12
|
#: documents/templates/account/login.html:12
|
||||||
#, python-format
|
msgid "Don't have an account yet?"
|
||||||
msgid "Don't have an account yet? <a href=\"%(signup_url)s\">Sign up</a>"
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/templates/account/login.html:12
|
||||||
|
#: documents/templates/account/signup.html:43
|
||||||
|
#: documents/templates/socialaccount/signup.html:29
|
||||||
|
msgid "Sign up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/login.html:25
|
#: documents/templates/account/login.html:25
|
||||||
#: documents/templates/account/signup.html:22
|
#: documents/templates/account/signup.html:22
|
||||||
#: documents/templates/socialaccount/signup.html:13
|
#: documents/templates/socialaccount/signup.html:15
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1399,6 +1404,7 @@ msgid "Password"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/login.html:36
|
#: documents/templates/account/login.html:36
|
||||||
|
#: documents/templates/account/signup.html:11
|
||||||
#: documents/templates/mfa/authenticate.html:23
|
#: documents/templates/mfa/authenticate.html:23
|
||||||
msgid "Sign in"
|
msgid "Sign in"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1477,10 +1483,11 @@ msgid "Password reset complete."
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/password_reset_from_key_done.html:14
|
#: documents/templates/account/password_reset_from_key_done.html:14
|
||||||
#, python-format
|
msgid "Your new password has been set. You can now"
|
||||||
msgid ""
|
msgstr ""
|
||||||
"Your new password has been set. You can now <a href=\"%(login_url)s\">log "
|
|
||||||
"in</a>"
|
#: documents/templates/account/password_reset_from_key_done.html:14
|
||||||
|
msgid "log in"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/signup.html:5
|
#: documents/templates/account/signup.html:5
|
||||||
@@ -1488,8 +1495,7 @@ msgid "Paperless-ngx sign up"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/signup.html:11
|
#: documents/templates/account/signup.html:11
|
||||||
#, python-format
|
msgid "Already have an account?"
|
||||||
msgid "Already have an account? <a href=\"%(login_url)s\">Sign in</a>"
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/signup.html:19
|
#: documents/templates/account/signup.html:19
|
||||||
@@ -1499,7 +1505,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/signup.html:23
|
#: documents/templates/account/signup.html:23
|
||||||
#: documents/templates/socialaccount/signup.html:14
|
#: documents/templates/socialaccount/signup.html:16
|
||||||
msgid "Email (optional)"
|
msgid "Email (optional)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1507,11 +1513,6 @@ msgstr ""
|
|||||||
msgid "Password (again)"
|
msgid "Password (again)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/account/signup.html:43
|
|
||||||
#: documents/templates/socialaccount/signup.html:27
|
|
||||||
msgid "Sign up"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: documents/templates/index.html:61
|
#: documents/templates/index.html:61
|
||||||
msgid "Paperless-ngx is loading..."
|
msgid "Paperless-ngx is loading..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1556,18 +1557,21 @@ msgid "Paperless-ngx social account sign in"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/socialaccount/authentication_error.html:10
|
#: documents/templates/socialaccount/authentication_error.html:10
|
||||||
#, python-format
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"An error occurred while attempting to login via your social network account. "
|
"An error occurred while attempting to login via your social network account. "
|
||||||
"Back to the <a href=\"%(login_url)s\">login page</a>"
|
"Back to the"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/socialaccount/login.html:10
|
#: documents/templates/socialaccount/authentication_error.html:10
|
||||||
|
msgid "login page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: documents/templates/socialaccount/login.html:11
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "You are about to connect a new third-party account from %(provider)s."
|
msgid "You are about to connect a new third-party account from %(provider)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/socialaccount/login.html:13
|
#: documents/templates/socialaccount/login.html:15
|
||||||
msgid "Continue"
|
msgid "Continue"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1575,12 +1579,12 @@ msgstr ""
|
|||||||
msgid "Paperless-ngx social account sign up"
|
msgid "Paperless-ngx social account sign up"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/socialaccount/signup.html:10
|
#: documents/templates/socialaccount/signup.html:11
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "You are about to use your %(provider_name)s account to login."
|
msgid "You are about to use your %(provider_name)s account to login."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/templates/socialaccount/signup.html:11
|
#: documents/templates/socialaccount/signup.html:13
|
||||||
msgid "As a final step, please complete the following form:"
|
msgid "As a final step, please complete the following form:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1605,24 +1609,24 @@ msgstr ""
|
|||||||
msgid "Unable to parse URI {value}"
|
msgid "Unable to parse URI {value}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:1983
|
#: documents/views.py:1983 documents/views.py:2030
|
||||||
msgid "Invalid more_like_id"
|
msgid "Invalid more_like_id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:3608
|
#: documents/views.py:3611
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Insufficient permissions to share document %(id)s."
|
msgid "Insufficient permissions to share document %(id)s."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:3651
|
#: documents/views.py:3654
|
||||||
msgid "Bundle is already being processed."
|
msgid "Bundle is already being processed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:3708
|
#: documents/views.py:3711
|
||||||
msgid "The share link bundle is still being prepared. Please try again later."
|
msgid "The share link bundle is still being prepared. Please try again later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:3718
|
#: documents/views.py:3721
|
||||||
msgid "The share link bundle is unavailable."
|
msgid "The share link bundle is unavailable."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{% autoescape off %}
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
@@ -13,36 +12,34 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="grid gap-x-2 bg-slate-200 p-4">
|
<div class="grid gap-x-2 bg-slate-200 p-4">
|
||||||
|
|
||||||
<div class="col-start-9 col-span-4 row-start-1 text-right">{{ date }}</div>
|
<div class="col-start-9 col-span-4 row-start-1 text-right">{{ date|safe }}</div>
|
||||||
|
|
||||||
<div class="col-start-1 row-start-1 text-slate-400 text-right">{{ from_label }}</div>
|
<div class="col-start-1 row-start-1 text-slate-400 text-right">{{ from_label }}</div>
|
||||||
<div class="col-start-2 col-span-7 row-start-1">{{ from }}</div>
|
<div class="col-start-2 col-span-7 row-start-1">{{ from|safe }}</div>
|
||||||
|
|
||||||
<div class="col-start-1 row-start-2 text-slate-400 text-right">{{ subject_label }}</div>
|
<div class="col-start-1 row-start-2 text-slate-400 text-right">{{ subject_label }}</div>
|
||||||
<div class=" col-start-2 col-span-10 row-start-2 font-bold">{{ subject }}</div>
|
<div class=" col-start-2 col-span-10 row-start-2 font-bold">{{ subject|safe }}</div>
|
||||||
|
|
||||||
<div class="col-start-1 row-start-3 text-slate-400 text-right">{{ to_label }}</div>
|
<div class="col-start-1 row-start-3 text-slate-400 text-right">{{ to_label }}</div>
|
||||||
<div class="col-start-2 col-span-10 row-start-3 text-sm my-0.5">{{ to }}</div>
|
<div class="col-start-2 col-span-10 row-start-3 text-sm my-0.5">{{ to|safe }}</div>
|
||||||
|
|
||||||
<div class="col-start-1 row-start-4 text-slate-400 text-right">{{ cc_label }}</div>
|
<div class="col-start-1 row-start-4 text-slate-400 text-right">{{ cc_label }}</div>
|
||||||
<div class="col-start-2 col-span-10 row-start-4 text-sm my-0.5">{{ cc }}</div>
|
<div class="col-start-2 col-span-10 row-start-4 text-sm my-0.5">{{ cc|safe }}</div>
|
||||||
|
|
||||||
<div class="col-start-1 row-start-5 text-slate-400 text-right">{{ bcc_label }}</div>
|
<div class="col-start-1 row-start-5 text-slate-400 text-right">{{ bcc_label }}</div>
|
||||||
<div class="col-start-2 col-span-10 row-start-5" text-sm my-0.5>{{ bcc }}</div>
|
<div class="col-start-2 col-span-10 row-start-5" text-sm my-0.5>{{ bcc|safe }}</div>
|
||||||
|
|
||||||
<div class="col-start-1 row-start-6 text-slate-400 text-right">{{ attachments_label }}</div>
|
<div class="col-start-1 row-start-6 text-slate-400 text-right">{{ attachments_label }}</div>
|
||||||
<div class="col-start-2 col-span-10 row-start-6">{{ attachments }}</div>
|
<div class="col-start-2 col-span-10 row-start-6">{{ attachments|safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Separator-->
|
<!-- Separator-->
|
||||||
<div class="border-t border-solid border-b w-full h-[1px] box-content border-black mb-5 bg-slate-200"></div>
|
<div class="border-t border-solid border-b w-full h-[1px] box-content border-black mb-5 bg-slate-200"></div>
|
||||||
|
|
||||||
<!-- Content-->
|
<!-- Content-->
|
||||||
<div class="w-full break-words">{{ content }}</div>
|
<div class="w-full break-words">{{ content|safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
{% endautoescape %}
|
|
||||||
|
|||||||
@@ -191,7 +191,10 @@ class TestMailOAuth(
|
|||||||
).exists(),
|
).exists(),
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertIn("Error getting access token: test_error", cm.output[0])
|
self.assertIn(
|
||||||
|
"Error getting access token from OAuth provider",
|
||||||
|
cm.output[0],
|
||||||
|
)
|
||||||
|
|
||||||
def test_oauth_callback_view_insufficient_permissions(self) -> None:
|
def test_oauth_callback_view_insufficient_permissions(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -138,13 +138,16 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin):
|
|||||||
existing_account.refresh_from_db()
|
existing_account.refresh_from_db()
|
||||||
account.password = existing_account.password
|
account.password = existing_account.password
|
||||||
else:
|
else:
|
||||||
|
logger.error(
|
||||||
|
"Mail account connectivity test failed: Unable to refresh oauth token",
|
||||||
|
)
|
||||||
raise MailError("Unable to refresh oauth token")
|
raise MailError("Unable to refresh oauth token")
|
||||||
|
|
||||||
mailbox_login(M, account)
|
mailbox_login(M, account)
|
||||||
return Response({"success": True})
|
return Response({"success": True})
|
||||||
except MailError as e:
|
except MailError:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Mail account {account} test failed: {e}",
|
"Mail account connectivity test failed",
|
||||||
)
|
)
|
||||||
return HttpResponseBadRequest("Unable to connect to server")
|
return HttpResponseBadRequest("Unable to connect to server")
|
||||||
|
|
||||||
@@ -218,7 +221,7 @@ class OauthCallbackView(GenericAPIView):
|
|||||||
|
|
||||||
if code is None:
|
if code is None:
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Invalid oauth callback request, code: {code}, scope: {scope}",
|
"Invalid oauth callback request: missing code",
|
||||||
)
|
)
|
||||||
return HttpResponseBadRequest("Invalid request, see logs for more detail")
|
return HttpResponseBadRequest("Invalid request, see logs for more detail")
|
||||||
|
|
||||||
@@ -229,7 +232,7 @@ class OauthCallbackView(GenericAPIView):
|
|||||||
state = request.query_params.get("state", "")
|
state = request.query_params.get("state", "")
|
||||||
if not oauth_manager.validate_state(state):
|
if not oauth_manager.validate_state(state):
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Invalid oauth callback request received state: {state}, expected: {oauth_manager.state}",
|
"Invalid oauth callback request: state validation failed",
|
||||||
)
|
)
|
||||||
return HttpResponseBadRequest("Invalid request, see logs for more detail")
|
return HttpResponseBadRequest("Invalid request, see logs for more detail")
|
||||||
|
|
||||||
@@ -276,8 +279,8 @@ class OauthCallbackView(GenericAPIView):
|
|||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
f"{oauth_manager.oauth_redirect_url}?oauth_success=1&account_id={account.pk}",
|
f"{oauth_manager.oauth_redirect_url}?oauth_success=1&account_id={account.pk}",
|
||||||
)
|
)
|
||||||
except GetAccessTokenError as e:
|
except GetAccessTokenError:
|
||||||
logger.error(f"Error getting access token: {e}")
|
logger.error("Error getting access token from OAuth provider")
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
f"{oauth_manager.oauth_redirect_url}?oauth_success=0",
|
f"{oauth_manager.oauth_redirect_url}?oauth_success=0",
|
||||||
)
|
)
|
||||||
|
|||||||
52
uv.lock
generated
52
uv.lock
generated
@@ -361,31 +361,31 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cbor2"
|
name = "cbor2"
|
||||||
version = "5.8.0"
|
version = "5.9.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d9/8e/8b4fdde28e42ffcd741a37f4ffa9fb59cd4fe01625b544dfcfd9ccb54f01/cbor2-5.8.0.tar.gz", hash = "sha256:b19c35fcae9688ac01ef75bad5db27300c2537eb4ee00ed07e05d8456a0d4931", size = 107825, upload-time = "2025-12-30T18:44:22.455Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/bd/cb/09939728be094d155b5d4ac262e39877875f5f7e36eea66beb359f647bd0/cbor2-5.9.0.tar.gz", hash = "sha256:85c7a46279ac8f226e1059275221e6b3d0e370d2bb6bd0500f9780781615bcea", size = 111231, upload-time = "2026-03-22T15:56:50.638Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/4b/623435ef9b98e86b6956a41863d39ff4fe4d67983948b5834f55499681dd/cbor2-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ac191640093e6c7fbcb174c006ffec4106c3d8ab788e70272c1c4d933cbe11", size = 69875, upload-time = "2025-12-30T18:43:35.888Z" },
|
{ url = "https://files.pythonhosted.org/packages/43/aa/317c7118b8dda4c9563125c1a12c70c5b41e36677964a49c72b1aac061ec/cbor2-5.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0485d3372fc832c5e16d4eb45fa1a20fc53e806e6c29a1d2b0d3e176cedd52b9", size = 70578, upload-time = "2026-03-22T15:56:03.835Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/17/f664201080b2a7d0f57c16c8e9e5922013b92f202e294863ec7e75b7ff7f/cbor2-5.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fddee9103a17d7bed5753f0c7fc6663faa506eb953e50d8287804eccf7b048e6", size = 268316, upload-time = "2025-12-30T18:43:37.161Z" },
|
{ url = "https://files.pythonhosted.org/packages/31/43/fe29b1f897770011a5e7497f4523c2712282ee4a6cbf775ea6383fb7afb9/cbor2-5.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a9d6e4e0f988b0e766509a8071975a8ee99f930e14a524620bf38083106158d2", size = 268738, upload-time = "2026-03-22T15:56:05.222Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/e1/072745b4ff01afe9df2cd627f8fc51a1acedb5d3d1253765625d2929db91/cbor2-5.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d2ea26fad620aba5e88d7541be8b10c5034a55db9a23809b7cb49f36803f05b", size = 258874, upload-time = "2025-12-30T18:43:38.878Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/1a/e494568f3d8aafbcdfe361df44c3bcf5cdab5183e25ea08e3d3f9fcf4075/cbor2-5.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5326336f633cc89dfe543c78829c16c3a6449c2c03277d1ddba99086c3323363", size = 262571, upload-time = "2026-03-22T15:56:06.411Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/10/61c262b886d22b62c56e8aac6d10fa06d0953c997879ab882a31a624952b/cbor2-5.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:de68b4b310b072b082d317adc4c5e6910173a6d9455412e6183d72c778d1f54c", size = 261971, upload-time = "2025-12-30T18:43:40.401Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/2e/92acd6f87382fd44a34d9d7e85cc45372e6ba664040b72d1d9df648b25d0/cbor2-5.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e702b02d42a5ace45425b595ffe70fe35aebaf9a3cdfdc2c758b6189c744422", size = 262356, upload-time = "2026-03-22T15:56:08.236Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/42/b7862f5e64364b10ad120ea53e87ec7e891fb268cb99c572348e647cf7e9/cbor2-5.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:418d2cf0e03e90160fa1474c05a40fe228bbb4a92d1628bdbbd13a48527cb34d", size = 254151, upload-time = "2025-12-30T18:43:41.938Z" },
|
{ url = "https://files.pythonhosted.org/packages/3f/68/52c039a28688baeeb78b0be7483855e6c66ea05884a937444deede0c87b8/cbor2-5.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2372d357d403e7912f104ff085950ffc82a5854d6d717f1ca1ce16a40a0ef5a7", size = 257604, upload-time = "2026-03-22T15:56:09.835Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/4f/3a16e3e8fd7e5fd86751a4f1aad218a8d19a96e75ec3989c3e95a8fe1d8f/cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b3f91fa699a5ce22470e973601c62dd9d55dc3ca20ee446516ac075fcab27c9", size = 70270, upload-time = "2025-12-30T18:43:46.005Z" },
|
{ url = "https://files.pythonhosted.org/packages/ee/39/72d8a5a4b06565561ec28f4fcb41aff7bb77f51705c01f00b8254a2aca4f/cbor2-5.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f223dffb1bcdd2764665f04c1152943d9daa4bc124a576cd8dee1cad4264313", size = 71223, upload-time = "2026-03-22T15:56:13.68Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/81/0d0cf0796fe8081492a61c45278f03def21a929535a492dd97c8438f5dbe/cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857", size = 286242, upload-time = "2025-12-30T18:43:47.026Z" },
|
{ url = "https://files.pythonhosted.org/packages/09/fd/7ddf3d3153b54c69c3be77172b8d9aa3a9d74f62a7fbde614d53eaeed9a4/cbor2-5.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae6c706ac1d85a0b3cb3395308fd0c4d55e3202b4760773675957e93cdff45fc", size = 287865, upload-time = "2026-03-22T15:56:14.813Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/a9/fdab6c10190cfb8d639e01f2b168f2406fc847a2a6bc00e7de78c3381d0a/cbor2-5.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cff2a1999e49cd51c23d1b6786a012127fd8f722c5946e82bd7ab3eb307443f3", size = 285412, upload-time = "2025-12-30T18:43:48.563Z" },
|
{ url = "https://files.pythonhosted.org/packages/db/9d/7ede2cc42f9bb4260492e7d29d2aab781eacbbcfb09d983de1e695077199/cbor2-5.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4cd43d8fc374b31643b2830910f28177a606a7bc84975a62675dd3f2e320fc7b", size = 288246, upload-time = "2026-03-22T15:56:16.113Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/59/746a8e630996217a3afd523f583fcf7e3d16640d63f9a03f0f4e4f74b5b1/cbor2-5.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c4492160212374973cdc14e46f0565f2462721ef922b40f7ea11e7d613dfb2a", size = 278041, upload-time = "2025-12-30T18:43:49.92Z" },
|
{ url = "https://files.pythonhosted.org/packages/ce/9d/588ebc7c5bc5843f609b05fe07be8575c7dec987735b0bbc908ac9c1264a/cbor2-5.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4aa07b392cc3d76fb31c08a46a226b58c320d1c172ff3073e864409ced7bc50f", size = 280214, upload-time = "2026-03-22T15:56:17.519Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/a3/f3bbeb6dedd45c6e0cddd627ea790dea295eaf82c83f0e2159b733365ebd/cbor2-5.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:546c7c7c4c6bcdc54a59242e0e82cea8f332b17b4465ae628718fef1fce401ca", size = 278185, upload-time = "2025-12-30T18:43:51.192Z" },
|
{ url = "https://files.pythonhosted.org/packages/f7/a1/6fc8f4b15c6a27e7fbb7966c30c2b4b18c274a3221fa2f5e6235502d34bc/cbor2-5.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:971d425b3a23b75953d8853d5f9911bdeefa09d759ee3b5e6b07b5ff3cbd9073", size = 282162, upload-time = "2026-03-22T15:56:18.975Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/0d/5a3f20bafaefeb2c1903d961416f051c0950f0d09e7297a3aa6941596b29/cbor2-5.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d8d104480845e2f28c6165b4c961bbe58d08cb5638f368375cfcae051c28015", size = 70332, upload-time = "2025-12-30T18:43:54.694Z" },
|
{ url = "https://files.pythonhosted.org/packages/81/c5/4901e21a8afe9448fd947b11e8f383903207cd6dd0800e5f5a386838de5b/cbor2-5.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fbb06f34aa645b4deca66643bba3d400d20c15312d1fe88d429be60c1ab50f27", size = 71284, upload-time = "2026-03-22T15:56:22.836Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/66/177a3f089e69db69c987453ab4934086408c3338551e4984734597be9f80/cbor2-5.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43efee947e5ab67d406d6e0dc61b5dee9d2f5e89ae176f90677a3741a20ca2e7", size = 285985, upload-time = "2025-12-30T18:43:55.733Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/10/df643a381aebc3f05486de4813662bc58accb640fc3275cb276a75e89694/cbor2-5.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac684fe195c39821fca70d18afbf748f728aefbfbf88456018d299e559b8cae0", size = 287682, upload-time = "2026-03-22T15:56:24.024Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/8e/9e17b8e4ed80a2ce97e2dfa5915c169dbb31599409ddb830f514b57f96cc/cbor2-5.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be7ae582f50be539e09c134966d0fd63723fc4789b8dff1f6c2e3f24ae3eaf32", size = 285173, upload-time = "2025-12-30T18:43:57.321Z" },
|
{ url = "https://files.pythonhosted.org/packages/c6/0c/8aa6b766059ae4a0ca1ec3ff96fe3823a69a7be880dba2e249f7fbe2700b/cbor2-5.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a54fbb32cb828c214f7f333a707e4aec61182e7efdc06ea5d9596d3ecee624a", size = 288009, upload-time = "2026-03-22T15:56:25.305Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/33/9f92e107d78f88ac22723ac15d0259d220ba98c1d855e51796317f4c4114/cbor2-5.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50f5c709561a71ea7970b4cd2bf9eda4eccacc0aac212577080fdfe64183e7f5", size = 278395, upload-time = "2025-12-30T18:43:58.497Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/07/6236bc25c183a9cf7e8062e5dddf9eae9b0b14ebf14a58a69fe5a1e872c6/cbor2-5.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4753a6d1bc71054d9179557bc65740860f185095ccb401d46637fff028a5b3ec", size = 280437, upload-time = "2026-03-22T15:56:26.479Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2f/3f/46b80050a4a35ce5cf7903693864a9fdea7213567dc8faa6e25cb375c182/cbor2-5.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6790ecc73aa93e76d2d9076fc42bf91a9e69f2295e5fa702e776dbe986465bd", size = 278330, upload-time = "2025-12-30T18:43:59.656Z" },
|
{ url = "https://files.pythonhosted.org/packages/4e/0a/84328d23c3c68874ac6497edb9b1900579a1028efa54734df3f1762bbc15/cbor2-5.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:380e534482b843e43442b87d8777a7bf9bed20cb7526f89b780c3400f617304b", size = 282247, upload-time = "2026-03-22T15:56:28.644Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/0c/0654233d7543ac8a50f4785f172430ddc97538ba418eb305d6e529d1a120/cbor2-5.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ad72381477133046ce217617d839ea4e9454f8b77d9a6351b229e214102daeb7", size = 70710, upload-time = "2025-12-30T18:44:03.209Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/7d/9ccc36d10ef96e6038e48046ebe1ce35a1e7814da0e1e204d09e6ef09b8d/cbor2-5.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23606d31ba1368bd1b6602e3020ee88fe9523ca80e8630faf6b2fc904fd84560", size = 71500, upload-time = "2026-03-22T15:56:31.876Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/62/4671d24e557d7f5a74a01b422c538925140c0495e57decde7e566f91d029/cbor2-5.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6da25190fad3434ce99876b11d4ca6b8828df6ca232cf7344cd14ae1166fb718", size = 285005, upload-time = "2025-12-30T18:44:05.109Z" },
|
{ url = "https://files.pythonhosted.org/packages/70/e1/a6cca2cc72e13f00030c6a649f57ae703eb2c620806ab70c40db8eab33fa/cbor2-5.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0322296b9d52f55880e300ba8ba09ecf644303b99b51138bbb1c0fb644fa7c3e", size = 286953, upload-time = "2026-03-22T15:56:33.292Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/85/0c67d763a08e848c9a80d7e4723ba497cce676f41bc7ca1828ae90a0a872/cbor2-5.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c13919e3a24c5a6d286551fa288848a4cedc3e507c58a722ccd134e461217d99", size = 282435, upload-time = "2025-12-30T18:44:06.465Z" },
|
{ url = "https://files.pythonhosted.org/packages/08/3c/24cd5ef488a957d90e016f200a3aad820e4c2f85edd61c9fe4523007a1ee/cbor2-5.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:422817286c1d0ce947fb2f7eca9212b39bddd7231e8b452e2d2cc52f15332dba", size = 285454, upload-time = "2026-03-22T15:56:34.703Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/01/0650972b4dbfbebcfbe37cbba7fc3cd9019a8da6397ab3446e07175e342b/cbor2-5.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f8c40d32e5972047a777f9bf730870828f3cf1c43b3eb96fd0429c57a1d3b9e6", size = 277493, upload-time = "2025-12-30T18:44:07.609Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/35/dca96818494c0ba47cdd73e8d809b27fa91f8fa0ce32a068a09237687454/cbor2-5.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9a4907e0c3035bb8836116854ed8e56d8aef23909d601fa59706320897ec2551", size = 279441, upload-time = "2026-03-22T15:56:35.888Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/6c/7704a4f32adc7f10f3b41ec067f500a4458f7606397af5e4cf2d368fd288/cbor2-5.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7627894bc0b3d5d0807f31e3107e11b996205470c4429dc2bb4ef8bfe7f64e1e", size = 276085, upload-time = "2025-12-30T18:44:09.021Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/44/d3362378b16e53cf7e535a3f5aed8476e2109068154e24e31981ef5bde9e/cbor2-5.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fb7afe77f8d269e42d7c4b515c6fd14f1ccc0625379fb6829b269f493d16eddd", size = 279673, upload-time = "2026-03-22T15:56:37.08Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/4f/101071f880b4da05771128c0b89f41e334cff044dee05fb013c8f4be661c/cbor2-5.8.0-py3-none-any.whl", hash = "sha256:3727d80f539567b03a7aa11890e57798c67092c38df9e6c23abb059e0f65069c", size = 24374, upload-time = "2025-12-30T18:44:21.476Z" },
|
{ url = "https://files.pythonhosted.org/packages/42/ff/b83492b096fbef26e9cb62c1a4bf2d3cef579ea7b33138c6c37c4ae66f67/cbor2-5.9.0-py3-none-any.whl", hash = "sha256:27695cbd70c90b8de5c4a284642c2836449b14e2c2e07e3ffe0744cb7669a01b", size = 24627, upload-time = "2026-03-22T15:56:48.847Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4211,7 +4211,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.33.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "certifi", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
@@ -4219,9 +4219,9 @@ dependencies = [
|
|||||||
{ name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
{ name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
{ name = "urllib3", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
|
{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user