mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-24 00:04:16 +03:00
Only display forms when user has permissions. #159
This commit is contained in:
parent
4d906938a9
commit
ee36e4ab0c
|
@ -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.
|
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
|
[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
|
||||||
[authentication]: authentication.md
|
[authentication]: authentication.md
|
||||||
[throttling]: throttling.md
|
[throttling]: throttling.md
|
||||||
|
|
|
@ -42,7 +42,8 @@ class SingleObjectBaseView(SingleObjectMixin, BaseView):
|
||||||
Override default to add support for object-level permissions.
|
Override default to add support for object-level permissions.
|
||||||
"""
|
"""
|
||||||
obj = super(SingleObjectBaseView, self).get_object()
|
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
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -88,8 +88,8 @@ class MetadataMixin(object):
|
||||||
'parses': [parser.media_type for parser in self.parser_classes],
|
'parses': [parser.media_type for parser in self.parser_classes],
|
||||||
}
|
}
|
||||||
# TODO: Add 'fields', from serializer info.
|
# TODO: Add 'fields', from serializer info.
|
||||||
# form = self.get_bound_form()
|
# serializer = self.get_serializer()
|
||||||
# if form is not None:
|
# if serializer is not None:
|
||||||
# field_name_types = {}
|
# field_name_types = {}
|
||||||
# for name, field in form.fields.iteritems():
|
# for name, field in form.fields.iteritems():
|
||||||
# field_name_types[name] = field.__class__.__name__
|
# field_name_types[name] = field.__class__.__name__
|
||||||
|
|
|
@ -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,
|
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.
|
and providing forms and links depending on the allowed methods, renderers and parsers on the View.
|
||||||
"""
|
"""
|
||||||
|
import copy
|
||||||
import string
|
import string
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.template import RequestContext, loader
|
from django.template import RequestContext, loader
|
||||||
|
@ -193,7 +194,7 @@ class DocumentingHTMLRenderer(BaseRenderer):
|
||||||
format = 'html'
|
format = 'html'
|
||||||
template = 'rest_framework/api.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.
|
Get the content as if it had been rendered by a non-documenting renderer.
|
||||||
|
|
||||||
|
@ -214,14 +215,31 @@ class DocumentingHTMLRenderer(BaseRenderer):
|
||||||
|
|
||||||
return content
|
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.
|
Get a form, possibly bound to either the input or output data.
|
||||||
In the absence on of the Resource having an associated form then
|
In the absence on of the Resource having an associated form then
|
||||||
provide a form that can be used to submit arbitrary content.
|
provide a form that can be used to submit arbitrary content.
|
||||||
"""
|
"""
|
||||||
if not hasattr(self.view, 'get_serializer'): # No serializer, no form.
|
if not method in view.allowed_methods:
|
||||||
return
|
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.
|
# We need to map our Fields to Django's Fields.
|
||||||
field_mapping = dict([
|
field_mapping = dict([
|
||||||
[serializers.FloatField.__name__, forms.FloatField],
|
[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
|
# Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
|
||||||
fields = {}
|
fields = {}
|
||||||
object, data = None, None
|
object, data = None, None
|
||||||
if hasattr(self.view, 'object'):
|
if getattr(view, 'object', None):
|
||||||
object = self.view.object
|
object = view.object
|
||||||
serializer = self.view.get_serializer(instance=object)
|
serializer = view.get_serializer(instance=object)
|
||||||
for k, v in serializer.fields.items():
|
for k, v in serializer.fields.items():
|
||||||
if v.readonly:
|
if v.readonly:
|
||||||
continue
|
continue
|
||||||
fields[k] = field_mapping[v.__class__.__name__]()
|
fields[k] = field_mapping[v.__class__.__name__]()
|
||||||
OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields)
|
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
|
data = serializer.data
|
||||||
form_instance = OnTheFlyForm(data)
|
form_instance = OnTheFlyForm(data)
|
||||||
return form_instance
|
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
|
Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms
|
||||||
(Which are typically application/x-www-form-urlencoded)
|
(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,
|
# 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.
|
# 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
|
return None
|
||||||
|
|
||||||
# NB. http://jacobian.org/writing/dynamic-form-generation/
|
# 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]
|
contenttype_choices = [(media_type, media_type) for media_type in parsed_media_types]
|
||||||
initial_contenttype = parsed_media_types[0]
|
initial_contenttype = parsed_media_types[0]
|
||||||
|
|
||||||
self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type',
|
self.fields[api_settings.FORM_CONTENTTYPE_OVERRIDE] = forms.ChoiceField(
|
||||||
|
label='Content Type',
|
||||||
choices=contenttype_choices,
|
choices=contenttype_choices,
|
||||||
initial=initial_contenttype)
|
initial=initial_contenttype
|
||||||
self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content',
|
)
|
||||||
widget=forms.Textarea)
|
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 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:
|
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
|
request = view.request
|
||||||
response = view.response
|
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')
|
put_form = self.get_form(view, 'PUT', request)
|
||||||
post_form_instance = self._get_form_instance(self.view, 'post')
|
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()
|
name = self.get_name()
|
||||||
description = self.get_description()
|
description = self.get_description()
|
||||||
|
@ -330,10 +355,12 @@ class DocumentingHTMLRenderer(BaseRenderer):
|
||||||
'name': name,
|
'name': name,
|
||||||
'version': VERSION,
|
'version': VERSION,
|
||||||
'breadcrumblist': breadcrumb_list,
|
'breadcrumblist': breadcrumb_list,
|
||||||
'allowed_methods': self.view.allowed_methods,
|
'allowed_methods': view.allowed_methods,
|
||||||
'available_formats': [renderer.format for renderer in self.view.renderer_classes],
|
'available_formats': [renderer.format for renderer in view.renderer_classes],
|
||||||
'put_form': put_form_instance,
|
'put_form': put_form,
|
||||||
'post_form': post_form_instance,
|
'post_form': post_form,
|
||||||
|
'delete_form': delete_form,
|
||||||
|
'options_form': options_form,
|
||||||
'api_settings': api_settings
|
'api_settings': api_settings
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -89,7 +89,7 @@
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% 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">
|
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
|
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% 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">
|
<form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
|
<input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
|
||||||
|
@ -121,7 +121,7 @@
|
||||||
|
|
||||||
{% if response.status_code != 403 %}
|
{% 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">
|
<form action="{{ request.get_full_path }}" method="POST" {% if post_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>POST: {{ name }}</h2>
|
<h2>POST: {{ name }}</h2>
|
||||||
|
@ -144,7 +144,7 @@
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% 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">
|
<form action="{{ request.get_full_path }}" method="POST" {% if put_form.is_multipart %}enctype="multipart/form-data"{% endif %} class="form-horizontal">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<h2>PUT: {{ name }}</h2>
|
<h2>PUT: {{ name }}</h2>
|
||||||
|
|
|
@ -2,8 +2,9 @@ import re
|
||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url, include
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
from django.test import TestCase
|
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.compat import yaml
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
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):
|
class RendererEndToEndTests(TestCase):
|
||||||
"""
|
"""
|
||||||
End-to-end testing of renderers using an RendererMixin on a generic view.
|
End-to-end testing of renderers using an RendererMixin on a generic view.
|
||||||
|
|
|
@ -169,13 +169,14 @@ class APIView(View):
|
||||||
conneg = self.content_negotiation_class()
|
conneg = self.content_negotiation_class()
|
||||||
return conneg.negotiate(request, renderers, self.format, force)
|
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():
|
for permission in self.get_permissions():
|
||||||
if not permission.has_permission(request, obj):
|
if not permission.has_permission(request, obj):
|
||||||
self.permission_denied(request)
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def check_throttles(self, request):
|
def check_throttles(self, request):
|
||||||
"""
|
"""
|
||||||
|
@ -197,7 +198,8 @@ class APIView(View):
|
||||||
Runs anything that needs to occur prior to calling the method handlers.
|
Runs anything that needs to occur prior to calling the method handlers.
|
||||||
"""
|
"""
|
||||||
self.format = self.get_format_suffix(**kwargs)
|
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.check_throttles(request)
|
||||||
self.renderer, self.accepted_media_type = self.perform_content_negotiation(request)
|
self.renderer, self.accepted_media_type = self.perform_content_negotiation(request)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user