aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/topics/browser-enhancements.md49
-rw-r--r--rest_framework/decorators.py14
-rw-r--r--rest_framework/exceptions.py8
-rw-r--r--rest_framework/fields.py37
-rw-r--r--rest_framework/generics.py2
-rw-r--r--rest_framework/negotiation.py3
-rw-r--r--rest_framework/reverse.py8
-rw-r--r--rest_framework/serializers.py5
-rw-r--r--rest_framework/static/rest_framework/css/default.css14
-rw-r--r--rest_framework/templates/rest_framework/base.html2
-rw-r--r--rest_framework/templates/rest_framework/login.html56
-rw-r--r--rest_framework/tests/generics.py49
-rw-r--r--rest_framework/tests/models.py10
-rw-r--r--rest_framework/tests/serializer.py13
-rw-r--r--rest_framework/throttling.py2
-rw-r--r--rest_framework/views.py2
16 files changed, 198 insertions, 76 deletions
diff --git a/docs/topics/browser-enhancements.md b/docs/topics/browser-enhancements.md
index d4e128ae..6a11f0fa 100644
--- a/docs/topics/browser-enhancements.md
+++ b/docs/topics/browser-enhancements.md
@@ -2,42 +2,63 @@
> "There are two noncontroversial uses for overloaded POST. The first is to *simulate* HTTP's uniform interface for clients like web browsers that don't support PUT or DELETE"
>
-> — [RESTful Web Services](1), Leonard Richardson & Sam Ruby.
+> — [RESTful Web Services][cite], Leonard Richardson & Sam Ruby.
## Browser based PUT, DELETE, etc...
-**TODO: Preamble.** Note that this is the same strategy as is used in [Ruby on Rails](2).
+REST framework supports browser-based `PUT`, `DELETE` and other methods, by
+overloading `POST` requests using a hidden form field.
+
+Note that this is the same strategy as is used in [Ruby on Rails][rails].
For example, given the following form:
<form action="/news-items/5" method="POST">
- <input type="hidden" name="_method" value="DELETE">
- </form>
+ <input type="hidden" name="_method" value="DELETE">
+ </form>
`request.method` would return `"DELETE"`.
## Browser based submission of non-form content
-Browser-based submission of content types other than form are supported by using form fields named `_content` and `_content_type`:
+Browser-based submission of content types other than form are supported by
+using form fields named `_content` and `_content_type`:
For example, given the following form:
<form action="/news-items/5" method="PUT">
- <input type="hidden" name="_content_type" value="application/json">
- <input name="_content" value="{'count': 1}">
- </form>
+ <input type="hidden" name="_content_type" value="application/json">
+ <input name="_content" value="{'count': 1}">
+ </form>
-`request.content_type` would return `"application/json"`, and `request.stream` would return `"{'count': 1}"`
+`request.content_type` would return `"application/json"`, and
+`request.stream` would return `"{'count': 1}"`
## URL based accept headers
+REST framework can take `?accept=application/json` style URL parameters,
+which allow the `Accept` header to be overridden.
+
+This can be useful for testing the API from a web browser, where you don't
+have any control over what is sent in the `Accept` header.
+
## URL based format suffixes
+REST framework can take `?format=json` style URL parameters, which can be a
+useful shortcut for determing which content type should be returned from
+the view.
+
+This is a more concise than using the `accept` override, but it also gives
+you less control. (For example you can't specify any media type parameters)
+
## Doesn't HTML5 support PUT and DELETE forms?
-Nope. It was at one point intended to support `PUT` and `DELETE` forms, but was later [dropped from the spec](3). There remains [ongoing discussion](4) about adding support for `PUT` and `DELETE`, as well as how to support content types other than form-encoded data.
+Nope. It was at one point intended to support `PUT` and `DELETE` forms, but
+was later [dropped from the spec][html5]. There remains
+[ongoing discussion][put_delete] about adding support for `PUT` and `DELETE`,
+as well as how to support content types other than form-encoded data.
-[1]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
-[2]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
-[3]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
-[4]: http://amundsen.com/examples/put-delete-forms/
+[cite]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
+[rails]: http://guides.rubyonrails.org/form_helpers.html#how-do-forms-with-put-or-delete-methods-work
+[html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
+[put_delete]: http://amundsen.com/examples/put-delete-forms/
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index 948973ae..a231f191 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -10,8 +10,18 @@ def api_view(http_method_names):
def decorator(func):
- class WrappedAPIView(APIView):
- pass
+ WrappedAPIView = type(
+ 'WrappedAPIView',
+ (APIView,),
+ {'__doc__': func.__doc__}
+ )
+
+ # Note, the above allows us to set the docstring.
+ # It is the equivelent of:
+ #
+ # class WrappedAPIView(APIView):
+ # pass
+ # WrappedAPIView.__doc__ = func.doc <--- Not possible to do this
allowed_methods = set(http_method_names) | set(('options',))
WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods]
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 572425b9..89479deb 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -31,14 +31,6 @@ class PermissionDenied(APIException):
self.detail = detail or self.default_detail
-class InvalidFormat(APIException):
- status_code = status.HTTP_404_NOT_FOUND
- default_detail = "Format suffix '.%s' not found."
-
- def __init__(self, format, detail=None):
- self.detail = (detail or self.default_detail) % format
-
-
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = "Method '%s' not allowed."
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index ffc0c9d4..bb7d0918 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -43,7 +43,7 @@ class Field(object):
Called to set up a field prior to field_to_native or field_from_native.
parent - The parent serializer.
- model_field - The model field this field corrosponds to, if one exists.
+ model_field - The model field this field corresponds to, if one exists.
"""
self.parent = parent
self.root = parent.root or parent
@@ -197,7 +197,7 @@ class WritableField(Field):
class ModelField(WritableField):
"""
- A generic field that can be used against an arbirtrary model field.
+ A generic field that can be used against an arbitrary model field.
"""
def __init__(self, *args, **kwargs):
try:
@@ -337,14 +337,16 @@ class HyperlinkedRelatedField(RelatedField):
self.view_name = kwargs.pop('view_name')
except:
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
+ self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def to_native(self, obj):
view_name = self.view_name
request = self.context.get('request', None)
+ format = self.format or self.context.get('format', None)
kwargs = {self.pk_url_kwarg: obj.pk}
try:
- return reverse(view_name, kwargs=kwargs, request=request)
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
@@ -355,13 +357,13 @@ class HyperlinkedRelatedField(RelatedField):
kwargs = {self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request)
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request)
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
@@ -411,13 +413,15 @@ class HyperlinkedIdentityField(Field):
# TODO: Make this mandatory, and have the HyperlinkedModelSerializer
# set it on-the-fly
self.view_name = kwargs.pop('view_name', None)
+ self.format = kwargs.pop('format', None)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
+ format = self.format or self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name
view_kwargs = {'pk': obj.pk}
- return reverse(view_name, kwargs=view_kwargs, request=request)
+ return reverse(view_name, kwargs=view_kwargs, request=request, format=format)
##### Typed Fields #####
@@ -515,7 +519,10 @@ class EmailField(CharField):
default_validators = [validators.validate_email]
def from_native(self, value):
- return super(EmailField, self).from_native(value).strip()
+ ret = super(EmailField, self).from_native(value)
+ if ret is None:
+ return None
+ return ret.strip()
def __deepcopy__(self, memo):
result = copy.copy(self)
@@ -537,8 +544,9 @@ class DateField(WritableField):
empty = None
def from_native(self, value):
- if value is None:
- return value
+ if value in validators.EMPTY_VALUES:
+ return None
+
if isinstance(value, datetime.datetime):
if timezone and settings.USE_TZ and timezone.is_aware(value):
# Convert aware datetimes to the default time zone
@@ -576,8 +584,9 @@ class DateTimeField(WritableField):
empty = None
def from_native(self, value):
- if value is None:
- return value
+ if value in validators.EMPTY_VALUES:
+ return None
+
if isinstance(value, datetime.datetime):
return value
if isinstance(value, datetime.date):
@@ -635,6 +644,7 @@ class IntegerField(WritableField):
def from_native(self, value):
if value in validators.EMPTY_VALUES:
return None
+
try:
value = int(str(value))
except (ValueError, TypeError):
@@ -650,8 +660,9 @@ class FloatField(WritableField):
}
def from_native(self, value):
- if value is None:
- return value
+ if value in validators.EMPTY_VALUES:
+ return None
+
try:
return float(value)
except (TypeError, ValueError):
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 190a5f79..27540a57 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -1,5 +1,5 @@
"""
-Generic views that provide commmonly needed behaviour.
+Generic views that provide commonly needed behaviour.
"""
from rest_framework import views, mixins
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 444f8056..dae38477 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -1,3 +1,4 @@
+from django.http import Http404
from rest_framework import exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
@@ -66,7 +67,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
renderers = [renderer for renderer in renderers
if renderer.format == format]
if not renderers:
- raise exceptions.InvalidFormat(format)
+ raise Http404
return renderers
def get_accept_list(self, request):
diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py
index ba663f98..c9db02f0 100644
--- a/rest_framework/reverse.py
+++ b/rest_framework/reverse.py
@@ -5,13 +5,15 @@ from django.core.urlresolvers import reverse as django_reverse
from django.utils.functional import lazy
-def reverse(viewname, *args, **kwargs):
+def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
"""
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
"""
- request = kwargs.pop('request', None)
- url = django_reverse(viewname, *args, **kwargs)
+ if format is not None:
+ kwargs = kwargs or {}
+ kwargs['format'] = format
+ url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
if request:
return request.build_absolute_uri(url)
return url
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index ce04b3e2..16e2c835 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -279,7 +279,7 @@ class BaseSerializer(Field):
def errors(self):
"""
Run deserialization and return error data,
- setting self.object if no errors occured.
+ setting self.object if no errors occurred.
"""
if self._errors is None:
obj = self.from_native(self.init_data)
@@ -393,6 +393,9 @@ class ModelSerializer(Serializer):
Creates a default instance of a basic non-relational field.
"""
kwargs = {}
+ if model_field.null:
+ kwargs['required'] = False
+
if model_field.has_default():
kwargs['required'] = False
kwargs['default'] = model_field.get_default()
diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css
index 739b9300..e29da395 100644
--- a/rest_framework/static/rest_framework/css/default.css
+++ b/rest_framework/static/rest_framework/css/default.css
@@ -32,6 +32,10 @@ h2, h3 {
margin-right: 1em;
}
+ul.breadcrumb {
+ margin: 58px 0 0 0;
+}
+
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
position: absolute;
@@ -55,6 +59,7 @@ pre {
.page-header {
border-bottom: none;
padding-bottom: 0px;
+ margin-bottom: 20px;
}
@@ -65,7 +70,7 @@ html{
background: none;
}
-body, .navbar .navbar-inner .container-fluid{
+body, .navbar .navbar-inner .container-fluid {
max-width: 1150px;
margin: 0 auto;
}
@@ -76,13 +81,14 @@ body{
}
#content{
- margin: 40px 0 0 0;
+ margin: 0;
}
/* custom navigation styles */
.wrapper .navbar{
- width:100%;
+ width: 100%;
position: absolute;
- left:0;
+ left: 0;
+ top: 0;
}
.navbar .navbar-inner{
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 5ac6ef67..e0f79481 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -109,7 +109,7 @@
<div class="content-main">
<div class="page-header"><h1>{{ name }}</h1></div>
- <p class="resource-description">{{ description }}</p>
+ {{ description }}
<div class="request-info">
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html
index 65af512e..c1271399 100644
--- a/rest_framework/templates/rest_framework/login.html
+++ b/rest_framework/templates/rest_framework/login.html
@@ -3,42 +3,50 @@
<html>
<head>
- <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/style.css'/>
+ <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/>
+ <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/>
+ <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/>
</head>
- <body class="login">
+ <body class="container">
- <div id="container">
-
- <div id="header">
- <div id="branding">
- <h1 id="site-name">Django REST framework</h1>
+<div class="container-fluid" style="margin-top: 30px">
+ <div class="row-fluid">
+
+ <div class="well" style="width: 320px; margin-left: auto; margin-right: auto">
+ <div class="row-fluid">
+ <div>
+ <h3 style="margin: 0 0 20px;">Django REST framework</h3>
</div>
- </div>
+ </div><!-- /row fluid -->
- <div id="content" class="colM">
- <div id="content-main">
- <form method="post" action="{% url 'rest_framework:login' %}" id="login-form">
+ <div class="row-fluid">
+ <div>
+ <form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
{% csrf_token %}
- <div class="form-row">
- <label for="id_username">Username:</label> {{ form.username }}
+ <div id="div_id_username" class="clearfix control-group">
+ <div class="controls" style="height: 30px">
+ <Label class="span4" style="margin-top: 3px">Username:</label>
+ <input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
+ </div>
</div>
- <div class="form-row">
- <label for="id_password">Password:</label> {{ form.password }}
- <input type="hidden" name="next" value="{{ next }}" />
+ <div id="div_id_password" class="clearfix control-group">
+ <div class="controls" style="height: 30px">
+ <Label class="span4" style="margin-top: 3px">Password:</label>
+ <input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
+ </div>
</div>
- <div class="form-row">
- <label>&nbsp;</label><input type="submit" value="Log in">
+ <input type="hidden" name="next" value="{{ next }}" />
+ <div class="form-actions-no-box">
+ <input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
</div>
</form>
- <script type="text/javascript">
- document.getElementById('id_username').focus()
- </script>
</div>
- <br class="clear">
- </div>
+ </div><!-- /row fluid -->
+ </div><!--/span-->
- <div id="footer"></div>
+ </div><!-- /.row-fluid -->
+ </div>
</div>
</body>
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index f4263478..d45ea976 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -2,7 +2,7 @@ from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import simplejson as json
from rest_framework import generics, serializers, status
-from rest_framework.tests.models import BasicModel, Comment
+from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
factory = RequestFactory()
@@ -22,6 +22,22 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
model = BasicModel
+class SlugSerializer(serializers.ModelSerializer):
+ slug = serializers.Field() # read only
+
+ class Meta:
+ model = SlugBasedModel
+ exclude = ('id',)
+
+
+class SlugBasedInstanceView(InstanceView):
+ """
+ A model with a slug-field.
+ """
+ model = SlugBasedModel
+ serializer_class = SlugSerializer
+
+
class TestRootView(TestCase):
def setUp(self):
"""
@@ -129,6 +145,7 @@ class TestInstanceView(TestCase):
for obj in self.objects.all()
]
self.view = InstanceView.as_view()
+ self.slug_based_view = SlugBasedInstanceView.as_view()
def test_get_instance_view(self):
"""
@@ -198,7 +215,7 @@ class TestInstanceView(TestCase):
def test_put_cannot_set_id(self):
"""
- POST requests to create a new object should not be able to set the id.
+ PUT requests to create a new object should not be able to set the id.
"""
content = {'id': 999, 'text': 'foobar'}
request = factory.put('/1', json.dumps(content),
@@ -224,6 +241,34 @@ class TestInstanceView(TestCase):
updated = self.objects.get(id=1)
self.assertEquals(updated.text, 'foobar')
+ def test_put_as_create_on_id_based_url(self):
+ """
+ PUT requests to RetrieveUpdateDestroyAPIView should create an object
+ at the requested url if it doesn't exist.
+ """
+ content = {'text': 'foobar'}
+ # pk fields can not be created on demand, only the database can set th pk for a new object
+ request = factory.put('/5', json.dumps(content),
+ content_type='application/json')
+ response = self.view(request, pk=5).render()
+ self.assertEquals(response.status_code, status.HTTP_200_OK)
+ new_obj = self.objects.get(pk=5)
+ self.assertEquals(new_obj.text, 'foobar')
+
+ def test_put_as_create_on_slug_based_url(self):
+ """
+ PUT requests to RetrieveUpdateDestroyAPIView should create an object
+ at the requested url if possible, else return HTTP_403_FORBIDDEN error-response.
+ """
+ content = {'text': 'foobar'}
+ request = factory.put('/test_slug', json.dumps(content),
+ content_type='application/json')
+ response = self.slug_based_view(request, slug='test_slug').render()
+ self.assertEquals(response.status_code, status.HTTP_200_OK)
+ self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'})
+ new_obj = SlugBasedModel.objects.get(slug='test_slug')
+ self.assertEquals(new_obj.text, 'foobar')
+
# Regression test for #285
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index d4ea729b..fb23e359 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -52,6 +52,11 @@ class BasicModel(RESTFrameworkModel):
text = models.CharField(max_length=100)
+class SlugBasedModel(RESTFrameworkModel):
+ text = models.CharField(max_length=100)
+ slug = models.SlugField(max_length=32)
+
+
class DefaultValueModel(RESTFrameworkModel):
text = models.CharField(default='foobar', max_length=100)
@@ -111,3 +116,8 @@ class BlogPost(RESTFrameworkModel):
class BlogPostComment(RESTFrameworkModel):
text = models.TextField()
blog_post = models.ForeignKey(BlogPost)
+
+
+class Person(RESTFrameworkModel):
+ name = models.CharField(max_length=10)
+ age = models.IntegerField(null=True, blank=True)
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 5df3bd7e..eb21dc46 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -43,6 +43,11 @@ class ActionItemSerializer(serializers.ModelSerializer):
model = ActionItem
+class PersonSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Person
+
+
class BasicTests(TestCase):
def setUp(self):
self.comment = Comment(
@@ -188,6 +193,14 @@ class ValidationTests(TestCase):
self.assertFalse(serializer.is_valid())
self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']})
+ def test_null_is_true_fields(self):
+ """
+ Omitting a value for null-field should validate.
+ """
+ serializer = PersonSerializer({'name': 'marko'})
+ self.assertEquals(serializer.is_valid(), True)
+ self.assertEquals(serializer.errors, {})
+
class MetadataTests(TestCase):
def test_empty(self):
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index 6860e6b9..8fe64248 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -16,7 +16,7 @@ class BaseThrottle(object):
def wait(self):
"""
- Optionally, return a recommeded number of seconds to wait before
+ Optionally, return a recommended number of seconds to wait before
the next request.
"""
return None
diff --git a/rest_framework/views.py b/rest_framework/views.py
index c721be3c..71e1fe6c 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -218,7 +218,7 @@ class APIView(View):
def get_throttles(self):
"""
- Instantiates and returns the list of thottles that this view uses.
+ Instantiates and returns the list of throttles that this view uses.
"""
return [throttle() for throttle in self.throttle_classes]