This commit is contained in:
Marko Tibold 2012-12-06 23:47:47 +01:00
commit c65f22e0e4
24 changed files with 216 additions and 100 deletions

View File

@ -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

View File

@ -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.

View File

@ -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'

View File

@ -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.

View File

@ -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.
@ -166,4 +173,11 @@ To contact the author directly:
[jonlil]: https://github.com/jonlil
[irrelative]: https://github.com/irrelative
[dbachrach]: https://github.com/dbachrach
[maspwr]: https://github.com/maspwr
[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

View File

@ -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

View File

@ -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].

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.
---

View File

@ -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

View File

@ -1,3 +1,3 @@
__version__ = '2.1.4'
__version__ = '2.1.6'
VERSION = __version__ # synonym

View File

@ -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()

View File

@ -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
@ -55,7 +57,7 @@ class Field(object):
self.root = parent.root or parent
self.context = self.root.context
if self.root.partial:
self.required = False
self.required = False
def field_from_native(self, data, files, field_name, into):
"""
@ -223,7 +225,7 @@ class ModelField(WritableField):
getattr(self.model_field, 'min_length', None))
self.max_length = kwargs.pop('max_length',
getattr(self.model_field, 'max_length', None))
super(ModelField, self).__init__(*args, **kwargs)
if self.min_length is not 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."),

View File

@ -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

View File

@ -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):

View File

@ -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):
"""

View File

@ -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)):

View File

@ -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."""

View File

@ -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)

View File

@ -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())

View File

@ -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):