mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-22 17:47:04 +03:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
44449fa1f5
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -7,7 +7,7 @@ html/
|
|||
coverage/
|
||||
build/
|
||||
dist/
|
||||
rest_framework.egg-info/
|
||||
*.egg-info/
|
||||
MANIFEST
|
||||
|
||||
!.gitignore
|
||||
|
|
20
README.md
20
README.md
|
@ -57,8 +57,28 @@ To run the tests.
|
|||
|
||||
# Changelog
|
||||
|
||||
## Master
|
||||
|
||||
* Minor field improvements (don't stringify dicts, more robust many-pk fields)
|
||||
|
||||
## 2.0.2
|
||||
|
||||
**Date**: 2nd Nov 2012
|
||||
|
||||
* Fix issues with pk related fields in the browsable API.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
**Date**: 1st Nov 2012
|
||||
|
||||
* Add support for relational fields in the browsable API.
|
||||
* Added SlugRelatedField and ManySlugRelatedField.
|
||||
* If PUT creates an instance return '201 Created', instead of '200 OK'.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
**Date**: 30th Oct 2012
|
||||
|
||||
* Redesign of core components.
|
||||
* Fix **all of the things**.
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ The default authentication policy may be set globally, using the `DEFAULT_AUTHEN
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework.authentication.UserBasicAuthentication',
|
||||
'rest_framework.authentication.BasicAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
)
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
class ExampleView(APIView):
|
||||
authentication_classes = (SessionAuthentication, UserBasicAuthentication)
|
||||
authentication_classes = (SessionAuthentication, BasicAuthentication)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
def get(self, request, format=None):
|
||||
|
@ -51,7 +51,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'])
|
||||
@authentication_classes((SessionAuthentication, UserBasicAuthentication))
|
||||
@authentication_classes((SessionAuthentication, BasicAuthentication))
|
||||
@permissions_classes((IsAuthenticated,))
|
||||
def example_view(request, format=None):
|
||||
content = {
|
||||
|
|
|
@ -235,44 +235,48 @@ Then an example output format for a Bookmark instance would be:
|
|||
'url': u'https://www.djangoproject.com/'
|
||||
}
|
||||
|
||||
## PrimaryKeyRelatedField
|
||||
## PrimaryKeyRelatedField / ManyPrimaryKeyRelatedField
|
||||
|
||||
This field can be applied to any "to-one" relationship, such as a `ForeignKey` field.
|
||||
`PrimaryKeyRelatedField` and `ManyPrimaryKeyRelatedField` will represent the target of the relationship using it's primary key.
|
||||
|
||||
`PrimaryKeyRelatedField` will represent the target of the field using it's primary key.
|
||||
Be default these fields read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
Be default, `PrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
|
||||
**Arguments**:
|
||||
|
||||
## ManyPrimaryKeyRelatedField
|
||||
* `queryset` - All relational fields must either set a queryset, or set `read_only=True`
|
||||
|
||||
This field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship.
|
||||
## SlugRelatedField / ManySlugRelatedField
|
||||
|
||||
`PrimaryKeyRelatedField` will represent the targets of the field using their primary key.
|
||||
`SlugRelatedField` and `ManySlugRelatedField` will represent the target of the relationship using a unique slug.
|
||||
|
||||
Be default, `ManyPrimaryKeyRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
|
||||
Be default these fields read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
## HyperlinkedRelatedField
|
||||
**Arguments**:
|
||||
|
||||
This field can be applied to any "to-one" relationship, such as a `ForeignKey` field.
|
||||
* `slug_field` - The field on the target that should used as the representation. This should be a field that uniquely identifies any given instance. For example, `username`.
|
||||
* `queryset` - All relational fields must either set a queryset, or set `read_only=True`
|
||||
|
||||
`HyperlinkedRelatedField` will represent the target of the field using a hyperlink. You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the target of the hyperlink.
|
||||
## HyperlinkedRelatedField / ManyHyperlinkedRelatedField
|
||||
|
||||
`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink.
|
||||
|
||||
Be default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
|
||||
|
||||
## ManyHyperlinkedRelatedField
|
||||
**Arguments**:
|
||||
|
||||
This field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship.
|
||||
|
||||
`ManyHyperlinkedRelatedField` will represent the targets of the field using hyperlinks. You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the target of the hyperlink.
|
||||
|
||||
Be default, `ManyHyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag.
|
||||
* `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` - All relational fields must either set a queryset, or set `read_only=True`
|
||||
|
||||
## HyperLinkedIdentityField
|
||||
|
||||
This field can be applied as an identity relationship, such as the `'url'` field on a HyperlinkedModelSerializer.
|
||||
|
||||
You must include a named URL pattern in your URL conf, with a name like `'{model-name}-detail'` that corresponds to the model.
|
||||
|
||||
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.
|
||||
|
||||
[cite]: http://www.python.org/dev/peps/pep-0020/
|
||||
|
|
|
@ -31,8 +31,8 @@ The default throttling policy may be set globally, using the `DEFAULT_THROTTLE_C
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'rest_framework.throttles.AnonThrottle',
|
||||
'rest_framework.throttles.UserThrottle'
|
||||
'rest_framework.throttling.AnonRateThrottle',
|
||||
'rest_framework.throttling.UserRateThrottle'
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'anon': '100/day',
|
||||
|
@ -136,7 +136,7 @@ For example, given the following views...
|
|||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_CLASSES': (
|
||||
'rest_framework.throttles.ScopedRateThrottle'
|
||||
'rest_framework.throttling.ScopedRateThrottle'
|
||||
),
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'contacts': '1000/day',
|
||||
|
|
|
@ -66,11 +66,9 @@ If you're intending to use the browseable API you'll want to add REST framework'
|
|||
|
||||
Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace.
|
||||
|
||||
<!--
|
||||
## Quickstart
|
||||
|
||||
Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running with REST framework.
|
||||
-->
|
||||
|
||||
## Tutorial
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Tutorial <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<!--<li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</a></li>-->
|
||||
<li><a href="{{ base_url }}/tutorial/quickstart{{ suffix }}">Quickstart</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/3-class-based-views{{ suffix }}">3 - Class based views</a></li>
|
||||
|
|
|
@ -51,6 +51,10 @@ The following people have helped make REST framework great.
|
|||
* Daniel Vaca Araujo - [diviei]
|
||||
* Madis Väin - [madisvain]
|
||||
* Stephan Groß - [minddust]
|
||||
* Pavel Savchenko - [asfaltboy]
|
||||
* Otto Yiu - [ottoyiu]
|
||||
* Jacob Magnusson - [jmagnusson]
|
||||
* Osiloke Harold Emoekpere - [osiloke]
|
||||
|
||||
Many thanks to everyone who's contributed to the project.
|
||||
|
||||
|
@ -136,4 +140,8 @@ To contact the author directly:
|
|||
[rdobson]: https://github.com/rdobson
|
||||
[diviei]: https://github.com/diviei
|
||||
[madisvain]: https://github.com/madisvain
|
||||
[minddust]: https://github.com/minddust
|
||||
[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
|
||||
|
|
|
@ -4,8 +4,30 @@
|
|||
>
|
||||
> — Eric S. Raymond, [The Cathedral and the Bazaar][cite].
|
||||
|
||||
## Master
|
||||
|
||||
* Support Django's cache framework.
|
||||
* Minor field improvements. (Don't stringify dicts, more robust many-pk fields.)
|
||||
* Bugfixes (Support choice field in Browseable API)
|
||||
|
||||
## 2.0.2
|
||||
|
||||
**Date**: 2nd Nov 2012
|
||||
|
||||
* Fix issues with pk related fields in the browsable API.
|
||||
|
||||
## 2.0.1
|
||||
|
||||
**Date**: 1st Nov 2012
|
||||
|
||||
* Add support for relational fields in the browsable API.
|
||||
* Added SlugRelatedField and ManySlugRelatedField.
|
||||
* If PUT creates an instance return '201 Created', instead of '200 OK'.
|
||||
|
||||
## 2.0.0
|
||||
|
||||
**Date**: 30th Oct 2012
|
||||
|
||||
* **Fix all of the things.** (Well, almost.)
|
||||
* For more information please see the [2.0 migration guide][migration].
|
||||
|
||||
|
|
|
@ -92,7 +92,7 @@ Let's take a look at how we can compose our views by using the mixin classes.
|
|||
|
||||
class SnippetList(mixins.ListModelMixin,
|
||||
mixins.CreateModelMixin,
|
||||
generics.MultipleObjectBaseView):
|
||||
generics.MultipleObjectAPIView):
|
||||
model = Snippet
|
||||
serializer_class = SnippetSerializer
|
||||
|
||||
|
@ -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 `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.
|
||||
|
||||
|
|
|
@ -19,12 +19,19 @@ First up we're going to define some serializers in `quickstart/serializers.py` t
|
|||
|
||||
|
||||
class GroupSerializer(serializers.HyperlinkedModelSerializer):
|
||||
permissions = serializers.ManySlugRelatedField(
|
||||
slug_field='codename',
|
||||
queryset=Permission.objects.all()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Group
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
Right, we'd better write some views then. Open `quickstart/views.py` and get typing.
|
||||
|
@ -152,7 +159,7 @@ We can now access our API, both from the command-line, using tools like `curl`..
|
|||
},
|
||||
{
|
||||
"email": "tom@example.com",
|
||||
"groups": [],
|
||||
"groups": [ ],
|
||||
"url": "http://127.0.0.1:8000/users/2/",
|
||||
"username": "tom"
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = '2.0.0'
|
||||
__version__ = '2.0.2'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
|||
from django.core.urlresolvers import resolve, get_script_prefix
|
||||
from django.conf import settings
|
||||
from django.forms import widgets
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.encoding import is_protected_type, smart_unicode
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.reverse import reverse
|
||||
|
@ -89,6 +90,8 @@ class Field(object):
|
|||
return value
|
||||
elif hasattr(value, '__iter__') and not isinstance(value, (dict, basestring)):
|
||||
return [self.to_native(item) for item in value]
|
||||
elif isinstance(value, dict):
|
||||
return dict(map(self.to_native, (k, v)) for k, v in value.items())
|
||||
return smart_unicode(value)
|
||||
|
||||
def attributes(self):
|
||||
|
@ -211,9 +214,9 @@ class ModelField(WritableField):
|
|||
def from_native(self, value):
|
||||
try:
|
||||
rel = self.model_field.rel
|
||||
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
||||
except:
|
||||
return self.model_field.to_python(value)
|
||||
return rel.to._meta.get_field(rel.field_name).to_python(value)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = self.model_field._get_val_from_obj(obj)
|
||||
|
@ -229,13 +232,77 @@ class ModelField(WritableField):
|
|||
##### Relational fields #####
|
||||
|
||||
|
||||
# Not actually Writable, but subclasses may need to be.
|
||||
class RelatedField(WritableField):
|
||||
"""
|
||||
Base class for related model fields.
|
||||
|
||||
If not overridden, this represents a to-one relatinship, using the unicode
|
||||
representation of the target.
|
||||
"""
|
||||
widget = widgets.Select
|
||||
cache_choices = False
|
||||
empty_label = None
|
||||
default_read_only = True # TODO: Remove this
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.queryset = kwargs.pop('queryset', None)
|
||||
super(RelatedField, self).__init__(*args, **kwargs)
|
||||
self.read_only = self.default_read_only
|
||||
|
||||
### We need this stuff to make form choices work...
|
||||
|
||||
# def __deepcopy__(self, memo):
|
||||
# result = super(RelatedField, self).__deepcopy__(memo)
|
||||
# result.queryset = result.queryset
|
||||
# return result
|
||||
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_unicode(obj)
|
||||
ident = smart_unicode(self.to_native(obj))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
def _get_queryset(self):
|
||||
return self._queryset
|
||||
|
||||
def _set_queryset(self, queryset):
|
||||
self._queryset = queryset
|
||||
self.widget.choices = self.choices
|
||||
|
||||
queryset = property(_get_queryset, _set_queryset)
|
||||
|
||||
def _get_choices(self):
|
||||
# If self._choices is set, then somebody must have manually set
|
||||
# the property self.choices. In this case, just return self._choices.
|
||||
if hasattr(self, '_choices'):
|
||||
return self._choices
|
||||
|
||||
# Otherwise, execute the QuerySet in self.queryset to determine the
|
||||
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
|
||||
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
|
||||
# time _get_choices() is called (and, thus, each time self.choices is
|
||||
# accessed) so that we can ensure the QuerySet has not been consumed. This
|
||||
# construct might look complicated but it allows for lazy evaluation of
|
||||
# the queryset.
|
||||
return ModelChoiceIterator(self)
|
||||
|
||||
def _set_choices(self, value):
|
||||
# Setting choices also sets the choices on the widget.
|
||||
# choices can be any iterable, but we call list() on it because
|
||||
# it will be consumed more than once.
|
||||
self._choices = self.widget.choices = list(value)
|
||||
|
||||
choices = property(_get_choices, _set_choices)
|
||||
|
||||
### Regular serializier stuff...
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(obj, self.source or field_name)
|
||||
|
@ -253,6 +320,8 @@ class ManyRelatedMixin(object):
|
|||
"""
|
||||
Mixin to convert a related field to a many related field.
|
||||
"""
|
||||
widget = widgets.SelectMultiple
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
value = getattr(obj, self.source or field_name)
|
||||
return [self.to_native(item) for item in value.all()]
|
||||
|
@ -276,6 +345,9 @@ class ManyRelatedMixin(object):
|
|||
class ManyRelatedField(ManyRelatedMixin, RelatedField):
|
||||
"""
|
||||
Base class for related model managers.
|
||||
|
||||
If not overridden, this represents a to-many relationship, using the unicode
|
||||
representations of the target, and is read-only.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
@ -284,9 +356,25 @@ class ManyRelatedField(ManyRelatedMixin, RelatedField):
|
|||
|
||||
class PrimaryKeyRelatedField(RelatedField):
|
||||
"""
|
||||
Serializes a related field or related object to a pk value.
|
||||
Represents a to-one relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
|
||||
# TODO: Remove these field hacks...
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj.pk)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_unicode(obj)
|
||||
ident = smart_unicode(self.to_native(obj.pk))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
# TODO: Possibly change this to just take `obj`, through prob less performant
|
||||
def to_native(self, pk):
|
||||
return pk
|
||||
|
||||
|
@ -297,7 +385,8 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
try:
|
||||
return self.queryset.get(pk=data)
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError('Invalid hyperlink - object does not exist.')
|
||||
msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
def field_to_native(self, obj, field_name):
|
||||
try:
|
||||
|
@ -313,8 +402,23 @@ class PrimaryKeyRelatedField(RelatedField):
|
|||
|
||||
class ManyPrimaryKeyRelatedField(ManyRelatedField):
|
||||
"""
|
||||
Serializes a to-many related field or related manager to a pk value.
|
||||
Represents a to-many relationship as a pk value.
|
||||
"""
|
||||
default_read_only = False
|
||||
|
||||
def prepare_value(self, obj):
|
||||
return self.to_native(obj.pk)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
Return a readable representation for use with eg. select widgets.
|
||||
"""
|
||||
desc = smart_unicode(obj)
|
||||
ident = smart_unicode(self.to_native(obj.pk))
|
||||
if desc == ident:
|
||||
return desc
|
||||
return "%s - %s" % (desc, ident)
|
||||
|
||||
def to_native(self, pk):
|
||||
return pk
|
||||
|
||||
|
@ -329,13 +433,55 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField):
|
|||
# Forward relationship
|
||||
return [self.to_native(item.pk) for item in queryset.all()]
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
try:
|
||||
return self.queryset.get(pk=data)
|
||||
except ObjectDoesNotExist:
|
||||
msg = "Invalid pk '%s' - object does not exist." % smart_unicode(data)
|
||||
raise ValidationError(msg)
|
||||
|
||||
### Slug relationships
|
||||
|
||||
|
||||
class SlugRelatedField(RelatedField):
|
||||
default_read_only = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.slug_field = kwargs.pop('slug_field', None)
|
||||
assert self.slug_field, 'slug_field is required'
|
||||
super(SlugRelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
def to_native(self, obj):
|
||||
return getattr(obj, self.slug_field)
|
||||
|
||||
def from_native(self, data):
|
||||
if self.queryset is None:
|
||||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
try:
|
||||
return self.queryset.get(**{self.slug_field: data})
|
||||
except ObjectDoesNotExist:
|
||||
raise ValidationError('Object with %s=%s does not exist.' %
|
||||
(self.slug_field, unicode(data)))
|
||||
|
||||
|
||||
class ManySlugRelatedField(ManyRelatedMixin, SlugRelatedField):
|
||||
pass
|
||||
|
||||
|
||||
### Hyperlinked relationships
|
||||
|
||||
class HyperlinkedRelatedField(RelatedField):
|
||||
"""
|
||||
Represents a to-one relationship, using hyperlinking.
|
||||
"""
|
||||
pk_url_kwarg = 'pk'
|
||||
slug_url_kwarg = 'slug'
|
||||
slug_field = 'slug'
|
||||
default_read_only = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
|
@ -419,16 +565,20 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
|
||||
|
||||
class ManyHyperlinkedRelatedField(ManyRelatedMixin, HyperlinkedRelatedField):
|
||||
"""
|
||||
Represents a to-many relationship, using hyperlinking.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class HyperlinkedIdentityField(Field):
|
||||
"""
|
||||
A field that represents the model's identity using a hyperlink.
|
||||
Represents the instance, or a property on the instance, using hyperlinking.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
# TODO: Make this mandatory, and have the HyperlinkedModelSerializer
|
||||
# set it on-the-fly
|
||||
# TODO: Make view_name mandatory, and have the
|
||||
# HyperlinkedModelSerializer set it on-the-fly
|
||||
self.view_name = kwargs.pop('view_name', None)
|
||||
self.format = kwargs.pop('format', None)
|
||||
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
|
||||
|
|
|
@ -3,9 +3,6 @@ Basic building blocks for generic class based views.
|
|||
|
||||
We don't bind behaviour to http method handlers yet,
|
||||
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 rest_framework import status
|
||||
|
@ -32,7 +29,7 @@ class CreateModelMixin(object):
|
|||
class ListModelMixin(object):
|
||||
"""
|
||||
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."
|
||||
|
||||
|
@ -78,15 +75,17 @@ class UpdateModelMixin(object):
|
|||
def update(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.object = self.get_object()
|
||||
success_status = status.HTTP_200_OK
|
||||
except Http404:
|
||||
self.object = None
|
||||
success_status = status.HTTP_201_CREATED
|
||||
|
||||
serializer = self.get_serializer(data=request.DATA, instance=self.object)
|
||||
|
||||
if serializer.is_valid():
|
||||
self.pre_save(serializer.object)
|
||||
self.object = serializer.save()
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.data, status=success_status)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
|
|
@ -100,7 +100,7 @@ class JSONPRenderer(JSONRenderer):
|
|||
callback = self.get_callback(renderer_context)
|
||||
json = super(JSONPRenderer, self).render(data, accepted_media_type,
|
||||
renderer_context)
|
||||
return "%s(%s);" % (callback, json)
|
||||
return u"%s(%s);" % (callback, json)
|
||||
|
||||
|
||||
class XMLRenderer(BaseRenderer):
|
||||
|
@ -281,11 +281,14 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
serializers.DateField: forms.DateField,
|
||||
serializers.EmailField: forms.EmailField,
|
||||
serializers.CharField: forms.CharField,
|
||||
serializers.ChoiceField: forms.ChoiceField,
|
||||
serializers.BooleanField: forms.BooleanField,
|
||||
serializers.PrimaryKeyRelatedField: forms.ModelChoiceField,
|
||||
serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField,
|
||||
serializers.HyperlinkedRelatedField: forms.ModelChoiceField,
|
||||
serializers.ManyHyperlinkedRelatedField: forms.ModelMultipleChoiceField
|
||||
serializers.PrimaryKeyRelatedField: forms.ChoiceField,
|
||||
serializers.ManyPrimaryKeyRelatedField: forms.MultipleChoiceField,
|
||||
serializers.SlugRelatedField: forms.ChoiceField,
|
||||
serializers.ManySlugRelatedField: forms.MultipleChoiceField,
|
||||
serializers.HyperlinkedRelatedField: forms.ChoiceField,
|
||||
serializers.ManyHyperlinkedRelatedField: forms.MultipleChoiceField
|
||||
}
|
||||
|
||||
fields = {}
|
||||
|
@ -296,19 +299,14 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
kwargs = {}
|
||||
kwargs['required'] = v.required
|
||||
|
||||
if getattr(v, 'queryset', None):
|
||||
kwargs['queryset'] = v.queryset
|
||||
#if getattr(v, 'queryset', None):
|
||||
# kwargs['queryset'] = v.queryset
|
||||
|
||||
if getattr(v, 'choices', None) is not None:
|
||||
kwargs['choices'] = v.choices
|
||||
|
||||
if getattr(v, 'widget', None):
|
||||
widget = copy.deepcopy(v.widget)
|
||||
# If choices have friendly readable names,
|
||||
# then add in the identities too
|
||||
if getattr(widget, 'choices', None):
|
||||
choices = widget.choices
|
||||
if any([ident != desc for (ident, desc) in choices]):
|
||||
choices = [(ident, "%s (%s)" % (desc, ident))
|
||||
for (ident, desc) in choices]
|
||||
widget.choices = choices
|
||||
kwargs['widget'] = widget
|
||||
|
||||
if getattr(v, 'default', None) is not None:
|
||||
|
@ -319,7 +317,10 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
try:
|
||||
fields[k] = field_mapping[v.__class__](**kwargs)
|
||||
except KeyError:
|
||||
fields[k] = forms.CharField(**kwargs)
|
||||
if getattr(v, 'choices', None) is not None:
|
||||
fields[k] = forms.ChoiceField(**kwargs)
|
||||
else:
|
||||
fields[k] = forms.CharField(**kwargs)
|
||||
return fields
|
||||
|
||||
def get_form(self, view, method, request):
|
||||
|
|
|
@ -45,3 +45,13 @@ class Response(SimpleTemplateResponse):
|
|||
# TODO: Deprecate and use a template tag instead
|
||||
# TODO: Status code text for RFC 6585 status codes
|
||||
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
|
||||
|
|
|
@ -32,10 +32,10 @@ def main():
|
|||
'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.',
|
||||
DeprecationWarning
|
||||
)
|
||||
failures = TestRunner(['rest_framework'])
|
||||
failures = TestRunner(['tests'])
|
||||
else:
|
||||
test_runner = TestRunner()
|
||||
failures = test_runner.run_tests(['rest_framework'])
|
||||
failures = test_runner.run_tests(['tests'])
|
||||
cov.stop()
|
||||
|
||||
# Discover the list of all modules that we should test coverage for
|
||||
|
|
|
@ -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:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
|
|
|
@ -36,6 +36,13 @@ ul.breadcrumb {
|
|||
margin: 58px 0 0 0;
|
||||
}
|
||||
|
||||
form select, form input {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
form select[multiple] {
|
||||
height: 150px;
|
||||
}
|
||||
/* To allow tooltips to work on disabled elements */
|
||||
.disabled-tooltip-shield {
|
||||
position: absolute;
|
||||
|
|
|
@ -131,12 +131,12 @@
|
|||
{% csrf_token %}
|
||||
{{ post_form.non_field_errors }}
|
||||
{% 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" }}
|
||||
<div class="controls">
|
||||
{{ field|add_class:"input-xlarge" }}
|
||||
{{ field }}
|
||||
<span class="help-inline">{{ field.help_text }}</span>
|
||||
{{ field.errors|add_class:"help-block" }}
|
||||
<!--{{ field.errors|add_class:"help-block" }}-->
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -156,12 +156,12 @@
|
|||
{% csrf_token %}
|
||||
{{ put_form.non_field_errors }}
|
||||
{% 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" }}
|
||||
<div class="controls">
|
||||
{{ field|add_class:"input-xlarge" }}
|
||||
{{ field }}
|
||||
<span class='help-inline'>{{ field.help_text }}</span>
|
||||
{{ field.errors|add_class:"help-block" }}
|
||||
<!--{{ field.errors|add_class:"help-block" }}-->
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -236,7 +236,7 @@ class TestInstanceView(TestCase):
|
|||
request = factory.put('/1', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.view(request, pk=1).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})
|
||||
updated = self.objects.get(id=1)
|
||||
self.assertEquals(updated.text, 'foobar')
|
||||
|
@ -251,7 +251,7 @@ class TestInstanceView(TestCase):
|
|||
request = factory.put('/5', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.view(request, pk=5).render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
new_obj = self.objects.get(pk=5)
|
||||
self.assertEquals(new_obj.text, 'foobar')
|
||||
|
||||
|
@ -264,7 +264,7 @@ class TestInstanceView(TestCase):
|
|||
request = factory.put('/test_slug', json.dumps(content),
|
||||
content_type='application/json')
|
||||
response = self.slug_based_view(request, slug='test_slug').render()
|
||||
self.assertEquals(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEquals(response.status_code, status.HTTP_201_CREATED)
|
||||
self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'})
|
||||
new_obj = SlugBasedModel.objects.get(slug='test_slug')
|
||||
self.assertEquals(new_obj.text, 'foobar')
|
||||
|
|
|
@ -122,6 +122,13 @@ 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):
|
||||
|
|
187
rest_framework/tests/pk_relations.py
Normal file
187
rest_framework/tests/pk_relations.py
Normal file
|
@ -0,0 +1,187 @@
|
|||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
# ManyToMany
|
||||
|
||||
class ManyToManyTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ManyToManySource(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
|
||||
|
||||
|
||||
class ManyToManyTargetSerializer(serializers.ModelSerializer):
|
||||
sources = serializers.ManyPrimaryKeyRelatedField(queryset=ManyToManySource.objects.all())
|
||||
|
||||
class Meta:
|
||||
model = ManyToManyTarget
|
||||
|
||||
|
||||
class ManyToManySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ManyToManySource
|
||||
|
||||
|
||||
# ForeignKey
|
||||
|
||||
class ForeignKeyTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ForeignKeySource(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
|
||||
|
||||
|
||||
class ForeignKeyTargetSerializer(serializers.ModelSerializer):
|
||||
sources = serializers.ManyPrimaryKeyRelatedField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ForeignKeyTarget
|
||||
|
||||
|
||||
class ForeignKeySourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ForeignKeySource
|
||||
|
||||
|
||||
# TODO: Add test that .data cannot be accessed prior to .is_valid
|
||||
|
||||
class PrimaryKeyManyToManyTests(TestCase):
|
||||
def setUp(self):
|
||||
for idx in range(1, 4):
|
||||
target = ManyToManyTarget(name='target-%d' % idx)
|
||||
target.save()
|
||||
source = ManyToManySource(name='source-%d' % idx)
|
||||
source.save()
|
||||
for target in ManyToManyTarget.objects.all():
|
||||
source.targets.add(target)
|
||||
|
||||
def test_many_to_many_retrieve(self):
|
||||
queryset = ManyToManySource.objects.all()
|
||||
serializer = ManyToManySourceSerializer(instance=queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'source-1', 'targets': [1]},
|
||||
{'id': 2, 'name': u'source-2', 'targets': [1, 2]},
|
||||
{'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_reverse_many_to_many_retrieve(self):
|
||||
queryset = ManyToManyTarget.objects.all()
|
||||
serializer = ManyToManyTargetSerializer(instance=queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
|
||||
{'id': 2, 'name': u'target-2', 'sources': [2, 3]},
|
||||
{'id': 3, 'name': u'target-3', 'sources': [3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_many_to_many_update(self):
|
||||
data = {'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]}
|
||||
instance = ManyToManySource.objects.get(pk=1)
|
||||
serializer = ManyToManySourceSerializer(data, instance=instance)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.data, data)
|
||||
serializer.save()
|
||||
|
||||
# Ensure source 1 is updated, and everything else is as expected
|
||||
queryset = ManyToManySource.objects.all()
|
||||
serializer = ManyToManySourceSerializer(instance=queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'source-1', 'targets': [1, 2, 3]},
|
||||
{'id': 2, 'name': u'source-2', 'targets': [1, 2]},
|
||||
{'id': 3, 'name': u'source-3', 'targets': [1, 2, 3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_reverse_many_to_many_update(self):
|
||||
data = {'id': 1, 'name': u'target-1', 'sources': [1]}
|
||||
instance = ManyToManyTarget.objects.get(pk=1)
|
||||
serializer = ManyToManyTargetSerializer(data, instance=instance)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.data, data)
|
||||
serializer.save()
|
||||
|
||||
# Ensure target 1 is updated, and everything else is as expected
|
||||
queryset = ManyToManyTarget.objects.all()
|
||||
serializer = ManyToManyTargetSerializer(instance=queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'target-1', 'sources': [1]},
|
||||
{'id': 2, 'name': u'target-2', 'sources': [2, 3]},
|
||||
{'id': 3, 'name': u'target-3', 'sources': [3]}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
|
||||
class PrimaryKeyForeignKeyTests(TestCase):
|
||||
def setUp(self):
|
||||
target = ForeignKeyTarget(name='target-1')
|
||||
target.save()
|
||||
new_target = ForeignKeyTarget(name='target-2')
|
||||
new_target.save()
|
||||
for idx in range(1, 4):
|
||||
source = ForeignKeySource(name='source-%d' % idx, target=target)
|
||||
source.save()
|
||||
|
||||
def test_foreign_key_retrieve(self):
|
||||
queryset = ForeignKeySource.objects.all()
|
||||
serializer = ForeignKeySourceSerializer(instance=queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'source-1', 'target': 1},
|
||||
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||
{'id': 3, 'name': u'source-3', 'target': 1}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_reverse_foreign_key_retrieve(self):
|
||||
queryset = ForeignKeyTarget.objects.all()
|
||||
serializer = ForeignKeyTargetSerializer(instance=queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'target-1', 'sources': [1, 2, 3]},
|
||||
{'id': 2, 'name': u'target-2', 'sources': []},
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
def test_foreign_key_update(self):
|
||||
data = {'id': 1, 'name': u'source-1', 'target': 2}
|
||||
instance = ForeignKeySource.objects.get(pk=1)
|
||||
serializer = ForeignKeySourceSerializer(data, instance=instance)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
self.assertEquals(serializer.data, data)
|
||||
serializer.save()
|
||||
|
||||
# # Ensure source 1 is updated, and everything else is as expected
|
||||
queryset = ForeignKeySource.objects.all()
|
||||
serializer = ForeignKeySourceSerializer(instance=queryset)
|
||||
expected = [
|
||||
{'id': 1, 'name': u'source-1', 'target': 2},
|
||||
{'id': 2, 'name': u'source-2', 'target': 1},
|
||||
{'id': 3, 'name': u'source-3', 'target': 1}
|
||||
]
|
||||
self.assertEquals(serializer.data, expected)
|
||||
|
||||
# reverse foreign keys MUST be read_only
|
||||
# In the general case they do not provide .remove() or .clear()
|
||||
# and cannot be arbitrarily set.
|
||||
|
||||
# def test_reverse_foreign_key_update(self):
|
||||
# data = {'id': 1, 'name': u'target-1', 'sources': [1]}
|
||||
# instance = ForeignKeyTarget.objects.get(pk=1)
|
||||
# serializer = ForeignKeyTargetSerializer(data, instance=instance)
|
||||
# self.assertTrue(serializer.is_valid())
|
||||
# self.assertEquals(serializer.data, data)
|
||||
# serializer.save()
|
||||
|
||||
# # Ensure target 1 is updated, and everything else is as expected
|
||||
# queryset = ForeignKeyTarget.objects.all()
|
||||
# serializer = ForeignKeyTargetSerializer(instance=queryset)
|
||||
# expected = [
|
||||
# {'id': 1, 'name': u'target-1', 'sources': [1]},
|
||||
# {'id': 2, 'name': u'target-2', 'sources': []},
|
||||
# ]
|
||||
# self.assertEquals(serializer.data, expected)
|
|
@ -1,6 +1,8 @@
|
|||
import pickle
|
||||
import re
|
||||
|
||||
from django.conf.urls.defaults import patterns, url, include
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
|
||||
|
@ -83,6 +85,7 @@ class HTMLView1(APIView):
|
|||
urlpatterns = patterns('',
|
||||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
||||
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
||||
url(r'^cache$', MockGETView.as_view()),
|
||||
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
|
||||
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
|
||||
url(r'^html$', HTMLView.as_view()),
|
||||
|
@ -416,3 +419,89 @@ class XMLRendererTestCase(TestCase):
|
|||
self.assertTrue(xml.startswith('<?xml version="1.0" encoding="utf-8"?>\n<root>'))
|
||||
self.assertTrue(xml.endswith('</root>'))
|
||||
self.assertTrue(string in xml, '%r not in %r' % (string, xml))
|
||||
|
||||
|
||||
# Tests for caching issue, #346
|
||||
class CacheRenderTest(TestCase):
|
||||
"""
|
||||
Tests specific to caching responses
|
||||
"""
|
||||
|
||||
urls = 'rest_framework.tests.renderers'
|
||||
|
||||
cache_key = 'just_a_cache_key'
|
||||
|
||||
@classmethod
|
||||
def _get_pickling_errors(cls, obj, seen=None):
|
||||
""" Return any errors that would be raised if `obj' is pickled
|
||||
Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897
|
||||
"""
|
||||
if seen == None:
|
||||
seen = []
|
||||
try:
|
||||
state = obj.__getstate__()
|
||||
except AttributeError:
|
||||
return
|
||||
if state == None:
|
||||
return
|
||||
if isinstance(state,tuple):
|
||||
if not isinstance(state[0],dict):
|
||||
state=state[1]
|
||||
else:
|
||||
state=state[0].update(state[1])
|
||||
result = {}
|
||||
for i in state:
|
||||
try:
|
||||
pickle.dumps(state[i],protocol=2)
|
||||
except pickle.PicklingError:
|
||||
if not state[i] in seen:
|
||||
seen.append(state[i])
|
||||
result[i] = cls._get_pickling_errors(state[i],seen)
|
||||
return result
|
||||
|
||||
def http_resp(self, http_method, url):
|
||||
"""
|
||||
Simple wrapper for Client http requests
|
||||
Removes the `client' and `request' attributes from as they are
|
||||
added by django.test.client.Client and not part of caching
|
||||
responses outside of tests.
|
||||
"""
|
||||
method = getattr(self.client, http_method)
|
||||
resp = method(url)
|
||||
del resp.client, resp.request
|
||||
return resp
|
||||
|
||||
def test_obj_pickling(self):
|
||||
"""
|
||||
Test that responses are properly pickled
|
||||
"""
|
||||
resp = self.http_resp('get', '/cache')
|
||||
|
||||
# Make sure that no pickling errors occurred
|
||||
self.assertEqual(self._get_pickling_errors(resp), {})
|
||||
|
||||
# Unfortunately LocMem backend doesn't raise PickleErrors but returns
|
||||
# None instead.
|
||||
cache.set(self.cache_key, resp)
|
||||
self.assertTrue(cache.get(self.cache_key) is not None)
|
||||
|
||||
def test_head_caching(self):
|
||||
"""
|
||||
Test caching of HEAD requests
|
||||
"""
|
||||
resp = self.http_resp('head', '/cache')
|
||||
cache.set(self.cache_key, resp)
|
||||
|
||||
cached_resp = cache.get(self.cache_key)
|
||||
self.assertIsInstance(cached_resp, Response)
|
||||
|
||||
def test_get_caching(self):
|
||||
"""
|
||||
Test caching of GET requests
|
||||
"""
|
||||
resp = self.http_resp('get', '/cache')
|
||||
cache.set(self.cache_key, resp)
|
||||
|
||||
cached_resp = cache.get(self.cache_key)
|
||||
self.assertIsInstance(cached_resp, Response)
|
||||
self.assertEqual(cached_resp.content, resp.content)
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import datetime
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.models import *
|
||||
from rest_framework.tests.models import (ActionItem, Anchor, BasicModel,
|
||||
BlankFieldModel, BlogPost, CallableDefaultValueModel, DefaultValueModel,
|
||||
ManyToManyModel, Person, ReadOnlyManyToManyModel)
|
||||
|
||||
|
||||
class SubComment(object):
|
||||
|
@ -44,8 +46,11 @@ class ActionItemSerializer(serializers.ModelSerializer):
|
|||
|
||||
|
||||
class PersonSerializer(serializers.ModelSerializer):
|
||||
info = serializers.Field(source='info')
|
||||
|
||||
class Meta:
|
||||
model = Person
|
||||
fields = ('name', 'age', 'info')
|
||||
|
||||
|
||||
class BasicTests(TestCase):
|
||||
|
@ -67,6 +72,9 @@ class BasicTests(TestCase):
|
|||
'created': datetime.datetime(2012, 1, 1),
|
||||
'sub_comment': 'And Merry Christmas!'
|
||||
}
|
||||
self.person_data = {'name': 'dwight', 'age': 35}
|
||||
self.person = Person(**self.person_data)
|
||||
self.person.save()
|
||||
|
||||
def test_empty(self):
|
||||
serializer = CommentSerializer()
|
||||
|
@ -97,6 +105,21 @@ class BasicTests(TestCase):
|
|||
self.assertEquals(serializer.object, expected)
|
||||
self.assertTrue(serializer.object is expected)
|
||||
self.assertEquals(serializer.data['sub_comment'], 'And Merry Christmas!')
|
||||
|
||||
def test_model_fields_as_expected(self):
|
||||
""" Make sure that the fields returned are the same as defined
|
||||
in the Meta data
|
||||
"""
|
||||
serializer = PersonSerializer(instance=self.person)
|
||||
self.assertEquals(set(serializer.data.keys()),
|
||||
set(['name', 'age', 'info']))
|
||||
|
||||
def test_field_with_dictionary(self):
|
||||
""" Make sure that dictionaries from fields are left intact
|
||||
"""
|
||||
serializer = PersonSerializer(instance=self.person)
|
||||
expected = self.person_data
|
||||
self.assertEquals(serializer.data['info'], expected)
|
||||
|
||||
|
||||
class ValidationTests(TestCase):
|
||||
|
|
Loading…
Reference in New Issue
Block a user