Commit 71e55cc4 by Tom Christie

Merge with latest master

parents 52847a21 e1076cfb
...@@ -7,8 +7,13 @@ html/ ...@@ -7,8 +7,13 @@ html/
coverage/ coverage/
build/ build/
dist/ dist/
rest_framework.egg-info/ *.egg-info/
MANIFEST MANIFEST
bin/
include/
lib/
local/
!.gitignore !.gitignore
!.travis.yml !.travis.yml
...@@ -6,11 +6,12 @@ python: ...@@ -6,11 +6,12 @@ python:
env: env:
- DJANGO=https://github.com/django/django/zipball/master - DJANGO=https://github.com/django/django/zipball/master
- DJANGO=django==1.4.1 --use-mirrors - DJANGO=django==1.4.3 --use-mirrors
- DJANGO=django==1.3.3 --use-mirrors - DJANGO=django==1.3.5 --use-mirrors
install: install:
- pip install $DJANGO - pip install $DJANGO
- pip install django-filter==0.5.4 --use-mirrors
- export PYTHONPATH=. - export PYTHONPATH=.
script: script:
......
...@@ -30,7 +30,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN ...@@ -30,7 +30,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.UserBasicAuthentication', 'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
) )
} }
...@@ -38,7 +38,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN ...@@ -38,7 +38,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
You can also set the authentication policy on a per-view basis, using the `APIView` class based views. You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
class ExampleView(APIView): class ExampleView(APIView):
authentication_classes = (SessionAuthentication, UserBasicAuthentication) authentication_classes = (SessionAuthentication, BasicAuthentication)
permission_classes = (IsAuthenticated,) permission_classes = (IsAuthenticated,)
def get(self, request, format=None): def get(self, request, format=None):
...@@ -50,9 +50,9 @@ You can also set the authentication policy on a per-view basis, using the `APIVi ...@@ -50,9 +50,9 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
Or, if you're using the `@api_view` decorator with function based views. Or, if you're using the `@api_view` decorator with function based views.
@api_view(('GET',)), @api_view(['GET'])
@authentication_classes((SessionAuthentication, UserBasicAuthentication)) @authentication_classes((SessionAuthentication, BasicAuthentication))
@permissions_classes((IsAuthenticated,)) @permission_classes((IsAuthenticated,))
def example_view(request, format=None): def example_view(request, format=None):
content = { content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance. 'user': unicode(request.user), # `django.contrib.auth.User` instance.
...@@ -68,7 +68,7 @@ This policy uses [HTTP Basic Authentication][basicauth], signed against a user's ...@@ -68,7 +68,7 @@ This policy uses [HTTP Basic Authentication][basicauth], signed against a user's
If successfully authenticated, `BasicAuthentication` provides the following credentials. If successfully authenticated, `BasicAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` instance. * `request.user` will be a Django `User` instance.
* `request.auth` will be `None`. * `request.auth` will be `None`.
**Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https` only. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage. **Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https` only. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
...@@ -92,19 +92,38 @@ For clients to authenticate, the token key should be included in the `Authorizat ...@@ -92,19 +92,38 @@ For clients to authenticate, the token key should be included in the `Authorizat
If successfully authenticated, `TokenAuthentication` provides the following credentials. If successfully authenticated, `TokenAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` instance. * `request.user` will be a Django `User` instance.
* `request.auth` will be a `rest_framework.tokenauth.models.BasicToken` instance. * `request.auth` will be a `rest_framework.tokenauth.models.BasicToken` instance.
**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only. **Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only.
## OAuthAuthentication If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
This policy uses the [OAuth 2.0][oauth] protocol to authenticate requests. OAuth is appropriate for server-server setups, such as when you want to allow a third-party service to access your API on a user's behalf. @receiver(post_save, sender=User)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
If successfully authenticated, `OAuthAuthentication` provides the following credentials. If you've already created some users, you can generate tokens for all existing users like this:
* `request.user` will be a `django.contrib.auth.models.User` instance. from django.contrib.auth.models import User
* `request.auth` will be a `rest_framework.models.OAuthToken` instance. from rest_framework.authtoken.models import Token
for user in User.objects.all():
Token.objects.get_or_create(user=user)
When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password.
REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
urlpatterns += patterns('',
url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token')
)
Note that the URL part of the pattern can be whatever you want to use.
The `obtain_auth_token` view will return a JSON response when valid `username` and `password` fields are POSTed to the view using form data or JSON:
{ 'token' : '9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b' }
## SessionAuthentication ## SessionAuthentication
...@@ -112,9 +131,11 @@ This policy uses Django's default session backend for authentication. Session a ...@@ -112,9 +131,11 @@ This policy uses Django's default session backend for authentication. Session a
If successfully authenticated, `SessionAuthentication` provides the following credentials. If successfully authenticated, `SessionAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` instance. * `request.user` will be a Django `User` instance.
* `request.auth` will be `None`. * `request.auth` will be `None`.
If you're using an AJAX style API with SessionAuthentication, you'll need to make sure you include a valid CSRF token for any "unsafe" HTTP method calls, such as `PUT`, `POST` or `DELETE` requests. See the [Django CSRF documentation][csrf-ajax] for more details.
# Custom authentication # Custom authentication
To implement a custom authentication policy, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise. To implement a custom authentication policy, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.
...@@ -124,3 +145,4 @@ To implement a custom authentication policy, subclass `BaseAuthentication` and o ...@@ -124,3 +145,4 @@ To implement a custom authentication policy, subclass `BaseAuthentication` and o
[oauth]: http://oauth.net/2/ [oauth]: http://oauth.net/2/
[permission]: permissions.md [permission]: permissions.md
[throttling]: throttling.md [throttling]: throttling.md
[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax
...@@ -2,11 +2,11 @@ ...@@ -2,11 +2,11 @@
# Serializer fields # Serializer fields
> Flat is better than nested. > Each field in a Form class is responsible not only for validating data, but also for "cleaning" it -- normalizing it to a consistent format.
> >
> — [The Zen of Python][cite] > — [Django documentation][cite]
Serializer fields handle converting between primative values and internal datatypes. They also deal with validating input values, as well as retrieving and setting the values from their parent objects. Serializer fields handle converting between primitive values and internal datatypes. They also deal with validating input values, as well as retrieving and setting the values from their parent objects.
--- ---
...@@ -14,6 +14,51 @@ Serializer fields handle converting between primative values and internal dataty ...@@ -14,6 +14,51 @@ Serializer fields handle converting between primative values and internal dataty
--- ---
## Core arguments
Each serializer field class constructor takes at least these arguments. Some Field classes take additional, field-specific arguments, but the following should always be accepted:
### `source`
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `Field(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `Field(source='user.email')`.
The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations. (See the implementation of the `PaginationSerializer` class for an example.)
Defaults to the name of the field.
### `read_only`
Set this to `True` to ensure that the field is used when serializing a representation, but is not used when updating an instance during deserialization.
Defaults to `False`
### `required`
Normally an error will be raised if a field is not supplied during deserialization.
Set to false if this field is not required to be present during deserialization.
Defaults to `True`.
### `default`
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
### `validators`
A list of Django validators that should be used to validate deserialized values.
### `error_messages`
A dictionary of error codes to error messages.
### `widget`
Used only if rendering the field to HTML.
This argument sets the widget that should be used to render the field.
---
# Generic Fields # Generic Fields
These generic fields are used for representing arbitrary model fields or the output of model methods. These generic fields are used for representing arbitrary model fields or the output of model methods.
...@@ -51,9 +96,9 @@ Would produce output similar to: ...@@ -51,9 +96,9 @@ Would produce output similar to:
'expired': True 'expired': True
} }
By default, the `Field` class will perform a basic translation of the source value into primative datatypes, falling back to unicode representations of complex datatypes when necessary. By default, the `Field` class will perform a basic translation of the source value into primitive datatypes, falling back to unicode representations of complex datatypes when necessary.
You can customize this behaviour by overriding the `.to_native(self, value)` method. You can customize this behavior by overriding the `.to_native(self, value)` method.
## WritableField ## WritableField
...@@ -65,6 +110,24 @@ A generic field that can be tied to any arbitrary model field. The `ModelField` ...@@ -65,6 +110,24 @@ A generic field that can be tied to any arbitrary model field. The `ModelField`
**Signature:** `ModelField(model_field=<Django ModelField class>)` **Signature:** `ModelField(model_field=<Django ModelField class>)`
## SerializerMethodField
This is a read-only field. It gets its value by calling a method on the serializer class it is attached to. It can be used to add any sort of data to the serialized representation of your object. The field's constructor accepts a single argument, which is the name of the method on the serializer to be called. The method should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example:
from rest_framework import serializers
from django.contrib.auth.models import User
from django.utils.timezone import now
class UserSerializer(serializers.ModelSerializer):
days_since_joined = serializers.SerializerMethodField('get_days_since_joined')
class Meta:
model = User
def get_days_since_joined(self, obj):
return (now() - obj.date_joined).days
--- ---
# Typed Fields # Typed Fields
...@@ -86,6 +149,18 @@ or `django.db.models.fields.TextField`. ...@@ -86,6 +149,18 @@ or `django.db.models.fields.TextField`.
**Signature:** `CharField(max_length=None, min_length=None)` **Signature:** `CharField(max_length=None, min_length=None)`
## URLField
Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.validators.URLValidator` for validation.
**Signature:** `CharField(max_length=200, min_length=None)`
## SlugField
Corresponds to `django.db.models.fields.SlugField`.
**Signature:** `CharField(max_length=50, min_length=None)`
## ChoiceField ## ChoiceField
A field that can accept a value out of a limited set of choices. A field that can accept a value out of a limited set of choices.
...@@ -96,6 +171,16 @@ A text representation, validates the text to be a valid e-mail address. ...@@ -96,6 +171,16 @@ A text representation, validates the text to be a valid e-mail address.
Corresponds to `django.db.models.fields.EmailField` Corresponds to `django.db.models.fields.EmailField`
## RegexField
A text representation, that validates the given value matches against a certain regular expression.
Uses Django's `django.core.validators.RegexValidator` for validation.
Corresponds to `django.forms.fields.RegexField`
**Signature:** `RegexField(regex, max_length=None, min_length=None)`
## DateField ## DateField
A date representation. A date representation.
...@@ -120,96 +205,32 @@ A floating point representation. ...@@ -120,96 +205,32 @@ A floating point representation.
Corresponds to `django.db.models.fields.FloatField`. Corresponds to `django.db.models.fields.FloatField`.
--- ## FileField
# Relational Fields
Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`. A file representation. Performs Django's standard FileField validation.
## RelatedField Corresponds to `django.forms.fields.FileField`.
This field can be applied to any of the following: **Signature:** `FileField(max_length=None, allow_empty_file=False)`
* A `ForeignKey` field. - `max_length` designates the maximum length for the file name.
* A `OneToOneField` field.
* A reverse OneToOne relationship - `allow_empty_file` designates if empty files are allowed.
* Any other "to-one" relationship.
By default `RelatedField` will represent the target of the field using it's `__unicode__` method. ## ImageField
You can customise this behaviour by subclassing `ManyRelatedField`, and overriding the `.to_native(self, value)` method. An image representation.
## ManyRelatedField Corresponds to `django.forms.fields.ImageField`.
This field can be applied to any of the following: Requires the `PIL` package.
* A `ManyToManyField` field.
* A reverse ManyToMany relationship.
* A reverse ForeignKey relationship
* Any other "to-many" relationship.
By default `ManyRelatedField` will represent the targets of the field using their `__unicode__` method. Signature and validation is the same as with `FileField`.
For example, given the following models: ---
class TaggedItem(models.Model):
"""
Tags arbitrary model instances using a generic relation.
See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
"""
tag = models.SlugField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
def __unicode__(self):
return self.tag
class Bookmark(models.Model):
"""
A bookmark consists of a URL, and 0 or more descriptive tags.
"""
url = models.URLField()
tags = GenericRelation(TaggedItem)
And a model serializer defined like this:
class BookmarkSerializer(serializers.ModelSerializer):
tags = serializers.ManyRelatedField(source='tags')
class Meta:
model = Bookmark
exclude = ('id',)
Then an example output format for a Bookmark instance would be:
{
'tags': [u'django', u'python'],
'url': u'https://www.djangoproject.com/'
}
## PrimaryKeyRelatedField
As with `RelatedField` field can be applied to any "to-one" relationship, such as a `ForeignKey` field.
`PrimaryKeyRelatedField` will represent the target of the field using it's primary key.
Be default, `PrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `readonly` flag.
## ManyPrimaryKeyRelatedField
As with `RelatedField` field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship.
`PrimaryKeyRelatedField` will represent the target of the field using their primary key.
Be default, `ManyPrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `readonly` flag.
## HyperlinkedRelatedField
## ManyHyperlinkedRelatedField
## HyperLinkedIdentityField **Note:** `FileFields` and `ImageFields` are only suitable for use with MultiPartParser, since e.g. json doesn't support file uploads.
Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
[cite]: http://www.python.org/dev/peps/pep-0020/ [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
<a class="github" href="filters.py"></a>
# Filtering
> The root QuerySet provided by the Manager describes all objects in the database table. Usually, though, you'll need to select only a subset of the complete set of objects.
>
> &mdash; [Django documentation][cite]
The default behavior of REST framework's generic list views is to return the entire queryset for a model manager. Often you will want your API to restrict the items that are returned by the queryset.
The simplest way to filter the queryset of any view that subclasses `MultipleObjectAPIView` is to override the `.get_queryset()` method.
Overriding this method allows you to customize the queryset returned by the view in a number of different ways.
## Filtering against the current user
You might want to filter the queryset to ensure that only results relevant to the currently authenticated user making the request are returned.
You can do so by filtering based on the value of `request.user`.
For example:
class PurchaseList(generics.ListAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
"""
This view should return a list of all the purchases
for the currently authenticated user.
"""
user = self.request.user
return Purchase.objects.filter(purchaser=user)
## Filtering against the URL
Another style of filtering might involve restricting the queryset based on some part of the URL.
For example if your URL config contained an entry like this:
url('^purchases/(?P<username>.+)/$', PurchaseList.as_view()),
You could then write a view that returned a purchase queryset filtered by the username portion of the URL:
class PurchaseList(generics.ListAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
"""
This view should return a list of all the purchases for
the user as determined by the username portion of the URL.
"""
username = self.kwargs['username']
return Purchase.objects.filter(purchaser__username=username)
## Filtering against query parameters
A final example of filtering the initial queryset would be to determine the initial queryset based on query parameters in the url.
We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL:
class PurchaseList(generics.ListAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
"""
Optionally restricts the returned purchases to a given user,
by filtering against a `username` query parameter in the URL.
"""
queryset = Purchase.objects.all()
username = self.request.QUERY_PARAMS.get('username', None)
if username is not None:
queryset = queryset.filter(purchaser__username=username)
return queryset
---
# Generic Filtering
As well as being able to override the default queryset, REST framework also includes support for generic filtering backends that allow you to easily construct complex filters that can be specified by the client using query parameters.
REST framework supports pluggable backends to implement filtering, and provides an implementation which uses the [django-filter] package.
To use REST framework's filtering backend, first install `django-filter`.
pip install django-filter
You must also set the filter backend to `DjangoFilterBackend` in your settings:
REST_FRAMEWORK = {
'FILTER_BACKEND': 'rest_framework.filters.DjangoFilterBackend'
}
## Specifying filter fields
If all you need is simple equality-based filtering, you can set a `filter_fields` attribute on the view, listing the set of fields you wish to filter against.
class ProductList(generics.ListAPIView):
model = Product
serializer_class = ProductSerializer
filter_fields = ('category', 'in_stock')
This will automatically create a `FilterSet` class for the given fields, and will allow you to make requests such as:
http://example.com/api/products?category=clothing&in_stock=True
## Specifying a FilterSet
For more advanced filtering requirements you can specify a `FilterSet` class that should be used by the view. For example:
class ProductFilter(django_filters.FilterSet):
min_price = django_filters.NumberFilter(lookup_type='gte')
max_price = django_filters.NumberFilter(lookup_type='lte')
class Meta:
model = Product
fields = ['category', 'in_stock', 'min_price', 'max_price']
class ProductList(generics.ListAPIView):
model = Product
serializer_class = ProductSerializer
filter_class = ProductFilter
Which will allow you to make requests such as:
http://example.com/api/products?category=clothing&max_price=10.00
For more details on using filter sets see the [django-filter documentation][django-filter-docs].
---
**Hints & Tips**
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'FILTER_BACKEND'` setting.
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
---
## Overriding the initial queryset
Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this:
class PurchasedProductsList(generics.ListAPIView):
"""
Return a list of all the products that the authenticated
user has ever purchased, with optional filtering.
"""
model = Product
serializer_class = ProductSerializer
filter_class = ProductFilter
def get_queryset(self):
user = self.request.user
return user.purchase_set.all()
---
# Custom generic filtering
You can also provide your own generic filtering backend, or write an installable app for other developers to use.
To do so override `BaseFilterBackend`, and override the `.filter_queryset(self, request, queryset, view)` method. The method should return a new, filtered queryset.
To install the filter backend, set the `'FILTER_BACKEND'` key in your `'REST_FRAMEWORK'` setting, using the dotted import path of the filter backend class.
For example:
REST_FRAMEWORK = {
'FILTER_BACKEND': 'custom_filters.CustomFilterBackend'
}
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
[django-filter]: https://github.com/alex/django-filter
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
\ No newline at end of file
...@@ -7,11 +7,11 @@ ...@@ -7,11 +7,11 @@
> >
> &mdash; [Django Documentation][cite] > &mdash; [Django Documentation][cite]
One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns. One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour. REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.
The generic views provided by REST framework allow you to quickly build API views that map closely to your database models. The generic views provided by REST framework allow you to quickly build API views that map closely to your database models.
If the generic views don't suit the needs of your API, you can drop down to using the regular `APIView` class, or reuse the mixins and base classes used by the generic views to compose your own set of reusable generic views. If the generic views don't suit the needs of your API, you can drop down to using the regular `APIView` class, or reuse the mixins and base classes used by the generic views to compose your own set of reusable generic views.
## Examples ## Examples
...@@ -29,8 +29,8 @@ For more complex cases you might also want to override various methods on the vi ...@@ -29,8 +29,8 @@ For more complex cases you might also want to override various methods on the vi
model = User model = User
serializer_class = UserSerializer serializer_class = UserSerializer
permission_classes = (IsAdminUser,) permission_classes = (IsAdminUser,)
def get_paginate_by(self): def get_paginate_by(self, queryset):
""" """
Use smaller pagination for HTML representations. Use smaller pagination for HTML representations.
""" """
...@@ -85,7 +85,7 @@ Extends: [SingleObjectAPIView], [DestroyModelMixin] ...@@ -85,7 +85,7 @@ Extends: [SingleObjectAPIView], [DestroyModelMixin]
Used for **update-only** endpoints for a **single model instance**. Used for **update-only** endpoints for a **single model instance**.
Provides a `put` method handler. Provides `put` and `patch` method handlers.
Extends: [SingleObjectAPIView], [UpdateModelMixin] Extends: [SingleObjectAPIView], [UpdateModelMixin]
...@@ -97,6 +97,14 @@ Provides `get` and `post` method handlers. ...@@ -97,6 +97,14 @@ Provides `get` and `post` method handlers.
Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin] Extends: [MultipleObjectAPIView], [ListModelMixin], [CreateModelMixin]
## RetrieveUpdateAPIView
Used for **read or update** endpoints to represent a **single model instance**.
Provides `get`, `put` and `patch` method handlers.
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin]
## RetrieveDestroyAPIView ## RetrieveDestroyAPIView
Used for **read or delete** endpoints to represent a **single model instance**. Used for **read or delete** endpoints to represent a **single model instance**.
...@@ -109,7 +117,7 @@ Extends: [SingleObjectAPIView], [RetrieveModelMixin], [DestroyModelMixin] ...@@ -109,7 +117,7 @@ Extends: [SingleObjectAPIView], [RetrieveModelMixin], [DestroyModelMixin]
Used for **read-write-delete** endpoints to represent a **single model instance**. Used for **read-write-delete** endpoints to represent a **single model instance**.
Provides `get`, `put` and `delete` method handlers. Provides `get`, `put`, `patch` and `delete` method handlers.
Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin] Extends: [SingleObjectAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyModelMixin]
...@@ -123,52 +131,90 @@ Each of the generic views provided is built by combining one of the base views b ...@@ -123,52 +131,90 @@ Each of the generic views provided is built by combining one of the base views b
Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets. Extends REST framework's `APIView` class, adding support for serialization of model instances and model querysets.
**Attributes**:
* `model` - The model that should be used for this view. Used as a fallback for determining the serializer if `serializer_class` is not set, and as a fallback for determining the queryset if `queryset` is not set. Otherwise not required.
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. If unset, this defaults to creating a serializer class using `self.model`, with the `DEFAULT_MODEL_SERIALIZER_CLASS` setting as the base serializer class.
## MultipleObjectAPIView ## MultipleObjectAPIView
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin]. Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [MultipleObjectMixin].
**See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy]. **See also:** ccbv.co.uk documentation for [MultipleObjectMixin][multiple-object-mixin-classy].
**Attributes**:
* `queryset` - The queryset that should be used for returning objects from this view. If unset, defaults to the default queryset manager for `self.model`.
* `paginate_by` - The size of pages to use with paginated data. If set to `None` then pagination is turned off. If unset this uses the same value as the `PAGINATE_BY` setting, which defaults to `None`.
* `paginate_by_param` - The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If unset this uses the same value as the `PAGINATE_BY_PARAM` setting, which defaults to `None`.
## SingleObjectAPIView ## SingleObjectAPIView
Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin]. Provides a base view for acting on a single object, by combining REST framework's `APIView`, and Django's [SingleObjectMixin].
**See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy]. **See also:** ccbv.co.uk documentation for [SingleObjectMixin][single-object-mixin-classy].
**Attributes**:
* `queryset` - The queryset that should be used when retrieving an object from this view. If unset, defaults to the default queryset manager for `self.model`.
* `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+]
* `slug_url_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`. [Can only be set to non-default on Django 1.4+]
* `slug_field` - The field on the model that should be used to look up objects by a slug. If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.
--- ---
# Mixins # Mixins
The mixin classes provide the actions that are used to provide the basic view behaviour. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behaviour. The mixin classes provide the actions that are used to provide the basic view behaviour. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behaviour.
## ListModelMixin ## ListModelMixin
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset. Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated.
If the queryset is empty this returns a `200 OK` reponse, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`.
Should be mixed in with [MultipleObjectAPIView]. Should be mixed in with [MultipleObjectAPIView].
## CreateModelMixin ## CreateModelMixin
Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance.
If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value.
If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
Should be mixed in with any [GenericAPIView]. Should be mixed in with any [GenericAPIView].
## RetrieveModelMixin ## RetrieveModelMixin
Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response. Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response.
If an object can be retrieve this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`.
Should be mixed in with [SingleObjectAPIView]. Should be mixed in with [SingleObjectAPIView].
## UpdateModelMixin ## UpdateModelMixin
Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance. Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance.
If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response.
If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response.
If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response.
A boolean `partial` keyword argument may be supplied to the `.update()` method. If `partial` is set to `True`, all fields for the update will be optional. This allows support for HTTP `PATCH` requests.
Should be mixed in with [SingleObjectAPIView]. Should be mixed in with [SingleObjectAPIView].
## DestroyModelMixin ## DestroyModelMixin
Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance. Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance.
If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`.
Should be mixed in with [SingleObjectAPIView]. Should be mixed in with [SingleObjectAPIView].
[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views [cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
...@@ -184,4 +230,4 @@ Should be mixed in with [SingleObjectAPIView]. ...@@ -184,4 +230,4 @@ Should be mixed in with [SingleObjectAPIView].
[CreateModelMixin]: #createmodelmixin [CreateModelMixin]: #createmodelmixin
[RetrieveModelMixin]: #retrievemodelmixin [RetrieveModelMixin]: #retrievemodelmixin
[UpdateModelMixin]: #updatemodelmixin [UpdateModelMixin]: #updatemodelmixin
[DestroyModelMixin]: #destroymodelmixin [DestroyModelMixin]: #destroymodelmixin
\ No newline at end of file
...@@ -70,33 +70,34 @@ We could now use our pagination serializer in a view like this. ...@@ -70,33 +70,34 @@ We could now use our pagination serializer in a view like this.
# If page is not an integer, deliver first page. # If page is not an integer, deliver first page.
users = paginator.page(1) users = paginator.page(1)
except EmptyPage: except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results. # If page is out of range (e.g. 9999),
# deliver last page of results.
users = paginator.page(paginator.num_pages) users = paginator.page(paginator.num_pages)
serializer_context = {'request': request} serializer_context = {'request': request}
serializer = PaginatedUserSerializer(instance=users, serializer = PaginatedUserSerializer(users,
context=serializer_context) context=serializer_context)
return Response(serializer.data) return Response(serializer.data)
## Pagination in the generic views ## Pagination in the generic views
The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, or by turning pagination off completely. The generic class based views `ListAPIView` and `ListCreateAPIView` provide pagination of the returned querysets by default. You can customise this behaviour by altering the pagination style, by modifying the default number of results, by allowing clients to override the page size using a query parameter, or by turning pagination off completely.
The default pagination style may be set globally, using the `PAGINATION_SERIALIZER` and `PAGINATE_BY` settings. For example. The default pagination style may be set globally, using the `DEFAULT_PAGINATION_SERIALIZER_CLASS`, `PAGINATE_BY` and `PAGINATE_BY_PARAM` settings. For example.
REST_FRAMEWORK = { REST_FRAMEWORK = {
'PAGINATION_SERIALIZER': ( 'PAGINATE_BY': 10,
'example_app.pagination.CustomPaginationSerializer', 'PAGINATE_BY_PARAM': 'page_size'
),
'PAGINATE_BY': 10
} }
You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view. You can also set the pagination style on a per-view basis, using the `ListAPIView` generic class-based view.
class PaginatedListView(ListAPIView): class PaginatedListView(ListAPIView):
model = ExampleModel model = ExampleModel
pagination_serializer_class = CustomPaginationSerializer
paginate_by = 10 paginate_by = 10
paginate_by_param = 'page_size'
Note that using a `paginate_by` value of `None` will turn off pagination for the view.
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods. For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
...@@ -122,4 +123,20 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam ...@@ -122,4 +123,20 @@ For example, to nest a pair of links labelled 'prev' and 'next', and set the nam
results_field = 'objects' results_field = 'objects'
## Using your custom pagination serializer
To have your custom pagination serializer be used by default, use the `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_SERIALIZER_CLASS':
'example_app.pagination.CustomPaginationSerializer',
}
Alternatively, to set your custom pagination serializer on a per-view basis, use the `pagination_serializer_class` attribute on a generic class based view:
class PaginatedListView(ListAPIView):
model = ExampleModel
pagination_serializer_class = CustomPaginationSerializer
paginate_by = 10
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/ [cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
...@@ -37,7 +37,7 @@ You can also set the renderers used for an individual view, using the `APIView` ...@@ -37,7 +37,7 @@ You can also set the renderers used for an individual view, using the `APIView`
Or, if you're using the `@api_view` decorator with function based views. Or, if you're using the `@api_view` decorator with function based views.
@api_view(('POST',)), @api_view(['POST'])
@parser_classes((YAMLParser,)) @parser_classes((YAMLParser,))
def example_view(request, format=None): def example_view(request, format=None):
""" """
...@@ -140,6 +140,7 @@ For example: ...@@ -140,6 +140,7 @@ For example:
""" """
A naive raw file upload parser. A naive raw file upload parser.
""" """
media_type = '*/*' # Accept anything
def parse(self, stream, media_type=None, parser_context=None): def parse(self, stream, media_type=None, parser_context=None):
content = stream.read() content = stream.read()
...@@ -158,4 +159,17 @@ For example: ...@@ -158,4 +159,17 @@ For example:
files = {name: uploaded} files = {name: uploaded}
return DataAndFiles(data, files) return DataAndFiles(data, files)
---
# Third party packages
The following third party packages are also available.
## MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
[cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion
[messagepack]: https://github.com/juanriaza/django-rest-framework-msgpack
[juanriaza]: https://github.com/juanriaza
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
\ No newline at end of file
...@@ -33,6 +33,12 @@ The default permission policy may be set globally, using the `DEFAULT_PERMISSION ...@@ -33,6 +33,12 @@ The default permission policy may be set globally, using the `DEFAULT_PERMISSION
) )
} }
If not specified, this setting defaults to allowing unrestricted access:
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
)
You can also set the authentication policy on a per-view basis, using the `APIView` class based views. You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
class ExampleView(APIView): class ExampleView(APIView):
...@@ -47,7 +53,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi ...@@ -47,7 +53,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
Or, if you're using the `@api_view` decorator with function based views. Or, if you're using the `@api_view` decorator with function based views.
@api_view('GET') @api_view('GET')
@permission_classes(IsAuthenticated) @permission_classes((IsAuthenticated, ))
def example_view(request, format=None): def example_view(request, format=None):
content = { content = {
'status': 'request was permitted' 'status': 'request was permitted'
...@@ -58,6 +64,12 @@ Or, if you're using the `@api_view` decorator with function based views. ...@@ -58,6 +64,12 @@ Or, if you're using the `@api_view` decorator with function based views.
# API Reference # API Reference
## AllowAny
The `AllowAny` permission class will allow unrestricted access, **regardless of if the request was authenticated or unauthenticated**.
This permission is not strictly required, since you can achieve the same result by using an empty list or tuple for the permissions setting, but you may find it useful to specify this class because it makes the intention explicit.
## IsAuthenticated ## IsAuthenticated
The `IsAuthenticated` permission class will deny permission to any unauthenticated user, and allow permission otherwise. The `IsAuthenticated` permission class will deny permission to any unauthenticated user, and allow permission otherwise.
...@@ -66,7 +78,7 @@ This permission is suitable if you want your API to only be accessible to regist ...@@ -66,7 +78,7 @@ This permission is suitable if you want your API to only be accessible to regist
## IsAdminUser ## IsAdminUser
The `IsAdminUser` permission class will deny permission to any user, unless `user.is_staff`is `True` in which case permission will be allowed. The `IsAdminUser` permission class will deny permission to any user, unless `user.is_staff` is `True` in which case permission will be allowed.
This permission is suitable is you want your API to only be accessible to a subset of trusted administrators. This permission is suitable is you want your API to only be accessible to a subset of trusted administrators.
......
<a class="github" href="relations.py"></a>
# Serializer relations
> Bad programmers worry about the code.
> Good programmers worry about data structures and their relationships.
>
> &mdash; [Linus Torvalds][cite]
Relational fields are used to represent model relationships. They can be applied to `ForeignKey`, `ManyToManyField` and `OneToOneField` relationships, as well as to reverse relationships, and custom relationships such as `GenericForeignKey`.
---
**Note:** The relational fields are declared in `relations.py`, but by convention you should import them using `from rest_framework import serializers` and refer to fields as `serializers.<FieldName>`.
---
## RelatedField
This field can be applied to any of the following:
* A `ForeignKey` field.
* A `OneToOneField` field.
* A reverse OneToOne relationship
* Any other "to-one" relationship.
By default `RelatedField` will represent the target of the field using it's `__unicode__` method.
You can customize this behavior by subclassing `ManyRelatedField`, and overriding the `.to_native(self, value)` method.
## ManyRelatedField
This field can be applied to any of the following:
* A `ManyToManyField` field.
* A reverse ManyToMany relationship.
* A reverse ForeignKey relationship
* Any other "to-many" relationship.
By default `ManyRelatedField` will represent the targets of the field using their `__unicode__` method.
For example, given the following models:
class TaggedItem(models.Model):
"""
Tags arbitrary model instances using a generic relation.
See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/
"""
tag = models.SlugField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
def __unicode__(self):
return self.tag
class Bookmark(models.Model):
"""
A bookmark consists of a URL, and 0 or more descriptive tags.
"""
url = models.URLField()
tags = GenericRelation(TaggedItem)
And a model serializer defined like this:
class BookmarkSerializer(serializers.ModelSerializer):
tags = serializers.ManyRelatedField(source='tags')
class Meta:
model = Bookmark
exclude = ('id',)
Then an example output format for a Bookmark instance would be:
{
'tags': [u'django', u'python'],
'url': u'https://www.djangoproject.com/'
}
## PrimaryKeyRelatedField
## ManyPrimaryKeyRelatedField
`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
By default these fields are read-write, although you can change this behavior using the `read_only` flag.
**Arguments**:
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
## SlugRelatedField
## ManySlugRelatedField
`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
By default these fields read-write, although you can change this behavior using the `read_only` flag.
**Arguments**:
* `slug_field` - The field on the target that should be used to represent it. This should be a field that uniquely identifies any given instance. For example, `username`.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
## HyperlinkedRelatedField
## ManyHyperlinkedRelatedField
`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
By default, `HyperlinkedRelatedField` is read-write, although you can change this behavior using the `read_only` flag.
**Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `queryset` - By default `ModelSerializer` classes will use the default queryset for the relationship. `Serializer` classes must either set a queryset explicitly, or set `read_only=True`.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
* `null` - If set to `True`, the field will accept values of `None` or the empty-string for nullable relationships.
## HyperLinkedIdentityField
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
This field is always read-only.
**Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
[cite]: http://lwn.net/Articles/193245/
...@@ -42,7 +42,7 @@ You can also set the renderers used for an individual view, using the `APIView` ...@@ -42,7 +42,7 @@ You can also set the renderers used for an individual view, using the `APIView`
Or, if you're using the `@api_view` decorator with function based views. Or, if you're using the `@api_view` decorator with function based views.
@api_view(('GET',)), @api_view(['GET'])
@renderer_classes((JSONRenderer, JSONPRenderer)) @renderer_classes((JSONRenderer, JSONPRenderer))
def user_count_view(request, format=None): def user_count_view(request, format=None):
""" """
...@@ -106,12 +106,12 @@ If you are considering using `XML` for your API, you may want to consider implem ...@@ -106,12 +106,12 @@ If you are considering using `XML` for your API, you may want to consider implem
**.format**: `'.xml'` **.format**: `'.xml'`
## HTMLRenderer ## TemplateHTMLRenderer
Renders data to HTML, using Django's standard template rendering. Renders data to HTML, using Django's standard template rendering.
Unlike other renderers, the data passed to the `Response` does not need to be serialized. Also, unlike other renderers, you may want to include a `template_name` argument when creating the `Response`. Unlike other renderers, the data passed to the `Response` does not need to be serialized. Also, unlike other renderers, you may want to include a `template_name` argument when creating the `Response`.
The HTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context. The TemplateHTMLRenderer will create a `RequestContext`, using the `response.data` as the context dict, and determine a template name to use to render the context.
The template name is determined by (in order of preference): The template name is determined by (in order of preference):
...@@ -119,27 +119,49 @@ The template name is determined by (in order of preference): ...@@ -119,27 +119,49 @@ The template name is determined by (in order of preference):
2. An explicit `.template_name` attribute set on this class. 2. An explicit `.template_name` attribute set on this class.
3. The return result of calling `view.get_template_names()`. 3. The return result of calling `view.get_template_names()`.
An example of a view that uses `HTMLRenderer`: An example of a view that uses `TemplateHTMLRenderer`:
class UserInstance(generics.RetrieveUserAPIView): class UserInstance(generics.RetrieveUserAPIView):
""" """
A view that returns a templated HTML representations of a given user. A view that returns a templated HTML representations of a given user.
""" """
model = Users model = Users
renderer_classes = (HTMLRenderer,) renderer_classes = (TemplateHTMLRenderer,)
def get(self, request, *args, **kwargs) def get(self, request, *args, **kwargs)
self.object = self.get_object() self.object = self.get_object()
return Response(self.object, template_name='user_detail.html') return Response({'user': self.object}, template_name='user_detail.html')
You can use `HTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint. You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
If you're building websites that use `HTMLRenderer` along with other renderer classes, you should consider listing `HTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers. If you're building websites that use `TemplateHTMLRenderer` along with other renderer classes, you should consider listing `TemplateHTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers.
**.media_type**: `text/html` **.media_type**: `text/html`
**.format**: `'.html'` **.format**: `'.html'`
See also: `StaticHTMLRenderer`
## StaticHTMLRenderer
A simple renderer that simply returns pre-rendered HTML. Unlike other renderers, the data passed to the response object should be a string representing the content to be returned.
An example of a view that uses `TemplateHTMLRenderer`:
@api_view(('GET',))
@renderer_classes((StaticHTMLRenderer,))
def simple_html_view(request):
data = '<html><body><h1>Hello, world</h1></body></html>'
return Response(data)
You can use `TemplateHTMLRenderer` either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
**.media_type**: `text/html`
**.format**: `'.html'`
See also: `TemplateHTMLRenderer`
## BrowsableAPIRenderer ## BrowsableAPIRenderer
Renders data into HTML for the Browseable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page. Renders data into HTML for the Browseable API. This renderer will determine which other renderer would have been given highest priority, and use that to display an API style response within the HTML page.
...@@ -207,7 +229,7 @@ In some cases you might want your view to use different serialization styles dep ...@@ -207,7 +229,7 @@ In some cases you might want your view to use different serialization styles dep
For example: For example:
@api_view(('GET',)) @api_view(('GET',))
@renderer_classes((HTMLRenderer, JSONRenderer)) @renderer_classes((TemplateHTMLRenderer, JSONRenderer))
def list_users(request): def list_users(request):
""" """
A view that can return JSON or HTML representations A view that can return JSON or HTML representations
...@@ -215,9 +237,9 @@ For example: ...@@ -215,9 +237,9 @@ For example:
""" """
queryset = Users.objects.filter(active=True) queryset = Users.objects.filter(active=True)
if request.accepted_media_type == 'text/html': if request.accepted_renderer.format == 'html':
# TemplateHTMLRenderer takes a context dict, # TemplateHTMLRenderer takes a context dict,
# and additionally requiresa 'template_name'. # and additionally requires a 'template_name'.
# It does not require serialization. # It does not require serialization.
data = {'users': queryset} data = {'users': queryset}
return Response(data, template_name='list_users.html') return Response(data, template_name='list_users.html')
...@@ -235,6 +257,34 @@ In [the words of Roy Fielding][quote], "A REST API should spend almost all of it ...@@ -235,6 +257,34 @@ In [the words of Roy Fielding][quote], "A REST API should spend almost all of it
For good examples of custom media types, see GitHub's use of a custom [application/vnd.github+json] media type, and Mike Amundsen's IANA approved [application/vnd.collection+json] JSON-based hypermedia. For good examples of custom media types, see GitHub's use of a custom [application/vnd.github+json] media type, and Mike Amundsen's IANA approved [application/vnd.collection+json] JSON-based hypermedia.
## HTML error views
Typically a renderer will behave the same regardless of if it's dealing with a regular response, or with a response caused by an exception being raised, such as an `Http404` or `PermissionDenied` exception, or a subclass of `APIException`.
If you're using either the `TemplateHTMLRenderer` or the `StaticHTMLRenderer` and an exception is raised, the behavior is slightly different, and mirrors [Django's default handling of error views][django-error-views].
Exceptions raised and handled by an HTML renderer will attempt to render using one of the following methods, by order of precedence.
* Load and render a template named `{status_code}.html`.
* Load and render a template named `api_exception.html`.
* Render the HTTP status code and text, for example "404 Not Found".
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
---
# Third party packages
The following third party packages are also available.
## MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
## CSV
Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md [conneg]: content-negotiation.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
...@@ -243,3 +293,9 @@ For good examples of custom media types, see GitHub's use of a custom [applicati ...@@ -243,3 +293,9 @@ For good examples of custom media types, see GitHub's use of a custom [applicati
[quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven [quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[application/vnd.github+json]: http://developer.github.com/v3/media/ [application/vnd.github+json]: http://developer.github.com/v3/media/
[application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/ [application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/
[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views
[messagepack]: http://msgpack.org/
[juanriaza]: https://github.com/juanriaza
[mjumbewu]: https://github.com/mjumbewu
[djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack
[djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv
\ No newline at end of file
...@@ -13,7 +13,7 @@ For example your project's `settings.py` file might include something like this: ...@@ -13,7 +13,7 @@ For example your project's `settings.py` file might include something like this:
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_RENDERER_CLASSES': ( 'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.YAMLRenderer', 'rest_framework.renderers.YAMLRenderer',
) ),
'DEFAULT_PARSER_CLASSES': ( 'DEFAULT_PARSER_CLASSES': (
'rest_framework.parsers.YAMLParser', 'rest_framework.parsers.YAMLParser',
) )
...@@ -42,7 +42,7 @@ Default: ...@@ -42,7 +42,7 @@ Default:
( (
'rest_framework.renderers.JSONRenderer', 'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer' 'rest_framework.renderers.BrowsableAPIRenderer',
'rest_framework.renderers.TemplateHTMLRenderer' 'rest_framework.renderers.TemplateHTMLRenderer'
) )
...@@ -65,14 +65,18 @@ Default: ...@@ -65,14 +65,18 @@ Default:
( (
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.UserBasicAuthentication' 'rest_framework.authentication.BasicAuthentication'
) )
## DEFAULT_PERMISSION_CLASSES ## DEFAULT_PERMISSION_CLASSES
A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view. A list or tuple of permission classes, that determines the default set of permissions checked at the start of a view.
Default: `()` Default:
(
'rest_framework.permissions.AllowAny',
)
## DEFAULT_THROTTLE_CLASSES ## DEFAULT_THROTTLE_CLASSES
...@@ -92,11 +96,21 @@ Default: `rest_framework.serializers.ModelSerializer` ...@@ -92,11 +96,21 @@ Default: `rest_framework.serializers.ModelSerializer`
Default: `rest_framework.pagination.PaginationSerializer` Default: `rest_framework.pagination.PaginationSerializer`
## FORMAT_SUFFIX_KWARG ## FILTER_BACKEND
**TODO** The filter backend class that should be used for generic filtering. If set to `None` then generic filtering is disabled.
Default: `'format'` ## PAGINATE_BY
The default page size to use for pagination. If set to `None`, pagination is disabled by default.
Default: `None`
## PAGINATE_BY_PARAM
The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size.
Default: `None`
## UNAUTHENTICATED_USER ## UNAUTHENTICATED_USER
...@@ -146,4 +160,10 @@ Default: `'accept'` ...@@ -146,4 +160,10 @@ Default: `'accept'`
Default: `'format'` Default: `'format'`
## FORMAT_SUFFIX_KWARG
**TODO**
Default: `'format'`
[cite]: http://www.python.org/dev/peps/pep-0020/ [cite]: http://www.python.org/dev/peps/pep-0020/
...@@ -87,7 +87,7 @@ Response status codes beginning with the digit "5" indicate cases in which the s ...@@ -87,7 +87,7 @@ Response status codes beginning with the digit "5" indicate cases in which the s
HTTP_503_SERVICE_UNAVAILABLE HTTP_503_SERVICE_UNAVAILABLE
HTTP_504_GATEWAY_TIMEOUT HTTP_504_GATEWAY_TIMEOUT
HTTP_505_HTTP_VERSION_NOT_SUPPORTED HTTP_505_HTTP_VERSION_NOT_SUPPORTED
HTTP_511_NETWORD_AUTHENTICATION_REQUIRED HTTP_511_NETWORK_AUTHENTICATION_REQUIRED
[rfc2324]: http://www.ietf.org/rfc/rfc2324.txt [rfc2324]: http://www.ietf.org/rfc/rfc2324.txt
......
...@@ -31,9 +31,9 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C ...@@ -31,9 +31,9 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': ( 'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttles.AnonThrottle', 'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttles.UserThrottle', 'rest_framework.throttling.UserRateThrottle'
) ),
'DEFAULT_THROTTLE_RATES': { 'DEFAULT_THROTTLE_RATES': {
'anon': '100/day', 'anon': '100/day',
'user': '1000/day' 'user': '1000/day'
...@@ -102,8 +102,8 @@ For example, multiple user throttle rates could be implemented by using the foll ...@@ -102,8 +102,8 @@ For example, multiple user throttle rates could be implemented by using the foll
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': ( 'DEFAULT_THROTTLE_CLASSES': (
'example.throttles.BurstRateThrottle', 'example.throttles.BurstRateThrottle',
'example.throttles.SustainedRateThrottle', 'example.throttles.SustainedRateThrottle'
) ),
'DEFAULT_THROTTLE_RATES': { 'DEFAULT_THROTTLE_RATES': {
'burst': '60/min', 'burst': '60/min',
'sustained': '1000/day' 'sustained': '1000/day'
...@@ -136,8 +136,8 @@ For example, given the following views... ...@@ -136,8 +136,8 @@ For example, given the following views...
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_THROTTLE_CLASSES': ( 'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttles.ScopedRateThrottle', 'rest_framework.throttling.ScopedRateThrottle'
) ),
'DEFAULT_THROTTLE_RATES': { 'DEFAULT_THROTTLE_RATES': {
'contacts': '1000/day', 'contacts': '1000/day',
'uploads': '20/day' 'uploads': '20/day'
......
...@@ -19,6 +19,10 @@ Using the `APIView` class is pretty much the same as using a regular `View` clas ...@@ -19,6 +19,10 @@ Using the `APIView` class is pretty much the same as using a regular `View` clas
For example: For example:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
class ListUsers(APIView): class ListUsers(APIView):
""" """
View to list all users in the system. View to list all users in the system.
...@@ -118,9 +122,51 @@ You won't typically need to override this method. ...@@ -118,9 +122,51 @@ You won't typically need to override this method.
> >
> &mdash; [Nick Coghlan][cite2] > &mdash; [Nick Coghlan][cite2]
REST framework also gives you to work with regular function based views... REST framework also allows you to work with regular function based views. It provides a set of simple decorators that wrap your function based views to ensure they receive an instance of `Request` (rather than the usual Django `HttpRequest`) and allows them to return a `Response` (instead of a Django `HttpResponse`), and allow you to configure how the request is processed.
## @api_view()
**Signature:** `@api_view(http_method_names)`
The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data:
from rest_framework.decorators import api_view
@api_view(['GET'])
def hello_world(request):
return Response({"message": "Hello, world!"})
This view will use the default renderers, parsers, authentication classes etc specified in the [settings](settings).
## API policy decorators
To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle](throttling) to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes:
from rest_framework.decorators import api_view, throttle_classes
from rest_framework.throttling import UserRateThrottle
class OncePerDayUserThrottle(UserRateThrottle):
rate = '1/day'
@api_view(['GET'])
@throttle_classes([OncePerDayUserThrottle])
def view(request):
return Response({"message": "Hello for today! See you tomorrow!"})
These decorators correspond to the attributes set on `APIView` subclasses, described above.
The available decorators are:
* `@renderer_classes(...)`
* `@parser_classes(...)`
* `@authentication_classes(...)`
* `@throttle_classes(...)`
* `@permission_classes(...)`
**[TODO]** Each of these decorators takes a single argument which must be a list or tuple of classes.
[cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html
[cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html [cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html
[settings]: api-guide/settings.md
[throttling]: api-guide/throttling.md
...@@ -5,12 +5,24 @@ ...@@ -5,12 +5,24 @@
**A toolkit for building well-connected, self-describing Web APIs.** **A toolkit for building well-connected, self-describing Web APIs.**
**WARNING: This documentation is for the 2.0 redesign of REST framework. It is a work in progress.** ---
**Note**: This documentation is for the 2.0 version of REST framework. If you are looking for earlier versions please see the [0.4.x branch][0.4] on GitHub.
---
Django REST framework is a lightweight library that makes it easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views. Django REST framework is a lightweight library that makes it easy to build Web APIs. It is designed as a modular and easy to customize architecture, based on Django's class based views.
Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box. Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers. It also supports a wide range of media types, authentication and permission policies out of the box.
If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcement][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities.
There is also a sandbox API you can use for testing purposes, [available here][sandbox].
**Below**: *Screenshot from the browseable API*
![Screenshot][image]
## Requirements ## Requirements
REST framework requires the following: REST framework requires the following:
...@@ -20,18 +32,18 @@ REST framework requires the following: ...@@ -20,18 +32,18 @@ REST framework requires the following:
The following packages are optional: The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the self describing API. * [Markdown][markdown] (2.1.0+) - Markdown support for the browseable API.
* [PyYAML][yaml] (3.10+) - YAML content-type support. * [PyYAML][yaml] (3.10+) - YAML content-type support.
* [django-filter][django-filter] (0.5.4+) - Filtering support.
## Installation ## Installation
**WARNING: These instructions will only become valid once this becomes the master version**
Install using `pip`, including any optional packages you want... Install using `pip`, including any optional packages you want...
pip install djangorestframework pip install djangorestframework
pip install markdown # Recommended if using the browseable API. pip install markdown # Markdown support for the browseable API.
pip install pyyaml # Required for yaml content-type support. pip install pyyaml # YAML content-type support.
pip install django-filter # Filtering support
...or clone the project from github. ...or clone the project from github.
...@@ -40,21 +52,21 @@ Install using `pip`, including any optional packages you want... ...@@ -40,21 +52,21 @@ Install using `pip`, including any optional packages you want...
pip install -r requirements.txt pip install -r requirements.txt
pip install -r optionals.txt pip install -r optionals.txt
Add `rest_framework` to your `INSTALLED_APPS`. Add `'rest_framework'` to your `INSTALLED_APPS` setting.
INSTALLED_APPS = ( INSTALLED_APPS = (
... ...
'rest_framework', 'rest_framework',
) )
If you're intending to use the browseable API you'll want to add REST framework's login and logout views. Add the following to your root `urls.py` file. If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
urlpatterns = patterns('', urlpatterns = patterns('',
... ...
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
) )
Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace. Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
## Quickstart ## Quickstart
...@@ -67,9 +79,8 @@ The tutorial will walk you through the building blocks that make up REST framewo ...@@ -67,9 +79,8 @@ The tutorial will walk you through the building blocks that make up REST framewo
* [1 - Serialization][tut-1] * [1 - Serialization][tut-1]
* [2 - Requests & Responses][tut-2] * [2 - Requests & Responses][tut-2]
* [3 - Class based views][tut-3] * [3 - Class based views][tut-3]
* [4 - Authentication, permissions & throttling][tut-4] * [4 - Authentication & permissions][tut-4]
* [5 - Relationships & hyperlinked APIs][tut-5] * [5 - Relationships & hyperlinked APIs][tut-5]
<!-- * [6 - Resource orientated projects][tut-6]-->
## API Guide ## API Guide
...@@ -83,9 +94,11 @@ The API guide is your complete reference manual to all the functionality provide ...@@ -83,9 +94,11 @@ The API guide is your complete reference manual to all the functionality provide
* [Renderers][renderers] * [Renderers][renderers]
* [Serializers][serializers] * [Serializers][serializers]
* [Serializer fields][fields] * [Serializer fields][fields]
* [Serializer relations][relations]
* [Authentication][authentication] * [Authentication][authentication]
* [Permissions][permissions] * [Permissions][permissions]
* [Throttling][throttling] * [Throttling][throttling]
* [Filtering][filtering]
* [Pagination][pagination] * [Pagination][pagination]
* [Content negotiation][contentnegotiation] * [Content negotiation][contentnegotiation]
* [Format suffixes][formatsuffixes] * [Format suffixes][formatsuffixes]
...@@ -98,12 +111,10 @@ The API guide is your complete reference manual to all the functionality provide ...@@ -98,12 +111,10 @@ The API guide is your complete reference manual to all the functionality provide
General guides to using REST framework. General guides to using REST framework.
* [CSRF][csrf]
* [Browser enhancements][browser-enhancements] * [Browser enhancements][browser-enhancements]
* [The Browsable API][browsableapi] * [The Browsable API][browsableapi]
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas] * [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
* [Contributing to REST framework][contributing] * [2.0 Announcement][rest-framework-2-announcement]
* [2.0 Migration Guide][migration]
* [Release Notes][release-notes] * [Release Notes][release-notes]
* [Credits][credits] * [Credits][credits]
...@@ -119,7 +130,6 @@ Run the tests: ...@@ -119,7 +130,6 @@ Run the tests:
./rest_framework/runtests/runtests.py ./rest_framework/runtests/runtests.py
For more information see the [Contributing to REST framework][contributing] section.
## Support ## Support
For support please see the [REST framework discussion group][group], or try the `#restframework` channel on `irc.freenode.net`. For support please see the [REST framework discussion group][group], or try the `#restframework` channel on `irc.freenode.net`.
...@@ -128,7 +138,7 @@ Paid support is also available from [DabApps], and can include work on REST fram ...@@ -128,7 +138,7 @@ Paid support is also available from [DabApps], and can include work on REST fram
## License ## License
Copyright (c) 2011-2012, Tom Christie Copyright (c) 2011-2013, Tom Christie
All rights reserved. All rights reserved.
Redistribution and use in source and binary forms, with or without Redistribution and use in source and binary forms, with or without
...@@ -151,19 +161,22 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, ...@@ -151,19 +161,22 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=restframework2 [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2 [travis-build-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
[urlobject]: https://github.com/zacharyvoase/urlobject [urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/ [markdown]: http://pypi.python.org/pypi/Markdown/
[yaml]: http://pypi.python.org/pypi/PyYAML [yaml]: http://pypi.python.org/pypi/PyYAML
[django-filter]: http://pypi.python.org/pypi/django-filter
[0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X
[image]: img/quickstart.png
[sandbox]: http://restframework.herokuapp.com/
[quickstart]: tutorial/quickstart.md [quickstart]: tutorial/quickstart.md
[tut-1]: tutorial/1-serialization.md [tut-1]: tutorial/1-serialization.md
[tut-2]: tutorial/2-requests-and-responses.md [tut-2]: tutorial/2-requests-and-responses.md
[tut-3]: tutorial/3-class-based-views.md [tut-3]: tutorial/3-class-based-views.md
[tut-4]: tutorial/4-authentication-permissions-and-throttling.md [tut-4]: tutorial/4-authentication-and-permissions.md
[tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md [tut-5]: tutorial/5-relationships-and-hyperlinked-apis.md
[tut-6]: tutorial/6-resource-orientated-projects.md
[request]: api-guide/requests.md [request]: api-guide/requests.md
[response]: api-guide/responses.md [response]: api-guide/responses.md
...@@ -173,9 +186,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -173,9 +186,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[renderers]: api-guide/renderers.md [renderers]: api-guide/renderers.md
[serializers]: api-guide/serializers.md [serializers]: api-guide/serializers.md
[fields]: api-guide/fields.md [fields]: api-guide/fields.md
[relations]: api-guide/relations.md
[authentication]: api-guide/authentication.md [authentication]: api-guide/authentication.md
[permissions]: api-guide/permissions.md [permissions]: api-guide/permissions.md
[throttling]: api-guide/throttling.md [throttling]: api-guide/throttling.md
[filtering]: api-guide/filtering.md
[pagination]: api-guide/pagination.md [pagination]: api-guide/pagination.md
[contentnegotiation]: api-guide/content-negotiation.md [contentnegotiation]: api-guide/content-negotiation.md
[formatsuffixes]: api-guide/format-suffixes.md [formatsuffixes]: api-guide/format-suffixes.md
...@@ -189,7 +204,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -189,7 +204,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[browsableapi]: topics/browsable-api.md [browsableapi]: topics/browsable-api.md
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md [rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
[contributing]: topics/contributing.md [contributing]: topics/contributing.md
[migration]: topics/migration.md [rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
[release-notes]: topics/release-notes.md [release-notes]: topics/release-notes.md
[credits]: topics/credits.md [credits]: topics/credits.md
......
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <html lang="en">
<head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta charset="utf-8"> <meta charset="utf-8">
<title>Django REST framework</title> <title>Django REST framework</title>
<link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon"> <link href="{{ base_url }}/img/favicon.ico" rel="icon" type="image/x-icon">
...@@ -17,6 +18,21 @@ ...@@ -17,6 +18,21 @@
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]--> <![endif]-->
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-18852272-2']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</head>
<body onload="prettyPrint()" class="{{ page_id }}-page"> <body onload="prettyPrint()" class="{{ page_id }}-page">
<div class="wrapper"> <div class="wrapper">
...@@ -24,7 +40,7 @@ ...@@ -24,7 +40,7 @@
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container-fluid"> <div class="container-fluid">
<a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/restframework2">GitHub</a> <a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/master">GitHub</a>
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse"> <a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
...@@ -41,9 +57,8 @@ ...@@ -41,9 +57,8 @@
<li><a href="{{ base_url }}/tutorial/1-serialization{{ suffix }}">1 - Serialization</a></li> <li><a href="{{ base_url }}/tutorial/1-serialization{{ suffix }}">1 - Serialization</a></li>
<li><a href="{{ base_url }}/tutorial/2-requests-and-responses{{ suffix }}">2 - Requests and responses</a></li> <li><a href="{{ base_url }}/tutorial/2-requests-and-responses{{ suffix }}">2 - Requests and responses</a></li>
<li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li> <li><a href="{{ base_url }}/tutorial/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
<li><a href="{{ base_url }}/tutorial/4-authentication-permissions-and-throttling{{ suffix }}">4 - Authentication, permissions and throttling</a></li> <li><a href="{{ base_url }}/tutorial/4-authentication-and-permissions{{ suffix }}">4 - Authentication and permissions</a></li>
<li><a href="{{ base_url }}/tutorial/5-relationships-and-hyperlinked-apis{{ suffix }}">5 - Relationships and hyperlinked APIs</a></li> <li><a href="{{ base_url }}/tutorial/5-relationships-and-hyperlinked-apis{{ suffix }}">5 - Relationships and hyperlinked APIs</a></li>
<!-- <li><a href="{{ base_url }}/tutorial/6-resource-orientated-projects{{ suffix }}">6 - Resource orientated projects</a></li> -->
</ul> </ul>
</li> </li>
<li class="dropdown"> <li class="dropdown">
...@@ -57,9 +72,11 @@ ...@@ -57,9 +72,11 @@
<li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li> <li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li>
<li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li> <li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
<li><a href="{{ base_url }}/api-guide/fields{{ suffix }}">Serializer fields</a></li> <li><a href="{{ base_url }}/api-guide/fields{{ suffix }}">Serializer fields</a></li>
<li><a href="{{ base_url }}/api-guide/relations{{ suffix }}">Serializer relations</a></li>
<li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li> <li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li>
<li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li> <li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li>
<li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li> <li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li>
<li><a href="{{ base_url }}/api-guide/filtering{{ suffix }}">Filtering</a></li>
<li><a href="{{ base_url }}/api-guide/pagination{{ suffix }}">Pagination</a></li> <li><a href="{{ base_url }}/api-guide/pagination{{ suffix }}">Pagination</a></li>
<li><a href="{{ base_url }}/api-guide/content-negotiation{{ suffix }}">Content negotiation</a></li> <li><a href="{{ base_url }}/api-guide/content-negotiation{{ suffix }}">Content negotiation</a></li>
<li><a href="{{ base_url }}/api-guide/format-suffixes{{ suffix }}">Format suffixes</a></li> <li><a href="{{ base_url }}/api-guide/format-suffixes{{ suffix }}">Format suffixes</a></li>
...@@ -72,12 +89,10 @@ ...@@ -72,12 +89,10 @@
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Topics <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{{ base_url }}/topics/csrf{{ suffix }}">Working with AJAX and CSRF</a></li>
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li> <li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li> <li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li> <li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
<li><a href="{{ base_url }}/topics/contributing{{ suffix }}">Contributing to REST framework</a></li> <li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
<li><a href="{{ base_url }}/topics/migration{{ suffix }}">2.0 Migration Guide</a></li>
<li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li> <li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li>
<li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li> <li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li>
</ul> </ul>
......
...@@ -2,42 +2,63 @@ ...@@ -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" > "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"
> >
> &mdash; [RESTful Web Services](1), Leonard Richardson & Sam Ruby. > &mdash; [RESTful Web Services][cite], Leonard Richardson & Sam Ruby.
## Browser based PUT, DELETE, etc... ## 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: For example, given the following form:
<form action="/news-items/5" method="POST"> <form action="/news-items/5" method="POST">
<input type="hidden" name="_method" value="DELETE"> <input type="hidden" name="_method" value="DELETE">
</form> </form>
`request.method` would return `"DELETE"`. `request.method` would return `"DELETE"`.
## Browser based submission of non-form content ## 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: For example, given the following form:
<form action="/news-items/5" method="PUT"> <form action="/news-items/5" method="PUT">
<input type="hidden" name="_content_type" value="application/json"> <input type="hidden" name="_content_type" value="application/json">
<input name="_content" value="{'count': 1}"> <input name="_content" value="{'count': 1}">
</form> </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 ## 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 ## 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? ## 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 [cite]: 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 [rails]: 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 [html5]: http://www.w3.org/TR/html5-diff/#changes-2010-06-24
[4]: http://amundsen.com/examples/put-delete-forms/ [put_delete]: http://amundsen.com/examples/put-delete-forms/
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
The following people have helped make REST framework great. The following people have helped make REST framework great.
* Tom Christie - [tomchristie] * Tom Christie - [tomchristie]
* Marko Tibold - [markotibold] * Marko Tibold - [markotibold]
* Paul Bagwell - [pbgwl] * Paul Bagwell - [pbgwl]
* Sébastien Piquemal - [sebpiq] * Sébastien Piquemal - [sebpiq]
...@@ -49,6 +49,48 @@ The following people have helped make REST framework great. ...@@ -49,6 +49,48 @@ The following people have helped make REST framework great.
* Tomi Pajunen - [eofs] * Tomi Pajunen - [eofs]
* Rob Dobson - [rdobson] * Rob Dobson - [rdobson]
* Daniel Vaca Araujo - [diviei] * Daniel Vaca Araujo - [diviei]
* Madis Väin - [madisvain]
* Stephan Groß - [minddust]
* Pavel Savchenko - [asfaltboy]
* Otto Yiu - [ottoyiu]
* Jacob Magnusson - [jmagnusson]
* Osiloke Harold Emoekpere - [osiloke]
* Michael Shepanski - [mjs7231]
* Toni Michel - [tonimichel]
* Ben Konrath - [benkonrath]
* Marc Aymerich - [glic3rinu]
* Ludwig Kraatz - [ludwigkraatz]
* Rob Romano - [robromano]
* Eugene Mechanism - [mechanism]
* Jonas Liljestrand - [jonlil]
* Justin Davis - [irrelative]
* Dustin Bachrach - [dbachrach]
* Mark Shirley - [maspwr]
* Olivier Aubert - [oaubert]
* Yuri Prezument - [yprez]
* Fabian Buechler - [fabianbuechler]
* Mark Hughes - [mhsparks]
* Michael van de Waeter - [mvdwaeter]
* Reinout van Rees - [reinout]
* Michael Richards - [justanotherbody]
* Ben Roberts - [roberts81]
* Venkata Subramanian Mahalingam - [annacoder]
* George Kappel - [gkappel]
* Colin Murtaugh - [cmurtaugh]
* Simon Pantzare - [pilt]
* Szymon Teżewski - [sunscrapers]
* Joel Marcotte - [joual]
* Trey Hunner - [treyhunner]
* Roman Akinfold - [akinfold]
* Toran Billups - [toranb]
* Sébastien Béal - [sebastibe]
* Andrew Hankinson - [ahankinson]
* Juan Riaza - [juanriaza]
* Michael Mior - [michaelmior]
* Marc Tamlyn - [mjtamlyn]
* Richard Wackerbarth - [wackerbarth]
* Johannes Spielmann - [shezi]
* James Cleveland - [radiosilence]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
...@@ -60,27 +102,31 @@ Project hosting is with [GitHub]. ...@@ -60,27 +102,31 @@ Project hosting is with [GitHub].
Continuous integration testing is managed with [Travis CI][travis-ci]. Continuous integration testing is managed with [Travis CI][travis-ci].
The [live sandbox][sandbox] is hosted on [Heroku].
Various inspiration taken from the [Piston], [Tastypie] and [Dagny] projects. Various inspiration taken from the [Piston], [Tastypie] and [Dagny] projects.
Development of REST framework 2.0 was sponsored by [DabApps]. Development of REST framework 2.0 was sponsored by [DabApps].
## Contact ## Contact
To contact the author directly: For usage questions please see the [REST framework discussion group][group].
You can also contact [@_tomchristie][twitter] directly on twitter.
* twitter: [@_tomchristie][twitter]
* email: [tom@tomchristie.com][email]
[email]: mailto:tom@tomchristie.com [email]: mailto:tom@tomchristie.com
[twitter]: http://twitter.com/_tomchristie [twitter]: http://twitter.com/_tomchristie
[bootstrap]: http://twitter.github.com/bootstrap/ [bootstrap]: http://twitter.github.com/bootstrap/
[markdown]: http://daringfireball.net/projects/markdown/ [markdown]: http://daringfireball.net/projects/markdown/
[github]: github.com/tomchristie/django-rest-framework [github]: https://github.com/tomchristie/django-rest-framework
[travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework [travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework
[piston]: https://bitbucket.org/jespern/django-piston [piston]: https://bitbucket.org/jespern/django-piston
[tastypie]: https://github.com/toastdriven/django-tastypie [tastypie]: https://github.com/toastdriven/django-tastypie
[dagny]: https://github.com/zacharyvoase/dagny [dagny]: https://github.com/zacharyvoase/dagny
[dabapps]: http://lab.dabapps.com [dabapps]: http://lab.dabapps.com
[sandbox]: http://restframework.herokuapp.com/
[heroku]: http://www.heroku.com/
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[tomchristie]: https://github.com/tomchristie [tomchristie]: https://github.com/tomchristie
[markotibold]: https://github.com/markotibold [markotibold]: https://github.com/markotibold
...@@ -129,3 +175,45 @@ To contact the author directly: ...@@ -129,3 +175,45 @@ To contact the author directly:
[eofs]: https://github.com/eofs [eofs]: https://github.com/eofs
[rdobson]: https://github.com/rdobson [rdobson]: https://github.com/rdobson
[diviei]: https://github.com/diviei [diviei]: https://github.com/diviei
[madisvain]: https://github.com/madisvain
[minddust]: https://github.com/minddust
[asfaltboy]: https://github.com/asfaltboy
[ottoyiu]: https://github.com/OttoYiu
[jmagnusson]: https://github.com/jmagnusson
[osiloke]: https://github.com/osiloke
[mjs7231]: https://github.com/mjs7231
[tonimichel]: https://github.com/tonimichel
[benkonrath]: https://github.com/benkonrath
[glic3rinu]: https://github.com/glic3rinu
[ludwigkraatz]: https://github.com/ludwigkraatz
[robromano]: https://github.com/robromano
[mechanism]: https://github.com/mechanism
[jonlil]: https://github.com/jonlil
[irrelative]: https://github.com/irrelative
[dbachrach]: https://github.com/dbachrach
[maspwr]: https://github.com/maspwr
[oaubert]: https://github.com/oaubert
[yprez]: https://github.com/yprez
[fabianbuechler]: https://github.com/fabianbuechler
[mhsparks]: https://github.com/mhsparks
[mvdwaeter]: https://github.com/mvdwaeter
[reinout]: https://github.com/reinout
[justanotherbody]: https://github.com/justanotherbody
[roberts81]: https://github.com/roberts81
[annacoder]: https://github.com/annacoder
[gkappel]: https://github.com/gkappel
[cmurtaugh]: https://github.com/cmurtaugh
[pilt]: https://github.com/pilt
[sunscrapers]: https://github.com/sunscrapers
[joual]: https://github.com/joual
[treyhunner]: https://github.com/treyhunner
[akinfold]: https://github.com/akinfold
[toranb]: https://github.com/toranb
[sebastibe]: https://github.com/sebastibe
[ahankinson]: https://github.com/ahankinson
[juanriaza]: https://github.com/juanriaza
[michaelmior]: https://github.com/michaelmior
[mjtamlyn]: https://github.com/mjtamlyn
[wackerbarth]: https://github.com/wackerbarth
[shezi]: https://github.com/shezi
[radiosilence]: https://github.com/radiosilence
# Django REST framework 2
What it is, and why you should care.
> Most people just make the mistake that it should be simple to design simple things. In reality, the effort required to design something is inversely proportional to the simplicity of the result.
>
> &mdash; [Roy Fielding][cite]
---
**Announcement:** REST framework 2 released - Tue 30th Oct 2012
---
REST framework 2 is an almost complete reworking of the original framework, which comprehensively addresses some of the original design issues.
Because the latest version should be considered a re-release, rather than an incremental improvement, we've skipped a version, and called this release Django REST framework 2.0.
This article is intended to give you a flavor of what REST framework 2 is, and why you might want to give it a try.
## User feedback
Before we get cracking, let's start with the hard sell, with a few bits of feedback from some early adopters…
"Django REST framework 2 is beautiful. Some of the API design is worthy of @kennethreitz." - [Kit La Touche][quote1]
"Since it's pretty much just Django, controlling things like URLs has been a breeze... I think [REST framework 2] has definitely got the right approach here; even simple things like being able to override a function called post to do custom work during rather than having to intimately know what happens during a post make a huge difference to your productivity." - [Ian Strachan][quote2]
"I switched to the 2.0 branch and I don't regret it - fully refactored my code in another &half; day and it's *much* more to my tastes" - [Bruno Desthuilliers][quote3]
Sounds good, right? Let's get into some details...
## Serialization
REST framework 2 includes a totally re-worked serialization engine, that was initially intended as a replacement for Django's existing inflexible fixture serialization, and which meets the following design goals:
* A declarative serialization API, that mirrors Django's `Forms`/`ModelForms` API.
* Structural concerns are decoupled from encoding concerns.
* Able to support rendering and parsing to many formats, including both machine-readable representations and HTML forms.
* Validation that can be mapped to obvious and comprehensive error responses.
* Serializers that support both nested, flat, and partially-nested representations.
* Relationships that can be expressed as primary keys, hyperlinks, slug fields, and other custom representations.
Mapping between the internal state of the system and external representations of that state is the core concern of building Web APIs. Designing serializers that allow the developer to do so in a flexible and obvious way is a deceptively difficult design task, and with the new serialization API we think we've pretty much nailed it.
## Generic views
When REST framework was initially released at the start of 2011, the current Django release was version 1.2. REST framework included a backport of Django 1.3's upcoming `View` class, but it didn't take full advantage of the generic view implementations.
With the new release the generic views in REST framework now tie in with Django's generic views. The end result is that framework is clean, lightweight and easy to use.
## Requests, Responses & Views
REST framework 2 includes `Request` and `Response` classes, than are used in place of Django's existing `HttpRequest` and `HttpResponse` classes. Doing so allows logic such as parsing the incoming request or rendering the outgoing response to be supported transparently by the framework.
The `Request`/`Response` approach leads to a much cleaner API, less logic in the view itself, and a simple, obvious request-response cycle.
REST framework 2 also allows you to work with both function-based and class-based views. For simple API views all you need is a single `@api_view` decorator, and you're good to go.
## API Design
Pretty much every aspect of REST framework has been reworked, with the aim of ironing out some of the design flaws of the previous versions. Each of the components of REST framework are cleanly decoupled, and can be used independantly of each-other, and there are no monolithic resource classes, overcomplicated mixin combinations, or opinionated serialization or URL routing decisions.
## The Browseable API
Django REST framework's most unique feature is the way it is able to serve up both machine-readable representations, and a fully browsable HTML representation to the same endpoints.
Browseable Web APIs are easier to work with, visualize and debug, and generally makes it easier and more frictionless to inspect and work with.
With REST framework 2, the browseable API gets a snazzy new bootstrap-based theme that looks great and is even nicer to work with.
There are also some functionality improvments - actions such as as `POST` and `DELETE` will only display if the user has the appropriate permissions.
![Browseable API][image]
**Image above**: An example of the browseable API in REST framework 2
## Documentation
As you can see the documentation for REST framework has been radically improved. It gets a completely new style, using markdown for the documentation source, and a bootstrap-based theme for the styling.
We're really pleased with how the docs style looks - it's simple and clean, is easy to navigate around, and we think it reads great.
## Summary
In short, we've engineered the hell outta this thing, and we're incredibly proud of the result.
If you're interested please take a browse around the documentation. [The tutorial][tut] is a great place to get started.
There's also a [live sandbox version of the tutorial API][sandbox] available for testing.
[cite]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-724
[quote1]: https://twitter.com/kobutsu/status/261689665952833536
[quote2]: https://groups.google.com/d/msg/django-rest-framework/heRGHzG6BWQ/ooVURgpwVC0J
[quote3]: https://groups.google.com/d/msg/django-rest-framework/flsXbvYqRoY/9lSyntOf5cUJ
[image]: ../img/quickstart.png
[readthedocs]: https://readthedocs.org/
[tut]: ../tutorial/1-serialization.md
[sandbox]: http://restframework.herokuapp.com/
...@@ -32,7 +32,7 @@ REST framework also includes [serialization] and [parser]/[renderer] components ...@@ -32,7 +32,7 @@ REST framework also includes [serialization] and [parser]/[renderer] components
## What REST framework doesn't provide. ## What REST framework doesn't provide.
What REST framework doesn't do is give you is machine readable hypermedia formats such as [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope. What REST framework doesn't do is give you is machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
[cite]: http://vimeo.com/channels/restfest/page:2 [cite]: http://vimeo.com/channels/restfest/page:2
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm [dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
...@@ -44,6 +44,7 @@ What REST framework doesn't do is give you is machine readable hypermedia format ...@@ -44,6 +44,7 @@ What REST framework doesn't do is give you is machine readable hypermedia format
[readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list [readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list
[maturitymodel]: http://martinfowler.com/articles/richardsonMaturityModel.html [maturitymodel]: http://martinfowler.com/articles/richardsonMaturityModel.html
[hal]: http://stateless.co/hal_specification.html
[collection]: http://www.amundsen.com/media-types/collection/ [collection]: http://www.amundsen.com/media-types/collection/
[microformats]: http://microformats.org/wiki/Main_Page [microformats]: http://microformats.org/wiki/Main_Page
[serialization]: ../api-guide/serializers.md [serialization]: ../api-guide/serializers.md
......
...@@ -31,69 +31,68 @@ These wrappers provide a few bits of functionality such as making sure you recei ...@@ -31,69 +31,68 @@ These wrappers provide a few bits of functionality such as making sure you recei
The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.DATA` with malformed input. The wrappers also provide behaviour such as returning `405 Method Not Allowed` responses when appropriate, and handling any `ParseError` exception that occurs when accessing `request.DATA` with malformed input.
## Pulling it all together ## Pulling it all together
Okay, let's go ahead and start using these new components to write a few views. Okay, let's go ahead and start using these new components to write a few views.
We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly.
from blog.models import Comment
from blog.serializers import CommentSerializer
from rest_framework import status from rest_framework import status
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
@api_view(['GET', 'POST']) @api_view(['GET', 'POST'])
def comment_root(request): def snippet_list(request):
""" """
List all comments, or create a new comment. List all snippets, or create a new snippet.
""" """
if request.method == 'GET': if request.method == 'GET':
comments = Comment.objects.all() snippets = Snippet.objects.all()
serializer = CommentSerializer(instance=comments) serializer = SnippetSerializer(snippets)
return Response(serializer.data) return Response(serializer.data)
elif request.method == 'POST': elif request.method == 'POST':
serializer = CommentSerializer(request.DATA) serializer = SnippetSerializer(data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
comment = serializer.object serializer.save()
comment.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
else: else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious.
Here is the view for an individual snippet.
@api_view(['GET', 'PUT', 'DELETE']) @api_view(['GET', 'PUT', 'DELETE'])
def comment_instance(request, pk): def snippet_detail(request, pk):
""" """
Retrieve, update or delete a comment instance. Retrieve, update or delete a snippet instance.
""" """
try: try:
comment = Comment.objects.get(pk=pk) snippet = Snippet.objects.get(pk=pk)
except Comment.DoesNotExist: except Snippet.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET': if request.method == 'GET':
serializer = CommentSerializer(instance=comment) serializer = SnippetSerializer(snippet)
return Response(serializer.data) return Response(serializer.data)
elif request.method == 'PUT': elif request.method == 'PUT':
serializer = CommentSerializer(request.DATA, instance=comment) serializer = SnippetSerializer(snippet, data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
comment = serializer.object serializer.save()
comment.save()
return Response(serializer.data) return Response(serializer.data)
else: else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
elif request.method == 'DELETE': elif request.method == 'DELETE':
comment.delete() snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
This should all feel very familiar - there's not a lot different to working with regular Django views. This should all feel very familiar - it is not a lot different from working with regular Django views.
Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us. Notice that we're no longer explicitly tying our requests or responses to a given content type. `request.DATA` can handle incoming `json` requests, but it can also handle `yaml` and other formats. Similarly we're returning response objects with data, but allowing REST framework to render the response into the correct content type for us.
...@@ -103,20 +102,20 @@ To take advantage of the fact that our responses are no longer hardwired to a si ...@@ -103,20 +102,20 @@ To take advantage of the fact that our responses are no longer hardwired to a si
Start by adding a `format` keyword argument to both of the views, like so. Start by adding a `format` keyword argument to both of the views, like so.
def comment_root(request, format=None): def snippet_list(request, format=None):
and and
def comment_instance(request, pk, format=None): def snippet_detail(request, pk, format=None):
Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs. Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs.
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = patterns('blog.views', urlpatterns = patterns('snippets.views',
url(r'^$', 'comment_root'), url(r'^snippets/$', 'snippet_list'),
url(r'^(?P<pk>[0-9]+)$', 'comment_instance') url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail'),
) )
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
...@@ -129,9 +128,7 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][ ...@@ -129,9 +128,7 @@ Go ahead and test the API from the command line, as we did in [tutorial part 1][
**TODO: Describe using accept headers, content-type headers, and format suffixed URLs** **TODO: Describe using accept headers, content-type headers, and format suffixed URLs**
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/][devserver]." Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver].
**Note: Right now the Browseable API only works with the CBV's. Need to fix that.**
### Browsability ### Browsability
...@@ -139,13 +136,12 @@ Because the API chooses a return format based on what the client asks for, it wi ...@@ -139,13 +136,12 @@ Because the API chooses a return format based on what the client asks for, it wi
See the [browsable api][browseable-api] topic for more information about the browsable API feature and how to customize it. See the [browsable api][browseable-api] topic for more information about the browsable API feature and how to customize it.
## What's next? ## What's next?
In [tutorial part 3][tut-3], we'll start using class based views, and see how generic views reduce the amount of code we need to write. In [tutorial part 3][tut-3], we'll start using class based views, and see how generic views reduce the amount of code we need to write.
[json-url]: http://example.com/api/items/4.json [json-url]: http://example.com/api/items/4.json
[devserver]: http://127.0.0.1:8000/ [devserver]: http://127.0.0.1:8000/snippets/
[browseable-api]: ../topics/browsable-api.md [browseable-api]: ../topics/browsable-api.md
[tut-1]: 1-serialization.md [tut-1]: 1-serialization.md
[tut-3]: 3-class-based-views.md [tut-3]: 3-class-based-views.md
...@@ -6,61 +6,58 @@ We can also write our API views using class based views, rather than function ba ...@@ -6,61 +6,58 @@ We can also write our API views using class based views, rather than function ba
We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring. We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
from blog.models import Comment from snippets.models import Snippet
from blog.serializers import CommentSerializer from snippets.serializers import SnippetSerializer
from django.http import Http404 from django.http import Http404
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
class CommentRoot(APIView): class SnippetList(APIView):
""" """
List all comments, or create a new comment. List all snippets, or create a new snippet.
""" """
def get(self, request, format=None): def get(self, request, format=None):
comments = Comment.objects.all() snippets = Snippet.objects.all()
serializer = CommentSerializer(instance=comments) serializer = SnippetSerializer(snippets)
return Response(serializer.data) return Response(serializer.data)
def post(self, request, format=None): def post(self, request, format=None):
serializer = CommentSerializer(request.DATA) serializer = SnippetSerializer(data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
comment = serializer.object serializer.save()
comment.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view. So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view.
class CommentInstance(APIView): class SnippetDetail(APIView):
""" """
Retrieve, update or delete a comment instance. Retrieve, update or delete a snippet instance.
""" """
def get_object(self, pk): def get_object(self, pk):
try: try:
return Comment.objects.get(pk=pk) return Snippet.objects.get(pk=pk)
except Comment.DoesNotExist: except Snippet.DoesNotExist:
raise Http404 raise Http404
def get(self, request, pk, format=None): def get(self, request, pk, format=None):
comment = self.get_object(pk) snippet = self.get_object(pk)
serializer = CommentSerializer(instance=comment) serializer = SnippetSerializer(snippet)
return Response(serializer.data) return Response(serializer.data)
def put(self, request, pk, format=None): def put(self, request, pk, format=None):
comment = self.get_object(pk) snippet = self.get_object(pk)
serializer = CommentSerializer(request.DATA, instance=comment) serializer = SnippetSerializer(snippet, data=request.DATA)
if serializer.is_valid(): if serializer.is_valid():
comment = serializer.object serializer.save()
comment.save()
return Response(serializer.data) return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None): def delete(self, request, pk, format=None):
comment = self.get_object(pk) snippet = self.get_object(pk)
comment.delete() snippet.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
That's looking good. Again, it's still pretty similar to the function based view right now. That's looking good. Again, it's still pretty similar to the function based view right now.
...@@ -69,11 +66,11 @@ We'll also need to refactor our URLconf slightly now we're using class based vie ...@@ -69,11 +66,11 @@ We'll also need to refactor our URLconf slightly now we're using class based vie
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.urlpatterns import format_suffix_patterns
from blogpost import views from snippets import views
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^$', views.CommentRoot.as_view()), url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^(?P<pk>[0-9]+)$', views.CommentInstance.as_view()) url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
) )
urlpatterns = format_suffix_patterns(urlpatterns) urlpatterns = format_suffix_patterns(urlpatterns)
...@@ -88,16 +85,16 @@ The create/retrieve/update/delete operations that we've been using so far are go ...@@ -88,16 +85,16 @@ The create/retrieve/update/delete operations that we've been using so far are go
Let's take a look at how we can compose our views by using the mixin classes. Let's take a look at how we can compose our views by using the mixin classes.
from blog.models import Comment from snippets.models import Snippet
from blog.serializers import CommentSerializer from snippets.serializers import SnippetSerializer
from rest_framework import mixins from rest_framework import mixins
from rest_framework import generics from rest_framework import generics
class CommentRoot(mixins.ListModelMixin, class SnippetList(mixins.ListModelMixin,
mixins.CreateModelMixin, mixins.CreateModelMixin,
generics.MultipleObjectBaseView): generics.MultipleObjectAPIView):
model = Comment model = Snippet
serializer_class = CommentSerializer serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs) return self.list(request, *args, **kwargs)
...@@ -105,16 +102,16 @@ Let's take a look at how we can compose our views by using the mixin classes. ...@@ -105,16 +102,16 @@ Let's take a look at how we can compose our views by using the mixin classes.
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs) return self.create(request, *args, **kwargs)
We'll take a moment to examine exactly what's happening here - We're building our view using `MultipleObjectBaseView`, and adding in `ListModelMixin` and `CreateModelMixin`. We'll take a moment to examine exactly what's happening here. We're building our view using `MultipleObjectAPIView`, and adding in `ListModelMixin` and `CreateModelMixin`.
The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far. The base class provides the core functionality, and the mixin classes provide the `.list()` and `.create()` actions. We're then explicitly binding the `get` and `post` methods to the appropriate actions. Simple enough stuff so far.
class CommentInstance(mixins.RetrieveModelMixin, class SnippetDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin, mixins.UpdateModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
generics.SingleObjectBaseView): generics.SingleObjectAPIView):
model = Comment model = Snippet
serializer_class = CommentSerializer serializer_class = SnippetSerializer
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs) return self.retrieve(request, *args, **kwargs)
...@@ -125,29 +122,29 @@ The base class provides the core functionality, and the mixin classes provide th ...@@ -125,29 +122,29 @@ The base class provides the core functionality, and the mixin classes provide th
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs) return self.destroy(request, *args, **kwargs)
Pretty similar. This time we're using the `SingleObjectBaseView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions. Pretty similar. This time we're using the `SingleObjectAPIView` class to provide the core functionality, and adding in mixins to provide the `.retrieve()`, `.update()` and `.destroy()` actions.
## Using generic class based views ## Using generic class based views
Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use. Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use.
from blog.models import Comment from snippets.models import Snippet
from blog.serializers import CommentSerializer from snippets.serializers import SnippetSerializer
from rest_framework import generics from rest_framework import generics
class CommentRoot(generics.ListCreateAPIView): class SnippetList(generics.ListCreateAPIView):
model = Comment model = Snippet
serializer_class = CommentSerializer serializer_class = SnippetSerializer
class CommentInstance(generics.RetrieveUpdateDestroyAPIView): class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
model = Comment model = Snippet
serializer_class = CommentSerializer serializer_class = SnippetSerializer
Wow, that's pretty concise. We've got a huge amount for free, and our code looks like good, clean, idiomatic Django. Wow, that's pretty concise. We've gotten a huge amount for free, and our code looks like good, clean, idiomatic Django.
Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can customize the behavior of our views to support a range of authentication, permissions, throttling and other aspects. Next we'll move onto [part 4 of the tutorial][tut-4], where we'll take a look at how we can deal with authentication and permissions for our API.
[dry]: http://en.wikipedia.org/wiki/Don't_repeat_yourself [dry]: http://en.wikipedia.org/wiki/Don't_repeat_yourself
[tut-4]: 4-authentication-permissions-and-throttling.md [tut-4]: 4-authentication-and-permissions.md
# Tutorial 4: Authentication & Permissions
Currently our API doesn't have any restrictions on who can edit or delete code snippets. We'd like to have some more advanced behavior in order to make sure that:
* Code snippets are always associated with a creator.
* Only authenticated users may create snippets.
* Only the creator of a snippet may update or delete it.
* Unauthenticated requests should have full read-only access.
## Adding information to our model
We're going to make a couple of changes to our `Snippet` model class.
First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code.
Add the following two fields to the model.
owner = models.ForeignKey('auth.User', related_name='snippets')
highlighted = models.TextField()
We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code higlighting library.
We'll need some extra imports:
from pygments.lexers import get_lexer_by_name
from pygments.formatters import HtmlFormatter
from pygments import highlight
And now we can add a `.save()` method to our model class:
def save(self, *args, **kwargs):
"""
Use the `pygments` library to create a highlighted HTML
representation of the code snippet.
"""
lexer = get_lexer_by_name(self.language)
linenos = self.linenos and 'table' or False
options = self.title and {'title': self.title} or {}
formatter = HtmlFormatter(style=self.style, linenos=linenos,
full=True, **options)
self.highlighted = highlight(self.code, lexer, formatter)
super(Snippet, self).save(*args, **kwargs)
When that's all done we'll need to update our database tables.
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
rm tmp.db
python ./manage.py syncdb
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command.
python ./manage.py createsuperuser
## Adding endpoints for our User models
Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy:
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.ManyPrimaryKeyRelatedField()
class Meta:
model = User
fields = ('id', 'username', 'snippets')
Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it.
We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views.
class UserList(generics.ListAPIView):
model = User
serializer_class = UserSerializer
class UserInstance(generics.RetrieveAPIView):
model = User
serializer_class = UserSerializer
Finally we need to add those views into the API, by referencing them from the URL conf.
url(r'^users/$', views.UserList.as_view()),
url(r'^users/(?P<pk>[0-9]+)/$', views.UserInstance.as_view()),
## Associating Snippets with Users
Right now, if we created a code snippet, there'd be no way of associating the user that created the snippet, with the snippet instance. The user isn't sent as part of the serialized representation, but is instead a property of the incoming request.
The way we deal with that is by overriding a `.pre_save()` method on our snippet views, that allows us to handle any information that is implicit in the incoming request or requested URL.
On **both** the `SnippetList` and `SnippetDetail` view classes, add the following method:
def pre_save(self, obj):
obj.owner = self.request.user
## Updating our serializer
Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition:
owner = serializers.Field(source='owner.username')
**Note**: Make sure you also add `'owner',` to the list of fields in the inner `Meta` class.
This field is doing something quite interesting. The `source` argument controls which attribute is used to populate a field, and can point at any attribute on the serialized instance. It can also take the dotted notation shown above, in which case it will traverse the given attributes, in a similar way as it is used with Django's template language.
The field we've added is the untyped `Field` class, in contrast to the other typed fields, such as `CharField`, `BooleanField` etc... The untyped `Field` is always read-only, and will be used for serialized representations, but will not be used for updating model instances when they are deserialized.
**TODO: Explain the SessionAuthentication and BasicAuthentication classes, and demonstrate using HTTP basic authentication with curl requests**
## Adding required permissions to views
Now that code snippets are associated with users, we want to make sure that only authenticated users are able to create, update and delete code snippets.
REST framework includes a number of permission classes that we can use to restrict who can access a given view. In this case the one we're looking for is `IsAuthenticatedOrReadOnly`, which will ensure that authenticated requests get read-write access, and unauthenticated requests get read-only access.
First add the following import in the views module
from rest_framework import permissions
Then, add the following property to **both** the `SnippetList` and `SnippetDetail` view classes.
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
**TODO: Now that the permissions are restricted, demonstrate using HTTP basic authentication with curl requests**
## Adding login to the Browseable API
If you open a browser and navigate to the browseable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
We can add a login view for use with the browseable API, by editing our URLconf once more.
Add the following import at the top of the file:
from django.conf.urls import include
And, at the end of the file, add a pattern to include the login and logout views for the browseable API.
urlpatterns += patterns('',
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
)
The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace.
Now if you open up the browser again and refresh the page you'll see a 'Login' link in the top right of the page. If you log in as one of the users you created earier, you'll be able to create code snippets again.
Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet pks that are associated with each user, in each user's 'snippets' field.
## Object level permissions
Really we'd like all code snippets to be visible to anyone, but also make sure that only the user that created a code snippet is able update or delete it.
To do that we're going to need to create a custom permission.
In the snippets app, create a new file, `permissions.py`
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_permission(self, request, view, obj=None):
# Skip the check unless this is an object-level test
if obj is None:
return True
# Read permissions are allowed to any request
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the snippet
return obj.owner == request.user
Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class:
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
Make sure to also import the `IsOwnerOrReadOnly` class.
from snippets.permissions import IsOwnerOrReadOnly
Now, if you open a browser again, you find that the 'DELETE' and 'PUT' actions only appear on a snippet instance endpoint if you're logged in as the same user that created the code snippet.
## Summary
We've now got a fairly fine-grained set of permissions on our Web API, and end points for users of the system and for the code snippets that they have created.
In [part 5][tut-5] of the tutorial we'll look at how we can tie everything together by creating an HTML endpoint for our hightlighted snippets, and improve the cohesion of our API by using hyperlinking for the relationships within the system.
[tut-5]: 5-relationships-and-hyperlinked-apis.md
\ No newline at end of file
# Tutorial 4: Authentication & Permissions
Nothing to see here. Onwards to [part 5][tut-5].
[tut-5]: 5-relationships-and-hyperlinked-apis.md
\ No newline at end of file
# Tutorial 5 - Relationships & Hyperlinked APIs # Tutorial 5 - Relationships & Hyperlinked APIs
**TODO** At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.
* Create BlogPost model ## Creating an endpoint for the root of our API
* Demonstrate nested relationships
* Demonstrate and describe hyperlinked relationships
<!-- Onwards to [part 6][tut-6]. Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier.
[tut-6]: 6-resource-orientated-projects.md --> from rest_framework import renderers
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
@api_view(('GET',))
def api_root(request, format=None):
return Response({
'users': reverse('user-list', request=request, format=format),
'snippets': reverse('snippet-list', request=request, format=format)
})
Notice that we're using REST framework's `reverse` function in order to return fully-qualified URLs.
## Creating an endpoint for the highlighted snippets
The other obvious thing that's still missing from our pastebin API is the code highlighting endpoints.
Unlike all our other API endpoints, we don't want to use JSON, but instead just present an HTML representation. There are two styles of HTML renderer provided by REST framework, one for dealing with HTML rendered using templates, the other for dealing with pre-rendered HTML. The second renderer is the one we'd like to use for this endpoint.
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your snippets.views add:
from rest_framework import renderers
from rest_framework.response import Response
class SnippetHighlight(generics.SingleObjectAPIView):
model = Snippet
renderer_classes = (renderers.StaticHTMLRenderer,)
def get(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
As usual we need to add the new views that we've created in to our URLconf.
We'll add a url pattern for our new API root:
url(r'^$', 'api_root'),
And then add a url pattern for the snippet highlights:
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', views.SnippetHighlight.as_view()),
## Hyperlinking our API
Dealing with relationships between entities is one of the more challenging aspects of Web API design. There are a number of different ways that we might choose to represent a relationship:
* Using primary keys.
* Using hyperlinking between entities.
* Using a unique identifying slug field on the related entity.
* Using the default string representation of the related entity.
* Nesting the related entity inside the parent representation.
* Some other custom representation.
REST framework supports all of these styles, and can apply them across forward or reverse relationships, or apply them across custom managers such as generic foreign keys.
In this case we'd like to use a hyperlinked style between entities. In order to do so, we'll modify our serializers to extend `HyperlinkedModelSerializer` instead of the existing `ModelSerializer`.
The `HyperlinkedModelSerializer` has the following differences from `ModelSerializer`:
* It does not include the `pk` field by default.
* It includes a `url` field, using `HyperlinkedIdentityField`.
* Relationships use `HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField`,
instead of `PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField`.
We can easily re-write our existing serializers to use hyperlinking.
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.Field(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
class Meta:
model = models.Snippet
fields = ('url', 'highlight', 'owner',
'title', 'code', 'linenos', 'language', 'style')
class UserSerializer(serializers.HyperlinkedModelSerializer):
snippets = serializers.ManyHyperlinkedRelatedField(view_name='snippet-detail')
class Meta:
model = User
fields = ('url', 'username', 'snippets')
Notice that we've also added a new `'highlight'` field. This field is of the same type as the `url` field, except that it points to the `'snippet-highlight'` url pattern, instead of the `'snippet-detail'` url pattern.
Because we've included format suffixed URLs such as `'.json'`, we also need to indicate on the `highlight` field that any format suffixed hyperlinks it returns should use the `'.html'` suffix.
## Making sure our URL patterns are named
If we're going to have a hyperlinked API, we need to make sure we name our URL patterns. Let's take a look at which URL patterns we need to name.
* The root of our API refers to `'user-list'` and `'snippet-list'`.
* Our snippet serializer includes a field that refers to `'snippet-highlight'`.
* Our user serializer includes a field that refers to `'snippet-detail'`.
* Our snippet and user serializers include `'url'` fields that by default will refer to `'{model_name}-detail'`, which in this case will be `'snippet-detail'` and `'user-detail'`.
After adding all those names into our URLconf, our final `'urls.py'` file should look something like this:
# API endpoints
urlpatterns = format_suffix_patterns(patterns('snippets.views',
url(r'^$', 'api_root'),
url(r'^snippets/$',
views.SnippetList.as_view(),
name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$',
views.SnippetDetail.as_view(),
name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$',
views.SnippetHighlight.as_view(),
name='snippet-highlight'),
url(r'^users/$',
views.UserList.as_view(),
name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$',
views.UserInstance.as_view(),
name='user-detail')
))
# Login and logout views for the browsable API
urlpatterns += patterns('',
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
)
## Adding pagination
The list views for users and code snippets could end up returning quite a lot of instances, so really we'd like to make sure we paginate the results, and allow the API client to step through each of the individual pages.
We can change the default list style to use pagination, by modifying our `settings.py` file slightly. Add the following setting:
REST_FRAMEWORK = {
'PAGINATE_BY': 10
}
Note that settings in REST framework are all namespaced into a single dictionary setting, named 'REST_FRAMEWORK', which helps keep them well seperated from your other project settings.
We could also customize the pagination style if we needed too, but in this case we'll just stick with the default.
## Reviewing our work
If we open a browser and navigate to the browseable API, you'll find that you can now work your way around the API simply by following links.
You'll also be able to see the 'highlight' links on the snippet instances, that will take you to the highlighted code HTML representations.
We've now got a complete pastebin Web API, which is fully web browseable, and comes complete with authentication, per-object permissions, and multiple renderer formats.
We've walked through each step of the design process, and seen how if we need to customize anything we can gradually work our way down to simply using regular Django views.
You can review the final [tutorial code][repo] on GitHub, or try out a live example in [the sandbox][sandbox].
## Onwards and upwards
We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
* Join the [REST framework discussion group][group], and help build the community.
* [Follow the author on Twitter][twitter] and say hi.
**Now go build awesome things.**
[repo]: https://github.com/tomchristie/rest-framework-tutorial
[sandbox]: http://restframework.herokuapp.com/
[github]: https://github.com/tomchristie/django-rest-framework
[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework
[twitter]: https://twitter.com/_tomchristie
\ No newline at end of file
...@@ -8,7 +8,7 @@ Create a new Django project, and start a new app called `quickstart`. Once you' ...@@ -8,7 +8,7 @@ Create a new Django project, and start a new app called `quickstart`. Once you'
First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations. First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations.
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group, Permission
from rest_framework import serializers from rest_framework import serializers
...@@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t ...@@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t
class GroupSerializer(serializers.HyperlinkedModelSerializer): class GroupSerializer(serializers.HyperlinkedModelSerializer):
permissions = serializers.ManySlugRelatedField(
slug_field='codename',
queryset=Permission.objects.all()
)
class Meta: class Meta:
model = Group model = Group
fields = ('url', 'name', 'permissions') fields = ('url', 'name', 'permissions')
Notice that we're using hyperlinked relations in this case, with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design. Notice that we're using hyperlinked relations in this case, with `HyperlinkedModelSerializer`. You can also use primary key and various other relationships, but hyperlinking is good RESTful design.
We've also overridden the `permission` field on the `GroupSerializer`. In this case we don't want to use a hyperlinked representation, but instead use the list of permission codenames associated with the group, so we've used a `ManySlugRelatedField`, using the `codename` field for the representation.
## Views ## Views
Right, we'd better write some views then. Open `quickstart/views.py` and get typing. Right, we'd better write some views then. Open `quickstart/views.py` and get typing.
...@@ -130,7 +137,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a ...@@ -130,7 +137,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a
'PAGINATE_BY': 10 'PAGINATE_BY': 10
} }
Okay, that's us done. Okay, we're done.
--- ---
...@@ -152,7 +159,7 @@ We can now access our API, both from the command-line, using tools like `curl`.. ...@@ -152,7 +159,7 @@ We can now access our API, both from the command-line, using tools like `curl`..
}, },
{ {
"email": "tom@example.com", "email": "tom@example.com",
"groups": [], "groups": [ ],
"url": "http://127.0.0.1:8000/users/2/", "url": "http://127.0.0.1:8000/users/2/",
"username": "tom" "username": "tom"
} }
......
...@@ -11,20 +11,21 @@ docs_dir = os.path.join(root_dir, 'docs') ...@@ -11,20 +11,21 @@ docs_dir = os.path.join(root_dir, 'docs')
html_dir = os.path.join(root_dir, 'html') html_dir = os.path.join(root_dir, 'html')
local = not '--deploy' in sys.argv local = not '--deploy' in sys.argv
preview = '-p' in sys.argv
if local: if local:
base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir)) base_url = 'file://%s/' % os.path.normpath(os.path.join(os.getcwd(), html_dir))
suffix = '.html' suffix = '.html'
index = 'index.html' index = 'index.html'
else: else:
base_url = 'http://tomchristie.github.com/django-rest-framework' base_url = 'http://django-rest-framework.org'
suffix = '' suffix = '.html'
index = '' index = ''
main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>' main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>' sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>'
code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/blob/restframework2/rest_framework/\1"><span class="label label-info">\1</span></a>' code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/tree/master/rest_framework/\1"><span class="label label-info">\1</span></a>'
page = open(os.path.join(docs_dir, 'template.html'), 'r').read() page = open(os.path.join(docs_dir, 'template.html'), 'r').read()
...@@ -80,3 +81,15 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): ...@@ -80,3 +81,15 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output) output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output) output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
open(output_path, 'w').write(output.encode('utf-8')) open(output_path, 'w').write(output.encode('utf-8'))
if preview:
import subprocess
url = 'html/index.html'
try:
subprocess.Popen(["open", url]) # Mac
except OSError:
subprocess.Popen(["xdg-open", url]) # Linux
except:
os.startfile(url) # Windows
markdown>=2.1.0 markdown>=2.1.0
PyYAML>=3.10 PyYAML>=3.10
django-filter>=0.5.4
__version__ = '2.0.0' __version__ = '2.1.16'
VERSION = __version__ # synonym VERSION = __version__ # synonym
...@@ -5,13 +5,21 @@ from south.v2 import SchemaMigration ...@@ -5,13 +5,21 @@ from south.v2 import SchemaMigration
from django.db import models from django.db import models
try:
from django.contrib.auth import get_user_model
except ImportError: # django < 1.5
from django.contrib.auth.models import User
else:
User = get_user_model()
class Migration(SchemaMigration): class Migration(SchemaMigration):
def forwards(self, orm): def forwards(self, orm):
# Adding model 'Token' # Adding model 'Token'
db.create_table('authtoken_token', ( db.create_table('authtoken_token', (
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)), ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['auth.User'])), ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
)) ))
db.send_create_signal('authtoken', ['Token']) db.send_create_signal('authtoken', ['Token'])
...@@ -36,7 +44,7 @@ class Migration(SchemaMigration): ...@@ -36,7 +44,7 @@ class Migration(SchemaMigration):
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
}, },
'auth.user': { "%s.%s" % (User._meta.app_label, User._meta.module_name): {
'Meta': {'object_name': 'User'}, 'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
...@@ -56,7 +64,7 @@ class Migration(SchemaMigration): ...@@ -56,7 +64,7 @@ class Migration(SchemaMigration):
'Meta': {'object_name': 'Token'}, 'Meta': {'object_name': 'Token'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}), 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['auth.User']"}) 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
}, },
'contenttypes.contenttype': { 'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
......
import uuid import uuid
import hmac import hmac
from hashlib import sha1 from hashlib import sha1
from rest_framework.compat import User
from django.db import models from django.db import models
...@@ -9,7 +10,7 @@ class Token(models.Model): ...@@ -9,7 +10,7 @@ class Token(models.Model):
The default authorization token model. The default authorization token model.
""" """
key = models.CharField(max_length=40, primary_key=True) key = models.CharField(max_length=40, primary_key=True)
user = models.OneToOneField('auth.User', related_name='auth_token') user = models.OneToOneField(User, related_name='auth_token')
created = models.DateTimeField(auto_now_add=True) created = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
......
from django.contrib.auth import authenticate
from rest_framework import serializers
class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
if username and password:
user = authenticate(username=username, password=password)
if user:
if not user.is_active:
raise serializers.ValidationError('User account is disabled.')
attrs['user'] = user
return attrs
else:
raise serializers.ValidationError('Unable to login with provided credentials.')
else:
raise serializers.ValidationError('Must include "username" and "password"')
from rest_framework.views import APIView
from rest_framework import status
from rest_framework import parsers
from rest_framework import renderers
from rest_framework.response import Response
from rest_framework.authtoken.models import Token
from rest_framework.authtoken.serializers import AuthTokenSerializer
class ObtainAuthToken(APIView):
throttle_classes = ()
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
serializer_class = AuthTokenSerializer
model = Token
def post(self, request):
serializer = self.serializer_class(data=request.DATA)
if serializer.is_valid():
token, created = Token.objects.get_or_create(user=serializer.object['user'])
return Response({'token': token.key})
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
obtain_auth_token = ObtainAuthToken.as_view()
""" """
The :mod:`compat` module provides support for backwards compatibility with older versions of django/python. The `compat` module provides support for backwards compatibility with older
versions of django/python, and compatibility wrappers around optional packages.
""" """
# flake8: noqa
import django import django
# location of patterns, url, include changes in 1.4 onwards
try:
from django.conf.urls import patterns, url, include
except:
from django.conf.urls.defaults import patterns, url, include
# django-filter is optional
try:
import django_filters
except:
django_filters = None
# cStringIO only if it's available, otherwise StringIO # cStringIO only if it's available, otherwise StringIO
try: try:
import cStringIO as StringIO import cStringIO as StringIO
...@@ -10,6 +25,16 @@ except ImportError: ...@@ -10,6 +25,16 @@ except ImportError:
import StringIO import StringIO
# Try to import PIL in either of the two ways it can end up installed.
try:
from PIL import Image
except ImportError:
try:
import Image
except ImportError:
Image = None
def get_concrete_model(model_cls): def get_concrete_model(model_cls):
try: try:
return model_cls._meta.concrete_model return model_cls._meta.concrete_model
...@@ -18,6 +43,20 @@ def get_concrete_model(model_cls): ...@@ -18,6 +43,20 @@ def get_concrete_model(model_cls):
return model_cls return model_cls
# Django 1.5 add support for custom auth user model
if django.VERSION >= (1, 5):
from django.conf import settings
if hasattr(settings, 'AUTH_USER_MODEL'):
User = settings.AUTH_USER_MODEL
else:
from django.contrib.auth.models import User
else:
try:
from django.contrib.auth.models import User
except ImportError:
raise ImportError(u"User model is not to be found.")
# First implementation of Django class-based views did not include head method # First implementation of Django class-based views did not include head method
# in base View class - https://code.djangoproject.com/ticket/15668 # in base View class - https://code.djangoproject.com/ticket/15668
if django.VERSION >= (1, 4): if django.VERSION >= (1, 4):
...@@ -57,6 +96,12 @@ else: ...@@ -57,6 +96,12 @@ else:
update_wrapper(view, cls.dispatch, assigned=()) update_wrapper(view, cls.dispatch, assigned=())
return view 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
# PUT, DELETE do not require CSRF until 1.4. They should. Make it better. # PUT, DELETE do not require CSRF until 1.4. They should. Make it better.
if django.VERSION >= (1, 4): if django.VERSION >= (1, 4):
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware
...@@ -331,7 +376,7 @@ try: ...@@ -331,7 +376,7 @@ try:
""" """
extensions = ['headerid(level=2)'] extensions = ['headerid(level=2)']
safe_mode = False, safe_mode = False
md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode) md = markdown.Markdown(extensions=extensions, safe_mode=safe_mode)
return md.convert(text) return md.convert(text)
...@@ -346,33 +391,6 @@ except ImportError: ...@@ -346,33 +391,6 @@ except ImportError:
yaml = None yaml = None
import unittest
try:
import unittest.skip
except ImportError: # python < 2.7
from unittest import TestCase
import functools
def skip(reason):
# Pasted from py27/lib/unittest/case.py
"""
Unconditionally skip a test.
"""
def decorator(test_item):
if not (isinstance(test_item, type) and issubclass(test_item, TestCase)):
@functools.wraps(test_item)
def skip_wrapper(*args, **kwargs):
pass
test_item = skip_wrapper
test_item.__unittest_skip__ = True
test_item.__unittest_skip_why__ = reason
return test_item
return decorator
unittest.skip = skip
# xml.etree.parse only throws ParseError for python >= 2.7 # xml.etree.parse only throws ParseError for python >= 2.7
try: try:
from xml.etree import ParseError as ETParseError from xml.etree import ParseError as ETParseError
......
...@@ -10,8 +10,18 @@ def api_view(http_method_names): ...@@ -10,8 +10,18 @@ def api_view(http_method_names):
def decorator(func): def decorator(func):
class WrappedAPIView(APIView): WrappedAPIView = type(
pass 'WrappedAPIView',
(APIView,),
{'__doc__': func.__doc__}
)
# Note, the above allows us to set the docstring.
# It is the equivalent of:
#
# class WrappedAPIView(APIView):
# pass
# WrappedAPIView.__doc__ = func.doc <--- Not possible to do this
allowed_methods = set(http_method_names) | set(('options',)) allowed_methods = set(http_method_names) | set(('options',))
WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods] WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods]
......
...@@ -31,14 +31,6 @@ class PermissionDenied(APIException): ...@@ -31,14 +31,6 @@ class PermissionDenied(APIException):
self.detail = detail or self.default_detail 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): class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = "Method '%s' not allowed." default_detail = "Method '%s' not allowed."
......
from rest_framework.compat import django_filters
FilterSet = django_filters and django_filters.FilterSet or None
class BaseFilterBackend(object):
"""
A base class from which all filter backend classes should inherit.
"""
def filter_queryset(self, request, queryset, view):
"""
Return a filtered queryset.
"""
raise NotImplementedError(".filter_queryset() must be overridden.")
class DjangoFilterBackend(BaseFilterBackend):
"""
A filter backend that uses django-filter.
"""
default_filter_set = FilterSet
def __init__(self):
assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed'
def get_filter_class(self, view):
"""
Return the django-filters `FilterSet` used to filter the queryset.
"""
filter_class = getattr(view, 'filter_class', None)
filter_fields = getattr(view, 'filter_fields', None)
view_model = getattr(view, 'model', None)
if filter_class:
filter_model = filter_class.Meta.model
assert issubclass(filter_model, view_model), \
'FilterSet model %s does not match view model %s' % \
(filter_model, view_model)
return filter_class
if filter_fields:
class AutoFilterSet(self.default_filter_set):
class Meta:
model = view_model
fields = filter_fields
return AutoFilterSet
return None
def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view)
if filter_class:
return filter_class(request.GET, queryset=queryset)
return queryset
""" """
Generic views that provide commmonly needed behaviour. Generic views that provide commonly needed behaviour.
""" """
from rest_framework import views, mixins from rest_framework import views, mixins
...@@ -14,6 +14,8 @@ class GenericAPIView(views.APIView): ...@@ -14,6 +14,8 @@ class GenericAPIView(views.APIView):
""" """
Base class for all other generic views. Base class for all other generic views.
""" """
model = None
serializer_class = None serializer_class = None
model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS model_serializer_class = api_settings.DEFAULT_MODEL_SERIALIZER_CLASS
...@@ -30,8 +32,10 @@ class GenericAPIView(views.APIView): ...@@ -30,8 +32,10 @@ class GenericAPIView(views.APIView):
def get_serializer_class(self): def get_serializer_class(self):
""" """
Return the class to use for the serializer. Return the class to use for the serializer.
Use `self.serializer_class`, falling back to constructing a
model serializer class from `self.model_serializer_class` 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 serializer_class = self.serializer_class
...@@ -43,12 +47,19 @@ class GenericAPIView(views.APIView): ...@@ -43,12 +47,19 @@ class GenericAPIView(views.APIView):
return serializer_class return serializer_class
def get_serializer(self, data=None, files=None, instance=None): def get_serializer(self, instance=None, data=None,
# TODO: add support for files files=None, partial=False):
# TODO: add support for seperate serializer/deserializer """
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class() serializer_class = self.get_serializer_class()
context = self.get_serializer_context() context = self.get_serializer_context()
return serializer_class(data, instance=instance, context=context) return serializer_class(instance, data=data, files=files,
partial=partial, context=context)
def pre_save(self, obj):
pass
class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
...@@ -56,37 +67,59 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView): ...@@ -56,37 +67,59 @@ class MultipleObjectAPIView(MultipleObjectMixin, GenericAPIView):
Base class for generic views onto a queryset. Base class for generic views onto a queryset.
""" """
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
paginate_by = api_settings.PAGINATE_BY paginate_by = api_settings.PAGINATE_BY
paginate_by_param = api_settings.PAGINATE_BY_PARAM
pagination_serializer_class = api_settings.DEFAULT_PAGINATION_SERIALIZER_CLASS
filter_backend = api_settings.FILTER_BACKEND
def get_pagination_serializer_class(self): def filter_queryset(self, queryset):
""" """
Return the class to use for the pagination serializer. 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_pagination_serializer(self, page=None):
"""
Return a serializer instance to use with paginated data.
""" """
class SerializerClass(self.pagination_serializer_class): class SerializerClass(self.pagination_serializer_class):
class Meta: class Meta:
object_serializer_class = self.get_serializer_class() object_serializer_class = self.get_serializer_class()
return SerializerClass pagination_serializer_class = SerializerClass
def get_pagination_serializer(self, page=None):
pagination_serializer_class = self.get_pagination_serializer_class()
context = self.get_serializer_context() context = self.get_serializer_context()
return pagination_serializer_class(instance=page, context=context) return pagination_serializer_class(instance=page, context=context)
def get_paginate_by(self, queryset):
"""
Return the size of pages to use with pagination.
"""
if self.paginate_by_param:
query_params = self.request.QUERY_PARAMS
try:
return int(query_params[self.paginate_by_param])
except (KeyError, ValueError):
pass
return self.paginate_by
class SingleObjectAPIView(SingleObjectMixin, GenericAPIView): class SingleObjectAPIView(SingleObjectMixin, GenericAPIView):
""" """
Base class for generic views onto a model instance. Base class for generic views onto a model instance.
""" """
pk_url_kwarg = 'pk' # Not provided in Django 1.3 pk_url_kwarg = 'pk' # Not provided in Django 1.3
slug_url_kwarg = 'slug' # Not provided in Django 1.3 slug_url_kwarg = 'slug' # Not provided in Django 1.3
slug_field = 'slug'
def get_object(self): def get_object(self, queryset=None):
""" """
Override default to add support for object-level permissions. Override default to add support for object-level permissions.
""" """
obj = super(SingleObjectAPIView, self).get_object() obj = super(SingleObjectAPIView, self).get_object(queryset)
if not self.has_permission(self.request, obj): if not self.has_permission(self.request, obj):
self.permission_denied(self.request) self.permission_denied(self.request)
return obj return obj
...@@ -125,7 +158,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, ...@@ -125,7 +158,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin,
class DestroyAPIView(mixins.DestroyModelMixin, class DestroyAPIView(mixins.DestroyModelMixin,
SingleObjectAPIView): SingleObjectAPIView):
""" """
Concrete view for deleting a model instance. Concrete view for deleting a model instance.
...@@ -143,6 +176,10 @@ class UpdateAPIView(mixins.UpdateModelMixin, ...@@ -143,6 +176,10 @@ class UpdateAPIView(mixins.UpdateModelMixin,
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs) return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
class ListCreateAPIView(mixins.ListModelMixin, class ListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin, mixins.CreateModelMixin,
...@@ -157,6 +194,23 @@ class ListCreateAPIView(mixins.ListModelMixin, ...@@ -157,6 +194,23 @@ class ListCreateAPIView(mixins.ListModelMixin,
return self.create(request, *args, **kwargs) return self.create(request, *args, **kwargs)
class RetrieveUpdateAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
SingleObjectAPIView):
"""
Concrete view for retrieving, updating a model instance.
"""
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
class RetrieveDestroyAPIView(mixins.RetrieveModelMixin, class RetrieveDestroyAPIView(mixins.RetrieveModelMixin,
mixins.DestroyModelMixin, mixins.DestroyModelMixin,
SingleObjectAPIView): SingleObjectAPIView):
...@@ -183,5 +237,9 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin, ...@@ -183,5 +237,9 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs) return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs) return self.destroy(request, *args, **kwargs)
...@@ -3,9 +3,6 @@ Basic building blocks for generic class based views. ...@@ -3,9 +3,6 @@ Basic building blocks for generic class based views.
We don't bind behaviour to http method handlers yet, We don't bind behaviour to http method handlers yet,
which allows mixin classes to be composed in interesting ways. which allows mixin classes to be composed in interesting ways.
Eg. Use mixins to build a Resource class, and have a Router class
perform the binding of http methods to actions for us.
""" """
from django.http import Http404 from django.http import Http404
from rest_framework import status from rest_framework import status
...@@ -18,30 +15,42 @@ class CreateModelMixin(object): ...@@ -18,30 +15,42 @@ class CreateModelMixin(object):
Should be mixed in with any `BaseView`. Should be mixed in with any `BaseView`.
""" """
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA) serializer = self.get_serializer(data=request.DATA, files=request.FILES)
if serializer.is_valid(): if serializer.is_valid():
self.pre_save(serializer.object) self.pre_save(serializer.object)
self.object = serializer.save() self.object = serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def get_success_headers(self, data):
try:
return {'Location': data['url']}
except (TypeError, KeyError):
return {}
class ListModelMixin(object): class ListModelMixin(object):
""" """
List a queryset. List a queryset.
Should be mixed in with `MultipleObjectBaseView`. Should be mixed in with `MultipleObjectAPIView`.
""" """
empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." empty_error = u"Empty list and '%(class_name)s.allow_empty' is False."
def list(self, request, *args, **kwargs): def list(self, request, *args, **kwargs):
self.object_list = self.get_queryset() queryset = self.get_queryset()
self.object_list = self.filter_queryset(queryset)
# Default is to allow empty querysets. This can be altered by setting # Default is to allow empty querysets. This can be altered by setting
# `.allow_empty = False`, to raise 404 errors on empty querysets. # `.allow_empty = False`, to raise 404 errors on empty querysets.
allow_empty = self.get_allow_empty() allow_empty = self.get_allow_empty()
if not allow_empty and len(self.object_list) == 0: if not allow_empty and not self.object_list:
error_args = {'class_name': self.__class__.__name__} class_name = self.__class__.__name__
raise Http404(self.empty_error % error_args) error_msg = self.empty_error % {'class_name': class_name}
raise Http404(error_msg)
# Pagination size is set by the `.paginate_by` attribute, # Pagination size is set by the `.paginate_by` attribute,
# which may be `None` to disable pagination. # which may be `None` to disable pagination.
...@@ -51,7 +60,7 @@ class ListModelMixin(object): ...@@ -51,7 +60,7 @@ class ListModelMixin(object):
paginator, page, queryset, is_paginated = packed paginator, page, queryset, is_paginated = packed
serializer = self.get_pagination_serializer(page) serializer = self.get_pagination_serializer(page)
else: else:
serializer = self.get_serializer(instance=self.object_list) serializer = self.get_serializer(self.object_list)
return Response(serializer.data) return Response(serializer.data)
...@@ -63,7 +72,7 @@ class RetrieveModelMixin(object): ...@@ -63,7 +72,7 @@ class RetrieveModelMixin(object):
""" """
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
serializer = self.get_serializer(instance=self.object) serializer = self.get_serializer(self.object)
return Response(serializer.data) return Response(serializer.data)
...@@ -73,17 +82,21 @@ class UpdateModelMixin(object): ...@@ -73,17 +82,21 @@ class UpdateModelMixin(object):
Should be mixed in with `SingleObjectBaseView`. Should be mixed in with `SingleObjectBaseView`.
""" """
def update(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
try: try:
self.object = self.get_object() self.object = self.get_object()
success_status_code = status.HTTP_200_OK
except Http404: except Http404:
self.object = None self.object = None
success_status_code = status.HTTP_201_CREATED
serializer = self.get_serializer(data=request.DATA, instance=self.object) serializer = self.get_serializer(self.object, data=request.DATA,
files=request.FILES, partial=partial)
if serializer.is_valid(): if serializer.is_valid():
self.pre_save(serializer.object) self.pre_save(serializer.object)
self.object = serializer.save() self.object = serializer.save()
return Response(serializer.data) return Response(serializer.data, status=success_status_code)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
...@@ -101,6 +114,11 @@ class UpdateModelMixin(object): ...@@ -101,6 +114,11 @@ class UpdateModelMixin(object):
slug_field = self.get_slug_field() slug_field = self.get_slug_field()
setattr(obj, slug_field, slug) setattr(obj, slug_field, slug)
# Ensure we clean the attributes so that we don't eg return integer
# pk using a string representation, as provided by the url conf kwarg.
if hasattr(obj, 'full_clean'):
obj.full_clean()
class DestroyModelMixin(object): class DestroyModelMixin(object):
""" """
...@@ -108,6 +126,6 @@ class DestroyModelMixin(object): ...@@ -108,6 +126,6 @@ class DestroyModelMixin(object):
Should be mixed in with `SingleObjectBaseView`. Should be mixed in with `SingleObjectBaseView`.
""" """
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
self.object = self.get_object() obj = self.get_object()
self.object.delete() obj.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
from django.http import Http404
from rest_framework import exceptions from rest_framework import exceptions
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
from rest_framework.utils.mediatypes import _MediaType
class BaseContentNegotiation(object): class BaseContentNegotiation(object):
...@@ -47,7 +49,8 @@ class DefaultContentNegotiation(BaseContentNegotiation): ...@@ -47,7 +49,8 @@ class DefaultContentNegotiation(BaseContentNegotiation):
for media_type in media_type_set: for media_type in media_type_set:
if media_type_matches(renderer.media_type, media_type): if media_type_matches(renderer.media_type, media_type):
# Return the most specific media type as accepted. # Return the most specific media type as accepted.
if len(renderer.media_type) > len(media_type): if (_MediaType(renderer.media_type).precedence >
_MediaType(media_type).precedence):
# Eg client requests '*/*' # Eg client requests '*/*'
# Accepted media type is 'application/json' # Accepted media type is 'application/json'
return renderer, renderer.media_type return renderer, renderer.media_type
...@@ -66,7 +69,7 @@ class DefaultContentNegotiation(BaseContentNegotiation): ...@@ -66,7 +69,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
renderers = [renderer for renderer in renderers renderers = [renderer for renderer in renderers
if renderer.format == format] if renderer.format == format]
if not renderers: if not renderers:
raise exceptions.InvalidFormat(format) raise Http404
return renderers return renderers
def get_accept_list(self, request): def get_accept_list(self, request):
......
from rest_framework import serializers from rest_framework import serializers
from rest_framework.templatetags.rest_framework import replace_query_param
# TODO: Support URLconf kwarg-style paging # TODO: Support URLconf kwarg-style paging
...@@ -7,30 +8,30 @@ class NextPageField(serializers.Field): ...@@ -7,30 +8,30 @@ class NextPageField(serializers.Field):
""" """
Field that returns a link to the next page in paginated results. Field that returns a link to the next page in paginated results.
""" """
page_field = 'page'
def to_native(self, value): def to_native(self, value):
if not value.has_next(): if not value.has_next():
return None return None
page = value.next_page_number() page = value.next_page_number()
request = self.context.get('request') request = self.context.get('request')
relative_url = '?page=%d' % page url = request and request.build_absolute_uri() or ''
if request: return replace_query_param(url, self.page_field, page)
return request.build_absolute_uri(relative_url)
return relative_url
class PreviousPageField(serializers.Field): class PreviousPageField(serializers.Field):
""" """
Field that returns a link to the previous page in paginated results. Field that returns a link to the previous page in paginated results.
""" """
page_field = 'page'
def to_native(self, value): def to_native(self, value):
if not value.has_previous(): if not value.has_previous():
return None return None
page = value.previous_page_number() page = value.previous_page_number()
request = self.context.get('request') request = self.context.get('request')
relative_url = '?page=%d' % page url = request and request.build_absolute_uri() or ''
if request: return replace_query_param(url, self.page_field, page)
return request.build_absolute_uri('?page=%d' % page)
return relative_url
class PaginationSerializerOptions(serializers.SerializerOptions): class PaginationSerializerOptions(serializers.SerializerOptions):
......
...@@ -8,11 +8,11 @@ on the request, such as form content or json encoded data. ...@@ -8,11 +8,11 @@ on the request, such as form content or json encoded data.
from django.http import QueryDict from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError from django.http.multipartparser import MultiPartParserError
from django.utils import simplejson as json
from rest_framework.compat import yaml, ETParseError from rest_framework.compat import yaml, ETParseError
from rest_framework.exceptions import ParseError from rest_framework.exceptions import ParseError
from xml.etree import ElementTree as ET from xml.etree import ElementTree as ET
from xml.parsers.expat import ExpatError from xml.parsers.expat import ExpatError
import json
import datetime import datetime
import decimal import decimal
......
...@@ -18,6 +18,17 @@ class BasePermission(object): ...@@ -18,6 +18,17 @@ class BasePermission(object):
raise NotImplementedError(".has_permission() must be overridden.") raise NotImplementedError(".has_permission() must be overridden.")
class AllowAny(BasePermission):
"""
Allow any access.
This isn't strictly required, since you could use an empty
permission_classes list, but it's useful because it makes the intention
more explicit.
"""
def has_permission(self, request, view, obj=None):
return True
class IsAuthenticated(BasePermission): class IsAuthenticated(BasePermission):
""" """
Allows access only to authenticated users. Allows access only to authenticated users.
...@@ -85,7 +96,7 @@ class DjangoModelPermissions(BasePermission): ...@@ -85,7 +96,7 @@ class DjangoModelPermissions(BasePermission):
""" """
kwargs = { kwargs = {
'app_label': model_cls._meta.app_label, 'app_label': model_cls._meta.app_label,
'model_name': model_cls._meta.module_name 'model_name': model_cls._meta.module_name
} }
return [perm % kwargs for perm in self.perms_map[method]] return [perm % kwargs for perm in self.perms_map[method]]
......
...@@ -21,8 +21,8 @@ def is_form_media_type(media_type): ...@@ -21,8 +21,8 @@ def is_form_media_type(media_type):
Return True if the media type is a valid form media type. Return True if the media type is a valid form media type.
""" """
base_media_type, params = parse_header(media_type) base_media_type, params = parse_header(media_type)
return base_media_type == 'application/x-www-form-urlencoded' or \ return (base_media_type == 'application/x-www-form-urlencoded' or
base_media_type == 'multipart/form-data' base_media_type == 'multipart/form-data')
class Empty(object): class Empty(object):
...@@ -169,6 +169,15 @@ class Request(object): ...@@ -169,6 +169,15 @@ class Request(object):
self._user, self._auth = self._authenticate() self._user, self._auth = self._authenticate()
return self._user return self._user
@user.setter
def user(self, value):
"""
Sets the user on the current request. This is necessary to maintain
compatilbility with django.contrib.auth where the user proprety is
set in the login and logout functions.
"""
self._user = value
@property @property
def auth(self): def auth(self):
""" """
...@@ -179,6 +188,14 @@ class Request(object): ...@@ -179,6 +188,14 @@ class Request(object):
self._user, self._auth = self._authenticate() self._user, self._auth = self._authenticate()
return self._auth return self._auth
@auth.setter
def auth(self, value):
"""
Sets any non-user authentication information associated with the
request, such as an authentication token.
"""
self._auth = value
def _load_data_and_files(self): def _load_data_and_files(self):
""" """
Parses the request content into self.DATA and self.FILES. Parses the request content into self.DATA and self.FILES.
......
...@@ -9,18 +9,23 @@ class Response(SimpleTemplateResponse): ...@@ -9,18 +9,23 @@ class Response(SimpleTemplateResponse):
""" """
def __init__(self, data=None, status=200, def __init__(self, data=None, status=200,
template_name=None, headers=None): template_name=None, headers=None,
exception=False):
""" """
Alters the init arguments slightly. Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'. For example, drop 'template_name', and instead use 'data'.
Setting 'renderer' and 'media_type' will typically be defered, Setting 'renderer' and 'media_type' will typically be deferred,
For example being set automatically by the `APIView`. For example being set automatically by the `APIView`.
""" """
super(Response, self).__init__(None, status=status) super(Response, self).__init__(None, status=status)
self.data = data self.data = data
self.headers = headers and headers[:] or []
self.template_name = template_name self.template_name = template_name
self.exception = exception
if headers:
for name,value in headers.iteritems():
self[name] = value
@property @property
def rendered_content(self): def rendered_content(self):
...@@ -45,3 +50,13 @@ class Response(SimpleTemplateResponse): ...@@ -45,3 +50,13 @@ class Response(SimpleTemplateResponse):
# TODO: Deprecate and use a template tag instead # TODO: Deprecate and use a template tag instead
# TODO: Status code text for RFC 6585 status codes # TODO: Status code text for RFC 6585 status codes
return STATUS_CODE_TEXT.get(self.status_code, '') return STATUS_CODE_TEXT.get(self.status_code, '')
def __getstate__(self):
"""
Remove attributes from the response that shouldn't be cached
"""
state = super(Response, self).__getstate__()
for key in ('accepted_renderer', 'renderer_context', 'data'):
if key in state:
del state[key]
return state
...@@ -5,13 +5,15 @@ from django.core.urlresolvers import reverse as django_reverse ...@@ -5,13 +5,15 @@ from django.core.urlresolvers import reverse as django_reverse
from django.utils.functional import lazy 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 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. and returns a fully qualified URL, using the request to get the base URL.
""" """
request = kwargs.pop('request', None) if format is not None:
url = django_reverse(viewname, *args, **kwargs) kwargs = kwargs or {}
kwargs['format'] = format
url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
if request: if request:
return request.build_absolute_uri(url) return request.build_absolute_uri(url)
return url return url
......
...@@ -8,6 +8,9 @@ Useful tool to run the test suite for rest_framework and generate a coverage rep ...@@ -8,6 +8,9 @@ Useful tool to run the test suite for rest_framework and generate a coverage rep
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py # http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os import os
import sys import sys
# fix sys path so we don't need to setup PYTHONPATH
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from coverage import coverage from coverage import coverage
...@@ -32,10 +35,10 @@ def main(): ...@@ -32,10 +35,10 @@ def main():
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.', 'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
DeprecationWarning DeprecationWarning
) )
failures = TestRunner(['rest_framework']) failures = TestRunner(['tests'])
else: else:
test_runner = TestRunner() test_runner = TestRunner()
failures = test_runner.run_tests(['rest_framework']) failures = test_runner.run_tests(['tests'])
cov.stop() cov.stop()
# Discover the list of all modules that we should test coverage for # Discover the list of all modules that we should test coverage for
...@@ -55,6 +58,12 @@ def main(): ...@@ -55,6 +58,12 @@ def main():
if 'compat.py' in files: if 'compat.py' in files:
files.remove('compat.py') files.remove('compat.py')
# Same applies to template tags module.
# This module has to include branching on Django versions,
# so it's never possible for it to have full coverage.
if 'rest_framework.py' in files:
files.remove('rest_framework.py')
cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')]) cov_files.extend([os.path.join(path, file) for file in files if file.endswith('.py')])
cov.report(cov_files) cov.report(cov_files)
......
...@@ -5,6 +5,9 @@ ...@@ -5,6 +5,9 @@
# http://code.djangoproject.com/svn/django/trunk/tests/runtests.py # http://code.djangoproject.com/svn/django/trunk/tests/runtests.py
import os import os
import sys import sys
# fix sys path so we don't need to setup PYTHONPATH
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings' os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
from django.conf import settings from django.conf import settings
...@@ -32,7 +35,7 @@ def main(): ...@@ -32,7 +35,7 @@ def main():
else: else:
print usage() print usage()
sys.exit(1) sys.exit(1)
failures = test_runner.run_tests(['rest_framework' + test_case]) failures = test_runner.run_tests(['tests' + test_case])
sys.exit(failures) sys.exit(failures)
......
...@@ -21,6 +21,12 @@ DATABASES = { ...@@ -21,6 +21,12 @@ DATABASES = {
} }
} }
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}
# Local time zone for this installation. Choices can be found here: # Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems. # although not all choices may be available on all operating systems.
...@@ -91,6 +97,7 @@ INSTALLED_APPS = ( ...@@ -91,6 +97,7 @@ INSTALLED_APPS = (
# 'django.contrib.admindocs', # 'django.contrib.admindocs',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'rest_framework.tests'
) )
STATIC_URL = '/static/' STATIC_URL = '/static/'
...@@ -100,13 +107,6 @@ import django ...@@ -100,13 +107,6 @@ import django
if django.VERSION < (1, 3): if django.VERSION < (1, 3):
INSTALLED_APPS += ('staticfiles',) INSTALLED_APPS += ('staticfiles',)
# OAuth support is optional, so we only test oauth if it's installed.
try:
import oauth_provider
except ImportError:
pass
else:
INSTALLED_APPS += ('oauth_provider',)
# If we're running on the Jenkins server we want to archive the coverage reports as XML. # If we're running on the Jenkins server we want to archive the coverage reports as XML.
import os import os
......
""" """
Blank URLConf just to keep runtests.py happy. Blank URLConf just to keep runtests.py happy.
""" """
from django.conf.urls.defaults import * from rest_framework.compat import patterns
urlpatterns = patterns('', urlpatterns = patterns('',
) )
...@@ -37,11 +37,14 @@ DEFAULTS = { ...@@ -37,11 +37,14 @@ DEFAULTS = {
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication' 'rest_framework.authentication.BasicAuthentication'
), ),
'DEFAULT_PERMISSION_CLASSES': (), 'DEFAULT_PERMISSION_CLASSES': (
'DEFAULT_THROTTLE_CLASSES': (), 'rest_framework.permissions.AllowAny',
),
'DEFAULT_THROTTLE_CLASSES': (
),
'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'DEFAULT_CONTENT_NEGOTIATION_CLASS':
'rest_framework.negotiation.DefaultContentNegotiation', 'rest_framework.negotiation.DefaultContentNegotiation',
'DEFAULT_MODEL_SERIALIZER_CLASS': 'DEFAULT_MODEL_SERIALIZER_CLASS':
'rest_framework.serializers.ModelSerializer', 'rest_framework.serializers.ModelSerializer',
'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'DEFAULT_PAGINATION_SERIALIZER_CLASS':
...@@ -51,18 +54,26 @@ DEFAULTS = { ...@@ -51,18 +54,26 @@ DEFAULTS = {
'user': None, 'user': None,
'anon': None, 'anon': None,
}, },
# Pagination
'PAGINATE_BY': None, 'PAGINATE_BY': None,
'PAGINATE_BY_PARAM': None,
# Filtering
'FILTER_BACKEND': None,
# Authentication
'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser',
'UNAUTHENTICATED_TOKEN': None, 'UNAUTHENTICATED_TOKEN': None,
# Browser enhancements
'FORM_METHOD_OVERRIDE': '_method', 'FORM_METHOD_OVERRIDE': '_method',
'FORM_CONTENT_OVERRIDE': '_content', 'FORM_CONTENT_OVERRIDE': '_content',
'FORM_CONTENTTYPE_OVERRIDE': '_content_type', 'FORM_CONTENTTYPE_OVERRIDE': '_content_type',
'URL_ACCEPT_OVERRIDE': 'accept', 'URL_ACCEPT_OVERRIDE': 'accept',
'URL_FORMAT_OVERRIDE': 'format', 'URL_FORMAT_OVERRIDE': 'format',
'FORMAT_SUFFIX_KWARG': 'format' 'FORMAT_SUFFIX_KWARG': 'format',
} }
...@@ -76,6 +87,7 @@ IMPORT_STRINGS = ( ...@@ -76,6 +87,7 @@ IMPORT_STRINGS = (
'DEFAULT_CONTENT_NEGOTIATION_CLASS', 'DEFAULT_CONTENT_NEGOTIATION_CLASS',
'DEFAULT_MODEL_SERIALIZER_CLASS', 'DEFAULT_MODEL_SERIALIZER_CLASS',
'DEFAULT_PAGINATION_SERIALIZER_CLASS', 'DEFAULT_PAGINATION_SERIALIZER_CLASS',
'FILTER_BACKEND',
'UNAUTHENTICATED_USER', 'UNAUTHENTICATED_USER',
'UNAUTHENTICATED_TOKEN', 'UNAUTHENTICATED_TOKEN',
) )
...@@ -103,8 +115,8 @@ def import_from_string(val, setting_name): ...@@ -103,8 +115,8 @@ def import_from_string(val, setting_name):
module_path, class_name = '.'.join(parts[:-1]), parts[-1] module_path, class_name = '.'.join(parts[:-1]), parts[-1]
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
return getattr(module, class_name) return getattr(module, class_name)
except: except ImportError as e:
msg = "Could not import '%s' for API setting '%s'" % (val, setting_name) msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e)
raise ImportError(msg) raise ImportError(msg)
...@@ -139,8 +151,15 @@ class APISettings(object): ...@@ -139,8 +151,15 @@ class APISettings(object):
if val and attr in self.import_strings: if val and attr in self.import_strings:
val = perform_import(val, attr) val = perform_import(val, attr)
self.validate_setting(attr, val)
# Cache the result # Cache the result
setattr(self, attr, val) setattr(self, attr, val)
return val return val
def validate_setting(self, attr, val):
if attr == 'FILTER_BACKEND' and val is not None:
# Make sure we can initialize the class
val()
api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
...@@ -32,6 +32,17 @@ h2, h3 { ...@@ -32,6 +32,17 @@ h2, h3 {
margin-right: 1em; margin-right: 1em;
} }
ul.breadcrumb {
margin: 58px 0 0 0;
}
form select, form input, form textarea {
width: 90%;
}
form select[multiple] {
height: 150px;
}
/* To allow tooltips to work on disabled elements */ /* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield { .disabled-tooltip-shield {
position: absolute; position: absolute;
...@@ -55,6 +66,7 @@ pre { ...@@ -55,6 +66,7 @@ pre {
.page-header { .page-header {
border-bottom: none; border-bottom: none;
padding-bottom: 0px; padding-bottom: 0px;
margin-bottom: 20px;
} }
...@@ -65,7 +77,7 @@ html{ ...@@ -65,7 +77,7 @@ html{
background: none; background: none;
} }
body, .navbar .navbar-inner .container-fluid{ body, .navbar .navbar-inner .container-fluid {
max-width: 1150px; max-width: 1150px;
margin: 0 auto; margin: 0 auto;
} }
...@@ -76,13 +88,14 @@ body{ ...@@ -76,13 +88,14 @@ body{
} }
#content{ #content{
margin: 40px 0 0 0; margin: 0;
} }
/* custom navigation styles */ /* custom navigation styles */
.wrapper .navbar{ .wrapper .navbar{
width:100%; width: 100%;
position: absolute; position: absolute;
left:0; left: 0;
top: 0;
} }
.navbar .navbar-inner{ .navbar .navbar-inner{
......
...@@ -49,4 +49,4 @@ HTTP_502_BAD_GATEWAY = 502 ...@@ -49,4 +49,4 @@ HTTP_502_BAD_GATEWAY = 502
HTTP_503_SERVICE_UNAVAILABLE = 503 HTTP_503_SERVICE_UNAVAILABLE = 503
HTTP_504_GATEWAY_TIMEOUT = 504 HTTP_504_GATEWAY_TIMEOUT = 504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505 HTTP_505_HTTP_VERSION_NOT_SUPPORTED = 505
HTTP_511_NETWORD_AUTHENTICATION_REQUIRED = 511 HTTP_511_NETWORK_AUTHENTICATION_REQUIRED = 511
{% load url from future %} {% load url from future %}
{% load rest_framework %} {% load rest_framework %}
{% load static %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
...@@ -14,10 +13,10 @@ ...@@ -14,10 +13,10 @@
<title>{% block title %}Django REST framework{% endblock %}</title> <title>{% block title %}Django REST framework{% endblock %}</title>
{% block style %} {% block style %}
<link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/> <link rel="stylesheet" type="text/css" href="{% static "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="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/prettify.css'/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}
...@@ -109,11 +108,11 @@ ...@@ -109,11 +108,11 @@
<div class="content-main"> <div class="content-main">
<div class="page-header"><h1>{{ name }}</h1></div> <div class="page-header"><h1>{{ name }}</h1></div>
<p class="resource-description">{{ description }}</p> {{ description }}
<div class="request-info"> <div class="request-info">
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
<div> </div>
<div class="response-info"> <div class="response-info">
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %} <pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|urlize_quoted_links }}</span> {% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|urlize_quoted_links }}</span>
...@@ -131,12 +130,12 @@ ...@@ -131,12 +130,12 @@
{% csrf_token %} {% csrf_token %}
{{ post_form.non_field_errors }} {{ post_form.non_field_errors }}
{% for field in post_form %} {% for field in post_form %}
<div class="control-group {% if field.errors %}error{% endif %}"> <div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
{{ field.label_tag|add_class:"control-label" }} {{ field.label_tag|add_class:"control-label" }}
<div class="controls"> <div class="controls">
{{ field|add_class:"input-xlarge" }} {{ field }}
<span class="help-inline">{{ field.help_text }}</span> <span class="help-inline">{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }} <!--{{ field.errors|add_class:"help-block" }}-->
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
...@@ -156,12 +155,12 @@ ...@@ -156,12 +155,12 @@
{% csrf_token %} {% csrf_token %}
{{ put_form.non_field_errors }} {{ put_form.non_field_errors }}
{% for field in put_form %} {% for field in put_form %}
<div class="control-group {% if field.errors %}error{% endif %}"> <div class="control-group"> <!--{% if field.errors %}error{% endif %}-->
{{ field.label_tag|add_class:"control-label" }} {{ field.label_tag|add_class:"control-label" }}
<div class="controls"> <div class="controls">
{{ field|add_class:"input-xlarge" }} {{ field }}
<span class='help-inline'>{{ field.help_text }}</span> <span class='help-inline'>{{ field.help_text }}</span>
{{ field.errors|add_class:"help-block" }} <!--{{ field.errors|add_class:"help-block" }}-->
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
...@@ -195,10 +194,10 @@ ...@@ -195,10 +194,10 @@
{% endblock %} {% endblock %}
{% block script %} {% block script %}
<script src="{% get_static_prefix %}rest_framework/js/jquery-1.8.1-min.js"></script> <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
<script src="{% get_static_prefix %}rest_framework/js/bootstrap.min.js"></script> <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
<script src="{% get_static_prefix %}rest_framework/js/prettify-min.js"></script> <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
<script src="{% get_static_prefix %}rest_framework/js/default.js"></script> <script src="{% static "rest_framework/js/default.js" %}"></script>
{% endblock %} {% endblock %}
</body> </body>
</html> </html>
{% load url from future %} {% load url from future %}
{% load static %} {% load rest_framework %}
<html> <html>
<head> <head>
<link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/style.css'/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
</head> </head>
<body class="login"> <body class="container">
<div id="container"> <div class="container-fluid" style="margin-top: 30px">
<div class="row-fluid">
<div id="header">
<div id="branding"> <div class="well" style="width: 320px; margin-left: auto; margin-right: auto">
<h1 id="site-name">Django REST framework</h1> <div class="row-fluid">
<div>
<h3 style="margin: 0 0 20px;">Django REST framework</h3>
</div> </div>
</div> </div><!-- /row fluid -->
<div id="content" class="colM"> <div class="row-fluid">
<div id="content-main"> <div>
<form method="post" action="{% url 'rest_framework:login' %}" id="login-form"> <form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
{% csrf_token %} {% csrf_token %}
<div class="form-row"> <div id="div_id_username" class="clearfix control-group">
<label for="id_username">Username:</label> {{ form.username }} <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>
<div class="form-row"> <div id="div_id_password" class="clearfix control-group">
<label for="id_password">Password:</label> {{ form.password }} <div class="controls" style="height: 30px">
<input type="hidden" name="next" value="{{ next }}" /> <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>
<div class="form-row"> <input type="hidden" name="next" value="{{ next }}" />
<label>&nbsp;</label><input type="submit" value="Log in"> <div class="form-actions-no-box">
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
</div> </div>
</form> </form>
<script type="text/javascript">
document.getElementById('id_username').focus()
</script>
</div> </div>
<br class="clear"> </div><!-- /row fluid -->
</div> </div><!--/span-->
<div id="footer"></div> </div><!-- /.row-fluid -->
</div>
</div> </div>
</body> </body>
......
...@@ -11,6 +11,101 @@ import string ...@@ -11,6 +11,101 @@ import string
register = template.Library() register = template.Library()
# Note we don't use 'load staticfiles', because we need a 1.3 compatible
# version, so instead we include the `static` template tag ourselves.
# When 1.3 becomes unsupported by REST framework, we can instead start to
# use the {% load staticfiles %} tag, remove the following code,
# and add a dependancy that `django.contrib.staticfiles` must be installed.
# Note: We can't put this into the `compat` module because the compat import
# from rest_framework.compat import ...
# conflicts with this rest_framework template tag module.
try: # Django 1.5+
from django.contrib.staticfiles.templatetags.staticfiles import StaticFilesNode
@register.tag('static')
def do_static(parser, token):
return StaticFilesNode.handle_token(parser, token)
except:
try: # Django 1.4
from django.contrib.staticfiles.storage import staticfiles_storage
@register.simple_tag
def static(path):
"""
A template tag that returns the URL to a file
using staticfiles' storage backend
"""
return staticfiles_storage.url(path)
except: # Django 1.3
from urlparse import urljoin
from django import template
from django.templatetags.static import PrefixNode
class StaticNode(template.Node):
def __init__(self, varname=None, path=None):
if path is None:
raise template.TemplateSyntaxError(
"Static template nodes must be given a path to return.")
self.path = path
self.varname = varname
def url(self, context):
path = self.path.resolve(context)
return self.handle_simple(path)
def render(self, context):
url = self.url(context)
if self.varname is None:
return url
context[self.varname] = url
return ''
@classmethod
def handle_simple(cls, path):
return urljoin(PrefixNode.handle_simple("STATIC_URL"), path)
@classmethod
def handle_token(cls, parser, token):
"""
Class method to parse prefix node and return a Node.
"""
bits = token.split_contents()
if len(bits) < 2:
raise template.TemplateSyntaxError(
"'%s' takes at least one argument (path to file)" % bits[0])
path = parser.compile_filter(bits[1])
if len(bits) >= 2 and bits[-2] == 'as':
varname = bits[3]
else:
varname = None
return cls(varname, path)
@register.tag('static')
def do_static_13(parser, token):
return StaticNode.handle_token(parser, token)
def replace_query_param(url, key, val):
"""
Given a URL and a key/val pair, set or replace an item in the query
parameters of the URL, and return the new URL.
"""
(scheme, netloc, path, query, fragment) = urlsplit(url)
query_dict = QueryDict(query).copy()
query_dict[key] = val
query = query_dict.urlencode()
return urlunsplit((scheme, netloc, path, query, fragment))
# Regex for adding classes to html snippets # Regex for adding classes to html snippets
class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])') class_re = re.compile(r'(?<=class=["\'])(.*)(?=["\'])')
...@@ -31,19 +126,6 @@ hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '| ...@@ -31,19 +126,6 @@ hard_coded_bullets_re = re.compile(r'((?:<p>(?:%s).*?[a-zA-Z].*?</p>\s*)+)' % '|
trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z') trailing_empty_content_re = re.compile(r'(?:<p>(?:&nbsp;|\s|<br \/>)*?</p>\s*)+\Z')
# Helper function for 'add_query_param'
def replace_query_param(url, key, val):
"""
Given a URL and a key/val pair, set or replace an item in the query
parameters of the URL, and return the new URL.
"""
(scheme, netloc, path, query, fragment) = urlsplit(url)
query_dict = QueryDict(query).copy()
query_dict[key] = val
query = query_dict.urlencode()
return urlunsplit((scheme, netloc, path, query, fragment))
# And the template tags themselves... # And the template tags themselves...
@register.simple_tag @register.simple_tag
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment