Compare commits

...

6 Commits

Author SHA1 Message Date
GitHub Actions
3ed7297939 Auto translate strings 2026-04-13 21:14:40 +00:00
Trenton H
3b6edcdd8e Chore: Add generic type params and update our baselines (#12566)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:12:59 -07:00
GitHub Actions
b27d10646e Auto translate strings 2026-04-13 20:12:04 +00:00
Trenton H
8c1225e120 Fixes an N+1 query in matching with the version content fetching by prefetching versions (#12562) 2026-04-13 13:10:28 -07:00
Trenton H
54d5269145 Fix: Use an iterator in the sanity checking (#12563) 2026-04-13 12:32:22 -07:00
Trenton H
f5729811fe Chore: Upgrades Django manually, since dependabot is failing. Resolves security alerts (#12567) 2026-04-13 10:20:35 -07:00
18 changed files with 6958 additions and 8067 deletions

View File

@@ -165,6 +165,7 @@ jobs:
contents: read contents: read
env: env:
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
PAPERLESS_SECRET_KEY: "ci-typing-not-a-real-secret"
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

View File

@@ -88,6 +88,7 @@ jobs:
uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt uv export --quiet --no-dev --all-extras --format requirements-txt --output-file requirements.txt
- name: Compile messages - name: Compile messages
env: env:
PAPERLESS_SECRET_KEY: "ci-release-not-a-real-secret"
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
run: | run: |
cd src/ cd src/
@@ -96,6 +97,7 @@ jobs:
manage.py compilemessages manage.py compilemessages
- name: Collect static files - name: Collect static files
env: env:
PAPERLESS_SECRET_KEY: "ci-release-not-a-real-secret"
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }} PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
run: | run: |
cd src/ cd src/

View File

@@ -36,6 +36,8 @@ jobs:
--group dev \ --group dev \
--frozen --frozen
- name: Generate backend translation strings - name: Generate backend translation strings
env:
PAPERLESS_SECRET_KEY: "ci-translate-not-a-real-secret"
run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*" run: cd src/ && uv run manage.py makemessages -l en_US -i "samples*"
- name: Install pnpm - name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0 uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

3
.gitignore vendored
View File

@@ -79,6 +79,7 @@ virtualenv
/docker-compose.env /docker-compose.env
/docker-compose.yml /docker-compose.yml
.ruff_cache/ .ruff_cache/
.mypy_cache/
# Used for development # Used for development
scripts/import-for-development scripts/import-for-development
@@ -111,4 +112,6 @@ celerybeat-schedule*
# ignore pnpm package store folder created when setting up the devcontainer # ignore pnpm package store folder created when setting up the devcontainer
.pnpm-store/ .pnpm-store/
# Git worktree local folder
.worktrees .worktrees

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -24,7 +24,7 @@ dependencies = [
"dateparser~=1.2", "dateparser~=1.2",
# WARNING: django does not use semver. # WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes. # Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.10", "django~=5.2.13",
"django-allauth[mfa,socialaccount]~=65.15.0", "django-allauth[mfa,socialaccount]~=65.15.0",
"django-auditlog~=3.4.1", "django-auditlog~=3.4.1",
"django-cachalot~=2.9.0", "django-cachalot~=2.9.0",

View File

@@ -650,6 +650,10 @@ class ConsumerPlugin(
# If we get here, it was successful. Proceed with post-consume # If we get here, it was successful. Proceed with post-consume
# hooks. If they fail, nothing will get changed. # hooks. If they fail, nothing will get changed.
document = Document.objects.prefetch_related("versions").get(
pk=document.pk,
)
document_consumption_finished.send( document_consumption_finished.send(
sender=self.__class__, sender=self.__class__,
document=document, document=document,

View File

@@ -381,7 +381,10 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
if isinstance(prefetched_cache, dict) if isinstance(prefetched_cache, dict)
else None else None
) )
if prefetched_versions: if prefetched_versions is not None:
# Empty list means prefetch ran and found no versions — use own content.
if not prefetched_versions:
return self.content
latest_prefetched = max(prefetched_versions, key=lambda doc: doc.id) latest_prefetched = max(prefetched_versions, key=lambda doc: doc.id)
return latest_prefetched.content return latest_prefetched.content

View File

@@ -182,8 +182,9 @@ def _check_thumbnail(
present_files: set[Path], present_files: set[Path],
) -> None: ) -> None:
"""Verify the thumbnail exists and is readable.""" """Verify the thumbnail exists and is readable."""
thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve() # doc.thumbnail_path already returns a resolved Path; no need to re-resolve.
if not thumbnail_path.exists() or not thumbnail_path.is_file(): thumbnail_path: Final[Path] = doc.thumbnail_path
if not thumbnail_path.is_file():
messages.error(doc.pk, "Thumbnail of document does not exist.") messages.error(doc.pk, "Thumbnail of document does not exist.")
return return
@@ -200,8 +201,9 @@ def _check_original(
present_files: set[Path], present_files: set[Path],
) -> None: ) -> None:
"""Verify the original file exists, is readable, and has matching checksum.""" """Verify the original file exists, is readable, and has matching checksum."""
source_path: Final[Path] = Path(doc.source_path).resolve() # doc.source_path already returns a resolved Path; no need to re-resolve.
if not source_path.exists() or not source_path.is_file(): source_path: Final[Path] = doc.source_path
if not source_path.is_file():
messages.error(doc.pk, "Original of document does not exist.") messages.error(doc.pk, "Original of document does not exist.")
return return
@@ -237,8 +239,9 @@ def _check_archive(
elif doc.has_archive_version: elif doc.has_archive_version:
if TYPE_CHECKING: if TYPE_CHECKING:
assert isinstance(doc.archive_path, Path) assert isinstance(doc.archive_path, Path)
archive_path: Final[Path] = Path(doc.archive_path).resolve() # doc.archive_path already returns a resolved Path; no need to re-resolve.
if not archive_path.exists() or not archive_path.is_file(): archive_path: Final[Path] = doc.archive_path # type: ignore[assignment]
if not archive_path.is_file():
messages.error(doc.pk, "Archived version of document does not exist.") messages.error(doc.pk, "Archived version of document does not exist.")
return return
@@ -314,7 +317,15 @@ def check_sanity(
messages = SanityCheckMessages() messages = SanityCheckMessages()
present_files = _build_present_files() present_files = _build_present_files()
documents = Document.global_objects.all() documents = Document.global_objects.only(
"pk",
"filename",
"mime_type",
"checksum",
"archive_checksum",
"archive_filename",
"content",
).iterator(chunk_size=500)
for doc in iter_wrapper(documents): for doc in iter_wrapper(documents):
_check_document(doc, messages, present_files) _check_document(doc, messages, present_files)

View File

@@ -100,7 +100,7 @@ logger = logging.getLogger("paperless.serializers")
# https://www.django-rest-framework.org/api-guide/serializers/#example # https://www.django-rest-framework.org/api-guide/serializers/#example
class DynamicFieldsModelSerializer(serializers.ModelSerializer): class DynamicFieldsModelSerializer(serializers.ModelSerializer[Any]):
""" """
A ModelSerializer that takes an additional `fields` argument that A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed. controls which fields should be displayed.
@@ -121,7 +121,7 @@ class DynamicFieldsModelSerializer(serializers.ModelSerializer):
self.fields.pop(field_name) self.fields.pop(field_name)
class MatchingModelSerializer(serializers.ModelSerializer): class MatchingModelSerializer(serializers.ModelSerializer[Any]):
document_count = serializers.IntegerField(read_only=True) document_count = serializers.IntegerField(read_only=True)
def get_slug(self, obj) -> str: def get_slug(self, obj) -> str:
@@ -261,7 +261,7 @@ class SetPermissionsSerializer(serializers.DictField):
class OwnedObjectSerializer( class OwnedObjectSerializer(
SerializerWithPerms, SerializerWithPerms,
serializers.ModelSerializer, serializers.ModelSerializer[Any],
SetPermissionsMixin, SetPermissionsMixin,
): ):
def __init__(self, *args, **kwargs) -> None: def __init__(self, *args, **kwargs) -> None:
@@ -469,7 +469,7 @@ class OwnedObjectSerializer(
return super().update(instance, validated_data) return super().update(instance, validated_data)
class OwnedObjectListSerializer(serializers.ListSerializer): class OwnedObjectListSerializer(serializers.ListSerializer[Any]):
def to_representation(self, documents): def to_representation(self, documents):
self.child.context["shared_object_pks"] = self.child.get_shared_object_pks( self.child.context["shared_object_pks"] = self.child.get_shared_object_pks(
documents, documents,
@@ -682,27 +682,27 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
return super().validate(attrs) return super().validate(attrs)
class CorrespondentField(serializers.PrimaryKeyRelatedField): class CorrespondentField(serializers.PrimaryKeyRelatedField[Correspondent]):
def get_queryset(self): def get_queryset(self):
return Correspondent.objects.all() return Correspondent.objects.all()
class TagsField(serializers.PrimaryKeyRelatedField): class TagsField(serializers.PrimaryKeyRelatedField[Tag]):
def get_queryset(self): def get_queryset(self):
return Tag.objects.all() return Tag.objects.all()
class DocumentTypeField(serializers.PrimaryKeyRelatedField): class DocumentTypeField(serializers.PrimaryKeyRelatedField[DocumentType]):
def get_queryset(self): def get_queryset(self):
return DocumentType.objects.all() return DocumentType.objects.all()
class StoragePathField(serializers.PrimaryKeyRelatedField): class StoragePathField(serializers.PrimaryKeyRelatedField[StoragePath]):
def get_queryset(self): def get_queryset(self):
return StoragePath.objects.all() return StoragePath.objects.all()
class CustomFieldSerializer(serializers.ModelSerializer): class CustomFieldSerializer(serializers.ModelSerializer[CustomField]):
data_type = serializers.ChoiceField( data_type = serializers.ChoiceField(
choices=CustomField.FieldDataType, choices=CustomField.FieldDataType,
read_only=False, read_only=False,
@@ -816,7 +816,7 @@ def validate_documentlink_targets(user, doc_ids):
) )
class CustomFieldInstanceSerializer(serializers.ModelSerializer): class CustomFieldInstanceSerializer(serializers.ModelSerializer[CustomFieldInstance]):
field = serializers.PrimaryKeyRelatedField(queryset=CustomField.objects.all()) field = serializers.PrimaryKeyRelatedField(queryset=CustomField.objects.all())
value = ReadWriteSerializerMethodField(allow_null=True) value = ReadWriteSerializerMethodField(allow_null=True)
@@ -922,14 +922,14 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
] ]
class BasicUserSerializer(serializers.ModelSerializer): class BasicUserSerializer(serializers.ModelSerializer[User]):
# Different than paperless.serializers.UserSerializer # Different than paperless.serializers.UserSerializer
class Meta: class Meta:
model = User model = User
fields = ["id", "username", "first_name", "last_name"] fields = ["id", "username", "first_name", "last_name"]
class NotesSerializer(serializers.ModelSerializer): class NotesSerializer(serializers.ModelSerializer[Note]):
user = BasicUserSerializer(read_only=True) user = BasicUserSerializer(read_only=True)
class Meta: class Meta:
@@ -1256,7 +1256,7 @@ class DocumentSerializer(
list_serializer_class = OwnedObjectListSerializer list_serializer_class = OwnedObjectListSerializer
class SearchResultListSerializer(serializers.ListSerializer): class SearchResultListSerializer(serializers.ListSerializer[Document]):
def to_representation(self, hits): def to_representation(self, hits):
document_ids = [hit["id"] for hit in hits] document_ids = [hit["id"] for hit in hits]
# Fetch all Document objects in the list in one SQL query. # Fetch all Document objects in the list in one SQL query.
@@ -1313,7 +1313,7 @@ class SearchResultSerializer(DocumentSerializer):
list_serializer_class = SearchResultListSerializer list_serializer_class = SearchResultListSerializer
class SavedViewFilterRuleSerializer(serializers.ModelSerializer): class SavedViewFilterRuleSerializer(serializers.ModelSerializer[SavedViewFilterRule]):
class Meta: class Meta:
model = SavedViewFilterRule model = SavedViewFilterRule
fields = ["rule_type", "value"] fields = ["rule_type", "value"]
@@ -2401,7 +2401,7 @@ class StoragePathSerializer(MatchingModelSerializer, OwnedObjectSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class UiSettingsViewSerializer(serializers.ModelSerializer): class UiSettingsViewSerializer(serializers.ModelSerializer[UiSettings]):
settings = serializers.DictField(required=False, allow_null=True) settings = serializers.DictField(required=False, allow_null=True)
class Meta: class Meta:
@@ -2760,7 +2760,7 @@ class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
return attrs return attrs
class WorkflowTriggerSerializer(serializers.ModelSerializer): class WorkflowTriggerSerializer(serializers.ModelSerializer[WorkflowTrigger]):
id = serializers.IntegerField(required=False, allow_null=True) id = serializers.IntegerField(required=False, allow_null=True)
sources = fields.MultipleChoiceField( sources = fields.MultipleChoiceField(
choices=WorkflowTrigger.DocumentSourceChoices.choices, choices=WorkflowTrigger.DocumentSourceChoices.choices,
@@ -2870,7 +2870,7 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer):
return super().update(instance, validated_data) return super().update(instance, validated_data)
class WorkflowActionEmailSerializer(serializers.ModelSerializer): class WorkflowActionEmailSerializer(serializers.ModelSerializer[WorkflowActionEmail]):
id = serializers.IntegerField(allow_null=True, required=False) id = serializers.IntegerField(allow_null=True, required=False)
class Meta: class Meta:
@@ -2884,7 +2884,9 @@ class WorkflowActionEmailSerializer(serializers.ModelSerializer):
] ]
class WorkflowActionWebhookSerializer(serializers.ModelSerializer): class WorkflowActionWebhookSerializer(
serializers.ModelSerializer[WorkflowActionWebhook],
):
id = serializers.IntegerField(allow_null=True, required=False) id = serializers.IntegerField(allow_null=True, required=False)
def validate_url(self, url): def validate_url(self, url):
@@ -2905,7 +2907,7 @@ class WorkflowActionWebhookSerializer(serializers.ModelSerializer):
] ]
class WorkflowActionSerializer(serializers.ModelSerializer): class WorkflowActionSerializer(serializers.ModelSerializer[WorkflowAction]):
id = serializers.IntegerField(required=False, allow_null=True) id = serializers.IntegerField(required=False, allow_null=True)
assign_correspondent = CorrespondentField(allow_null=True, required=False) assign_correspondent = CorrespondentField(allow_null=True, required=False)
assign_tags = TagsField(many=True, allow_null=True, required=False) assign_tags = TagsField(many=True, allow_null=True, required=False)
@@ -3027,7 +3029,7 @@ class WorkflowActionSerializer(serializers.ModelSerializer):
return attrs return attrs
class WorkflowSerializer(serializers.ModelSerializer): class WorkflowSerializer(serializers.ModelSerializer[Workflow]):
order = serializers.IntegerField(required=False) order = serializers.IntegerField(required=False)
triggers = WorkflowTriggerSerializer(many=True) triggers = WorkflowTriggerSerializer(many=True)

View File

@@ -291,7 +291,7 @@ class IndexView(TemplateView):
return context return context
class PassUserMixin(GenericAPIView): class PassUserMixin(GenericAPIView[Any]):
""" """
Pass a user object to serializer Pass a user object to serializer
""" """
@@ -457,7 +457,10 @@ class PermissionsAwareDocumentCountMixin(BulkPermissionMixin, PassUserMixin):
@extend_schema_view(**generate_object_with_permissions_schema(CorrespondentSerializer)) @extend_schema_view(**generate_object_with_permissions_schema(CorrespondentSerializer))
class CorrespondentViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet): class CorrespondentViewSet(
PermissionsAwareDocumentCountMixin,
ModelViewSet[Correspondent],
):
model = Correspondent model = Correspondent
queryset = Correspondent.objects.select_related("owner").order_by(Lower("name")) queryset = Correspondent.objects.select_related("owner").order_by(Lower("name"))
@@ -494,7 +497,7 @@ class CorrespondentViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet):
@extend_schema_view(**generate_object_with_permissions_schema(TagSerializer)) @extend_schema_view(**generate_object_with_permissions_schema(TagSerializer))
class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet): class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[Tag]):
model = Tag model = Tag
serializer_class = TagSerializer serializer_class = TagSerializer
document_count_through = Document.tags.through document_count_through = Document.tags.through
@@ -573,7 +576,10 @@ class TagViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet):
@extend_schema_view(**generate_object_with_permissions_schema(DocumentTypeSerializer)) @extend_schema_view(**generate_object_with_permissions_schema(DocumentTypeSerializer))
class DocumentTypeViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet): class DocumentTypeViewSet(
PermissionsAwareDocumentCountMixin,
ModelViewSet[DocumentType],
):
model = DocumentType model = DocumentType
queryset = DocumentType.objects.select_related("owner").order_by(Lower("name")) queryset = DocumentType.objects.select_related("owner").order_by(Lower("name"))
@@ -808,7 +814,7 @@ class DocumentViewSet(
UpdateModelMixin, UpdateModelMixin,
DestroyModelMixin, DestroyModelMixin,
ListModelMixin, ListModelMixin,
GenericViewSet, GenericViewSet[Document],
): ):
model = Document model = Document
queryset = Document.objects.all() queryset = Document.objects.all()
@@ -1248,7 +1254,10 @@ class DocumentViewSet(
), ),
) )
def suggestions(self, request, pk=None): def suggestions(self, request, pk=None):
doc = get_object_or_404(Document.objects.select_related("owner"), pk=pk) doc = get_object_or_404(
Document.objects.select_related("owner").prefetch_related("versions"),
pk=pk,
)
if request.user is not None and not has_perms_owner_aware( if request.user is not None and not has_perms_owner_aware(
request.user, request.user,
"view_document", "view_document",
@@ -1952,7 +1961,7 @@ class ChatStreamingSerializer(serializers.Serializer):
], ],
name="dispatch", name="dispatch",
) )
class ChatStreamingView(GenericAPIView): class ChatStreamingView(GenericAPIView[Any]):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = ChatStreamingSerializer serializer_class = ChatStreamingSerializer
@@ -2278,7 +2287,7 @@ class LogViewSet(ViewSet):
@extend_schema_view(**generate_object_with_permissions_schema(SavedViewSerializer)) @extend_schema_view(**generate_object_with_permissions_schema(SavedViewSerializer))
class SavedViewViewSet(BulkPermissionMixin, PassUserMixin, ModelViewSet): class SavedViewViewSet(BulkPermissionMixin, PassUserMixin, ModelViewSet[SavedView]):
model = SavedView model = SavedView
queryset = SavedView.objects.select_related("owner").prefetch_related( queryset = SavedView.objects.select_related("owner").prefetch_related(
@@ -2756,7 +2765,7 @@ class RemovePasswordDocumentsView(DocumentOperationPermissionMixin):
}, },
), ),
) )
class PostDocumentView(GenericAPIView): class PostDocumentView(GenericAPIView[Any]):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = PostDocumentSerializer serializer_class = PostDocumentSerializer
parser_classes = (parsers.MultiPartParser,) parser_classes = (parsers.MultiPartParser,)
@@ -2877,7 +2886,7 @@ class PostDocumentView(GenericAPIView):
}, },
), ),
) )
class SelectionDataView(GenericAPIView): class SelectionDataView(GenericAPIView[Any]):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = DocumentListSerializer serializer_class = DocumentListSerializer
parser_classes = (parsers.MultiPartParser, parsers.JSONParser) parser_classes = (parsers.MultiPartParser, parsers.JSONParser)
@@ -2981,7 +2990,7 @@ class SelectionDataView(GenericAPIView):
}, },
), ),
) )
class SearchAutoCompleteView(GenericAPIView): class SearchAutoCompleteView(GenericAPIView[Any]):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
def get(self, request, format=None): def get(self, request, format=None):
@@ -3262,7 +3271,7 @@ class GlobalSearchView(PassUserMixin):
}, },
), ),
) )
class StatisticsView(GenericAPIView): class StatisticsView(GenericAPIView[Any]):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
def get(self, request, format=None): def get(self, request, format=None):
@@ -3364,7 +3373,7 @@ class StatisticsView(GenericAPIView):
) )
class BulkDownloadView(DocumentSelectionMixin, GenericAPIView): class BulkDownloadView(DocumentSelectionMixin, GenericAPIView[Any]):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
serializer_class = BulkDownloadSerializer serializer_class = BulkDownloadSerializer
parser_classes = (parsers.JSONParser,) parser_classes = (parsers.JSONParser,)
@@ -3417,7 +3426,7 @@ class BulkDownloadView(DocumentSelectionMixin, GenericAPIView):
@extend_schema_view(**generate_object_with_permissions_schema(StoragePathSerializer)) @extend_schema_view(**generate_object_with_permissions_schema(StoragePathSerializer))
class StoragePathViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet): class StoragePathViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[StoragePath]):
model = StoragePath model = StoragePath
queryset = StoragePath.objects.select_related("owner").order_by( queryset = StoragePath.objects.select_related("owner").order_by(
@@ -3481,7 +3490,7 @@ class StoragePathViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet):
return Response(result) return Response(result)
class UiSettingsView(GenericAPIView): class UiSettingsView(GenericAPIView[Any]):
queryset = UiSettings.objects.all() queryset = UiSettings.objects.all()
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = UiSettingsViewSerializer serializer_class = UiSettingsViewSerializer
@@ -3579,7 +3588,7 @@ class UiSettingsView(GenericAPIView):
}, },
), ),
) )
class RemoteVersionView(GenericAPIView): class RemoteVersionView(GenericAPIView[Any]):
cache_key = "remote_version_view_latest_release" cache_key = "remote_version_view_latest_release"
def get(self, request, format=None): def get(self, request, format=None):
@@ -3656,7 +3665,7 @@ class RemoteVersionView(GenericAPIView):
), ),
], ],
) )
class TasksViewSet(ReadOnlyModelViewSet): class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = TasksViewSerializer serializer_class = TasksViewSerializer
filter_backends = ( filter_backends = (
@@ -3730,7 +3739,7 @@ class TasksViewSet(ReadOnlyModelViewSet):
) )
class ShareLinkViewSet(ModelViewSet, PassUserMixin): class ShareLinkViewSet(PassUserMixin, ModelViewSet[ShareLink]):
model = ShareLink model = ShareLink
queryset = ShareLink.objects.all() queryset = ShareLink.objects.all()
@@ -3747,7 +3756,7 @@ class ShareLinkViewSet(ModelViewSet, PassUserMixin):
ordering_fields = ("created", "expiration", "document") ordering_fields = ("created", "expiration", "document")
class ShareLinkBundleViewSet(ModelViewSet, PassUserMixin): class ShareLinkBundleViewSet(PassUserMixin, ModelViewSet[ShareLinkBundle]):
model = ShareLinkBundle model = ShareLinkBundle
queryset = ShareLinkBundle.objects.all() queryset = ShareLinkBundle.objects.all()
@@ -4104,7 +4113,7 @@ class BulkEditObjectsView(PassUserMixin):
return Response({"result": "OK"}) return Response({"result": "OK"})
class WorkflowTriggerViewSet(ModelViewSet): class WorkflowTriggerViewSet(ModelViewSet[WorkflowTrigger]):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = WorkflowTriggerSerializer serializer_class = WorkflowTriggerSerializer
@@ -4122,7 +4131,7 @@ class WorkflowTriggerViewSet(ModelViewSet):
return super().partial_update(request, *args, **kwargs) return super().partial_update(request, *args, **kwargs)
class WorkflowActionViewSet(ModelViewSet): class WorkflowActionViewSet(ModelViewSet[WorkflowAction]):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = WorkflowActionSerializer serializer_class = WorkflowActionSerializer
@@ -4147,7 +4156,7 @@ class WorkflowActionViewSet(ModelViewSet):
return super().partial_update(request, *args, **kwargs) return super().partial_update(request, *args, **kwargs)
class WorkflowViewSet(ModelViewSet): class WorkflowViewSet(ModelViewSet[Workflow]):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = WorkflowSerializer serializer_class = WorkflowSerializer
@@ -4165,7 +4174,7 @@ class WorkflowViewSet(ModelViewSet):
) )
class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet): class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[CustomField]):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = CustomFieldSerializer serializer_class = CustomFieldSerializer

