mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-20 17:44:56 -05:00
Compare commits
9 Commits
419ee9d6e7
...
422bffe1a6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
422bffe1a6 | ||
![]() |
31351c5f5c | ||
![]() |
c30cf2e0cd | ||
![]() |
e97cfb9b5e | ||
![]() |
42100588d5 | ||
![]() |
bc2facc87f | ||
![]() |
4e082f997c | ||
![]() |
1512599f4f | ||
![]() |
6c8f0b54ad |
@ -78,7 +78,7 @@ optional-dependencies.postgres = [
|
|||||||
"psycopg-c==3.2.5",
|
"psycopg-c==3.2.5",
|
||||||
]
|
]
|
||||||
optional-dependencies.webserver = [
|
optional-dependencies.webserver = [
|
||||||
"granian[uvloop]~=2.2.0",
|
"granian[uvloop]~=2.3.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
@ -221,15 +221,6 @@ lint.per-file-ignores."src/documents/parsers.py" = [
|
|||||||
lint.per-file-ignores."src/documents/signals/handlers.py" = [
|
lint.per-file-ignores."src/documents/signals/handlers.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
lint.per-file-ignores."src/documents/tests/test_consumer.py" = [
|
|
||||||
"PTH",
|
|
||||||
] # TODO Enable & remove
|
|
||||||
lint.per-file-ignores."src/documents/tests/test_file_handling.py" = [
|
|
||||||
"PTH",
|
|
||||||
] # TODO Enable & remove
|
|
||||||
lint.per-file-ignores."src/documents/tests/test_migration_archive_files.py" = [
|
|
||||||
"PTH",
|
|
||||||
] # TODO Enable & remove
|
|
||||||
lint.per-file-ignores."src/documents/views.py" = [
|
lint.per-file-ignores."src/documents/views.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
@ -239,9 +230,6 @@ lint.per-file-ignores."src/paperless/checks.py" = [
|
|||||||
lint.per-file-ignores."src/paperless/settings.py" = [
|
lint.per-file-ignores."src/paperless/settings.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
lint.per-file-ignores."src/paperless/views.py" = [
|
|
||||||
"PTH",
|
|
||||||
] # TODO Enable & remove
|
|
||||||
lint.per-file-ignores."src/paperless_mail/mail.py" = [
|
lint.per-file-ignores."src/paperless_mail/mail.py" = [
|
||||||
"PTH",
|
"PTH",
|
||||||
] # TODO Enable & remove
|
] # TODO Enable & remove
|
||||||
|
@ -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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/alert/alert.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/node_modules/src/alert/alert.ts</context>
|
||||||
<context context-type="linenumber">51</context>
|
<context context-type="linenumber">51</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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">132,136</context>
|
<context context-type="linenumber">132,136</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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">158,160</context>
|
<context context-type="linenumber">158,160</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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/carousel/carousel.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/node_modules/src/carousel/carousel.ts</context>
|
||||||
<context context-type="linenumber">199</context>
|
<context context-type="linenumber">199</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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">77,79</context>
|
<context context-type="linenumber">77,79</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">102</context>
|
<context context-type="linenumber">102</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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">102</context>
|
<context context-type="linenumber">102</context>
|
||||||
</context-group>
|
</context-group>
|
||||||
<context-group purpose="location">
|
<context-group purpose="location">
|
||||||
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/node_modules/src/datepicker/datepicker-navigation.ts</context>
|
||||||
<context context-type="linenumber">102</context>
|
<context context-type="linenumber">102</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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/ngb-config.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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@18.0.0_@angular+common@19.2.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/node_modules/src/progressbar/progressbar.ts</context>
|
<context context-type="sourcefile">node_modules/.pnpm/@ng-bootstrap+ng-bootstrap@18.0.0_@angular+common@19.2.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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,16 +12,16 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^19.2.14",
|
"@angular/cdk": "^19.2.14",
|
||||||
"@angular/common": "~19.2.9",
|
"@angular/common": "~19.2.13",
|
||||||
"@angular/compiler": "~19.2.9",
|
"@angular/compiler": "~19.2.14",
|
||||||
"@angular/core": "~19.2.9",
|
"@angular/core": "~19.2.14",
|
||||||
"@angular/forms": "~19.2.9",
|
"@angular/forms": "~19.2.14",
|
||||||
"@angular/localize": "~19.2.9",
|
"@angular/localize": "~19.2.14",
|
||||||
"@angular/platform-browser": "~19.2.9",
|
"@angular/platform-browser": "~19.2.14",
|
||||||
"@angular/platform-browser-dynamic": "~19.2.9",
|
"@angular/platform-browser-dynamic": "~19.2.14",
|
||||||
"@angular/router": "~19.2.9",
|
"@angular/router": "~19.2.14",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
|
||||||
"@ng-select/ng-select": "^14.7.0",
|
"@ng-select/ng-select": "^14.9.0",
|
||||||
"@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.6",
|
||||||
@ -42,16 +42,16 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "^19.0.1",
|
"@angular-builders/custom-webpack": "^19.0.1",
|
||||||
"@angular-builders/jest": "^19.0.1",
|
"@angular-builders/jest": "^19.0.1",
|
||||||
"@angular-devkit/build-angular": "^19.2.10",
|
"@angular-devkit/build-angular": "^19.2.14",
|
||||||
"@angular-devkit/core": "^19.2.10",
|
"@angular-devkit/core": "^19.2.14",
|
||||||
"@angular-devkit/schematics": "^19.2.10",
|
"@angular-devkit/schematics": "^19.2.14",
|
||||||
"@angular-eslint/builder": "19.3.0",
|
"@angular-eslint/builder": "19.7.0",
|
||||||
"@angular-eslint/eslint-plugin": "19.3.0",
|
"@angular-eslint/eslint-plugin": "19.7.0",
|
||||||
"@angular-eslint/eslint-plugin-template": "19.3.0",
|
"@angular-eslint/eslint-plugin-template": "19.7.0",
|
||||||
"@angular-eslint/schematics": "19.3.0",
|
"@angular-eslint/schematics": "19.7.0",
|
||||||
"@angular-eslint/template-parser": "19.3.0",
|
"@angular-eslint/template-parser": "19.7.0",
|
||||||
"@angular/cli": "~19.2.10",
|
"@angular/cli": "~19.2.14",
|
||||||
"@angular/compiler-cli": "~19.2.9",
|
"@angular/compiler-cli": "~19.2.14",
|
||||||
"@codecov/webpack-plugin": "^1.9.1",
|
"@codecov/webpack-plugin": "^1.9.1",
|
||||||
"@playwright/test": "^1.51.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
|
1783
src-ui/pnpm-lock.yaml
generated
1783
src-ui/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -98,15 +98,7 @@ class ConsumerStatusShortMessage(str, Enum):
|
|||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
class ConsumerPlugin(
|
class ConsumerPluginMixin:
|
||||||
AlwaysRunPluginMixin,
|
|
||||||
NoSetupPluginMixin,
|
|
||||||
NoCleanupPluginMixin,
|
|
||||||
LoggingMixin,
|
|
||||||
ConsumeTaskPlugin,
|
|
||||||
):
|
|
||||||
logging_name = "paperless.consumer"
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
input_doc: ConsumableDocument,
|
input_doc: ConsumableDocument,
|
||||||
@ -155,88 +147,16 @@ class ConsumerPlugin(
|
|||||||
self.log.error(log_message or message, exc_info=exc_info)
|
self.log.error(log_message or message, exc_info=exc_info)
|
||||||
raise ConsumerError(f"{self.filename}: {log_message or message}") from exception
|
raise ConsumerError(f"{self.filename}: {log_message or message}") from exception
|
||||||
|
|
||||||
def pre_check_file_exists(self):
|
|
||||||
"""
|
|
||||||
Confirm the input file still exists where it should
|
|
||||||
"""
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert isinstance(self.input_doc.original_file, Path), (
|
|
||||||
self.input_doc.original_file
|
|
||||||
)
|
|
||||||
if not self.input_doc.original_file.is_file():
|
|
||||||
self._fail(
|
|
||||||
ConsumerStatusShortMessage.FILE_NOT_FOUND,
|
|
||||||
f"Cannot consume {self.input_doc.original_file}: File not found.",
|
|
||||||
)
|
|
||||||
|
|
||||||
def pre_check_duplicate(self):
|
class ConsumerPlugin(
|
||||||
"""
|
AlwaysRunPluginMixin,
|
||||||
Using the MD5 of the file, check this exact file doesn't already exist
|
NoSetupPluginMixin,
|
||||||
"""
|
NoCleanupPluginMixin,
|
||||||
with Path(self.input_doc.original_file).open("rb") as f:
|
LoggingMixin,
|
||||||
checksum = hashlib.md5(f.read()).hexdigest()
|
ConsumerPluginMixin,
|
||||||
existing_doc = Document.global_objects.filter(
|
ConsumeTaskPlugin,
|
||||||
Q(checksum=checksum) | Q(archive_checksum=checksum),
|
|
||||||
)
|
|
||||||
if existing_doc.exists():
|
|
||||||
msg = ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS
|
|
||||||
log_msg = f"Not consuming {self.filename}: It is a duplicate of {existing_doc.get().title} (#{existing_doc.get().pk})."
|
|
||||||
|
|
||||||
if existing_doc.first().deleted_at is not None:
|
|
||||||
msg = ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS_IN_TRASH
|
|
||||||
log_msg += " Note: existing document is in the trash."
|
|
||||||
|
|
||||||
if settings.CONSUMER_DELETE_DUPLICATES:
|
|
||||||
Path(self.input_doc.original_file).unlink()
|
|
||||||
self._fail(
|
|
||||||
msg,
|
|
||||||
log_msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
def pre_check_directories(self):
|
|
||||||
"""
|
|
||||||
Ensure all required directories exist before attempting to use them
|
|
||||||
"""
|
|
||||||
settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
settings.THUMBNAIL_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
settings.ORIGINALS_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
settings.ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
def pre_check_asn_value(self):
|
|
||||||
"""
|
|
||||||
Check that if override_asn is given, it is unique and within a valid range
|
|
||||||
"""
|
|
||||||
if self.metadata.asn is None:
|
|
||||||
# check not necessary in case no ASN gets set
|
|
||||||
return
|
|
||||||
# Validate the range is above zero and less than uint32_t max
|
|
||||||
# otherwise, Whoosh can't handle it in the index
|
|
||||||
if (
|
|
||||||
self.metadata.asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
|
||||||
or self.metadata.asn > Document.ARCHIVE_SERIAL_NUMBER_MAX
|
|
||||||
):
|
):
|
||||||
self._fail(
|
logging_name = "paperless.consumer"
|
||||||
ConsumerStatusShortMessage.ASN_RANGE,
|
|
||||||
f"Not consuming {self.filename}: "
|
|
||||||
f"Given ASN {self.metadata.asn} is out of range "
|
|
||||||
f"[{Document.ARCHIVE_SERIAL_NUMBER_MIN:,}, "
|
|
||||||
f"{Document.ARCHIVE_SERIAL_NUMBER_MAX:,}]",
|
|
||||||
)
|
|
||||||
existing_asn_doc = Document.global_objects.filter(
|
|
||||||
archive_serial_number=self.metadata.asn,
|
|
||||||
)
|
|
||||||
if existing_asn_doc.exists():
|
|
||||||
msg = ConsumerStatusShortMessage.ASN_ALREADY_EXISTS
|
|
||||||
log_msg = f"Not consuming {self.filename}: Given ASN {self.metadata.asn} already exists!"
|
|
||||||
|
|
||||||
if existing_asn_doc.first().deleted_at is not None:
|
|
||||||
msg = ConsumerStatusShortMessage.ASN_ALREADY_EXISTS_IN_TRASH
|
|
||||||
log_msg += " Note: existing document is in the trash."
|
|
||||||
|
|
||||||
self._fail(
|
|
||||||
msg,
|
|
||||||
log_msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
def run_pre_consume_script(self):
|
def run_pre_consume_script(self):
|
||||||
"""
|
"""
|
||||||
@ -366,20 +286,7 @@ class ConsumerPlugin(
|
|||||||
tempdir = None
|
tempdir = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._send_progress(
|
# Preflight has already run including progress update to 0%
|
||||||
0,
|
|
||||||
100,
|
|
||||||
ProgressStatusOptions.STARTED,
|
|
||||||
ConsumerStatusShortMessage.NEW_FILE,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Make sure that preconditions for consuming the file are met.
|
|
||||||
|
|
||||||
self.pre_check_file_exists()
|
|
||||||
self.pre_check_directories()
|
|
||||||
self.pre_check_duplicate()
|
|
||||||
self.pre_check_asn_value()
|
|
||||||
|
|
||||||
self.log.info(f"Consuming {self.filename}")
|
self.log.info(f"Consuming {self.filename}")
|
||||||
|
|
||||||
# For the actual work, copy the file into a tempdir
|
# For the actual work, copy the file into a tempdir
|
||||||
@ -837,3 +744,113 @@ class ConsumerPlugin(
|
|||||||
copy_basic_file_stats(source, target)
|
copy_basic_file_stats(source, target)
|
||||||
except Exception: # pragma: no cover
|
except Exception: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConsumerPreflightPlugin(
|
||||||
|
NoCleanupPluginMixin,
|
||||||
|
NoSetupPluginMixin,
|
||||||
|
AlwaysRunPluginMixin,
|
||||||
|
LoggingMixin,
|
||||||
|
ConsumerPluginMixin,
|
||||||
|
ConsumeTaskPlugin,
|
||||||
|
):
|
||||||
|
NAME: str = "ConsumerPreflightPlugin"
|
||||||
|
logging_name = "paperless.consumer"
|
||||||
|
|
||||||
|
def pre_check_file_exists(self):
|
||||||
|
"""
|
||||||
|
Confirm the input file still exists where it should
|
||||||
|
"""
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(self.input_doc.original_file, Path), (
|
||||||
|
self.input_doc.original_file
|
||||||
|
)
|
||||||
|
if not self.input_doc.original_file.is_file():
|
||||||
|
self._fail(
|
||||||
|
ConsumerStatusShortMessage.FILE_NOT_FOUND,
|
||||||
|
f"Cannot consume {self.input_doc.original_file}: File not found.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def pre_check_duplicate(self):
|
||||||
|
"""
|
||||||
|
Using the MD5 of the file, check this exact file doesn't already exist
|
||||||
|
"""
|
||||||
|
with Path(self.input_doc.original_file).open("rb") as f:
|
||||||
|
checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
|
existing_doc = Document.global_objects.filter(
|
||||||
|
Q(checksum=checksum) | Q(archive_checksum=checksum),
|
||||||
|
)
|
||||||
|
if existing_doc.exists():
|
||||||
|
msg = ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS
|
||||||
|
log_msg = f"Not consuming {self.filename}: It is a duplicate of {existing_doc.get().title} (#{existing_doc.get().pk})."
|
||||||
|
|
||||||
|
if existing_doc.first().deleted_at is not None:
|
||||||
|
msg = ConsumerStatusShortMessage.DOCUMENT_ALREADY_EXISTS_IN_TRASH
|
||||||
|
log_msg += " Note: existing document is in the trash."
|
||||||
|
|
||||||
|
if settings.CONSUMER_DELETE_DUPLICATES:
|
||||||
|
Path(self.input_doc.original_file).unlink()
|
||||||
|
self._fail(
|
||||||
|
msg,
|
||||||
|
log_msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
def pre_check_directories(self):
|
||||||
|
"""
|
||||||
|
Ensure all required directories exist before attempting to use them
|
||||||
|
"""
|
||||||
|
settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
settings.THUMBNAIL_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
settings.ORIGINALS_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
settings.ARCHIVE_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def pre_check_asn_value(self):
|
||||||
|
"""
|
||||||
|
Check that if override_asn is given, it is unique and within a valid range
|
||||||
|
"""
|
||||||
|
if self.metadata.asn is None:
|
||||||
|
# check not necessary in case no ASN gets set
|
||||||
|
return
|
||||||
|
# Validate the range is above zero and less than uint32_t max
|
||||||
|
# otherwise, Whoosh can't handle it in the index
|
||||||
|
if (
|
||||||
|
self.metadata.asn < Document.ARCHIVE_SERIAL_NUMBER_MIN
|
||||||
|
or self.metadata.asn > Document.ARCHIVE_SERIAL_NUMBER_MAX
|
||||||
|
):
|
||||||
|
self._fail(
|
||||||
|
ConsumerStatusShortMessage.ASN_RANGE,
|
||||||
|
f"Not consuming {self.filename}: "
|
||||||
|
f"Given ASN {self.metadata.asn} is out of range "
|
||||||
|
f"[{Document.ARCHIVE_SERIAL_NUMBER_MIN:,}, "
|
||||||
|
f"{Document.ARCHIVE_SERIAL_NUMBER_MAX:,}]",
|
||||||
|
)
|
||||||
|
existing_asn_doc = Document.global_objects.filter(
|
||||||
|
archive_serial_number=self.metadata.asn,
|
||||||
|
)
|
||||||
|
if existing_asn_doc.exists():
|
||||||
|
msg = ConsumerStatusShortMessage.ASN_ALREADY_EXISTS
|
||||||
|
log_msg = f"Not consuming {self.filename}: Given ASN {self.metadata.asn} already exists!"
|
||||||
|
|
||||||
|
if existing_asn_doc.first().deleted_at is not None:
|
||||||
|
msg = ConsumerStatusShortMessage.ASN_ALREADY_EXISTS_IN_TRASH
|
||||||
|
log_msg += " Note: existing document is in the trash."
|
||||||
|
|
||||||
|
self._fail(
|
||||||
|
msg,
|
||||||
|
log_msg,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
self._send_progress(
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
ProgressStatusOptions.STARTED,
|
||||||
|
ConsumerStatusShortMessage.NEW_FILE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make sure that preconditions for consuming the file are met.
|
||||||
|
|
||||||
|
self.pre_check_file_exists()
|
||||||
|
self.pre_check_duplicate()
|
||||||
|
self.pre_check_directories()
|
||||||
|
self.pre_check_asn_value()
|
||||||
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
|
from fnmatch import translate as fnmatch_translate
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
@ -18,6 +19,8 @@ from documents.models import WorkflowTrigger
|
|||||||
from documents.permissions import get_objects_for_user_owner_aware
|
from documents.permissions import get_objects_for_user_owner_aware
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
from documents.classifier import DocumentClassifier
|
from documents.classifier import DocumentClassifier
|
||||||
|
|
||||||
logger = logging.getLogger("paperless.matching")
|
logger = logging.getLogger("paperless.matching")
|
||||||
@ -389,6 +392,40 @@ def existing_document_matches_workflow(
|
|||||||
return (trigger_matched, reason)
|
return (trigger_matched, reason)
|
||||||
|
|
||||||
|
|
||||||
|
def prefilter_documents_by_workflowtrigger(
|
||||||
|
documents: QuerySet[Document],
|
||||||
|
trigger: WorkflowTrigger,
|
||||||
|
) -> QuerySet[Document]:
|
||||||
|
"""
|
||||||
|
To prevent scheduled workflows checking every document, we prefilter the
|
||||||
|
documents by the workflow trigger filters. This is done before e.g.
|
||||||
|
document_matches_workflow in run_workflows
|
||||||
|
"""
|
||||||
|
|
||||||
|
if trigger.filter_has_tags.all().count() > 0:
|
||||||
|
documents = documents.filter(
|
||||||
|
tags__in=trigger.filter_has_tags.all(),
|
||||||
|
).distinct()
|
||||||
|
|
||||||
|
if trigger.filter_has_correspondent is not None:
|
||||||
|
documents = documents.filter(
|
||||||
|
correspondent=trigger.filter_has_correspondent,
|
||||||
|
)
|
||||||
|
|
||||||
|
if trigger.filter_has_document_type is not None:
|
||||||
|
documents = documents.filter(
|
||||||
|
document_type=trigger.filter_has_document_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if trigger.filter_filename is not None and len(trigger.filter_filename) > 0:
|
||||||
|
# the true fnmatch will actually run later so we just want a loose filter here
|
||||||
|
regex = fnmatch_translate(trigger.filter_filename).lstrip("^").rstrip("$")
|
||||||
|
regex = f"(?i){regex}"
|
||||||
|
documents = documents.filter(original_filename__regex=regex)
|
||||||
|
|
||||||
|
return documents
|
||||||
|
|
||||||
|
|
||||||
def document_matches_workflow(
|
def document_matches_workflow(
|
||||||
document: ConsumableDocument | Document,
|
document: ConsumableDocument | Document,
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
|
@ -26,12 +26,14 @@ from documents.caching import clear_document_caches
|
|||||||
from documents.classifier import DocumentClassifier
|
from documents.classifier import DocumentClassifier
|
||||||
from documents.classifier import load_classifier
|
from documents.classifier import load_classifier
|
||||||
from documents.consumer import ConsumerPlugin
|
from documents.consumer import ConsumerPlugin
|
||||||
|
from documents.consumer import ConsumerPreflightPlugin
|
||||||
from documents.consumer import WorkflowTriggerPlugin
|
from documents.consumer import WorkflowTriggerPlugin
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.double_sided import CollatePlugin
|
from documents.double_sided import CollatePlugin
|
||||||
from documents.file_handling import create_source_path_directory
|
from documents.file_handling import create_source_path_directory
|
||||||
from documents.file_handling import generate_unique_filename
|
from documents.file_handling import generate_unique_filename
|
||||||
|
from documents.matching import prefilter_documents_by_workflowtrigger
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
from documents.models import Document
|
from documents.models import Document
|
||||||
@ -144,6 +146,7 @@ def consume_file(
|
|||||||
overrides = DocumentMetadataOverrides()
|
overrides = DocumentMetadataOverrides()
|
||||||
|
|
||||||
plugins: list[type[ConsumeTaskPlugin]] = [
|
plugins: list[type[ConsumeTaskPlugin]] = [
|
||||||
|
ConsumerPreflightPlugin,
|
||||||
CollatePlugin,
|
CollatePlugin,
|
||||||
BarcodePlugin,
|
BarcodePlugin,
|
||||||
WorkflowTriggerPlugin,
|
WorkflowTriggerPlugin,
|
||||||
@ -471,6 +474,12 @@ def check_scheduled_workflows():
|
|||||||
|
|
||||||
documents = Document.objects.filter(id__in=matched_ids)
|
documents = Document.objects.filter(id__in=matched_ids)
|
||||||
|
|
||||||
|
if documents.count() > 0:
|
||||||
|
documents = prefilter_documents_by_workflowtrigger(
|
||||||
|
documents,
|
||||||
|
trigger,
|
||||||
|
)
|
||||||
|
|
||||||
if documents.count() > 0:
|
if documents.count() > 0:
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Found {documents.count()} documents for trigger {trigger}",
|
f"Found {documents.count()} documents for trigger {trigger}",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import tempfile
|
import tempfile
|
||||||
@ -66,7 +65,7 @@ class CopyParser(_BaseTestParser):
|
|||||||
|
|
||||||
def parse(self, document_path, mime_type, file_name=None):
|
def parse(self, document_path, mime_type, file_name=None):
|
||||||
self.text = "The text"
|
self.text = "The text"
|
||||||
self.archive_path = os.path.join(self.tempdir, "archive.pdf")
|
self.archive_path = Path(self.tempdir / "archive.pdf")
|
||||||
shutil.copy(document_path, self.archive_path)
|
shutil.copy(document_path, self.archive_path)
|
||||||
|
|
||||||
|
|
||||||
@ -96,15 +95,16 @@ class FaultyGenericExceptionParser(_BaseTestParser):
|
|||||||
|
|
||||||
def fake_magic_from_file(file, *, mime=False):
|
def fake_magic_from_file(file, *, mime=False):
|
||||||
if mime:
|
if mime:
|
||||||
if file.name.startswith("invalid_pdf"):
|
filepath = Path(file)
|
||||||
|
if filepath.name.startswith("invalid_pdf"):
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
if os.path.splitext(file)[1] == ".pdf":
|
if filepath.suffix == ".pdf":
|
||||||
return "application/pdf"
|
return "application/pdf"
|
||||||
elif os.path.splitext(file)[1] == ".png":
|
elif filepath.suffix == ".png":
|
||||||
return "image/png"
|
return "image/png"
|
||||||
elif os.path.splitext(file)[1] == ".webp":
|
elif filepath.suffix == ".webp":
|
||||||
return "image/webp"
|
return "image/webp"
|
||||||
elif os.path.splitext(file)[1] == ".eml":
|
elif filepath.suffix == ".eml":
|
||||||
return "message/rfc822"
|
return "message/rfc822"
|
||||||
else:
|
else:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
@ -225,7 +225,7 @@ class TestConsumer(
|
|||||||
self.assertEqual(document.content, "The Text")
|
self.assertEqual(document.content, "The Text")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
document.title,
|
document.title,
|
||||||
os.path.splitext(os.path.basename(filename))[0],
|
Path(filename).stem,
|
||||||
)
|
)
|
||||||
self.assertIsNone(document.correspondent)
|
self.assertIsNone(document.correspondent)
|
||||||
self.assertIsNone(document.document_type)
|
self.assertIsNone(document.document_type)
|
||||||
@ -254,7 +254,7 @@ class TestConsumer(
|
|||||||
# https://github.com/jonaswinkler/paperless-ng/discussions/1037
|
# https://github.com/jonaswinkler/paperless-ng/discussions/1037
|
||||||
|
|
||||||
filename = self.get_test_file()
|
filename = self.get_test_file()
|
||||||
shadow_file = os.path.join(self.dirs.scratch_dir, "._sample.pdf")
|
shadow_file = Path(self.dirs.scratch_dir / "._sample.pdf")
|
||||||
|
|
||||||
shutil.copy(filename, shadow_file)
|
shutil.copy(filename, shadow_file)
|
||||||
|
|
||||||
@ -484,8 +484,8 @@ class TestConsumer(
|
|||||||
self._assert_first_last_send_progress()
|
self._assert_first_last_send_progress()
|
||||||
|
|
||||||
def testNotAFile(self):
|
def testNotAFile(self):
|
||||||
with self.get_consumer(Path("non-existing-file")) as consumer:
|
|
||||||
with self.assertRaisesMessage(ConsumerError, "File not found"):
|
with self.assertRaisesMessage(ConsumerError, "File not found"):
|
||||||
|
with self.get_consumer(Path("non-existing-file")) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
|
|
||||||
@ -493,8 +493,8 @@ class TestConsumer(
|
|||||||
with self.get_consumer(self.get_test_file()) as consumer:
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
with self.get_consumer(self.get_test_file()) as consumer:
|
|
||||||
with self.assertRaisesMessage(ConsumerError, "It is a duplicate"):
|
with self.assertRaisesMessage(ConsumerError, "It is a duplicate"):
|
||||||
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
@ -503,8 +503,8 @@ class TestConsumer(
|
|||||||
with self.get_consumer(self.get_test_file()) as consumer:
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
with self.get_consumer(self.get_test_archive_file()) as consumer:
|
|
||||||
with self.assertRaisesMessage(ConsumerError, "It is a duplicate"):
|
with self.assertRaisesMessage(ConsumerError, "It is a duplicate"):
|
||||||
|
with self.get_consumer(self.get_test_archive_file()) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
@ -521,8 +521,8 @@ class TestConsumer(
|
|||||||
|
|
||||||
Document.objects.all().delete()
|
Document.objects.all().delete()
|
||||||
|
|
||||||
with self.get_consumer(self.get_test_file()) as consumer:
|
|
||||||
with self.assertRaisesMessage(ConsumerError, "document is in the trash"):
|
with self.assertRaisesMessage(ConsumerError, "document is in the trash"):
|
||||||
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
def testAsnExists(self):
|
def testAsnExists(self):
|
||||||
@ -532,11 +532,11 @@ class TestConsumer(
|
|||||||
) as consumer:
|
) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(ConsumerError, "ASN 123 already exists"):
|
||||||
with self.get_consumer(
|
with self.get_consumer(
|
||||||
self.get_test_file2(),
|
self.get_test_file2(),
|
||||||
DocumentMetadataOverrides(asn=123),
|
DocumentMetadataOverrides(asn=123),
|
||||||
) as consumer:
|
) as consumer:
|
||||||
with self.assertRaisesMessage(ConsumerError, "ASN 123 already exists"):
|
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
def testAsnExistsInTrash(self):
|
def testAsnExistsInTrash(self):
|
||||||
@ -549,22 +549,22 @@ class TestConsumer(
|
|||||||
document = Document.objects.first()
|
document = Document.objects.first()
|
||||||
document.delete()
|
document.delete()
|
||||||
|
|
||||||
|
with self.assertRaisesMessage(ConsumerError, "document is in the trash"):
|
||||||
with self.get_consumer(
|
with self.get_consumer(
|
||||||
self.get_test_file2(),
|
self.get_test_file2(),
|
||||||
DocumentMetadataOverrides(asn=123),
|
DocumentMetadataOverrides(asn=123),
|
||||||
) as consumer:
|
) as consumer:
|
||||||
with self.assertRaisesMessage(ConsumerError, "document is in the trash"):
|
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
@mock.patch("documents.parsers.document_consumer_declaration.send")
|
@mock.patch("documents.parsers.document_consumer_declaration.send")
|
||||||
def testNoParsers(self, m):
|
def testNoParsers(self, m):
|
||||||
m.return_value = []
|
m.return_value = []
|
||||||
|
|
||||||
with self.get_consumer(self.get_test_file()) as consumer:
|
|
||||||
with self.assertRaisesMessage(
|
with self.assertRaisesMessage(
|
||||||
ConsumerError,
|
ConsumerError,
|
||||||
"sample.pdf: Unsupported mime type application/pdf",
|
"sample.pdf: Unsupported mime type application/pdf",
|
||||||
):
|
):
|
||||||
|
with self.get_consumer(self.get_test_file()) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
self._assert_first_last_send_progress(last_status="FAILED")
|
self._assert_first_last_send_progress(last_status="FAILED")
|
||||||
@ -726,8 +726,8 @@ class TestConsumer(
|
|||||||
dst = self.get_test_file()
|
dst = self.get_test_file()
|
||||||
self.assertIsFile(dst)
|
self.assertIsFile(dst)
|
||||||
|
|
||||||
with self.get_consumer(dst) as consumer:
|
|
||||||
with self.assertRaises(ConsumerError):
|
with self.assertRaises(ConsumerError):
|
||||||
|
with self.get_consumer(dst) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
self.assertIsNotFile(dst)
|
self.assertIsNotFile(dst)
|
||||||
@ -751,11 +751,11 @@ class TestConsumer(
|
|||||||
dst = self.get_test_file()
|
dst = self.get_test_file()
|
||||||
self.assertIsFile(dst)
|
self.assertIsFile(dst)
|
||||||
|
|
||||||
with self.get_consumer(dst) as consumer:
|
|
||||||
with self.assertRaisesRegex(
|
with self.assertRaisesRegex(
|
||||||
ConsumerError,
|
ConsumerError,
|
||||||
r"sample\.pdf: Not consuming sample\.pdf: It is a duplicate of sample \(#\d+\)",
|
r"sample\.pdf: Not consuming sample\.pdf: It is a duplicate of sample \(#\d+\)",
|
||||||
):
|
):
|
||||||
|
with self.get_consumer(dst) as consumer:
|
||||||
consumer.run()
|
consumer.run()
|
||||||
|
|
||||||
self.assertIsFile(dst)
|
self.assertIsFile(dst)
|
||||||
@ -1082,8 +1082,8 @@ class PreConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
|
|||||||
outfile.write("echo This message goes to stderr >&2")
|
outfile.write("echo This message goes to stderr >&2")
|
||||||
|
|
||||||
# Make the file executable
|
# Make the file executable
|
||||||
st = os.stat(script.name)
|
st = Path(script.name).stat()
|
||||||
os.chmod(script.name, st.st_mode | stat.S_IEXEC)
|
Path(script.name).chmod(st.st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
||||||
with self.assertLogs("paperless.consumer", level="INFO") as cm:
|
with self.assertLogs("paperless.consumer", level="INFO") as cm:
|
||||||
@ -1114,8 +1114,8 @@ class PreConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
|
|||||||
outfile.write("exit 100\n")
|
outfile.write("exit 100\n")
|
||||||
|
|
||||||
# Make the file executable
|
# Make the file executable
|
||||||
st = os.stat(script.name)
|
st = Path(script.name).stat()
|
||||||
os.chmod(script.name, st.st_mode | stat.S_IEXEC)
|
Path(script.name).chmod(st.st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
with override_settings(PRE_CONSUME_SCRIPT=script.name):
|
||||||
with self.get_consumer(self.test_file) as c:
|
with self.get_consumer(self.test_file) as c:
|
||||||
@ -1237,8 +1237,8 @@ class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
|
|||||||
outfile.write("exit -500\n")
|
outfile.write("exit -500\n")
|
||||||
|
|
||||||
# Make the file executable
|
# Make the file executable
|
||||||
st = os.stat(script.name)
|
st = Path(script.name).stat()
|
||||||
os.chmod(script.name, st.st_mode | stat.S_IEXEC)
|
Path(script.name).chmod(st.st_mode | stat.S_IEXEC)
|
||||||
|
|
||||||
with override_settings(POST_CONSUME_SCRIPT=script.name):
|
with override_settings(POST_CONSUME_SCRIPT=script.name):
|
||||||
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
doc = Document.objects.create(title="Test", mime_type="application/pdf")
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
@ -71,7 +70,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
# test that creating dirs for the source_path creates the correct directory
|
# test that creating dirs for the source_path creates the correct directory
|
||||||
create_source_path_directory(document.source_path)
|
create_source_path_directory(document.source_path)
|
||||||
Path(document.source_path).touch()
|
Path(document.source_path).touch()
|
||||||
self.assertIsDir(os.path.join(settings.ORIGINALS_DIR, "none"))
|
self.assertIsDir(settings.ORIGINALS_DIR / "none")
|
||||||
|
|
||||||
# Set a correspondent and save the document
|
# Set a correspondent and save the document
|
||||||
document.correspondent = Correspondent.objects.get_or_create(name="test")[0]
|
document.correspondent = Correspondent.objects.get_or_create(name="test")[0]
|
||||||
@ -108,7 +107,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Make the folder read- and execute-only (no writing and no renaming)
|
# Make the folder read- and execute-only (no writing and no renaming)
|
||||||
os.chmod(os.path.join(settings.ORIGINALS_DIR, "none"), 0o555)
|
(settings.ORIGINALS_DIR / "none").chmod(0o555)
|
||||||
|
|
||||||
# Set a correspondent and save the document
|
# Set a correspondent and save the document
|
||||||
document.correspondent = Correspondent.objects.get_or_create(name="test")[0]
|
document.correspondent = Correspondent.objects.get_or_create(name="test")[0]
|
||||||
@ -120,7 +119,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(document.filename, "none/none.pdf")
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
|
|
||||||
os.chmod(os.path.join(settings.ORIGINALS_DIR, "none"), 0o777)
|
(settings.ORIGINALS_DIR / "none").chmod(0o777)
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}")
|
||||||
def test_file_renaming_database_error(self):
|
def test_file_renaming_database_error(self):
|
||||||
@ -160,7 +159,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
# Check proper handling of files
|
# Check proper handling of files
|
||||||
self.assertIsFile(document.source_path)
|
self.assertIsFile(document.source_path)
|
||||||
self.assertIsFile(
|
self.assertIsFile(
|
||||||
os.path.join(settings.ORIGINALS_DIR, "none/none.pdf"),
|
settings.ORIGINALS_DIR / "none" / "none.pdf",
|
||||||
)
|
)
|
||||||
self.assertEqual(document.filename, "none/none.pdf")
|
self.assertEqual(document.filename, "none/none.pdf")
|
||||||
|
|
||||||
@ -183,9 +182,9 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.delete()
|
document.delete()
|
||||||
empty_trash([document.pk])
|
empty_trash([document.pk])
|
||||||
self.assertIsNotFile(
|
self.assertIsNotFile(
|
||||||
os.path.join(settings.ORIGINALS_DIR, "none", "none.pdf"),
|
settings.ORIGINALS_DIR / "none" / "none.pdf",
|
||||||
)
|
)
|
||||||
self.assertIsNotDir(os.path.join(settings.ORIGINALS_DIR, "none"))
|
self.assertIsNotDir(settings.ORIGINALS_DIR / "none")
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
FILENAME_FORMAT="{correspondent}/{correspondent}",
|
FILENAME_FORMAT="{correspondent}/{correspondent}",
|
||||||
@ -206,15 +205,15 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
Path(document.source_path).touch()
|
Path(document.source_path).touch()
|
||||||
|
|
||||||
# Ensure file was moved to trash after delete
|
# Ensure file was moved to trash after delete
|
||||||
self.assertIsNotFile(os.path.join(settings.EMPTY_TRASH_DIR, "none", "none.pdf"))
|
self.assertIsNotFile(Path(settings.EMPTY_TRASH_DIR) / "none" / "none.pdf")
|
||||||
document.delete()
|
document.delete()
|
||||||
empty_trash([document.pk])
|
empty_trash([document.pk])
|
||||||
self.assertIsNotFile(
|
self.assertIsNotFile(
|
||||||
os.path.join(settings.ORIGINALS_DIR, "none", "none.pdf"),
|
settings.ORIGINALS_DIR / "none" / "none.pdf",
|
||||||
)
|
)
|
||||||
self.assertIsNotDir(os.path.join(settings.ORIGINALS_DIR, "none"))
|
self.assertIsNotDir(settings.ORIGINALS_DIR / "none")
|
||||||
self.assertIsFile(os.path.join(settings.EMPTY_TRASH_DIR, "none.pdf"))
|
self.assertIsFile(Path(settings.EMPTY_TRASH_DIR) / "none.pdf")
|
||||||
self.assertIsNotFile(os.path.join(settings.EMPTY_TRASH_DIR, "none_01.pdf"))
|
self.assertIsNotFile(Path(settings.EMPTY_TRASH_DIR) / "none_01.pdf")
|
||||||
|
|
||||||
# Create an identical document and ensure it is trashed under a new name
|
# Create an identical document and ensure it is trashed under a new name
|
||||||
document = Document()
|
document = Document()
|
||||||
@ -227,7 +226,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
Path(document.source_path).touch()
|
Path(document.source_path).touch()
|
||||||
document.delete()
|
document.delete()
|
||||||
empty_trash([document.pk])
|
empty_trash([document.pk])
|
||||||
self.assertIsFile(os.path.join(settings.EMPTY_TRASH_DIR, "none_01.pdf"))
|
self.assertIsFile(Path(settings.EMPTY_TRASH_DIR) / "none_01.pdf")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{correspondent}")
|
||||||
def test_document_delete_nofile(self):
|
def test_document_delete_nofile(self):
|
||||||
@ -261,8 +260,8 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
document.save()
|
document.save()
|
||||||
|
|
||||||
# Check proper handling of files
|
# Check proper handling of files
|
||||||
self.assertIsDir(os.path.join(settings.ORIGINALS_DIR, "test"))
|
self.assertIsDir(settings.ORIGINALS_DIR / "test")
|
||||||
self.assertIsDir(os.path.join(settings.ORIGINALS_DIR, "none"))
|
self.assertIsDir(settings.ORIGINALS_DIR / "none")
|
||||||
self.assertIsFile(important_file)
|
self.assertIsFile(important_file)
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{document_type} - {title}")
|
@override_settings(FILENAME_FORMAT="{document_type} - {title}")
|
||||||
@ -371,16 +370,16 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
Path(document.source_path).touch()
|
Path(document.source_path).touch()
|
||||||
|
|
||||||
# Check proper handling of files
|
# Check proper handling of files
|
||||||
self.assertIsDir(os.path.join(settings.ORIGINALS_DIR, "none/none"))
|
self.assertIsDir(settings.ORIGINALS_DIR / "none" / "none")
|
||||||
|
|
||||||
document.delete()
|
document.delete()
|
||||||
empty_trash([document.pk])
|
empty_trash([document.pk])
|
||||||
|
|
||||||
self.assertIsNotFile(
|
self.assertIsNotFile(
|
||||||
os.path.join(settings.ORIGINALS_DIR, "none/none/none.pdf"),
|
settings.ORIGINALS_DIR / "none" / "none" / "none.pdf",
|
||||||
)
|
)
|
||||||
self.assertIsNotDir(os.path.join(settings.ORIGINALS_DIR, "none/none"))
|
self.assertIsNotDir(settings.ORIGINALS_DIR / "none" / "none")
|
||||||
self.assertIsNotDir(os.path.join(settings.ORIGINALS_DIR, "none"))
|
self.assertIsNotDir(settings.ORIGINALS_DIR / "none")
|
||||||
self.assertIsDir(settings.ORIGINALS_DIR)
|
self.assertIsDir(settings.ORIGINALS_DIR)
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{doc_pk}")
|
@override_settings(FILENAME_FORMAT="{doc_pk}")
|
||||||
@ -415,12 +414,12 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
(tmp / "notempty" / "empty").mkdir(exist_ok=True, parents=True)
|
(tmp / "notempty" / "empty").mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
delete_empty_directories(
|
delete_empty_directories(
|
||||||
os.path.join(tmp, "notempty", "empty"),
|
tmp / "notempty" / "empty",
|
||||||
root=settings.ORIGINALS_DIR,
|
root=settings.ORIGINALS_DIR,
|
||||||
)
|
)
|
||||||
self.assertIsDir(os.path.join(tmp, "notempty"))
|
self.assertIsDir(tmp / "notempty")
|
||||||
self.assertIsFile(os.path.join(tmp, "notempty", "file"))
|
self.assertIsFile(tmp / "notempty" / "file")
|
||||||
self.assertIsNotDir(os.path.join(tmp, "notempty", "empty"))
|
self.assertIsNotDir(tmp / "notempty" / "empty")
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{% if x is None %}/{title]")
|
@override_settings(FILENAME_FORMAT="{% if x is None %}/{title]")
|
||||||
def test_invalid_format(self):
|
def test_invalid_format(self):
|
||||||
@ -585,8 +584,8 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
|||||||
class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
|
||||||
@override_settings(FILENAME_FORMAT=None)
|
@override_settings(FILENAME_FORMAT=None)
|
||||||
def test_create_no_format(self):
|
def test_create_no_format(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@ -604,8 +603,8 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
||||||
def test_create_with_format(self):
|
def test_create_with_format(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@ -632,8 +631,8 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
||||||
def test_move_archive_gone(self):
|
def test_move_archive_gone(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
mime_type="application/pdf",
|
mime_type="application/pdf",
|
||||||
@ -651,9 +650,9 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
||||||
def test_move_archive_exists(self):
|
def test_move_archive_exists(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
existing_archive_file = os.path.join(settings.ARCHIVE_DIR, "none", "my_doc.pdf")
|
existing_archive_file = settings.ARCHIVE_DIR / "none" / "my_doc.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
(settings.ARCHIVE_DIR / "none").mkdir(parents=True, exist_ok=True)
|
(settings.ARCHIVE_DIR / "none").mkdir(parents=True, exist_ok=True)
|
||||||
@ -676,8 +675,8 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{title}")
|
@override_settings(FILENAME_FORMAT="{title}")
|
||||||
def test_move_original_only(self):
|
def test_move_original_only(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "document_01.pdf")
|
original = settings.ORIGINALS_DIR / "document_01.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "document.pdf")
|
archive = settings.ARCHIVE_DIR / "document.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
|
|
||||||
@ -698,8 +697,8 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{title}")
|
@override_settings(FILENAME_FORMAT="{title}")
|
||||||
def test_move_archive_only(self):
|
def test_move_archive_only(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "document.pdf")
|
original = settings.ORIGINALS_DIR / "document.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "document_01.pdf")
|
archive = settings.ARCHIVE_DIR / "document_01.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
|
|
||||||
@ -725,13 +724,13 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
if "archive" in str(src):
|
if "archive" in str(src):
|
||||||
raise OSError
|
raise OSError
|
||||||
else:
|
else:
|
||||||
os.remove(src)
|
Path(src).unlink()
|
||||||
Path(dst).touch()
|
Path(dst).touch()
|
||||||
|
|
||||||
m.side_effect = fake_rename
|
m.side_effect = fake_rename
|
||||||
|
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@ -751,8 +750,8 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
||||||
def test_move_file_gone(self):
|
def test_move_file_gone(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
# Path(original).touch()
|
# Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@ -776,13 +775,13 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
if "original" in str(src):
|
if "original" in str(src):
|
||||||
raise OSError
|
raise OSError
|
||||||
else:
|
else:
|
||||||
os.remove(src)
|
Path(src).unlink()
|
||||||
Path(dst).touch()
|
Path(dst).touch()
|
||||||
|
|
||||||
m.side_effect = fake_rename
|
m.side_effect = fake_rename
|
||||||
|
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@ -802,8 +801,8 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="")
|
@override_settings(FILENAME_FORMAT="")
|
||||||
def test_archive_deleted(self):
|
def test_archive_deleted(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
doc = Document.objects.create(
|
doc = Document.objects.create(
|
||||||
@ -830,9 +829,9 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{title}")
|
@override_settings(FILENAME_FORMAT="{title}")
|
||||||
def test_archive_deleted2(self):
|
def test_archive_deleted2(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "document.webp")
|
original = settings.ORIGINALS_DIR / "document.webp"
|
||||||
original2 = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original2 = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(original2).touch()
|
Path(original2).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
@ -865,8 +864,8 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
|
|||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
@override_settings(FILENAME_FORMAT="{correspondent}/{title}")
|
||||||
def test_database_error(self):
|
def test_database_error(self):
|
||||||
original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
|
original = settings.ORIGINALS_DIR / "0000001.pdf"
|
||||||
archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
|
archive = settings.ARCHIVE_DIR / "0000001.pdf"
|
||||||
Path(original).touch()
|
Path(original).touch()
|
||||||
Path(archive).touch()
|
Path(archive).touch()
|
||||||
doc = Document(
|
doc = Document(
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
@ -21,7 +20,7 @@ migration_1012_obj = importlib.import_module(
|
|||||||
|
|
||||||
|
|
||||||
def archive_name_from_filename(filename):
|
def archive_name_from_filename(filename):
|
||||||
return os.path.splitext(filename)[0] + ".pdf"
|
return Path(filename).stem + ".pdf"
|
||||||
|
|
||||||
|
|
||||||
def archive_path_old(self):
|
def archive_path_old(self):
|
||||||
@ -30,12 +29,12 @@ def archive_path_old(self):
|
|||||||
else:
|
else:
|
||||||
fname = f"{self.pk:07}.pdf"
|
fname = f"{self.pk:07}.pdf"
|
||||||
|
|
||||||
return os.path.join(settings.ARCHIVE_DIR, fname)
|
return Path(settings.ARCHIVE_DIR) / fname
|
||||||
|
|
||||||
|
|
||||||
def archive_path_new(doc):
|
def archive_path_new(doc):
|
||||||
if doc.archive_filename is not None:
|
if doc.archive_filename is not None:
|
||||||
return os.path.join(settings.ARCHIVE_DIR, str(doc.archive_filename))
|
return Path(settings.ARCHIVE_DIR) / str(doc.archive_filename)
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -48,7 +47,7 @@ def source_path(doc):
|
|||||||
if doc.storage_type == STORAGE_TYPE_GPG:
|
if doc.storage_type == STORAGE_TYPE_GPG:
|
||||||
fname += ".gpg" # pragma: no cover
|
fname += ".gpg" # pragma: no cover
|
||||||
|
|
||||||
return os.path.join(settings.ORIGINALS_DIR, fname)
|
return Path(settings.ORIGINALS_DIR) / fname
|
||||||
|
|
||||||
|
|
||||||
def thumbnail_path(doc):
|
def thumbnail_path(doc):
|
||||||
@ -56,7 +55,7 @@ def thumbnail_path(doc):
|
|||||||
if doc.storage_type == STORAGE_TYPE_GPG:
|
if doc.storage_type == STORAGE_TYPE_GPG:
|
||||||
file_name += ".gpg"
|
file_name += ".gpg"
|
||||||
|
|
||||||
return os.path.join(settings.THUMBNAIL_DIR, file_name)
|
return Path(settings.THUMBNAIL_DIR) / file_name
|
||||||
|
|
||||||
|
|
||||||
def make_test_document(
|
def make_test_document(
|
||||||
@ -76,7 +75,7 @@ def make_test_document(
|
|||||||
doc.save()
|
doc.save()
|
||||||
|
|
||||||
shutil.copy2(original, source_path(doc))
|
shutil.copy2(original, source_path(doc))
|
||||||
with open(original, "rb") as f:
|
with Path(original).open("rb") as f:
|
||||||
doc.checksum = hashlib.md5(f.read()).hexdigest()
|
doc.checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
|
|
||||||
if archive:
|
if archive:
|
||||||
@ -86,7 +85,7 @@ def make_test_document(
|
|||||||
else:
|
else:
|
||||||
shutil.copy2(archive, archive_path_old(doc))
|
shutil.copy2(archive, archive_path_old(doc))
|
||||||
|
|
||||||
with open(archive, "rb") as f:
|
with Path(archive).open("rb") as f:
|
||||||
doc.archive_checksum = hashlib.md5(f.read()).hexdigest()
|
doc.archive_checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
|
|
||||||
doc.save()
|
doc.save()
|
||||||
@ -96,25 +95,17 @@ def make_test_document(
|
|||||||
return doc
|
return doc
|
||||||
|
|
||||||
|
|
||||||
simple_jpg = os.path.join(os.path.dirname(__file__), "samples", "simple.jpg")
|
simple_jpg = Path(__file__).parent / "samples" / "simple.jpg"
|
||||||
simple_pdf = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
|
simple_pdf = Path(__file__).parent / "samples" / "simple.pdf"
|
||||||
simple_pdf2 = os.path.join(
|
simple_pdf2 = (
|
||||||
os.path.dirname(__file__),
|
Path(__file__).parent / "samples" / "documents" / "originals" / "0000002.pdf"
|
||||||
"samples",
|
|
||||||
"documents",
|
|
||||||
"originals",
|
|
||||||
"0000002.pdf",
|
|
||||||
)
|
)
|
||||||
simple_pdf3 = os.path.join(
|
simple_pdf3 = (
|
||||||
os.path.dirname(__file__),
|
Path(__file__).parent / "samples" / "documents" / "originals" / "0000003.pdf"
|
||||||
"samples",
|
|
||||||
"documents",
|
|
||||||
"originals",
|
|
||||||
"0000003.pdf",
|
|
||||||
)
|
)
|
||||||
simple_txt = os.path.join(os.path.dirname(__file__), "samples", "simple.txt")
|
simple_txt = Path(__file__).parent / "samples" / "simple.txt"
|
||||||
simple_png = os.path.join(os.path.dirname(__file__), "samples", "simple-noalpha.png")
|
simple_png = Path(__file__).parent / "samples" / "simple-noalpha.png"
|
||||||
simple_png2 = os.path.join(os.path.dirname(__file__), "examples", "no-text.png")
|
simple_png2 = Path(__file__).parent / "examples" / "no-text.png"
|
||||||
|
|
||||||
|
|
||||||
@override_settings(FILENAME_FORMAT="")
|
@override_settings(FILENAME_FORMAT="")
|
||||||
@ -198,13 +189,13 @@ class TestMigrateArchiveFiles(DirectoriesMixin, FileSystemAssertsMixin, TestMigr
|
|||||||
else:
|
else:
|
||||||
self.assertIsNone(doc.archive_filename)
|
self.assertIsNone(doc.archive_filename)
|
||||||
|
|
||||||
with open(source_path(doc), "rb") as f:
|
with Path(source_path(doc)).open("rb") as f:
|
||||||
original_checksum = hashlib.md5(f.read()).hexdigest()
|
original_checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
self.assertEqual(original_checksum, doc.checksum)
|
self.assertEqual(original_checksum, doc.checksum)
|
||||||
|
|
||||||
if doc.archive_checksum:
|
if doc.archive_checksum:
|
||||||
self.assertIsFile(archive_path_new(doc))
|
self.assertIsFile(archive_path_new(doc))
|
||||||
with open(archive_path_new(doc), "rb") as f:
|
with archive_path_new(doc).open("rb") as f:
|
||||||
archive_checksum = hashlib.md5(f.read()).hexdigest()
|
archive_checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
self.assertEqual(archive_checksum, doc.archive_checksum)
|
self.assertEqual(archive_checksum, doc.archive_checksum)
|
||||||
|
|
||||||
@ -301,7 +292,7 @@ class TestMigrateArchiveFilesErrors(DirectoriesMixin, TestMigrations):
|
|||||||
"clash.pdf",
|
"clash.pdf",
|
||||||
simple_pdf,
|
simple_pdf,
|
||||||
)
|
)
|
||||||
os.unlink(archive_path_old(doc))
|
archive_path_old(doc).unlink()
|
||||||
|
|
||||||
self.assertRaisesMessage(
|
self.assertRaisesMessage(
|
||||||
ValueError,
|
ValueError,
|
||||||
@ -494,13 +485,13 @@ class TestMigrateArchiveFilesBackwards(
|
|||||||
for doc in Document.objects.all():
|
for doc in Document.objects.all():
|
||||||
if doc.archive_checksum:
|
if doc.archive_checksum:
|
||||||
self.assertIsFile(archive_path_old(doc))
|
self.assertIsFile(archive_path_old(doc))
|
||||||
with open(source_path(doc), "rb") as f:
|
with Path(source_path(doc)).open("rb") as f:
|
||||||
original_checksum = hashlib.md5(f.read()).hexdigest()
|
original_checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
self.assertEqual(original_checksum, doc.checksum)
|
self.assertEqual(original_checksum, doc.checksum)
|
||||||
|
|
||||||
if doc.archive_checksum:
|
if doc.archive_checksum:
|
||||||
self.assertIsFile(archive_path_old(doc))
|
self.assertIsFile(archive_path_old(doc))
|
||||||
with open(archive_path_old(doc), "rb") as f:
|
with archive_path_old(doc).open("rb") as f:
|
||||||
archive_checksum = hashlib.md5(f.read()).hexdigest()
|
archive_checksum = hashlib.md5(f.read()).hexdigest()
|
||||||
self.assertEqual(archive_checksum, doc.archive_checksum)
|
self.assertEqual(archive_checksum, doc.archive_checksum)
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ from documents import tasks
|
|||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
from documents.matching import document_matches_workflow
|
from documents.matching import document_matches_workflow
|
||||||
|
from documents.matching import prefilter_documents_by_workflowtrigger
|
||||||
from documents.models import Correspondent
|
from documents.models import Correspondent
|
||||||
from documents.models import CustomField
|
from documents.models import CustomField
|
||||||
from documents.models import CustomFieldInstance
|
from documents.models import CustomFieldInstance
|
||||||
@ -1711,6 +1712,55 @@ class TestWorkflows(
|
|||||||
doc2.refresh_from_db()
|
doc2.refresh_from_db()
|
||||||
self.assertIsNone(doc2.owner) # has not triggered yet
|
self.assertIsNone(doc2.owner) # has not triggered yet
|
||||||
|
|
||||||
|
def test_workflow_scheduled_filters_queryset(self):
|
||||||
|
"""
|
||||||
|
GIVEN:
|
||||||
|
- Existing workflow with scheduled trigger
|
||||||
|
WHEN:
|
||||||
|
- Workflows run and matching documents are found
|
||||||
|
THEN:
|
||||||
|
- prefilter_documents_by_workflowtrigger appropriately filters
|
||||||
|
"""
|
||||||
|
trigger = WorkflowTrigger.objects.create(
|
||||||
|
type=WorkflowTrigger.WorkflowTriggerType.SCHEDULED,
|
||||||
|
schedule_offset_days=-7,
|
||||||
|
schedule_date_field=WorkflowTrigger.ScheduleDateField.CREATED,
|
||||||
|
filter_filename="*sample*",
|
||||||
|
filter_has_document_type=self.dt,
|
||||||
|
filter_has_correspondent=self.c,
|
||||||
|
)
|
||||||
|
trigger.filter_has_tags.set([self.t1])
|
||||||
|
trigger.save()
|
||||||
|
action = WorkflowAction.objects.create(
|
||||||
|
assign_owner=self.user2,
|
||||||
|
)
|
||||||
|
w = Workflow.objects.create(
|
||||||
|
name="Workflow 1",
|
||||||
|
order=0,
|
||||||
|
)
|
||||||
|
w.triggers.add(trigger)
|
||||||
|
w.actions.add(action)
|
||||||
|
w.save()
|
||||||
|
|
||||||
|
# create 10 docs with half having the document type
|
||||||
|
for i in range(10):
|
||||||
|
doc = Document.objects.create(
|
||||||
|
title=f"sample test {i}",
|
||||||
|
checksum=f"checksum{i}",
|
||||||
|
correspondent=self.c,
|
||||||
|
original_filename=f"sample_{i}.pdf",
|
||||||
|
document_type=self.dt if i % 2 == 0 else None,
|
||||||
|
)
|
||||||
|
doc.tags.set([self.t1])
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
documents = Document.objects.all()
|
||||||
|
filtered_docs = prefilter_documents_by_workflowtrigger(
|
||||||
|
documents,
|
||||||
|
trigger,
|
||||||
|
)
|
||||||
|
self.assertEqual(filtered_docs.count(), 5)
|
||||||
|
|
||||||
def test_workflow_enabled_disabled(self):
|
def test_workflow_enabled_disabled(self):
|
||||||
trigger = WorkflowTrigger.objects.create(
|
trigger = WorkflowTrigger.objects.create(
|
||||||
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
type=WorkflowTrigger.WorkflowTriggerType.DOCUMENT_ADDED,
|
||||||
|
@ -21,6 +21,7 @@ from django.test import TransactionTestCase
|
|||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from documents.consumer import ConsumerPlugin
|
from documents.consumer import ConsumerPlugin
|
||||||
|
from documents.consumer import ConsumerPreflightPlugin
|
||||||
from documents.data_models import ConsumableDocument
|
from documents.data_models import ConsumableDocument
|
||||||
from documents.data_models import DocumentMetadataOverrides
|
from documents.data_models import DocumentMetadataOverrides
|
||||||
from documents.data_models import DocumentSource
|
from documents.data_models import DocumentSource
|
||||||
@ -344,12 +345,21 @@ class GetConsumerMixin:
|
|||||||
) -> Generator[ConsumerPlugin, None, None]:
|
) -> Generator[ConsumerPlugin, None, None]:
|
||||||
# Store this for verification
|
# Store this for verification
|
||||||
self.status = DummyProgressManager(filepath.name, None)
|
self.status = DummyProgressManager(filepath.name, None)
|
||||||
reader = ConsumerPlugin(
|
doc = ConsumableDocument(
|
||||||
ConsumableDocument(
|
|
||||||
source,
|
source,
|
||||||
original_file=filepath,
|
original_file=filepath,
|
||||||
mailrule_id=mailrule_id or None,
|
mailrule_id=mailrule_id or None,
|
||||||
),
|
)
|
||||||
|
preflight_plugin = ConsumerPreflightPlugin(
|
||||||
|
doc,
|
||||||
|
overrides or DocumentMetadataOverrides(),
|
||||||
|
self.status, # type: ignore
|
||||||
|
self.dirs.scratch_dir,
|
||||||
|
"task-id",
|
||||||
|
)
|
||||||
|
preflight_plugin.setup()
|
||||||
|
reader = ConsumerPlugin(
|
||||||
|
doc,
|
||||||
overrides or DocumentMetadataOverrides(),
|
overrides or DocumentMetadataOverrides(),
|
||||||
self.status, # type: ignore
|
self.status, # type: ignore
|
||||||
self.dirs.scratch_dir,
|
self.dirs.scratch_dir,
|
||||||
@ -357,6 +367,7 @@ class GetConsumerMixin:
|
|||||||
)
|
)
|
||||||
reader.setup()
|
reader.setup()
|
||||||
try:
|
try:
|
||||||
|
preflight_plugin.run()
|
||||||
yield reader
|
yield reader
|
||||||
finally:
|
finally:
|
||||||
reader.cleanup()
|
reader.cleanup()
|
||||||
|
25
src/paperless/tests/test_views.py
Normal file
25
src/paperless/tests/test_views.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def test_favicon_view(client):
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
static_dir = Path(tmpdir)
|
||||||
|
favicon_path = static_dir / "paperless" / "img" / "favicon.ico"
|
||||||
|
favicon_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
favicon_path.write_bytes(b"FAKE ICON DATA")
|
||||||
|
|
||||||
|
settings.STATIC_ROOT = static_dir
|
||||||
|
|
||||||
|
response = client.get("/favicon.ico")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response["Content-Type"] == "image/x-icon"
|
||||||
|
assert b"".join(response.streaming_content) == b"FAKE ICON DATA"
|
||||||
|
|
||||||
|
|
||||||
|
def test_favicon_view_missing_file(client):
|
||||||
|
settings.STATIC_ROOT = Path(tempfile.mkdtemp())
|
||||||
|
response = client.get("/favicon.ico")
|
||||||
|
assert response.status_code == 404
|
@ -1,5 +1,5 @@
|
|||||||
import os
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from allauth.mfa import signals
|
from allauth.mfa import signals
|
||||||
from allauth.mfa.adapter import get_adapter as get_mfa_adapter
|
from allauth.mfa.adapter import get_adapter as get_mfa_adapter
|
||||||
@ -11,8 +11,9 @@ from allauth.socialaccount.adapter import get_adapter
|
|||||||
from allauth.socialaccount.models import SocialAccount
|
from allauth.socialaccount.models import SocialAccount
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.staticfiles.storage import staticfiles_storage
|
||||||
from django.db.models.functions import Lower
|
from django.db.models.functions import Lower
|
||||||
from django.http import HttpResponse
|
from django.http import FileResponse
|
||||||
from django.http import HttpResponseBadRequest
|
from django.http import HttpResponseBadRequest
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from django.http import HttpResponseNotFound
|
from django.http import HttpResponseNotFound
|
||||||
@ -92,16 +93,12 @@ class StandardPagination(PageNumberPagination):
|
|||||||
|
|
||||||
|
|
||||||
class FaviconView(View):
|
class FaviconView(View):
|
||||||
def get(self, request, *args, **kwargs): # pragma: no cover
|
def get(self, request, *args, **kwargs):
|
||||||
favicon = os.path.join(
|
try:
|
||||||
os.path.dirname(__file__),
|
path = Path(staticfiles_storage.path("paperless/img/favicon.ico"))
|
||||||
"static",
|
return FileResponse(path.open("rb"), content_type="image/x-icon")
|
||||||
"paperless",
|
except FileNotFoundError:
|
||||||
"img",
|
return HttpResponseNotFound("favicon.ico not found")
|
||||||
"favicon.ico",
|
|
||||||
)
|
|
||||||
with open(favicon, "rb") as f:
|
|
||||||
return HttpResponse(f, content_type="image/x-icon")
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ModelViewSet):
|
class UserViewSet(ModelViewSet):
|
||||||
|
@ -323,7 +323,7 @@ def error_callback(
|
|||||||
folder=rule.folder,
|
folder=rule.folder,
|
||||||
uid=message_uid,
|
uid=message_uid,
|
||||||
subject=message_subject,
|
subject=message_subject,
|
||||||
received=message_date,
|
received=make_aware(message_date) if is_naive(message_date) else message_date,
|
||||||
status="FAILED",
|
status="FAILED",
|
||||||
error=traceback.format_exc(),
|
error=traceback.format_exc(),
|
||||||
)
|
)
|
||||||
@ -887,7 +887,9 @@ class MailAccountHandler(LoggingMixin):
|
|||||||
folder=rule.folder,
|
folder=rule.folder,
|
||||||
uid=message.uid,
|
uid=message.uid,
|
||||||
subject=message.subject,
|
subject=message.subject,
|
||||||
received=message.date,
|
received=make_aware(message.date)
|
||||||
|
if is_naive(message.date)
|
||||||
|
else message.date,
|
||||||
status="PROCESSED_WO_CONSUMPTION",
|
status="PROCESSED_WO_CONSUMPTION",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user