Compare commits

..

1 Commits

14 changed files with 7838 additions and 6698 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

@@ -650,6 +650,10 @@ 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,7 +381,10 @@ class Document(SoftDeleteModel, ModelWithOwner): # type: ignore[django-manager-
if isinstance(prefetched_cache, dict)
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)
return latest_prefetched.content

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

@@ -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,7 +1248,10 @@ class DocumentViewSet(
),
)
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(
request.user,
"view_document",
@@ -1958,7 +1955,7 @@ class ChatStreamingSerializer(serializers.Serializer):
],
name="dispatch",
)
class ChatStreamingView(GenericAPIView[Any]):
class ChatStreamingView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = ChatStreamingSerializer
@@ -2284,7 +2281,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(
@@ -2762,7 +2759,7 @@ class RemovePasswordDocumentsView(DocumentOperationPermissionMixin):
},
),
)
class PostDocumentView(GenericAPIView[Any]):
class PostDocumentView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = PostDocumentSerializer
parser_classes = (parsers.MultiPartParser,)
@@ -2883,7 +2880,7 @@ class PostDocumentView(GenericAPIView[Any]):
},
),
)
class SelectionDataView(GenericAPIView[Any]):
class SelectionDataView(GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = DocumentListSerializer
parser_classes = (parsers.MultiPartParser, parsers.JSONParser)
@@ -2987,7 +2984,7 @@ class SelectionDataView(GenericAPIView[Any]):
},
),
)
class SearchAutoCompleteView(GenericAPIView[Any]):
class SearchAutoCompleteView(GenericAPIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
@@ -3268,7 +3265,7 @@ class GlobalSearchView(PassUserMixin):
},
),
)
class StatisticsView(GenericAPIView[Any]):
class StatisticsView(GenericAPIView):
permission_classes = (IsAuthenticated,)
def get(self, request, format=None):
@@ -3370,7 +3367,7 @@ class StatisticsView(GenericAPIView[Any]):
)
class BulkDownloadView(DocumentSelectionMixin, GenericAPIView[Any]):
class BulkDownloadView(DocumentSelectionMixin, GenericAPIView):
permission_classes = (IsAuthenticated,)
serializer_class = BulkDownloadSerializer
parser_classes = (parsers.JSONParser,)
@@ -3423,7 +3420,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(
@@ -3487,7 +3484,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
@@ -3585,7 +3582,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):
@@ -3662,7 +3659,7 @@ class RemoteVersionView(GenericAPIView[Any]):
),
],
)
class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
class TasksViewSet(ReadOnlyModelViewSet):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = TasksViewSerializer
filter_backends = (
@@ -3736,7 +3733,7 @@ class TasksViewSet(ReadOnlyModelViewSet[PaperlessTask]):
)
class ShareLinkViewSet(PassUserMixin, ModelViewSet[ShareLink]):
class ShareLinkViewSet(ModelViewSet, PassUserMixin):
model = ShareLink
queryset = ShareLink.objects.all()
@@ -3753,7 +3750,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()
@@ -4110,7 +4107,7 @@ class BulkEditObjectsView(PassUserMixin):
return Response({"result": "OK"})
class WorkflowTriggerViewSet(ModelViewSet[WorkflowTrigger]):
class WorkflowTriggerViewSet(ModelViewSet):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = WorkflowTriggerSerializer
@@ -4128,7 +4125,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
@@ -4153,7 +4150,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
@@ -4171,7 +4168,7 @@ class WorkflowViewSet(ModelViewSet[Workflow]):
)
class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet[CustomField]):
class CustomFieldViewSet(PermissionsAwareDocumentCountMixin, ModelViewSet):
permission_classes = (IsAuthenticated, PaperlessObjectPermissions)
serializer_class = CustomFieldSerializer

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):