File diff suppressed because it is too large Load Diff

View File

@@ -74,7 +74,7 @@ class PaperlessAuthTokenSerializer(AuthTokenSerializer):
return attrs return attrs
class UserSerializer(PasswordValidationMixin, serializers.ModelSerializer): class UserSerializer(PasswordValidationMixin, serializers.ModelSerializer[User]):
password = ObfuscatedPasswordField(required=False) password = ObfuscatedPasswordField(required=False)
user_permissions = serializers.SlugRelatedField( user_permissions = serializers.SlugRelatedField(
many=True, many=True,
@@ -142,7 +142,7 @@ class UserSerializer(PasswordValidationMixin, serializers.ModelSerializer):
return user return user
class GroupSerializer(serializers.ModelSerializer): class GroupSerializer(serializers.ModelSerializer[Group]):
permissions = serializers.SlugRelatedField( permissions = serializers.SlugRelatedField(
many=True, many=True,
queryset=Permission.objects.exclude(content_type__app_label="admin"), queryset=Permission.objects.exclude(content_type__app_label="admin"),
@@ -158,7 +158,7 @@ class GroupSerializer(serializers.ModelSerializer):
) )
class SocialAccountSerializer(serializers.ModelSerializer): class SocialAccountSerializer(serializers.ModelSerializer[SocialAccount]):
name = serializers.SerializerMethodField() name = serializers.SerializerMethodField()
class Meta: class Meta:
@@ -176,7 +176,7 @@ class SocialAccountSerializer(serializers.ModelSerializer):
return "Unknown App" return "Unknown App"
class ProfileSerializer(PasswordValidationMixin, serializers.ModelSerializer): class ProfileSerializer(PasswordValidationMixin, serializers.ModelSerializer[User]):
email = serializers.EmailField(allow_blank=True, required=False) email = serializers.EmailField(allow_blank=True, required=False)
password = ObfuscatedPasswordField(required=False, allow_null=False) password = ObfuscatedPasswordField(required=False, allow_null=False)
auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key") auth_token = serializers.SlugRelatedField(read_only=True, slug_field="key")
@@ -209,7 +209,9 @@ class ProfileSerializer(PasswordValidationMixin, serializers.ModelSerializer):
) )
class ApplicationConfigurationSerializer(serializers.ModelSerializer): class ApplicationConfigurationSerializer(
serializers.ModelSerializer[ApplicationConfiguration],
):
user_args = serializers.JSONField(binary=True, allow_null=True) user_args = serializers.JSONField(binary=True, allow_null=True)
barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True) barcode_tag_mapping = serializers.JSONField(binary=True, allow_null=True)
llm_api_key = ObfuscatedPasswordField( llm_api_key = ObfuscatedPasswordField(

View File

@@ -1,5 +1,6 @@
from collections import OrderedDict from collections import OrderedDict
from pathlib import Path from pathlib import Path
from typing import Any
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
@@ -114,7 +115,7 @@ class FaviconView(View):
return HttpResponseNotFound("favicon.ico not found") return HttpResponseNotFound("favicon.ico not found")
class UserViewSet(ModelViewSet): class UserViewSet(ModelViewSet[User]):
_BOOL_NOT_PROVIDED = object() _BOOL_NOT_PROVIDED = object()
model = User model = User
@@ -216,7 +217,7 @@ class UserViewSet(ModelViewSet):
return HttpResponseNotFound("TOTP not found") return HttpResponseNotFound("TOTP not found")
class GroupViewSet(ModelViewSet): class GroupViewSet(ModelViewSet[Group]):
model = Group model = Group
queryset = Group.objects.order_by(Lower("name")) queryset = Group.objects.order_by(Lower("name"))
@@ -229,7 +230,7 @@ class GroupViewSet(ModelViewSet):
ordering_fields = ("name",) ordering_fields = ("name",)
class ProfileView(GenericAPIView): class ProfileView(GenericAPIView[Any]):
""" """
User profile view, only available when logged in User profile view, only available when logged in
""" """
@@ -288,7 +289,7 @@ class ProfileView(GenericAPIView):
}, },
), ),
) )
class TOTPView(GenericAPIView): class TOTPView(GenericAPIView[Any]):
""" """
TOTP views TOTP views
""" """
@@ -368,7 +369,7 @@ class TOTPView(GenericAPIView):
}, },
), ),
) )
class GenerateAuthTokenView(GenericAPIView): class GenerateAuthTokenView(GenericAPIView[Any]):
""" """
Generates (or re-generates) an auth token, requires a logged in user Generates (or re-generates) an auth token, requires a logged in user
unlike the default DRF endpoint unlike the default DRF endpoint
@@ -397,7 +398,7 @@ class GenerateAuthTokenView(GenericAPIView):
}, },
), ),
) )
class ApplicationConfigurationViewSet(ModelViewSet): class ApplicationConfigurationViewSet(ModelViewSet[ApplicationConfiguration]):
model = ApplicationConfiguration model = ApplicationConfiguration
queryset = ApplicationConfiguration.objects queryset = ApplicationConfiguration.objects
@@ -450,7 +451,7 @@ class ApplicationConfigurationViewSet(ModelViewSet):
}, },
), ),
) )
class DisconnectSocialAccountView(GenericAPIView): class DisconnectSocialAccountView(GenericAPIView[Any]):
""" """
Disconnects a social account provider from the user account Disconnects a social account provider from the user account
""" """
@@ -476,7 +477,7 @@ class DisconnectSocialAccountView(GenericAPIView):
}, },
), ),
) )
class SocialAccountProvidersView(GenericAPIView): class SocialAccountProvidersView(GenericAPIView[Any]):
""" """
List of social account providers List of social account providers
""" """

