Compare commits

..

No commits in common. "422bffe1a60d2563a4ef35628504efb341318f9a" and "419ee9d6e71e30191dd11132a9f33913eb71e482" have entirely different histories.

16 changed files with 2820 additions and 2923 deletions

View File

@ -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.3.2", "granian[uvloop]~=2.2.0",
] ]
[dependency-groups] [dependency-groups]
@ -221,6 +221,15 @@ 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
@ -230,6 +239,9 @@ 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

View File

@ -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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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&lt;NgbSli"/> of <x id="INTERPOLATION_1" equiv-text="EventSource = N"/> </source> <source> Slide <x id="INTERPOLATION" equiv-text="ueryList&lt;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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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.13_@angular+core@19.2.14_rxjs@7._ce5a45f3b9d5ca136f928f24177f8d04/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.9_@angular+core@19.2.9_rxjs@7.8._86f32aa280b87d913a06e5a1e90ee24c/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>

View File

@ -12,16 +12,16 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/cdk": "^19.2.14", "@angular/cdk": "^19.2.14",
"@angular/common": "~19.2.13", "@angular/common": "~19.2.9",
"@angular/compiler": "~19.2.14", "@angular/compiler": "~19.2.9",
"@angular/core": "~19.2.14", "@angular/core": "~19.2.9",
"@angular/forms": "~19.2.14", "@angular/forms": "~19.2.9",
"@angular/localize": "~19.2.14", "@angular/localize": "~19.2.9",
"@angular/platform-browser": "~19.2.14", "@angular/platform-browser": "~19.2.9",
"@angular/platform-browser-dynamic": "~19.2.14", "@angular/platform-browser-dynamic": "~19.2.9",
"@angular/router": "~19.2.14", "@angular/router": "~19.2.9",
"@ng-bootstrap/ng-bootstrap": "^18.0.0", "@ng-bootstrap/ng-bootstrap": "^18.0.0",
"@ng-select/ng-select": "^14.9.0", "@ng-select/ng-select": "^14.7.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.14", "@angular-devkit/build-angular": "^19.2.10",
"@angular-devkit/core": "^19.2.14", "@angular-devkit/core": "^19.2.10",
"@angular-devkit/schematics": "^19.2.14", "@angular-devkit/schematics": "^19.2.10",
"@angular-eslint/builder": "19.7.0", "@angular-eslint/builder": "19.3.0",
"@angular-eslint/eslint-plugin": "19.7.0", "@angular-eslint/eslint-plugin": "19.3.0",
"@angular-eslint/eslint-plugin-template": "19.7.0", "@angular-eslint/eslint-plugin-template": "19.3.0",
"@angular-eslint/schematics": "19.7.0", "@angular-eslint/schematics": "19.3.0",
"@angular-eslint/template-parser": "19.7.0", "@angular-eslint/template-parser": "19.3.0",
"@angular/cli": "~19.2.14", "@angular/cli": "~19.2.10",
"@angular/compiler-cli": "~19.2.14", "@angular/compiler-cli": "~19.2.9",
"@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

File diff suppressed because it is too large Load Diff

View File

