mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-16 17:25:11 -05:00
Compare commits
33 Commits
b0e4ddac10
...
fff7a9229c
Author | SHA1 | Date | |
---|---|---|---|
![]() |
fff7a9229c | ||
![]() |
d4f35c23ce | ||
![]() |
f96523a638 | ||
![]() |
ce403730ca | ||
![]() |
0c724e3590 | ||
![]() |
3c1073aaef | ||
![]() |
d922ec4d6e | ||
![]() |
a7cdecd5a2 | ||
![]() |
de9cc7420d | ||
![]() |
a3b6de99fd | ||
![]() |
855b128213 | ||
![]() |
45e70fc99d | ||
![]() |
d439b2bb17 | ||
![]() |
ae174613ff | ||
![]() |
8ee8f2d4c5 | ||
![]() |
3dece8f872 | ||
![]() |
dd1cd5524a | ||
![]() |
52a3884fc4 | ||
![]() |
401e5d68d8 | ||
![]() |
ef764065b8 | ||
![]() |
0418bc58b5 | ||
![]() |
d3644463cc | ||
![]() |
1cd21d0f38 | ||
![]() |
f940ed0b7b | ||
![]() |
3180ccf4cb | ||
![]() |
43abb0541b | ||
![]() |
a3a405354f | ||
![]() |
09e98d600e | ||
![]() |
01a39b9bb4 | ||
![]() |
3b0b40f071 | ||
![]() |
6dce83865f | ||
![]() |
18252a19d7 | ||
![]() |
733a9674d6 |
2
.github/workflows/translate-strings.yml
vendored
2
.github/workflows/translate-strings.yml
vendored
@ -61,7 +61,7 @@ jobs:
|
|||||||
cd src-ui
|
cd src-ui
|
||||||
pnpm run ng extract-i18n
|
pnpm run ng extract-i18n
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: stefanzweifel/git-auto-commit-action@v5
|
uses: stefanzweifel/git-auto-commit-action@v6
|
||||||
with:
|
with:
|
||||||
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
file_pattern: 'src-ui/messages.xlf src/locale/en_US/LC_MESSAGES/django.po'
|
||||||
commit_message: "Auto translate strings"
|
commit_message: "Auto translate strings"
|
||||||
|
@ -573,12 +573,14 @@ The following custom field types are supported:
|
|||||||
|
|
||||||
## PDF Actions
|
## PDF Actions
|
||||||
|
|
||||||
Paperless-ngx supports four basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files):
|
Paperless-ngx supports basic editing operations for PDFs (these operations currently cannot be performed on non-PDF files). When viewing an individual document you can
|
||||||
|
open the 'PDF Editor' to use a simple UI for re-arranging, rotating, deleting pages and splitting documents.
|
||||||
|
|
||||||
- Merging documents: available when selecting multiple documents for 'bulk editing'.
|
- Merging documents: available when selecting multiple documents for 'bulk editing'.
|
||||||
- Rotating documents: available when selecting multiple documents for 'bulk editing' and from an individual document's details page.
|
- Rotating documents: available when selecting multiple documents for 'bulk editing' and via the pdf editor on an individual document's details page.
|
||||||
- Splitting documents: available from an individual document's details page.
|
- Splitting documents: via the pdf editor on an individual document's details page.
|
||||||
- Deleting pages: available from an individual document's details page.
|
- Deleting pages: via the pdf editor on an individual document's details page.
|
||||||
|
- Re-arranging pages: via the pdf editor on an individual document's details page.
|
||||||
|
|
||||||
!!! important
|
!!! important
|
||||||
|
|
||||||
|
@ -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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/alert/alert.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/alert/alert.ts</context>
|
||||||
<context context-type="linenumber">50</context>
|
<context context-type="linenumber">50</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" equiv-text="ueryList<NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
|
<source> Slide <x id="INTERPOLATION" equiv-text="ueryList<NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">131,135</context>
|
<context context-type="linenumber">131,135</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,212 +20,212 @@
|
|||||||
<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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">157,159</context>
|
<context context-type="linenumber">157,159</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">198</context>
|
<context context-type="linenumber">198</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">83,85</context>
|
<context context-type="linenumber">83,85</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">112</context>
|
<context context-type="linenumber">112</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">112</context>
|
<context context-type="linenumber">112</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">112</context>
|
<context context-type="linenumber">112</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</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@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/ngb-config.ts</context>
|
||||||
<context context-type="linenumber">13</context>
|
<context context-type="linenumber">13</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
@ -233,7 +233,7 @@
|
|||||||
<source><x id="INTERPOLATION" equiv-text="barConfig);
|
<source><x id="INTERPOLATION" equiv-text="barConfig);
|
||||||
pu"/></source>
|
pu"/></source>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.0_@angular+common@20.0.5_@angular+core@20.0.5_@angular+_d3ede862fd3f3ef56c2b56ed21f85d68/node_modules/src/progressbar/progressbar.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@19.0.1_@angular+common@20.0.6_@angular+core@20.0.6_@angular+_05316f125479eb303e49e3702b630d0f/node_modules/src/progressbar/progressbar.ts</context>
|
||||||
<context context-type="linenumber">41,42</context>
|
<context context-type="linenumber">41,42</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
@ -12,26 +12,26 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^20.0.4",
|
"@angular/cdk": "^20.0.4",
|
||||||
"@angular/common": "~20.0.5",
|
"@angular/common": "~20.0.6",
|
||||||
"@angular/compiler": "~20.0.5",
|
"@angular/compiler": "~20.0.6",
|
||||||
"@angular/core": "~20.0.5",
|
"@angular/core": "~20.0.6",
|
||||||
"@angular/forms": "~20.0.5",
|
"@angular/forms": "~20.0.6",
|
||||||
"@angular/localize": "~20.0.5",
|
"@angular/localize": "~20.0.6",
|
||||||
"@angular/platform-browser": "~20.0.5",
|
"@angular/platform-browser": "~20.0.6",
|
||||||
"@angular/platform-browser-dynamic": "~20.0.5",
|
"@angular/platform-browser-dynamic": "~20.0.6",
|
||||||
"@angular/router": "~20.0.5",
|
"@angular/router": "~20.0.6",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^19.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^19.0.1",
|
||||||
"@ng-select/ng-select": "^15.1.2",
|
"@ng-select/ng-select": "^15.1.3",
|
||||||
"@ngneat/dirty-check-forms": "^3.0.3",
|
"@ngneat/dirty-check-forms": "^3.0.3",
|
||||||
"@popperjs/core": "^2.11.8",
|
"@popperjs/core": "^2.11.8",
|
||||||
"bootstrap": "^5.3.6",
|
"bootstrap": "^5.3.7",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"mime-names": "^1.0.0",
|
"mime-names": "^1.0.0",
|
||||||
"ng2-pdf-viewer": "^10.4.0",
|
"ng2-pdf-viewer": "^10.4.0",
|
||||||
"ngx-bootstrap-icons": "^1.9.3",
|
"ngx-bootstrap-icons": "^1.9.3",
|
||||||
"ngx-color": "^10.0.0",
|
"ngx-color": "^10.0.0",
|
||||||
"ngx-cookie-service": "^19.1.2",
|
"ngx-cookie-service": "^20.0.1",
|
||||||
"ngx-device-detector": "^9.0.0",
|
"ngx-device-detector": "^10.0.2",
|
||||||
"ngx-ui-tour-ng-bootstrap": "^17.0.0",
|
"ngx-ui-tour-ng-bootstrap": "^17.0.0",
|
||||||
"rxjs": "^7.8.2",
|
"rxjs": "^7.8.2",
|
||||||
"tslib": "^2.8.1",
|
"tslib": "^2.8.1",
|
||||||
@ -51,15 +51,15 @@
|
|||||||
"@angular-eslint/template-parser": "20.1.1",
|
"@angular-eslint/template-parser": "20.1.1",
|
||||||
"@angular/build": "^20.0.4",
|
"@angular/build": "^20.0.4",
|
||||||
"@angular/cli": "~20.0.4",
|
"@angular/cli": "~20.0.4",
|
||||||
"@angular/compiler-cli": "~20.0.5",
|
"@angular/compiler-cli": "~20.0.6",
|
||||||
"@codecov/webpack-plugin": "^1.9.1",
|
"@codecov/webpack-plugin": "^1.9.1",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.53.2",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^22.15.29",
|
"@types/node": "^24.0.10",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.33.1",
|
"@typescript-eslint/eslint-plugin": "^8.35.1",
|
||||||
"@typescript-eslint/parser": "^8.33.1",
|
"@typescript-eslint/parser": "^8.35.1",
|
||||||
"@typescript-eslint/utils": "^8.33.1",
|
"@typescript-eslint/utils": "^8.35.1",
|
||||||
"eslint": "^9.28.0",
|
"eslint": "^9.30.1",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-junit": "^16.0.0",
|
"jest-junit": "^16.0.0",
|
||||||
@ -68,7 +68,7 @@
|
|||||||
"prettier-plugin-organize-imports": "^4.1.0",
|
"prettier-plugin-organize-imports": "^4.1.0",
|
||||||
"ts-node": "~10.9.1",
|
"ts-node": "~10.9.1",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.8.3",
|
||||||
"webpack": "^5.98.0"
|
"webpack": "^5.99.9"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
1606
src-ui/pnpm-lock.yaml
generated
1606
src-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -119,6 +119,26 @@ Object.defineProperty(window, 'location', {
|
|||||||
value: { reload: jest.fn() },
|
value: { reload: jest.fn() },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (typeof IntersectionObserver === 'undefined') {
|
||||||
|
class MockIntersectionObserver {
|
||||||
|
constructor(
|
||||||
|
public callback: IntersectionObserverCallback,
|
||||||
|
public options?: IntersectionObserverInit
|
||||||
|
) {}
|
||||||
|
|
||||||
|
observe = jest.fn()
|
||||||
|
unobserve = jest.fn()
|
||||||
|
disconnect = jest.fn()
|
||||||
|
takeRecords = jest.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'IntersectionObserver', {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: MockIntersectionObserver,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
HTMLCanvasElement.prototype.getContext = <
|
HTMLCanvasElement.prototype.getContext = <
|
||||||
typeof HTMLCanvasElement.prototype.getContext
|
typeof HTMLCanvasElement.prototype.getContext
|
||||||
>jest.fn()
|
>jest.fn()
|
||||||
|
@ -76,18 +76,18 @@
|
|||||||
<div class="modal-footer flex-column">
|
<div class="modal-footer flex-column">
|
||||||
<div class="d-flex w-100 justify-content-between align-items-center">
|
<div class="d-flex w-100 justify-content-between align-items-center">
|
||||||
<div class="btn-group" role="group">
|
<div class="btn-group" role="group">
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="EditMode.Create" id="editModeCreate" name="editmode">
|
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="PdfEditorEditMode.Create" id="editModeCreate" name="editmode">
|
||||||
<label for="editModeCreate" class="btn btn-outline-primary btn-sm">
|
<label for="editModeCreate" class="btn btn-outline-primary btn-sm">
|
||||||
<i-bs name="plus"></i-bs>
|
<i-bs name="plus"></i-bs>
|
||||||
<span class="form-check-label ms-1" i18n>Create new document(s)</span>
|
<span class="form-check-label ms-1" i18n>Create new document(s)</span>
|
||||||
</label>
|
</label>
|
||||||
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="EditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()">
|
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="PdfEditorEditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()">
|
||||||
<label for="editModeUpdate" class="btn btn-outline-primary btn-sm">
|
<label for="editModeUpdate" class="btn btn-outline-primary btn-sm">
|
||||||
<i-bs name="pencil"></i-bs>
|
<i-bs name="pencil"></i-bs>
|
||||||
<span class="form-check-label ms-2" i18n>Update existing document</span>
|
<span class="form-check-label ms-2" i18n>Update existing document</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@if (editMode === EditMode.Create) {
|
@if (editMode === PdfEditorEditMode.Create) {
|
||||||
<div class="form-check ms-3">
|
<div class="form-check ms-3">
|
||||||
<input class="form-check-input" type="checkbox" id="copyMeta" [(ngModel)]="includeMetadata">
|
<input class="form-check-input" type="checkbox" id="copyMeta" [(ngModel)]="includeMetadata">
|
||||||
<label class="form-check-label" for="copyMeta" i18n>Copy metadata</label>
|
<label class="form-check-label" for="copyMeta" i18n>Copy metadata</label>
|
||||||
|
@ -19,7 +19,7 @@ interface PageOperation {
|
|||||||
loaded?: boolean
|
loaded?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EditMode {
|
export enum PdfEditorEditMode {
|
||||||
Update = 'update',
|
Update = 'update',
|
||||||
Create = 'create',
|
Create = 'create',
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@ enum EditMode {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class PDFEditorComponent extends ConfirmDialogComponent {
|
export class PDFEditorComponent extends ConfirmDialogComponent {
|
||||||
public EditMode = EditMode
|
public PdfEditorEditMode = PdfEditorEditMode
|
||||||
|
|
||||||
private documentService = inject(DocumentService)
|
private documentService = inject(DocumentService)
|
||||||
activeModal: NgbActiveModal = inject(NgbActiveModal)
|
activeModal: NgbActiveModal = inject(NgbActiveModal)
|
||||||
@ -44,9 +44,8 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
|
|||||||
documentID: number
|
documentID: number
|
||||||
pages: PageOperation[] = []
|
pages: PageOperation[] = []
|
||||||
totalPages = 0
|
totalPages = 0
|
||||||
editMode: EditMode = EditMode.Create
|
editMode: PdfEditorEditMode = PdfEditorEditMode.Create
|
||||||
deleteOriginal: boolean = false
|
deleteOriginal: boolean = false
|
||||||
updateDocument: boolean = false
|
|
||||||
includeMetadata: boolean = true
|
includeMetadata: boolean = true
|
||||||
|
|
||||||
get pdfSrc(): string {
|
get pdfSrc(): string {
|
||||||
@ -88,7 +87,7 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
|
|||||||
this.pages[i].splitAfter = !this.pages[i].splitAfter
|
this.pages[i].splitAfter = !this.pages[i].splitAfter
|
||||||
if (this.pages[i].splitAfter) {
|
if (this.pages[i].splitAfter) {
|
||||||
// force create mode
|
// force create mode
|
||||||
this.editMode = EditMode.Create
|
this.editMode = PdfEditorEditMode.Create
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,12 +116,11 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getOperations() {
|
getOperations() {
|
||||||
const operations = this.pages.map((p, idx) => ({
|
return this.pages.map((p, idx) => ({
|
||||||
page: p.page,
|
page: p.page,
|
||||||
rotate: p.rotate,
|
rotate: p.rotate,
|
||||||
doc: this.computeDocIndex(idx),
|
doc: this.computeDocIndex(idx),
|
||||||
}))
|
}))
|
||||||
return operations
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeDocIndex(index: number): number {
|
private computeDocIndex(index: number): number {
|
||||||
|
@ -1142,7 +1142,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
).not.toBeUndefined()
|
).not.toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support pdf editor', () => {
|
it('should support pdf editor, handle error', () => {
|
||||||
let modal: NgbModalRef
|
let modal: NgbModalRef
|
||||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||||
initNormally()
|
initNormally()
|
||||||
@ -1151,7 +1151,7 @@ describe('DocumentDetailComponent', () => {
|
|||||||
modal.componentInstance.documentID = doc.id
|
modal.componentInstance.documentID = doc.id
|
||||||
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }]
|
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }]
|
||||||
modal.componentInstance.confirm()
|
modal.componentInstance.confirm()
|
||||||
const req = httpTestingController.expectOne(
|
let req = httpTestingController.expectOne(
|
||||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
)
|
)
|
||||||
expect(req.request.body).toEqual({
|
expect(req.request.body).toEqual({
|
||||||
@ -1159,11 +1159,23 @@ describe('DocumentDetailComponent', () => {
|
|||||||
method: 'edit_pdf',
|
method: 'edit_pdf',
|
||||||
parameters: {
|
parameters: {
|
||||||
operations: [{ page: 1, rotate: 0, doc: 0 }],
|
operations: [{ page: 1, rotate: 0, doc: 0 }],
|
||||||
|
delete_original: false,
|
||||||
update_document: false,
|
update_document: false,
|
||||||
include_metadata: true,
|
include_metadata: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
req.flush(true)
|
req.flush(true)
|
||||||
|
|
||||||
|
component.editPdf()
|
||||||
|
modal.componentInstance.documentID = doc.id
|
||||||
|
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: true }]
|
||||||
|
modal.componentInstance.confirm()
|
||||||
|
const errorSpy = jest.spyOn(toastService, 'showError')
|
||||||
|
req = httpTestingController.expectOne(
|
||||||
|
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||||
|
)
|
||||||
|
req.error(new ErrorEvent('failed'))
|
||||||
|
expect(errorSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should support keyboard shortcuts', () => {
|
it('should support keyboard shortcuts', () => {
|
||||||
|
@ -98,7 +98,10 @@ import { TagsComponent } from '../common/input/tags/tags.component'
|
|||||||
import { TextComponent } from '../common/input/text/text.component'
|
import { TextComponent } from '../common/input/text/text.component'
|
||||||
import { UrlComponent } from '../common/input/url/url.component'
|
import { UrlComponent } from '../common/input/url/url.component'
|
||||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||||
import { PDFEditorComponent } from '../common/pdf-editor/pdf-editor.component'
|
import {
|
||||||
|
PDFEditorComponent,
|
||||||
|
PdfEditorEditMode,
|
||||||
|
} from '../common/pdf-editor/pdf-editor.component'
|
||||||
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-links-dialog.component'
|
||||||
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
import { DocumentHistoryComponent } from '../document-history/document-history.component'
|
||||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||||
@ -1350,7 +1353,9 @@ export class DocumentDetailComponent
|
|||||||
this.documentsService
|
this.documentsService
|
||||||
.bulkEdit([this.document.id], 'edit_pdf', {
|
.bulkEdit([this.document.id], 'edit_pdf', {
|
||||||
operations: modal.componentInstance.getOperations(),
|
operations: modal.componentInstance.getOperations(),
|
||||||
update_document: modal.componentInstance.updateDocument,
|
delete_original: modal.componentInstance.deleteOriginal,
|
||||||
|
update_document:
|
||||||
|
modal.componentInstance.editMode == PdfEditorEditMode.Update,
|
||||||
include_metadata: modal.componentInstance.includeMetadata,
|
include_metadata: modal.componentInstance.includeMetadata,
|
||||||
})
|
})
|
||||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||||
|
@ -1369,7 +1369,8 @@ class BulkEditSerializer(
|
|||||||
return bulk_edit.delete_pages
|
return bulk_edit.delete_pages
|
||||||
elif method == "edit_pdf":
|
elif method == "edit_pdf":
|
||||||
return bulk_edit.edit_pdf
|
return bulk_edit.edit_pdf
|
||||||
else:
|
else: # pragma: no cover
|
||||||
|
# This will never happen as it is handled by the ChoiceField
|
||||||
raise serializers.ValidationError("Unsupported method.")
|
raise serializers.ValidationError("Unsupported method.")
|
||||||
|
|
||||||
def _validate_parameters_tags(self, parameters):
|
def _validate_parameters_tags(self, parameters):
|
||||||
|
@ -394,11 +394,9 @@ def check_scheduled_workflows():
|
|||||||
Check and run all enabled scheduled workflows.
|
Check and run all enabled scheduled workflows.
|
||||||
|
|
||||||
Scheduled triggers are evaluated based on a target date field (e.g. added, created, modified, or a custom date field),
|
Scheduled triggers are evaluated based on a target date field (e.g. added, created, modified, or a custom date field),
|
||||||
combined with a day offset.
|
combined with a day offset:
|
||||||
|
- Positive offsets mean the workflow should trigger AFTER the specified date (e.g., offset = +7 → trigger 7 days after)
|
||||||
The offset is mathematically negated resulting in the following behavior:
|
- Negative offsets mean the workflow should trigger BEFORE the specified date (e.g., offset = -7 → trigger 7 days before)
|
||||||
- Positive offsets mean the workflow should trigger BEFORE the specified date (e.g., offset = +7 → trigger 7 days before)
|
|
||||||
- Negative offsets mean the workflow should trigger AFTER the specified date (e.g., offset = -7 → trigger 7 days after)
|
|
||||||
|
|
||||||
Once a document satisfies this condition, and recurring/non-recurring constraints are met, the workflow is run.
|
Once a document satisfies this condition, and recurring/non-recurring constraints are met, the workflow is run.
|
||||||
"""
|
"""
|
||||||
|
@ -1393,6 +1393,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
self.assertEqual(kwargs["user"], self.user)
|
self.assertEqual(kwargs["user"], self.user)
|
||||||
|
|
||||||
def test_edit_pdf_invalid_params(self):
|
def test_edit_pdf_invalid_params(self):
|
||||||
|
# multiple documents
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/bulk_edit/",
|
"/api/documents/bulk_edit/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
@ -1404,10 +1405,10 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn(b"Edit PDF method only supports one document", response.content)
|
self.assertIn(b"Edit PDF method only supports one document", response.content)
|
||||||
|
|
||||||
|
# no operations specified
|
||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
"/api/documents/bulk_edit/",
|
"/api/documents/bulk_edit/",
|
||||||
json.dumps(
|
json.dumps(
|
||||||
@ -1419,10 +1420,141 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
|
|||||||
),
|
),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
self.assertIn(b"operations not specified", response.content)
|
self.assertIn(b"operations not specified", response.content)
|
||||||
|
|
||||||
|
# operations not a list
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": "not_a_list"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"operations must be a list", response.content)
|
||||||
|
|
||||||
|
# invalid operation
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": ["invalid_operation"]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"invalid operation entry", response.content)
|
||||||
|
|
||||||
|
# page not an int
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": "not_an_int"}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"page must be an integer", response.content)
|
||||||
|
|
||||||
|
# rotate not an int
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 1, "rotate": "not_an_int"}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"rotate must be an integer", response.content)
|
||||||
|
|
||||||
|
# doc not an int
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {"operations": [{"page": 1, "doc": "not_an_int"}]},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"doc must be an integer", response.content)
|
||||||
|
|
||||||
|
# update_document not a boolean
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {
|
||||||
|
"update_document": "not_a_bool",
|
||||||
|
"operations": [{"page": 1}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"update_document must be a boolean", response.content)
|
||||||
|
|
||||||
|
# include_metadata not a boolean
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {
|
||||||
|
"include_metadata": "not_a_bool",
|
||||||
|
"operations": [{"page": 1}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(b"include_metadata must be a boolean", response.content)
|
||||||
|
|
||||||
|
# update_document True but output would be multiple documents
|
||||||
|
response = self.client.post(
|
||||||
|
"/api/documents/bulk_edit/",
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"documents": [self.doc2.id],
|
||||||
|
"method": "edit_pdf",
|
||||||
|
"parameters": {
|
||||||
|
"update_document": True,
|
||||||
|
"operations": [{"page": 1, "doc": 1}, {"page": 2, "doc": 2}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||||
|
self.assertIn(
|
||||||
|
b"update_document only allowed with a single output document",
|
||||||
|
response.content,
|
||||||
|
)
|
||||||
|
|
||||||
@override_settings(AUDIT_LOG_ENABLED=True)
|
@override_settings(AUDIT_LOG_ENABLED=True)
|
||||||
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
def test_bulk_edit_audit_log_enabled_simple_field(self):
|
||||||
"""
|
"""
|
||||||
|
@ -909,3 +909,156 @@ class TestPDFActions(DirectoriesMixin, TestCase):
|
|||||||
expected_str = "Error deleting pages from document"
|
expected_str = "Error deleting pages from document"
|
||||||
self.assertIn(expected_str, error_str)
|
self.assertIn(expected_str, error_str)
|
||||||
mock_update_archive_file.assert_not_called()
|
mock_update_archive_file.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_basic_operations(self, mock_consume_file, mock_group):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with two operations to split the doc and rotate pages
|
||||||
|
THEN:
|
||||||
|
- A grouped task is generated and delay() is called
|
||||||
|
"""
|
||||||
|
mock_group.return_value.delay.return_value = None
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1, "doc": 0}, {"page": 2, "doc": 1, "rotate": 90}]
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations)
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
mock_group.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_with_user_override(self, mock_consume_file, mock_group):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with user override
|
||||||
|
THEN:
|
||||||
|
- Task is created with user context
|
||||||
|
"""
|
||||||
|
mock_group.return_value.delay.return_value = None
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1, "doc": 0}, {"page": 2, "doc": 1}]
|
||||||
|
user = User.objects.create(username="editor")
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations, user=user)
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
mock_group.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.chord")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_with_delete_original(self, mock_consume_file, mock_chord):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with delete_original=True
|
||||||
|
THEN:
|
||||||
|
- Task group is triggered
|
||||||
|
"""
|
||||||
|
mock_chord.return_value.delay.return_value = None
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1}, {"page": 2}]
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations, delete_original=True)
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
mock_chord.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.tasks.update_document_content_maybe_archive_file.delay")
|
||||||
|
def test_edit_pdf_with_update_document(self, mock_update_document):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- A single existing PDF document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with update_document=True and a single output
|
||||||
|
THEN:
|
||||||
|
- The original document is updated in-place
|
||||||
|
- The update_document_content_maybe_archive_file task is triggered
|
||||||
|
"""
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1}, {"page": 2}]
|
||||||
|
original_checksum = self.doc2.checksum
|
||||||
|
original_page_count = self.doc2.page_count
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(
|
||||||
|
doc_ids,
|
||||||
|
operations=operations,
|
||||||
|
update_document=True,
|
||||||
|
delete_original=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
self.doc2.refresh_from_db()
|
||||||
|
self.assertNotEqual(self.doc2.checksum, original_checksum)
|
||||||
|
self.assertNotEqual(self.doc2.page_count, original_page_count)
|
||||||
|
mock_update_document.assert_called_once_with(document_id=self.doc2.id)
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_without_metadata(self, mock_consume_file, mock_group):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with include_metadata=False
|
||||||
|
THEN:
|
||||||
|
- Tasks are created with empty metadata
|
||||||
|
"""
|
||||||
|
mock_group.return_value.delay.return_value = None
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [{"page": 1}]
|
||||||
|
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations, include_metadata=False)
|
||||||
|
self.assertEqual(result, "OK")
|
||||||
|
mock_group.return_value.delay.assert_called_once()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_open_failure(self, mock_consume_file, mock_group):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf fails to open PDF
|
||||||
|
THEN:
|
||||||
|
- Task group is not called
|
||||||
|
"""
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [
|
||||||
|
{"page": 9999}, # invalid page, forces error during PDF load
|
||||||
|
]
|
||||||
|
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations)
|
||||||
|
self.assertEqual(result, "ERROR")
|
||||||
|
mock_group.assert_not_called()
|
||||||
|
mock_consume_file.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("documents.bulk_edit.group")
|
||||||
|
@mock.patch("documents.tasks.consume_file.s")
|
||||||
|
def test_edit_pdf_multiple_outputs_with_update_flag_errors(
|
||||||
|
self,
|
||||||
|
mock_consume_file,
|
||||||
|
mock_group,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing document
|
||||||
|
WHEN:
|
||||||
|
- edit_pdf is called with multiple outputs and update_document=True
|
||||||
|
THEN:
|
||||||
|
- An error is logged and task group is not called
|
||||||
|
"""
|
||||||
|
doc_ids = [self.doc2.id]
|
||||||
|
operations = [
|
||||||
|
{"page": 1, "doc": 0},
|
||||||
|
{"page": 2, "doc": 1},
|
||||||
|
]
|
||||||
|
with self.assertLogs("paperless.bulk_edit", level="ERROR"):
|
||||||
|
result = bulk_edit.edit_pdf(doc_ids, operations, update_document=True)
|
||||||
|
self.assertEqual(result, "ERROR")
|
||||||
|
mock_group.assert_not_called()
|
||||||
|
mock_consume_file.assert_not_called()
|
||||||
|
@ -918,11 +918,10 @@ CELERY_BEAT_SCHEDULE_FILENAME = str(DATA_DIR / "celerybeat-schedule.db")
|
|||||||
|
|
||||||
# Cachalot: Database read cache.
|
# Cachalot: Database read cache.
|
||||||
def _parse_cachalot_settings():
|
def _parse_cachalot_settings():
|
||||||
global INSTALLED_APPS
|
|
||||||
ttl = __get_int("PAPERLESS_READ_CACHE_TTL", 3600)
|
ttl = __get_int("PAPERLESS_READ_CACHE_TTL", 3600)
|
||||||
ttl = min(ttl, 31536000) if ttl > 0 else 3600
|
ttl = min(ttl, 31536000) if ttl > 0 else 3600
|
||||||
_, redis_url = _parse_redis_url(
|
_, redis_url = _parse_redis_url(
|
||||||
os.getenv("PAPERLESS_READ_CACHE_REDIS_URL", None),
|
os.getenv("PAPERLESS_READ_CACHE_REDIS_URL", _CHANNELS_REDIS_URL),
|
||||||
)
|
)
|
||||||
result = {
|
result = {
|
||||||
"CACHALOT_CACHE": "read-cache",
|
"CACHALOT_CACHE": "read-cache",
|
||||||
@ -936,18 +935,18 @@ def _parse_cachalot_settings():
|
|||||||
"CACHALOT_REDIS_URL": redis_url,
|
"CACHALOT_REDIS_URL": redis_url,
|
||||||
"CACHALOT_TIMEOUT": ttl,
|
"CACHALOT_TIMEOUT": ttl,
|
||||||
}
|
}
|
||||||
if result["CACHALOT_ENABLED"]:
|
|
||||||
INSTALLED_APPS.append("cachalot")
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
_cachalot_settings = _parse_cachalot_settings()
|
cachalot_settings = _parse_cachalot_settings()
|
||||||
CACHALOT_ENABLED = _cachalot_settings["CACHALOT_ENABLED"]
|
CACHALOT_ENABLED = cachalot_settings["CACHALOT_ENABLED"]
|
||||||
CACHALOT_CACHE = _cachalot_settings["CACHALOT_CACHE"]
|
if CACHALOT_ENABLED: # pragma: no cover
|
||||||
CACHALOT_TIMEOUT = _cachalot_settings["CACHALOT_TIMEOUT"]
|
INSTALLED_APPS.append("cachalot")
|
||||||
CACHALOT_QUERY_KEYGEN = _cachalot_settings["CACHALOT_QUERY_KEYGEN"]
|
CACHALOT_CACHE = cachalot_settings["CACHALOT_CACHE"]
|
||||||
CACHALOT_TABLE_KEYGEN = _cachalot_settings["CACHALOT_TABLE_KEYGEN"]
|
CACHALOT_TIMEOUT = cachalot_settings["CACHALOT_TIMEOUT"]
|
||||||
CACHALOT_FINAL_SQL_CHECK = _cachalot_settings["CACHALOT_FINAL_SQL_CHECK"]
|
CACHALOT_QUERY_KEYGEN = cachalot_settings["CACHALOT_QUERY_KEYGEN"]
|
||||||
|
CACHALOT_TABLE_KEYGEN = cachalot_settings["CACHALOT_TABLE_KEYGEN"]
|
||||||
|
CACHALOT_FINAL_SQL_CHECK = cachalot_settings["CACHALOT_FINAL_SQL_CHECK"]
|
||||||
|
|
||||||
|
|
||||||
# Django default & Cachalot cache configuration
|
# Django default & Cachalot cache configuration
|
||||||
@ -968,7 +967,7 @@ def _parse_caches():
|
|||||||
},
|
},
|
||||||
"read-cache": {
|
"read-cache": {
|
||||||
"BACKEND": _CACHE_BACKEND,
|
"BACKEND": _CACHE_BACKEND,
|
||||||
"LOCATION": _parse_cachalot_settings()["CACHALOT_REDIS_URL"],
|
"LOCATION": cachalot_settings["CACHALOT_REDIS_URL"],
|
||||||
"KEY_PREFIX": _REDIS_KEY_PREFIX,
|
"KEY_PREFIX": _REDIS_KEY_PREFIX,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -977,9 +976,6 @@ def _parse_caches():
|
|||||||
CACHES = _parse_caches()
|
CACHES = _parse_caches()
|
||||||
|
|
||||||
|
|
||||||
del _cachalot_settings
|
|
||||||
|
|
||||||
|
|
||||||
def default_threads_per_worker(task_workers) -> int:
|
def default_threads_per_worker(task_workers) -> int:
|
||||||
# always leave one core open
|
# always leave one core open
|
||||||
available_cores = max(multiprocessing.cpu_count(), 1)
|
available_cores = max(multiprocessing.cpu_count(), 1)
|
||||||
|
@ -63,26 +63,20 @@ class TestDbCacheSettings:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
def test_cachalot_custom_settings(self):
|
def test_cachalot_custom_settings(self):
|
||||||
cachalot_settings = _parse_cachalot_settings()
|
settings = _parse_cachalot_settings()
|
||||||
assert "cachalot" in settings.INSTALLED_APPS
|
|
||||||
caches = _parse_caches()
|
|
||||||
|
|
||||||
# Modifiable settings
|
assert settings["CACHALOT_ENABLED"]
|
||||||
assert cachalot_settings["CACHALOT_ENABLED"]
|
assert settings["CACHALOT_TIMEOUT"] == 7200
|
||||||
assert cachalot_settings["CACHALOT_TIMEOUT"] == 7200
|
assert settings["CACHALOT_CACHE"] == "read-cache"
|
||||||
assert caches["read-cache"]["LOCATION"] == "redis://localhost:6380/7"
|
|
||||||
|
|
||||||
# Fixed settings
|
|
||||||
assert cachalot_settings["CACHALOT_CACHE"] == "read-cache"
|
|
||||||
assert (
|
assert (
|
||||||
cachalot_settings["CACHALOT_QUERY_KEYGEN"]
|
settings["CACHALOT_QUERY_KEYGEN"]
|
||||||
== "paperless.db_cache.custom_get_query_cache_key"
|
== "paperless.db_cache.custom_get_query_cache_key"
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
cachalot_settings["CACHALOT_TABLE_KEYGEN"]
|
settings["CACHALOT_TABLE_KEYGEN"]
|
||||||
== "paperless.db_cache.custom_get_table_cache_key"
|
== "paperless.db_cache.custom_get_table_cache_key"
|
||||||
)
|
)
|
||||||
assert cachalot_settings["CACHALOT_FINAL_SQL_CHECK"] is True
|
assert settings["CACHALOT_FINAL_SQL_CHECK"] is True
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("env_var_ttl", "expected_cachalot_timeout"),
|
("env_var_ttl", "expected_cachalot_timeout"),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user