Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Stephan Groß 2012-11-05 11:45:49 +01:00
commit 44449fa1f5
25 changed files with 612 additions and 74 deletions

2
.gitignore vendored
View File

@ -7,7 +7,7 @@ html/
coverage/
build/
dist/
rest_framework.egg-info/
*.egg-info/
MANIFEST
!.gitignore

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,8 +4,30 @@
>
> &mdash; 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].

View File

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

View File

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

View File

@ -1,3 +1,3 @@
__version__ = '2.0.0'
__version__ = '2.0.2'
VERSION = __version__ # synonym

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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