249 lines
8.1 KiB
Python
249 lines
8.1 KiB
Python
from collections import OrderedDict
|
|
from typing import Any, Dict, Iterable, Optional, Type
|
|
|
|
from django.http import Http404
|
|
from rest_framework import status
|
|
from rest_framework.decorators import detail_route, list_route
|
|
from rest_framework.metadata import SimpleMetadata
|
|
from rest_framework.mixins import (
|
|
CreateModelMixin,
|
|
DestroyModelMixin,
|
|
ListModelMixin as _ListModelMixin,
|
|
RetrieveModelMixin as _RetrieveModelMixin,
|
|
UpdateModelMixin,
|
|
)
|
|
from rest_framework.relations import MANY_RELATION_KWARGS
|
|
from rest_framework.response import Response
|
|
from rest_framework.routers import DefaultRouter
|
|
from rest_framework.serializers import (
|
|
CharField,
|
|
DictField,
|
|
Field,
|
|
FileField,
|
|
IntegerField,
|
|
JSONField,
|
|
ListField,
|
|
ListSerializer,
|
|
ManyRelatedField,
|
|
ModelSerializer as _ModelSerializer,
|
|
PrimaryKeyRelatedField,
|
|
RelatedField,
|
|
Serializer,
|
|
SerializerMethodField,
|
|
ValidationError,
|
|
)
|
|
from rest_framework.viewsets import (
|
|
GenericViewSet as _GenericViewSet,
|
|
ModelViewSet as _ModelViewSet,
|
|
ViewSet as _ViewSet,
|
|
)
|
|
|
|
from .access_permissions import BaseAccessPermissions
|
|
from .auth import user_to_collection_user
|
|
from .collection import Collection, CollectionElement
|
|
|
|
|
|
__all__ = ['status', 'detail_route', 'list_route', 'SimpleMetadata', 'CreateModelMixin',
|
|
'DestroyModelMixin', 'UpdateModelMixin', 'CharField', 'DictField', 'FileField',
|
|
'IntegerField', 'JSONField', 'ListField', 'ListSerializer', 'RelatedField',
|
|
'SerializerMethodField', 'ValidationError']
|
|
|
|
|
|
router = DefaultRouter()
|
|
|
|
|
|
class IdManyRelatedField(ManyRelatedField):
|
|
"""
|
|
ManyRelatedField that appends an suffix to the sub-fields.
|
|
|
|
Only works together with the IdPrimaryKeyRelatedField and our
|
|
ModelSerializer.
|
|
"""
|
|
field_name_suffix = '_id'
|
|
|
|
def bind(self, field_name: str, parent: Any) -> None:
|
|
"""
|
|
Called when the field is bound to the serializer.
|
|
|
|
See IdPrimaryKeyRelatedField for more informations.
|
|
"""
|
|
self.source = field_name[:-len(self.field_name_suffix)]
|
|
super().bind(field_name, parent)
|
|
|
|
|
|
class IdPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
|
"""
|
|
Field, that renames the field name to FIELD_NAME_id.
|
|
|
|
Only works together the our ModelSerializer.
|
|
"""
|
|
field_name_suffix = '_id'
|
|
|
|
def bind(self, field_name: str, parent: Any) -> None:
|
|
"""
|
|
Called when the field is bound to the serializer.
|
|
|
|
Changes the source so that the original field name is used (removes
|
|
the _id suffix).
|
|
"""
|
|
if field_name:
|
|
# field_name is an empty string when the field is created with the
|
|
# attribute many=True. In this case the suffix is added with the
|
|
# IdManyRelatedField class.
|
|
self.source = field_name[:-len(self.field_name_suffix)]
|
|
super().bind(field_name, parent)
|
|
|
|
@classmethod
|
|
def many_init(cls, *args: Any, **kwargs: Any) -> IdManyRelatedField:
|
|
"""
|
|
Method from rest_framework.relations.RelatedField That uses our
|
|
IdManyRelatedField class instead of
|
|
rest_framework.relations.ManyRelatedField class.
|
|
"""
|
|
list_kwargs = {'child_relation': cls(*args, **kwargs)}
|
|
for key in kwargs.keys():
|
|
if key in MANY_RELATION_KWARGS:
|
|
list_kwargs[key] = kwargs[key]
|
|
return IdManyRelatedField(**list_kwargs)
|
|
|
|
|
|
class PermissionMixin:
|
|
"""
|
|
Mixin for subclasses of APIView like GenericViewSet and ModelViewSet.
|
|
|
|
The method check_view_permissions is evaluated. If it returns False
|
|
self.permission_denied() is called. Django REST Framework's permission
|
|
system is disabled.
|
|
|
|
Also connects container to handle access permissions for model and
|
|
viewset.
|
|
"""
|
|
access_permissions: Optional[BaseAccessPermissions] = None
|
|
|
|
def get_permissions(self) -> Iterable[str]:
|
|
"""
|
|
Overridden method to check view permissions. Returns an empty
|
|
iterable so Django REST framework won't do any other permission
|
|
checks by evaluating Django REST framework style permission classes
|
|
and the request passes.
|
|
"""
|
|
if not self.check_view_permissions():
|
|
self.permission_denied(self.request) # type: ignore
|
|
return ()
|
|
|
|
def check_view_permissions(self) -> bool:
|
|
"""
|
|
Override this and return True if the requesting user should be able to
|
|
get access to your view.
|
|
|
|
Don't forget to use access permissions container for list and retrieve
|
|
requests.
|
|
"""
|
|
return False
|
|
|
|
def get_access_permissions(self) -> BaseAccessPermissions:
|
|
"""
|
|
Returns a container to handle access permissions for this viewset and
|
|
its corresponding model.
|
|
"""
|
|
return self.access_permissions # type: ignore
|
|
|
|
def get_serializer_class(self) -> Type[Serializer]:
|
|
"""
|
|
Overridden method to return the serializer class given by the
|
|
access permissions container.
|
|
"""
|
|
if self.get_access_permissions() is not None:
|
|
serializer_class = self.get_access_permissions().get_serializer_class(self.request.user) # type: ignore
|
|
else:
|
|
serializer_class = super().get_serializer_class() # type: ignore
|
|
return serializer_class
|
|
|
|
|
|
class ModelSerializer(_ModelSerializer):
|
|
"""
|
|
ModelSerializer that changes the field names of related fields to
|
|
FIELD_NAME_id.
|
|
"""
|
|
serializer_related_field = IdPrimaryKeyRelatedField
|
|
|
|
def get_fields(self) -> Any:
|
|
"""
|
|
Returns all fields of the serializer.
|
|
"""
|
|
fields: Dict[str, Field] = OrderedDict()
|
|
|
|
for field_name, field in super().get_fields().items():
|
|
try:
|
|
field_name += field.field_name_suffix
|
|
except AttributeError:
|
|
pass
|
|
fields[field_name] = field
|
|
return fields
|
|
|
|
|
|
class ListModelMixin(_ListModelMixin):
|
|
"""
|
|
Mixin to add the caching system to list requests.
|
|
|
|
It is not allowed to use the method get_queryset() in derivated classes.
|
|
The attribute queryset has to be used in the following form:
|
|
|
|
queryset = Model.objects.all()
|
|
"""
|
|
def list(self, request: Any, *args: Any, **kwargs: Any) -> Response:
|
|
model = self.get_queryset().model
|
|
try:
|
|
collection_string = model.get_collection_string()
|
|
except AttributeError:
|
|
# The corresponding queryset does not support caching.
|
|
response = super().list(request, *args, **kwargs)
|
|
else:
|
|
collection = Collection(collection_string)
|
|
user = user_to_collection_user(request.user)
|
|
response = Response(collection.as_list_for_user(user))
|
|
return response
|
|
|
|
|
|
class RetrieveModelMixin(_RetrieveModelMixin):
|
|
"""
|
|
Mixin to add the caching system to retrieve requests.
|
|
|
|
It is not allowed to use the method get_queryset() in derivated classes.
|
|
The attribute queryset has to be used in the following form:
|
|
|
|
queryset = Model.objects.all()
|
|
"""
|
|
def retrieve(self, request: Any, *args: Any, **kwargs: Any) -> Response:
|
|
model = self.get_queryset().model
|
|
try:
|
|
collection_string = model.get_collection_string()
|
|
except AttributeError:
|
|
# The corresponding queryset does not support caching.
|
|
response = super().retrieve(request, *args, **kwargs)
|
|
else:
|
|
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
|
|
collection_element = CollectionElement.from_values(
|
|
collection_string, self.kwargs[lookup_url_kwarg])
|
|
user = user_to_collection_user(request.user)
|
|
try:
|
|
content = collection_element.as_dict_for_user(user)
|
|
except collection_element.get_model().DoesNotExist:
|
|
raise Http404
|
|
if content is None:
|
|
self.permission_denied(request)
|
|
response = Response(content)
|
|
return response
|
|
|
|
|
|
class GenericViewSet(PermissionMixin, _GenericViewSet):
|
|
pass
|
|
|
|
|
|
class ModelViewSet(PermissionMixin, ListModelMixin, RetrieveModelMixin, _ModelViewSet):
|
|
pass
|
|
|
|
|
|
class ViewSet(PermissionMixin, _ViewSet):
|
|
pass
|