diff options
| author | Tom Christie | 2012-09-27 21:51:46 +0100 | 
|---|---|---|
| committer | Tom Christie | 2012-09-27 21:51:46 +0100 | 
| commit | ee36e4ab0c0508a590c6b73a23ec82b7f1e49bd0 (patch) | |
| tree | 8c74086caf32984fe5adb9776558f04e5e56dc76 | |
| parent | 4d906938a9304be119a9b15f6540a8cbab6f1154 (diff) | |
| download | django-rest-framework-ee36e4ab0c0508a590c6b73a23ec82b7f1e49bd0.tar.bz2 | |
Only display forms when user has permissions.  #159
| -rw-r--r-- | docs/api-guide/permissions.md | 1 | ||||
| -rw-r--r-- | rest_framework/generics.py | 3 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 4 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 71 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 8 | ||||
| -rw-r--r-- | rest_framework/tests/renderers.py | 31 | ||||
| -rw-r--r-- | rest_framework/views.py | 10 | 
7 files changed, 93 insertions, 35 deletions
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index e0ceb1ea..9a3c39ab 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -92,7 +92,6 @@ To implement a custom permission, override `BasePermission` and implement the `.  The method should return `True` if the request should be granted access, and `False` otherwise. -  [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html  [authentication]: authentication.md  [throttling]: throttling.md diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 106c9992..f70c6f19 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -42,7 +42,8 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView):          Override default to add support for object-level permissions.          """          obj = super(SingleObjectBaseView, self).get_object() -        self.check_permissions(self.request, obj) +        if not self.has_permission(self.request, obj): +            self.permission_denied(self.request)          return obj diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 33a363d3..fe12dc8f 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -88,8 +88,8 @@ class MetadataMixin(object):              'parses': [parser.media_type for parser in self.parser_classes],          }          #  TODO: Add 'fields', from serializer info. -        # form = self.get_bound_form() -        # if form is not None: +        # serializer = self.get_serializer() +        # if serializer is not None:          #     field_name_types = {}          #     for name, field in form.fields.iteritems():          #         field_name_types[name] = field.__class__.__name__ diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index eeba7339..35a2a733 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -5,6 +5,7 @@ Django REST framework also provides HTML and PlainText renderers that help self-  by serializing the output along with documentation regarding the View, output status and headers,  and providing forms and links depending on the allowed methods, renderers and parsers on the View.  """ +import copy  import string  from django import forms  from django.template import RequestContext, loader @@ -193,7 +194,7 @@ class DocumentingHTMLRenderer(BaseRenderer):      format = 'html'      template = 'rest_framework/api.html' -    def _get_content(self, view, request, obj, media_type): +    def get_content(self, view, request, obj, media_type):          """          Get the content as if it had been rendered by a non-documenting renderer. @@ -214,14 +215,31 @@ class DocumentingHTMLRenderer(BaseRenderer):          return content -    def _get_form_instance(self, view, method): +    def get_form(self, view, method, request):          """          Get a form, possibly bound to either the input or output data.          In the absence on of the Resource having an associated form then          provide a form that can be used to submit arbitrary content.          """ -        if not hasattr(self.view, 'get_serializer'):  # No serializer, no form. -            return +        if not method in view.allowed_methods: +            return  # Not a valid method + +        if not api_settings.FORM_METHOD_OVERRIDE: +            return  # Cannot use form overloading + +        temp = request._method +        request._method = method.upper() +        if not view.has_permission(request): +            request._method = temp +            return  # Don't have permission +        request._method = temp + +        if method == 'DELETE' or method == 'OPTIONS': +            return True  # Don't actually need to return a form + +        if not getattr(view, 'get_serializer', None): +            return self.get_generic_content_form(view) +          #  We need to map our Fields to Django's Fields.          field_mapping = dict([           [serializers.FloatField.__name__, forms.FloatField], @@ -236,20 +254,20 @@ class DocumentingHTMLRenderer(BaseRenderer):          # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python          fields = {}          object, data = None, None -        if hasattr(self.view, 'object'): -            object = self.view.object -        serializer = self.view.get_serializer(instance=object) +        if getattr(view, 'object', None): +            object = view.object +        serializer = view.get_serializer(instance=object)          for k, v in serializer.fields.items():              if v.readonly:                  continue              fields[k] = field_mapping[v.__class__.__name__]()          OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) -        if object and not self.view.request.method == 'DELETE':  # Don't fill in the form when the object is deleted +        if object and not view.request.method == 'DELETE':  # Don't fill in the form when the object is deleted              data = serializer.data          form_instance = OnTheFlyForm(data)          return form_instance -    def _get_generic_content_form(self, view): +    def get_generic_content_form(self, view):          """          Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms          (Which are typically application/x-www-form-urlencoded) @@ -257,7 +275,8 @@ class DocumentingHTMLRenderer(BaseRenderer):          # If we're not using content overloading there's no point in supplying a generic form,          # as the view won't treat the form's value as the content of the request. -        if not getattr(view.request, '_USE_FORM_OVERLOADING', False): +        if not (api_settings.FORM_CONTENT_OVERRIDE +                and api_settings.FORM_CONTENTTYPE_OVERRIDE):              return None          # NB. http://jacobian.org/writing/dynamic-form-generation/ @@ -272,11 +291,15 @@ class DocumentingHTMLRenderer(BaseRenderer):                  contenttype_choices = [(media_type, media_type) for media_type in parsed_media_types]                  initial_contenttype = parsed_media_types[0] -                self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', -                                                                         choices=contenttype_choices, -                                                                         initial=initial_contenttype) -                self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content', -                                                                   widget=forms.Textarea) +                self.fields[api_settings.FORM_CONTENTTYPE_OVERRIDE] = forms.ChoiceField( +                    label='Content Type', +                    choices=contenttype_choices, +                    initial=initial_contenttype +                ) +                self.fields[api_settings.FORM_CONTENT_OVERRIDE] = forms.CharField( +                    label='Content', +                    widget=forms.Textarea +                )          # If either of these reserved parameters are turned off then content tunneling is not possible          if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None: @@ -310,10 +333,12 @@ class DocumentingHTMLRenderer(BaseRenderer):          request = view.request          response = view.response -        content = self._get_content(view, request, obj, media_type) +        content = self.get_content(view, request, obj, media_type) -        put_form_instance = self._get_form_instance(self.view, 'put') -        post_form_instance = self._get_form_instance(self.view, 'post') +        put_form = self.get_form(view, 'PUT', request) +        post_form = self.get_form(view, 'POST', request) +        delete_form = self.get_form(view, 'DELETE', request) +        options_form = self.get_form(view, 'OPTIONS', request)          name = self.get_name()          description = self.get_description() @@ -330,10 +355,12 @@ class DocumentingHTMLRenderer(BaseRenderer):              'name': name,              'version': VERSION,              'breadcrumblist': breadcrumb_list, -            'allowed_methods': self.view.allowed_methods, -            'available_formats': [renderer.format for renderer in self.view.renderer_classes], -            'put_form': put_form_instance, -            'post_form': post_form_instance, +            'allowed_methods': view.allowed_methods, +            'available_formats': [renderer.format for renderer in view.renderer_classes], +            'put_form': put_form, +            'post_form': post_form, +            'delete_form': delete_form, +            'options_form': options_form,              'api_settings': api_settings          }) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index d0a25e2e..867051e6 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -89,7 +89,7 @@              </form>          {% endif %} -        {% if 'OPTIONS' in allowed_methods and api_settings.FORM_METHOD_OVERRIDE %} +        {% if options_form %}              <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">                  {% csrf_token %}                  <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" /> @@ -97,7 +97,7 @@              </form>          {% endif %} -        {% if 'DELETE' in allowed_methods and api_settings.FORM_METHOD_OVERRIDE %} +        {% if delete_form %}              <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">                  {% csrf_token %}                  <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" /> @@ -121,7 +121,7 @@              {% if response.status_code != 403 %} -                {% if 'POST' in allowed_methods %} +                {% if post_form %}                      <form action="{{ request.get_full_path }}" method="POST" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">                          <fieldset>                              <h2>POST: {{ name }}</h2> @@ -144,7 +144,7 @@                      </form>                  {% endif %} -                {% if 'PUT' in allowed_methods and api_settings.FORM_METHOD_OVERRIDE %} +                {% if put_form %}                      <form action="{{ request.get_full_path }}" method="POST" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">                          <fieldset>                              <h2>PUT: {{ name }}</h2> diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index b7c386a3..751f548f 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -2,8 +2,9 @@ import re  from django.conf.urls.defaults import patterns, url, include  from django.test import TestCase +from django.test.client import RequestFactory -from rest_framework import status +from rest_framework import status, permissions  from rest_framework.compat import yaml  from rest_framework.response import Response  from rest_framework.views import APIView @@ -89,6 +90,34 @@ urlpatterns = patterns('',  ) +class POSTDeniedPermission(permissions.BasePermission): +    def has_permission(self, request, obj=None): +        return request.method != 'POST' + + +class POSTDeniedView(APIView): +    renderer_classes = (DocumentingHTMLRenderer,) +    permission_classes = (POSTDeniedPermission,) + +    def get(self, request): +        return Response() + +    def post(self, request): +        return Response() + +    def put(self, request): +        return Response() + + +class DocumentingRendererTests(TestCase): +    def test_only_permitted_forms_are_displayed(self): +        view = POSTDeniedView.as_view() +        request = RequestFactory().get('/') +        response = view(request).render() +        self.assertNotContains(response, '>POST<') +        self.assertContains(response, '>PUT<') + +  class RendererEndToEndTests(TestCase):      """      End-to-end testing of renderers using an RendererMixin on a generic view. diff --git a/rest_framework/views.py b/rest_framework/views.py index 3605b1ee..c1a066d3 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -169,13 +169,14 @@ class APIView(View):          conneg = self.content_negotiation_class()          return conneg.negotiate(request, renderers, self.format, force) -    def check_permissions(self, request, obj=None): +    def has_permission(self, request, obj=None):          """ -        Check if request should be permitted. +        Return `True` if the request should be permitted.          """          for permission in self.get_permissions():              if not permission.has_permission(request, obj): -                self.permission_denied(request) +                return False +        return True      def check_throttles(self, request):          """ @@ -197,7 +198,8 @@ class APIView(View):          Runs anything that needs to occur prior to calling the method handlers.          """          self.format = self.get_format_suffix(**kwargs) -        self.check_permissions(request) +        if not self.has_permission(request): +            self.permission_denied(request)          self.check_throttles(request)          self.renderer, self.accepted_media_type = self.perform_content_negotiation(request)  | 
