Compare commits

..

1 Commits

20 changed files with 8192 additions and 7033 deletions

View File

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

View File

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

View File

@@ -36,8 +36,6 @@ jobs:
--group dev \
--frozen
- 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*"
- name: Install pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

3
.gitignore vendored
View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -241,3 +241,66 @@ For example:
}
}
```
## Consume Script Positional Arguments Removed
Pre- and post-consumption scripts no longer receive positional arguments. All information is
now passed exclusively via environment variables, which have been available since earlier versions.
### Pre-consumption script
Previously, the original file path was passed as `$1`. It is now only available as
`DOCUMENT_SOURCE_PATH`.
**Before:**
```bash
#!/usr/bin/env bash
# $1 was the original file path
process_document "$1"
```
**After:**
```bash
#!/usr/bin/env bash
process_document "${DOCUMENT_SOURCE_PATH}"
```
### Post-consumption script
Previously, document metadata was passed as positional arguments `$1` through `$8`:
| Argument | Environment Variable Equivalent |
| -------- | ------------------------------- |
| `$1` | `DOCUMENT_ID` |
| `$2` | `DOCUMENT_FILE_NAME` |
| `$3` | `DOCUMENT_SOURCE_PATH` |
| `$4` | `DOCUMENT_THUMBNAIL_PATH` |
| `$5` | `DOCUMENT_DOWNLOAD_URL` |
| `$6` | `DOCUMENT_THUMBNAIL_URL` |
| `$7` | `DOCUMENT_CORRESPONDENT` |
| `$8` | `DOCUMENT_TAGS` |
**Before:**
```bash
#!/usr/bin/env bash
DOCUMENT_ID=$1
CORRESPONDENT=$7
TAGS=$8
```
**After:**
```bash
#!/usr/bin/env bash
# Use environment variables directly
echo "Document ${DOCUMENT_ID} from ${DOCUMENT_CORRESPONDENT} tagged: ${DOCUMENT_TAGS}"
```
### Action Required
Update any pre- or post-consumption scripts that read `$1`, `$2`, etc. to use the
corresponding environment variables instead. Environment variables have been the preferred
option since v1.8.0.

View File

@@ -24,7 +24,7 @@ dependencies = [
"dateparser~=1.2",
# WARNING: django does not use semver.
# Only patch versions are guaranteed to not introduce breaking changes.
"django~=5.2.13",
"django~=5.2.10",
"django-allauth[mfa,socialaccount]~=65.15.0",
"django-auditlog~=3.4.1",
"django-cachalot~=2.9.0",
@@ -113,7 +113,7 @@ testing = [
"factory-boy~=3.3.1",
"faker~=40.12.0",
"imagehash",
"pytest~=9.0.3",
"pytest~=9.0.0",
"pytest-cov~=7.1.0",
"pytest-django~=4.12.0",
"pytest-env~=1.6.0",

View File

@@ -313,7 +313,6 @@ class ConsumerPlugin(
run_subprocess(
[
settings.PRE_CONSUME_SCRIPT,
original_file_path,
],
script_env,
self.log,
@@ -383,14 +382,6 @@ class ConsumerPlugin(
run_subprocess(
[
settings.POST_CONSUME_SCRIPT,
str(document.pk),
document.get_public_filename(),
os.path.normpath(document.source_path),
os.path.normpath(document.thumbnail_path),
reverse("document-download", kwargs={"pk": document.pk}),
reverse("document-thumb", kwargs={"pk": document.pk}),
str(document.correspondent),
str(",".join(document.tags.all().values_list("name", flat=True))),
],
script_env,
self.log,
@@ -650,10 +641,6 @@ class ConsumerPlugin(
# If we get here, it was successful. Proceed with post-consume
# hooks. If they fail, nothing will get changed.
document = Document.objects.prefetch_related("versions").get(
pk=document.pk,
)
document_consumption_finished.send(
sender=self.__class__,
document=document,

View File

@@ -381,10 +381,7 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
if isinstance(prefetched_cache, dict)
else None
)
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
if prefetched_versions:
latest_prefetched = max(prefetched_versions, key=lambda doc: doc.id)
return latest_prefetched.content

View File

@@ -182,9 +182,8 @@ def _check_thumbnail(
present_files: set[Path],
) -> None:
"""Verify the thumbnail exists and is readable."""
# doc.thumbnail_path already returns a resolved Path; no need to re-resolve.
thumbnail_path: Final[Path] = doc.thumbnail_path
if not thumbnail_path.is_file():
thumbnail_path: Final[Path] = Path(doc.thumbnail_path).resolve()
if not thumbnail_path.exists() or not thumbnail_path.is_file():
messages.error(doc.pk, "Thumbnail of document does not exist.")
return
@@ -201,9 +200,8 @@ def _check_original(
present_files: set[Path],
) -> None:
"""Verify the original file exists, is readable, and has matching checksum."""
# doc.source_path already returns a resolved Path; no need to re-resolve.
source_path: Final[Path] = doc.source_path
if not source_path.is_file():
source_path: Final[Path] = Path(doc.source_path).resolve()
if not source_path.exists() or not source_path.is_file():
messages.error(doc.pk, "Original of document does not exist.")
return
@@ -239,9 +237,8 @@ def _check_archive(
elif doc.has_archive_version:
if TYPE_CHECKING:
assert isinstance(doc.archive_path, Path)
# doc.archive_path already returns a resolved Path; no need to re-resolve.
archive_path: Final[Path] = doc.archive_path # type: ignore[assignment]
if not archive_path.is_file():
archive_path: Final[Path] = Path(doc.archive_path).resolve()
if not archive_path.exists() or not archive_path.is_file():
messages.error(doc.pk, "Archived version of document does not exist.")
return
@@ -317,15 +314,7 @@ def check_sanity(
messages = SanityCheckMessages()
present_files = _build_present_files()
documents = Document.global_objects.only(
"pk",
"filename",
"mime_type",
"checksum",
"archive_checksum",
"archive_filename",
"content",
).iterator(chunk_size=500)
documents = Document.global_objects.all()
for doc in iter_wrapper(documents):
_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
class DynamicFieldsModelSerializer(serializers.ModelSerializer[Any]):
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
@@ -121,7 +121,7 @@ class DynamicFieldsModelSerializer(serializers.ModelSerializer[Any]):
self.fields.pop(field_name)
class MatchingModelSerializer(serializers.ModelSerializer[Any]):
class MatchingModelSerializer(serializers.ModelSerializer):
document_count = serializers.IntegerField(read_only=True)
def get_slug(self, obj) -> str:
@@ -261,7 +261,7 @@ class SetPermissionsSerializer(serializers.DictField):
class OwnedObjectSerializer(
SerializerWithPerms,
serializers.ModelSerializer[Any],
serializers.ModelSerializer,
SetPermissionsMixin,
):
def __init__(self, *args, **kwargs) -> None:
@@ -469,7 +469,7 @@ class OwnedObjectSerializer(
return super().update(instance, validated_data)
class OwnedObjectListSerializer(serializers.ListSerializer[Any]):
class OwnedObjectListSerializer(serializers.ListSerializer):
def to_representation(self, documents):
self.child.context["shared_object_pks"] = self.child.get_shared_object_pks(
documents,
@@ -682,27 +682,27 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer):
return super().validate(attrs)
class CorrespondentField(serializers.PrimaryKeyRelatedField[Correspondent]):
class CorrespondentField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return Correspondent.objects.all()
class TagsField(serializers.PrimaryKeyRelatedField[Tag]):
class TagsField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return Tag.objects.all()
class DocumentTypeField(serializers.PrimaryKeyRelatedField[DocumentType]):
class DocumentTypeField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return DocumentType.objects.all()
class StoragePathField(serializers.PrimaryKeyRelatedField[StoragePath]):
class StoragePathField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
return StoragePath.objects.all()
class CustomFieldSerializer(serializers.ModelSerializer[CustomField]):
class CustomFieldSerializer(serializers.ModelSerializer):
data_type = serializers.ChoiceField(
choices=CustomField.FieldDataType,
read_only=False,
@@ -816,7 +816,7 @@ def validate_documentlink_targets(user, doc_ids):
)
class CustomFieldInstanceSerializer(serializers.ModelSerializer[CustomFieldInstance]):
class CustomFieldInstanceSerializer(serializers.ModelSerializer):
field = serializers.PrimaryKeyRelatedField(queryset=CustomField.objects.all())
value = ReadWriteSerializerMethodField(allow_null=True)
@@ -922,14 +922,14 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer[CustomFieldInsta
]
class BasicUserSerializer(serializers.ModelSerializer[User]):
class BasicUserSerializer(serializers.ModelSerializer):
# Different than paperless.serializers.UserSerializer
class Meta:
model = User
fields = ["id", "username", "first_name", "last_name"]
class NotesSerializer(serializers.ModelSerializer[Note]):
class NotesSerializer(serializers.ModelSerializer):
user = BasicUserSerializer(read_only=True)
class Meta:
@@ -1256,7 +1256,7 @@ class DocumentSerializer(
list_serializer_class = OwnedObjectListSerializer
class SearchResultListSerializer(serializers.ListSerializer[Document]):
class SearchResultListSerializer(serializers.ListSerializer):
def to_representation(self, hits):
document_ids = [hit["id"] for hit in hits]
# Fetch all Document objects in the list in one SQL query.
@@ -1313,7 +1313,7 @@ class SearchResultSerializer(DocumentSerializer):
list_serializer_class = SearchResultListSerializer
class SavedViewFilterRuleSerializer(serializers.ModelSerializer[SavedViewFilterRule]):
class SavedViewFilterRuleSerializer(serializers.ModelSerializer):
class Meta:
model = SavedViewFilterRule
fields = ["rule_type", "value"]
@@ -2401,7 +2401,7 @@ class StoragePathSerializer(MatchingModelSerializer, OwnedObjectSerializer):
return super().update(instance, validated_data)
class UiSettingsViewSerializer(serializers.ModelSerializer[UiSettings]):
class UiSettingsViewSerializer(serializers.ModelSerializer):
settings = serializers.DictField(required=False, allow_null=True)
class Meta:
@@ -2760,7 +2760,7 @@ class BulkEditObjectsSerializer(SerializerWithPerms, SetPermissionsMixin):
return attrs
class WorkflowTriggerSerializer(serializers.ModelSerializer[WorkflowTrigger]):
class WorkflowTriggerSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False, allow_null=True)
sources = fields.MultipleChoiceField(
choices=WorkflowTrigger.DocumentSourceChoices.choices,
@@ -2870,7 +2870,7 @@ class WorkflowTriggerSerializer(serializers.ModelSerializer[WorkflowTrigger]):
return super().update(instance, validated_data)
class WorkflowActionEmailSerializer(serializers.ModelSerializer[WorkflowActionEmail]):
class WorkflowActionEmailSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(allow_null=True, required=False)
class Meta:
@@ -2884,9 +2884,7 @@ class WorkflowActionEmailSerializer(serializers.ModelSerializer[WorkflowActionEm
]
class WorkflowActionWebhookSerializer(
serializers.ModelSerializer[WorkflowActionWebhook],
):
class WorkflowActionWebhookSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(allow_null=True, required=False)
def validate_url(self, url):
@@ -2907,7 +2905,7 @@ class WorkflowActionWebhookSerializer(
]
class WorkflowActionSerializer(serializers.ModelSerializer[WorkflowAction]):
class WorkflowActionSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(required=False, allow_null=True)
assign_correspondent = CorrespondentField(allow_null=True, required=False)
assign_tags = TagsField(many=True, allow_null=True, required=False)
@@ -3029,7 +3027,7 @@ class WorkflowActionSerializer(serializers.ModelSerializer[WorkflowAction]):
return attrs
class WorkflowSerializer(serializers.ModelSerializer[Workflow]):
class WorkflowSerializer(serializers.ModelSerializer):
order = serializers.IntegerField(required=False)
triggers = WorkflowTriggerSerializer(many=True)

View File

@@ -1328,7 +1328,7 @@ class PreConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
environment = args[1]
self.assertEqual(command[0], script.name)
self.assertEqual(command[1], str(self.test_file))
self.assertEqual(len(command), 1)
subset = {
"DOCUMENT_SOURCE_PATH": str(c.input_doc.original_file),
@@ -1478,11 +1478,7 @@ class PostConsumeTestCase(DirectoriesMixin, GetConsumerMixin, TestCase):
environment = args[1]
self.assertEqual(command[0], script.name)
self.assertEqual(command[1], str(doc.pk))
self.assertEqual(command[5], f"/api/documents/{doc.pk}/download/")
self.assertEqual(command[6], f"/api/documents/{doc.pk}/thumb/")
self.assertEqual(command[7], "my_bank")
self.assertCountEqual(command[8].split(","), ["a", "b"])
self.assertEqual(len(command), 1)
subset = {
"DOCUMENT_ID": str(doc.pk),

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

18
uv.lock generated
View File

@@ -875,15 +875,15 @@ wheels = [
[[package]]
name = "django"
version = "5.2.13"
version = "5.2.12"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "asgiref", 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/1f/c5/c69e338eb2959f641045802e5ea87ca4bf5ac90c5fd08953ca10742fad51/django-5.2.13.tar.gz", hash = "sha256:a31589db5188d074c63f0945c3888fad104627dfcc236fb2b97f71f89da33bc4", size = 10890368, upload-time = "2026-04-07T14:02:15.072Z" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]
@@ -3014,7 +3014,7 @@ requires-dist = [
{ name = "channels-redis", specifier = "~=4.2" },
{ name = "concurrent-log-handler", specifier = "~=0.9.25" },
{ name = "dateparser", specifier = "~=1.2" },
{ name = "django", specifier = "~=5.2.13" },
{ name = "django", specifier = "~=5.2.10" },
{ name = "django-allauth", extras = ["mfa", "socialaccount"], specifier = "~=65.15.0" },
{ name = "django-auditlog", specifier = "~=3.4.1" },
{ name = "django-cachalot", specifier = "~=2.9.0" },
@@ -3087,7 +3087,7 @@ dev = [
{ name = "faker", specifier = "~=40.12.0" },
{ name = "imagehash" },
{ name = "prek", specifier = "~=0.3.0" },
{ name = "pytest", specifier = "~=9.0.3" },
{ name = "pytest", specifier = "~=9.0.0" },
{ name = "pytest-cov", specifier = "~=7.1.0" },
{ name = "pytest-django", specifier = "~=4.12.0" },
{ name = "pytest-env", specifier = "~=1.6.0" },
@@ -3110,7 +3110,7 @@ testing = [
{ name = "factory-boy", specifier = "~=3.3.1" },
{ name = "faker", specifier = "~=40.12.0" },
{ name = "imagehash" },
{ name = "pytest", specifier = "~=9.0.3" },
{ name = "pytest", specifier = "~=9.0.0" },
{ name = "pytest-cov", specifier = "~=7.1.0" },
{ name = "pytest-django", specifier = "~=4.12.0" },
{ name = "pytest-env", specifier = "~=1.6.0" },
@@ -3769,7 +3769,7 @@ wheels = [
[[package]]
name = "pytest"
version = "9.0.3"
version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "iniconfig", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -3777,9 +3777,9 @@ dependencies = [
{ name = "pluggy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "pygments", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[package]]