# coding: utf-8
# pylint: disable=too-few-public-methods
"""
This module provides redefined DRF's generic views and viewsets leveraging serializer registration.
One of the main issues with creating traditional DRF APIs is a lot of bloat (and we're writing Python, not Java or C#,
to avoid bloat) that's completely unnecessary in a structured Django project. Therefore, this module aims to provide
a better and simpler way to write simple API endpoints - without limiting the ability to create more complex views.
The particular means to that end are:
* :class:`rest_easy.scopes.ScopeQuerySet` and its subclasses (:class:`rest_easy.scopes.UrlKwargScopeQuerySet` and
:class:`rest_easy.scopes.RequestAttrScopeQuerySet`) provide a simple way to scope views and viewsets.
by resource (ie. limiting results to single account, or /resource/<resource_pk>/inner_resource/<inner_resource_pk>/)
* generic views leveraging the above, as well as model-and-schema specification instead of queryset, serializer and
helper methods - all generic views that were available in DRF as well as GenericAPIView are redefined to support
this.
* Generic :class:`rest_easy.views.ModelViewSet` which allows for very simple definition of resource
endpoint.
To make the new views work, all that\'s required is a serializer::
from users.models import User
from accounts.models import Account
from rest_easy.serializers import ModelSerializer
class UserSerializer(ModelSerializer):
class Meta:
model = User
fields = '__all__'
schema = 'default'
class UserViewSet(ModelViewSet):
model = User
scope = UrlKwargScopeQuerySet(Account)
and in urls.py::
from django.conf.urls import url, include
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'accounts/(?P<account_pk>[0-9]+)/users', UserViewSet)
urlpatterns = [url(r'^', include(router.urls))]
The above will provide the users scoped by account primary key as resources: with list, retrieve, create, update and
partial update methods, as well as standard HEAD and OPTIONS autogenerated responses.
You can easily add custom paths to viewsets when needed - it's described in DRF documentation.
"""
from django.conf import settings
from rest_framework.viewsets import ViewSetMixin
from rest_framework import generics, mixins
from six import with_metaclass
from rest_easy.exceptions import RestEasyException
from rest_easy.registers import serializer_register
from rest_easy.scopes import ScopeQuerySet
__all__ = ['GenericAPIView', 'CreateAPIView', 'ListAPIView', 'RetrieveAPIView', 'DestroyAPIView', 'UpdateAPIView',
'ListCreateAPIView', 'RetrieveUpdateAPIView', 'RetrieveDestroyAPIView', 'RetrieveUpdateDestroyAPIView',
'ReadOnlyModelViewSet', 'ModelViewSet']
def get_additional_bases():
"""
Looks for additional view bases in settings.REST_EASY_VIEW_BASES.
:return:
"""
resolved_bases = []
from importlib import import_module
for base in getattr(settings, 'REST_EASY_VIEW_BASES', []):
mod, cls = base.rsplit('.', 1)
resolved_bases.append(getattr(import_module(mod), cls))
return resolved_bases
def get_additional_mixins():
"""
Looks for additional view bases in settings.REST_EASY_VIEW_MIXINS.
:return:
"""
resolved_bases = []
from importlib import import_module
for base in getattr(settings, 'REST_EASY_GENERIC_VIEW_MIXINS', []):
mod, cls = base.rsplit('.', 1)
resolved_bases.append(getattr(import_module(mod), cls))
return resolved_bases
ADDITIONAL_MIXINS = get_additional_mixins()
class ScopedViewMixin(object):
"""
This class provides a get_queryset method that works with ScopeQuerySet.
Queryset obtained from superclass is filtered by view.scope's (if it exists) child_queryset() method.
"""
def get_queryset(self):
"""
Calls scope's child_queryset methods on queryset as obtained from superclass.
:return: queryset.
"""
queryset = super(ScopedViewMixin, self).get_queryset()
if hasattr(self, 'scope') and self.scope:
for scope in self.scope:
queryset = scope.child_queryset(queryset, self)
return queryset
def get_scoped_object(self, handle):
"""
Obtains object from scope when scope's get_object_handle was set.
:param handle: get_object_handle used in scope initialization.
:return: object used by scope to filter.
"""
scope = self.rest_easy_available_object_handles.get(handle, None)
if scope:
return scope.get_object(self)
raise AttributeError('{} get_object handle not found on object {}'.format(handle, self))
def __getattr__(self, item):
"""
A shortcut providing get_{get_object_handle} to be able to easily access objects used by this view's scopes
for filtering. For example, scope = UrlKwargScopeQuerySet(Account) will be available with self.get_account().
:param item: item to obtain plus 'get_' prefix
:return: object used by scope for filtering.
"""
if not item.startswith('get_'):
raise AttributeError('{} not found on object {}'.format(item, self))
handle = item[4:]
try:
return self.get_scoped_object(handle)
except AttributeError:
raise AttributeError('{} not found on object {}'.format(item, self))
class ViewEasyMetaclass(type): # pylint: disable=too-few-public-methods
"""
This metaclass sets default queryset on a model-and-schema based views and fills in concrete views with bases.
It's required for compatibility with some of DRF's elements, like routers.
"""
def __new__(mcs, name, bases, attrs):
"""
Create the class.
"""
if ('queryset' not in attrs or attrs['queryset'] is None) and 'model' in attrs:
attrs['queryset'] = attrs['model'].objects.all()
if 'scope' in attrs and isinstance(attrs['scope'], ScopeQuerySet):
attrs['scope'] = [attrs['scope']]
attrs['rest_easy_available_object_handles'] = {}
cls = super(ViewEasyMetaclass, mcs).__new__(mcs, name, bases, attrs)
for scope in getattr(cls, 'scope', []):
scope.contribute_to_class(cls)
return cls
class ChainingCreateUpdateMixin(object):
"""
Chain-enabled versions of perform_create and perform_update.
"""
def perform_create(self, serializer, **kwargs): # pylint: disable=no-self-use
"""
Extend default implementation with kwarg chaining.
"""
return serializer.save(**kwargs)
def perform_update(self, serializer, **kwargs): # pylint: disable=no-self-use
"""
Extend default implementation with kwarg chaining.
"""
return serializer.save(**kwargs)
class GenericAPIViewBase(ScopedViewMixin, generics.GenericAPIView):
"""
Provides a base for all generic views and viewsets leveraging registered serializers and ScopeQuerySets.
Adds additional DRF-verb-wise override for obtaining serializer class: serializer_schema_for_verb property.
It should be a dictionary of DRF verbs and serializer schemas (they work in conjunction with model property).
serializer_schema_for_verb = {'update': 'schema-mutate', 'create': 'schema-mutate'}
The priority for obtaining serializer class is:
* get_serializer_class override
* serializer_class property
* model + serializer_schema_for_verb[verb] lookup in :class:`rest_easy.registers.SerializerRegister`
* model + schema lookup in :class:`rest_easy.registers.SerializerRegister`
"""
serializer_schema_for_verb = {}
def __init__(self, **kwargs):
"""
Set object cache to empty dict.
:param kwargs: Passthrough to Django view.
"""
super(GenericAPIViewBase, self).__init__(**kwargs)
self.rest_easy_object_cache = {}
def get_drf_verb(self):
"""
Obtain the DRF verb used for a request.
"""
method = self.request.method.lower()
if method == 'get':
if self.lookup_url_kwarg in self.kwargs:
return 'retrieve'
mapping = {
'get': 'list',
'post': 'create',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
}
return mapping[method]
def get_serializer_name(self, verb=None):
"""
Obtains registered serializer name for this view.
Leverages :class:`rest_easy.registers.SerializerRegister`. Works when either of or both model
and schema properties are available on this view.
:return: registered serializer key.
"""
model = getattr(self, 'model', None)
schema = None
if not model and not hasattr(self, 'schema') and (verb and verb not in self.serializer_schema_for_verb):
raise RestEasyException('Either model or schema fields need to be set on a model-based GenericAPIView.')
if verb:
schema = self.serializer_schema_for_verb.get(verb, None)
if schema is None:
schema = getattr(self, 'schema', 'default')
return serializer_register.get_name(model, schema)
def get_serializer_class(self):
"""
Gets serializer appropriate for this view.
Leverages :class:`rest_easy.registers.SerializerRegister`. Works when either of or both model
and schema properties are available on this view.
:return: serializer class.
"""
if hasattr(self, 'serializer_class') and self.serializer_class:
return self.serializer_class
serializer = serializer_register.lookup(self.get_serializer_name(verb=self.get_drf_verb()))
if serializer:
return serializer
raise RestEasyException(u'Serializer for model {} and schema {} cannot be found.'.format(
getattr(self, 'model', '[no model]'),
getattr(self, 'schema', '[no schema]')
))
[docs]class GenericAPIView(with_metaclass(ViewEasyMetaclass, *(get_additional_bases() + [GenericAPIViewBase]))):
"""
Base view with compat metaclass.
"""
__abstract__ = True
def create(self, request, *args, **kwargs): # pragma: no cover
"""
Shortcut method.
"""
return self.create(request, *args, **kwargs)
def list_(self, request, *args, **kwargs): # pragma: no cover
"""
Shortcut method.
"""
return self.list(request, *args, **kwargs)
def retrieve(self, request, *args, **kwargs): # pragma: no cover
"""
Shortcut method.
"""
return self.retrieve(request, *args, **kwargs)
def destroy(self, request, *args, **kwargs): # pragma: no cover
"""
Shortcut method.
"""
return self.destroy(request, *args, **kwargs)
def update(self, request, *args, **kwargs): # pragma: no cover
"""
Shortcut method.
"""
return self.update(request, *args, **kwargs)
def partial_update(self, request, *args, **kwargs): # pragma: no cover
"""
Shortcut method.
"""
return self.partial_update(request, *args, **kwargs)
CreateAPIView = type('CreateAPIView',
tuple(ADDITIONAL_MIXINS + [ChainingCreateUpdateMixin, mixins.CreateModelMixin, GenericAPIView]),
{'post': create,
'__doc__': "Concrete view for retrieving or deleting a model instance."})
ListAPIView = type('ListAPIView',
tuple(ADDITIONAL_MIXINS + [mixins.ListModelMixin, GenericAPIView]),
{'get': list_,
'__doc__': "Concrete view for listing a queryset."})
RetrieveAPIView = type('RetrieveAPIView',
tuple(ADDITIONAL_MIXINS + [mixins.RetrieveModelMixin, GenericAPIView]),
{'get': retrieve,
'__doc__': "Concrete view for retrieving a model instance."})
DestroyAPIView = type('DestroyAPIView',
tuple(ADDITIONAL_MIXINS + [mixins.DestroyModelMixin, GenericAPIView]),
{'delete': destroy,
'__doc__': "Concrete view for deleting a model instance."})
UpdateAPIView = type('UpdateAPIView',
tuple(ADDITIONAL_MIXINS + [ChainingCreateUpdateMixin, mixins.UpdateModelMixin, GenericAPIView]),
{'put': update,
'patch': partial_update,
'__doc__': "Concrete view for updating a model instance."})
ListCreateAPIView = type('ListCreateAPIView',
tuple(ADDITIONAL_MIXINS +
[ChainingCreateUpdateMixin, mixins.ListModelMixin,
mixins.CreateModelMixin, GenericAPIView]),
{'get': list_,
'post': create,
'__doc__': "Concrete view for listing a queryset or creating a model instance."})
RetrieveUpdateAPIView = type('RetrieveUpdateAPIView',
tuple(ADDITIONAL_MIXINS +
[ChainingCreateUpdateMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, GenericAPIView]),
{'get': retrieve,
'put': update,
'patch': partial_update,
'__doc__': "Concrete view for retrieving, updating a model instance."})
RetrieveDestroyAPIView = type('RetrieveDestroyAPIView',
tuple(ADDITIONAL_MIXINS +
[mixins.RetrieveModelMixin, mixins.DestroyModelMixin, GenericAPIView]),
{'get': retrieve,
'delete': destroy,
'__doc__': "Concrete view for retrieving or deleting a model instance."})
RetrieveUpdateDestroyAPIView = type('RetrieveUpdateDestroyAPIView',
tuple(ADDITIONAL_MIXINS +
[ChainingCreateUpdateMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.DestroyModelMixin, GenericAPIView]),
{'get': retrieve,
'put': update,
'patch': partial_update,
'delete': destroy,
'__doc__': "Concrete view for retrieving, updating or deleting a model instance."
})
class GenericViewSet(ViewSetMixin, GenericAPIView): # pragma: no cover
"""
The GenericViewSet class does not provide any actions by default,
but does include the base set of generic view behavior, such as
the `get_object` and `get_queryset` methods.
"""
ReadOnlyModelViewSet = type('ReadOnlyModelViewSet',
tuple(ADDITIONAL_MIXINS +
[mixins.RetrieveModelMixin, mixins.ListModelMixin, GenericViewSet]),
{'__doc__': "A viewset that provides default `list()` and `retrieve()` actions."})
ModelViewSet = type('ModelViewSet',
tuple(ADDITIONAL_MIXINS +
[ChainingCreateUpdateMixin, mixins.CreateModelMixin, mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet]),
{'__doc__': "A viewset that provides default `create()`, `retrieve()`, `update()`, "
"`partial_update()`, `destroy()` and `list()` actions."})