From 52847a215d4e8de88e81d9ae79ce8bee9a36a9a2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 15 Jan 2013 17:50:51 +0000 Subject: Fix implementation --- rest_framework/mixins.py | 3 -- rest_framework/resources.py | 67 ++++++++++++++++----------------------------- 2 files changed, 23 insertions(+), 47 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 8873e4ae..9bd566da 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -25,9 +25,6 @@ class CreateModelMixin(object): return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def pre_save(self, obj): - pass - class ListModelMixin(object): """ diff --git a/rest_framework/resources.py b/rest_framework/resources.py index dd8a5471..d4019a94 100644 --- a/rest_framework/resources.py +++ b/rest_framework/resources.py @@ -1,31 +1,27 @@ ##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### from functools import update_wrapper -import inspect from django.utils.decorators import classonlymethod -from rest_framework import views, generics - - -def wrapped(source, dest): - """ - Copy public, non-method attributes from source to dest, and return dest. - """ - for attr in [attr for attr in dir(source) - if not attr.startswith('_') and not inspect.ismethod(attr)]: - setattr(dest, attr, getattr(source, attr)) - return dest +from rest_framework import views, generics, mixins ##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### class ResourceMixin(object): """ - Clone Django's `View.as_view()` behaviour *except* using REST framework's - 'method -> action' binding for resources. + This is the magic. + + Overrides `.as_view()` so that it takes an `actions` keyword that performs + the binding of HTTP methods to actions on the Resource. + + For example, to create a concrete view binding the 'GET' and 'POST' methods + to the 'list' and 'create' actions... + + my_resource = MyResource.as_view({'get': 'list', 'post': 'create'}) """ @classonlymethod - def as_view(cls, actions, **initkwargs): + def as_view(cls, actions=None, **initkwargs): """ Main entry point for a request-response process. """ @@ -61,36 +57,19 @@ class ResourceMixin(object): return view -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - class Resource(ResourceMixin, views.APIView): pass -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ModelResource(ResourceMixin, views.APIView): - # TODO: Actually delegation won't work - root_class = generics.ListCreateAPIView - detail_class = generics.RetrieveUpdateDestroyAPIView - - def root_view(self): - return wrapped(self, self.root_class()) - - def detail_view(self): - return wrapped(self, self.detail_class()) - - def list(self, request, *args, **kwargs): - return self.root_view().list(request, args, kwargs) - - def create(self, request, *args, **kwargs): - return self.root_view().create(request, args, kwargs) - - def retrieve(self, request, *args, **kwargs): - return self.detail_view().retrieve(request, args, kwargs) - - def update(self, request, *args, **kwargs): - return self.detail_view().update(request, args, kwargs) - - def destroy(self, request, *args, **kwargs): - return self.detail_view().destroy(request, args, kwargs) +# Note the inheritence of both MultipleObjectAPIView *and* SingleObjectAPIView +# is a bit weird given the diamond inheritence, but it will work for now. +# There's some implementation clean up that can happen later. +class ModelResource(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + ResourceMixin, + generics.MultipleObjectAPIView, + generics.SingleObjectAPIView): + pass -- cgit v1.2.3 From 4a7139e41d2500776c30e663c1cebce74b49270d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 15 Jan 2013 21:49:24 +0000 Subject: Tweaks --- rest_framework/routers.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 rest_framework/routers.py (limited to 'rest_framework') diff --git a/rest_framework/routers.py b/rest_framework/routers.py new file mode 100644 index 00000000..a5aef5b7 --- /dev/null +++ b/rest_framework/routers.py @@ -0,0 +1,33 @@ +# Not properly implemented yet, just the basic idea + + +class BaseRouter(object): + def __init__(self): + self.resources = [] + + def register(self, name, resource): + self.resources.append((name, resource)) + + @property + def urlpatterns(self): + ret = [] + + for name, resource in self.resources: + list_actions = { + 'get': getattr(resource, 'list', None), + 'post': getattr(resource, 'create', None) + } + detail_actions = { + 'get': getattr(resource, 'retrieve', None), + 'put': getattr(resource, 'update', None), + 'delete': getattr(resource, 'destroy', None) + } + list_regex = r'^%s/$' % name + detail_regex = r'^%s/(?P[0-9]+)/$' % name + list_name = '%s-list' + detail_name = '%s-detail' + + ret += url(list_regex, resource.as_view(list_actions), list_name) + ret += url(detail_regex, resource.as_view(detail_actions), detail_name) + + return ret -- cgit v1.2.3 From 922ee61d8611b41e2944b6503af736b1790abe83 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 18 Mar 2013 21:05:13 +0000 Subject: Remove erronous pre_save --- rest_framework/generics.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 55918267..36ecf915 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -82,9 +82,6 @@ class GenericAPIView(views.APIView): """ pass - def pre_save(self, obj): - pass - class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): """ -- cgit v1.2.3 From ec076a00786c6b89a55b6ffe2556bb3b777100f5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 31 Mar 2013 11:36:58 +0100 Subject: Add viewsets/routers to indexs etc --- rest_framework/routers.py | 33 --------------------------------- rest_framework/viewsets.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 33 deletions(-) delete mode 100644 rest_framework/routers.py create mode 100644 rest_framework/viewsets.py (limited to 'rest_framework') diff --git a/rest_framework/routers.py b/rest_framework/routers.py deleted file mode 100644 index a5aef5b7..00000000 --- a/rest_framework/routers.py +++ /dev/null @@ -1,33 +0,0 @@ -# Not properly implemented yet, just the basic idea - - -class BaseRouter(object): - def __init__(self): - self.resources = [] - - def register(self, name, resource): - self.resources.append((name, resource)) - - @property - def urlpatterns(self): - ret = [] - - for name, resource in self.resources: - list_actions = { - 'get': getattr(resource, 'list', None), - 'post': getattr(resource, 'create', None) - } - detail_actions = { - 'get': getattr(resource, 'retrieve', None), - 'put': getattr(resource, 'update', None), - 'delete': getattr(resource, 'destroy', None) - } - list_regex = r'^%s/$' % name - detail_regex = r'^%s/(?P[0-9]+)/$' % name - list_name = '%s-list' - detail_name = '%s-detail' - - ret += url(list_regex, resource.as_view(list_actions), list_name) - ret += url(detail_regex, resource.as_view(detail_actions), detail_name) - - return ret diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py new file mode 100644 index 00000000..a5aef5b7 --- /dev/null +++ b/rest_framework/viewsets.py @@ -0,0 +1,33 @@ +# Not properly implemented yet, just the basic idea + + +class BaseRouter(object): + def __init__(self): + self.resources = [] + + def register(self, name, resource): + self.resources.append((name, resource)) + + @property + def urlpatterns(self): + ret = [] + + for name, resource in self.resources: + list_actions = { + 'get': getattr(resource, 'list', None), + 'post': getattr(resource, 'create', None) + } + detail_actions = { + 'get': getattr(resource, 'retrieve', None), + 'put': getattr(resource, 'update', None), + 'delete': getattr(resource, 'destroy', None) + } + list_regex = r'^%s/$' % name + detail_regex = r'^%s/(?P[0-9]+)/$' % name + list_name = '%s-list' + detail_name = '%s-detail' + + ret += url(list_regex, resource.as_view(list_actions), list_name) + ret += url(detail_regex, resource.as_view(detail_actions), detail_name) + + return ret -- cgit v1.2.3 From c785628300d2b7cce63862a18915c537f8a3ab24 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 20:00:44 +0100 Subject: Fleshing out viewsets/routers --- rest_framework/resources.py | 75 ---------------------------- rest_framework/routers.py | 43 ++++++++++++++++ rest_framework/viewsets.py | 119 ++++++++++++++++++++++++++++++++------------ 3 files changed, 129 insertions(+), 108 deletions(-) delete mode 100644 rest_framework/resources.py create mode 100644 rest_framework/routers.py (limited to 'rest_framework') diff --git a/rest_framework/resources.py b/rest_framework/resources.py deleted file mode 100644 index d4019a94..00000000 --- a/rest_framework/resources.py +++ /dev/null @@ -1,75 +0,0 @@ -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -from functools import update_wrapper -from django.utils.decorators import classonlymethod -from rest_framework import views, generics, mixins - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ResourceMixin(object): - """ - This is the magic. - - Overrides `.as_view()` so that it takes an `actions` keyword that performs - the binding of HTTP methods to actions on the Resource. - - For example, to create a concrete view binding the 'GET' and 'POST' methods - to the 'list' and 'create' actions... - - my_resource = MyResource.as_view({'get': 'list', 'post': 'create'}) - """ - - @classonlymethod - def as_view(cls, actions=None, **initkwargs): - """ - Main entry point for a request-response process. - """ - # sanitize keyword arguments - for key in initkwargs: - if key in cls.http_method_names: - raise TypeError("You tried to pass in the %s method name as a " - "keyword argument to %s(). Don't do that." - % (key, cls.__name__)) - if not hasattr(cls, key): - raise TypeError("%s() received an invalid keyword %r" % ( - cls.__name__, key)) - - def view(request, *args, **kwargs): - self = cls(**initkwargs) - - # Bind methods to actions - for method, action in actions.items(): - handler = getattr(self, action) - setattr(self, method, handler) - - # As you were, solider. - if hasattr(self, 'get') and not hasattr(self, 'head'): - self.head = self.get - return self.dispatch(request, *args, **kwargs) - - # take name and docstring from class - update_wrapper(view, cls, updated=()) - - # and possible attributes set by decorators - # like csrf_exempt from dispatch - update_wrapper(view, cls.dispatch, assigned=()) - return view - - -class Resource(ResourceMixin, views.APIView): - pass - - -# Note the inheritence of both MultipleObjectAPIView *and* SingleObjectAPIView -# is a bit weird given the diamond inheritence, but it will work for now. -# There's some implementation clean up that can happen later. -class ModelResource(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - ResourceMixin, - generics.MultipleObjectAPIView, - generics.SingleObjectAPIView): - pass diff --git a/rest_framework/routers.py b/rest_framework/routers.py new file mode 100644 index 00000000..63eae5d7 --- /dev/null +++ b/rest_framework/routers.py @@ -0,0 +1,43 @@ +from django.conf.urls import url, patterns + + +class BaseRouter(object): + def __init__(self): + self.registry = [] + + def register(self, prefix, viewset, base_name): + self.registry.append((prefix, viewset, base_name)) + + def get_urlpatterns(self): + raise NotImplemented('get_urlpatterns must be overridden') + + @property + def urlpatterns(self): + if not hasattr(self, '_urlpatterns'): + print self.get_urlpatterns() + self._urlpatterns = patterns('', *self.get_urlpatterns()) + return self._urlpatterns + + +class DefaultRouter(BaseRouter): + route_list = [ + (r'$', {'get': 'list', 'post': 'create'}, '%s-list'), + (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, '%s-detail'), + ] + + def get_urlpatterns(self): + ret = [] + for prefix, viewset, base_name in self.registry: + for suffix, action_mapping, name_format in self.route_list: + + # Only actions which actually exist on the viewset will be bound + bound_actions = {} + for method, action in action_mapping.items(): + if hasattr(viewset, action): + bound_actions[method] = action + + regex = prefix + suffix + view = viewset.as_view(bound_actions) + name = name_format % base_name + ret.append(url(regex, view, name=name)) + return ret diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index a5aef5b7..887a9722 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -1,33 +1,86 @@ -# Not properly implemented yet, just the basic idea - - -class BaseRouter(object): - def __init__(self): - self.resources = [] - - def register(self, name, resource): - self.resources.append((name, resource)) - - @property - def urlpatterns(self): - ret = [] - - for name, resource in self.resources: - list_actions = { - 'get': getattr(resource, 'list', None), - 'post': getattr(resource, 'create', None) - } - detail_actions = { - 'get': getattr(resource, 'retrieve', None), - 'put': getattr(resource, 'update', None), - 'delete': getattr(resource, 'destroy', None) - } - list_regex = r'^%s/$' % name - detail_regex = r'^%s/(?P[0-9]+)/$' % name - list_name = '%s-list' - detail_name = '%s-detail' - - ret += url(list_regex, resource.as_view(list_actions), list_name) - ret += url(detail_regex, resource.as_view(detail_actions), detail_name) - - return ret +from functools import update_wrapper +from django.utils.decorators import classonlymethod +from rest_framework import views, generics, mixins + + +class ViewSetMixin(object): + """ + This is the magic. + + Overrides `.as_view()` so that it takes an `actions` keyword that performs + the binding of HTTP methods to actions on the Resource. + + For example, to create a concrete view binding the 'GET' and 'POST' methods + to the 'list' and 'create' actions... + + view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) + """ + + @classonlymethod + def as_view(cls, actions=None, **initkwargs): + """ + Main entry point for a request-response process. + + Because of the way class based views create a closure around the + instantiated view, we need to totally reimplement `.as_view`, + and slightly modify the view function that is created and returned. + """ + # sanitize keyword arguments + for key in initkwargs: + if key in cls.http_method_names: + raise TypeError("You tried to pass in the %s method name as a " + "keyword argument to %s(). Don't do that." + % (key, cls.__name__)) + if not hasattr(cls, key): + raise TypeError("%s() received an invalid keyword %r" % ( + cls.__name__, key)) + + def view(request, *args, **kwargs): + self = cls(**initkwargs) + + # Bind methods to actions + # This is the bit that's different to a standard view + for method, action in actions.items(): + handler = getattr(self, action) + setattr(self, method, handler) + + # Patch this in as it's otherwise only present from 1.5 onwards + if hasattr(self, 'get') and not hasattr(self, 'head'): + self.head = self.get + + # And continue as usual + return self.dispatch(request, *args, **kwargs) + + # take name and docstring from class + update_wrapper(view, cls, updated=()) + + # and possible attributes set by decorators + # like csrf_exempt from dispatch + update_wrapper(view, cls.dispatch, assigned=()) + return view + + +class ViewSet(ViewSetMixin, views.APIView): + pass + + +# Note the inheritence of both MultipleObjectAPIView *and* SingleObjectAPIView +# is a bit weird given the diamond inheritence, but it will work for now. +# There's some implementation clean up that can happen later. +class ModelViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + ViewSetMixin, + generics.MultipleObjectAPIView, + generics.SingleObjectAPIView): + pass + + +class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, + mixins.ListModelMixin, + ViewSetMixin, + generics.MultipleObjectAPIView, + generics.SingleObjectAPIView): + pass -- cgit v1.2.3 From fb41d2ac8f495ae0728e3f38c6a21306f0507316 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 20:35:40 +0100 Subject: Add support for action and link routing --- rest_framework/decorators.py | 22 ++++++++++++++++++++++ rest_framework/routers.py | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 8250cd3b..00b37f8b 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -97,3 +97,25 @@ def permission_classes(permission_classes): func.permission_classes = permission_classes return func return decorator + + +def link(**kwargs): + """ + Used to mark a method on a ViewSet that should be routed for GET requests. + """ + def decorator(func): + func.bind_to_method = 'get' + func.kwargs = kwargs + return func + return decorator + + +def action(**kwargs): + """ + Used to mark a method on a ViewSet that should be routed for POST requests. + """ + def decorator(func): + func.bind_to_method = 'post' + func.kwargs = kwargs + return func + return decorator diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 63eae5d7..d1e96156 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -24,10 +24,12 @@ class DefaultRouter(BaseRouter): (r'$', {'get': 'list', 'post': 'create'}, '%s-list'), (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, '%s-detail'), ] + extra_routes = (r'(?P[^/]+)/%s/$', '%s-%s') def get_urlpatterns(self): ret = [] for prefix, viewset, base_name in self.registry: + # Bind standard routes for suffix, action_mapping, name_format in self.route_list: # Only actions which actually exist on the viewset will be bound @@ -36,8 +38,26 @@ class DefaultRouter(BaseRouter): if hasattr(viewset, action): bound_actions[method] = action + # Build the url pattern regex = prefix + suffix view = viewset.as_view(bound_actions) name = name_format % base_name ret.append(url(regex, view, name=name)) + + # Bind any extra @action or @link routes + for attr in dir(viewset): + func = getattr(viewset, attr) + http_method = getattr(func, 'bind_to_method', None) + if not http_method: + continue + + regex_format, name_format = self.extra_routes + + # Build the url pattern + regex = regex_format % attr + view = viewset.as_view({http_method: attr}, **func.kwargs) + name = name_format % (base_name, attr) + ret.append(url(regex, view, name=name)) + + # Return a list of url patterns return ret -- cgit v1.2.3 From 9e24db022cd8da1a588dd43e6239e07798881c02 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 20:38:42 +0100 Subject: Commenting --- rest_framework/routers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/routers.py b/rest_framework/routers.py index d1e96156..283add8d 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -29,7 +29,7 @@ class DefaultRouter(BaseRouter): def get_urlpatterns(self): ret = [] for prefix, viewset, base_name in self.registry: - # Bind standard routes + # Bind standard CRUD routes for suffix, action_mapping, name_format in self.route_list: # Only actions which actually exist on the viewset will be bound @@ -44,10 +44,12 @@ class DefaultRouter(BaseRouter): name = name_format % base_name ret.append(url(regex, view, name=name)) - # Bind any extra @action or @link routes + # Bind any extra `@action` or `@link` routes for attr in dir(viewset): func = getattr(viewset, attr) http_method = getattr(func, 'bind_to_method', None) + + # Skip if this is not an @action or @link method if not http_method: continue -- cgit v1.2.3 From f68721ade8d66806296323116ff9a61773ad2be1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 21:42:26 +0100 Subject: Factor view names/descriptions out of View class --- rest_framework/renderers.py | 11 ++--- rest_framework/routers.py | 34 ++++++++------ rest_framework/utils/breadcrumbs.py | 5 ++- rest_framework/utils/formatting.py | 77 ++++++++++++++++++++++++++++++++ rest_framework/views.py | 89 ++++--------------------------------- rest_framework/viewsets.py | 5 ++- 6 files changed, 117 insertions(+), 104 deletions(-) create mode 100644 rest_framework/utils/formatting.py (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4c15e0db..752306ad 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -24,6 +24,7 @@ from rest_framework.settings import api_settings from rest_framework.request import clone_request from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs +from rest_framework.utils.formatting import get_view_name, get_view_description from rest_framework import exceptions, parsers, status, VERSION @@ -438,16 +439,10 @@ class BrowsableAPIRenderer(BaseRenderer): return GenericContentForm() def get_name(self, view): - try: - return view.get_name() - except AttributeError: - return smart_text(view.__class__.__name__) + return get_view_name(view.__class__) def get_description(self, view): - try: - return view.get_description(html=True) - except AttributeError: - return smart_text(view.__doc__ or '') + return get_view_description(view.__class__, html=True) def render(self, data, accepted_media_type=None, renderer_context=None): """ diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 283add8d..c37909ff 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -14,23 +14,31 @@ class BaseRouter(object): @property def urlpatterns(self): if not hasattr(self, '_urlpatterns'): - print self.get_urlpatterns() self._urlpatterns = patterns('', *self.get_urlpatterns()) return self._urlpatterns class DefaultRouter(BaseRouter): route_list = [ - (r'$', {'get': 'list', 'post': 'create'}, '%s-list'), - (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, '%s-detail'), + (r'$', {'get': 'list', 'post': 'create'}, 'list'), + (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, 'detail'), ] - extra_routes = (r'(?P[^/]+)/%s/$', '%s-%s') + extra_routes = r'(?P[^/]+)/%s/$' + name_format = '%s-%s' def get_urlpatterns(self): ret = [] for prefix, viewset, base_name in self.registry: + # Bind regular views + if not getattr(viewset, '_is_viewset', False): + regex = prefix + view = viewset + name = base_name + ret.append(url(regex, view, name=name)) + continue + # Bind standard CRUD routes - for suffix, action_mapping, name_format in self.route_list: + for suffix, action_mapping, action_name in self.route_list: # Only actions which actually exist on the viewset will be bound bound_actions = {} @@ -40,25 +48,25 @@ class DefaultRouter(BaseRouter): # Build the url pattern regex = prefix + suffix - view = viewset.as_view(bound_actions) - name = name_format % base_name + view = viewset.as_view(bound_actions, name_suffix=action_name) + name = self.name_format % (base_name, action_name) ret.append(url(regex, view, name=name)) # Bind any extra `@action` or `@link` routes - for attr in dir(viewset): - func = getattr(viewset, attr) + for action_name in dir(viewset): + func = getattr(viewset, action_name) http_method = getattr(func, 'bind_to_method', None) # Skip if this is not an @action or @link method if not http_method: continue - regex_format, name_format = self.extra_routes + suffix = self.extra_routes % action_name # Build the url pattern - regex = regex_format % attr - view = viewset.as_view({http_method: attr}, **func.kwargs) - name = name_format % (base_name, attr) + regex = prefix + suffix + view = viewset.as_view({http_method: action_name}, **func.kwargs) + name = self.name_format % (base_name, action_name) ret.append(url(regex, view, name=name)) # Return a list of url patterns diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index af21ac79..18b3b207 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from django.core.urlresolvers import resolve, get_script_prefix +from rest_framework.utils.formatting import get_view_name def get_breadcrumbs(url): @@ -16,11 +17,11 @@ def get_breadcrumbs(url): pass else: # Check if this is a REST framework view, and if so add it to the breadcrumbs - if isinstance(getattr(view, 'cls_instance', None), APIView): + if issubclass(getattr(view, 'cls', None), APIView): # Don't list the same view twice in a row. # Probably an optional trailing slash. if not seen or seen[-1] != view: - breadcrumbs_list.insert(0, (view.cls_instance.get_name(), prefix + url)) + breadcrumbs_list.insert(0, (get_view_name(view.cls), prefix + url)) seen.append(view) if url == '': diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py new file mode 100644 index 00000000..79566db1 --- /dev/null +++ b/rest_framework/utils/formatting.py @@ -0,0 +1,77 @@ +""" +Utility functions to return a formatted name and description for a given view. +""" +from __future__ import unicode_literals + +from django.utils.html import escape +from django.utils.safestring import mark_safe +from rest_framework.compat import apply_markdown +import re + + +def _remove_trailing_string(content, trailing): + """ + Strip trailing component `trailing` from `content` if it exists. + Used when generating names from view classes. + """ + if content.endswith(trailing) and content != trailing: + return content[:-len(trailing)] + return content + + +def _remove_leading_indent(content): + """ + Remove leading indent from a block of text. + Used when generating descriptions from docstrings. + """ + whitespace_counts = [len(line) - len(line.lstrip(' ')) + for line in content.splitlines()[1:] if line.lstrip()] + + # unindent the content if needed + if whitespace_counts: + whitespace_pattern = '^' + (' ' * min(whitespace_counts)) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) + content = content.strip('\n') + return content + + +def _camelcase_to_spaces(content): + """ + Translate 'CamelCaseNames' to 'Camel Case Names'. + Used when generating names from view classes. + """ + camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' + content = re.sub(camelcase_boundry, ' \\1', content).strip() + return ' '.join(content.split('_')).title() + + +def get_view_name(cls): + """ + Return a formatted name for an `APIView` class or `@api_view` function. + """ + name = cls.__name__ + name = _remove_trailing_string(name, 'View') + name = _remove_trailing_string(name, 'ViewSet') + return _camelcase_to_spaces(name) + + +def get_view_description(cls, html=False): + """ + Return a description for an `APIView` class or `@api_view` function. + """ + description = cls.__doc__ or '' + description = _remove_leading_indent(description) + if html: + return markup_description(description) + return description + + +def markup_description(description): + """ + Apply HTML markup to the given description. + """ + if apply_markdown: + description = apply_markdown(description) + else: + description = escape(description).replace('\n', '
') + return mark_safe(description) diff --git a/rest_framework/views.py b/rest_framework/views.py index 81cbdcbb..12298ca5 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -4,51 +4,13 @@ Provides an APIView class that is used as the base of all class-based views. from __future__ import unicode_literals from django.core.exceptions import PermissionDenied from django.http import Http404 -from django.utils.html import escape -from django.utils.safestring import mark_safe from django.views.decorators.csrf import csrf_exempt from rest_framework import status, exceptions -from rest_framework.compat import View, apply_markdown +from rest_framework.compat import View from rest_framework.response import Response from rest_framework.request import Request from rest_framework.settings import api_settings -import re - - -def _remove_trailing_string(content, trailing): - """ - Strip trailing component `trailing` from `content` if it exists. - Used when generating names from view classes. - """ - if content.endswith(trailing) and content != trailing: - return content[:-len(trailing)] - return content - - -def _remove_leading_indent(content): - """ - Remove leading indent from a block of text. - Used when generating descriptions from docstrings. - """ - whitespace_counts = [len(line) - len(line.lstrip(' ')) - for line in content.splitlines()[1:] if line.lstrip()] - - # unindent the content if needed - if whitespace_counts: - whitespace_pattern = '^' + (' ' * min(whitespace_counts)) - content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) - content = content.strip('\n') - return content - - -def _camelcase_to_spaces(content): - """ - Translate 'CamelCaseNames' to 'Camel Case Names'. - Used when generating names from view classes. - """ - camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' - content = re.sub(camelcase_boundry, ' \\1', content).strip() - return ' '.join(content.split('_')).title() +from rest_framework.utils.formatting import get_view_name, get_view_description class APIView(View): @@ -64,13 +26,13 @@ class APIView(View): @classmethod def as_view(cls, **initkwargs): """ - Override the default :meth:`as_view` to store an instance of the view - as an attribute on the callable function. This allows us to discover - information about the view when we do URL reverse lookups. + Store the original class on the view function. + + This allows us to discover information about the view when we do URL + reverse lookups. Used for breadcrumb generation. """ - # TODO: deprecate? view = super(APIView, cls).as_view(**initkwargs) - view.cls_instance = cls(**initkwargs) + view.cls = cls return view @property @@ -90,43 +52,10 @@ class APIView(View): 'Vary': 'Accept' } - def get_name(self): - """ - Return the resource or view class name for use as this view's name. - Override to customize. - """ - # TODO: deprecate? - name = self.__class__.__name__ - name = _remove_trailing_string(name, 'View') - return _camelcase_to_spaces(name) - - def get_description(self, html=False): - """ - Return the resource or view docstring for use as this view's description. - Override to customize. - """ - # TODO: deprecate? - description = self.__doc__ or '' - description = _remove_leading_indent(description) - if html: - return self.markup_description(description) - return description - - def markup_description(self, description): - """ - Apply HTML markup to the description of this view. - """ - # TODO: deprecate? - if apply_markdown: - description = apply_markdown(description) - else: - description = escape(description).replace('\n', '
') - return mark_safe(description) - def metadata(self, request): return { - 'name': self.get_name(), - 'description': self.get_description(), + 'name': get_view_name(self.__class__), + 'description': get_view_description(self.__class__), 'renders': [renderer.media_type for renderer in self.renderer_classes], 'parses': [parser.media_type for parser in self.parser_classes], } diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 887a9722..0818c0d9 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -15,9 +15,10 @@ class ViewSetMixin(object): view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) """ + _is_viewset = True @classonlymethod - def as_view(cls, actions=None, **initkwargs): + def as_view(cls, actions=None, name_suffix=None, **initkwargs): """ Main entry point for a request-response process. @@ -57,6 +58,8 @@ class ViewSetMixin(object): # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) + + view.cls = cls return view -- cgit v1.2.3 From fd3f538e9f9ef5d4d929c107b9619e0735e426f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 21:48:23 +0100 Subject: Fix up view name/description tests --- rest_framework/tests/description.py | 63 ++++++++++++++----------------------- 1 file changed, 23 insertions(+), 40 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/description.py b/rest_framework/tests/description.py index 5b3315bc..52c1a34c 100644 --- a/rest_framework/tests/description.py +++ b/rest_framework/tests/description.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from django.test import TestCase from rest_framework.views import APIView from rest_framework.compat import apply_markdown +from rest_framework.utils.formatting import get_view_name, get_view_description # We check that docstrings get nicely un-indented. DESCRIPTION = """an example docstring @@ -49,22 +50,16 @@ MARKED_DOWN_gte_21 = """