View File

@@ -57,7 +57,7 @@ class MailAccountSerializer(OwnedObjectSerializer):
return instance return instance
class AccountField(serializers.PrimaryKeyRelatedField): class AccountField(serializers.PrimaryKeyRelatedField[MailAccount]):
def get_queryset(self): def get_queryset(self):
return MailAccount.objects.all().order_by("-id") return MailAccount.objects.all().order_by("-id")

View File

@@ -1,6 +1,7 @@
import datetime import datetime
import logging import logging
from datetime import timedelta from datetime import timedelta
from typing import Any
from django.http import HttpResponseBadRequest from django.http import HttpResponseBadRequest
from django.http import HttpResponseForbidden from django.http import HttpResponseForbidden
@@ -65,7 +66,7 @@ from paperless_mail.tasks import process_mail_accounts
}, },
), ),
) )
class MailAccountViewSet(ModelViewSet, PassUserMixin): class MailAccountViewSet(PassUserMixin, ModelViewSet[MailAccount]):
model = MailAccount model = MailAccount
queryset = MailAccount.objects.all().order_by("pk") queryset = MailAccount.objects.all().order_by("pk")
@@ -159,7 +160,7 @@ class MailAccountViewSet(ModelViewSet, PassUserMixin):
return Response({"result": "OK"}) return Response({"result": "OK"})
class ProcessedMailViewSet(ReadOnlyModelViewSet, PassUserMixin): class ProcessedMailViewSet(PassUserMixin, ReadOnlyModelViewSet[ProcessedMail]):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions) permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = ProcessedMailSerializer serializer_class = ProcessedMailSerializer
pagination_class = StandardPagination pagination_class = StandardPagination
@@ -187,7 +188,7 @@ class ProcessedMailViewSet(ReadOnlyModelViewSet, PassUserMixin):
return Response({"result": "OK", "deleted_mail_ids": mail_ids}) return Response({"result": "OK", "deleted_mail_ids": mail_ids})
class MailRuleViewSet(ModelViewSet, PassUserMixin): class MailRuleViewSet(PassUserMixin, ModelViewSet[MailRule]):
model = MailRule model = MailRule
queryset = MailRule.objects.all().order_by("order") queryset = MailRule.objects.all().order_by("order")
@@ -203,7 +204,7 @@ class MailRuleViewSet(ModelViewSet, PassUserMixin):
responses={200: None}, responses={200: None},
), ),
) )
class OauthCallbackView(GenericAPIView): class OauthCallbackView(GenericAPIView[Any]):
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
def get(self, request, format=None): def get(self, request, format=None):

