Source code for rest_easy.scopes

# coding: utf-8
"""
This module provides scopes usable with django-rest-easy's generic views.

See :mod:`rest_easy.views` for detailed explanation.
"""
from __future__ import unicode_literals

from django.db.models import QuerySet, Model
from django.http import Http404
from django.shortcuts import get_object_or_404

from rest_easy.exceptions import RestEasyException

__all__ = ['ScopeQuerySet', 'UrlKwargScopeQuerySet', 'RequestAttrScopeQuerySet']


[docs]class ScopeQuerySet(object): """ This class provides a scope-by-parent-element functionality to views and their querysets. It works by selecting a proper parent model instance and filtering view's queryset with it automatically. """ def __init__(self, qs_or_obj, parent_field='pk', related_field=None, raise_404=False, allow_none=False, get_object_handle='', parent=None): """ Sets instance properties, infers sane defaults and ensures qs_or_obj is correct. :param qs_or_obj: This can be a queryset or a Django model or explicit None (for particular subclasses) :param parent_field: the field to filter by in the parent queryset (qs_or_obj), by default 'id'. :param related_field: the field to filter by in the view queryset, by default model_name. :param raise_404: whether 404 should be raised if parent object cannot be found. :param allow_none: if filtering view queryset by object=None should be allowed. If it's false, resulting queryset is guaranteed to be empty if parent object can't be found and 404 is not raised. :param get_object_handle: the name under which the object should be available in view. ie. view.get_scoped_object(get_object_handle) or view.get_{get_object_handle}. If None, the object will not be available from view level. By default will be infered to qs_or_obj's model_name. :param parent: if this object's queryset should be filtered by another parameter, parent attribute should be an instance of ScopeQuerySet. This allows for ScopeQuerySetChaining (ie. for messages we might have UrlKwargScopeQuerySet(User, parent=UrlKwargScopeQuerySet(Account)) for scoping by user and limiting users to an account. """ if isinstance(qs_or_obj, QuerySet): self.queryset = qs_or_obj elif isinstance(qs_or_obj, type) and issubclass(qs_or_obj, Model): self.queryset = qs_or_obj.objects.all() elif qs_or_obj is None: self.queryset = None else: raise RestEasyException('Queryset parameter must be an instance of QuerySet or a Model subclass.') if related_field is None: try: related_field = '{}'.format(self.queryset.model._meta.model_name) # pylint: disable=protected-access except AttributeError: raise RestEasyException('Either related_field or qs_or_obj must be given.') self.parent_field = parent_field self.related_field = related_field self.raise_404 = raise_404 self.parent = ([parent] if isinstance(parent, ScopeQuerySet) else parent) or [] self.allow_none = allow_none self.get_object_handle = get_object_handle if self.get_object_handle == '': try: self.get_object_handle = self.queryset.model._meta.model_name # pylint: disable=protected-access except AttributeError: raise RestEasyException('Either qs_or_obj or explicit get_object_handle (can be None) must be given.')
[docs] def contribute_to_class(self, view): """ Put self.get_object_handle into view's available handles dict to allow easy access to the scope's get_object() method in case the object needs to be reused (ie. in child object creation). :param view: View the scope is added to. """ if self.get_object_handle: if self.get_object_handle in view.rest_easy_available_object_handles: raise RestEasyException( 'ImproperlyConfigured: multiple scopes with {} get_object handle!'.format(self.get_object_handle) ) view.rest_easy_available_object_handles[self.get_object_handle] = self for scope in self.parent: scope.contribute_to_class(view)
[docs] def get_value(self, view): """ Get value used to filter qs_or_objs's field specified for filtering (parent_field in init). :param view: DRF view instance - as it provides access to both request and kwargs. :return: value to filter by. """ raise NotImplementedError('You need to use ScopeQueryset subclass with get_value implemented.')
[docs] def get_queryset(self, view): """ Obtains parent queryset (init's qs_or_obj) along with any chaining (init's parent) required. :param view: DRF view instance. :return: queryset instance. """ queryset = self.queryset for parent in self.parent: queryset = parent.child_queryset(queryset, view) return queryset
[docs] def get_object(self, view): """ Caching wrapper around _get_object. :param view: DRF view instance. :return: object (instance of init's qs_or_obj model except shadowed by subclass). """ if self.get_object_handle: obj = view.rest_easy_object_cache.get(self.get_object_handle, None) if not obj: obj = self._get_object(view) view.rest_easy_object_cache[self.get_object_handle] = obj else: obj = self._get_object(view) return obj
def _get_object(self, view): """ Obtains parent object by which view queryset should be filtered. :param view: DRF view instance. :return: object (instance of init's qs_or_obj model except shadowed by subclass). """ queryset = self.get_queryset(view) queryset = queryset.filter(**{self.parent_field: self.get_value(view)}) try: obj = get_object_or_404(queryset) except Http404: if self.raise_404: raise obj = None return obj
[docs] def child_queryset(self, queryset, view): """ Performs filtering of the view queryset. :param queryset: view queryset instance. :param view: view object. :return: filtered queryset. """ obj = self.get_object(view) if obj is None and not self.allow_none: return queryset.none() return queryset.filter(**{self.related_field: obj})
[docs]class UrlKwargScopeQuerySet(ScopeQuerySet): """ ScopeQuerySet that obtains parent object from url kwargs. """ def __init__(self, *args, **kwargs): """ Adds url_kwarg to :class:`rest_easy.views.ScopeQuerySet` init parameters. :param args: same as :class:`rest_easy.views.ScopeQuerySet`. :param url_kwarg: name of url field to be obtained from view's kwargs. By default it will be inferred as model_name_pk. :param kwargs: same as :class:`rest_easy.views.ScopeQuerySet`. """ self.url_kwarg = kwargs.pop('url_kwarg', None) super(UrlKwargScopeQuerySet, self).__init__(*args, **kwargs) if not self.url_kwarg: try: self.url_kwarg = '{}_pk'.format(self.queryset.model._meta.model_name) # pylint: disable=protected-access except AttributeError: raise RestEasyException('Either related_field or qs_or_obj must be given.')
[docs] def get_value(self, view): """ Obtains value from url kwargs. :param view: DRF view instance. :return: Value determining parent object. """ return view.kwargs.get(self.url_kwarg)
[docs]class RequestAttrScopeQuerySet(ScopeQuerySet): """ ScopeQuerySet that obtains parent object from view's request property. It can work two-fold: * the request's property contains full object: in this case no filtering of parent's queryset is required. When using such approach, is_object must be set to True, and qs_or_obj can be None. Chaining will be disabled since it is inherent to filtering process. * the request's property contains object's id, uuid, or other unique property. In that case is_object needs to be explicitly set to False, and qs_or_obj needs to be a Django model or queryset. Chaining will be performed as usually. """ def __init__(self, *args, **kwargs): """ Adds is_object and request_attr to :class:`rest_easy.views.ScopeQuerySet` init parameters. :param args: same as :class:`rest_easy.views.ScopeQuerySet`. :param request_attr: name of property to be obtained from view.request. :param is_object: if request's property will be an object or a value to filter by. True by default. :param kwargs: same as :class:`rest_easy.views.ScopeQuerySet`. """ self.request_attr = kwargs.pop('request_attr', None) if self.request_attr is None: raise RestEasyException('request_attr must be set explicitly on an {} init.'.format( self.__class__.__name__)) self.is_object = kwargs.pop('is_object', True) super(RequestAttrScopeQuerySet, self).__init__(*args, **kwargs)
[docs] def get_value(self, view): """ Obtains value from url kwargs. :param view: DRF view instance. :return: Value determining parent object. """ return getattr(view.request, self.request_attr, None)
def _get_object(self, view): """ Extends standard _get_object's behaviour with handling values that are already objects. :param view: DRF view instance. :return: object to filter view's queryset by. """ if self.is_object: return self.get_value(view) return super(RequestAttrScopeQuerySet, self)._get_object(view)