mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-11-01 01:05:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45e99cde5a | ||
|
|
7ea58eaf46 | ||
|
|
a19804a0b7 | ||
|
|
3289be7383 | ||
|
|
d98918dd97 | ||
|
|
34c88613f6 |
No files matched your search
+28
-28
@@ -1402,7 +1402,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">239,240</context>
|
<context context-type="linenumber">240,241</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/document.ts</context>
|
<context context-type="sourcefile">src/app/data/document.ts</context>
|
||||||
@@ -1733,7 +1733,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">248,249</context>
|
<context context-type="linenumber">249,250</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||||
@@ -1768,7 +1768,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">257,258</context>
|
<context context-type="linenumber">258,259</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||||
@@ -1811,7 +1811,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">221,222</context>
|
<context context-type="linenumber">222,223</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.html</context>
|
||||||
@@ -2289,7 +2289,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">266,267</context>
|
<context context-type="linenumber">267,268</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/manage/share-links/share-link-bundle-list/share-link-bundle-list.component.html</context>
|
||||||
@@ -4115,7 +4115,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">388</context>
|
<context context-type="linenumber">391</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7515883357904500238" datatype="html">
|
<trans-unit id="7515883357904500238" datatype="html">
|
||||||
@@ -4389,7 +4389,7 @@
|
|||||||
<source>Create new field</source>
|
<source>Create new field</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/custom-fields-dropdown/custom-fields-dropdown.component.html</context>
|
||||||
<context context-type="linenumber">20</context>
|
<context context-type="linenumber">19</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6973528734330066202" datatype="html">
|
<trans-unit id="6973528734330066202" datatype="html">
|
||||||
@@ -4786,7 +4786,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">275,276</context>
|
<context context-type="linenumber">276,277</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/document.ts</context>
|
<context context-type="sourcefile">src/app/data/document.ts</context>
|
||||||
@@ -6604,7 +6604,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">218,219</context>
|
<context context-type="linenumber">219,220</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
<context context-type="sourcefile">src/app/components/document-list/filter-editor/filter-editor.component.ts</context>
|
||||||
@@ -8410,7 +8410,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.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="78870852467682010" datatype="html">
|
<trans-unit id="78870852467682010" datatype="html">
|
||||||
@@ -8425,7 +8425,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">360</context>
|
<context context-type="linenumber">363</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="157572966557284263" datatype="html">
|
<trans-unit id="157572966557284263" datatype="html">
|
||||||
@@ -8440,7 +8440,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">367</context>
|
<context context-type="linenumber">370</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="883965278435032344" datatype="html">
|
<trans-unit id="883965278435032344" datatype="html">
|
||||||
@@ -8458,7 +8458,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">388</context>
|
<context context-type="linenumber">391</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="872092479747931526" datatype="html">
|
<trans-unit id="872092479747931526" datatype="html">
|
||||||
@@ -9919,7 +9919,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">336</context>
|
<context context-type="linenumber">339</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="106713086593101376" datatype="html">
|
<trans-unit id="106713086593101376" datatype="html">
|
||||||
@@ -10234,21 +10234,21 @@
|
|||||||
<source>Sort by title</source>
|
<source>Sort by title</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">213</context>
|
<context context-type="linenumber">214</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6232673011753681091" datatype="html">
|
<trans-unit id="6232673011753681091" datatype="html">
|
||||||
<source>Sort by owner</source>
|
<source>Sort by owner</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">226</context>
|
<context context-type="linenumber">227</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3715596725146409911" datatype="html">
|
<trans-unit id="3715596725146409911" datatype="html">
|
||||||
<source>Owner</source>
|
<source>Owner</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">230,231</context>
|
<context context-type="linenumber">231,232</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/document.ts</context>
|
<context context-type="sourcefile">src/app/data/document.ts</context>
|
||||||
@@ -10263,49 +10263,49 @@
|
|||||||
<source>Sort by notes</source>
|
<source>Sort by notes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">235</context>
|
<context context-type="linenumber">236</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5499001829734502606" datatype="html">
|
<trans-unit id="5499001829734502606" datatype="html">
|
||||||
<source>Sort by document type</source>
|
<source>Sort by document type</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">244</context>
|
<context context-type="linenumber">245</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6213829731736042759" datatype="html">
|
<trans-unit id="6213829731736042759" datatype="html">
|
||||||
<source>Sort by storage path</source>
|
<source>Sort by storage path</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">253</context>
|
<context context-type="linenumber">254</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3406167410329973166" datatype="html">
|
<trans-unit id="3406167410329973166" datatype="html">
|
||||||
<source>Sort by created date</source>
|
<source>Sort by created date</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">262</context>
|
<context context-type="linenumber">263</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3769035778779263084" datatype="html">
|
<trans-unit id="3769035778779263084" datatype="html">
|
||||||
<source>Sort by added date</source>
|
<source>Sort by added date</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">271</context>
|
<context context-type="linenumber">272</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4874754501044009042" datatype="html">
|
<trans-unit id="4874754501044009042" datatype="html">
|
||||||
<source>Sort by number of pages</source>
|
<source>Sort by number of pages</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">280</context>
|
<context context-type="linenumber">281</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3817498941817715969" datatype="html">
|
<trans-unit id="3817498941817715969" datatype="html">
|
||||||
<source>Pages</source>
|
<source>Pages</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">284,285</context>
|
<context context-type="linenumber">285,286</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/data/document.ts</context>
|
<context context-type="sourcefile">src/app/data/document.ts</context>
|
||||||
@@ -10324,28 +10324,28 @@
|
|||||||
<source> Shared </source>
|
<source> Shared </source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">288</context>
|
<context context-type="linenumber">289</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5083658411133224968" datatype="html">
|
<trans-unit id="5083658411133224968" datatype="html">
|
||||||
<source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source>
|
<source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">294,295</context>
|
<context context-type="linenumber">295,296</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3420321797707163677" datatype="html">
|
<trans-unit id="3420321797707163677" datatype="html">
|
||||||
<source>Preview document</source>
|
<source>Preview document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">328</context>
|
<context context-type="linenumber">327</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2179847500064178686" datatype="html">
|
<trans-unit id="2179847500064178686" datatype="html">
|
||||||
<source>Edit document</source>
|
<source>Edit document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
|
||||||
<context context-type="linenumber">331</context>
|
<context context-type="linenumber">335</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="4512084577073831437" datatype="html">
|
<trans-unit id="4512084577073831437" datatype="html">
|
||||||
|
|||||||
+2
-4
@@ -11,14 +11,12 @@
|
|||||||
</div>
|
</div>
|
||||||
@for (field of filteredFields; track field.id) {
|
@for (field of filteredFields; track field.id) {
|
||||||
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button>
|
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="addField(field)" #button>
|
||||||
<small class="d-flex">{{field.name}} <small class="ms-auto text-muted">{{getDataTypeLabel(field.data_type)}}</small></small>
|
<div class="d-flex">{{field.name}} <small class="ms-auto text-muted">{{getDataTypeLabel(field.data_type)}}</small></div>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (!filterText?.length || filteredFields.length === 0) {
|
@if (!filterText?.length || filteredFields.length === 0) {
|
||||||
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
|
<button type="button" class="list-group-item list-group-item-action bg-light" (click)="createField(filterText)" [disabled]="!canCreateFields" #button>
|
||||||
<small>
|
<i-bs width=".9em" height=".9em" name="asterisk" class="me-1"></i-bs><ng-container i18n>Create new field</ng-container>
|
||||||
<i-bs width=".9em" height=".9em" name="asterisk" class="me-1"></i-bs><ng-container i18n>Create new field</ng-container>
|
|
||||||
</small>
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -208,7 +208,8 @@
|
|||||||
i18n>Correspondent</th>
|
i18n>Correspondent</th>
|
||||||
}
|
}
|
||||||
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||||
<th class="cursor-pointer ps-5"
|
<th style="width: 2rem"><!-- Preview --></th>
|
||||||
|
<th class="cursor-pointer"
|
||||||
pngxSortable="title"
|
pngxSortable="title"
|
||||||
title="Sort by title" i18n-title
|
title="Sort by title" i18n-title
|
||||||
[currentSortField]="list.sortField"
|
[currentSortField]="list.sortField"
|
||||||
@@ -321,15 +322,17 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
|
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||||
|
<td class="pe-0 text-end align-baseline" (mouseleave)="popupPreview.close()">
|
||||||
|
<pngx-preview-popup [document]="d" linkClasses="btn btn-sm btn-link text-secondary px-1" linkTitle="Preview document" (click)="$event.stopPropagation()" i18n-linkTitle #popupPreview>
|
||||||
|
<i-bs name="eye"></i-bs>
|
||||||
|
</pngx-preview-popup>
|
||||||
|
</td>
|
||||||
|
}
|
||||||
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
|
@if (activeDisplayFields.includes(DisplayField.TITLE) || activeDisplayFields.includes(DisplayField.TAGS)) {
|
||||||
<td width="30%" class="position-relative" [class.ps-5]="activeDisplayFields.includes(DisplayField.TITLE)">
|
<td width="30%" class="align-baseline">
|
||||||
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
@if (activeDisplayFields.includes(DisplayField.TITLE)) {
|
||||||
<div class="d-inline-block" (mouseleave)="popupPreview.close()">
|
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
||||||
<pngx-preview-popup class="position-absolute start-0" [document]="d" linkClasses="btn btn-sm btn-link text-secondary" linkTitle="Preview document" (click)="$event.stopPropagation()" i18n-linkTitle #popupPreview>
|
|
||||||
<i-bs name="eye"></i-bs>
|
|
||||||
</pngx-preview-popup>
|
|
||||||
<a routerLink="/documents/{{d.id}}" title="Edit document" i18n-title style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
@if (activeDisplayFields.includes(DisplayField.TAGS)) {
|
@if (activeDisplayFields.includes(DisplayField.TAGS)) {
|
||||||
@for (tagID of d.tags; track tagID) {
|
@for (tagID of d.tags; track tagID) {
|
||||||
|
|||||||
@@ -87,10 +87,6 @@ $paperless-card-breakpoints: (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table .ps-5 {
|
|
||||||
padding-left: 2.2rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export interface SystemStatus {
|
|||||||
status: SystemStatusItemStatus
|
status: SystemStatusItemStatus
|
||||||
error?: string
|
error?: string
|
||||||
migration_status: {
|
migration_status: {
|
||||||
latest_migration: string
|
latest_migration: string | null
|
||||||
unapplied_migrations: string[]
|
unapplied_migrations: string[]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -476,13 +476,13 @@ def update_filename_and_move_files(
|
|||||||
return
|
return
|
||||||
|
|
||||||
with FileLock(settings.MEDIA_LOCK):
|
with FileLock(settings.MEDIA_LOCK):
|
||||||
try:
|
# If this was waiting for the lock, the filename or archive_filename
|
||||||
# If this was waiting for the lock, the filename or archive_filename
|
# of this document may have been updated. This happens if multiple updates
|
||||||
# of this document may have been updated. This happens if multiple updates
|
# get queued from the UI for the same document
|
||||||
# get queued from the UI for the same document
|
# So freshen up the data before doing anything
|
||||||
# So freshen up the data before doing anything
|
instance.refresh_from_db()
|
||||||
instance.refresh_from_db()
|
|
||||||
|
|
||||||
|
try:
|
||||||
old_filename = instance.filename
|
old_filename = instance.filename
|
||||||
old_source_path = instance.source_path
|
old_source_path = instance.source_path
|
||||||
move_original = False
|
move_original = False
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ def localize_date(value: date | datetime | str, format: str, locale: str) -> str
|
|||||||
|
|
||||||
def format_datetime(value: str | datetime, format: str) -> str:
|
def format_datetime(value: str | datetime, format: str) -> str:
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = parse_date(value)
|
parsed = parse_datetime(value) or parse_date(value)
|
||||||
|
if parsed is None:
|
||||||
|
raise ValueError(f"Invalid date or datetime string: {value!r}")
|
||||||
|
return parsed.strftime(format=format)
|
||||||
return value.strftime(format=format)
|
return value.strftime(format=format)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -539,3 +539,50 @@ class TestSystemStatus(APITestCase):
|
|||||||
self.assertEqual(response.data["tasks"]["summary"]["success_count"], 1)
|
self.assertEqual(response.data["tasks"]["summary"]["success_count"], 1)
|
||||||
self.assertEqual(response.data["tasks"]["summary"]["failure_count"], 1)
|
self.assertEqual(response.data["tasks"]["summary"]["failure_count"], 1)
|
||||||
self.assertEqual(response.data["tasks"]["summary"]["pending_count"], 1)
|
self.assertEqual(response.data["tasks"]["summary"]["pending_count"], 1)
|
||||||
|
|
||||||
|
def test_system_status_reports_database_error(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Reading the migration state fails
|
||||||
|
WHEN:
|
||||||
|
- The system status is requested
|
||||||
|
THEN:
|
||||||
|
- The response reports the database error rather than a server error
|
||||||
|
"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
with mock.patch(
|
||||||
|
"documents.views.MigrationLoader",
|
||||||
|
side_effect=Exception("database is down"),
|
||||||
|
):
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
database = response.data["database"]
|
||||||
|
self.assertEqual(database["status"], "ERROR")
|
||||||
|
self.assertIsNotNone(database["error"])
|
||||||
|
self.assertEqual(
|
||||||
|
database["migration_status"],
|
||||||
|
{"latest_migration": None, "unapplied_migrations": []},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_system_status_migration_query_error(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- The migration graph loads but reading the applied migrations fails
|
||||||
|
WHEN:
|
||||||
|
- The system status is requested
|
||||||
|
THEN:
|
||||||
|
- No migrations are reported as unapplied, since the state is unknown
|
||||||
|
"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
with mock.patch("documents.views.MigrationRecorder") as recorder:
|
||||||
|
recorder.Migration.objects.all.side_effect = Exception("query failed")
|
||||||
|
response = self.client.get(self.ENDPOINT)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||||
|
database = response.data["database"]
|
||||||
|
self.assertEqual(database["status"], "ERROR")
|
||||||
|
self.assertEqual(
|
||||||
|
database["migration_status"],
|
||||||
|
{"latest_migration": None, "unapplied_migrations": []},
|
||||||
|
)
|
||||||
@@ -31,6 +31,7 @@ from documents.serialisers import DocumentSerializer
|
|||||||
from documents.tasks import empty_trash
|
from documents.tasks import empty_trash
|
||||||
from paperless_testing.assertions import FileSystemAssertsMixin
|
from paperless_testing.assertions import FileSystemAssertsMixin
|
||||||
from paperless_testing.dirs import DirectoriesMixin
|
from paperless_testing.dirs import DirectoriesMixin
|
||||||
|
from paperless_testing.dirs import PaperlessDirs
|
||||||
from paperless_testing.factories import DocumentFactory
|
from paperless_testing.factories import DocumentFactory
|
||||||
from paperless_testing.factories import UserFactory
|
from paperless_testing.factories import UserFactory
|
||||||
|
|
||||||
@@ -2134,3 +2135,28 @@ class TestPathDateLocalization:
|
|||||||
with override_settings(FILENAME_FORMAT=filename_format):
|
with override_settings(FILENAME_FORMAT=filename_format):
|
||||||
filename = generate_filename(document)
|
filename = generate_filename(document)
|
||||||
assert filename == Path(expected_filename)
|
assert filename == Path(expected_filename)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_refresh_failure_surfaces_database_error(
|
||||||
|
paperless_dirs: PaperlessDirs,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A document with a stored filename
|
||||||
|
WHEN:
|
||||||
|
- Saving it triggers a filename update and refreshing it from the database fails
|
||||||
|
THEN:
|
||||||
|
- The database error is raised, not an unbound variable error from the rollback
|
||||||
|
"""
|
||||||
|
document = DocumentFactory(filename="some.pdf")
|
||||||
|
|
||||||
|
with (
|
||||||
|
mock.patch.object(
|
||||||
|
Document,
|
||||||
|
"refresh_from_db",
|
||||||
|
side_effect=DatabaseError("database is locked"),
|
||||||
|
),
|
||||||
|
pytest.raises(DatabaseError, match="database is locked"),
|
||||||
|
):
|
||||||
|
document.save()
|
||||||
@@ -4,6 +4,7 @@ from typing import Literal
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from documents.templating.filters import format_datetime
|
||||||
from documents.templating.filters import localize_date
|
from documents.templating.filters import localize_date
|
||||||
|
|
||||||
|
|
||||||
@@ -294,3 +295,23 @@ class TestDateLocalization:
|
|||||||
Tests `localize_date` with `date` string across different locales and formats.
|
Tests `localize_date` with `date` string across different locales and formats.
|
||||||
"""
|
"""
|
||||||
assert localize_date(value, format_style, locale_str) == expected_output
|
assert localize_date(value, format_style, locale_str) == expected_output
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatDatetime:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("value", "expected"),
|
||||||
|
[
|
||||||
|
pytest.param("2026-09-21", "2026-09-21 00:00", id="date"),
|
||||||
|
pytest.param(
|
||||||
|
"2026-09-21T13:45:00+00:00",
|
||||||
|
"2026-09-21 13:45",
|
||||||
|
id="datetime",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parses_iso_strings(self, value: str, expected: str) -> None:
|
||||||
|
assert format_datetime(value, "%Y-%m-%d %H:%M") == expected
|
||||||
|
|
||||||
|
def test_rejects_unparseable_string(self) -> None:
|
||||||
|
with pytest.raises(ValueError, match="not a date"):
|
||||||
|
format_datetime("not a date", "%Y")
|
||||||
+10
-3
@@ -5268,7 +5268,9 @@ class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Custom
|
|||||||
"migration_status": inline_serializer(
|
"migration_status": inline_serializer(
|
||||||
name="MigrationStatus",
|
name="MigrationStatus",
|
||||||
fields={
|
fields={
|
||||||
"latest_migration": serializers.CharField(),
|
"latest_migration": serializers.CharField(
|
||||||
|
allow_null=True,
|
||||||
|
),
|
||||||
"unapplied_migrations": serializers.ListSerializer(
|
"unapplied_migrations": serializers.ListSerializer(
|
||||||
child=serializers.CharField(),
|
child=serializers.CharField(),
|
||||||
),
|
),
|
||||||
@@ -5343,6 +5345,8 @@ class SystemStatusView(PassUserMixin):
|
|||||||
db_conn = connections["default"]
|
db_conn = connections["default"]
|
||||||
db_url = str(db_conn.settings_dict["NAME"])
|
db_url = str(db_conn.settings_dict["NAME"])
|
||||||
db_error = None
|
db_error = None
|
||||||
|
all_migrations: list[str] = []
|
||||||
|
applied_migrations: list[str] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
db_conn.ensure_connection()
|
db_conn.ensure_connection()
|
||||||
@@ -5353,7 +5357,8 @@ class SystemStatusView(PassUserMixin):
|
|||||||
f"{m.app}.{m.name}"
|
f"{m.app}.{m.name}"
|
||||||
for m in MigrationRecorder.Migration.objects.all().order_by("id")
|
for m in MigrationRecorder.Migration.objects.all().order_by("id")
|
||||||
]
|
]
|
||||||
except Exception as e: # pragma: no cover
|
except Exception as e:
|
||||||
|
all_migrations = []
|
||||||
applied_migrations = []
|
applied_migrations = []
|
||||||
db_status = "ERROR"
|
db_status = "ERROR"
|
||||||
logger.exception(
|
logger.exception(
|
||||||
@@ -5549,7 +5554,9 @@ class SystemStatusView(PassUserMixin):
|
|||||||
"status": db_status,
|
"status": db_status,
|
||||||
"error": db_error,
|
"error": db_error,
|
||||||
"migration_status": {
|
"migration_status": {
|
||||||
"latest_migration": applied_migrations[-1],
|
"latest_migration": (
|
||||||
|
applied_migrations[-1] if applied_migrations else None
|
||||||
|
),
|
||||||
"unapplied_migrations": [
|
"unapplied_migrations": [
|
||||||
m for m in all_migrations if m not in applied_migrations
|
m for m in all_migrations if m not in applied_migrations
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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-10-31 04:47+0000\n"
|
"POT-Creation-Date: 2026-10-31 16:33+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"
|
||||||
@@ -2274,151 +2274,151 @@ msgstr ""
|
|||||||
msgid "paperless application settings"
|
msgid "paperless application settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:566
|
#: paperless/settings/__init__.py:571
|
||||||
msgid "English (US)"
|
msgid "English (US)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:567
|
#: paperless/settings/__init__.py:572
|
||||||
msgid "Arabic"
|
msgid "Arabic"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:568
|
#: paperless/settings/__init__.py:573
|
||||||
msgid "Afrikaans"
|
msgid "Afrikaans"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:569
|
#: paperless/settings/__init__.py:574
|
||||||
msgid "Belarusian"
|
msgid "Belarusian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:570
|
#: paperless/settings/__init__.py:575
|
||||||
msgid "Bulgarian"
|
msgid "Bulgarian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:571
|
#: paperless/settings/__init__.py:576
|
||||||
msgid "Catalan"
|
msgid "Catalan"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:572
|
#: paperless/settings/__init__.py:577
|
||||||
msgid "Czech"
|
msgid "Czech"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:573
|
#: paperless/settings/__init__.py:578
|
||||||
msgid "Danish"
|
msgid "Danish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:574
|
#: paperless/settings/__init__.py:579
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:575
|
#: paperless/settings/__init__.py:580
|
||||||
msgid "Greek"
|
msgid "Greek"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:576
|
#: paperless/settings/__init__.py:581
|
||||||
msgid "English (GB)"
|
msgid "English (GB)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:577
|
#: paperless/settings/__init__.py:582
|
||||||
msgid "Spanish"
|
msgid "Spanish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:578
|
#: paperless/settings/__init__.py:583
|
||||||
msgid "Persian"
|
msgid "Persian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:579
|
#: paperless/settings/__init__.py:584
|
||||||
msgid "Finnish"
|
msgid "Finnish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:580
|
#: paperless/settings/__init__.py:585
|
||||||
msgid "French"
|
msgid "French"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:581
|
#: paperless/settings/__init__.py:586
|
||||||
msgid "Hungarian"
|
msgid "Hungarian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:582
|
#: paperless/settings/__init__.py:587
|
||||||
msgid "Indonesian"
|
msgid "Indonesian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:583
|
#: paperless/settings/__init__.py:588
|
||||||
msgid "Italian"
|
msgid "Italian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:584
|
#: paperless/settings/__init__.py:589
|
||||||
msgid "Japanese"
|
msgid "Japanese"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:585
|
#: paperless/settings/__init__.py:590
|
||||||
msgid "Korean"
|
msgid "Korean"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:586
|
#: paperless/settings/__init__.py:591
|
||||||
msgid "Luxembourgish"
|
msgid "Luxembourgish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:587
|
#: paperless/settings/__init__.py:592
|
||||||
msgid "Norwegian"
|
msgid "Norwegian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:588
|
#: paperless/settings/__init__.py:593
|
||||||
msgid "Dutch"
|
msgid "Dutch"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:589
|
#: paperless/settings/__init__.py:594
|
||||||
msgid "Polish"
|
msgid "Polish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:590
|
#: paperless/settings/__init__.py:595
|
||||||
msgid "Portuguese (Brazil)"
|
msgid "Portuguese (Brazil)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:591
|
#: paperless/settings/__init__.py:596
|
||||||
msgid "Portuguese"
|
msgid "Portuguese"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:592
|
#: paperless/settings/__init__.py:597
|
||||||
msgid "Romanian"
|
msgid "Romanian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:593
|
#: paperless/settings/__init__.py:598
|
||||||
msgid "Russian"
|
msgid "Russian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:594
|
#: paperless/settings/__init__.py:599
|
||||||
msgid "Slovak"
|
msgid "Slovak"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:595
|
#: paperless/settings/__init__.py:600
|
||||||
msgid "Slovenian"
|
msgid "Slovenian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:596
|
#: paperless/settings/__init__.py:601
|
||||||
msgid "Serbian"
|
msgid "Serbian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:597
|
#: paperless/settings/__init__.py:602
|
||||||
msgid "Swedish"
|
msgid "Swedish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:598
|
#: paperless/settings/__init__.py:603
|
||||||
msgid "Turkish"
|
msgid "Turkish"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:599
|
#: paperless/settings/__init__.py:604
|
||||||
msgid "Ukrainian"
|
msgid "Ukrainian"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:600
|
#: paperless/settings/__init__.py:605
|
||||||
msgid "Vietnamese"
|
msgid "Vietnamese"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:601
|
#: paperless/settings/__init__.py:606
|
||||||
msgid "Chinese Simplified"
|
msgid "Chinese Simplified"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: paperless/settings/__init__.py:602
|
#: paperless/settings/__init__.py:607
|
||||||
msgid "Chinese Traditional"
|
msgid "Chinese Traditional"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -461,9 +461,14 @@ def _parse_paperless_url():
|
|||||||
global CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS, ALLOWED_HOSTS
|
global CSRF_TRUSTED_ORIGINS, CORS_ALLOWED_ORIGINS, ALLOWED_HOSTS
|
||||||
url = os.getenv("PAPERLESS_URL")
|
url = os.getenv("PAPERLESS_URL")
|
||||||
if url:
|
if url:
|
||||||
|
hostname = urlparse(url).hostname
|
||||||
|
if not hostname:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
f"PAPERLESS_URL {url!r} has no hostname, expected a URL like https://example.com",
|
||||||
|
)
|
||||||
CSRF_TRUSTED_ORIGINS.append(url)
|
CSRF_TRUSTED_ORIGINS.append(url)
|
||||||
CORS_ALLOWED_ORIGINS.append(url)
|
CORS_ALLOWED_ORIGINS.append(url)
|
||||||
ALLOWED_HOSTS.append(urlparse(url).hostname)
|
ALLOWED_HOSTS.append(hostname)
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|||||||
@@ -166,3 +166,22 @@ class TestPaperlessURLSettings(TestCase):
|
|||||||
|
|
||||||
self.assertIn(url, settings.CSRF_TRUSTED_ORIGINS)
|
self.assertIn(url, settings.CSRF_TRUSTED_ORIGINS)
|
||||||
self.assertIn(url, settings.CORS_ALLOWED_ORIGINS)
|
self.assertIn(url, settings.CORS_ALLOWED_ORIGINS)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url", ["example.com", "https://", "/just/a/path"])
|
||||||
|
def test_paperless_url_without_hostname_is_rejected(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
url: str,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- PAPERLESS_URL is set to a value with no hostname
|
||||||
|
WHEN:
|
||||||
|
- The URL is parsed
|
||||||
|
THEN:
|
||||||
|
- A configuration error is raised instead of adding None to ALLOWED_HOSTS
|
||||||
|
"""
|
||||||
|
monkeypatch.setenv("PAPERLESS_URL", url)
|
||||||
|
|
||||||
|
with pytest.raises(ImproperlyConfigured, match="no hostname"):
|
||||||
|
_parse_paperless_url()
|
||||||
@@ -169,9 +169,9 @@ class TagMailAction(BaseMailAction):
|
|||||||
# The custom tag should look like "apple:<color>"
|
# The custom tag should look like "apple:<color>"
|
||||||
if "apple:" in parameter.lower():
|
if "apple:" in parameter.lower():
|
||||||
_, self.color = parameter.split(":")
|
_, self.color = parameter.split(":")
|
||||||
self.color = self.color.strip()
|
self.color = self.color.strip().lower()
|
||||||
|
|
||||||
if self.color.lower() not in APPLE_MAIL_TAG_COLORS:
|
if self.color not in APPLE_MAIL_TAG_COLORS:
|
||||||
raise MailError("Not a valid AppleMail tag color.")
|
raise MailError("Not a valid AppleMail tag color.")
|
||||||
|
|
||||||
self.keyword = None
|
self.keyword = None
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ class PaperlessMailOAuth2Manager:
|
|||||||
"""
|
"""
|
||||||
logger = logging.getLogger("paperless_mail")
|
logger = logging.getLogger("paperless_mail")
|
||||||
logger.debug(f"Attempting to refresh oauth token for account {account}")
|
logger.debug(f"Attempting to refresh oauth token for account {account}")
|
||||||
|
if account.refresh_token is None:
|
||||||
|
logger.error(f"Account {account} has no refresh token to use")
|
||||||
|
return False
|
||||||
try:
|
try:
|
||||||
result: OAuth2Token
|
result: OAuth2Token
|
||||||
if account.account_type == MailAccount.MailAccountType.GMAIL_OAUTH:
|
if account.account_type == MailAccount.MailAccountType.GMAIL_OAUTH:
|
||||||
@@ -103,6 +106,9 @@ class PaperlessMailOAuth2Manager:
|
|||||||
refresh_token=account.refresh_token,
|
refresh_token=account.refresh_token,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
logger.error(f"Account {account} does not use OAuth")
|
||||||
|
return False
|
||||||
if "refresh_token" in result:
|
if "refresh_token" in result:
|
||||||
# Outlook returns a new refresh token on refresh, Gmail does not
|
# Outlook returns a new refresh token on refresh, Gmail does not
|
||||||
account.refresh_token = result["refresh_token"]
|
account.refresh_token = result["refresh_token"]
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ from rest_framework.test import APITestCase
|
|||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import MatchingModel
|
from documents.models import MatchingModel
|
||||||
from paperless_mail import tasks
|
from paperless_mail import tasks
|
||||||
|
from paperless_mail.mail import APPLE_MAIL_TAG_COLORS
|
||||||
from paperless_mail.mail import MailAccountHandler
|
from paperless_mail.mail import MailAccountHandler
|
||||||
from paperless_mail.mail import MailError
|
from paperless_mail.mail import MailError
|
||||||
from paperless_mail.mail import TagMailAction
|
from paperless_mail.mail import TagMailAction
|
||||||
@@ -1873,6 +1874,32 @@ class TestErrorCallback:
|
|||||||
assert processed_mails.get().owner == owner
|
assert processed_mails.get().owner == owner
|
||||||
|
|
||||||
|
|
||||||
|
class TestTagMailActionColor:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"parameter",
|
||||||
|
["apple:green", "apple:Green", "APPLE: GREEN "],
|
||||||
|
)
|
||||||
|
def test_color_case_is_ignored(self, parameter: str) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- An Apple Mail tag parameter in any letter case
|
||||||
|
WHEN:
|
||||||
|
- The action tags a message
|
||||||
|
THEN:
|
||||||
|
- The lowercase color's flag bits are set on the message
|
||||||
|
"""
|
||||||
|
action = TagMailAction(parameter, supports_gmail_labels=False)
|
||||||
|
mailbox = mock.Mock()
|
||||||
|
|
||||||
|
action.post_consume(mailbox, "1", parameter)
|
||||||
|
|
||||||
|
assert mailbox.flag.call_args_list[1] == mock.call(
|
||||||
|
"1",
|
||||||
|
APPLE_MAIL_TAG_COLORS["green"],
|
||||||
|
value=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestManagementCommand(TestCase):
|
class TestManagementCommand(TestCase):
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
"paperless_mail.management.commands.mail_fetcher.tasks.process_mail_accounts",
|
"paperless_mail.management.commands.mail_fetcher.tasks.process_mail_accounts",
|
||||||
|
|||||||
@@ -264,6 +264,28 @@ class TestMailOAuth(
|
|||||||
MailAccount.objects.filter(imap_server="outlook.office365.com").exists(),
|
MailAccount.objects.filter(imap_server="outlook.office365.com").exists(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_oauth_callback_view_unrecognized_scope(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A valid OAuth state and a callback scope that is not Google's
|
||||||
|
WHEN:
|
||||||
|
- OAuth callback is called
|
||||||
|
THEN:
|
||||||
|
- 400 is returned instead of a server error and no account is created
|
||||||
|
"""
|
||||||
|
session = self.client.session
|
||||||
|
session["oauth_state"] = "test_state"
|
||||||
|
session.save()
|
||||||
|
|
||||||
|
for scope in ("", "openid", "https://example.com/other"):
|
||||||
|
with self.subTest(scope=scope):
|
||||||
|
response = self.client.get(
|
||||||
|
"/api/oauth/callback/",
|
||||||
|
{"code": "test_code", "scope": scope, "state": "test_state"},
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertFalse(MailAccount.objects.exists())
|
||||||
|
|
||||||
@mock.patch("paperless_mail.mail.get_mailbox")
|
@mock.patch("paperless_mail.mail.get_mailbox")
|
||||||
@mock.patch(
|
@mock.patch(
|
||||||
"httpx_oauth.oauth2.BaseOAuth2.refresh_token",
|
"httpx_oauth.oauth2.BaseOAuth2.refresh_token",
|
||||||
@@ -366,3 +388,39 @@ class TestMailOAuth(
|
|||||||
f"Failed to refresh oauth token for account {mail_account}: test_error",
|
f"Failed to refresh oauth token for account {mail_account}: test_error",
|
||||||
cm.output[0],
|
cm.output[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_refresh_account_oauth_token_non_oauth_account(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A mail account which does not use OAuth
|
||||||
|
WHEN:
|
||||||
|
- A token refresh is requested
|
||||||
|
THEN:
|
||||||
|
- False is returned rather than an unbound variable error
|
||||||
|
"""
|
||||||
|
account = MailAccountFactory(
|
||||||
|
account_type=MailAccount.MailAccountType.IMAP,
|
||||||
|
refresh_token="token",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
PaperlessMailOAuth2Manager().refresh_account_oauth_token(account),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_refresh_account_oauth_token_without_refresh_token(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- An OAuth mail account with no refresh token stored
|
||||||
|
WHEN:
|
||||||
|
- A token refresh is requested
|
||||||
|
THEN:
|
||||||
|
- False is returned
|
||||||
|
"""
|
||||||
|
account = MailAccountFactory(
|
||||||
|
account_type=MailAccount.MailAccountType.GMAIL_OAUTH,
|
||||||
|
refresh_token=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(
|
||||||
|
PaperlessMailOAuth2Manager().refresh_account_oauth_token(account),
|
||||||
|
)
|
||||||
@@ -319,6 +319,14 @@ class OauthCallbackView(GenericAPIView[Any]):
|
|||||||
|
|
||||||
result = oauth_manager.get_outlook_access_token(code)
|
result = oauth_manager.get_outlook_access_token(code)
|
||||||
|
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Invalid oauth callback request: unrecognized scope {scope!r}",
|
||||||
|
)
|
||||||
|
return HttpResponseBadRequest(
|
||||||
|
"Invalid request, see logs for more detail",
|
||||||
|
)
|
||||||
|
|
||||||
access_token = result["access_token"]
|
access_token = result["access_token"]
|
||||||
refresh_token = result["refresh_token"]
|
refresh_token = result["refresh_token"]
|
||||||
expires_in = result["expires_in"]
|
expires_in = result["expires_in"]
|
||||||
|
|||||||
Reference in new issue
Block a user