Compare commits

..

7 Commits

Author SHA1 Message Date
shamoon
828e3dc4c6 Update bulk_edit.py 2026-03-03 11:52:50 -08:00
shamoon
8f67342841 This is all unused now 2026-03-03 11:52:50 -08:00
shamoon
a8a703a1ab Oh this needs to be here 2026-03-03 11:52:50 -08:00
shamoon
244ffd331f Use source mode to determine which docs bulk edit use 2026-03-03 11:52:50 -08:00
shamoon
0031036b03 Add a source mode param, pass from frontend detail 2026-03-03 11:52:50 -08:00
GitHub Actions
16b58c2de5 Auto translate strings 2026-03-03 19:25:03 +00:00
shamoon
c724fbb5d9 Clarify bulk edit wording with versions 2026-03-03 11:22:22 -08:00
15 changed files with 3046 additions and 2851 deletions

View File

@@ -264,7 +264,6 @@ src/documents/management/commands/document_exporter.py:0: error: Function is mis
src/documents/management/commands/document_exporter.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "Path") [assignment]
src/documents/management/commands/document_exporter.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice[Any, Any, Any]" [index]
src/documents/management/commands/document_exporter.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice[Any, Any, Any]" [index]
src/documents/management/commands/document_exporter.py:0: error: Library stubs not installed for "tqdm" [import-untyped]
src/documents/management/commands/document_exporter.py:0: error: Missing type parameters for generic type "QuerySet" [type-arg]
src/documents/management/commands/document_exporter.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/management/commands/document_exporter.py:0: error: Missing type parameters for generic type "dict" [type-arg]
@@ -283,18 +282,23 @@ src/documents/management/commands/document_importer.py:0: error: Function is mis
src/documents/management/commands/document_importer.py:0: error: Incompatible types in assignment (expression has type "str | None", base class "CryptMixin" defined the type as "str") [assignment]
src/documents/management/commands/document_importer.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice[Any, Any, Any]" [index]
src/documents/management/commands/document_importer.py:0: error: Invalid index type "str" for "str"; expected type "SupportsIndex | slice[Any, Any, Any]" [index]
src/documents/management/commands/document_importer.py:0: error: Library stubs not installed for "tqdm" [import-untyped]
src/documents/management/commands/document_importer.py:0: error: Missing type parameters for generic type "Generator" [type-arg]
src/documents/management/commands/document_importer.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/management/commands/document_importer.py:0: error: Need type annotation for "manifest_paths" (hint: "manifest_paths: list[<type>] = ...") [var-annotated]
src/documents/management/commands/document_importer.py:0: error: Skipping analyzing "auditlog.registry": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/management/commands/document_index.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_index.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_llmindex.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_llmindex.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_renamer.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_retagger.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/management/commands/document_retagger.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/management/commands/document_renamer.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_retagger.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_retagger.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_sanity_checker.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_sanity_checker.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_thumbnails.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/document_thumbnails.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/management/commands/document_thumbnails.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/management/commands/loaddata_stdin.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/loaddata_stdin.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/loaddata_stdin.py:0: error: Incompatible types in assignment (expression has type "tuple[Callable[[Any, Any], TextIO | Any], None]", target has type "tuple[Callable[[str, Literal['r', 'rb']], BufferedReader[_BufferedReaderStream]]]") [assignment]
@@ -304,8 +308,11 @@ src/documents/management/commands/mixins.py:0: error: Attribute "kdf_algorithm"
src/documents/management/commands/mixins.py:0: error: Attribute "key_iterations" already defined on line 0 [no-redef]
src/documents/management/commands/mixins.py:0: error: Attribute "key_size" already defined on line 0 [no-redef]
src/documents/management/commands/mixins.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/management/commands/mixins.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/management/commands/mixins.py:0: error: Incompatible types in assignment (expression has type "list[dict[str, Sequence[str]]]", variable has type "CryptFields") [assignment]
src/documents/management/commands/mixins.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/management/commands/mixins.py:0: error: Unsupported operand types for // ("None" and "int") [operator]
src/documents/management/commands/prune_audit_logs.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/prune_audit_logs.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/management/commands/prune_audit_logs.py:0: error: Skipping analyzing "auditlog.models": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/matching.py:0: error: Argument 1 to "existing_document_matches_workflow" has incompatible type "ConsumableDocument | Document"; expected "Document" [arg-type]
@@ -441,7 +448,18 @@ src/documents/plugins/helpers.py:0: error: Function is missing a type annotation
src/documents/plugins/helpers.py:0: error: Skipping analyzing "channels_redis.pubsub": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/regex.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/regex.py:0: error: Library stubs not installed for "regex" [import-untyped]
src/documents/sanity_checker.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type]
src/documents/sanity_checker.py:0: error: Cannot use Final inside a loop [misc]
src/documents/sanity_checker.py:0: error: Cannot use Final inside a loop [misc]
src/documents/sanity_checker.py:0: error: Cannot use Final inside a loop [misc]
src/documents/sanity_checker.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/sanity_checker.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/sanity_checker.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/sanity_checker.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/sanity_checker.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/sanity_checker.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/sanity_checker.py:0: error: Incompatible type for "task_id" of "PaperlessTask" (got "UUID", expected "str | int | Combinable") [misc]
src/documents/sanity_checker.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/schema.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/schema.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/schema.py:0: error: Function is missing a type annotation [no-untyped-def]
@@ -532,8 +550,6 @@ src/documents/serialisers.py:0: error: Function is missing a type annotation [n
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/serialisers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -624,7 +640,6 @@ src/documents/serialisers.py:0: error: Missing type parameters for generic type
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "Serializer" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/serialisers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/serialisers.py:0: error: Need type annotation for "document" [var-annotated]
@@ -649,6 +664,9 @@ src/documents/signals/handlers.py:0: error: Argument 2 to "match_storage_paths"
src/documents/signals/handlers.py:0: error: Argument 2 to "match_tags" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 2 to "validate_move" has incompatible type "Path | Any | None"; expected "Path" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 3 to "validate_move" has incompatible type "Path | Any | None"; expected "Path" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Argument 5 to "_suggestion_printer" has incompatible type "Any | None"; expected "MatchingModel" [arg-type]
src/documents/signals/handlers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation [no-untyped-def]
@@ -668,6 +686,12 @@ src/documents/signals/handlers.py:0: error: Function is missing a type annotatio
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/signals/handlers.py:0: error: Incompatible types in assignment (expression has type "list[Tag]", variable has type "set[Tag]") [assignment]
src/documents/signals/handlers.py:0: error: Incompatible types in assignment (expression has type "tuple[Any, Any, Any]", variable has type "tuple[Any, Any]") [assignment]
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "save" [union-attr]
src/documents/signals/handlers.py:0: error: Item "ConsumableDocument" of "Document | ConsumableDocument" has no attribute "source_path" [union-attr]
@@ -695,6 +719,7 @@ src/documents/tasks.py:0: error: Function is missing a type annotation for one o
src/documents/tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tasks.py:0: error: Incompatible type for "task_id" of "PaperlessTask" (got "UUID", expected "str | int | Combinable") [misc]
src/documents/tasks.py:0: error: Incompatible type for "task_id" of "PaperlessTask" (got "UUID", expected "str | int | Combinable") [misc]
src/documents/tasks.py:0: error: Incompatible types in assignment (expression has type "Path", variable has type "str") [assignment]
@@ -721,80 +746,8 @@ src/documents/templating/utils.py:0: error: Function is missing a type annotatio
src/documents/templating/workflows.py:0: error: Incompatible return value type (got "None", expected "str") [return-value]
src/documents/tests/conftest.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/conftest.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/conftest.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/conftest.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/conftest.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/conftest.py:0: error: Incompatible return value type (got "DocumentFactory", expected "Document") [return-value]
src/documents/tests/factories.py:0: error: Missing type parameters for generic type "DjangoModelFactory" [type-arg]
src/documents/tests/factories.py:0: error: Missing type parameters for generic type "DjangoModelFactory" [type-arg]
src/documents/tests/factories.py:0: error: Missing type parameters for generic type "DjangoModelFactory" [type-arg]
src/documents/tests/factories.py:0: error: Missing type parameters for generic type "DjangoModelFactory" [type-arg]
src/documents/tests/factories.py:0: error: Missing type parameters for generic type "DjangoModelFactory" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Incompatible types in assignment (expression has type "StringIO", variable has type "OutputWrapper") [assignment]
src/documents/tests/management/test_management_base_cmd.py:0: error: Missing type parameters for generic type "QuerySet" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Missing type parameters for generic type "list" [type-arg]
src/documents/tests/management/test_management_base_cmd.py:0: error: Need type annotation for "result" [var-annotated]
src/documents/tests/management/test_management_base_cmd.py:0: error: Need type annotation for "result" (hint: "result: list[<type>] = ...") [var-annotated]
src/documents/tests/management/test_management_base_cmd.py:0: error: Need type annotation for "results" [var-annotated]
src/documents/tests/management/test_management_sanity_checker.py:0: error: "DocumentFactory" has no attribute "source_path"; maybe "storage_path"? [attr-defined]
src/documents/tests/management/test_management_sanity_checker.py:0: error: "DocumentFactory" has no attribute "thumbnail_path" [attr-defined]
src/documents/tests/test_admin.py:0: error: "PaperlessUserForm" has no attribute "request" [attr-defined]
src/documents/tests/test_admin.py:0: error: "PaperlessUserForm" has no attribute "request" [attr-defined]
src/documents/tests/test_admin.py:0: error: Function is missing a type annotation [no-untyped-def]
@@ -822,7 +775,6 @@ src/documents/tests/test_api_app_config.py:0: error: Item "None" of "Application
src/documents/tests/test_api_bulk_download.py:0: error: Value of type variable "_StrPathT" of "copy" cannot be "Path | None" [type-var]
src/documents/tests/test_api_bulk_edit.py:0: error: "type[MatchingModel]" has no attribute "objects" [attr-defined]
src/documents/tests/test_api_bulk_edit.py:0: error: "type[MatchingModel]" has no attribute "objects" [attr-defined]
src/documents/tests/test_api_bulk_edit.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/test_api_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_api_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_api_bulk_edit.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -875,10 +827,8 @@ src/documents/tests/test_api_custom_fields.py:0: error: Value of type "Any | Non
src/documents/tests/test_api_documents.py:0: error: "None" object is not iterable [misc]
src/documents/tests/test_api_documents.py:0: error: "object" has no attribute "get" [attr-defined]
src/documents/tests/test_api_documents.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type]
src/documents/tests/test_api_documents.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type]
src/documents/tests/test_api_documents.py:0: error: Argument 1 to "assertCountEqual" of "TestCase" has incompatible type "list[int] | None"; expected "Iterable[Any]" [arg-type]
src/documents/tests/test_api_documents.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/test_api_documents.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/test_api_documents.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/test_api_documents.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/test_api_documents.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -1223,20 +1173,7 @@ src/documents/tests/test_management_exporter.py:0: error: Function is missing a
src/documents/tests/test_management_exporter.py:0: error: Item "None" of "MailAccount | None" has no attribute "password" [union-attr]
src/documents/tests/test_management_exporter.py:0: error: Skipping analyzing "allauth.socialaccount.models": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/documents/tests/test_management_fuzzy.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/test_management_retagger.py:0: error: "DocumentFactory" has no attribute "refresh_from_db" [attr-defined]
src/documents/tests/test_management_retagger.py:0: error: "DocumentFactory" has no attribute "refresh_from_db" [attr-defined]
src/documents/tests/test_management_retagger.py:0: error: "DocumentFactory" has no attribute "refresh_from_db" [attr-defined]
src/documents/tests/test_management_retagger.py:0: error: "DocumentFactory" has no attribute "tags" [attr-defined]
src/documents/tests/test_management_retagger.py:0: error: "DocumentFactory" has no attribute "tags" [attr-defined]
src/documents/tests/test_management_retagger.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_management_retagger.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_management_retagger.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_management_retagger.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_management_retagger.py:0: error: Incompatible return value type (got "tuple[CorrespondentFactory, CorrespondentFactory]", expected "tuple[Correspondent, Correspondent]") [return-value]
src/documents/tests/test_management_retagger.py:0: error: Incompatible return value type (got "tuple[DocumentFactory, DocumentFactory, DocumentFactory, DocumentFactory]", expected "tuple[Document, Document, Document, Document]") [return-value]
src/documents/tests/test_management_retagger.py:0: error: Incompatible return value type (got "tuple[DocumentTypeFactory, DocumentTypeFactory]", expected "tuple[DocumentType, DocumentType]") [return-value]
src/documents/tests/test_management_retagger.py:0: error: Incompatible return value type (got "tuple[StoragePathFactory, StoragePathFactory, StoragePathFactory]", expected "tuple[StoragePath, StoragePath, StoragePath]") [return-value]
src/documents/tests/test_management_retagger.py:0: error: Incompatible return value type (got "tuple[TagFactory, TagFactory, TagFactory, TagFactory, TagFactory]", expected "tuple[Tag, Tag, Tag, Tag, Tag]") [return-value]
src/documents/tests/test_management_retagger.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/test_management_superuser.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/tests/test_migration_share_link_bundle.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_migration_share_link_bundle.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -1253,10 +1190,9 @@ src/documents/tests/test_parsers.py:0: error: Function is missing a type annotat
src/documents/tests/test_parsers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_parsers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_parsers.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_sanity_check.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type]
src/documents/tests/test_sanity_check.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type]
src/documents/tests/test_sanity_check.py:0: error: Argument 1 to "Path" has incompatible type "Path | None"; expected "str | PathLike[str]" [arg-type]
src/documents/tests/test_sanity_check.py:0: error: Unsupported right operand type for in ("str | None") [operator]
src/documents/tests/test_sanity_check.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/documents/tests/test_sanity_check.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_sanity_check.py:0: error: Invalid index type "None" for "dict[int, list[dict[Any, Any]]]"; expected type "int" [index]
src/documents/tests/test_share_link_bundles.py:0: error: "_MonkeyPatchedResponse" has no attribute "streaming_content" [attr-defined]
src/documents/tests/test_share_link_bundles.py:0: error: Argument 1 to "ZipFile" has incompatible type "Path | None"; expected "str | PathLike[str] | IO[bytes]" [arg-type]
src/documents/tests/test_share_link_bundles.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
@@ -1287,6 +1223,10 @@ src/documents/tests/test_tasks.py:0: error: Function is missing a type annotatio
src/documents/tests/test_tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_tasks.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/tests/test_views.py:0: error: "_MonkeyPatchedWSGIResponse" has no attribute "render" [attr-defined]
src/documents/tests/test_views.py:0: error: "_MonkeyPatchedWSGIResponse" has no attribute "render" [attr-defined]
src/documents/tests/test_views.py:0: error: "_MonkeyPatchedWSGIResponse" has no attribute "url" [attr-defined]
@@ -1594,6 +1534,7 @@ src/documents/views.py:0: error: "get_serializer_context" undefined in superclas
src/documents/views.py:0: error: "object" not callable [operator]
src/documents/views.py:0: error: "type[Model]" has no attribute "objects" [attr-defined]
src/documents/views.py:0: error: Argument "path" to "EmailAttachment" has incompatible type "Path | None"; expected "Path" [arg-type]
src/documents/views.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_correspondents" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_document_types" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
src/documents/views.py:0: error: Argument 2 to "match_storage_paths" has incompatible type "DocumentClassifier | None"; expected "DocumentClassifier" [arg-type]
@@ -1668,8 +1609,6 @@ src/documents/views.py:0: error: Function is missing a type annotation [no-unty
src/documents/views.py:0: error: Function is missing a type annotation [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/documents/views.py:0: error: Incompatible type for lookup 'owner': (got "User | AnonymousUser", expected "User | int | None") [misc]
src/documents/views.py:0: error: Incompatible types in assignment (expression has type "Any | None", variable has type "dict[Any, Any]") [assignment]
src/documents/views.py:0: error: Incompatible types in assignment (expression has type "QuerySet[Any, Any]", variable has type "list[Any]") [assignment]
@@ -1925,54 +1864,39 @@ src/paperless/serialisers.py:0: error: Skipping analyzing "allauth.mfa.adapter":
src/paperless/serialisers.py:0: error: Skipping analyzing "allauth.mfa.models": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/serialisers.py:0: error: Skipping analyzing "allauth.mfa.totp.internal.auth": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/serialisers.py:0: error: Skipping analyzing "allauth.socialaccount.models": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/settings/__init__.py:0: error: "Sequence[str]" has no attribute "append" [attr-defined]
src/paperless/settings/__init__.py:0: error: "Sequence[str]" has no attribute "insert" [attr-defined]
src/paperless/settings/__init__.py:0: error: Argument 1 to "_parse_ignore_dates" has incompatible type "str | None"; expected "str" [arg-type]
src/paperless/settings/__init__.py:0: error: Argument 1 to "append" of "list" has incompatible type "str | None"; expected "str" [arg-type]
src/paperless/settings/__init__.py:0: error: Argument 2 to "__get_path" has incompatible type "str | None"; expected "PathLike[Any] | str" [arg-type]
src/paperless/settings/__init__.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/settings/__init__.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/settings/__init__.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/settings/__init__.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/settings/__init__.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/settings/__init__.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/paperless/settings/__init__.py:0: error: Incompatible return value type (got "set[date]", expected "set[datetime]") [return-value]
src/paperless/settings/__init__.py:0: error: Incompatible return value type (got "tuple[str | None, str, str, str, str]", expected "tuple[str, str, str, str, str]") [return-value]
src/paperless/settings/__init__.py:0: error: Incompatible types in assignment (expression has type "bool", variable has type "str") [assignment]
src/paperless/settings/__init__.py:0: error: Incompatible types in assignment (expression has type "set[datetime]", variable has type "set[date]") [assignment]
src/paperless/settings/__init__.py:0: error: Missing type parameters for generic type "PathLike" [type-arg]
src/paperless/settings/__init__.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/paperless/settings/__init__.py:0: error: No overload variant of "getenv" matches argument types "Collection[str]", "Collection[str]" [call-overload]
src/paperless/settings/__init__.py:0: error: Skipping analyzing "compression_middleware.middleware": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/settings/parsers.py:0: error: Incompatible types in assignment (expression has type "Path", variable has type "bool") [assignment]
src/paperless/settings/parsers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/paperless/settings/parsers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/paperless/settings/parsers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/paperless/settings.py:0: error: "Sequence[str]" has no attribute "append" [attr-defined]
src/paperless/settings.py:0: error: "Sequence[str]" has no attribute "insert" [attr-defined]
src/paperless/settings.py:0: error: "object" has no attribute "update" [attr-defined]
src/paperless/settings.py:0: error: "object" has no attribute "update" [attr-defined]
src/paperless/settings.py:0: error: "object" has no attribute "update" [attr-defined]
src/paperless/settings.py:0: error: "object" has no attribute "update" [attr-defined]
src/paperless/settings.py:0: error: Argument 1 to "_parse_ignore_dates" has incompatible type "str | None"; expected "str" [arg-type]
src/paperless/settings.py:0: error: Argument 1 to "append" of "list" has incompatible type "str | None"; expected "str" [arg-type]
src/paperless/settings.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
src/paperless/settings.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
src/paperless/settings.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
src/paperless/settings.py:0: error: Argument 1 to "int" has incompatible type "str | None"; expected "str | Buffer | SupportsInt | SupportsIndex | SupportsTrunc" [arg-type]
src/paperless/settings.py:0: error: Argument 2 to "__get_path" has incompatible type "str | None"; expected "PathLike[Any] | str" [arg-type]
src/paperless/settings.py:0: error: Argument 2 to "getenv" has incompatible type "None"; expected "Collection[str]" [arg-type]
src/paperless/settings.py:0: error: Argument 2 to "getenv" has incompatible type "None"; expected "Collection[str]" [arg-type]
src/paperless/settings.py:0: error: Argument 2 to "getenv" has incompatible type "None"; expected "Collection[str]" [arg-type]
src/paperless/settings.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/settings.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/settings.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/settings.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/settings.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/settings.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/paperless/settings.py:0: error: Incompatible return value type (got "set[date]", expected "set[datetime]") [return-value]
src/paperless/settings.py:0: error: Incompatible return value type (got "tuple[str | None, str, str, str, str]", expected "tuple[str, str, str, str, str]") [return-value]
src/paperless/settings.py:0: error: Incompatible types in assignment (expression has type "bool", variable has type "str") [assignment]
src/paperless/settings.py:0: error: Incompatible types in assignment (expression has type "set[datetime]", variable has type "set[date]") [assignment]
src/paperless/settings.py:0: error: Missing type parameters for generic type "PathLike" [type-arg]
src/paperless/settings.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/paperless/settings.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/paperless/settings.py:0: error: No overload variant of "getenv" matches argument types "Collection[str]", "Collection[str]" [call-overload]
src/paperless/settings.py:0: error: Skipping analyzing "compression_middleware.middleware": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/signals.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/signals.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/tests/settings/test_custom_parsers.py:0: error: Missing type parameters for generic type "dict" [type-arg]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a return type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/tests/settings/test_environment_parsers.py:0: error: Function is missing a type annotation [no-untyped-def]
src/paperless/tests/test_adapter.py:0: error: Cannot assign to a method [method-assign]
src/paperless/tests/test_adapter.py:0: error: Cannot assign to a method [method-assign]
src/paperless/tests/test_adapter.py:0: error: Cannot assign to a method [method-assign]
@@ -1980,12 +1904,6 @@ src/paperless/tests/test_adapter.py:0: error: Function is missing a type annotat
src/paperless/tests/test_adapter.py:0: error: Skipping analyzing "allauth.account.adapter": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/tests/test_adapter.py:0: error: Skipping analyzing "allauth.core": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/tests/test_adapter.py:0: error: Skipping analyzing "allauth.socialaccount.adapter": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/tests/test_checks.py:0: error: Generator has incompatible item type "str | None"; expected "str" [misc]
src/paperless/tests/test_checks.py:0: error: Unsupported right operand type for in ("str | None") [operator]
src/paperless/tests/test_checks.py:0: error: Unsupported right operand type for in ("str | None") [operator]
src/paperless/tests/test_checks.py:0: error: Unsupported right operand type for in ("str | None") [operator]
src/paperless/tests/test_checks.py:0: error: Unsupported right operand type for in ("str | None") [operator]
src/paperless/tests/test_checks.py:0: error: Unsupported right operand type for in ("str | None") [operator]
src/paperless/tests/test_db_cache.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]
src/paperless/tests/test_db_cache.py:0: error: Skipping analyzing "cachalot.settings": module is installed, but missing library stubs or py.typed marker [import-untyped]
src/paperless/tests/test_settings.py:0: error: Function is missing a type annotation for one or more arguments [no-untyped-def]

File diff suppressed because it is too large Load Diff

View File

@@ -1238,8 +1238,8 @@
<context context-type="linenumber">82</context>
</context-group>
</trans-unit>
<trans-unit id="8035757452478567832" datatype="html">
<source>Update existing document</source>
<trans-unit id="7860582931776068318" datatype="html">
<source>Add document version</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/admin/settings/settings.component.html</context>
<context context-type="linenumber">280</context>
@@ -8411,8 +8411,8 @@
<context context-type="linenumber">832</context>
</context-group>
</trans-unit>
<trans-unit id="6390006284731990222" datatype="html">
<source>This operation will permanently rotate the original version of <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source>
<trans-unit id="5203024009814367559" datatype="html">
<source>This operation will add rotated versions of the <x id="PH" equiv-text="this.list.selected.size"/> document(s).</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/bulk-editor/bulk-editor.component.ts</context>
<context context-type="linenumber">833</context>

View File

@@ -277,7 +277,7 @@
<div class="col">
<select class="form-select" formControlName="pdfEditorDefaultEditMode">
<option [ngValue]="PdfEditorEditMode.Create" i18n>Create new document(s)</option>
<option [ngValue]="PdfEditorEditMode.Update" i18n>Update existing document</option>
<option [ngValue]="PdfEditorEditMode.Update" i18n>Add document version</option>
</select>
</div>
</div>

View File

@@ -84,7 +84,7 @@
<input type="radio" class="btn-check" [(ngModel)]="editMode" [value]="PdfEditorEditMode.Update" id="editModeUpdate" name="editmode" [disabled]="hasSplit()">
<label for="editModeUpdate" class="btn btn-outline-primary btn-sm">
<i-bs name="pencil"></i-bs>
<span class="form-check-label ms-2" i18n>Update existing document</span>
<span class="form-check-label ms-2" i18n>Add document version</span>
</label>
</div>
@if (editMode === PdfEditorEditMode.Create) {

View File

@@ -3,6 +3,7 @@ import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { DocumentService } from 'src/app/services/rest/document.service'
import { PDFEditorComponent } from './pdf-editor.component'
describe('PDFEditorComponent', () => {
@@ -139,4 +140,16 @@ describe('PDFEditorComponent', () => {
expect(component.pages[1].page).toBe(2)
expect(component.pages[2].page).toBe(3)
})
it('should include selected version in preview source when provided', () => {
const documentService = TestBed.inject(DocumentService)
const previewSpy = jest
.spyOn(documentService, 'getPreviewUrl')
.mockReturnValue('preview-version')
component.documentID = 3
component.versionID = 10
expect(component.pdfSrc).toBe('preview-version')
expect(previewSpy).toHaveBeenCalledWith(3, false, 10)
})
})

View File

@@ -46,6 +46,7 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
activeModal: NgbActiveModal = inject(NgbActiveModal)
documentID: number
versionID?: number
pages: PageOperation[] = []
totalPages = 0
editMode: PdfEditorEditMode = this.settingsService.get(
@@ -55,7 +56,11 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
includeMetadata: boolean = true
get pdfSrc(): string {
return this.documentService.getPreviewUrl(this.documentID)
return this.documentService.getPreviewUrl(
this.documentID,
false,
this.versionID
)
}
pdfLoaded(pdf: PngxPdfDocumentProxy) {

View File

@@ -1459,22 +1459,25 @@ describe('DocumentDetailComponent', () => {
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
const errorSpy = jest.spyOn(toastService, 'showError')
initNormally()
component.selectedVersionId = 10
component.editPdf()
expect(modal).not.toBeUndefined()
modal.componentInstance.documentID = doc.id
expect(modal.componentInstance.versionID).toBe(10)
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }]
modal.componentInstance.confirm()
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/bulk_edit/`
)
expect(req.request.body).toEqual({
documents: [doc.id],
documents: [10],
method: 'edit_pdf',
parameters: {
operations: [{ page: 1, rotate: 0, doc: 0 }],
delete_original: false,
update_document: false,
include_metadata: true,
source_mode: 'explicit_selection',
},
})
req.error(new ErrorEvent('failed'))
@@ -1496,6 +1499,7 @@ describe('DocumentDetailComponent', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[0]))
initNormally()
component.selectedVersionId = 10
component.password = 'secret'
component.removePassword()
const dialog =
@@ -1508,13 +1512,14 @@ describe('DocumentDetailComponent', () => {
`${environment.apiBaseUrl}documents/bulk_edit/`
)
expect(req.request.body).toEqual({
documents: [doc.id],
documents: [10],
method: 'remove_password',
parameters: {
password: 'secret',
update_document: false,
include_metadata: false,
delete_original: true,
source_mode: 'explicit_selection',
},
})
req.flush(true)

View File

@@ -73,7 +73,10 @@ import {
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import {
BulkEditSourceMode,
DocumentService,
} from 'src/app/services/rest/document.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service'
@@ -1626,20 +1629,23 @@ export class DocumentDetailComponent
size: 'xl',
scrollable: true,
})
const sourceDocumentId = this.selectedVersionId ?? this.document.id
modal.componentInstance.title = $localize`PDF Editor`
modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.documentID = this.document.id
modal.componentInstance.versionID = sourceDocumentId
modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
modal.componentInstance.buttonsEnabled = false
this.documentsService
.bulkEdit([this.document.id], 'edit_pdf', {
.bulkEdit([sourceDocumentId], 'edit_pdf', {
operations: modal.componentInstance.getOperations(),
delete_original: modal.componentInstance.deleteOriginal,
update_document:
modal.componentInstance.editMode == PdfEditorEditMode.Update,
include_metadata: modal.componentInstance.includeMetadata,
source_mode: BulkEditSourceMode.EXPLICIT_SELECTION,
})
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
@@ -1685,16 +1691,18 @@ export class DocumentDetailComponent
modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
const sourceDocumentId = this.selectedVersionId ?? this.document.id
const dialog =
modal.componentInstance as PasswordRemovalConfirmDialogComponent
dialog.buttonsEnabled = false
this.networkActive = true
this.documentsService
.bulkEdit([this.document.id], 'remove_password', {
.bulkEdit([sourceDocumentId], 'remove_password', {
password: this.password,
update_document: dialog.updateDocument,
include_metadata: dialog.includeMetadata,
delete_original: dialog.deleteOriginal,
source_mode: BulkEditSourceMode.EXPLICIT_SELECTION,
})
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({

View File

@@ -830,7 +830,7 @@ export class BulkEditorComponent
})
const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent
rotateDialog.title = $localize`Rotate confirm`
rotateDialog.messageBold = $localize`This operation will permanently rotate the original version of ${this.list.selected.size} document(s).`
rotateDialog.messageBold = $localize`This operation will add rotated versions of the ${this.list.selected.size} document(s).`
rotateDialog.btnClass = 'btn-danger'
rotateDialog.btnCaption = $localize`Proceed`
rotateDialog.documentID = Array.from(this.list.selected)[0]

View File

@@ -37,6 +37,11 @@ export interface SelectionData {
selected_custom_fields: SelectionDataItem[]
}
export enum BulkEditSourceMode {
LATEST_VERSION = 'latest_version',
EXPLICIT_SELECTION = 'explicit_selection',
}
@Injectable({
providedIn: 'root',
})

View File

@@ -29,12 +29,21 @@ from documents.plugins.helpers import DocumentsStatusManager
from documents.tasks import bulk_update_documents
from documents.tasks import consume_file
from documents.tasks import update_document_content_maybe_archive_file
from documents.versioning import get_latest_version_for_root
from documents.versioning import get_root_document
if TYPE_CHECKING:
from django.contrib.auth.models import User
logger: logging.Logger = logging.getLogger("paperless.bulk_edit")
SourceMode = Literal["latest_version", "explicit_selection"]
class SourceModeChoices:
LATEST_VERSION: SourceMode = "latest_version"
EXPLICIT_SELECTION: SourceMode = "explicit_selection"
@shared_task(bind=True)
def restore_archive_serial_numbers_task(
@@ -72,46 +81,21 @@ def restore_archive_serial_numbers(backup: dict[int, int | None]) -> None:
logger.info(f"Restored archive serial numbers for documents {list(backup.keys())}")
def _get_root_ids_by_doc_id(doc_ids: list[int]) -> dict[int, int]:
"""
Resolve each provided document id to its root document id.
def _resolve_root_and_source_doc(
doc: Document,
*,
source_mode: SourceMode = SourceModeChoices.LATEST_VERSION,
) -> tuple[Document, Document]:
root_doc = get_root_document(doc)
- If the id is already a root document: root id is itself.
- If the id is a version document: root id is its `root_document_id`.
"""
qs = Document.objects.filter(id__in=doc_ids).only("id", "root_document_id")
return {doc.id: doc.root_document_id or doc.id for doc in qs}
if source_mode == SourceModeChoices.EXPLICIT_SELECTION:
return root_doc, doc
# Version IDs are explicit by default, only a selected root resolves to latest
if doc.root_document_id is not None:
return root_doc, doc
def _get_root_and_current_docs_by_root_id(
root_ids: set[int],
) -> tuple[dict[int, Document], dict[int, Document]]:
"""
Returns:
- root_docs: root_id -> root Document
- current_docs: root_id -> newest version Document (or root if none)
"""
root_docs = {
doc.id: doc
for doc in Document.objects.filter(id__in=root_ids).select_related(
"owner",
)
}
latest_versions_by_root_id: dict[int, Document] = {}
for version_doc in Document.objects.filter(root_document_id__in=root_ids).order_by(
"root_document_id",
"-id",
):
root_id = version_doc.root_document_id
if root_id is None:
continue
latest_versions_by_root_id.setdefault(root_id, version_doc)
current_docs: dict[int, Document] = {
root_id: latest_versions_by_root_id.get(root_id, root_docs[root_id])
for root_id in root_docs
}
return root_docs, current_docs
return root_doc, get_latest_version_for_root(root_doc)
def set_correspondent(
@@ -421,21 +405,32 @@ def rotate(
doc_ids: list[int],
degrees: int,
*,
source_mode: SourceMode = SourceModeChoices.LATEST_VERSION,
user: User | None = None,
) -> Literal["OK"]:
logger.info(
f"Attempting to rotate {len(doc_ids)} documents by {degrees} degrees.",
)
doc_to_root_id = _get_root_ids_by_doc_id(doc_ids)
root_ids = set(doc_to_root_id.values())
root_docs_by_id, current_docs_by_root_id = _get_root_and_current_docs_by_root_id(
root_ids,
)
docs_by_id = {
doc.id: doc
for doc in Document.objects.select_related("root_document").filter(
id__in=doc_ids,
)
}
docs_by_root_id: dict[int, tuple[Document, Document]] = {}
for doc_id in doc_ids:
doc = docs_by_id.get(doc_id)
if doc is None:
continue
root_doc, source_doc = _resolve_root_and_source_doc(
doc,
source_mode=source_mode,
)
docs_by_root_id.setdefault(root_doc.id, (root_doc, source_doc))
import pikepdf
for root_id in root_ids:
root_doc = root_docs_by_id[root_id]
source_doc = current_docs_by_root_id[root_id]
for root_doc, source_doc in docs_by_root_id.values():
if source_doc.mime_type != "application/pdf":
logger.warning(
f"Document {root_doc.id} is not a PDF, skipping rotation.",
@@ -659,25 +654,17 @@ def delete_pages(
doc_ids: list[int],
pages: list[int],
*,
source_mode: SourceMode = SourceModeChoices.LATEST_VERSION,
user: User | None = None,
) -> Literal["OK"]:
logger.info(
f"Attempting to delete pages {pages} from {len(doc_ids)} documents",
)
doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
root_doc: Document
if doc.root_document_id is None or doc.root_document is None:
root_doc = doc
else:
root_doc = doc.root_document
source_doc = (
Document.objects.filter(Q(id=root_doc.id) | Q(root_document=root_doc))
.order_by("-id")
.first()
root_doc, source_doc = _resolve_root_and_source_doc(
doc,
source_mode=source_mode,
)
if source_doc is None:
source_doc = root_doc
pages = sorted(pages) # sort pages to avoid index issues
import pikepdf
@@ -722,6 +709,7 @@ def edit_pdf(
delete_original: bool = False,
update_document: bool = False,
include_metadata: bool = True,
source_mode: SourceMode = SourceModeChoices.LATEST_VERSION,
user: User | None = None,
) -> Literal["OK"]:
"""
@@ -736,19 +724,10 @@ def edit_pdf(
f"Editing PDF of document {doc_ids[0]} with {len(operations)} operations",
)
doc = Document.objects.select_related("root_document").get(id=doc_ids[0])
root_doc: Document
if doc.root_document_id is None or doc.root_document is None:
root_doc = doc
else:
root_doc = doc.root_document
source_doc = (
Document.objects.filter(Q(id=root_doc.id) | Q(root_document=root_doc))
.order_by("-id")
.first()
root_doc, source_doc = _resolve_root_and_source_doc(
doc,
source_mode=source_mode,
)
if source_doc is None:
source_doc = root_doc
import pikepdf
pdf_docs: list[pikepdf.Pdf] = []
@@ -859,6 +838,7 @@ def remove_password(
update_document: bool = False,
delete_original: bool = False,
include_metadata: bool = True,
source_mode: SourceMode = SourceModeChoices.LATEST_VERSION,
user: User | None = None,
) -> Literal["OK"]:
"""
@@ -868,19 +848,10 @@ def remove_password(
for doc_id in doc_ids:
doc = Document.objects.select_related("root_document").get(id=doc_id)
root_doc: Document
if doc.root_document_id is None or doc.root_document is None:
root_doc = doc
else:
root_doc = doc.root_document
source_doc = (
Document.objects.filter(Q(id=root_doc.id) | Q(root_document=root_doc))
.order_by("-id")
.first()
root_doc, source_doc = _resolve_root_and_source_doc(
doc,
source_mode=source_mode,
)
if source_doc is None:
source_doc = root_doc
try:
logger.info(
f"Attempting password removal from document {doc_ids[0]}",

View File

@@ -1724,6 +1724,15 @@ class BulkEditSerializer(
except ValueError:
raise serializers.ValidationError("invalid rotation degrees")
def _validate_source_mode(self, parameters) -> None:
source_mode = parameters.get(
"source_mode",
bulk_edit.SourceModeChoices.LATEST_VERSION,
)
if source_mode not in bulk_edit.SourceModeChoices.__dict__.values():
raise serializers.ValidationError("Invalid source_mode")
parameters["source_mode"] = source_mode
def _validate_parameters_split(self, parameters) -> None:
if "pages" not in parameters:
raise serializers.ValidationError("pages not specified")
@@ -1824,6 +1833,9 @@ class BulkEditSerializer(
method = attrs["method"]
parameters = attrs["parameters"]
if "source_mode" in parameters:
self._validate_source_mode(parameters)
if method == bulk_edit.set_correspondent:
self._validate_parameters_correspondent(parameters)
elif method == bulk_edit.set_document_type:

View File

@@ -1395,7 +1395,10 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
{
"documents": [self.doc2.id],
"method": "edit_pdf",
"parameters": {"operations": [{"page": 1}]},
"parameters": {
"operations": [{"page": 1}],
"source_mode": "explicit_selection",
},
},
),
content_type="application/json",
@@ -1407,6 +1410,7 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
args, kwargs = m.call_args
self.assertCountEqual(args[0], [self.doc2.id])
self.assertEqual(kwargs["operations"], [{"page": 1}])
self.assertEqual(kwargs["source_mode"], "explicit_selection")
self.assertEqual(kwargs["user"], self.user)
def test_edit_pdf_invalid_params(self) -> None:
@@ -1572,6 +1576,24 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase):
response.content,
)
# invalid source mode
response = self.client.post(
"/api/documents/bulk_edit/",
json.dumps(
{
"documents": [self.doc2.id],
"method": "edit_pdf",
"parameters": {
"operations": [{"page": 1}],
"source_mode": "not_a_mode",
},
},
),
content_type="application/json",
)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn(b"Invalid source_mode", response.content)
@mock.patch("documents.serialisers.bulk_edit.edit_pdf")
def test_edit_pdf_page_out_of_bounds(self, m) -> None:
"""

View File

@@ -405,7 +405,9 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
self.assertTrue(Document.objects.filter(id=self.doc1.id).exists())
self.assertFalse(Document.objects.filter(id=version.id).exists())
def test_get_root_and_current_doc_mapping(self) -> None:
def test_resolve_root_and_source_doc_latest_version_prefers_newest_version(
self,
) -> None:
version1 = Document.objects.create(
checksum="B-v1",
title="B version 1",
@@ -417,18 +419,14 @@ class TestBulkEdit(DirectoriesMixin, TestCase):
root_document=self.doc2,
)
root_ids_by_doc_id = bulk_edit._get_root_ids_by_doc_id(
[self.doc2.id, version1.id, version2.id],
root_doc, source_doc = bulk_edit._resolve_root_and_source_doc(
self.doc2,
source_mode="latest_version",
)
self.assertEqual(root_ids_by_doc_id[self.doc2.id], self.doc2.id)
self.assertEqual(root_ids_by_doc_id[version1.id], self.doc2.id)
self.assertEqual(root_ids_by_doc_id[version2.id], self.doc2.id)
root_docs, current_docs = bulk_edit._get_root_and_current_docs_by_root_id(
{self.doc2.id},
)
self.assertEqual(root_docs[self.doc2.id].id, self.doc2.id)
self.assertEqual(current_docs[self.doc2.id].id, version2.id)
self.assertEqual(root_doc.id, self.doc2.id)
self.assertEqual(source_doc.id, version2.id)
self.assertNotEqual(source_doc.id, version1.id)
@mock.patch("documents.tasks.bulk_update_documents.delay")
def test_set_permissions(self, m) -> None:
@@ -1041,6 +1039,34 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertIsNotNone(overrides)
self.assertEqual(result, "OK")
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.open")
def test_rotate_explicit_selection_uses_root_source_when_root_selected(
self,
mock_open,
mock_consume_delay,
mock_magic,
):
Document.objects.create(
checksum="B-v1",
title="B version 1",
root_document=self.doc2,
)
fake_pdf = mock.MagicMock()
fake_pdf.pages = [mock.Mock()]
mock_open.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.rotate(
[self.doc2.id],
90,
source_mode="explicit_selection",
)
self.assertEqual(result, "OK")
mock_open.assert_called_once_with(self.doc2.source_path)
mock_consume_delay.assert_called_once()
@mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.Pdf.save")
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@@ -1065,6 +1091,34 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertIsNotNone(overrides)
self.assertEqual(result, "OK")
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.open")
def test_delete_pages_explicit_selection_uses_root_source_when_root_selected(
self,
mock_open,
mock_consume_delay,
mock_magic,
):
Document.objects.create(
checksum="B-v1",
title="B version 1",
root_document=self.doc2,
)
fake_pdf = mock.MagicMock()
fake_pdf.pages = [mock.Mock(), mock.Mock()]
mock_open.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.delete_pages(
[self.doc2.id],
[1],
source_mode="explicit_selection",
)
self.assertEqual(result, "OK")
mock_open.assert_called_once_with(self.doc2.source_path)
mock_consume_delay.assert_called_once()
@mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.Pdf.save")
def test_delete_pages_with_error(self, mock_pdf_save, mock_consume_delay):
@@ -1213,6 +1267,40 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertTrue(str(consumable.original_file).endswith("_edited.pdf"))
self.assertIsNotNone(overrides)
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.new")
@mock.patch("pikepdf.open")
def test_edit_pdf_explicit_selection_uses_root_source_when_root_selected(
self,
mock_open,
mock_new,
mock_consume_delay,
mock_magic,
):
Document.objects.create(
checksum="B-v1",
title="B version 1",
root_document=self.doc2,
)
fake_pdf = mock.MagicMock()
fake_pdf.pages = [mock.Mock()]
mock_open.return_value.__enter__.return_value = fake_pdf
output_pdf = mock.MagicMock()
output_pdf.pages = []
mock_new.return_value = output_pdf
result = bulk_edit.edit_pdf(
[self.doc2.id],
operations=[{"page": 1}],
update_document=True,
source_mode="explicit_selection",
)
self.assertEqual(result, "OK")
mock_open.assert_called_once_with(self.doc2.source_path)
mock_consume_delay.assert_called_once()
@mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s")
def test_edit_pdf_without_metadata(
@@ -1333,6 +1421,34 @@ class TestPDFActions(DirectoriesMixin, TestCase):
self.assertEqual(consumable.root_document_id, doc.id)
self.assertIsNotNone(overrides)
@mock.patch("documents.data_models.magic.from_file", return_value="application/pdf")
@mock.patch("documents.tasks.consume_file.delay")
@mock.patch("pikepdf.open")
def test_remove_password_explicit_selection_uses_root_source_when_root_selected(
self,
mock_open,
mock_consume_delay,
mock_magic,
) -> None:
Document.objects.create(
checksum="A-v1",
title="A version 1",
root_document=self.doc1,
)
fake_pdf = mock.MagicMock()
mock_open.return_value.__enter__.return_value = fake_pdf
result = bulk_edit.remove_password(
[self.doc1.id],
password="secret",
update_document=True,
source_mode="explicit_selection",
)
self.assertEqual(result, "OK")
mock_open.assert_called_once_with(self.doc1.source_path, password="secret")
mock_consume_delay.assert_called_once()
@mock.patch("documents.bulk_edit.chord")
@mock.patch("documents.bulk_edit.group")
@mock.patch("documents.tasks.consume_file.s")