8
uv.lock generated
View File

@@ -875,15 +875,15 @@ wheels = [
[[package]] [[package]]
name = "django" name = "django"
version = "5.2.12" version = "5.2.13"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "asgiref", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sqlparse", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/bd/55/b9445fc0695b03746f355c05b2eecc54c34e05198c686f4fc4406b722b52/django-5.2.12.tar.gz", hash = "sha256:6b809af7165c73eff5ce1c87fdae75d4da6520d6667f86401ecf55b681eb1eeb", size = 10860574, upload-time = "2026-03-03T13:56:05.509Z" } sdist = { url = "https://files.pythonhosted.org/packages/1f/c5/c69e338eb2959f641045802e5ea87ca4bf5ac90c5fd08953ca10742fad51/django-5.2.13.tar.gz", hash = "sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4", size = 10890368, upload-time = "2026-04-07T14:02:15.072Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/4e/32/4b144e125678efccf5d5b61581de1c4088d6b0286e46096e3b8de0d556c8/django-5.2.12-py3-none-any.whl", hash = "sha256:4853482f395c3a151937f6991272540fcbf531464f254a347bf7c89f53c8cff7", size = 8310245, upload-time = "2026-03-03T13:56:01.174Z" }, { url = "https://files.pythonhosted.org/packages/59/b1/51ab36b2eefcf8cdb9338c7188668a157e29e30306bfc98a379704c9e10d/django-5.2.13-py3-none-any.whl", hash = "sha256:5788fce61da23788a8ce6f02583765ab060d396720924789f97fa42119d37f7a", size = 8310982, upload-time = "2026-04-07T14:02:08.883Z" },
] ]
[[package]] [[package]]
@@ -3014,7 +3014,7 @@ requires-dist = [
{ name = "channels-redis", specifier = "~=4.2" }, { name = "channels-redis", specifier = "~=4.2" },
{ name = "concurrent-log-handler", specifier = "~=0.9.25" }, { name = "concurrent-log-handler", specifier = "~=0.9.25" },
{ name = "dateparser", specifier = "~=1.2" }, { name = "dateparser", specifier = "~=1.2" },
{ name = "django", specifier = "~=5.2.10" }, { name = "django", specifier = "~=5.2.13" },
{ name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.15.0" }, { name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.15.0" },
{ name = "django-auditlog", specifier = "~=3.4.1" }, { name = "django-auditlog", specifier = "~=3.4.1" },
{ name = "django-cachalot", specifier = "~=2.9.0" }, { name = "django-cachalot", specifier = "~=2.9.0" },