aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/api-guide/permissions.md1
-rw-r--r--rest_framework/generics.py3
-rw-r--r--rest_framework/mixins.py4
-rw-r--r--rest_framework/renderers.py71
-rw-r--r--rest_framework/templates/rest_framework/base.html8
-rw-r--r--rest_framework/tests/renderers.py31
-rw-r--r--rest_framework/views.py10
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)