mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-01-23 15:54:16 +03:00
Merge branch 'master' of https://github.com/tomchristie/django-rest-framework into #431
This commit is contained in:
commit
c65f22e0e4
12
README.md
12
README.md
|
@ -58,6 +58,18 @@ To run the tests.
|
|||
|
||||
# Changelog
|
||||
|
||||
## 2.1.6
|
||||
|
||||
**Date**: 23rd Nov 2012
|
||||
|
||||
* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
|
||||
|
||||
## 2.1.5
|
||||
|
||||
**Date**: 23rd Nov 2012
|
||||
|
||||
* Bugfix: Fix DjangoModelPermissions.
|
||||
|
||||
## 2.1.4
|
||||
|
||||
**Date**: 22nd Nov 2012
|
||||
|
|
|
@ -116,7 +116,7 @@ When using `TokenAuthentication`, you may want to provide a mechanism for client
|
|||
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.obtain_auth_token')
|
||||
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.
|
||||
|
|
|
@ -53,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.
|
||||
|
||||
@api_view('GET')
|
||||
@permission_classes(IsAuthenticated)
|
||||
@permission_classes((IsAuthenticated, ))
|
||||
def example_view(request, format=None):
|
||||
content = {
|
||||
'status': 'request was permitted'
|
||||
|
|
|
@ -19,6 +19,10 @@ Using the `APIView` class is pretty much the same as using a regular `View` clas
|
|||
|
||||
For example:
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import authentication, permissions
|
||||
|
||||
class ListUsers(APIView):
|
||||
"""
|
||||
View to list all users in the system.
|
||||
|
|
|
@ -66,6 +66,13 @@ The following people have helped make REST framework great.
|
|||
* 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]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -167,3 +174,10 @@ To contact the author directly:
|
|||
[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
|
||||
|
|
|
@ -4,6 +4,18 @@
|
|||
>
|
||||
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
||||
|
||||
## 2.1.6
|
||||
|
||||
**Date**: 23rd Nov 2012
|
||||
|
||||
* Bugfix: Unfix DjangoModelPermissions. (I am a doofus.)
|
||||
|
||||
## 2.1.5
|
||||
|
||||
**Date**: 23rd Nov 2012
|
||||
|
||||
* Bugfix: Fix DjangoModelPermissions.
|
||||
|
||||
## 2.1.4
|
||||
|
||||
**Date**: 22nd Nov 2012
|
||||
|
|
|
@ -14,7 +14,7 @@ The tutorial is fairly in-depth, so you should probably get a cookie and a cup o
|
|||
|
||||
## Setting up a new environment
|
||||
|
||||
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is keep nicely isolated from any other projects we're working on.
|
||||
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is kept nicely isolated from any other projects we're working on.
|
||||
|
||||
:::bash
|
||||
mkdir ~/env
|
||||
|
@ -39,7 +39,6 @@ To get started, let's create a new project to work with.
|
|||
cd tutorial
|
||||
|
||||
Once that's done we can create an app that we'll use to create a simple Web API.
|
||||
We're going to create a project that
|
||||
|
||||
python manage.py startapp snippets
|
||||
|
||||
|
@ -64,7 +63,7 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I
|
|||
'snippets'
|
||||
)
|
||||
|
||||
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet views.
|
||||
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^', include('snippets.urls')),
|
||||
|
@ -105,7 +104,7 @@ Don't forget to sync the database for the first time.
|
|||
|
||||
## Creating a Serializer class
|
||||
|
||||
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similarly to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
||||
The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
|
||||
|
||||
from django.forms import widgets
|
||||
from rest_framework import serializers
|
||||
|
@ -146,7 +145,7 @@ We can actually also save ourselves some time by using the `ModelSerializer` cla
|
|||
|
||||
## Working with Serializers
|
||||
|
||||
Before we go any further we'll familiarise ourselves with using our new Serializer class. Let's drop into the Django shell.
|
||||
Before we go any further we'll familiarize ourselves with using our new Serializer class. Let's drop into the Django shell.
|
||||
|
||||
python manage.py shell
|
||||
|
||||
|
@ -166,7 +165,7 @@ We've now got a few snippet instances to play with. Let's take a look at serial
|
|||
serializer.data
|
||||
# {'pk': 1, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'}
|
||||
|
||||
At this point we've translated the model instance into python native datatypes. To finalise the serialization process we render the data into `json`.
|
||||
At this point we've translated the model instance into python native datatypes. To finalize the serialization process we render the data into `json`.
|
||||
|
||||
content = JSONRenderer().render(serializer.data)
|
||||
content
|
||||
|
@ -292,7 +291,7 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file:
|
|||
url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail')
|
||||
)
|
||||
|
||||
It's worth noting that there's a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
|
||||
It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
|
||||
|
||||
## Testing our first attempt at a Web API
|
||||
|
||||
|
@ -304,7 +303,7 @@ It's worth noting that there's a couple of edge cases we're not dealing with pro
|
|||
|
||||
We're doing okay so far, we've got a serialization API that feels pretty similar to Django's Forms API, and some regular Django views.
|
||||
|
||||
Our API views don't do anything particularly special at the moment, beyond serve `json` responses, and there's some error handling edge cases we'd still like to clean up, but it's a functioning Web API.
|
||||
Our API views don't do anything particularly special at the moment, beyond serving `json` responses, and there are some error handling edge cases we'd still like to clean up, but it's a functioning Web API.
|
||||
|
||||
We'll see how we can start to improve things in [part 2 of the tutorial][tut-2].
|
||||
|
||||
|
|
|
@ -41,8 +41,8 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
|
||||
|
||||
@api_view(['GET', 'POST'])
|
||||
|
@ -66,6 +66,8 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On
|
|||
|
||||
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'])
|
||||
def snippet_detail(request, pk):
|
||||
"""
|
||||
|
@ -92,7 +94,7 @@ Our instance view is an improvement over the previous example. It's a little mo
|
|||
snippet.delete()
|
||||
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.
|
||||
|
||||
|
@ -113,7 +115,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
|
|||
from django.conf.urls import patterns, url
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
|
||||
urlpatterns = patterns('snippet.views',
|
||||
urlpatterns = patterns('snippets.views',
|
||||
url(r'^snippets/$', 'snippet_list'),
|
||||
url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail')
|
||||
)
|
||||
|
@ -128,7 +130,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**
|
||||
|
||||
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver]."
|
||||
Now go and open the API in a web browser, by visiting [http://127.0.0.1:8000/snippets/][devserver].
|
||||
|
||||
### Browsability
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ 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.
|
||||
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
from django.http import Http404
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
|
@ -66,7 +66,7 @@ We'll also need to refactor our URLconf slightly now we're using class based vie
|
|||
|
||||
from django.conf.urls import patterns, url
|
||||
from rest_framework.urlpatterns import format_suffix_patterns
|
||||
from snippetpost import views
|
||||
from snippets import views
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^snippets/$', views.SnippetList.as_view()),
|
||||
|
@ -85,8 +85,8 @@ 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.
|
||||
|
||||
from snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
from rest_framework import mixins
|
||||
from rest_framework import generics
|
||||
|
||||
|
@ -102,7 +102,7 @@ Let's take a look at how we can compose our views by using the mixin classes.
|
|||
def post(self, 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 `MultipleObjectAPIView`, 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.
|
||||
|
||||
|
@ -128,8 +128,8 @@ Pretty similar. This time we're using the `SingleObjectBaseView` class to provi
|
|||
|
||||
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 snippet.models import Snippet
|
||||
from snippet.serializers import SnippetSerializer
|
||||
from snippets.models import Snippet
|
||||
from snippets.serializers import SnippetSerializer
|
||||
from rest_framework import generics
|
||||
|
||||
|
||||
|
@ -142,7 +142,7 @@ Using the mixin classes we've rewritten the views to use slightly less code than
|
|||
model = Snippet
|
||||
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 deal with authentication and permissions for our API.
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ Now that we've got some users to work with, we'd better add representations of t
|
|||
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've needed to add an explicit field for it.
|
||||
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.
|
||||
|
||||
|
@ -92,9 +92,7 @@ On **both** the `SnippetList` and `SnippetDetail` view classes, add the followin
|
|||
|
||||
## 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:
|
||||
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')
|
||||
|
||||
|
@ -108,7 +106,7 @@ The field we've added is the untyped `Field` class, in contrast to the other typ
|
|||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ Notice that we're using REST framework's `reverse` function in order to return f
|
|||
|
||||
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 style 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.
|
||||
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.
|
||||
|
||||
|
@ -151,7 +151,7 @@ We could also customize the pagination style if we needed too, but in this case
|
|||
|
||||
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 hightlighted code HTML representations.
|
||||
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.
|
||||
|
||||
|
|
|
@ -137,7 +137,7 @@ We'd also like to set a few global settings. We'd like to turn on pagination, a
|
|||
'PAGINATE_BY': 10
|
||||
}
|
||||
|
||||
Okay, that's us done.
|
||||
Okay, we're done.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
markdown>=2.1.0
|
||||
PyYAML>=3.10
|
||||
-e git+https://github.com/alex/django-filter.git@0e4b3d703b31574922ab86fc78a86164aad0c1d0#egg=django-filter
|
||||
django-filter>=0.5.4
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = '2.1.4'
|
||||
__version__ = '2.1.6'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
|
|
@ -18,7 +18,7 @@ class ObtainAuthToken(APIView):
|
|||
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)
|
||||
return Response(serializer.errors, status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
obtain_auth_token = ObtainAuthToken.as_view()
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.core import validators
|
|||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.urlresolvers import resolve, get_script_prefix
|
||||
from django.conf import settings
|
||||
from django import forms
|
||||
from django.forms import widgets
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.encoding import is_protected_type, smart_unicode
|
||||
|
@ -35,6 +36,7 @@ class Field(object):
|
|||
empty = ''
|
||||
type_name = None
|
||||
_use_files = None
|
||||
form_field_class = forms.CharField
|
||||
|
||||
def __init__(self, source=None):
|
||||
self.parent = None
|
||||
|
@ -394,6 +396,7 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
Represents a to-one relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
# TODO: Remove these field hacks...
|
||||
def prepare_value(self, obj):
|
||||
|
@ -440,6 +443,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
|
|||
Represents a to-many relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj.pk)
|
||||
|
@ -483,6 +487,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
|
|||
|
||||
class SlugRelatedField(RelatedField):
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.slug_field = kwargs.pop('slug_field', None)
|
||||
|
@ -504,7 +509,7 @@ class SlugRelatedField(RelatedField):
|
|||
|
||||
|
||||
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
|
||||
pass
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
|
||||
### Hyperlinked relationships
|
||||
|
@ -517,6 +522,7 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
slug_field = 'slug'
|
||||
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
|
||||
default_read_only = False
|
||||
form_field_class = forms.ChoiceField
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
|
@ -616,7 +622,7 @@ class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
|
|||
"""
|
||||
Represents a to-many relationship, using hyperlinking.
|
||||
"""
|
||||
pass
|
||||
form_field_class = forms.MultipleChoiceField
|
||||
|
||||
|
||||
class HyperlinkedIdentityField(Field):
|
||||
|
@ -674,6 +680,7 @@ class HyperlinkedIdentityField(Field):
|
|||
|
||||
class BooleanField(WritableField):
|
||||
type_name = 'BooleanField'
|
||||
form_field_class = forms.BooleanField
|
||||
widget = widgets.CheckboxInput
|
||||
default_error_messages = {
|
||||
'invalid': _(u"'%s' value must be either True or False."),
|
||||
|
@ -686,15 +693,16 @@ class BooleanField(WritableField):
|
|||
default = False
|
||||
|
||||
def from_native(self, value):
|
||||
if value in ('t', 'True', '1'):
|
||||
if value in ('true', 't', 'True', '1'):
|
||||
return True
|
||||
if value in ('f', 'False', '0'):
|
||||
if value in ('false', 'f', 'False', '0'):
|
||||
return False
|
||||
return bool(value)
|
||||
|
||||
|
||||
class CharField(WritableField):
|
||||
type_name = 'CharField'
|
||||
form_field_class = forms.CharField
|
||||
|
||||
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
|
||||
self.max_length, self.min_length = max_length, min_length
|
||||
|
@ -739,6 +747,7 @@ class SlugField(CharField):
|
|||
|
||||
class ChoiceField(WritableField):
|
||||
type_name = 'ChoiceField'
|
||||
form_field_class = forms.ChoiceField
|
||||
widget = widgets.Select
|
||||
default_error_messages = {
|
||||
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
|
||||
|
@ -785,6 +794,7 @@ class ChoiceField(WritableField):
|
|||
|
||||
class EmailField(CharField):
|
||||
type_name = 'EmailField'
|
||||
form_field_class = forms.EmailField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _('Enter a valid e-mail address.'),
|
||||
|
@ -836,6 +846,7 @@ class RegexField(CharField):
|
|||
class DateField(WritableField):
|
||||
type_name = 'DateField'
|
||||
widget = widgets.DateInput
|
||||
form_field_class = forms.DateField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _(u"'%s' value has an invalid date format. It must be "
|
||||
|
@ -874,6 +885,7 @@ class DateField(WritableField):
|
|||
class DateTimeField(WritableField):
|
||||
type_name = 'DateTimeField'
|
||||
widget = widgets.DateTimeInput
|
||||
form_field_class = forms.DateTimeField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _(u"'%s' value has an invalid format. It must be in "
|
||||
|
@ -928,6 +940,7 @@ class DateTimeField(WritableField):
|
|||
|
||||
class IntegerField(WritableField):
|
||||
type_name = 'IntegerField'
|
||||
form_field_class = forms.IntegerField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _('Enter a whole number.'),
|
||||
|
@ -957,6 +970,7 @@ class IntegerField(WritableField):
|
|||
|
||||
class FloatField(WritableField):
|
||||
type_name = 'FloatField'
|
||||
form_field_class = forms.FloatField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid': _("'%s' value must be a float."),
|
||||
|
@ -976,6 +990,7 @@ class FloatField(WritableField):
|
|||
class FileField(WritableField):
|
||||
_use_files = True
|
||||
type_name = 'FileField'
|
||||
form_field_class = forms.FileField
|
||||
widget = widgets.FileInput
|
||||
|
||||
default_error_messages = {
|
||||
|
@ -1018,6 +1033,7 @@ class FileField(WritableField):
|
|||
|
||||
class ImageField(FileField):
|
||||
_use_files = True
|
||||
form_field_class = forms.ImageField
|
||||
|
||||
default_error_messages = {
|
||||
'invalid_image': _("Upload a valid image. The file you uploaded was either not an image or a corrupted image."),
|
||||
|
|
|
@ -2,6 +2,7 @@ from django.http import Http404
|
|||
from rest_framework import exceptions
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
|
||||
from rest_framework.utils.mediatypes import _MediaType
|
||||
|
||||
|
||||
class BaseContentNegotiation(object):
|
||||
|
@ -48,7 +49,8 @@ class DefaultContentNegotiation(BaseContentNegotiation):
|
|||
for media_type in media_type_set:
|
||||
if media_type_matches(renderer.media_type, media_type):
|
||||
# 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 '*/*'
|
||||
# Accepted media type is 'application/json'
|
||||
return renderer, renderer.media_type
|
||||
|
|
|
@ -306,26 +306,6 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
return True
|
||||
|
||||
def serializer_to_form_fields(self, serializer):
|
||||
field_mapping = {
|
||||
serializers.FloatField: forms.FloatField,
|
||||
serializers.IntegerField: forms.IntegerField,
|
||||
serializers.DateTimeField: forms.DateTimeField,
|
||||
serializers.DateField: forms.DateField,
|
||||
serializers.EmailField: forms.EmailField,
|
||||
serializers.RegexField: forms.RegexField,
|
||||
serializers.CharField: forms.CharField,
|
||||
serializers.ChoiceField: forms.ChoiceField,
|
||||
serializers.BooleanField: forms.BooleanField,
|
||||
serializers.PrimaryKeyRelatedField: forms.ChoiceField,
|
||||
serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
|
||||
serializers.SlugRelatedField: forms.ChoiceField,
|
||||
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
|
||||
serializers.HyperlinkedRelatedField: forms.ChoiceField,
|
||||
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField,
|
||||
serializers.FileField: forms.FileField,
|
||||
serializers.ImageField: forms.ImageField,
|
||||
}
|
||||
|
||||
fields = {}
|
||||
for k, v in serializer.get_fields().items():
|
||||
if getattr(v, 'read_only', True):
|
||||
|
@ -349,13 +329,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
|
||||
kwargs['label'] = k
|
||||
|
||||
try:
|
||||
fields[k] = field_mapping[v.__class__](**kwargs)
|
||||
except KeyError:
|
||||
if getattr(v, 'choices', None) is not None:
|
||||
fields[k] = forms.ChoiceField(**kwargs)
|
||||
else:
|
||||
fields[k] = forms.CharField(**kwargs)
|
||||
fields[k] = v.form_field_class(**kwargs)
|
||||
return fields
|
||||
|
||||
def get_form(self, view, method, request):
|
||||
|
|
|
@ -169,6 +169,15 @@ class Request(object):
|
|||
self._user, self._auth = self._authenticate()
|
||||
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
|
||||
def auth(self):
|
||||
"""
|
||||
|
|
|
@ -60,7 +60,7 @@ def _get_declared_fields(bases, attrs):
|
|||
|
||||
# If this class is subclassing another Serializer, add that Serializer's
|
||||
# fields. Note that we loop over the bases in *reverse*. This is necessary
|
||||
# in order to the correct order of fields.
|
||||
# in order to maintain the correct order of fields.
|
||||
for base in bases[::-1]:
|
||||
if hasattr(base, 'base_fields'):
|
||||
fields = base.base_fields.items() + fields
|
||||
|
@ -94,7 +94,6 @@ class BaseSerializer(Field):
|
|||
def __init__(self, instance=None, data=None, files=None, context=None, partial=False, **kwargs):
|
||||
super(BaseSerializer, self).__init__(**kwargs)
|
||||
self.opts = self._options_class(self.Meta)
|
||||
self.fields = copy.deepcopy(self.base_fields)
|
||||
self.parent = None
|
||||
self.root = None
|
||||
self.partial = partial
|
||||
|
@ -104,7 +103,7 @@ class BaseSerializer(Field):
|
|||
self.init_data = data
|
||||
self.init_files = files
|
||||
self.object = instance
|
||||
self.default_fields = self.get_default_fields()
|
||||
self.fields = self.get_fields()
|
||||
|
||||
self._data = None
|
||||
self._files = None
|
||||
|
@ -140,13 +139,15 @@ class BaseSerializer(Field):
|
|||
ret = SortedDict()
|
||||
|
||||
# Get the explicitly declared fields
|
||||
for key, field in self.fields.items():
|
||||
base_fields = copy.deepcopy(self.base_fields)
|
||||
for key, field in base_fields.items():
|
||||
ret[key] = field
|
||||
# Set up the field
|
||||
field.initialize(parent=self, field_name=key)
|
||||
|
||||
# Add in the default fields
|
||||
for key, val in self.default_fields.items():
|
||||
default_fields = self.get_default_fields()
|
||||
for key, val in default_fields.items():
|
||||
if key not in ret:
|
||||
ret[key] = val
|
||||
|
||||
|
@ -193,8 +194,7 @@ class BaseSerializer(Field):
|
|||
ret = self._dict_class()
|
||||
ret.fields = {}
|
||||
|
||||
fields = self.get_fields()
|
||||
for field_name, field in fields.items():
|
||||
for field_name, field in self.fields.items():
|
||||
key = self.get_field_key(field_name)
|
||||
value = field.field_to_native(obj, field_name)
|
||||
ret[key] = value
|
||||
|
@ -206,9 +206,8 @@ class BaseSerializer(Field):
|
|||
Core of deserialization, together with `restore_object`.
|
||||
Converts a dictionary of data into a dictionary of deserialized fields.
|
||||
"""
|
||||
fields = self.get_fields()
|
||||
reverted_data = {}
|
||||
for field_name, field in fields.items():
|
||||
for field_name, field in self.fields.items():
|
||||
try:
|
||||
field.field_from_native(data, files, field_name, reverted_data)
|
||||
except ValidationError as err:
|
||||
|
@ -220,10 +219,7 @@ class BaseSerializer(Field):
|
|||
"""
|
||||
Run `validate_<fieldname>()` and `validate()` methods on the serializer
|
||||
"""
|
||||
# TODO: refactor this so we're not determining the fields again
|
||||
fields = self.get_fields()
|
||||
|
||||
for field_name, field in fields.items():
|
||||
for field_name, field in self.fields.items():
|
||||
try:
|
||||
validate_method = getattr(self, 'validate_%s' % field_name, None)
|
||||
if validate_method:
|
||||
|
@ -294,10 +290,18 @@ class BaseSerializer(Field):
|
|||
Override default so that we can apply ModelSerializer as a nested
|
||||
field to relationships.
|
||||
"""
|
||||
obj = getattr(obj, self.source or field_name)
|
||||
|
||||
if is_simple_callable(obj):
|
||||
obj = obj()
|
||||
if self.source:
|
||||
value = obj
|
||||
for component in self.source.split('.'):
|
||||
value = getattr(value, component)
|
||||
if is_simple_callable(value):
|
||||
value = value()
|
||||
obj = value
|
||||
else:
|
||||
value = getattr(obj, field_name)
|
||||
if is_simple_callable(value):
|
||||
obj = value()
|
||||
|
||||
# If the object has an "all" method, assume it's a relationship
|
||||
if is_simple_callable(getattr(obj, 'all', None)):
|
||||
|
|
|
@ -167,14 +167,14 @@ class TokenAuthTests(TestCase):
|
|||
client = Client(enforce_csrf_checks=True)
|
||||
response = client.post('/auth-token/login/',
|
||||
json.dumps({'username': self.username, 'password': "badpass"}), 'application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_token_login_json_missing_fields(self):
|
||||
"""Ensure token login view using JSON POST fails if missing fields."""
|
||||
client = Client(enforce_csrf_checks=True)
|
||||
response = client.post('/auth-token/login/',
|
||||
json.dumps({'username': self.username}), 'application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
def test_token_login_form(self):
|
||||
"""Ensure token login view using form POST works."""
|
||||
|
|
|
@ -124,8 +124,21 @@ class ActionItem(RESTFrameworkModel):
|
|||
|
||||
|
||||
# Models for reverse relations
|
||||
class Person(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=10)
|
||||
age = models.IntegerField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'age': self.age,
|
||||
}
|
||||
|
||||
|
||||
class BlogPost(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=100)
|
||||
writer = models.ForeignKey(Person, null=True, blank=True)
|
||||
|
||||
def get_first_comment(self):
|
||||
return self.blogpostcomment_set.all()[0]
|
||||
|
@ -145,18 +158,6 @@ class Photo(RESTFrameworkModel):
|
|||
album = models.ForeignKey(Album)
|
||||
|
||||
|
||||
class Person(RESTFrameworkModel):
|
||||
name = models.CharField(max_length=10)
|
||||
age = models.IntegerField(null=True, blank=True)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return {
|
||||
'name': self.name,
|
||||
'age': self.age,
|
||||
}
|
||||
|
||||
|
||||
# Model for issue #324
|
||||
class BlankFieldModel(RESTFrameworkModel):
|
||||
title = models.CharField(max_length=100, blank=True, null=True)
|
||||
|
|
|
@ -3,6 +3,8 @@ Tests for content parsing, and form-overloaded content parsing.
|
|||
"""
|
||||
from django.conf.urls.defaults import patterns
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.test import TestCase, Client
|
||||
from django.utils import simplejson as json
|
||||
|
||||
|
@ -276,3 +278,29 @@ class TestContentParsingWithAuthentication(TestCase):
|
|||
|
||||
# response = self.csrf_client.post('/', content)
|
||||
# self.assertEqual(status.OK, response.status_code, "POST data is malformed")
|
||||
|
||||
|
||||
class TestUserSetter(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Pass request object through session middleware so session is
|
||||
# available to login and logout functions
|
||||
self.request = Request(factory.get('/'))
|
||||
SessionMiddleware().process_request(self.request)
|
||||
|
||||
User.objects.create_user('ringo', 'starr@thebeatles.com', 'yellow')
|
||||
self.user = authenticate(username='ringo', password='yellow')
|
||||
|
||||
def test_user_can_be_set(self):
|
||||
self.request.user = self.user
|
||||
self.assertEqual(self.request.user, self.user)
|
||||
|
||||
def test_user_can_login(self):
|
||||
login(self.request, self.user)
|
||||
self.assertEqual(self.request.user, self.user)
|
||||
|
||||
def test_user_can_logout(self):
|
||||
self.request.user = self.user
|
||||
self.assertFalse(self.request.user.is_anonymous())
|
||||
logout(self.request)
|
||||
self.assertTrue(self.request.user.is_anonymous())
|
||||
|
|
|
@ -577,6 +577,47 @@ class ManyRelatedTests(TestCase):
|
|||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
class RelatedTraversalTest(TestCase):
|
||||
def test_nested_traversal(self):
|
||||
user = Person.objects.create(name="django")
|
||||
post = BlogPost.objects.create(title="Test blog post", writer=user)
|
||||
post.blogpostcomment_set.create(text="I love this blog post")
|
||||
|
||||
from rest_framework.tests.models import BlogPostComment
|
||||
|
||||
class PersonSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = ("name", "age")
|
||||
|
||||
class BlogPostCommentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = BlogPostComment
|
||||
fields = ("text", "post_owner")
|
||||
|
||||
text = serializers.CharField()
|
||||
post_owner = PersonSerializer(source='blog_post.writer')
|
||||
|
||||
class BlogPostSerializer(serializers.Serializer):
|
||||
title = serializers.CharField()
|
||||
comments = BlogPostCommentSerializer(source='blogpostcomment_set')
|
||||
|
||||
serializer = BlogPostSerializer(instance=post)
|
||||
|
||||
expected = {
|
||||
'title': u'Test blog post',
|
||||
'comments': [{
|
||||
'text': u'I love this blog post',
|
||||
'post_owner': {
|
||||
"name": u"django",
|
||||
"age": None
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
class SerializerMethodFieldTests(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user