diff options
| author | Stephan Groß | 2012-11-05 11:45:49 +0100 |
|---|---|---|
| committer | Stephan Groß | 2012-11-05 11:45:49 +0100 |
| commit | 44449fa1f5f2d68740c9fb6a13c8e0a0eb41434c (patch) | |
| tree | c4b6d8ed8af239dd5651d9f6a18ddbfe2079bc07 | |
| parent | ff7725f05e8ca624e54d707f7c655e3d5c8b8888 (diff) | |
| parent | 5b397e50ddb999f85949a7359d0a26c3531c78a9 (diff) | |
| download | django-rest-framework-44449fa1f5f2d68740c9fb6a13c8e0a0eb41434c.tar.bz2 | |
Merge remote-tracking branch 'upstream/master'
25 files changed, 612 insertions, 74 deletions
@@ -7,7 +7,7 @@ html/ coverage/ build/ dist/ -rest_framework.egg-info/ +*.egg-info/ MANIFEST !.gitignore @@ -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**. diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 889d16c0..3137b9d4 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -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 = { diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 8c3df067..86460b4b 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -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 -Be default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag. - -## ManyHyperlinkedRelatedField +`HyperlinkedRelatedField` and `ManyHyperlinkedRelatedField` will represent the target of the relationship using a hyperlink. -This field can be applied to any "to-many" relationship, such as a `ManyToManyField` field, or a reverse `ForeignKey` relationship. +Be default, `HyperlinkedRelatedField` is read-write, although you can change this behaviour using the `read_only` flag. -`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. +**Arguments**: -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/ diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index bfda7079..b03bc9e0 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -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', diff --git a/docs/index.md b/docs/index.md index 75a1cf6e..5e086872 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/docs/template.html b/docs/template.html index 94fc269f..c428dff3 100644 --- a/docs/template.html +++ b/docs/template.html @@ -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> diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 69d57802..ee00fc8b 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -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
\ No newline at end of file +[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 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index a466f4b1..14d910bd 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -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]. diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index a31dccb2..91ef4038 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -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. diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 6bde725b..93da1a59 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -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" } diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 557f5943..56b9a35c 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.0.0' +__version__ = '2.0.2' VERSION = __version__ # synonym diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 63e15923..2185cf34 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -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) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 8873e4ae..47e4edf7 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -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) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8dff0c77..0a659bd1 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -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): diff --git a/rest_framework/response.py b/rest_framework/response.py index 7a459c8f..006d7eeb 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -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 diff --git a/rest_framework/runtests/runcoverage.py b/rest_framework/runtests/runcoverage.py index ea2e3d45..0ce379eb 100755 --- a/rest_framework/runtests/runcoverage.py +++ b/rest_framework/runtests/runcoverage.py @@ -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 diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 951b1e72..b48f85e4 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -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. diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index e29da395..fdf45659 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -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; diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index e0f79481..fb0e19f0 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -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 %} diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index d45ea976..a8279ef2 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -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') diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 415e4d06..1a0078e8 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -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): diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/pk_relations.py new file mode 100644 index 00000000..9095dcd8 --- /dev/null +++ b/rest_framework/tests/pk_relations.py @@ -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) diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 48d8d9bd..9be4b114 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -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) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index d4b43862..3fd49064 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -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): |
