mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-03 08:27:15 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f440cade2 | ||
|
|
61397749b7 | ||
|
|
3666234f51 | ||
|
|
ec7745e71f | ||
|
|
98e87d91ad |
+65
-61
@@ -5,14 +5,14 @@
|
|||||||
<trans-unit id="ngb.alert.close" datatype="html">
|
<trans-unit id="ngb.alert.close" datatype="html">
|
||||||
<source>Close</source>
|
<source>Close</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/alert/alert.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/alert/alert.ts</context>
|
||||||
<context context-type="linenumber">39</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.carousel.slide-number" datatype="html">
|
<trans-unit id="ngb.carousel.slide-number" datatype="html">
|
||||||
<source> Slide <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> </source>
|
<source> Slide <x id="INTERPOLATION"/> of <x id="INTERPOLATION_1"/> </source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">129</context>
|
<context context-type="linenumber">129</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<note priority="1" from="description">Currently selected slide number read by screen reader</note>
|
<note priority="1" from="description">Currently selected slide number read by screen reader</note>
|
||||||
@@ -20,219 +20,219 @@
|
|||||||
<trans-unit id="ngb.carousel.previous" datatype="html">
|
<trans-unit id="ngb.carousel.previous" datatype="html">
|
||||||
<source>Previous</source>
|
<source>Previous</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">147,148</context>
|
<context context-type="linenumber">147,148</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.carousel.next" datatype="html">
|
<trans-unit id="ngb.carousel.next" datatype="html">
|
||||||
<source>Next</source>
|
<source>Next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">187</context>
|
<context context-type="linenumber">187</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.datepicker.select-month" datatype="html">
|
<trans-unit id="ngb.datepicker.select-month" datatype="html">
|
||||||
<source>Select month</source>
|
<source>Select month</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
||||||
<context context-type="linenumber">89</context>
|
<context context-type="linenumber">89</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
||||||
<context context-type="linenumber">89</context>
|
<context context-type="linenumber">89</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.datepicker.select-year" datatype="html">
|
<trans-unit id="ngb.datepicker.select-year" datatype="html">
|
||||||
<source>Select year</source>
|
<source>Select year</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
||||||
<context context-type="linenumber">89</context>
|
<context context-type="linenumber">89</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation-select.ts</context>
|
||||||
<context context-type="linenumber">89</context>
|
<context context-type="linenumber">89</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.datepicker.previous-month" datatype="html">
|
<trans-unit id="ngb.datepicker.previous-month" datatype="html">
|
||||||
<source>Previous month</source>
|
<source>Previous month</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">74,76</context>
|
<context context-type="linenumber">74,76</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">103</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.datepicker.next-month" datatype="html">
|
<trans-unit id="ngb.datepicker.next-month" datatype="html">
|
||||||
<source>Next month</source>
|
<source>Next month</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">103</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">103</context>
|
<context context-type="linenumber">103</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.first" datatype="html">
|
<trans-unit id="ngb.pagination.first" datatype="html">
|
||||||
<source>««</source>
|
<source>««</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
|
||||||
<context context-type="linenumber">297</context>
|
<context context-type="linenumber">297</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.previous" datatype="html">
|
<trans-unit id="ngb.pagination.previous" datatype="html">
|
||||||
<source>«</source>
|
<source>«</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
|
||||||
<context context-type="linenumber">322</context>
|
<context context-type="linenumber">322</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.next" datatype="html">
|
<trans-unit id="ngb.pagination.next" datatype="html">
|
||||||
<source>»</source>
|
<source>»</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
|
||||||
<context context-type="linenumber">358</context>
|
<context context-type="linenumber">358</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.last" datatype="html">
|
<trans-unit id="ngb.pagination.last" datatype="html">
|
||||||
<source>»»</source>
|
<source>»»</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
|
||||||
<context context-type="linenumber">396</context>
|
<context context-type="linenumber">396</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.first-aria" datatype="html">
|
<trans-unit id="ngb.pagination.first-aria" datatype="html">
|
||||||
<source>First</source>
|
<source>First</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
|
||||||
<context context-type="linenumber">414,415</context>
|
<context context-type="linenumber">414,415</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.previous-aria" datatype="html">
|
<trans-unit id="ngb.pagination.previous-aria" datatype="html">
|
||||||
<source>Previous</source>
|
<source>Previous</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
|
||||||
<context context-type="linenumber">444,445</context>
|
<context context-type="linenumber">444,445</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.next-aria" datatype="html">
|
<trans-unit id="ngb.pagination.next-aria" datatype="html">
|
||||||
<source>Next</source>
|
<source>Next</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
|
||||||
<context context-type="linenumber">479,480</context>
|
<context context-type="linenumber">479,480</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.pagination.last-aria" datatype="html">
|
<trans-unit id="ngb.pagination.last-aria" datatype="html">
|
||||||
<source>Last</source>
|
<source>Last</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/pagination/pagination.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/pagination/pagination.ts</context>
|
||||||
<context context-type="linenumber">506</context>
|
<context context-type="linenumber">506</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.progressbar.value" datatype="html">
|
<trans-unit id="ngb.progressbar.value" datatype="html">
|
||||||
<source><x id="INTERPOLATION"/></source>
|
<source><x id="INTERPOLATION"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/progressbar/progressbar.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/progressbar/progressbar.ts</context>
|
||||||
<context context-type="linenumber">40</context>
|
<context context-type="linenumber">40</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.HH" datatype="html">
|
<trans-unit id="ngb.timepicker.HH" datatype="html">
|
||||||
<source>HH</source>
|
<source>HH</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">256,259</context>
|
<context context-type="linenumber">256,259</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.hours" datatype="html">
|
<trans-unit id="ngb.timepicker.hours" datatype="html">
|
||||||
<source>Hours</source>
|
<source>Hours</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">293,295</context>
|
<context context-type="linenumber">293,295</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.MM" datatype="html">
|
<trans-unit id="ngb.timepicker.MM" datatype="html">
|
||||||
<source>MM</source>
|
<source>MM</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">322</context>
|
<context context-type="linenumber">322</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.minutes" datatype="html">
|
<trans-unit id="ngb.timepicker.minutes" datatype="html">
|
||||||
<source>Minutes</source>
|
<source>Minutes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">360</context>
|
<context context-type="linenumber">360</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.increment-hours" datatype="html">
|
<trans-unit id="ngb.timepicker.increment-hours" datatype="html">
|
||||||
<source>Increment hours</source>
|
<source>Increment hours</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">397,399</context>
|
<context context-type="linenumber">397,399</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
|
<trans-unit id="ngb.timepicker.decrement-hours" datatype="html">
|
||||||
<source>Decrement hours</source>
|
<source>Decrement hours</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">435,438</context>
|
<context context-type="linenumber">435,438</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
|
<trans-unit id="ngb.timepicker.increment-minutes" datatype="html">
|
||||||
<source>Increment minutes</source>
|
<source>Increment minutes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">442</context>
|
<context context-type="linenumber">442</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
|
<trans-unit id="ngb.timepicker.decrement-minutes" datatype="html">
|
||||||
<source>Decrement minutes</source>
|
<source>Decrement minutes</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">442</context>
|
<context context-type="linenumber">442</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.SS" datatype="html">
|
<trans-unit id="ngb.timepicker.SS" datatype="html">
|
||||||
<source>SS</source>
|
<source>SS</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">442</context>
|
<context context-type="linenumber">442</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.seconds" datatype="html">
|
<trans-unit id="ngb.timepicker.seconds" datatype="html">
|
||||||
<source>Seconds</source>
|
<source>Seconds</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">442</context>
|
<context context-type="linenumber">442</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
|
<trans-unit id="ngb.timepicker.increment-seconds" datatype="html">
|
||||||
<source>Increment seconds</source>
|
<source>Increment seconds</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">442</context>
|
<context context-type="linenumber">442</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
|
<trans-unit id="ngb.timepicker.decrement-seconds" datatype="html">
|
||||||
<source>Decrement seconds</source>
|
<source>Decrement seconds</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">442</context>
|
<context context-type="linenumber">442</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.timepicker.PM" datatype="html">
|
<trans-unit id="ngb.timepicker.PM" datatype="html">
|
||||||
<source><x id="INTERPOLATION"/></source>
|
<source><x id="INTERPOLATION"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/timepicker/timepicker.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/timepicker/timepicker.ts</context>
|
||||||
<context context-type="linenumber">442</context>
|
<context context-type="linenumber">442</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="ngb.toast.close-aria" datatype="html">
|
<trans-unit id="ngb.toast.close-aria" datatype="html">
|
||||||
<source>Close</source>
|
<source>Close</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.3_@angular+core@22.1.3_@angular+_6e980a1a1b6a042b5112673ade17a76b/node_modules/src/toast/toast.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@21.0.0_@angular+common@22.1.1_@angular+core@22.1.1_@angular+_69897756b3f091add524dceba470da9d/node_modules/src/toast/toast.ts</context>
|
||||||
<context context-type="linenumber">110,112</context>
|
<context context-type="linenumber">110,112</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@@ -775,7 +775,7 @@
|
|||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">20,21</context>
|
<context context-type="linenumber">19,20</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3894950702316166331" datatype="html">
|
<trans-unit id="3894950702316166331" datatype="html">
|
||||||
@@ -788,6 +788,10 @@
|
|||||||
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/logs/logs.component.html</context>
|
||||||
<context context-type="linenumber">49</context>
|
<context context-type="linenumber">49</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
|
<context-group purpose="location">
|
||||||
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
|
<context context-type="linenumber">26,29</context>
|
||||||
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
|
||||||
<context context-type="linenumber">47</context>
|
<context context-type="linenumber">47</context>
|
||||||
@@ -1850,7 +1854,7 @@
|
|||||||
<source>Clear selection</source>
|
<source>Clear selection</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">10</context>
|
<context context-type="linenumber">9</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
|
||||||
@@ -1865,14 +1869,14 @@
|
|||||||
<source>Dismiss all</source>
|
<source>Dismiss all</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">16</context>
|
<context context-type="linenumber">15</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1616102757855967475" datatype="html">
|
<trans-unit id="1616102757855967475" datatype="html">
|
||||||
<source>All</source>
|
<source>All</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">35</context>
|
<context context-type="linenumber">39</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
@@ -1911,14 +1915,14 @@
|
|||||||
<source>Filter by</source>
|
<source>Filter by</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">55</context>
|
<context context-type="linenumber">59</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="424356320420294719" datatype="html">
|
<trans-unit id="424356320420294719" datatype="html">
|
||||||
<source>All types</source>
|
<source>All types</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">60,61</context>
|
<context context-type="linenumber">64,65</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
@@ -1929,7 +1933,7 @@
|
|||||||
<source>All sources</source>
|
<source>All sources</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">69,70</context>
|
<context context-type="linenumber">73,74</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
@@ -1940,7 +1944,7 @@
|
|||||||
<source>Clear search</source>
|
<source>Clear search</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">88</context>
|
<context context-type="linenumber">92</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>
|
||||||
@@ -1951,14 +1955,14 @@
|
|||||||
<source>Search tasks</source>
|
<source>Search tasks</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">92,93</context>
|
<context context-type="linenumber">96,97</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6849725902312323996" datatype="html">
|
<trans-unit id="6849725902312323996" datatype="html">
|
||||||
<source>Reset filters</source>
|
<source>Reset filters</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">101,102</context>
|
<context context-type="linenumber">105,106</context>
|
||||||
</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>
|
||||||
@@ -1973,14 +1977,14 @@
|
|||||||
<source>{VAR_PLURAL, plural, =1 {1 task} other {<x id="INTERPOLATION"/> tasks}}</source>
|
<source>{VAR_PLURAL, plural, =1 {1 task} other {<x id="INTERPOLATION"/> tasks}}</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">122</context>
|
<context context-type="linenumber">126</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8953033926734869941" datatype="html">
|
<trans-unit id="8953033926734869941" datatype="html">
|
||||||
<source>Name</source>
|
<source>Name</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">144,145</context>
|
<context context-type="linenumber">148,149</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
@@ -2091,7 +2095,7 @@
|
|||||||
<source>Created</source>
|
<source>Created</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">145,146</context>
|
<context context-type="linenumber">149,150</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context>
|
<context context-type="sourcefile">src/app/components/common/dates-dropdown/dates-dropdown.component.html</context>
|
||||||
@@ -2122,21 +2126,21 @@
|
|||||||
<source>Results</source>
|
<source>Results</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">147,148</context>
|
<context context-type="linenumber">151,152</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="314315645942131479" datatype="html">
|
<trans-unit id="314315645942131479" datatype="html">
|
||||||
<source>Info</source>
|
<source>Info</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">149,150</context>
|
<context context-type="linenumber">153,154</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="3193976279273491157" datatype="html">
|
<trans-unit id="3193976279273491157" datatype="html">
|
||||||
<source>Actions</source>
|
<source>Actions</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">150,151</context>
|
<context context-type="linenumber">154,155</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/trash/trash.component.html</context>
|
||||||
@@ -2251,21 +2255,21 @@
|
|||||||
<source>click for full output</source>
|
<source>click for full output</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">201</context>
|
<context context-type="linenumber">205</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="8946500279334661567" datatype="html">
|
<trans-unit id="8946500279334661567" datatype="html">
|
||||||
<source>View task details</source>
|
<source>View task details</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">207</context>
|
<context context-type="linenumber">211</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1536087519743707362" datatype="html">
|
<trans-unit id="1536087519743707362" datatype="html">
|
||||||
<source>Dismiss</source>
|
<source>Dismiss</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">214</context>
|
<context context-type="linenumber">218</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.ts</context>
|
||||||
@@ -2288,28 +2292,28 @@
|
|||||||
<source>Open Document</source>
|
<source>Open Document</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">219</context>
|
<context context-type="linenumber">223</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="5404759957685833020" datatype="html">
|
<trans-unit id="5404759957685833020" datatype="html">
|
||||||
<source>Result message</source>
|
<source>Result message</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">232,233</context>
|
<context context-type="linenumber">236,237</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="6621329748219109148" datatype="html">
|
<trans-unit id="6621329748219109148" datatype="html">
|
||||||
<source>Duplicate</source>
|
<source>Duplicate</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">239,240</context>
|
<context context-type="linenumber">243,244</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7593555694782789615" datatype="html">
|
<trans-unit id="7593555694782789615" datatype="html">
|
||||||
<source>Open</source>
|
<source>Open</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">247</context>
|
<context context-type="linenumber">251</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
|
<context context-type="sourcefile">src/app/components/app-frame/global-search/global-search.component.html</context>
|
||||||
@@ -2340,21 +2344,21 @@
|
|||||||
<source>Input data</source>
|
<source>Input data</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">257,258</context>
|
<context context-type="linenumber">261,262</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="1585185618099050920" datatype="html">
|
<trans-unit id="1585185618099050920" datatype="html">
|
||||||
<source>Result data</source>
|
<source>Result data</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">263,264</context>
|
<context context-type="linenumber">267,268</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="7976920528153858271" datatype="html">
|
<trans-unit id="7976920528153858271" datatype="html">
|
||||||
<source>No tasks match the current filters.</source>
|
<source>No tasks match the current filters.</source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
<context context-type="sourcefile">src/app/components/admin/tasks/tasks.component.html</context>
|
||||||
<context context-type="linenumber">285,287</context>
|
<context context-type="linenumber">289,291</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="2525230676386818985" datatype="html">
|
<trans-unit id="2525230676386818985" datatype="html">
|
||||||
|
|||||||
+13
-13
@@ -15,14 +15,14 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^22.1.4",
|
"@angular/cdk": "^22.1.1",
|
||||||
"@angular/common": "~22.1.3",
|
"@angular/common": "~22.1.1",
|
||||||
"@angular/compiler": "~22.1.3",
|
"@angular/compiler": "~22.1.1",
|
||||||
"@angular/core": "~22.1.3",
|
"@angular/core": "~22.1.1",
|
||||||
"@angular/forms": "~22.1.3",
|
"@angular/forms": "~22.1.1",
|
||||||
"@angular/localize": "~22.1.3",
|
"@angular/localize": "~22.1.1",
|
||||||
"@angular/platform-browser": "~22.1.3",
|
"@angular/platform-browser": "~22.1.1",
|
||||||
"@angular/router": "~22.1.3",
|
"@angular/router": "~22.1.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^21.0.0",
|
||||||
"@ng-select/ng-select": "~23.6.0",
|
"@ng-select/ng-select": "~23.6.0",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
@@ -45,16 +45,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/jest": "^22.0.1",
|
"@angular-builders/jest": "^22.0.1",
|
||||||
"@angular-devkit/core": "^22.1.6",
|
"@angular-devkit/core": "^22.1.3",
|
||||||
"@angular-devkit/schematics": "^22.1.6",
|
"@angular-devkit/schematics": "^22.1.3",
|
||||||
"@angular-eslint/builder": "22.1.0",
|
"@angular-eslint/builder": "22.1.0",
|
||||||
"@angular-eslint/eslint-plugin": "22.1.0",
|
"@angular-eslint/eslint-plugin": "22.1.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "22.1.0",
|
"@angular-eslint/eslint-plugin-template": "22.1.0",
|
||||||
"@angular-eslint/schematics": "22.1.0",
|
"@angular-eslint/schematics": "22.1.0",
|
||||||
"@angular-eslint/template-parser": "22.1.0",
|
"@angular-eslint/template-parser": "22.1.0",
|
||||||
"@angular/build": "22.1.6",
|
"@angular/build": "22.1.3",
|
||||||
"@angular/cli": "22.1.6",
|
"@angular/cli": "22.1.3",
|
||||||
"@angular/compiler-cli": "~22.1.3",
|
"@angular/compiler-cli": "~22.1.1",
|
||||||
"@playwright/test": "^1.62.1",
|
"@playwright/test": "^1.62.1",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^26.2.0",
|
"@types/node": "^26.2.0",
|
||||||
|
|||||||
Generated
+629
-437
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,6 @@
|
|||||||
i18n-title
|
i18n-title
|
||||||
info="Tasks shows detailed information about document consumption and system tasks."
|
info="Tasks shows detailed information about document consumption and system tasks."
|
||||||
i18n-info
|
i18n-info
|
||||||
[loading]="loading() && pagedTasks().length === 0"
|
|
||||||
>
|
>
|
||||||
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
|
<div class="btn-toolbar col col-md-auto align-items-center gap-2">
|
||||||
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
<button class="btn btn-sm btn-outline-secondary me-2" (click)="clearSelection()" [hidden]="selectedTasks.size === 0">
|
||||||
@@ -22,6 +21,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</pngx-page-header>
|
</pngx-page-header>
|
||||||
|
|
||||||
|
@if (loading() && pagedTasks().length === 0) {
|
||||||
|
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||||
|
<div class="visually-hidden" i18n>Loading...</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="task-controls mb-3 gap-3 btn-toolbar align-items-center" role="toolbar">
|
<div class="task-controls mb-3 gap-3 btn-toolbar align-items-center" role="toolbar">
|
||||||
<div class="task-view-scope btn-group btn-group-sm" role="group">
|
<div class="task-view-scope btn-group btn-group-sm" role="group">
|
||||||
<input
|
<input
|
||||||
@@ -281,6 +285,6 @@
|
|||||||
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks: tasksForSection(section), section: section}"></ng-container>
|
<ng-container [ngTemplateOutlet]="tasksTemplate" [ngTemplateOutletContext]="{tasks: tasksForSection(section), section: section}"></ng-container>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
} @else if (!loading() || pagedTasks().length > 0) {
|
} @else {
|
||||||
<div class="alert alert-secondary fst-italic" i18n>No tasks match the current filters.</div>
|
<div class="alert alert-secondary fst-italic" i18n>No tasks match the current filters.</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
--pngx-sidebar-width: var(--pngx-sidebar-expanded-width);
|
&.expanded {
|
||||||
|
--pngx-sidebar-width: var(--pngx-sidebar-expanded-width);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
@@ -111,13 +113,6 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only animate when the user toggles slim mode
|
|
||||||
.sidebar:not(.animating),
|
|
||||||
.sidebar:not(.animating) ~ main,
|
|
||||||
.sidebar:not(.animating) .sidebar-slim-toggler {
|
|
||||||
transition: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar.slim {
|
.sidebar.slim {
|
||||||
max-width: 55px;
|
max-width: 55px;
|
||||||
|
|
||||||
@@ -128,6 +123,8 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.slim:not(.animating) {
|
.sidebar.slim:not(.animating) {
|
||||||
|
transition: none;
|
||||||
|
|
||||||
li.nav-item span,
|
li.nav-item span,
|
||||||
.sidebar-heading span {
|
.sidebar-heading span {
|
||||||
display: none;
|
display: none;
|
||||||
@@ -147,6 +144,10 @@ main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar.slim:not(.animating) ~ main.col-slim {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar.animating {
|
.sidebar.animating {
|
||||||
li.nav-item span,
|
li.nav-item span,
|
||||||
.sidebar-heading span {
|
.sidebar-heading span {
|
||||||
@@ -195,26 +196,6 @@ main {
|
|||||||
--bs-popover-body-padding-y: .5rem;
|
--bs-popover-body-padding-y: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
.sidebar-sticky > ul,
|
|
||||||
.sidebar-sticky > .nav-group {
|
|
||||||
animation: sidebar-nav-in .3s cubic-bezier(.22, .61, .36, 1) backwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@for $i from 2 through 5 {
|
|
||||||
.sidebar-sticky > :nth-child(#{$i}) {
|
|
||||||
animation-delay: #{($i - 1) * 0.04}s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes sidebar-nav-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(6px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-sticky {
|
.sidebar-sticky {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
-6
@@ -7,8 +7,6 @@
|
|||||||
padding-left: calc(calc(var(--depth) - 2) * 1rem);
|
padding-left: calc(calc(var(--depth) - 2) * 1rem);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -20,7 +18,3 @@
|
|||||||
margin-left: .5rem;
|
margin-left: .5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ def _consume_file(
|
|||||||
consumption_dir: Path,
|
consumption_dir: Path,
|
||||||
*,
|
*,
|
||||||
subdirs_as_tags: bool,
|
subdirs_as_tags: bool,
|
||||||
) -> bool:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Queue a file for consumption.
|
Queue a file for consumption.
|
||||||
|
|
||||||
@@ -322,20 +322,15 @@ def _consume_file(
|
|||||||
filepath: Path to the file to consume.
|
filepath: Path to the file to consume.
|
||||||
consumption_dir: Base consumption directory.
|
consumption_dir: Base consumption directory.
|
||||||
subdirs_as_tags: Whether to create tags from subdirectory names.
|
subdirs_as_tags: Whether to create tags from subdirectory names.
|
||||||
|
|
||||||
Returns:
|
|
||||||
True if the file was successfully handed to Celery, False otherwise.
|
|
||||||
Callers must not record the file as queued on failure, or the rescan
|
|
||||||
will never retry it.
|
|
||||||
"""
|
"""
|
||||||
# Verify file still exists and is accessible
|
# Verify file still exists and is accessible
|
||||||
try:
|
try:
|
||||||
if not filepath.is_file():
|
if not filepath.is_file():
|
||||||
logger.debug(f"Not consuming {filepath}: not a file or doesn't exist")
|
logger.debug(f"Not consuming {filepath}: not a file or doesn't exist")
|
||||||
return False
|
return
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.warning(f"Not consuming {filepath}: {e}")
|
logger.warning(f"Not consuming {filepath}: {e}")
|
||||||
return False
|
return
|
||||||
|
|
||||||
# Get tags from path if configured
|
# Get tags from path if configured
|
||||||
tag_ids: list[int] | None = None
|
tag_ids: list[int] | None = None
|
||||||
@@ -360,9 +355,6 @@ def _consume_file(
|
|||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception(f"Error while queuing document {filepath}")
|
logger.exception(f"Error while queuing document {filepath}")
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@@ -500,12 +492,12 @@ class Command(BaseCommand):
|
|||||||
if not consumer_filter(Change.added, str(filepath)):
|
if not consumer_filter(Change.added, str(filepath)):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if _consume_file(
|
_consume_file(
|
||||||
filepath=filepath,
|
filepath=filepath,
|
||||||
consumption_dir=directory,
|
consumption_dir=directory,
|
||||||
subdirs_as_tags=subdirs_as_tags,
|
subdirs_as_tags=subdirs_as_tags,
|
||||||
):
|
)
|
||||||
queued.add(filepath.resolve())
|
queued.add(filepath.resolve())
|
||||||
|
|
||||||
return queued
|
return queued
|
||||||
|
|
||||||
@@ -659,16 +651,14 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
# Check for stable files
|
# Check for stable files
|
||||||
for stable_path in tracker.get_stable_files():
|
for stable_path in tracker.get_stable_files():
|
||||||
# Only remember files that were actually queued, so the
|
_consume_file(
|
||||||
# rescan does not re-queue them while the consume task
|
|
||||||
# has yet to remove them from disk, but does retry a
|
|
||||||
# failed publish instead of stranding it
|
|
||||||
if _consume_file(
|
|
||||||
filepath=stable_path,
|
filepath=stable_path,
|
||||||
consumption_dir=directory,
|
consumption_dir=directory,
|
||||||
subdirs_as_tags=subdirs_as_tags,
|
subdirs_as_tags=subdirs_as_tags,
|
||||||
):
|
)
|
||||||
queued.add(stable_path)
|
# Remember it so the rescan does not re-queue it while
|
||||||
|
# the consume task has yet to remove it from disk
|
||||||
|
queued.add(stable_path)
|
||||||
|
|
||||||
# Exit watch loop to reconfigure timeout
|
# Exit watch loop to reconfigure timeout
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1003,7 +1003,7 @@ def run_workflows(
|
|||||||
|
|
||||||
# kwargs so the PaperlessTask record can note the
|
# kwargs so the PaperlessTask record can note the
|
||||||
# document, see _extract_input_data
|
# document, see _extract_input_data
|
||||||
apply_ai_suggestions.delay_on_commit(
|
apply_ai_suggestions.delay(
|
||||||
action_id=action.pk,
|
action_id=action.pk,
|
||||||
document_id=document.pk,
|
document_id=document.pk,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -445,13 +445,12 @@ class TestConsumeFile:
|
|||||||
target = consumption_dir / "document.pdf"
|
target = consumption_dir / "document.pdf"
|
||||||
shutil.copy(sample_pdf, target)
|
shutil.copy(sample_pdf, target)
|
||||||
|
|
||||||
result = _consume_file(
|
_consume_file(
|
||||||
filepath=target,
|
filepath=target,
|
||||||
consumption_dir=consumption_dir,
|
consumption_dir=consumption_dir,
|
||||||
subdirs_as_tags=False,
|
subdirs_as_tags=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result is True
|
|
||||||
mock_consume_file_delay.apply_async.assert_called_once()
|
mock_consume_file_delay.apply_async.assert_called_once()
|
||||||
call_args = mock_consume_file_delay.apply_async.call_args
|
call_args = mock_consume_file_delay.apply_async.call_args
|
||||||
consumable_doc = call_args.kwargs["kwargs"]["input_doc"]
|
consumable_doc = call_args.kwargs["kwargs"]["input_doc"]
|
||||||
@@ -465,12 +464,11 @@ class TestConsumeFile:
|
|||||||
mock_consume_file_delay: MagicMock,
|
mock_consume_file_delay: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test _consume_file handles nonexistent files gracefully."""
|
"""Test _consume_file handles nonexistent files gracefully."""
|
||||||
result = _consume_file(
|
_consume_file(
|
||||||
filepath=consumption_dir / "nonexistent.pdf",
|
filepath=consumption_dir / "nonexistent.pdf",
|
||||||
consumption_dir=consumption_dir,
|
consumption_dir=consumption_dir,
|
||||||
subdirs_as_tags=False,
|
subdirs_as_tags=False,
|
||||||
)
|
)
|
||||||
assert result is False
|
|
||||||
mock_consume_file_delay.apply_async.assert_not_called()
|
mock_consume_file_delay.apply_async.assert_not_called()
|
||||||
|
|
||||||
def test_consume_directory(
|
def test_consume_directory(
|
||||||
@@ -482,12 +480,11 @@ class TestConsumeFile:
|
|||||||
subdir = consumption_dir / "subdir"
|
subdir = consumption_dir / "subdir"
|
||||||
subdir.mkdir()
|
subdir.mkdir()
|
||||||
|
|
||||||
result = _consume_file(
|
_consume_file(
|
||||||
filepath=subdir,
|
filepath=subdir,
|
||||||
consumption_dir=consumption_dir,
|
consumption_dir=consumption_dir,
|
||||||
subdirs_as_tags=False,
|
subdirs_as_tags=False,
|
||||||
)
|
)
|
||||||
assert result is False
|
|
||||||
mock_consume_file_delay.apply_async.assert_not_called()
|
mock_consume_file_delay.apply_async.assert_not_called()
|
||||||
|
|
||||||
def test_consume_with_permission_error(
|
def test_consume_with_permission_error(
|
||||||
@@ -502,33 +499,13 @@ class TestConsumeFile:
|
|||||||
shutil.copy(sample_pdf, target)
|
shutil.copy(sample_pdf, target)
|
||||||
|
|
||||||
mocker.patch.object(Path, "is_file", side_effect=PermissionError("denied"))
|
mocker.patch.object(Path, "is_file", side_effect=PermissionError("denied"))
|
||||||
result = _consume_file(
|
_consume_file(
|
||||||
filepath=target,
|
filepath=target,
|
||||||
consumption_dir=consumption_dir,
|
consumption_dir=consumption_dir,
|
||||||
subdirs_as_tags=False,
|
subdirs_as_tags=False,
|
||||||
)
|
)
|
||||||
assert result is False
|
|
||||||
mock_consume_file_delay.apply_async.assert_not_called()
|
mock_consume_file_delay.apply_async.assert_not_called()
|
||||||
|
|
||||||
def test_consume_with_apply_async_failure(
|
|
||||||
self,
|
|
||||||
consumption_dir: Path,
|
|
||||||
sample_pdf: Path,
|
|
||||||
mock_consume_file_delay: MagicMock,
|
|
||||||
) -> None:
|
|
||||||
"""Test _consume_file reports failure when apply_async raises."""
|
|
||||||
target = consumption_dir / "document.pdf"
|
|
||||||
shutil.copy(sample_pdf, target)
|
|
||||||
|
|
||||||
mock_consume_file_delay.apply_async.side_effect = Exception("broker down")
|
|
||||||
|
|
||||||
result = _consume_file(
|
|
||||||
filepath=target,
|
|
||||||
consumption_dir=consumption_dir,
|
|
||||||
subdirs_as_tags=False,
|
|
||||||
)
|
|
||||||
assert result is False
|
|
||||||
|
|
||||||
def test_consume_with_tags_error(
|
def test_consume_with_tags_error(
|
||||||
self,
|
self,
|
||||||
consumption_dir: Path,
|
consumption_dir: Path,
|
||||||
@@ -545,12 +522,11 @@ class TestConsumeFile:
|
|||||||
side_effect=DatabaseError("Something happened"),
|
side_effect=DatabaseError("Something happened"),
|
||||||
)
|
)
|
||||||
|
|
||||||
result = _consume_file(
|
_consume_file(
|
||||||
filepath=target,
|
filepath=target,
|
||||||
consumption_dir=consumption_dir,
|
consumption_dir=consumption_dir,
|
||||||
subdirs_as_tags=True,
|
subdirs_as_tags=True,
|
||||||
)
|
)
|
||||||
assert result is True
|
|
||||||
mock_consume_file_delay.apply_async.assert_called_once()
|
mock_consume_file_delay.apply_async.assert_called_once()
|
||||||
call_args = mock_consume_file_delay.apply_async.call_args
|
call_args = mock_consume_file_delay.apply_async.call_args
|
||||||
overrides = call_args.kwargs["kwargs"]["overrides"]
|
overrides = call_args.kwargs["kwargs"]["overrides"]
|
||||||
@@ -1273,52 +1249,6 @@ class TestProcessExistingFilesQueued:
|
|||||||
assert target.resolve() in queued
|
assert target.resolve() in queued
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
|
||||||
@pytest.mark.django_db
|
|
||||||
class TestCommandRetryAfterQueueFailure:
|
|
||||||
"""
|
|
||||||
Regression test for GH #13923.
|
|
||||||
|
|
||||||
A file whose ``apply_async`` publish fails (e.g. broker briefly down)
|
|
||||||
must not be marked as queued, so the periodic rescan retries it once
|
|
||||||
the broker recovers, instead of stranding it until the consumer
|
|
||||||
process is restarted.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_watch_loop_retries_failed_publish_on_rescan(
|
|
||||||
self,
|
|
||||||
consumption_dir: Path,
|
|
||||||
sample_pdf: Path,
|
|
||||||
mock_consume_file_delay: MagicMock,
|
|
||||||
start_consumer: Callable[..., ConsumerThread],
|
|
||||||
) -> None:
|
|
||||||
"""A publish failure from the watch loop is retried by the rescan."""
|
|
||||||
apply_async = mock_consume_file_delay.apply_async
|
|
||||||
|
|
||||||
def fail_first_call(*args: object, **kwargs: object) -> None:
|
|
||||||
if apply_async.call_count == 1:
|
|
||||||
raise Exception("broker down")
|
|
||||||
|
|
||||||
apply_async.side_effect = fail_first_call
|
|
||||||
|
|
||||||
thread = start_consumer(stability_delay=0.1, rescan_interval=0.3)
|
|
||||||
|
|
||||||
target = consumption_dir / "document.pdf"
|
|
||||||
shutil.copy(sample_pdf, target)
|
|
||||||
|
|
||||||
deadline = monotonic() + 5.0
|
|
||||||
while apply_async.call_count < 2 and monotonic() < deadline:
|
|
||||||
sleep(0.1)
|
|
||||||
|
|
||||||
if thread.exception:
|
|
||||||
raise thread.exception
|
|
||||||
|
|
||||||
assert apply_async.call_count >= 2, (
|
|
||||||
"Expected the failed publish to be retried by the rescan, "
|
|
||||||
f"but apply_async was only called {apply_async.call_count} time(s)"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.management
|
@pytest.mark.management
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
class TestCommandRescanRecovery:
|
class TestCommandRescanRecovery:
|
||||||
|
|||||||
@@ -5621,15 +5621,11 @@ class TestApplyAISuggestionsWorkflowAction(
|
|||||||
action = self.make_action()
|
action = self.make_action()
|
||||||
self.make_workflow(action, WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED)
|
self.make_workflow(action, WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED)
|
||||||
|
|
||||||
with (
|
with mock.patch("documents.tasks.apply_ai_suggestions.delay") as delay:
|
||||||
mock.patch("documents.tasks.apply_ai_suggestions.delay") as delay,
|
|
||||||
self.captureOnCommitCallbacks(execute=True),
|
|
||||||
):
|
|
||||||
run_workflows(
|
run_workflows(
|
||||||
WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||||
self.doc,
|
self.doc,
|
||||||
)
|
)
|
||||||
delay.assert_not_called()
|
|
||||||
|
|
||||||
delay.assert_called_once_with(action_id=action.pk, document_id=self.doc.pk)
|
delay.assert_called_once_with(action_id=action.pk, document_id=self.doc.pk)
|
||||||
|
|
||||||
|
|||||||
@@ -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-09-02 18:09+0000\n"
|
"POT-Creation-Date: 2026-09-01 19:54+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"
|
||||||
@@ -1628,7 +1628,7 @@ msgid "workflow runs"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:524 documents/serialisers.py:878
|
#: documents/serialisers.py:524 documents/serialisers.py:878
|
||||||
#: documents/serialisers.py:2830 documents/views.py:314 documents/views.py:2623
|
#: documents/serialisers.py:2830 documents/views.py:313 documents/views.py:2619
|
||||||
#: paperless_mail/serialisers.py:156
|
#: paperless_mail/serialisers.py:156
|
||||||
msgid "Insufficient permissions."
|
msgid "Insufficient permissions."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1669,7 +1669,7 @@ msgstr ""
|
|||||||
msgid "Duplicate document identifiers are not allowed."
|
msgid "Duplicate document identifiers are not allowed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/serialisers.py:2916 documents/views.py:4624
|
#: documents/serialisers.py:2916 documents/views.py:4620
|
||||||
#, python-format
|
#, python-format
|
||||||
msgid "Documents not found: %(ids)s"
|
msgid "Documents not found: %(ids)s"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -1937,36 +1937,36 @@ msgstr ""
|
|||||||
msgid "Unable to parse URI {value}"
|
msgid "Unable to parse URI {value}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:307 documents/views.py:2620
|
#: documents/views.py:306 documents/views.py:2616
|
||||||
msgid "Invalid more_like_id"
|
msgid "Invalid more_like_id"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:1591
|
#: documents/views.py:1590
|
||||||
msgid "Invalid AI configuration."
|
msgid "Invalid AI configuration."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:1602
|
#: documents/views.py:1601
|
||||||
msgid "AI backend request timed out."
|
msgid "AI backend request timed out."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:2445 documents/views.py:2766
|
#: documents/views.py:2441 documents/views.py:2762
|
||||||
msgid "Specify only one of text, title_search, query, or more_like_id."
|
msgid "Specify only one of text, title_search, query, or more_like_id."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:4637
|
#: documents/views.py:4633
|
||||||
#, 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:4683
|
#: documents/views.py:4679
|
||||||
msgid "Bundle is already being processed."
|
msgid "Bundle is already being processed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: documents/views.py:4747
|
#: documents/views.py:4743
|
||||||
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:4761
|
#: documents/views.py:4757
|
||||||
msgid "The share link bundle is unavailable."
|
msgid "The share link bundle is unavailable."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ def _rewrite_request_to_pinned_ip(
|
|||||||
method=request.method,
|
method=request.method,
|
||||||
url=new_url,
|
url=new_url,
|
||||||
headers=new_headers,
|
headers=new_headers,
|
||||||
stream=request.stream,
|
content=request.stream,
|
||||||
extensions=request.extensions,
|
extensions=request.extensions,
|
||||||
)
|
)
|
||||||
rewritten_request.extensions["sni_hostname"] = hostname
|
rewritten_request.extensions["sni_hostname"] = hostname
|
||||||
|
|||||||
@@ -705,12 +705,6 @@ CELERY_BROKER_TRANSPORT_OPTIONS = {
|
|||||||
CELERY_TASK_TRACK_STARTED = True
|
CELERY_TASK_TRACK_STARTED = True
|
||||||
CELERY_TASK_TIME_LIMIT: Final[int] = get_int_from_env("PAPERLESS_WORKER_TIMEOUT", 1800)
|
CELERY_TASK_TIME_LIMIT: Final[int] = get_int_from_env("PAPERLESS_WORKER_TIMEOUT", 1800)
|
||||||
|
|
||||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std-setting-task_allow_error_cb_on_chord_header
|
|
||||||
# Without this, a failing chord header never triggers the errback, so a mail
|
|
||||||
# whose attachments all fail is never recorded and is re-fetched forever.
|
|
||||||
# The errback runs once per failed header task, so it must be idempotent.
|
|
||||||
CELERY_TASK_ALLOW_ERROR_CB_ON_CHORD_HEADER = True
|
|
||||||
|
|
||||||
CELERY_CACHE_BACKEND = "default"
|
CELERY_CACHE_BACKEND = "default"
|
||||||
|
|
||||||
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-serializer
|
# https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-serializer
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ def _stream_chat_with_documents(
|
|||||||
# limit (_MAX_IN_VALUES) on large installs. Trashed documents stay
|
# limit (_MAX_IN_VALUES) on large installs. Trashed documents stay
|
||||||
# indexed until permanent deletion (delete_document_from_llm_index
|
# indexed until permanent deletion (delete_document_from_llm_index
|
||||||
# hangs off post_delete, not trash), so must be excluded explicitly.
|
# hangs off post_delete, not trash), so must be excluded explicitly.
|
||||||
trashed_ids = Document.deleted_objects.values_list("pk", flat=True)
|
trashed_ids = Document.global_objects.filter(
|
||||||
|
deleted_at__isnull=False,
|
||||||
|
).values_list("pk", flat=True)
|
||||||
filters = exclude_document_ids_filter(str(pk) for pk in trashed_ids)
|
filters = exclude_document_ids_filter(str(pk) for pk in trashed_ids)
|
||||||
else:
|
else:
|
||||||
filters = document_id_filters(
|
filters = document_id_filters(
|
||||||
|
|||||||
@@ -131,10 +131,11 @@ class AIClient:
|
|||||||
|
|
||||||
from llama_index.core.llms import ChatMessage
|
from llama_index.core.llms import ChatMessage
|
||||||
|
|
||||||
|
user_msg = ChatMessage(role="user", content=prompt)
|
||||||
if self.settings.llm_backend == LLMBackend.OLLAMA:
|
if self.settings.llm_backend == LLMBackend.OLLAMA:
|
||||||
with self._normalize_timeouts():
|
with self._normalize_timeouts():
|
||||||
result = self.llm.chat(
|
result = self.llm.chat(
|
||||||
[ChatMessage(role="user", content=prompt)],
|
[user_msg],
|
||||||
format=DocumentClassifierSchema.model_json_schema(),
|
format=DocumentClassifierSchema.model_json_schema(),
|
||||||
think=False,
|
think=False,
|
||||||
)
|
)
|
||||||
@@ -148,11 +149,6 @@ class AIClient:
|
|||||||
from llama_index.core.program.function_program import get_function_tool
|
from llama_index.core.program.function_program import get_function_tool
|
||||||
|
|
||||||
tool = get_function_tool(DocumentClassifierSchema)
|
tool = get_function_tool(DocumentClassifierSchema)
|
||||||
user_msg = ChatMessage(
|
|
||||||
role="user",
|
|
||||||
content=f"{prompt}\n\n"
|
|
||||||
f"Answer by calling the {tool.metadata.name} tool. Do not write the answer as text.",
|
|
||||||
)
|
|
||||||
with self._normalize_timeouts():
|
with self._normalize_timeouts():
|
||||||
result = self.llm.chat_with_tools(
|
result = self.llm.chat_with_tools(
|
||||||
tools=[tool],
|
tools=[tool],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Rewrite only the "title", "tags", "document_types", and "storage_paths" fields i
|
|||||||
|
|
||||||
Do not translate correspondents or dates.
|
Do not translate correspondents or dates.
|
||||||
Preserve proper nouns, organization names, product names, and exact official document names. Translate generic category words when a {{ language_name }} equivalent exists.
|
Preserve proper nouns, organization names, product names, and exact official document names. Translate generic category words when a {{ language_name }} equivalent exists.
|
||||||
Keep every entry you were given in those four fields, in the same order, using the original wording where no translation applies.
|
Return the same JSON schema with all fields present.
|
||||||
|
|
||||||
Suggestions:
|
Suggestions:
|
||||||
{{ suggestions_json }}
|
{{ suggestions_json }}
|
||||||
|
|||||||
@@ -154,6 +154,35 @@ class DocumentMetaTable:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PermittedIdsTable:
|
||||||
|
"""Per-connection scratch space for an oversized IN-filter id list.
|
||||||
|
|
||||||
|
A literal ``IN (?,?,...)`` list binds one SQL parameter per id, capped by
|
||||||
|
SQLite's own SQLITE_MAX_VARIABLE_NUMBER (see _MAX_IN_VALUES in
|
||||||
|
vector_store.py). Loading the ids into a TEMP TABLE and filtering via a
|
||||||
|
subquery instead has no such limit. TEMP tables live in a
|
||||||
|
connection-private namespace -- never visible to another connection,
|
||||||
|
even under this identical name -- so this is safe under the vector
|
||||||
|
store's one-connection-per-request model without any extra locking or
|
||||||
|
per-call naming scheme.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TABLE_NAME = "permitted_document_ids"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load(conn: sqlite3.Connection, ids: Iterable[int]) -> None:
|
||||||
|
"""Replace this connection's scratch table with ``ids``."""
|
||||||
|
conn.execute(f"DROP TABLE IF EXISTS temp.{PermittedIdsTable.TABLE_NAME}")
|
||||||
|
conn.execute(
|
||||||
|
f"CREATE TEMP TABLE {PermittedIdsTable.TABLE_NAME} "
|
||||||
|
"(id INTEGER PRIMARY KEY)",
|
||||||
|
)
|
||||||
|
conn.executemany(
|
||||||
|
f"INSERT INTO {PermittedIdsTable.TABLE_NAME} (id) VALUES (?)",
|
||||||
|
((i,) for i in ids),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class IndexMetaTable:
|
class IndexMetaTable:
|
||||||
"""Typed accessors over index_meta's key/value rows -- replaces
|
"""Typed accessors over index_meta's key/value rows -- replaces
|
||||||
PaperlessSqliteVecVectorStore._meta_get_on/_meta_set_on, which returned
|
PaperlessSqliteVecVectorStore._meta_get_on/_meta_set_on, which returned
|
||||||
|
|||||||
@@ -146,8 +146,6 @@ def test_run_llm_query_ollama_uses_structured_json(mock_ai_config, mock_ollama_l
|
|||||||
format=ANY,
|
format=ANY,
|
||||||
think=False,
|
think=False,
|
||||||
)
|
)
|
||||||
messages = mock_llm_instance.chat.call_args.args[0]
|
|
||||||
assert messages[0].content == "test_prompt"
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_llm_query_openai_uses_tools(mock_ai_config, mock_openai_llm):
|
def test_run_llm_query_openai_uses_tools(mock_ai_config, mock_openai_llm):
|
||||||
@@ -185,13 +183,6 @@ def test_run_llm_query_openai_uses_tools(mock_ai_config, mock_openai_llm):
|
|||||||
assert result["title"] == "Test Title"
|
assert result["title"] == "Test Title"
|
||||||
assert result["tags"] == {"existing_ids": [1], "new_names": []}
|
assert result["tags"] == {"existing_ids": [1], "new_names": []}
|
||||||
mock_llm_instance.chat_with_tools.assert_called_once()
|
mock_llm_instance.chat_with_tools.assert_called_once()
|
||||||
kwargs = mock_llm_instance.chat_with_tools.call_args.kwargs
|
|
||||||
offered_tool_name = kwargs["tools"][0].metadata.name
|
|
||||||
assert kwargs["user_msg"].content == (
|
|
||||||
"test_prompt\n\n"
|
|
||||||
f"Answer by calling the {offered_tool_name} tool. "
|
|
||||||
"Do not write the answer as text."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_run_llm_query_openai_timeout_raises_local_error(
|
def test_run_llm_query_openai_timeout_raises_local_error(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from paperless_ai.tables import DocumentChunksTable
|
|||||||
from paperless_ai.tables import DocumentMetaRow
|
from paperless_ai.tables import DocumentMetaRow
|
||||||
from paperless_ai.tables import DocumentMetaTable
|
from paperless_ai.tables import DocumentMetaTable
|
||||||
from paperless_ai.tables import IndexMetaTable
|
from paperless_ai.tables import IndexMetaTable
|
||||||
|
from paperless_ai.tables import PermittedIdsTable
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -338,3 +339,85 @@ class TestIndexMetaTable:
|
|||||||
IndexMetaTable.increment_total_inserts(conn, 100)
|
IndexMetaTable.increment_total_inserts(conn, 100)
|
||||||
IndexMetaTable.reset_total_inserts(conn, 7)
|
IndexMetaTable.reset_total_inserts(conn, 7)
|
||||||
assert IndexMetaTable.get_total_inserts(conn) == 7
|
assert IndexMetaTable.get_total_inserts(conn) == 7
|
||||||
|
|
||||||
|
|
||||||
|
class TestPermittedIdsTable:
|
||||||
|
def _loaded_ids(self, conn: sqlite3.Connection) -> list[int]:
|
||||||
|
return [
|
||||||
|
row["id"]
|
||||||
|
for row in conn.execute(
|
||||||
|
f"SELECT id FROM {PermittedIdsTable.TABLE_NAME} ORDER BY id",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_load_then_read_back_all_ids(self, conn: sqlite3.Connection) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A bare sqlite3 connection
|
||||||
|
WHEN:
|
||||||
|
- load() is called with a set of ids
|
||||||
|
THEN:
|
||||||
|
- Every id is present in the TEMP TABLE, and only those ids
|
||||||
|
"""
|
||||||
|
PermittedIdsTable.load(conn, [3, 1, 2])
|
||||||
|
assert self._loaded_ids(conn) == [1, 2, 3]
|
||||||
|
|
||||||
|
def test_load_replaces_previous_contents(self, conn: sqlite3.Connection) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A connection whose PermittedIdsTable already holds one id set
|
||||||
|
WHEN:
|
||||||
|
- load() is called again with a different id set
|
||||||
|
THEN:
|
||||||
|
- Only the new ids are present -- a connection reused across
|
||||||
|
multiple queries in one request never leaks a stale filter
|
||||||
|
"""
|
||||||
|
PermittedIdsTable.load(conn, [1, 2, 3])
|
||||||
|
PermittedIdsTable.load(conn, [4, 5])
|
||||||
|
assert self._loaded_ids(conn) == [4, 5]
|
||||||
|
|
||||||
|
def test_load_is_connection_private(self) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Two separate connections
|
||||||
|
WHEN:
|
||||||
|
- Each loads PermittedIdsTable with a different id set, under
|
||||||
|
the identical TABLE_NAME
|
||||||
|
THEN:
|
||||||
|
- Each connection sees only its own ids -- TEMP TABLE is
|
||||||
|
connection-private, so concurrent requests never collide or
|
||||||
|
cross-contaminate despite sharing the same table name (the
|
||||||
|
vector store opens one connection per request; see
|
||||||
|
PaperlessSqliteVecVectorStore)
|
||||||
|
"""
|
||||||
|
conn_a = sqlite3.connect(":memory:")
|
||||||
|
conn_a.row_factory = sqlite3.Row
|
||||||
|
conn_b = sqlite3.connect(":memory:")
|
||||||
|
conn_b.row_factory = sqlite3.Row
|
||||||
|
try:
|
||||||
|
PermittedIdsTable.load(conn_a, [1, 2, 3])
|
||||||
|
PermittedIdsTable.load(conn_b, [4, 5, 6])
|
||||||
|
assert self._loaded_ids(conn_a) == [1, 2, 3]
|
||||||
|
assert self._loaded_ids(conn_b) == [4, 5, 6]
|
||||||
|
finally:
|
||||||
|
conn_a.close()
|
||||||
|
conn_b.close()
|
||||||
|
|
||||||
|
def test_load_handles_more_ids_than_a_bound_parameter_list_could(
|
||||||
|
self,
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- An id count over SQLite's own bound-parameter limit
|
||||||
|
(SQLITE_MAX_VARIABLE_NUMBER, 32766 by default) -- more than a
|
||||||
|
literal IN(?,?,...) list could ever bind in one statement
|
||||||
|
WHEN:
|
||||||
|
- load() is called with that many ids
|
||||||
|
THEN:
|
||||||
|
- Every id is loaded without error, since executemany() binds
|
||||||
|
one row at a time rather than one statement with N parameters
|
||||||
|
"""
|
||||||
|
ids = list(range(40_000))
|
||||||
|
PermittedIdsTable.load(conn, ids)
|
||||||
|
assert self._loaded_ids(conn) == ids
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from paperless_ai.migrations import Migration
|
|||||||
from paperless_ai.migrations import m0001_v1_to_v2
|
from paperless_ai.migrations import m0001_v1_to_v2
|
||||||
from paperless_ai.tables import DocumentChunksTable
|
from paperless_ai.tables import DocumentChunksTable
|
||||||
from paperless_ai.tables import DocumentMetaTable
|
from paperless_ai.tables import DocumentMetaTable
|
||||||
|
from paperless_ai.tables import PermittedIdsTable
|
||||||
from paperless_ai.vector_store import _MAX_IN_VALUES
|
from paperless_ai.vector_store import _MAX_IN_VALUES
|
||||||
from paperless_ai.vector_store import DB_FILENAME
|
from paperless_ai.vector_store import DB_FILENAME
|
||||||
from paperless_ai.vector_store import DEFAULT_TABLE_NAME
|
from paperless_ai.vector_store import DEFAULT_TABLE_NAME
|
||||||
@@ -280,8 +281,23 @@ class TestCrud:
|
|||||||
|
|
||||||
|
|
||||||
class TestBuildWhere:
|
class TestBuildWhere:
|
||||||
def test_ne_filter_translates_to_not_equal_clause(self) -> None:
|
@pytest.fixture
|
||||||
where, params = _build_where(_ne_filter(1))
|
def conn(self) -> Generator[sqlite3.Connection, None, None]:
|
||||||
|
"""A bare connection, sufficient for _build_where(): it only ever
|
||||||
|
touches the connection via PermittedIdsTable, which needs no vec0
|
||||||
|
extension loaded.
|
||||||
|
"""
|
||||||
|
connection = sqlite3.connect(":memory:")
|
||||||
|
try:
|
||||||
|
yield connection
|
||||||
|
finally:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
def test_ne_filter_translates_to_not_equal_clause(
|
||||||
|
self,
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
) -> None:
|
||||||
|
where, params = _build_where(conn, _ne_filter(1))
|
||||||
assert where == "(document_id != ?)"
|
assert where == "(document_id != ?)"
|
||||||
assert params == [1]
|
assert params == [1]
|
||||||
|
|
||||||
@@ -293,8 +309,11 @@ class TestBuildWhere:
|
|||||||
"b1",
|
"b1",
|
||||||
]
|
]
|
||||||
|
|
||||||
def test_nin_filter_translates_to_not_in_clause(self) -> None:
|
def test_nin_filter_translates_to_not_in_clause(
|
||||||
where, params = _build_where(_nin_filter([1, 2]))
|
self,
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
) -> None:
|
||||||
|
where, params = _build_where(conn, _nin_filter([1, 2]))
|
||||||
assert where == "(document_id NOT IN (?,?))"
|
assert where == "(document_id NOT IN (?,?))"
|
||||||
assert params == [1, 2]
|
assert params == [1, 2]
|
||||||
|
|
||||||
@@ -304,7 +323,10 @@ class TestBuildWhere:
|
|||||||
_query(store, [0.0] * DIM, top_k=5, filters=_nin_filter([1, 2])).ids,
|
_query(store, [0.0] * DIM, top_k=5, filters=_nin_filter([1, 2])).ids,
|
||||||
) == ["c1"]
|
) == ["c1"]
|
||||||
|
|
||||||
def test_empty_in_filter_excludes_everything(self) -> None:
|
def test_empty_in_filter_excludes_everything(
|
||||||
|
self,
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- An IN filter with an empty value list
|
- An IN filter with an empty value list
|
||||||
@@ -314,11 +336,14 @@ class TestBuildWhere:
|
|||||||
- It excludes everything (the opposite of an empty NOT IN
|
- It excludes everything (the opposite of an empty NOT IN
|
||||||
filter) -- an empty inclusion list must never widen results
|
filter) -- an empty inclusion list must never widen results
|
||||||
"""
|
"""
|
||||||
where, params = _build_where(_in_filter([]))
|
where, params = _build_where(conn, _in_filter([]))
|
||||||
assert where == "(1 = 0)"
|
assert where == "(1 = 0)"
|
||||||
assert params == []
|
assert params == []
|
||||||
|
|
||||||
def test_empty_nin_filter_excludes_nothing(self) -> None:
|
def test_empty_nin_filter_excludes_nothing(
|
||||||
|
self,
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- A NOT IN filter with an empty value list -- e.g. an
|
- A NOT IN filter with an empty value list -- e.g. an
|
||||||
@@ -330,11 +355,14 @@ class TestBuildWhere:
|
|||||||
excludes everything) -- an empty exclusion list must never
|
excludes everything) -- an empty exclusion list must never
|
||||||
narrow results
|
narrow results
|
||||||
"""
|
"""
|
||||||
where, params = _build_where(_nin_filter([]))
|
where, params = _build_where(conn, _nin_filter([]))
|
||||||
assert where == "(1 = 1)"
|
assert where == "(1 = 1)"
|
||||||
assert params == []
|
assert params == []
|
||||||
|
|
||||||
def test_fails_closed_when_no_filter_is_translatable(self) -> None:
|
def test_fails_closed_when_no_filter_is_translatable(
|
||||||
|
self,
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
) -> None:
|
||||||
# A nested MetadataFilters is not a MetadataFilter, so it is skipped.
|
# A nested MetadataFilters is not a MetadataFilter, so it is skipped.
|
||||||
# With no translatable clauses, the function must fail closed rather
|
# With no translatable clauses, the function must fail closed rather
|
||||||
# than emit "()" (invalid SQL) and never widen document access.
|
# than emit "()" (invalid SQL) and never widen document access.
|
||||||
@@ -347,42 +375,88 @@ class TestBuildWhere:
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
where, params = _build_where(MetadataFilters(filters=[nested]))
|
where, params = _build_where(conn, MetadataFilters(filters=[nested]))
|
||||||
assert where == "1 = 0"
|
assert where == "1 = 0"
|
||||||
assert params == []
|
assert params == []
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"build_filter",
|
("build_filter", "sql_op"),
|
||||||
[_in_filter, _nin_filter],
|
[(_in_filter, "IN"), (_nin_filter, "NOT IN")],
|
||||||
ids=["in", "nin"],
|
ids=["in", "nin"],
|
||||||
)
|
)
|
||||||
def test_fails_closed_when_filter_exceeds_max_values(
|
def test_filter_over_max_values_uses_permitted_ids_table(
|
||||||
self,
|
self,
|
||||||
|
conn: sqlite3.Connection,
|
||||||
build_filter: Callable[[list[str]], MetadataFilters],
|
build_filter: Callable[[list[str]], MetadataFilters],
|
||||||
caplog: pytest.LogCaptureFixture,
|
sql_op: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
GIVEN:
|
GIVEN:
|
||||||
- An IN or NOT IN filter with more values than _MAX_IN_VALUES
|
- An IN or NOT IN filter with more values than _MAX_IN_VALUES
|
||||||
(SQLite's own bound-parameter limit is 32766; this guard sits
|
(SQLite's own bound-parameter limit is 32766; this threshold
|
||||||
below that with headroom for the query's other bound parameters)
|
sits below that with headroom for the query's other bound
|
||||||
|
parameters)
|
||||||
WHEN:
|
WHEN:
|
||||||
- _build_where() translates it to SQL
|
- _build_where() translates it to SQL
|
||||||
THEN:
|
THEN:
|
||||||
- It fails closed ("1 = 0", no params) instead of building a
|
- It builds a subquery against PermittedIdsTable's TEMP TABLE,
|
||||||
clause SQLite would reject, and logs a warning -- this filter
|
loaded with every id, instead of a literal list SQLite would
|
||||||
scopes document access, so refusing to build it must never
|
reject past its own limit -- true for NOT IN too (e.g. an
|
||||||
widen the scope to "everything" by accident. Failing open on
|
install with an enormous trash), not just IN
|
||||||
a NOT IN would surface exactly the excluded rows
|
|
||||||
"""
|
"""
|
||||||
oversized = build_filter([str(i) for i in range(_MAX_IN_VALUES + 1)])
|
ids = list(range(_MAX_IN_VALUES + 1))
|
||||||
|
oversized = build_filter([str(i) for i in ids])
|
||||||
|
|
||||||
with caplog.at_level("WARNING"):
|
where, params = _build_where(conn, oversized)
|
||||||
where, params = _build_where(oversized)
|
|
||||||
|
|
||||||
assert where == "(1 = 0)"
|
assert where == (
|
||||||
|
f"(document_id {sql_op} (SELECT id FROM {PermittedIdsTable.TABLE_NAME}))"
|
||||||
|
)
|
||||||
assert params == []
|
assert params == []
|
||||||
assert "document_id" in caplog.text
|
loaded = [
|
||||||
|
row[0]
|
||||||
|
for row in conn.execute(
|
||||||
|
f"SELECT id FROM {PermittedIdsTable.TABLE_NAME} ORDER BY id",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
assert loaded == ids
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("build_filter", "expected_ids"),
|
||||||
|
[(_in_filter, ["b1", "c1"]), (_nin_filter, ["a1"])],
|
||||||
|
ids=["in", "nin"],
|
||||||
|
)
|
||||||
|
def test_query_and_get_nodes_scope_correctly_when_filter_exceeds_max_values(
|
||||||
|
self,
|
||||||
|
store: PaperlessSqliteVecVectorStore,
|
||||||
|
mocker: MockerFixture,
|
||||||
|
build_filter: Callable[[list[int]], MetadataFilters],
|
||||||
|
expected_ids: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- _MAX_IN_VALUES lowered so a small IN/NOT IN filter exceeds it
|
||||||
|
WHEN:
|
||||||
|
- query() and get_nodes() are called with that filter
|
||||||
|
THEN:
|
||||||
|
- Both still correctly scope results -- the PermittedIdsTable
|
||||||
|
temp-table path behaves identically to the literal
|
||||||
|
IN(...)/NOT IN(...) path it replaces above the threshold
|
||||||
|
"""
|
||||||
|
mocker.patch("paperless_ai.vector_store._MAX_IN_VALUES", 1)
|
||||||
|
store.add(
|
||||||
|
[
|
||||||
|
make_node("a1", 1, seed=0.0),
|
||||||
|
make_node("b1", 2, seed=1.0),
|
||||||
|
make_node("c1", 3, seed=2.0),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
result = _query(store, [0.0] * DIM, top_k=10, filters=build_filter([2, 3]))
|
||||||
|
nodes = store.get_nodes(filters=build_filter([2, 3]))
|
||||||
|
|
||||||
|
assert sorted(result.ids) == expected_ids
|
||||||
|
assert sorted(n.node_id for n in nodes) == expected_ids
|
||||||
|
|
||||||
def test_query_with_untranslatable_filter_returns_no_rows(
|
def test_query_with_untranslatable_filter_returns_no_rows(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ from paperless_ai.tables import DocumentChunksTable
|
|||||||
from paperless_ai.tables import DocumentMetaRow
|
from paperless_ai.tables import DocumentMetaRow
|
||||||
from paperless_ai.tables import DocumentMetaTable
|
from paperless_ai.tables import DocumentMetaTable
|
||||||
from paperless_ai.tables import IndexMetaTable
|
from paperless_ai.tables import IndexMetaTable
|
||||||
|
from paperless_ai.tables import PermittedIdsTable
|
||||||
|
|
||||||
logger = logging.getLogger("paperless_ai.vector_store")
|
logger = logging.getLogger("paperless_ai.vector_store")
|
||||||
|
|
||||||
@@ -75,14 +76,12 @@ class _Row(NamedTuple):
|
|||||||
embedding: bytes
|
embedding: bytes
|
||||||
|
|
||||||
|
|
||||||
# _build_where(): the largest IN value list translated into bound SQL
|
# _build_where(): the largest IN value list translated into a literal
|
||||||
# parameters. SQLite's own hard limit (SQLITE_MAX_VARIABLE_NUMBER) is 32766
|
# IN (?,?,...) clause. SQLite's own hard limit (SQLITE_MAX_VARIABLE_NUMBER)
|
||||||
# by default; this leaves headroom below that for the query's other bound
|
# is 32766 by default; this leaves headroom below that for the query's other
|
||||||
# parameters (the embedding blob, k, and any NE clause) and for the limit
|
# bound parameters (the embedding blob, k, and any NE clause) and for the
|
||||||
# itself to move. An IN filter this large should not happen in practice --
|
# limit itself to move. Above this threshold _build_where() switches to
|
||||||
# callers are expected to pass None (no filter) rather than every id when
|
# PermittedIdsTable instead of failing closed -- see its docstring.
|
||||||
# the filter would not actually narrow anything -- so this is a guard
|
|
||||||
# against a future regression, not a normal code path.
|
|
||||||
_MAX_IN_VALUES = 32700
|
_MAX_IN_VALUES = 32700
|
||||||
|
|
||||||
|
|
||||||
@@ -106,7 +105,10 @@ def _vec0_params(rows: list[_Row]) -> list[tuple[str, int, str, bytes]]:
|
|||||||
return [(r.chunk_id, r.document_id, r.node_content, r.embedding) for r in rows]
|
return [(r.chunk_id, r.document_id, r.node_content, r.embedding) for r in rows]
|
||||||
|
|
||||||
|
|
||||||
def _build_where(filters: MetadataFilters | None) -> tuple[str, list[int]]:
|
def _build_where(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
filters: MetadataFilters | None,
|
||||||
|
) -> tuple[str, list[int]]:
|
||||||
"""Translate the EQ / IN / NIN / NE filters we use into a parameterized
|
"""Translate the EQ / IN / NIN / NE filters we use into a parameterized
|
||||||
SQL clause on vec0 metadata columns. Returns ("", []) when there is
|
SQL clause on vec0 metadata columns. Returns ("", []) when there is
|
||||||
nothing to filter. document_id is vec0's only filterable column and is
|
nothing to filter. document_id is vec0's only filterable column and is
|
||||||
@@ -114,6 +116,10 @@ def _build_where(filters: MetadataFilters | None) -> tuple[str, list[int]]:
|
|||||||
still pass strings in places, e.g. indexing.py's MetadataFilter
|
still pass strings in places, e.g. indexing.py's MetadataFilter
|
||||||
construction) don't have to be individually correct -- vec0 doesn't
|
construction) don't have to be individually correct -- vec0 doesn't
|
||||||
coerce types itself.
|
coerce types itself.
|
||||||
|
|
||||||
|
``conn`` is only used for an IN/NOT IN filter over _MAX_IN_VALUES: it
|
||||||
|
loads the ids into PermittedIdsTable's TEMP TABLE on that connection
|
||||||
|
rather than binding them as SQL parameters.
|
||||||
"""
|
"""
|
||||||
if filters is None or not filters.filters:
|
if filters is None or not filters.filters:
|
||||||
return "", []
|
return "", []
|
||||||
@@ -136,20 +142,15 @@ def _build_where(filters: MetadataFilters | None) -> tuple[str, list[int]]:
|
|||||||
clauses.append("1 = 0" if is_in else "1 = 1")
|
clauses.append("1 = 0" if is_in else "1 = 1")
|
||||||
continue
|
continue
|
||||||
if len(values) > _MAX_IN_VALUES:
|
if len(values) > _MAX_IN_VALUES:
|
||||||
# Refuse rather than risk SQLite's own bound-parameter limit
|
# A literal list this large would exceed SQLite's own
|
||||||
# ("too many SQL variables"): a list this large must match no
|
# bound-parameter limit. Load the ids into a TEMP TABLE on
|
||||||
# rows, never widen the scope to "everything" -- true for
|
# this connection instead and filter via subquery, which has
|
||||||
# NOT IN too, where failing open would surface every
|
# no such limit -- see PermittedIdsTable. Applies to NOT IN
|
||||||
# excluded row.
|
# too (e.g. an install with an enormous trash), not just IN.
|
||||||
logger.warning(
|
PermittedIdsTable.load(conn, values)
|
||||||
"Refusing to build a %s filter on %r with %d values "
|
clauses.append(
|
||||||
"(over the %d-value safety limit); returning no rows.",
|
f"{f.key} {sql_op} (SELECT id FROM {PermittedIdsTable.TABLE_NAME})",
|
||||||
sql_op,
|
|
||||||
f.key,
|
|
||||||
len(values),
|
|
||||||
_MAX_IN_VALUES,
|
|
||||||
)
|
)
|
||||||
clauses.append("1 = 0")
|
|
||||||
continue
|
continue
|
||||||
placeholders = ",".join("?" for _ in values)
|
placeholders = ",".join("?" for _ in values)
|
||||||
clauses.append(f"{f.key} {sql_op} ({placeholders})")
|
clauses.append(f"{f.key} {sql_op} ({placeholders})")
|
||||||
@@ -488,7 +489,7 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
|
|||||||
)
|
)
|
||||||
if not self.table_exists():
|
if not self.table_exists():
|
||||||
return []
|
return []
|
||||||
where, params = _build_where(filters)
|
where, params = _build_where(self._conn, filters)
|
||||||
sql = "SELECT node_content, embedding FROM " + DEFAULT_TABLE_NAME
|
sql = "SELECT node_content, embedding FROM " + DEFAULT_TABLE_NAME
|
||||||
if where:
|
if where:
|
||||||
sql += " WHERE " + where
|
sql += " WHERE " + where
|
||||||
@@ -504,7 +505,7 @@ class PaperlessSqliteVecVectorStore(BasePydanticVectorStore):
|
|||||||
if query.query_embedding is None: # pragma: no cover
|
if query.query_embedding is None: # pragma: no cover
|
||||||
return VectorStoreQueryResult(nodes=[], similarities=[], ids=[])
|
return VectorStoreQueryResult(nodes=[], similarities=[], ids=[])
|
||||||
top_k = query.similarity_top_k if query.similarity_top_k is not None else 10
|
top_k = query.similarity_top_k if query.similarity_top_k is not None else 10
|
||||||
where, params = _build_where(query.filters)
|
where, params = _build_where(self._conn, query.filters)
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT id, node_content, embedding, distance FROM "
|
"SELECT id, node_content, embedding, distance FROM "
|
||||||
+ DEFAULT_TABLE_NAME
|
+ DEFAULT_TABLE_NAME
|
||||||
|
|||||||
@@ -334,24 +334,18 @@ def error_callback(
|
|||||||
"""
|
"""
|
||||||
A shared task that is called whenever something goes wrong during
|
A shared task that is called whenever something goes wrong during
|
||||||
consumption of a file. See queue_consumption_tasks.
|
consumption of a file. See queue_consumption_tasks.
|
||||||
|
|
||||||
With CELERY_TASK_ALLOW_ERROR_CB_ON_CHORD_HEADER enabled this runs once per
|
|
||||||
failed header task, not once per chord, so it must be idempotent.
|
|
||||||
"""
|
"""
|
||||||
rule = MailRule.objects.get(pk=rule_id)
|
rule = MailRule.objects.get(pk=rule_id)
|
||||||
received = make_aware(message_date) if is_naive(message_date) else message_date
|
|
||||||
|
|
||||||
ProcessedMail.objects.get_or_create(
|
ProcessedMail.objects.create(
|
||||||
rule=rule,
|
rule=rule,
|
||||||
folder=rule.folder,
|
folder=rule.folder,
|
||||||
uid=message_uid,
|
uid=message_uid,
|
||||||
uid_validity=uid_validity,
|
uid_validity=uid_validity,
|
||||||
defaults={
|
subject=message_subject,
|
||||||
"subject": message_subject,
|
received=make_aware(message_date) if is_naive(message_date) else message_date,
|
||||||
"received": received,
|
status="FAILED",
|
||||||
"status": "FAILED",
|
error=traceback.format_exc(),
|
||||||
"error": traceback.format_exc(),
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -708,9 +708,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss-selector-parser": {
|
"node_modules/postcss-selector-parser": {
|
||||||
"version": "6.1.4",
|
"version": "6.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
|
||||||
"integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==",
|
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ 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
|
||||||
from paperless_mail.mail import apply_mail_action
|
from paperless_mail.mail import apply_mail_action
|
||||||
from paperless_mail.mail import error_callback
|
|
||||||
from paperless_mail.mail import get_mailbox
|
from paperless_mail.mail import get_mailbox
|
||||||
from paperless_mail.models import MailAccount
|
from paperless_mail.models import MailAccount
|
||||||
from paperless_mail.models import MailRule
|
from paperless_mail.models import MailRule
|
||||||
@@ -2046,44 +2045,6 @@ class TestPostConsumeAction(TestCase):
|
|||||||
self.assertIn("Test Exception", processed_mail.error)
|
self.assertIn("Test Exception", processed_mail.error)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
|
||||||
class TestErrorCallback:
|
|
||||||
def test_error_callback_is_idempotent_for_same_mail(self) -> None:
|
|
||||||
"""
|
|
||||||
GIVEN:
|
|
||||||
- A mail rule and a mail that failed to be consumed
|
|
||||||
WHEN:
|
|
||||||
- error_callback is invoked more than once for the same mail, as
|
|
||||||
happens when task_allow_error_cb_on_chord_header fires the
|
|
||||||
errback once per failed header task in a chord
|
|
||||||
THEN:
|
|
||||||
- Only one ProcessedMail row is created for that mail
|
|
||||||
"""
|
|
||||||
rule = MailRuleFactory()
|
|
||||||
message_uid = "12345"
|
|
||||||
|
|
||||||
for _ in range(2):
|
|
||||||
error_callback(
|
|
||||||
None,
|
|
||||||
Exception("Test Exception"),
|
|
||||||
None,
|
|
||||||
rule_id=rule.pk,
|
|
||||||
message_uid=message_uid,
|
|
||||||
message_subject="Test Subject",
|
|
||||||
message_date=timezone.make_aware(
|
|
||||||
timezone.datetime(2023, 1, 1, 12, 0, 0),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
processed_mails = ProcessedMail.objects.filter(
|
|
||||||
rule=rule,
|
|
||||||
uid=message_uid,
|
|
||||||
folder=rule.folder,
|
|
||||||
)
|
|
||||||
assert processed_mails.count() == 1
|
|
||||||
assert processed_mails.get().status == "FAILED"
|
|
||||||
|
|
||||||
|
|
||||||
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",
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ requires-python = ">=3.11"
|
|||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.15' and sys_platform == 'darwin'",
|
"python_full_version >= '3.15' and sys_platform == 'darwin'",
|
||||||
"python_full_version >= '3.15' and sys_platform == 'linux'",
|
"python_full_version >= '3.15' and sys_platform == 'linux'",
|
||||||
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
|
||||||
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
|
||||||
"python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'darwin'",
|
"python_full_version >= '3.12' and python_full_version < '3.15' and sys_platform == 'darwin'",
|
||||||
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||||
|
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
|
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||||
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
|
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
|
||||||
"python_full_version < '3.12' and sys_platform == 'darwin'",
|
"python_full_version < '3.12' and sys_platform == 'darwin'",
|
||||||
"python_full_version < '3.12' and sys_platform == 'linux'",
|
"python_full_version < '3.12' and sys_platform == 'linux'",
|
||||||
@@ -1116,14 +1116,14 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djangorestframework"
|
name = "djangorestframework"
|
||||||
version = "3.17.2"
|
version = "3.17.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "django" },
|
{ name = "django" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/3b/35/c96055e700fdff25da3a7b7756cfd1d4dc54f38b9bc6d6c5e19e3a0fdc20/djangorestframework-3.17.2.tar.gz", hash = "sha256:89ed713b6dc83e1539f214b7d10808ae19bb8511004beba886225da6d5c9dafa", size = 906683, upload-time = "2026-08-05T07:47:22.5Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ca/d7/c016e69fac19ff8afdc89db9d31d9ae43ae031e4d1993b20aca179b8301a/djangorestframework-3.17.1.tar.gz", hash = "sha256:a6def5f447fe78ff853bff1d47a3c59bf38f5434b031780b351b0c73a62db1a5", size = 905742, upload-time = "2026-03-24T16:58:33.705Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a2/46/c14108e400b208c394325eb63fbae06c81341b6447fa1a6f9da718b17fe7/djangorestframework-3.17.2-py3-none-any.whl", hash = "sha256:cb0546a7415d5b46c04e0f4fe0a54b2109f4fdd5e83ca773c8c6183a6493d042", size = 899109, upload-time = "2026-08-05T07:47:20.853Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/e1/2c516bdc83652b1a60c6119366ac2c0607b479ed05cd6093f916ca8928f8/djangorestframework-3.17.1-py3-none-any.whl", hash = "sha256:c3c74dd3e83a5a3efc37b3c18d92bd6f86a6791c7b7d4dff62bb068500e76457", size = 898844, upload-time = "2026-03-24T16:58:31.845Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2718,7 +2718,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nltk"
|
name = "nltk"
|
||||||
version = "3.10.3"
|
version = "3.10.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
@@ -2727,9 +2727,9 @@ dependencies = [
|
|||||||
{ name = "regex" },
|
{ name = "regex" },
|
||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/e6/fe51d2bb1a3b446f59c5c8165999a9fee208bc346af90a7cbf7657bc0d75/nltk-3.10.3.tar.gz", hash = "sha256:bb9327a461c3811c2fa4900e03840401f2126adfb30c0072827c433bd2444ea4", size = 5137152, upload-time = "2026-08-12T23:46:37.258Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/96/02/df4f105b28a7c16b0e41423bc09cf0f1b8a305df4ef0b10ca74a2e4c648c/nltk-3.10.0.tar.gz", hash = "sha256:4fbac1d98203cbcd1b5d94a2877fb822300072d80604a5e7fae49d2c5f84e8c1", size = 3089244, upload-time = "2026-07-08T02:39:13.562Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/6d/ebd2af4640b12168fdf0cb74b6118df2f32a2f62ec7e0c06fbfd80706639/nltk-3.10.3-py3-none-any.whl", hash = "sha256:ff9598a8e20518ee0d557745890cc4435b9578489e2dcbc69c4f81fa060caf7c", size = 1798643, upload-time = "2026-08-12T23:44:13.478Z" },
|
{ url = "https://files.pythonhosted.org/packages/6e/89/a0b0f35e2820d6a99d75ea1c11977ee6d5c9e6658eceb45b0c7620881faa/nltk-3.10.0-py3-none-any.whl", hash = "sha256:54ff84d4916d3ef127e8953bee0023f6a6b320b75d634a19e06ef056d3d244bf", size = 1716144, upload-time = "2026-07-08T02:39:09.753Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5014,10 +5014,10 @@ version = "2.13.0+cpu"
|
|||||||
source = { registry = "https://download.pytorch.org/whl/cpu" }
|
source = { registry = "https://download.pytorch.org/whl/cpu" }
|
||||||
resolution-markers = [
|
resolution-markers = [
|
||||||
"python_full_version >= '3.15' and sys_platform == 'linux'",
|
"python_full_version >= '3.15' and sys_platform == 'linux'",
|
||||||
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
|
||||||
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
|
||||||
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
"python_full_version == '3.12.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
"python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||||
|
"python_full_version == '3.14.*' and platform_machine == 'x86_64' and sys_platform == 'linux'",
|
||||||
|
"python_full_version == '3.14.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
|
||||||
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
|
"(python_full_version >= '3.12' and python_full_version < '3.15' and platform_machine != 'aarch64' and platform_machine != 'x86_64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and platform_machine == 'x86_64' and sys_platform == 'linux')",
|
||||||
"python_full_version < '3.12' and sys_platform == 'linux'",
|
"python_full_version < '3.12' and sys_platform == 'linux'",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user