diff options
41 files changed, 990 insertions, 148 deletions
| @@ -10,5 +10,10 @@ dist/  *.egg-info/  MANIFEST +bin/ +include/ +lib/ +local/ +  !.gitignore  !.travis.yml diff --git a/.travis.yml b/.travis.yml index ccfdeacb..0dc87837 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,8 +6,8 @@ python:  env:    - DJANGO=https://github.com/django/django/zipball/master -  - DJANGO=django==1.4.1 --use-mirrors -  - DJANGO=django==1.3.3 --use-mirrors +  - DJANGO=django==1.4.3 --use-mirrors +  - DJANGO=django==1.3.5 --use-mirrors  install:    - pip install $DJANGO @@ -2,7 +2,9 @@  **A toolkit for building well-connected, self-describing web APIs.** -**Author:** Tom Christie.  [Follow me on Twitter][twitter] +**Author:** Tom Christie.  [Follow me on Twitter][twitter]. + +**Support:** [REST framework discussion group][group].  [![build-status-image]][travis] @@ -37,14 +39,35 @@ There is also a sandbox API you can use for testing purposes, [available here][s  # Installation -Install using `pip`... +Install using `pip`, including any optional packages you want...      pip install djangorestframework +    pip install markdown  # Markdown support for the browseable API. +    pip install pyyaml    # YAML content-type support. +    pip install django-filter  # Filtering support  ...or clone the project from github.      git clone git@github.com:tomchristie/django-rest-framework.git +    cd django-rest-framework      pip install -r requirements.txt +    pip install -r optionals.txt + +Add `'rest_framework'` to your `INSTALLED_APPS` setting. + +    INSTALLED_APPS = ( +        ... +        'rest_framework',         +    ) + +If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views.  Add the following to your root `urls.py` file. + +    urlpatterns = patterns('', +        ... +        url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) +    ) + +Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.  # Development @@ -58,6 +81,34 @@ To run the tests.  # Changelog +### 2.1.13 + +**Date**: 28th Dec 2012 + +* Support configurable `STATICFILES_STORAGE` storage. +* Bugfix: Related fields now respect the required flag, and may be required=False. + +### 2.1.12 + +**Date**: 21st Dec 2012 + +* Bugfix: Fix bug that could occur using ChoiceField. +* Bugfix: Fix exception in browseable API on DELETE. +* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg. + +## 2.1.11 + +**Date**: 17th Dec 2012 + +* Bugfix: Fix issue with M2M fields in browseable API. + +## 2.1.10 + +**Date**: 17th Dec 2012 + +* Bugfix: Ensure read-only fields don't have model validation applied. +* Bugfix: Fix hyperlinked fields in paginated results. +  ## 2.1.9  **Date**: 11th Dec 2012 @@ -198,6 +249,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  [build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2  [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master  [twitter]: https://twitter.com/_tomchristie +[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework  [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X  [sandbox]: http://restframework.herokuapp.com/  [rest-framework-2-announcement]: http://django-rest-framework.org/topics/rest-framework-2-announcement.html diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 428323b8..27c7d3f6 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -7,11 +7,11 @@  >  > — [Django Documentation][cite] -One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour.  REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.  +One of the key benefits of class based views is the way they allow you to compose bits of reusable behaviour.  REST framework takes advantage of this by providing a number of pre-built views that provide for commonly used patterns.  The generic views provided by REST framework allow you to quickly build API views that map closely to your database models. -If the generic views don't suit the needs of your API, you can drop down to using the regular `APIView` class, or reuse the mixins and base classes used by the generic views to compose your own set of reusable generic views.  +If the generic views don't suit the needs of your API, you can drop down to using the regular `APIView` class, or reuse the mixins and base classes used by the generic views to compose your own set of reusable generic views.  ## Examples @@ -29,7 +29,7 @@ For more complex cases you might also want to override various methods on the vi          model = User          serializer_class = UserSerializer          permission_classes = (IsAdminUser,) -         +          def get_paginate_by(self, queryset):              """              Use smaller pagination for HTML representations. @@ -150,14 +150,14 @@ Provides a base view for acting on a single object, by combining REST framework'  * `queryset` - The queryset that should be used when retrieving an object from this view.  If unset, defaults to the default queryset manager for `self.model`.  * `pk_kwarg` - The URL kwarg that should be used to look up objects by primary key. Defaults to `'pk'`. [Can only be set to non-default on Django 1.4+] -* `slug_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`.  [Can only be set to non-default on Django 1.4+] +* `slug_url_kwarg` - The URL kwarg that should be used to look up objects by a slug. Defaults to `'slug'`.  [Can only be set to non-default on Django 1.4+]  * `slug_field` - The field on the model that should be used to look up objects by a slug.  If used, this should typically be set to a field with `unique=True`. Defaults to `'slug'`.  ---  # Mixins -The mixin classes provide the actions that are used to provide the basic view behaviour.  Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly.  This allows for more flexible composition of behaviour.  +The mixin classes provide the actions that are used to provide the basic view behaviour.  Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly.  This allows for more flexible composition of behaviour.  ## ListModelMixin @@ -220,4 +220,4 @@ Should be mixed in with [SingleObjectAPIView].  [CreateModelMixin]: #createmodelmixin  [RetrieveModelMixin]: #retrievemodelmixin  [UpdateModelMixin]: #updatemodelmixin -[DestroyModelMixin]: #destroymodelmixin
\ No newline at end of file +[DestroyModelMixin]: #destroymodelmixin diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 19efde3c..d98a602f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -4,8 +4,7 @@  > Expanding the usefulness of the serializers is something that we would  like to address. However, it's not a trivial problem, and it -will take some serious design work. Any offers to help out in this -area would be gratefully accepted. +will take some serious design work.  >  > — Russell Keith-Magee, [Django users group][cite] @@ -110,7 +109,22 @@ Your `validate_<fieldname>` methods should either just return the `attrs` dictio  ### Object-level validation -To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`. +To do any other validation that requires access to multiple fields, add a method called `.validate()` to your `Serializer` subclass. This method takes a single argument, which is the `attrs` dictionary. It should raise a `ValidationError` if necessary, or just return `attrs`.  For example: + +    from rest_framework import serializers + +    class EventSerializer(serializers.Serializer): +        description = serializers.CahrField(max_length=100) +        start = serializers.DateTimeField() +        finish = serializers.DateTimeField() + +        def validate(self, attrs): +            """ +            Check that the start is before the stop. +            """ +            if attrs['start'] < attrs['finish']: +                raise serializers.ValidationError("finish must occur after start") +            return attrs  ## Saving object state diff --git a/docs/index.md b/docs/index.md index cc0f2a13..69d972d0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,7 +15,7 @@ Django REST framework is a lightweight library that makes it easy to build Web A  Web APIs built using REST framework are fully self-describing and web browseable - a huge useability win for your developers.  It also supports a wide range of media types, authentication and permission policies out of the box. -If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcment][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities. +If you are considering using REST framework for your API, we recommend reading the [REST framework 2 announcement][rest-framework-2-announcement] which gives a good overview of the framework and it's capabilities.  There is also a sandbox API you can use for testing purposes, [available here][sandbox]. @@ -52,21 +52,21 @@ Install using `pip`, including any optional packages you want...      pip install -r requirements.txt      pip install -r optionals.txt -Add `rest_framework` to your `INSTALLED_APPS`. +Add `'rest_framework'` to your `INSTALLED_APPS` setting.      INSTALLED_APPS = (          ...          'rest_framework',              ) -If you're intending to use the browseable API you'll want to add REST framework's login and logout views.  Add the following to your root `urls.py` file. +If you're intending to use the browseable API you'll probably also want to add REST framework's login and logout views.  Add the following to your root `urls.py` file.      urlpatterns = patterns('',          ...          url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))      ) -Note that the URL path can be whatever you want, but you must include `rest_framework.urls` with the `rest_framework` namespace. +Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.  ## Quickstart diff --git a/docs/topics/credits.md b/docs/topics/credits.md index cdf57f7e..e4b8da4c 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -77,6 +77,11 @@ The following people have helped make REST framework great.  * Venkata Subramanian Mahalingam - [annacoder]  * George Kappel - [gkappel]  * Colin Murtaugh - [cmurtaugh] +* Simon Pantzare - [pilt] +* Szymon Teżewski - [sunscrapers] +* Joel Marcotte - [joual] +* Trey Hunner - [treyhunner] +* Roman Akinfold - [akinfold]  Many thanks to everyone who's contributed to the project. @@ -96,10 +101,9 @@ Development of REST framework 2.0 was sponsored by [DabApps].  ## Contact -To contact the author directly: +For usage questions please see the [REST framework discussion group][group]. -* twitter: [@_tomchristie][twitter] -* email: [tom@tomchristie.com][email] +You can also contact [@_tomchristie][twitter] directly on twitter.  [email]: mailto:tom@tomchristie.com  [twitter]: http://twitter.com/_tomchristie @@ -113,6 +117,7 @@ To contact the author directly:  [dabapps]: http://lab.dabapps.com  [sandbox]: http://restframework.herokuapp.com/  [heroku]: http://www.heroku.com/ +[group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework  [tomchristie]: https://github.com/tomchristie  [markotibold]: https://github.com/markotibold @@ -189,3 +194,8 @@ To contact the author directly:  [annacoder]: https://github.com/annacoder  [gkappel]: https://github.com/gkappel  [cmurtaugh]: https://github.com/cmurtaugh +[pilt]: https://github.com/pilt +[sunscrapers]: https://github.com/sunscrapers +[joual]: https://github.com/joual +[treyhunner]: https://github.com/treyhunner +[akinfold]: https://github.com/akinfold diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 4f83cfd8..dd54a613 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -4,7 +4,51 @@  >  > — Eric S. Raymond, [The Cathedral and the Bazaar][cite]. -## 2.1.9 +## Versioning + +Minor version numbers (0.0.x) are used for changes that are API compatible.  You should be able to upgrade between minor point releases without any other code changes. + +Medium version numbers (0.x.0) may include minor API changes.  You should read the release notes carefully before upgrading between medium point releases. + +Major version numbers (x.0.0) are reserved for project milestones.  No major point releases are currently planned. + +--- + +## 2.1.x series + +### Master + +* Bugfix: Nested serializers now support nullable relationships. + +### 2.1.13 + +**Date**: 28th Dec 2012 + +* Support configurable `STATICFILES_STORAGE` storage. +* Bugfix: Related fields now respect the required flag, and may be required=False. + +### 2.1.12 + +**Date**: 21st Dec 2012 + +* Bugfix: Fix bug that could occur using ChoiceField. +* Bugfix: Fix exception in browseable API on DELETE. +* Bugfix: Fix issue where pk was was being set to a string if set by URL kwarg. + +### 2.1.11 + +**Date**: 17th Dec 2012 + +* Bugfix: Fix issue with M2M fields in browseable API. + +### 2.1.10 + +**Date**: 17th Dec 2012 + +* Bugfix: Ensure read-only fields don't have model validation applied. +* Bugfix: Fix hyperlinked fields in paginated results. + +### 2.1.9  **Date**: 11th Dec 2012 @@ -12,14 +56,14 @@  * Bugfix: Fix `Meta.fields` only working as tuple not as list.  * Bugfix: Edge case if unnecessarily specifying `required=False` on read only field. -## 2.1.8 +### 2.1.8  **Date**: 8th Dec 2012  * Fix for creating nullable Foreign Keys with `''` as well as `None`.  * Added `null=<bool>` related field option. -## 2.1.7 +### 2.1.7  **Date**: 7th Dec 2012 @@ -31,19 +75,19 @@  * Make `Request.user` settable.  * Bugfix: Fix `RegexField` to work with `BrowsableAPIRenderer`. -## 2.1.6 +### 2.1.6  **Date**: 23rd Nov 2012  * Bugfix: Unfix DjangoModelPermissions.  (I am a doofus.) -## 2.1.5 +### 2.1.5  **Date**: 23rd Nov 2012  * Bugfix: Fix DjangoModelPermissions. -## 2.1.4 +### 2.1.4  **Date**: 22nd Nov 2012 @@ -54,7 +98,7 @@  * Added `obtain_token_view` to get tokens when using `TokenAuthentication`.  * Bugfix: Django 1.5 configurable user support for `TokenAuthentication`. -## 2.1.3 +### 2.1.3  **Date**: 16th Nov 2012 @@ -65,29 +109,27 @@  * 201 Responses now return a 'Location' header.  * Bugfix: Serializer fields now respect `max_length`. -## 2.1.2 +### 2.1.2  **Date**: 9th Nov 2012  * **Filtering support.**  * Bugfix: Support creation of objects with reverse M2M relations. -## 2.1.1 +### 2.1.1  **Date**: 7th Nov 2012  * Support use of HTML exception templates.  Eg. `403.html`  * Hyperlinked fields take optional `slug_field`, `slug_url_kwarg` and `pk_url_kwarg` arguments. -* Bugfix: Deal with optional trailing slashs properly when generating breadcrumbs. +* Bugfix: Deal with optional trailing slashes properly when generating breadcrumbs.  * Bugfix: Make textareas same width as other fields in browsable API.  * Private API change: `.get_serializer` now uses same `instance` and `data` ordering as serializer initialization. -## 2.1.0 +### 2.1.0  **Date**: 5th Nov 2012 -**Warning**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0. -  * **Serializer `instance` and `data` keyword args have their position swapped.**  * `queryset` argument is now optional on writable model fields.  * Hyperlinked related fields optionally take `slug_field` and `slug_url_kwarg` arguments. @@ -96,13 +138,19 @@  * Bugfix: Support choice field in Browseable API.  * Bugfix: Related fields with `read_only=True` do not require a `queryset` argument. -## 2.0.2 +**API-incompatible changes**: Please read [this thread][2.1.0-notes] regarding the `instance` and `data` keyword args before updating to 2.1.0. + +--- + +## 2.0.x series + +### 2.0.2  **Date**: 2nd Nov 2012  * Fix issues with pk related fields in the browsable API. -## 2.0.1 +### 2.0.1  **Date**: 1st Nov 2012 @@ -110,7 +158,7 @@  * Added SlugRelatedField and ManySlugRelatedField.  * If PUT creates an instance return '201 Created', instead of '200 OK'. -## 2.0.0 +### 2.0.0  **Date**: 30th Oct 2012 @@ -119,34 +167,40 @@  --- -## 0.4.0 +## 0.4.x series + +### 0.4.0  * Supports Django 1.5.  * Fixes issues with 'HEAD' method.  * Allow views to specify template used by TemplateRenderer  * More consistent error responses  * Some serializer fixes -* Fix internet explorer ajax behaviour +* Fix internet explorer ajax behavior  * Minor xml and yaml fixes -* Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX) +* Improve setup (e.g. use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)  * Sensible absolute URL generation, not using hacky set_script_prefix -## 0.3.3 +--- + +## 0.3.x series + +### 0.3.3  * Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.  * Use `staticfiles` for css files. -  - Easier to override.  Won't conflict with customised admin styles (eg grappelli) +  - Easier to override.  Won't conflict with customized admin styles (e.g. grappelli)  * Templates are now nicely namespaced.    - Allows easier overriding.  * Drop implied 'pk' filter if last arg in urlconf is unnamed. -  - Too magical.  Explict is better than implicit. -* Saner template variable autoescaping. -* Tider setup.py +  - Too magical.  Explicit is better than implicit. +* Saner template variable auto-escaping. +* Tidier setup.py  * Updated for URLObject 2.0  * Bugfixes:    - Bug with PerUserThrottling when user contains unicode chars. -## 0.3.2 +### 0.3.2  * Bugfixes:    * Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115) @@ -158,37 +212,41 @@  * get_name, get_description become methods on the view - makes them overridable.  * Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering -## 0.3.1 +### 0.3.1  * [not documented] -## 0.3.0 +### 0.3.0  * JSONP Support  * Bugfixes, including support for latest markdown release -## 0.2.4 +--- + +## 0.2.x series + +### 0.2.4  * Fix broken IsAdminUser permission.  * OPTIONS support.  * XMLParser.  * Drop mentions of Blog, BitBucket. -## 0.2.3 +### 0.2.3  * Fix some throttling bugs.  * ``X-Throttle`` header on throttling.  * Support for nesting resources on related models. -## 0.2.2 +### 0.2.2  * Throttling support complete. -## 0.2.1 +### 0.2.1  * Couple of simple bugfixes over 0.2.0 -## 0.2.0 +### 0.2.0  * Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.    The public API has been massively cleaned up.  Expect it to be fairly stable from here on in. @@ -212,14 +270,20 @@  * The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``    You can reuse these mixin classes individually without using the ``View`` class. -## 0.1.1 +--- + +## 0.1.x series + +### 0.1.1  * Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1. -## 0.1.0 +### 0.1.0  * Initial release.  [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html +[staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag +[staticfiles13]: https://docs.djangoproject.com/en/1.3/howto/static-files/#with-a-template-tag  [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion  [announcement]: rest-framework-2-announcement.md diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index b5d37875..216ca433 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -163,9 +163,9 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam  We've reached the end of our tutorial.  If you want to get more involved in the REST framework project, here's a few places you can start: -* Contribute on [GitHub][github] by reviewing and subitting issues, and making pull requests. +* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.  * Join the [REST framework discussion group][group], and help build the community. -* Follow the author [on Twitter][twitter] and say hi. +* [Follow the author on Twitter][twitter] and say hi.  **Now go build awesome things.** diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 83a6f302..2e38d863 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,3 +1,3 @@ -__version__ = '2.1.9' +__version__ = '2.1.13'  VERSION = __version__  # synonym diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d4901437..86952fb8 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -5,6 +5,12 @@ versions of django/python, and compatibility wrappers around optional packages.  # flake8: noqa  import django +# location of patterns, url, include changes in 1.4 onwards +try: +    from django.conf.urls import patterns, url, include +except: +    from django.conf.urls.defaults import patterns, url, include +  # django-filter is optional  try:      import django_filters diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 75ce1b9f..dd90c3f8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -32,6 +32,7 @@ def is_simple_callable(obj):  class Field(object): +    read_only = True      creation_counter = 0      empty = ''      type_name = None @@ -350,7 +351,12 @@ class RelatedField(WritableField):          if self.read_only:              return -        value = data.get(field_name) +        try: +            value = data[field_name] +        except KeyError: +            if self.required: +                raise ValidationError(self.error_messages['required']) +            return          if value in (None, '') and not self.null:              raise ValidationError('Value may not be null') @@ -383,6 +389,7 @@ class ManyRelatedMixin(object):          else:              if value == ['']:                  value = [] +          into[field_name] = [self.from_native(item) for item in value] @@ -581,7 +588,7 @@ class HyperlinkedRelatedField(RelatedField):          except:              pass -        raise ValidationError('Could not resolve URL for field using view name "%s"', view_name) +        raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)      def from_native(self, value):          # Convert URL -> model instance pk @@ -680,7 +687,7 @@ class HyperlinkedIdentityField(Field):          except:              pass -        raise ValidationError('Could not resolve URL for field using view name "%s"', view_name) +        raise ValidationError('Could not resolve URL for field using view name "%s"' % view_name)  ##### Typed Fields ##### @@ -794,7 +801,7 @@ class ChoiceField(WritableField):                      if value == smart_unicode(k2):                          return True              else: -                if value == smart_unicode(k): +                if value == smart_unicode(k) or value == k:                      return True          return False diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 1edcfa5c..2700606d 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -113,6 +113,10 @@ class UpdateModelMixin(object):              slug_field = self.get_slug_field()              setattr(obj, slug_field, slug) +        # Ensure we clean the attributes so that we don't eg return integer +        # pk using a string representation, as provided by the url conf kwarg. +        obj.full_clean() +  class DestroyModelMixin(object):      """ @@ -120,6 +124,6 @@ class DestroyModelMixin(object):      Should be mixed in with `SingleObjectBaseView`.      """      def destroy(self, request, *args, **kwargs): -        self.object = self.get_object() -        self.object.delete() +        obj = self.get_object() +        obj.delete()          return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 1220bca1..a4ae717d 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -20,7 +20,7 @@ from rest_framework.utils import dict2xml  from rest_framework.utils import encoders  from rest_framework.utils.breadcrumbs import get_breadcrumbs  from rest_framework import VERSION, status -from rest_framework import serializers, parsers +from rest_framework import parsers  class BaseRenderer(object): diff --git a/rest_framework/request.py b/rest_framework/request.py index 39c64321..b7133608 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -188,6 +188,14 @@ class Request(object):              self._user, self._auth = self._authenticate()          return self._auth +    @auth.setter +    def auth(self, value): +        """ +        Sets any non-user authentication information associated with the +        request, such as an authentication token. +        """ +        self._auth = value +      def _load_data_and_files(self):          """          Parses the request content into self.DATA and self.FILES. diff --git a/rest_framework/runtests/urls.py b/rest_framework/runtests/urls.py index 4b7da787..ed5baeae 100644 --- a/rest_framework/runtests/urls.py +++ b/rest_framework/runtests/urls.py @@ -1,7 +1,7 @@  """  Blank URLConf just to keep runtests.py happy.  """ -from django.conf.urls.defaults import * +from rest_framework.compat import patterns  urlpatterns = patterns('',  ) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 9c53a17a..6eb9c3e1 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -128,17 +128,6 @@ class BaseSerializer(Field):          """          return {} -    def get_excluded_fieldnames(self): -        """ -        Returns the fieldnames that should not be validated. -        """ -        excluded_fields = list(self.opts.exclude) -        if self.opts.fields: -            for field in self.fields.keys() + self.get_default_fields().keys(): -                if field not in list(self.opts.fields) + excluded_fields: -                    excluded_fields.append(field) -        return excluded_fields -      def get_fields(self):          """          Returns the complete set of fields for the object as a dict. @@ -171,7 +160,6 @@ class BaseSerializer(Field):              for key in self.opts.exclude:                  ret.pop(key, None) -        # Initialize the fields          for key, field in ret.items():              field.initialize(parent=self, field_name=key) @@ -189,13 +177,6 @@ class BaseSerializer(Field):          if parent.opts.depth:              self.opts.depth = parent.opts.depth - 1 -        # We need to call initialize here to ensure any nested -        # serializers that will have already called initialize on their -        # descendants get updated with *their* parent. -        # We could be a bit more smart about this, but it'll do for now. -        for key, field in self.fields.items(): -            field.initialize(parent=self, field_name=key) -      #####      # Methods to convert or revert from objects <--> primitive representations. @@ -214,6 +195,7 @@ class BaseSerializer(Field):          ret.fields = {}          for field_name, field in self.fields.items(): +            field.initialize(parent=self, field_name=field_name)              key = self.get_field_key(field_name)              value = field.field_to_native(obj, field_name)              ret[key] = value @@ -227,6 +209,7 @@ class BaseSerializer(Field):          """          reverted_data = {}          for field_name, field in self.fields.items(): +            field.initialize(parent=self, field_name=field_name)              try:                  field.field_from_native(data, files, field_name, reverted_data)              except ValidationError as err: @@ -324,6 +307,9 @@ class BaseSerializer(Field):          if is_simple_callable(getattr(obj, 'all', None)):              return [self.to_native(item) for item in obj.all()] +        if obj is None: +            return None +          return self.to_native(obj)      @property @@ -407,7 +393,6 @@ class ModelSerializer(Serializer):                  field = self.get_field(model_field)              if field: -                field.initialize(parent=self, field_name=model_field.name)                  ret[model_field.name] = field          for field_name in self.opts.read_only_fields: @@ -494,6 +479,18 @@ class ModelSerializer(Serializer):          except KeyError:              return ModelField(model_field=model_field, **kwargs) +    def get_validation_exclusions(self): +        """ +        Return a list of field names to exclude from model validation. +        """ +        cls = self.opts.model +        opts = get_concrete_model(cls)._meta +        exclusions = [field.name for field in opts.fields + opts.many_to_many] +        for field_name, field in self.fields.items(): +            if field_name in exclusions and not field.read_only: +                exclusions.remove(field_name) +        return exclusions +      def restore_object(self, attrs, instance=None):          """          Restore the model instance. @@ -503,25 +500,27 @@ class ModelSerializer(Serializer):          if instance is not None:              for key, val in attrs.items():                  setattr(instance, key, val) -            return instance -        # Reverse relations -        for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model(): -            field_name = obj.field.related_query_name() -            if field_name in attrs: -                self.m2m_data[field_name] = attrs.pop(field_name) +        else: +            # Reverse relations +            for (obj, model) in self.opts.model._meta.get_all_related_m2m_objects_with_model(): +                field_name = obj.field.related_query_name() +                if field_name in attrs: +                    self.m2m_data[field_name] = attrs.pop(field_name) + +            # Forward relations +            for field in self.opts.model._meta.many_to_many: +                if field.name in attrs: +                    self.m2m_data[field.name] = attrs.pop(field.name) -        # Forward relations -        for field in self.opts.model._meta.many_to_many: -            if field.name in attrs: -                self.m2m_data[field.name] = attrs.pop(field.name) +            instance = self.opts.model(**attrs) -        instance = self.opts.model(**attrs)          try: -            instance.full_clean(exclude=list(self.get_excluded_fieldnames())) +            instance.full_clean(exclude=self.get_validation_exclusions())          except ValidationError, err:              self._errors = err.message_dict              return None +          return instance      def save(self, save_m2m=True): diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index fb0e19f0..42e49cb9 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -1,6 +1,5 @@  {% load url from future %}  {% load rest_framework %} -{% load static %}  <!DOCTYPE html>  <html>      <head> @@ -14,10 +13,10 @@          <title>{% block title %}Django REST framework{% endblock %}</title>          {% block style %} -        <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/> -        <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/> -        <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/prettify.css'/> -        <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/> +        <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/> +        <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> +        <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/> +        <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>          {% endblock %}      {% endblock %} @@ -195,10 +194,10 @@      {% endblock %}      {% block script %} -    <script src="{% get_static_prefix %}rest_framework/js/jquery-1.8.1-min.js"></script> -    <script src="{% get_static_prefix %}rest_framework/js/bootstrap.min.js"></script> -    <script src="{% get_static_prefix %}rest_framework/js/prettify-min.js"></script> -    <script src="{% get_static_prefix %}rest_framework/js/default.js"></script> +    <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script> +    <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script> +    <script src="{% static "rest_framework/js/prettify-min.js" %}"></script> +    <script src="{% static "rest_framework/js/default.js" %}"></script>      {% endblock %}    </body>  </html> diff --git a/rest_framework/templates/rest_framework/login.html b/rest_framework/templates/rest_framework/login.html index c1271399..e3f3d799 100644 --- a/rest_framework/templates/rest_framework/login.html +++ b/rest_framework/templates/rest_framework/login.html @@ -1,11 +1,10 @@  {% load url from future %} -{% load static %}  <html>      <head> -        <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap.min.css"/> -        <link rel="stylesheet" type="text/css" href="{% get_static_prefix %}rest_framework/css/bootstrap-tweaks.css"/> -        <link rel="stylesheet" type="text/css" href='{% get_static_prefix %}rest_framework/css/default.css'/> +        <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/> +        <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> +        <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>      </head>      <body class="container"> diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 4e0181ee..09c658bc 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -11,6 +11,89 @@ import string  register = template.Library() +# Note we don't use 'load staticfiles', because we need a 1.3 compatible +# version, so instead we include the `static` template tag ourselves. + +# When 1.3 becomes unsupported by REST framework, we can instead start to +# use the {% load staticfiles %} tag, remove the following code, +# and add a dependancy that `django.contrib.staticfiles` must be installed. + +# Note: We can't put this into the `compat` module because the compat import +# from rest_framework.compat import ... +# conflicts with this rest_framework template tag module. + +try:  # Django 1.5+ +    from django.contrib.staticfiles.templatetags import StaticFilesNode + +    @register.tag('static') +    def do_static(parser, token): +        return StaticFilesNode.handle_token(parser, token) + +except: +    try:  # Django 1.4 +        from django.contrib.staticfiles.storage import staticfiles_storage + +        @register.simple_tag +        def static(path): +            """ +            A template tag that returns the URL to a file +            using staticfiles' storage backend +            """ +            return staticfiles_storage.url(path) + +    except:  # Django 1.3 +        from urlparse import urljoin +        from django import template +        from django.templatetags.static import PrefixNode + +        class StaticNode(template.Node): +            def __init__(self, varname=None, path=None): +                if path is None: +                    raise template.TemplateSyntaxError( +                        "Static template nodes must be given a path to return.") +                self.path = path +                self.varname = varname + +            def url(self, context): +                path = self.path.resolve(context) +                return self.handle_simple(path) + +            def render(self, context): +                url = self.url(context) +                if self.varname is None: +                    return url +                context[self.varname] = url +                return '' + +            @classmethod +            def handle_simple(cls, path): +                return urljoin(PrefixNode.handle_simple("STATIC_URL"), path) + +            @classmethod +            def handle_token(cls, parser, token): +                """ +                Class method to parse prefix node and return a Node. +                """ +                bits = token.split_contents() + +                if len(bits) < 2: +                    raise template.TemplateSyntaxError( +                        "'%s' takes at least one argument (path to file)" % bits[0]) + +                path = parser.compile_filter(bits[1]) + +                if len(bits) >= 2 and bits[-2] == 'as': +                    varname = bits[3] +                else: +                    varname = None + +                return cls(varname, path) + +        @register.tag('static') +        def do_static_13(parser, token): +            return StaticNode.handle_token(parser, token) + +  def replace_query_param(url, key, val):      """      Given a URL and a key/val pair, set or replace an item in the query diff --git a/rest_framework/tests/authentication.py b/rest_framework/tests/authentication.py index d498ae3e..838e081b 100644 --- a/rest_framework/tests/authentication.py +++ b/rest_framework/tests/authentication.py @@ -1,15 +1,13 @@ -from django.conf.urls.defaults import patterns  from django.contrib.auth.models import User +from django.http import HttpResponse  from django.test import Client, TestCase -  from django.utils import simplejson as json -from django.http import HttpResponse -from rest_framework.views import APIView  from rest_framework import permissions -  from rest_framework.authtoken.models import Token  from rest_framework.authentication import TokenAuthentication +from rest_framework.compat import patterns +from rest_framework.views import APIView  import base64 diff --git a/rest_framework/tests/breadcrumbs.py b/rest_framework/tests/breadcrumbs.py index 647ab96d..df891683 100644 --- a/rest_framework/tests/breadcrumbs.py +++ b/rest_framework/tests/breadcrumbs.py @@ -1,5 +1,5 @@ -from django.conf.urls.defaults import patterns, url  from django.test import TestCase +from rest_framework.compat import patterns, url  from rest_framework.utils.breadcrumbs import get_breadcrumbs  from rest_framework.views import APIView diff --git a/rest_framework/tests/decorators.py b/rest_framework/tests/decorators.py index 41864d71..8079c8cb 100644 --- a/rest_framework/tests/decorators.py +++ b/rest_framework/tests/decorators.py @@ -1,7 +1,7 @@  from django.test import TestCase +from django.test.client import RequestFactory  from rest_framework import status  from rest_framework.response import Response -from django.test.client import RequestFactory  from rest_framework.renderers import JSONRenderer  from rest_framework.parsers import JSONParser  from rest_framework.authentication import BasicAuthentication diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index a8279ef2..7c24d84e 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -1,3 +1,4 @@ +from django.db import models  from django.test import TestCase  from django.test.client import RequestFactory  from django.utils import simplejson as json @@ -174,7 +175,7 @@ class TestInstanceView(TestCase):          content = {'text': 'foobar'}          request = factory.put('/1', json.dumps(content),                                content_type='application/json') -        response = self.view(request, pk=1).render() +        response = self.view(request, pk='1').render()          self.assertEquals(response.status_code, status.HTTP_200_OK)          self.assertEquals(response.data, {'id': 1, 'text': 'foobar'})          updated = self.objects.get(id=1) @@ -301,3 +302,36 @@ class TestCreateModelWithAutoNowAddField(TestCase):          self.assertEquals(response.status_code, status.HTTP_201_CREATED)          created = self.objects.get(id=1)          self.assertEquals(created.content, 'foobar') + + +# Test for particularly ugly reression with m2m in browseable API +class ClassB(models.Model): +    name = models.CharField(max_length=255) + + +class ClassA(models.Model): +    name = models.CharField(max_length=255) +    childs = models.ManyToManyField(ClassB, blank=True, null=True) + + +class ClassASerializer(serializers.ModelSerializer): +    childs = serializers.ManyPrimaryKeyRelatedField(source='childs') + +    class Meta: +        model = ClassA + + +class ExampleView(generics.ListCreateAPIView): +    serializer_class = ClassASerializer +    model = ClassA + + +class TestM2MBrowseableAPI(TestCase): +    def test_m2m_in_browseable_api(self): +        """ +        Test for particularly ugly reression with m2m in browseable API +        """ +        request = factory.get('/', HTTP_ACCEPT='text/html') +        view = ExampleView().as_view() +        response = view(request).render() +        self.assertEquals(response.status_code, status.HTTP_200_OK) diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index 4caed59e..54096206 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -1,9 +1,9 @@  from django.core.exceptions import PermissionDenied -from django.conf.urls.defaults import patterns, url  from django.http import Http404  from django.test import TestCase  from django.template import TemplateDoesNotExist, Template  import django.template.loader +from rest_framework.compat import patterns, url  from rest_framework.decorators import api_view, renderer_classes  from rest_framework.renderers import TemplateHTMLRenderer  from rest_framework.response import Response diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py index 24bf61bf..ee4d8e57 100644 --- a/rest_framework/tests/hyperlinkedserializers.py +++ b/rest_framework/tests/hyperlinkedserializers.py @@ -1,8 +1,8 @@ -from django.conf.urls.defaults import patterns, url  from django.test import TestCase  from django.test.client import RequestFactory  from django.utils import simplejson as json  from rest_framework import generics, status, serializers +from rest_framework.compat import patterns, url  from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment, Album, Photo, OptionalRelationModel  factory = RequestFactory() diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 3a939a24..0759650a 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -51,6 +51,11 @@ class RESTFrameworkModel(models.Model):          abstract = True +class HasPositiveIntegerAsChoice(RESTFrameworkModel): +    some_choices = ((1, 'A'), (2, 'B'), (3, 'C')) +    some_integer = models.PositiveIntegerField(choices=some_choices) + +  class Anchor(RESTFrameworkModel):      text = models.CharField(max_length=100, default='anchor') @@ -61,7 +66,7 @@ class BasicModel(RESTFrameworkModel):  class SlugBasedModel(RESTFrameworkModel):      text = models.CharField(max_length=100) -    slug = models.SlugField(max_length=32, blank=True) +    slug = models.SlugField(max_length=32)  class DefaultValueModel(RESTFrameworkModel): diff --git a/rest_framework/tests/modelviews.py b/rest_framework/tests/modelviews.py index 1f8468e8..f12e3b97 100644 --- a/rest_framework/tests/modelviews.py +++ b/rest_framework/tests/modelviews.py @@ -1,4 +1,4 @@ -# from django.conf.urls.defaults import patterns, url +# from rest_framework.compat import patterns, url  # from django.forms import ModelForm  # from django.contrib.auth.models import Group, User  # from rest_framework.resources import ModelResource diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 3062007d..81d297a1 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -4,7 +4,7 @@ from django.core.paginator import Paginator  from django.test import TestCase  from django.test.client import RequestFactory  from django.utils import unittest -from rest_framework import generics, status, pagination, filters +from rest_framework import generics, status, pagination, filters, serializers  from rest_framework.compat import django_filters  from rest_framework.tests.models import BasicModel, FilterableItem @@ -148,6 +148,11 @@ class IntegrationTestPaginationAndFiltering(TestCase):          self.assertEquals(response.data['previous'], None) +class PassOnContextPaginationSerializer(pagination.PaginationSerializer): +    class Meta: +        object_serializer_class = serializers.Serializer + +  class UnitTestPagination(TestCase):      """      Unit tests for pagination of primitive objects. @@ -172,6 +177,15 @@ class UnitTestPagination(TestCase):          self.assertEquals(serializer.data['previous'], '?page=2')          self.assertEquals(serializer.data['results'], self.objects[20:]) +    def test_context_available_in_result(self): +        """ +        Ensure context gets passed through to the object serializer. +        """ +        serializer = PassOnContextPaginationSerializer(self.first_page) +        serializer.data +        results = serializer.fields[serializer.results_field] +        self.assertTrue(serializer.context is results.context) +  class TestUnpaginated(TestCase):      """ @@ -236,3 +250,32 @@ class TestCustomPaginateByParam(TestCase):          response = self.view(request).render()          self.assertEquals(response.data['count'], 13)          self.assertEquals(response.data['results'], self.data[:5]) + + +class CustomField(serializers.Field): +    def to_native(self, value): +        if not 'view' in self.context: +            raise RuntimeError("context isn't getting passed into custom field") +        return "value" + + +class BasicModelSerializer(serializers.Serializer): +    text = CustomField() + + +class TestContextPassedToCustomField(TestCase): +    def setUp(self): +        BasicModel.objects.create(text='ala ma kota') + +    def test_with_pagination(self): +        class ListView(generics.ListCreateAPIView): +            model = BasicModel +            serializer_class = BasicModelSerializer +            paginate_by = 1 + +        self.view = ListView.as_view() +        request = factory.get('/') +        response = self.view(request).render() + +        self.assertEquals(response.status_code, status.HTTP_200_OK) + diff --git a/rest_framework/tests/relations_hyperlink.py b/rest_framework/tests/relations_hyperlink.py new file mode 100644 index 00000000..53ce0074 --- /dev/null +++ b/rest_framework/tests/relations_hyperlink.py @@ -0,0 +1,358 @@ +from django.db import models +from django.test import TestCase +from rest_framework import serializers +from rest_framework.compat import patterns, url + + +def dummy_view(request, pk): +    pass + +urlpatterns = patterns('', +    url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'), +    url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'), +    url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'), +    url(r'^foreignkeytarget/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeytarget-detail'), +    url(r'^nullableforeignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='nullableforeignkeysource-detail'), +) + + +# 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.HyperlinkedModelSerializer): +    sources = serializers.ManyHyperlinkedRelatedField(view_name='manytomanysource-detail') + +    class Meta: +        model = ManyToManyTarget + + +class ManyToManySourceSerializer(serializers.HyperlinkedModelSerializer): +    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.HyperlinkedModelSerializer): +    sources = serializers.ManyHyperlinkedRelatedField(view_name='foreignkeysource-detail', read_only=True) + +    class Meta: +        model = ForeignKeyTarget + + +class ForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): +    class Meta: +        model = ForeignKeySource + + +# Nullable ForeignKey + +class NullableForeignKeySource(models.Model): +    name = models.CharField(max_length=100) +    target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, +                               related_name='nullable_sources') + + +class NullableForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): +    class Meta: +        model = NullableForeignKeySource + + +# TODO: Add test that .data cannot be accessed prior to .is_valid + +class HyperlinkedManyToManyTests(TestCase): +    urls = 'rest_framework.tests.relations_hyperlink' + +    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(queryset) +        expected = [ +                {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']}, +                {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, +                {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} +        ] +        self.assertEquals(serializer.data, expected) + +    def test_reverse_many_to_many_retrieve(self): +        queryset = ManyToManyTarget.objects.all() +        serializer = ManyToManyTargetSerializer(queryset) +        expected = [ +            {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, +            {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, +            {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']} +        ] +        self.assertEquals(serializer.data, expected) + +    def test_many_to_many_update(self): +        data = {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} +        instance = ManyToManySource.objects.get(pk=1) +        serializer = ManyToManySourceSerializer(instance, data=data) +        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(queryset) +        expected = [ +                {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, +                {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, +                {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']} +        ] +        self.assertEquals(serializer.data, expected) + +    def test_reverse_many_to_many_update(self): +        data = {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']} +        instance = ManyToManyTarget.objects.get(pk=1) +        serializer = ManyToManyTargetSerializer(instance, data=data) +        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(queryset) +        expected = [ +            {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/']}, +            {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, +            {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']} + +        ] +        self.assertEquals(serializer.data, expected) + +    def test_many_to_many_create(self): +        data = {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} +        serializer = ManyToManySourceSerializer(data=data) +        self.assertTrue(serializer.is_valid()) +        obj = serializer.save() +        self.assertEquals(serializer.data, data) +        self.assertEqual(obj.name, u'source-4') + +        # Ensure source 4 is added, and everything else is as expected +        queryset = ManyToManySource.objects.all() +        serializer = ManyToManySourceSerializer(queryset) +        expected = [ +            {'url': '/manytomanysource/1/', 'name': u'source-1', 'targets': ['/manytomanytarget/1/']}, +            {'url': '/manytomanysource/2/', 'name': u'source-2', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/']}, +            {'url': '/manytomanysource/3/', 'name': u'source-3', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/2/', '/manytomanytarget/3/']}, +            {'url': '/manytomanysource/4/', 'name': u'source-4', 'targets': ['/manytomanytarget/1/', '/manytomanytarget/3/']} +        ] +        self.assertEquals(serializer.data, expected) + +    def test_reverse_many_to_many_create(self): +        data = {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} +        serializer = ManyToManyTargetSerializer(data=data) +        self.assertTrue(serializer.is_valid()) +        obj = serializer.save() +        self.assertEquals(serializer.data, data) +        self.assertEqual(obj.name, u'target-4') + +        # Ensure target 4 is added, and everything else is as expected +        queryset = ManyToManyTarget.objects.all() +        serializer = ManyToManyTargetSerializer(queryset) +        expected = [ +            {'url': '/manytomanytarget/1/', 'name': u'target-1', 'sources': ['/manytomanysource/1/', '/manytomanysource/2/', '/manytomanysource/3/']}, +            {'url': '/manytomanytarget/2/', 'name': u'target-2', 'sources': ['/manytomanysource/2/', '/manytomanysource/3/']}, +            {'url': '/manytomanytarget/3/', 'name': u'target-3', 'sources': ['/manytomanysource/3/']}, +            {'url': '/manytomanytarget/4/', 'name': u'target-4', 'sources': ['/manytomanysource/1/', '/manytomanysource/3/']} +        ] +        self.assertEquals(serializer.data, expected) + + +class HyperlinkedForeignKeyTests(TestCase): +    urls = 'rest_framework.tests.relations_hyperlink' + +    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(queryset) +        expected = [ +            {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, +            {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, +            {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'} +        ] +        self.assertEquals(serializer.data, expected) + +    def test_reverse_foreign_key_retrieve(self): +        queryset = ForeignKeyTarget.objects.all() +        serializer = ForeignKeyTargetSerializer(queryset) +        expected = [ +            {'url': '/foreignkeytarget/1/', 'name': u'target-1', 'sources': ['/foreignkeysource/1/', '/foreignkeysource/2/', '/foreignkeysource/3/']}, +            {'url': '/foreignkeytarget/2/', 'name': u'target-2', 'sources': []}, +        ] +        self.assertEquals(serializer.data, expected) + +    def test_foreign_key_update(self): +        data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'} +        instance = ForeignKeySource.objects.get(pk=1) +        serializer = ForeignKeySourceSerializer(instance, data=data) +        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(queryset) +        expected = [ +            {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/2/'}, +            {'url': '/foreignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, +            {'url': '/foreignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'} +        ] +        self.assertEquals(serializer.data, expected) + +    def test_foreign_key_update_with_invalid_null(self): +        data = {'url': '/foreignkeysource/1/', 'name': u'source-1', 'target': None} +        instance = ForeignKeySource.objects.get(pk=1) +        serializer = ForeignKeySourceSerializer(instance, data=data) +        self.assertFalse(serializer.is_valid()) +        self.assertEquals(serializer.errors, {'target': [u'Value may not be null']}) + + +class HyperlinkedNullableForeignKeyTests(TestCase): +    urls = 'rest_framework.tests.relations_hyperlink' + +    def setUp(self): +        target = ForeignKeyTarget(name='target-1') +        target.save() +        for idx in range(1, 4): +            source = NullableForeignKeySource(name='source-%d' % idx, target=target) +            source.save() + +    def test_foreign_key_create_with_valid_null(self): +        data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} +        serializer = NullableForeignKeySourceSerializer(data=data) +        self.assertTrue(serializer.is_valid()) +        obj = serializer.save() +        self.assertEquals(serializer.data, data) +        self.assertEqual(obj.name, u'source-4') + +        # Ensure source 4 is created, and everything else is as expected +        queryset = NullableForeignKeySource.objects.all() +        serializer = NullableForeignKeySourceSerializer(queryset) +        expected = [ +            {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, +            {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, +            {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, +            {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} +        ] +        self.assertEquals(serializer.data, expected) + +    def test_foreign_key_create_with_valid_emptystring(self): +        """ +        The emptystring should be interpreted as null in the context +        of relationships. +        """ +        data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': ''} +        expected_data = {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} +        serializer = NullableForeignKeySourceSerializer(data=data) +        self.assertTrue(serializer.is_valid()) +        obj = serializer.save() +        self.assertEquals(serializer.data, expected_data) +        self.assertEqual(obj.name, u'source-4') + +        # Ensure source 4 is created, and everything else is as expected +        queryset = NullableForeignKeySource.objects.all() +        serializer = NullableForeignKeySourceSerializer(queryset) +        expected = [ +            {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': '/foreignkeytarget/1/'}, +            {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, +            {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, +            {'url': '/nullableforeignkeysource/4/', 'name': u'source-4', 'target': None} +        ] +        self.assertEquals(serializer.data, expected) + +    def test_foreign_key_update_with_valid_null(self): +        data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None} +        instance = NullableForeignKeySource.objects.get(pk=1) +        serializer = NullableForeignKeySourceSerializer(instance, data=data) +        self.assertTrue(serializer.is_valid()) +        self.assertEquals(serializer.data, data) +        serializer.save() + +        # Ensure source 1 is updated, and everything else is as expected +        queryset = NullableForeignKeySource.objects.all() +        serializer = NullableForeignKeySourceSerializer(queryset) +        expected = [ +            {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}, +            {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, +            {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/1/'}, +        ] +        self.assertEquals(serializer.data, expected) + +    def test_foreign_key_update_with_valid_emptystring(self): +        """ +        The emptystring should be interpreted as null in the context +        of relationships. +        """ +        data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': ''} +        expected_data = {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None} +        instance = NullableForeignKeySource.objects.get(pk=1) +        serializer = NullableForeignKeySourceSerializer(instance, data=data) +        self.assertTrue(serializer.is_valid()) +        self.assertEquals(serializer.data, expected_data) +        serializer.save() + +        # Ensure source 1 is updated, and everything else is as expected +        queryset = NullableForeignKeySource.objects.all() +        serializer = NullableForeignKeySourceSerializer(queryset) +        expected = [ +            {'url': '/nullableforeignkeysource/1/', 'name': u'source-1', 'target': None}, +            {'url': '/nullableforeignkeysource/2/', 'name': u'source-2', 'target': '/foreignkeytarget/1/'}, +            {'url': '/nullableforeignkeysource/3/', 'name': u'source-3', 'target': '/foreignkeytarget/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(instance, data=data) +    #     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(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/relations_nested.py b/rest_framework/tests/relations_nested.py new file mode 100644 index 00000000..b1147378 --- /dev/null +++ b/rest_framework/tests/relations_nested.py @@ -0,0 +1,102 @@ +from django.db import models +from django.test import TestCase +from rest_framework import serializers + + +# 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 ForeignKeySourceSerializer(serializers.ModelSerializer): +    class Meta: +        depth = 1 +        model = ForeignKeySource + + +class FlatForeignKeySourceSerializer(serializers.ModelSerializer): +    class Meta: +        model = ForeignKeySource + + +class ForeignKeyTargetSerializer(serializers.ModelSerializer): +    sources = FlatForeignKeySourceSerializer() + +    class Meta: +        model = ForeignKeyTarget + + +# Nullable ForeignKey + +class NullableForeignKeySource(models.Model): +    name = models.CharField(max_length=100) +    target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, +                               related_name='nullable_sources') + + +class NullableForeignKeySourceSerializer(serializers.ModelSerializer): +    class Meta: +        depth = 1 +        model = NullableForeignKeySource + + +class ReverseForeignKeyTests(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(queryset) +        expected = [ +            {'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}}, +            {'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}}, +            {'id': 3, 'name': u'source-3', 'target': {'id': 1, 'name': u'target-1'}}, +        ] +        self.assertEquals(serializer.data, expected) + +    def test_reverse_foreign_key_retrieve(self): +        queryset = ForeignKeyTarget.objects.all() +        serializer = ForeignKeyTargetSerializer(queryset) +        expected = [ +            {'id': 1, 'name': u'target-1', 'sources': [ +                {'id': 1, 'name': u'source-1', 'target': 1}, +                {'id': 2, 'name': u'source-2', 'target': 1}, +                {'id': 3, 'name': u'source-3', 'target': 1}, +            ]}, +            {'id': 2, 'name': u'target-2', 'sources': [ +            ]} +        ] +        self.assertEquals(serializer.data, expected) + + +class NestedNullableForeignKeyTests(TestCase): +    def setUp(self): +        target = ForeignKeyTarget(name='target-1') +        target.save() +        for idx in range(1, 4): +            if idx == 3: +                target = None +            source = NullableForeignKeySource(name='source-%d' % idx, target=target) +            source.save() + +    def test_foreign_key_retrieve_with_null(self): +        queryset = NullableForeignKeySource.objects.all() +        serializer = NullableForeignKeySourceSerializer(queryset) +        expected = [ +            {'id': 1, 'name': u'source-1', 'target': {'id': 1, 'name': u'target-1'}}, +            {'id': 2, 'name': u'source-2', 'target': {'id': 1, 'name': u'target-1'}}, +            {'id': 3, 'name': u'source-3', 'target': None}, +        ] +        self.assertEquals(serializer.data, expected) diff --git a/rest_framework/tests/pk_relations.py b/rest_framework/tests/relations_pk.py index e3360939..e3360939 100644 --- a/rest_framework/tests/pk_relations.py +++ b/rest_framework/tests/relations_pk.py diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 9be4b114..c1b4e624 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -1,13 +1,12 @@  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  from rest_framework import status, permissions -from rest_framework.compat import yaml +from rest_framework.compat import yaml, patterns, url, include  from rest_framework.response import Response  from rest_framework.views import APIView  from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ @@ -444,19 +443,19 @@ class CacheRenderTest(TestCase):              return          if state == None:              return -        if isinstance(state,tuple): -            if not isinstance(state[0],dict): -                state=state[1] +        if isinstance(state, tuple): +            if not isinstance(state[0], dict): +                state = state[1]              else: -                state=state[0].update(state[1]) +                state = state[0].update(state[1])          result = {}          for i in state:              try: -                pickle.dumps(state[i],protocol=2) +                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) +                    result[i] = cls._get_pickling_errors(state[i], seen)          return result      def http_resp(self, http_method, url): diff --git a/rest_framework/tests/request.py b/rest_framework/tests/request.py index 2850992d..1f05ff8f 100644 --- a/rest_framework/tests/request.py +++ b/rest_framework/tests/request.py @@ -1,16 +1,15 @@  """  Tests for content parsing, and form-overloaded content parsing.  """ -from django.conf.urls.defaults import patterns  from django.contrib.auth.models import User  from django.contrib.auth import authenticate, login, logout  from django.contrib.sessions.middleware import SessionMiddleware  from django.test import TestCase, Client +from django.test.client import RequestFactory  from django.utils import simplejson as json -  from rest_framework import status  from rest_framework.authentication import SessionAuthentication -from django.test.client import RequestFactory +from rest_framework.compat import patterns  from rest_framework.parsers import (      BaseParser,      FormParser, @@ -304,3 +303,11 @@ class TestUserSetter(TestCase):          self.assertFalse(self.request.user.is_anonymous())          logout(self.request)          self.assertTrue(self.request.user.is_anonymous()) + + +class TestAuthSetter(TestCase): + +    def test_auth_can_be_set(self): +        request = Request(factory.get('/')) +        request.auth = 'DUMMY' +        self.assertEqual(request.auth, 'DUMMY') diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py index d7b75450..875f4d42 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -1,8 +1,5 @@ -import unittest - -from django.conf.urls.defaults import patterns, url, include  from django.test import TestCase - +from rest_framework.compat import patterns, url, include  from rest_framework.response import Response  from rest_framework.views import APIView  from rest_framework import status diff --git a/rest_framework/tests/reverse.py b/rest_framework/tests/reverse.py index fd9a7d64..8c86e1fb 100644 --- a/rest_framework/tests/reverse.py +++ b/rest_framework/tests/reverse.py @@ -1,6 +1,6 @@ -from django.conf.urls.defaults import patterns, url  from django.test import TestCase  from django.test.client import RequestFactory +from rest_framework.compat import patterns, url  from rest_framework.reverse import reverse  factory = RequestFactory() diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 7b05d259..701b2f47 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -2,7 +2,7 @@ import datetime  import pickle  from django.test import TestCase  from rest_framework import serializers -from rest_framework.tests.models import (Album, ActionItem, Anchor, BasicModel, +from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,      BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel,      ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) @@ -69,6 +69,11 @@ class AlbumsSerializer(serializers.ModelSerializer):          model = Album          fields = ['title']  # lists are also valid options +class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): +    class Meta: +        model = HasPositiveIntegerAsChoice +        fields = ['some_integer'] +  class BasicTests(TestCase):      def setUp(self): @@ -285,6 +290,12 @@ class ValidationTests(TestCase):          self.assertEquals(serializer.errors, {'info': [u'Ensure this value has at most 12 characters (it has 13).']}) +class PositiveIntegerAsChoiceTests(TestCase): +    def test_positive_integer_in_json_is_correctly_parsed(self): +        data = {'some_integer':1} +        serializer = PositiveIntegerAsChoiceSerializer(data=data) +        self.assertEquals(serializer.is_valid(), True) +  class ModelValidationTests(TestCase):      def test_validate_unique(self):          """ @@ -297,6 +308,38 @@ class ModelValidationTests(TestCase):          self.assertFalse(second_serializer.is_valid())          self.assertEqual(second_serializer.errors,  {'title': [u'Album with this Title already exists.']}) +    def test_foreign_key_with_partial(self): +        """ +        Test ModelSerializer validation with partial=True + +        Specifically test foreign key validation. +        """ + +        album = Album(title='test') +        album.save() + +        class PhotoSerializer(serializers.ModelSerializer): +            class Meta: +                model = Photo + +        photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk}) +        self.assertTrue(photo_serializer.is_valid()) +        photo = photo_serializer.save() + +        # Updating only the album (foreign key) +        photo_serializer = PhotoSerializer(instance=photo, data={'album': album.pk}, partial=True) +        self.assertTrue(photo_serializer.is_valid()) +        self.assertTrue(photo_serializer.save()) + +        # Updating only the description +        photo_serializer = PhotoSerializer(instance=photo, +                                           data={'description': 'new'}, +                                           partial=True) + +        self.assertTrue(photo_serializer.is_valid()) +        self.assertTrue(photo_serializer.save()) + +  class RegexValidationTest(TestCase):      def test_create_failed(self): @@ -688,6 +731,10 @@ class BlankFieldTests(TestCase):          serializer = self.model_serializer_class(data=self.data)          self.assertEquals(serializer.is_valid(), True) +    def test_create_model_null_field(self): +        serializer = self.model_serializer_class(data={'title': None}) +        self.assertEquals(serializer.is_valid(), True) +      def test_create_not_blank_field(self):          """          Test to ensure blank data in a field not marked as blank=True diff --git a/rest_framework/tests/testcases.py b/rest_framework/tests/testcases.py index c90224aa..97f492ff 100644 --- a/rest_framework/tests/testcases.py +++ b/rest_framework/tests/testcases.py @@ -6,6 +6,7 @@ from django.test import TestCase  NO_SETTING = ('!', None) +  class TestSettingsManager(object):      """      A class which can modify some Django settings temporarily for a @@ -19,7 +20,7 @@ class TestSettingsManager(object):          self._original_settings = {}      def set(self, **kwargs): -        for k,v in kwargs.iteritems(): +        for k, v in kwargs.iteritems():              self._original_settings.setdefault(k, getattr(settings, k,                                                            NO_SETTING))              setattr(settings, k, v) @@ -31,7 +32,7 @@ class TestSettingsManager(object):          call_command('syncdb', verbosity=0)      def revert(self): -        for k,v in self._original_settings.iteritems(): +        for k, v in self._original_settings.iteritems():              if v == NO_SETTING:                  delattr(settings, k)              else: @@ -57,6 +58,7 @@ class SettingsTestCase(TestCase):      def tearDown(self):          self.settings_manager.revert() +  class TestModelsTestCase(SettingsTestCase):      def setUp(self, *args, **kwargs):          installed_apps = tuple(settings.INSTALLED_APPS) + ('rest_framework.tests',) diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 0ad926fa..143928c9 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,4 +1,4 @@ -from django.conf.urls.defaults import url +from rest_framework.compat import url  from rest_framework.settings import api_settings diff --git a/rest_framework/urls.py b/rest_framework/urls.py index bcdc23e7..fbe4bc07 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -12,7 +12,7 @@ your authentication settings include `SessionAuthentication`.          url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))      )  """ -from django.conf.urls.defaults import patterns, url +from rest_framework.compat import patterns, url  template_name = {'template_name': 'rest_framework/login.html'} @@ -12,12 +12,12 @@ deps = https://github.com/django/django/zipball/master  [testenv:py2.7-django1.4]  basepython = python2.7 -deps = django==1.4.1 +deps = django==1.4.3         django-filter==0.5.4  [testenv:py2.7-django1.3]  basepython = python2.7 -deps = django==1.3.3 +deps = django==1.3.5         django-filter==0.5.4  [testenv:py2.6-django1.5] @@ -27,10 +27,10 @@ deps = https://github.com/django/django/zipball/master  [testenv:py2.6-django1.4]  basepython = python2.6 -deps = django==1.4.1 +deps = django==1.4.3         django-filter==0.5.4  [testenv:py2.6-django1.3]  basepython = python2.6 -deps = django==1.3.3 +deps = django==1.3.5         django-filter==0.5.4 | 