@ -98,7 +98,15 @@ class ConsumerStatusShortMessage(str, Enum):
FAILED = "failed" FAILED = "failed"
class ConsumerPluginMixin: class ConsumerPlugin(
AlwaysRunPluginMixin,
NoSetupPluginMixin,
NoCleanupPluginMixin,
LoggingMixin,
ConsumeTaskPlugin,
):
logging_name = "paperless.consumer"
def __init__( def __init__(
self, self,
input_doc: ConsumableDocument, input_doc: ConsumableDocument,
@ -147,16 +155,88 @@ class ConsumerPluginMixin:
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.",
)
class ConsumerPlugin( def pre_check_duplicate(self):
AlwaysRunPluginMixin, """
NoSetupPluginMixin, Using the MD5 of the file, check this exact file doesn't already exist
NoCleanupPluginMixin, """
LoggingMixin, with Path(self.input_doc.original_file).open("rb") as f:
ConsumerPluginMixin, checksum = hashlib.md5(f.read()).hexdigest()
ConsumeTaskPlugin, existing_doc = Document.global_objects.filter(
): Q(checksum=checksum) | Q(archive_checksum=checksum),
logging_name = "paperless.consumer" )
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_pre_consume_script(self): def run_pre_consume_script(self):
""" """
@ -286,7 +366,20 @@ class ConsumerPlugin(
tempdir = None tempdir = None
try: try:
# Preflight has already run including progress update to 0% 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_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
@ -744,113 +837,3 @@ 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()

View File

@ -3,7 +3,6 @@ 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
@ -19,8 +18,6 @@ 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")
@ -392,40 +389,6 @@ 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,

View File

@ -26,14 +26,12 @@ 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
@ -146,7 +144,6 @@ def consume_file(
overrides = DocumentMetadataOverrides() overrides = DocumentMetadataOverrides()
plugins: list[type[ConsumeTaskPlugin]] = [ plugins: list[type[ConsumeTaskPlugin]] = [
ConsumerPreflightPlugin,
CollatePlugin, CollatePlugin,
BarcodePlugin, BarcodePlugin,
WorkflowTriggerPlugin, WorkflowTriggerPlugin,
@ -474,12 +471,6 @@ 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}",

View File

@ -1,4 +1,5 @@
import datetime import datetime
import os
import shutil import shutil
import stat import stat
import tempfile import tempfile
@ -65,7 +66,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 = Path(self.tempdir / "archive.pdf") self.archive_path = os.path.join(self.tempdir, "archive.pdf")
shutil.copy(document_path, self.archive_path) shutil.copy(document_path, self.archive_path)
@ -95,16 +96,15 @@ class FaultyGenericExceptionParser(_BaseTestParser):
def fake_magic_from_file(file, *, mime=False): def fake_magic_from_file(file, *, mime=False):
if mime: if mime:
filepath = Path(file) if file.name.startswith("invalid_pdf"):
if filepath.name.startswith("invalid_pdf"):
return "application/octet-stream" return "application/octet-stream"
if filepath.suffix == ".pdf": if os.path.splitext(file)[1] == ".pdf":
return "application/pdf" return "application/pdf"
elif filepath.suffix == ".png": elif os.path.splitext(file)[1] == ".png":
return "image/png" return "image/png"
elif filepath.suffix == ".webp": elif os.path.splitext(file)[1] == ".webp":
return "image/webp" return "image/webp"
elif filepath.suffix == ".eml": elif os.path.splitext(file)[1] == ".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,
Path(filename).stem, os.path.splitext(os.path.basename(filename))[0],
) )
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 = Path(self.dirs.scratch_dir / "._sample.pdf") shadow_file = os.path.join(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.assertRaisesMessage(ConsumerError, "File not found"):
with self.get_consumer(Path("non-existing-file")) as consumer: with self.get_consumer(Path("non-existing-file")) as consumer:
with self.assertRaisesMessage(ConsumerError, "File not found"):
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.assertRaisesMessage(ConsumerError, "It is a duplicate"):
with self.get_consumer(self.get_test_file()) as consumer: with self.get_consumer(self.get_test_file()) as consumer:
with self.assertRaisesMessage(ConsumerError, "It is a duplicate"):
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.assertRaisesMessage(ConsumerError, "It is a duplicate"):
with self.get_consumer(self.get_test_archive_file()) as consumer: with self.get_consumer(self.get_test_archive_file()) as consumer:
with self.assertRaisesMessage(ConsumerError, "It is a duplicate"):
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.assertRaisesMessage(ConsumerError, "document is in the trash"):
with self.get_consumer(self.get_test_file()) as consumer: with self.get_consumer(self.get_test_file()) as consumer:
with self.assertRaisesMessage(ConsumerError, "document is in the trash"):
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.assertRaises(ConsumerError):
with self.get_consumer(dst) as consumer: with self.get_consumer(dst) as consumer:
with self.assertRaises(ConsumerError):
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 = Path(script.name).stat() st = os.stat(script.name)
Path(script.name).chmod(st.st_mode | stat.S_IEXEC) os.chmod(script.name, 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 = Path(script.name).stat() st = os.stat(script.name)
Path(script.name).chmod(st.st_mode | stat.S_IEXEC) os.chmod(script.name, 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 = Path(script.name).stat() st = os.stat(script.name)
Path(script.name).chmod(st.st_mode | stat.S_IEXEC) os.chmod(script.name, 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")

View File

@ -1,5 +1,6 @@
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
@ -70,7 +71,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(settings.ORIGINALS_DIR / "none") self.assertIsDir(os.path.join(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]
@ -107,7 +108,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)
(settings.ORIGINALS_DIR / "none").chmod(0o555) os.chmod(os.path.join(settings.ORIGINALS_DIR, "none"), 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]
@ -119,7 +120,7 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
) )
self.assertEqual(document.filename, "none/none.pdf") self.assertEqual(document.filename, "none/none.pdf")
(settings.ORIGINALS_DIR / "none").chmod(0o777) os.chmod(os.path.join(settings.ORIGINALS_DIR, "none"), 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):
@ -159,7 +160,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(
settings.ORIGINALS_DIR / "none" / "none.pdf", os.path.join(settings.ORIGINALS_DIR, "none/none.pdf"),
) )
self.assertEqual(document.filename, "none/none.pdf") self.assertEqual(document.filename, "none/none.pdf")
@ -182,9 +183,9 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
document.delete() document.delete()
empty_trash([document.pk]) empty_trash([document.pk])
self.assertIsNotFile( self.assertIsNotFile(
settings.ORIGINALS_DIR / "none" / "none.pdf", os.path.join(settings.ORIGINALS_DIR, "none", "none.pdf"),
) )
self.assertIsNotDir(settings.ORIGINALS_DIR / "none") self.assertIsNotDir(os.path.join(settings.ORIGINALS_DIR, "none"))
@override_settings( @override_settings(
FILENAME_FORMAT="{correspondent}/{correspondent}", FILENAME_FORMAT="{correspondent}/{correspondent}",
@ -205,15 +206,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(Path(settings.EMPTY_TRASH_DIR) / "none" / "none.pdf") self.assertIsNotFile(os.path.join(settings.EMPTY_TRASH_DIR, "none", "none.pdf"))
document.delete() document.delete()
empty_trash([document.pk]) empty_trash([document.pk])
self.assertIsNotFile( self.assertIsNotFile(
settings.ORIGINALS_DIR / "none" / "none.pdf", os.path.join(settings.ORIGINALS_DIR, "none", "none.pdf"),
) )
self.assertIsNotDir(settings.ORIGINALS_DIR / "none") self.assertIsNotDir(os.path.join(settings.ORIGINALS_DIR, "none"))
self.assertIsFile(Path(settings.EMPTY_TRASH_DIR) / "none.pdf") self.assertIsFile(os.path.join(settings.EMPTY_TRASH_DIR, "none.pdf"))
self.assertIsNotFile(Path(settings.EMPTY_TRASH_DIR) / "none_01.pdf") self.assertIsNotFile(os.path.join(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()
@ -226,7 +227,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(Path(settings.EMPTY_TRASH_DIR) / "none_01.pdf") self.assertIsFile(os.path.join(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):
@ -260,8 +261,8 @@ class TestFileHandling(DirectoriesMixin, FileSystemAssertsMixin, TestCase):
document.save() document.save()
# Check proper handling of files # Check proper handling of files
self.assertIsDir(settings.ORIGINALS_DIR / "test") self.assertIsDir(os.path.join(settings.ORIGINALS_DIR, "test"))
self.assertIsDir(settings.ORIGINALS_DIR / "none") self.assertIsDir(os.path.join(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}")
@ -370,16 +371,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(settings.ORIGINALS_DIR / "none" / "none") self.assertIsDir(os.path.join(settings.ORIGINALS_DIR, "none/none"))
document.delete() document.delete()
empty_trash([document.pk]) empty_trash([document.pk])
self.assertIsNotFile( self.assertIsNotFile(
settings.ORIGINALS_DIR / "none" / "none" / "none.pdf", os.path.join(settings.ORIGINALS_DIR, "none/none/none.pdf"),
) )
self.assertIsNotDir(settings.ORIGINALS_DIR / "none" / "none") self.assertIsNotDir(os.path.join(settings.ORIGINALS_DIR, "none/none"))
self.assertIsNotDir(settings.ORIGINALS_DIR / "none") self.assertIsNotDir(os.path.join(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}")
@ -414,12 +415,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(
tmp / "notempty" / "empty", os.path.join(tmp, "notempty", "empty"),
root=settings.ORIGINALS_DIR, root=settings.ORIGINALS_DIR,
) )
self.assertIsDir(tmp / "notempty") self.assertIsDir(os.path.join(tmp, "notempty"))
self.assertIsFile(tmp / "notempty" / "file") self.assertIsFile(os.path.join(tmp, "notempty", "file"))
self.assertIsNotDir(tmp / "notempty" / "empty") self.assertIsNotDir(os.path.join(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):
@ -584,8 +585,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 = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(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(
@ -603,8 +604,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 = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(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(
@ -631,8 +632,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 = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(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",
@ -650,9 +651,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 = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
existing_archive_file = settings.ARCHIVE_DIR / "none" / "my_doc.pdf" existing_archive_file = os.path.join(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)
@ -675,8 +676,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 = settings.ORIGINALS_DIR / "document_01.pdf" original = os.path.join(settings.ORIGINALS_DIR, "document_01.pdf")
archive = settings.ARCHIVE_DIR / "document.pdf" archive = os.path.join(settings.ARCHIVE_DIR, "document.pdf")
Path(original).touch() Path(original).touch()
Path(archive).touch() Path(archive).touch()
@ -697,8 +698,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 = settings.ORIGINALS_DIR / "document.pdf" original = os.path.join(settings.ORIGINALS_DIR, "document.pdf")
archive = settings.ARCHIVE_DIR / "document_01.pdf" archive = os.path.join(settings.ARCHIVE_DIR, "document_01.pdf")
Path(original).touch() Path(original).touch()
Path(archive).touch() Path(archive).touch()
@ -724,13 +725,13 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
if "archive" in str(src): if "archive" in str(src):
raise OSError raise OSError
else: else:
Path(src).unlink() os.remove(src)
Path(dst).touch() Path(dst).touch()
m.side_effect = fake_rename m.side_effect = fake_rename
original = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(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(
@ -750,8 +751,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 = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(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(
@ -775,13 +776,13 @@ class TestFileHandlingWithArchive(DirectoriesMixin, FileSystemAssertsMixin, Test
if "original" in str(src): if "original" in str(src):
raise OSError raise OSError
else: else:
Path(src).unlink() os.remove(src)
Path(dst).touch() Path(dst).touch()
m.side_effect = fake_rename m.side_effect = fake_rename
original = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(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(
@ -801,8 +802,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 = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(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(
@ -829,9 +830,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 = settings.ORIGINALS_DIR / "document.webp" original = os.path.join(settings.ORIGINALS_DIR, "document.webp")
original2 = settings.ORIGINALS_DIR / "0000001.pdf" original2 = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch() Path(original).touch()
Path(original2).touch() Path(original2).touch()
Path(archive).touch() Path(archive).touch()
@ -864,8 +865,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 = settings.ORIGINALS_DIR / "0000001.pdf" original = os.path.join(settings.ORIGINALS_DIR, "0000001.pdf")
archive = settings.ARCHIVE_DIR / "0000001.pdf" archive = os.path.join(settings.ARCHIVE_DIR, "0000001.pdf")
Path(original).touch() Path(original).touch()
Path(archive).touch() Path(archive).touch()
doc = Document( doc = Document(

View File

@ -1,5 +1,6 @@
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
@ -20,7 +21,7 @@ migration_1012_obj = importlib.import_module(
def archive_name_from_filename(filename): def archive_name_from_filename(filename):
return Path(filename).stem + ".pdf" return os.path.splitext(filename)[0] + ".pdf"
def archive_path_old(self): def archive_path_old(self):
@ -29,12 +30,12 @@ def archive_path_old(self):
else: else:
fname = f"{self.pk:07}.pdf" fname = f"{self.pk:07}.pdf"
return Path(settings.ARCHIVE_DIR) / fname return os.path.join(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 Path(settings.ARCHIVE_DIR) / str(doc.archive_filename) return os.path.join(settings.ARCHIVE_DIR, str(doc.archive_filename))
else: else:
return None return None
@ -47,7 +48,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 Path(settings.ORIGINALS_DIR) / fname return os.path.join(settings.ORIGINALS_DIR, fname)
def thumbnail_path(doc): def thumbnail_path(doc):
@ -55,7 +56,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 Path(settings.THUMBNAIL_DIR) / file_name return os.path.join(settings.THUMBNAIL_DIR, file_name)
def make_test_document( def make_test_document(
@ -75,7 +76,7 @@ def make_test_document(
doc.save() doc.save()
shutil.copy2(original, source_path(doc)) shutil.copy2(original, source_path(doc))
with Path(original).open("rb") as f: with open(original, "rb") as f:
doc.checksum = hashlib.md5(f.read()).hexdigest() doc.checksum = hashlib.md5(f.read()).hexdigest()
if archive: if archive:
@ -85,7 +86,7 @@ def make_test_document(
else: else:
shutil.copy2(archive, archive_path_old(doc)) shutil.copy2(archive, archive_path_old(doc))
with Path(archive).open("rb") as f: with open(archive, "rb") as f:
doc.archive_checksum = hashlib.md5(f.read()).hexdigest() doc.archive_checksum = hashlib.md5(f.read()).hexdigest()
doc.save() doc.save()
@ -95,17 +96,25 @@ def make_test_document(
return doc return doc
simple_jpg = Path(__file__).parent / "samples" / "simple.jpg" simple_jpg = os.path.join(os.path.dirname(__file__), "samples", "simple.jpg")
simple_pdf = Path(__file__).parent / "samples" / "simple.pdf" simple_pdf = os.path.join(os.path.dirname(__file__), "samples", "simple.pdf")
simple_pdf2 = ( simple_pdf2 = os.path.join(
Path(__file__).parent / "samples" / "documents" / "originals" / "0000002.pdf" os.path.dirname(__file__),
"samples",
"documents",
"originals",
"0000002.pdf",
) )
simple_pdf3 = ( simple_pdf3 = os.path.join(
Path(__file__).parent / "samples" / "documents" / "originals" / "0000003.pdf" os.path.dirname(__file__),
"samples",
"documents",
"originals",
"0000003.pdf",
) )
simple_txt = Path(__file__).parent / "samples" / "simple.txt" simple_txt = os.path.join(os.path.dirname(__file__), "samples", "simple.txt")
simple_png = Path(__file__).parent / "samples" / "simple-noalpha.png" simple_png = os.path.join(os.path.dirname(__file__), "samples", "simple-noalpha.png")
simple_png2 = Path(__file__).parent / "examples" / "no-text.png" simple_png2 = os.path.join(os.path.dirname(__file__), "examples", "no-text.png")
@override_settings(FILENAME_FORMAT="") @override_settings(FILENAME_FORMAT="")
@ -189,13 +198,13 @@ class TestMigrateArchiveFiles(DirectoriesMixin, FileSystemAssertsMixin, TestMigr
else: else:
self.assertIsNone(doc.archive_filename) self.assertIsNone(doc.archive_filename)
with Path(source_path(doc)).open("rb") as f: with open(source_path(doc), "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 archive_path_new(doc).open("rb") as f: with open(archive_path_new(doc), "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)
@ -292,7 +301,7 @@ class TestMigrateArchiveFilesErrors(DirectoriesMixin, TestMigrations):
"clash.pdf", "clash.pdf",
simple_pdf, simple_pdf,
) )
archive_path_old(doc).unlink() os.unlink(archive_path_old(doc))
self.assertRaisesMessage( self.assertRaisesMessage(
ValueError, ValueError,
@ -485,13 +494,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 Path(source_path(doc)).open("rb") as f: with open(source_path(doc), "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 archive_path_old(doc).open("rb") as f: with open(archive_path_old(doc), "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)

View File

@ -25,7 +25,6 @@ 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
@ -1712,55 +1711,6 @@ 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,

View File

@ -21,7 +21,6 @@ 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
@ -345,21 +344,12 @@ 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)
doc = ConsumableDocument( reader = ConsumerPlugin(
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,
@ -367,7 +357,6 @@ class GetConsumerMixin:
) )
reader.setup() reader.setup()
try: try:
preflight_plugin.run()
yield reader yield reader
finally: finally:
reader.cleanup() reader.cleanup()

View File

@ -1,25 +0,0 @@
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

View File

@ -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,9 +11,8 @@ 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 FileResponse from django.http import HttpResponse
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
@ -93,12 +92,16 @@ class StandardPagination(PageNumberPagination):
class FaviconView(View): class FaviconView(View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs): # pragma: no cover
try: favicon = os.path.join(
path = Path(staticfiles_storage.path("paperless/img/favicon.ico")) os.path.dirname(__file__),
return FileResponse(path.open("rb"), content_type="image/x-icon") "static",
except FileNotFoundError: "paperless",
return HttpResponseNotFound("favicon.ico not found") "img",
"favicon.ico",
)
with open(favicon, "rb") as f:
return HttpResponse(f, content_type="image/x-icon")
class UserViewSet(ModelViewSet): class UserViewSet(ModelViewSet):

View File

@ -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=make_aware(message_date) if is_naive(message_date) else message_date, received=message_date,
status="FAILED", status="FAILED",
error=traceback.format_exc(), error=traceback.format_exc(),
) )
@ -887,9 +887,7 @@ class MailAccountHandler(LoggingMixin):
folder=rule.folder, folder=rule.folder,
uid=message.uid, uid=message.uid,
subject=message.subject, subject=message.subject,
received=make_aware(message.date) received=message.date,
if is_naive(message.date)
else message.date,
status="PROCESSED_WO_CONSUMPTION", status="PROCESSED_WO_CONSUMPTION",
) )

3202
uv.lock generated

File diff suppressed because it is too large Load Diff