an example docstring

class TestViewNamesAndDescriptions(TestCase): - def test_resource_name_uses_classname_by_default(self): - """Ensure Resource names are based on the classname by default.""" + def test_view_name_uses_class_name(self): + """ + Ensure view names are based on the class name. + """ class MockView(APIView): pass - self.assertEqual(MockView().get_name(), 'Mock') + self.assertEqual(get_view_name(MockView), 'Mock') - def test_resource_name_can_be_set_explicitly(self): - """Ensure Resource names can be set using the 'get_name' method.""" - example = 'Some Other Name' - class MockView(APIView): - def get_name(self): - return example - self.assertEqual(MockView().get_name(), example) - - def test_resource_description_uses_docstring_by_default(self): - """Ensure Resource names are based on the docstring by default.""" + def test_view_description_uses_docstring(self): + """Ensure view descriptions are based on the docstring.""" class MockView(APIView): """an example docstring ==================== @@ -81,44 +76,32 @@ class TestViewNamesAndDescriptions(TestCase): # hash style header #""" - self.assertEqual(MockView().get_description(), DESCRIPTION) - - def test_resource_description_can_be_set_explicitly(self): - """Ensure Resource descriptions can be set using the 'get_description' method.""" - example = 'Some other description' - - class MockView(APIView): - """docstring""" - def get_description(self): - return example - self.assertEqual(MockView().get_description(), example) + self.assertEqual(get_view_description(MockView), DESCRIPTION) - def test_resource_description_supports_unicode(self): + def test_view_description_supports_unicode(self): + """ + Unicode in docstrings should be respected. + """ class MockView(APIView): """Проверка""" pass - self.assertEqual(MockView().get_description(), "Проверка") - - - def test_resource_description_does_not_require_docstring(self): - """Ensure that empty docstrings do not affect the Resource's description if it has been set using the 'get_description' method.""" - example = 'Some other description' - - class MockView(APIView): - def get_description(self): - return example - self.assertEqual(MockView().get_description(), example) + self.assertEqual(get_view_description(MockView), "Проверка") - def test_resource_description_can_be_empty(self): - """Ensure that if a resource has no doctring or 'description' class attribute, then it's description is the empty string.""" + def test_view_description_can_be_empty(self): + """ + Ensure that if a view has no docstring, + then it's description is the empty string. + """ class MockView(APIView): pass - self.assertEqual(MockView().get_description(), '') + self.assertEqual(get_view_description(MockView), '') def test_markdown(self): - """Ensure markdown to HTML works as expected""" + """ + Ensure markdown to HTML works as expected. + """ if apply_markdown: gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 -- cgit v1.2.3 From 371698331c979305b5684f864ee6bf5b6d11a44e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Apr 2013 22:24:30 +0100 Subject: Tweaks --- rest_framework/generics.py | 9 +++------ rest_framework/mixins.py | 4 ++++ rest_framework/routers.py | 12 ++++++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 36ecf915..dea980a5 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -187,8 +187,7 @@ class UpdateAPIView(mixins.UpdateModelMixin, return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): - kwargs['partial'] = True - return self.update(request, *args, **kwargs) + return self.partial_update(request, *args, **kwargs) class ListCreateAPIView(mixins.ListModelMixin, @@ -217,8 +216,7 @@ class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): - kwargs['partial'] = True - return self.update(request, *args, **kwargs) + return self.partial_update(request, *args, **kwargs) class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, @@ -248,8 +246,7 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, return self.update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): - kwargs['partial'] = True - return self.update(request, *args, **kwargs) + return self.partial_update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 7d9a6e65..c700602e 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -137,6 +137,10 @@ class UpdateModelMixin(object): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def partial_update(self, request, *args, **kwargs): + kwargs['partial'] = True + return self.update(request, *args, **kwargs) + def pre_save(self, obj): """ Set any attributes on the object that are implicit in the request. diff --git a/rest_framework/routers.py b/rest_framework/routers.py index c37909ff..afc51f3b 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -20,8 +20,16 @@ class BaseRouter(object): class DefaultRouter(BaseRouter): route_list = [ - (r'$', {'get': 'list', 'post': 'create'}, 'list'), - (r'(?P[^/]+)/$', {'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}, 'detail'), + (r'$', { + 'get': 'list', + 'post': 'create' + }, 'list'), + (r'(?P[^/]+)/$', { + 'get': 'retrieve', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy' + }, 'detail'), ] extra_routes = r'(?P[^/]+)/%s/$' name_format = '%s-%s' -- cgit v1.2.3 From c73d0e1e39e661c7324eb0df8c3ce6e18f57915b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 18:22:39 +0100 Subject: Minor cleaning up on View --- rest_framework/compat.py | 20 ++++++++++++-------- rest_framework/views.py | 8 ++++---- 2 files changed, 16 insertions(+), 12 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 6551723a..8bfebe68 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -87,9 +87,7 @@ else: raise ImportError("User model is not to be found.") -# First implementation of Django class-based views did not include head method -# in base View class - https://code.djangoproject.com/ticket/15668 -if django.VERSION >= (1, 4): +if django.VERSION >= (1, 5): from django.views.generic import View else: from django.views.generic import View as _View @@ -97,6 +95,8 @@ else: from django.utils.functional import update_wrapper class View(_View): + # 1.3 does not include head method in base View class + # See: https://code.djangoproject.com/ticket/15668 @classonlymethod def as_view(cls, **initkwargs): """ @@ -126,11 +126,15 @@ else: update_wrapper(view, cls.dispatch, assigned=()) return view -# Taken from @markotibold's attempt at supporting PATCH. -# https://github.com/markotibold/django-rest-framework/tree/patch -http_method_names = set(View.http_method_names) -http_method_names.add('patch') -View.http_method_names = list(http_method_names) # PATCH method is not implemented by Django + # _allowed_methods only present from 1.5 onwards + def _allowed_methods(self): + return [m.upper() for m in self.http_method_names if hasattr(self, m)] + + +# PATCH method is not implemented by Django +if 'patch' not in View.http_method_names: + View.http_method_names = View.http_method_names + ['patch'] + # PUT, DELETE do not require CSRF until 1.4. They should. Make it better. if django.VERSION >= (1, 4): diff --git a/rest_framework/views.py b/rest_framework/views.py index 12298ca5..d7d3a2e2 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -38,10 +38,9 @@ class APIView(View): @property def allowed_methods(self): """ - Return the list of allowed HTTP methods, uppercased. + Wrap Django's private `_allowed_methods` interface in a public property. """ - return [method.upper() for method in self.http_method_names - if hasattr(self, method)] + return self._allowed_methods() @property def default_response_headers(self): @@ -69,7 +68,8 @@ class APIView(View): def http_method_not_allowed(self, request, *args, **kwargs): """ - Called if `request.method` does not correspond to a handler method. + If `request.method` does not correspond to a handler method, + determine what kind of exception to raise. """ raise exceptions.MethodNotAllowed(request.method) -- cgit v1.2.3 From 099163f81f9d89746de50f3aed2955ead54dba4e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 18:45:15 +0100 Subject: Removed SingleObjectMixin and MultipleObjectMixin --- rest_framework/generics.py | 139 +++++++++++++++++++++++++++++++++------------ rest_framework/mixins.py | 5 +- 2 files changed, 106 insertions(+), 38 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index dea980a5..af3b69da 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -4,21 +4,35 @@ Generic views that provide commonly needed behaviour. from __future__ import unicode_literals from rest_framework import views, mixins from rest_framework.settings import api_settings -from django.views.generic.detail import SingleObjectMixin -from django.views.generic.list import MultipleObjectMixin - +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.core.paginator import Paginator, InvalidPage +from django.http import Http404 +from django.utils.translation import ugettext as _ ### Base classes for the generic views ### + class GenericAPIView(views.APIView): """ Base class for all other generic views. """ - model = None + queryset = None serializer_class = None - model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS + filter_backend = api_settings.FILTER_BACKEND + paginate_by = api_settings.PAGINATE_BY + paginate_by_param = api_settings.PAGINATE_BY_PARAM + pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS + allow_empty = True + page_kwarg = 'page' + + # Pending deprecation + model = None + model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS + pk_url_kwarg = 'pk' # Not provided in Django 1.3 + slug_url_kwarg = 'slug' # Not provided in Django 1.3 + slug_field = 'slug' def filter_queryset(self, queryset): """ @@ -82,15 +96,7 @@ class GenericAPIView(views.APIView): """ pass - -class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): - """ - Base class for generic views onto a queryset. - """ - - paginate_by = api_settings.PAGINATE_BY - paginate_by_param = api_settings.PAGINATE_BY_PARAM - pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS + # Pagination def get_pagination_serializer(self, page=None): """ @@ -116,28 +122,81 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): pass return self.paginate_by - -class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): - """ - Base class for generic views onto a model instance. - """ - - pk_url_kwarg = 'pk' # Not provided in Django 1.3 - slug_url_kwarg = 'slug' # Not provided in Django 1.3 - slug_field = 'slug' + def paginate_queryset(self, queryset, page_size, paginator_class=Paginator): + """ + Paginate a queryset. + """ + paginator = paginator_class(queryset, page_size, allow_empty_first_page=self.allow_empty) + page_kwarg = self.page_kwarg + page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1 + try: + page_number = int(page) + except ValueError: + if page == 'last': + page_number = paginator.num_pages + else: + raise Http404(_("Page is not 'last', nor can it be converted to an int.")) + try: + page = paginator.page(page_number) + return (paginator, page, page.object_list, page.has_other_pages()) + except InvalidPage as e: + raise Http404(_('Invalid page (%(page_number)s): %(message)s') % { + 'page_number': page_number, + 'message': str(e) + }) + + def get_queryset(self): + """ + Get the list of items for this view. This must be an iterable, and may + be a queryset (in which qs-specific behavior will be enabled). + """ + if self.queryset is not None: + queryset = self.queryset + if hasattr(queryset, '_clone'): + queryset = queryset._clone() + elif self.model is not None: + queryset = self.model._default_manager.all() + else: + raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" + % self.__class__.__name__) + return queryset def get_object(self, queryset=None): """ - Override default to add support for object-level permissions. + Returns the object the view is displaying. + By default this requires `self.queryset` and a `pk` or `slug` argument + in the URLconf, but subclasses can override this to return any object. """ - obj = super(SingleObjectAPIView, self).get_object(queryset) + # Use a custom queryset if provided; this is required for subclasses + # like DateDetailView + if queryset is None: + queryset = self.get_queryset() + # Next, try looking up by primary key. + pk = self.kwargs.get(self.pk_url_kwarg, None) + slug = self.kwargs.get(self.slug_url_kwarg, None) + if pk is not None: + queryset = queryset.filter(pk=pk) + # Next, try looking up by slug. + elif slug is not None: + queryset = queryset.filter(**{self.slug_field: slug}) + # If none of those are defined, it's an error. + else: + raise AttributeError("Generic detail view %s must be called with " + "either an object pk or a slug." + % self.__class__.__name__) + try: + # Get the single item from the filtered queryset + obj = queryset.get() + except ObjectDoesNotExist: + raise Http404(_("No %(verbose_name)s found matching the query") % + {'verbose_name': queryset.model._meta.verbose_name}) + self.check_object_permissions(self.request, obj) return obj ### Concrete view classes that provide method handlers ### -### by composing the mixin classes with a base view. ### - +### by composing the mixin classes with the base view. ### class CreateAPIView(mixins.CreateModelMixin, GenericAPIView): @@ -150,7 +209,7 @@ class CreateAPIView(mixins.CreateModelMixin, class ListAPIView(mixins.ListModelMixin, - MultipleObjectAPIView): + GenericAPIView): """ Concrete view for listing a queryset. """ @@ -159,7 +218,7 @@ class ListAPIView(mixins.ListModelMixin, class RetrieveAPIView(mixins.RetrieveModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for retrieving a model instance. """ @@ -168,7 +227,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, class DestroyAPIView(mixins.DestroyModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for deleting a model instance. @@ -178,7 +237,7 @@ class DestroyAPIView(mixins.DestroyModelMixin, class UpdateAPIView(mixins.UpdateModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for updating a model instance. @@ -192,7 +251,7 @@ class UpdateAPIView(mixins.UpdateModelMixin, class ListCreateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, - MultipleObjectAPIView): + GenericAPIView): """ Concrete view for listing a queryset or creating a model instance. """ @@ -205,7 +264,7 @@ class ListCreateAPIView(mixins.ListModelMixin, class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for retrieving, updating a model instance. """ @@ -221,7 +280,7 @@ class RetrieveUpdateAPIView(mixins.RetrieveModelMixin, class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, mixins.DestroyModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for retrieving or deleting a model instance. """ @@ -235,7 +294,7 @@ class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, - SingleObjectAPIView): + GenericAPIView): """ Concrete view for retrieving, updating or deleting a model instance. """ @@ -250,3 +309,13 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, def delete(self, request, *args, **kwargs): return self.destroy(request, *args, **kwargs) + + +### Deprecated classes ### + +class MultipleObjectAPIView(GenericAPIView): + pass + + +class SingleObjectAPIView(GenericAPIView): + pass diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index c700602e..b15cb11f 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -72,8 +72,7 @@ class ListModelMixin(object): # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. - allow_empty = self.get_allow_empty() - if not allow_empty and not self.object_list: + if not self.allow_empty and not self.object_list: class_name = self.__class__.__name__ error_msg = self.empty_error % {'class_name': class_name} raise Http404(error_msg) @@ -148,7 +147,7 @@ class UpdateModelMixin(object): # pk and/or slug attributes are implicit in the URL. pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) - slug_field = slug and self.get_slug_field() or None + slug_field = slug and self.slug_field or None if pk: setattr(obj, 'pk', pk) -- cgit v1.2.3 From dc45bc7bfad64a17f3e5ed0f5a487bccc379aac2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 19:01:01 +0100 Subject: Add lookup_kwarg --- rest_framework/generics.py | 18 ++++++++++++------ rest_framework/tests/filterset.py | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index af3b69da..d4a50dcd 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -26,6 +26,7 @@ class GenericAPIView(views.APIView): pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS allow_empty = True page_kwarg = 'page' + lookup_kwarg = 'pk' # Pending deprecation model = None @@ -167,23 +168,26 @@ class GenericAPIView(views.APIView): By default this requires `self.queryset` and a `pk` or `slug` argument in the URLconf, but subclasses can override this to return any object. """ - # Use a custom queryset if provided; this is required for subclasses - # like DateDetailView + # Determine the base queryset to use. if queryset is None: queryset = self.get_queryset() - # Next, try looking up by primary key. + + # Perform the lookup filtering. pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) - if pk is not None: + lookup = self.kwargs.get(self.lookup_kwarg, None) + + if lookup is not None: + queryset = queryset.filter(**{self.lookup_kwarg: lookup}) + elif pk is not None: queryset = queryset.filter(pk=pk) - # Next, try looking up by slug. elif slug is not None: queryset = queryset.filter(**{self.slug_field: slug}) - # If none of those are defined, it's an error. else: raise AttributeError("Generic detail view %s must be called with " "either an object pk or a slug." % self.__class__.__name__) + try: # Get the single item from the filtered queryset obj = queryset.get() @@ -191,7 +195,9 @@ class GenericAPIView(views.APIView): raise Http404(_("No %(verbose_name)s found matching the query") % {'verbose_name': queryset.model._meta.verbose_name}) + # May raise a permission denied self.check_object_permissions(self.request, obj) + return obj diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 1a71558c..1e53a5cd 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -61,7 +61,7 @@ if django_filters: class CommonFilteringTestCase(TestCase): def _serialize_object(self, obj): return {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} - + def setUp(self): """ Create 10 FilterableItem instances. @@ -190,7 +190,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): Integration tests for filtered detail views. """ urls = 'rest_framework.tests.filterset' - + def _get_url(self, item): return reverse('detail-view', kwargs=dict(pk=item.pk)) @@ -221,7 +221,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, low_item_data) - + # Tests that multiple filters works. search_decimal = Decimal('5.25') search_date = datetime.date(2012, 10, 2) -- cgit v1.2.3 From 1de6cff11b71e4aaa7b76219d4d2118021e23a00 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 19:06:49 +0100 Subject: Cleaning up get_object and get_queryset --- rest_framework/generics.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index d4a50dcd..4ae2ac8e 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -148,25 +148,22 @@ class GenericAPIView(views.APIView): def get_queryset(self): """ - Get the list of items for this view. This must be an iterable, and may - be a queryset (in which qs-specific behavior will be enabled). + Get the list of items for this view. + + This must be an iterable, and may be a queryset. """ if self.queryset is not None: - queryset = self.queryset - if hasattr(queryset, '_clone'): - queryset = queryset._clone() - elif self.model is not None: - queryset = self.model._default_manager.all() - else: - raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" - % self.__class__.__name__) - return queryset + return self.queryset._clone() + + if self.model is not None: + return self.model._default_manager.all() + + raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" + % self.__class__.__name__) def get_object(self, queryset=None): """ Returns the object the view is displaying. - By default this requires `self.queryset` and a `pk` or `slug` argument - in the URLconf, but subclasses can override this to return any object. """ # Determine the base queryset to use. if queryset is None: -- cgit v1.2.3 From 9bb1277e512a88e6c11c52457d0c24e73f30bb98 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 19:37:19 +0100 Subject: Cleaning up around bits of API that will be pending deprecation --- rest_framework/generics.py | 116 +++++++++++++++++++++++++++------------------ rest_framework/mixins.py | 9 ++-- rest_framework/viewsets.py | 6 +-- 3 files changed, 75 insertions(+), 56 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 4ae2ac8e..124dba38 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -35,15 +35,6 @@ class GenericAPIView(views.APIView): slug_url_kwarg = 'slug' # Not provided in Django 1.3 slug_field = 'slug' - def filter_queryset(self, queryset): - """ - Given a queryset, filter it with whichever filter backend is in use. - """ - if not self.filter_backend: - return queryset - backend = self.filter_backend() - return backend.filter_queryset(self.request, queryset, self) - def get_serializer_context(self): """ Extra context provided to the serializer class. @@ -54,24 +45,6 @@ class GenericAPIView(views.APIView): 'view': self } - def get_serializer_class(self): - """ - Return the class to use for the serializer. - - Defaults to using `self.serializer_class`, falls back to constructing a - model serializer class using `self.model_serializer_class`, with - `self.model` as the model. - """ - serializer_class = self.serializer_class - - if serializer_class is None: - class DefaultSerializer(self.model_serializer_class): - class Meta: - model = self.model - serializer_class = DefaultSerializer - - return serializer_class - def get_serializer(self, instance=None, data=None, files=None, many=False, partial=False): """ @@ -83,22 +56,6 @@ class GenericAPIView(views.APIView): return serializer_class(instance, data=data, files=files, many=many, partial=partial, context=context) - def pre_save(self, obj): - """ - Placeholder method for calling before saving an object. - May be used eg. to set attributes on the object that are implicit - in either the request, or the url. - """ - pass - - def post_save(self, obj, created=False): - """ - Placeholder method for calling after saving an object. - """ - pass - - # Pagination - def get_pagination_serializer(self, page=None): """ Return a serializer instance to use with paginated data. @@ -111,9 +68,14 @@ class GenericAPIView(views.APIView): context = self.get_serializer_context() return pagination_serializer_class(instance=page, context=context) - def get_paginate_by(self, queryset): + def get_paginate_by(self, queryset=None): """ Return the size of pages to use with pagination. + + If `PAGINATE_BY_PARAM` is set it will attempt to get the page size + from a named query parameter in the url, eg. ?page_size=100 + + Otherwise defaults to using `self.paginate_by`. """ if self.paginate_by_param: query_params = self.request.QUERY_PARAMS @@ -121,6 +83,7 @@ class GenericAPIView(views.APIView): return int(query_params[self.paginate_by_param]) except (KeyError, ValueError): pass + return self.paginate_by def paginate_queryset(self, queryset, page_size, paginator_class=Paginator): @@ -146,16 +109,54 @@ class GenericAPIView(views.APIView): 'message': str(e) }) + def filter_queryset(self, queryset): + """ + Given a queryset, filter it with whichever filter backend is in use. + """ + if not self.filter_backend: + return queryset + backend = self.filter_backend() + return backend.filter_queryset(self.request, queryset, self) + + ### The following methods provide default implementations + ### that you may want to override for more complex cases. + + def get_serializer_class(self): + """ + Return the class to use for the serializer. + Defaults to using `self.serializer_class`. + + You may want to override this if you need to provide different + serializations depending on the incoming request. + + (Eg. admins get full serialization, others get basic serilization) + """ + serializer_class = self.serializer_class + if serializer_class is not None: + return serializer_class + + # TODO: Deprecation warning + class DefaultSerializer(self.model_serializer_class): + class Meta: + model = self.model + return DefaultSerializer + def get_queryset(self): """ Get the list of items for this view. - This must be an iterable, and may be a queryset. + Defaults to using `self.queryset`. + + You may want to override this if you need to provide different + querysets depending on the incoming request. + + (Eg. return a list of items that is specific to the user) """ if self.queryset is not None: return self.queryset._clone() if self.model is not None: + # TODO: Deprecation warning return self.model._default_manager.all() raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" @@ -164,10 +165,14 @@ class GenericAPIView(views.APIView): def get_object(self, queryset=None): """ Returns the object the view is displaying. + + You may want to override this if you need to provide non-standard + queryset lookups. Eg if objects are referenced using multiple + keyword arguments in the url conf. """ # Determine the base queryset to use. if queryset is None: - queryset = self.get_queryset() + queryset = self.filter_queryset(self.get_queryset()) # Perform the lookup filtering. pk = self.kwargs.get(self.pk_url_kwarg, None) @@ -177,8 +182,10 @@ class GenericAPIView(views.APIView): if lookup is not None: queryset = queryset.filter(**{self.lookup_kwarg: lookup}) elif pk is not None: + # TODO: Deprecation warning queryset = queryset.filter(pk=pk) elif slug is not None: + # TODO: Deprecation warning queryset = queryset.filter(**{self.slug_field: slug}) else: raise AttributeError("Generic detail view %s must be called with " @@ -197,6 +204,23 @@ class GenericAPIView(views.APIView): return obj + ### The following methods are intended to be overridden. + + def pre_save(self, obj): + """ + Placeholder method for calling before saving an object. + + May be used to set attributes on the object that are implicit + in either the request, or the url. + """ + pass + + def post_save(self, obj, created=False): + """ + Placeholder method for calling after saving an object. + """ + pass + ### Concrete view classes that provide method handlers ### ### by composing the mixin classes with the base view. ### diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index b15cb11f..6e40b5c4 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -67,8 +67,7 @@ class ListModelMixin(object): empty_error = "Empty list and '%(class_name)s.allow_empty' is False." def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - self.object_list = self.filter_queryset(queryset) + self.object_list = self.filter_queryset(self.get_queryset()) # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. @@ -79,7 +78,7 @@ class ListModelMixin(object): # Pagination size is set by the `.paginate_by` attribute, # which may be `None` to disable pagination. - page_size = self.get_paginate_by(self.object_list) + page_size = self.get_paginate_by() if page_size: packed = self.paginate_queryset(self.object_list, page_size) paginator, page, queryset, is_paginated = packed @@ -96,9 +95,7 @@ class RetrieveModelMixin(object): Should be mixed in with `SingleObjectAPIView`. """ def retrieve(self, request, *args, **kwargs): - queryset = self.get_queryset() - filtered_queryset = self.filter_queryset(queryset) - self.object = self.get_object(filtered_queryset) + self.object = self.get_object() serializer = self.get_serializer(self.object) return Response(serializer.data) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 0818c0d9..28ab30e2 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -76,14 +76,12 @@ class ModelViewSet(mixins.CreateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, ViewSetMixin, - generics.MultipleObjectAPIView, - generics.SingleObjectAPIView): + generics.GenericAPIView): pass class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, ViewSetMixin, - generics.MultipleObjectAPIView, - generics.SingleObjectAPIView): + generics.GenericAPIView): pass -- cgit v1.2.3 From 07af4373616c28e7600ee2ec7981b5a1d0a92f7d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Apr 2013 19:47:16 +0100 Subject: Cleaning up around bits of API that will be pending deprecation --- rest_framework/generics.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 124dba38..ba7d1f43 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -24,15 +24,17 @@ class GenericAPIView(views.APIView): paginate_by = api_settings.PAGINATE_BY paginate_by_param = api_settings.PAGINATE_BY_PARAM pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS - allow_empty = True page_kwarg = 'page' lookup_kwarg = 'pk' + allow_empty = True + + ###################################### + # These are all pending deprecation... - # Pending deprecation model = None model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS - pk_url_kwarg = 'pk' # Not provided in Django 1.3 - slug_url_kwarg = 'slug' # Not provided in Django 1.3 + pk_url_kwarg = 'pk' + slug_url_kwarg = 'slug' slug_field = 'slug' def get_serializer_context(self): @@ -90,7 +92,8 @@ class GenericAPIView(views.APIView): """ Paginate a queryset. """ - paginator = paginator_class(queryset, page_size, allow_empty_first_page=self.allow_empty) + paginator = paginator_class(queryset, page_size, + allow_empty_first_page=self.allow_empty) page_kwarg = self.page_kwarg page = self.kwargs.get(page_kwarg) or self.request.GET.get(page_kwarg) or 1 try: @@ -118,6 +121,7 @@ class GenericAPIView(views.APIView): backend = self.filter_backend() return backend.filter_queryset(self.request, queryset, self) + ######################## ### The following methods provide default implementations ### that you may want to override for more complex cases. @@ -204,7 +208,9 @@ class GenericAPIView(views.APIView): return obj - ### The following methods are intended to be overridden. + ######################## + ### The following are placeholder methods, + ### and are intended to be overridden. def pre_save(self, obj): """ @@ -222,8 +228,10 @@ class GenericAPIView(views.APIView): pass +########################################################## ### Concrete view classes that provide method handlers ### ### by composing the mixin classes with the base view. ### +########################################################## class CreateAPIView(mixins.CreateModelMixin, GenericAPIView): @@ -338,7 +346,9 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, return self.destroy(request, *args, **kwargs) +########################## ### Deprecated classes ### +########################## class MultipleObjectAPIView(GenericAPIView): pass -- cgit v1.2.3 From e0020c5b033308cd789408a8823d6707deed8032 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Apr 2013 15:48:18 +0100 Subject: Simplify get_object --- rest_framework/generics.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index ba7d1f43..ea62123d 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -4,9 +4,10 @@ Generic views that provide commonly needed behaviour. from __future__ import unicode_literals from rest_framework import views, mixins from rest_framework.settings import api_settings -from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Paginator, InvalidPage from django.http import Http404 +from django.shortcuts import get_object_or_404 from django.utils.translation import ugettext as _ ### Base classes for the generic views ### @@ -163,7 +164,7 @@ class GenericAPIView(views.APIView): # TODO: Deprecation warning return self.model._default_manager.all() - raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'" + raise ImproperlyConfigured("'%s' must define 'queryset'" % self.__class__.__name__) def get_object(self, queryset=None): @@ -177,6 +178,8 @@ class GenericAPIView(views.APIView): # Determine the base queryset to use. if queryset is None: queryset = self.filter_queryset(self.get_queryset()) + else: + pass # Deprecation warning # Perform the lookup filtering. pk = self.kwargs.get(self.pk_url_kwarg, None) @@ -184,24 +187,19 @@ class GenericAPIView(views.APIView): lookup = self.kwargs.get(self.lookup_kwarg, None) if lookup is not None: - queryset = queryset.filter(**{self.lookup_kwarg: lookup}) + filter_kwargs = {self.lookup_kwarg: lookup} elif pk is not None: # TODO: Deprecation warning - queryset = queryset.filter(pk=pk) + filter_kwargs = {'pk': pk} elif slug is not None: # TODO: Deprecation warning - queryset = queryset.filter(**{self.slug_field: slug}) + filter_kwargs = {self.slug_field: slug} else: raise AttributeError("Generic detail view %s must be called with " "either an object pk or a slug." % self.__class__.__name__) - try: - # Get the single item from the filtered queryset - obj = queryset.get() - except ObjectDoesNotExist: - raise Http404(_("No %(verbose_name)s found matching the query") % - {'verbose_name': queryset.model._meta.verbose_name}) + obj = get_object_or_404(queryset, **filter_kwargs) # May raise a permission denied self.check_object_permissions(self.request, obj) -- cgit v1.2.3