diff --git a/.travis.yml b/.travis.yml index 0c9b44553..b2da9e816 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,10 +8,10 @@ python: - "3.4" env: - - DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/" - - DJANGO="django==1.6.3" - - DJANGO="django==1.5.6" - - DJANGO="django==1.4.11" + - DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/" + - DJANGO="django==1.6.5" + - DJANGO="django==1.5.8" + - DJANGO="django==1.4.13" - DJANGO="django==1.3.7" install: @@ -24,7 +24,7 @@ install: - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi" - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" + - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7.b4/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - export PYTHONPATH=. script: @@ -33,16 +33,16 @@ script: matrix: exclude: - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/" + env: DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/" - python: "3.2" - env: DJANGO="django==1.4.11" + env: DJANGO="django==1.4.13" - python: "3.2" env: DJANGO="django==1.3.7" - python: "3.3" - env: DJANGO="django==1.4.11" + env: DJANGO="django==1.4.13" - python: "3.3" env: DJANGO="django==1.3.7" - python: "3.4" - env: DJANGO="django==1.4.11" + env: DJANGO="django==1.4.13" - python: "3.4" env: DJANGO="django==1.3.7" diff --git a/README.md b/README.md index 73e4b13fc..eea002b4a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ +--- + +#### Django REST framework 3 - Kickstarter announcement! + +We are currently running a Kickstarter campaign to help fund the development of Django REST framework 3. + +If you want to help drive sustainable open-source development forward, then **please check out [the Kickstarter project](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding us.** + +--- + # Django REST framework [![build-status-image]][travis] @@ -136,6 +146,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 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=master [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [twitter]: https://twitter.com/_tomchristie diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 88a7a0119..1cb37d67f 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -119,7 +119,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401 This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients. -To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting: +To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting: INSTALLED_APPS = ( ... diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 67fa65d2d..cebfaac92 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -184,7 +184,9 @@ Corresponds to `django.db.models.fields.SlugField`. ## ChoiceField -A field that can accept a value out of a limited set of choices. +A field that can accept a value out of a limited set of choices. Optionally takes a `blank_display_value` parameter that customizes the display value of an empty choice. + +**Signature:** `ChoiceField(choices=(), blank_display_value=None)` ## EmailField @@ -355,9 +357,16 @@ The following third party packages are also available. The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type. +## DRF Extra Fields + +The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes. + + + [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [iso8601]: http://www.w3.org/TR/NOTE-datetime [drf-compound-fields]: http://drf-compound-fields.readthedocs.org +[drf-extra-fields]: https://github.com/Hipo/drf-extra-fields \ No newline at end of file diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 6a8a267b2..ec5ab61fe 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -199,8 +199,7 @@ This enables us to make queries like: http://example.com/api/products?manufacturer__name=foo -This is nice, but it shows underlying model structure in REST API, which may -be undesired, but you can use: +This is nice, but it exposes the Django's double underscore convention as part of the API. If you instead want to explicitly name the filter argument you can instead explicitly include it on the `FilterSet` class: import django_filters from myapp.models import Product @@ -208,7 +207,6 @@ be undesired, but you can use: from rest_framework import generics class ProductFilter(django_filters.FilterSet): - manufacturer = django_filters.CharFilter(name="manufacturer__name") class Meta: diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 7d06f246c..bb748981e 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -187,7 +187,7 @@ Remember that the `pre_save()` method is not called by `GenericAPIView` itself, You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`. * `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys. -* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance. +* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False, allow_add_remove=False)` - Returns a serializer instance. * `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data. * `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view. * `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset. diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 50f669a2d..c44b22de8 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -36,6 +36,12 @@ For example: self.check_object_permissions(self.request, obj) return obj +#### Limitations of object level permissions + +For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects. + +Often when you're using object level permissions you'll also want to [filter the queryset][filtering] appropriately, to ensure that users only have visibility onto instances that they are permitted to view. + ## Setting the permission policy The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example. @@ -237,6 +243,7 @@ The [REST Condition][rest-condition] package is another extension for building c [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md +[filtering]: filtering.md [contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions [objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions [guardian]: https://github.com/lukaszb/django-guardian diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 7efc140a5..64f05af39 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -179,7 +179,16 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an app.router.register_model(MyModel) +## DRF-extensions + +The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names]. + [cite]: http://guides.rubyonrails.org/routing.html [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db]: http://wq.io/wq.db [wq.db-router]: http://wq.io/docs/app.py +[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ +[drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers +[drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes +[drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers +[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name \ No newline at end of file diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 0044f0701..cedf1ff7b 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -73,8 +73,8 @@ Sometimes when serializing objects, you may not want to represent everything exa If you need to customize the serialized value of a particular field, you can do this by creating a `transform_` method. For example if you needed to render some markdown from a text field: - description = serializers.TextField() - description_html = serializers.TextField(source='description', read_only=True) + description = serializers.CharField() + description_html = serializers.CharField(source='description', read_only=True) def transform_description_html(self, obj, value): from django.contrib.markup.templatetags.markup import markdown diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 23b16575f..b3085f75c 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -137,7 +137,7 @@ The `@action` and `@link` decorators can additionally take extra arguments that def set_password(self, request, pk=None): ... -The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example: +The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: @action(methods=['POST', 'DELETE']) def unset_password(self, request, pk=None): diff --git a/docs/index.md b/docs/index.md index 2a4ad8859..d9c686c4a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,6 +9,14 @@ --- +#### Django REST framework 3 - Kickstarter announcement! + +We are currently running a Kickstarter campaign to help fund the development of Django REST framework 3. + +If you want to help drive sustainable open-source development **please [check out the Kickstarter project](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding us.** + +--- +

@@ -106,6 +121,7 @@
  • 2.0 Announcement
  • 2.2 Announcement
  • 2.3 Announcement
  • +
  • Kickstarter Announcement
  • Release Notes
  • Credits
  • @@ -169,11 +185,9 @@
    @@ -199,6 +213,7 @@ + """) else: output = output.replace('{{ ad_block }}', '') diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2d76b55d5..01036cefa 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _ """ __title__ = 'Django REST framework' -__version__ = '2.3.13' +__version__ = '2.3.14' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2014 Tom Christie' diff --git a/rest_framework/compat.py b/rest_framework/compat.py index d155f5542..fdf12448a 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -51,6 +51,7 @@ except ImportError: # guardian is optional try: import guardian + import guardian.shortcuts # Fixes #1624 except ImportError: guardian = None diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3a5b644f0..cda1748d0 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -187,7 +187,7 @@ class Field(object): def field_to_native(self, obj, field_name): """ - Given and object and a field name, returns the value that should be + Given an object and a field name, returns the value that should be serialized for that field. """ if obj is None: @@ -475,8 +475,12 @@ class CharField(WritableField): self.validators.append(validators.MaxLengthValidator(max_length)) def from_native(self, value): - if isinstance(value, six.string_types) or value is None: + if isinstance(value, six.string_types): return value + + if value is None: + return '' + return smart_text(value) @@ -507,7 +511,7 @@ class SlugField(CharField): class ChoiceField(WritableField): type_name = 'ChoiceField' - type_label = 'multiple choice' + type_label = 'choice' form_field_class = forms.ChoiceField widget = widgets.Select default_error_messages = { @@ -515,12 +519,16 @@ class ChoiceField(WritableField): 'the available choices.'), } - def __init__(self, choices=(), *args, **kwargs): + def __init__(self, choices=(), blank_display_value=None, *args, **kwargs): self.empty = kwargs.pop('empty', '') super(ChoiceField, self).__init__(*args, **kwargs) self.choices = choices if not self.required: - self.choices = BLANK_CHOICE_DASH + self.choices + if blank_display_value is None: + blank_choice = BLANK_CHOICE_DASH + else: + blank_choice = [('', blank_display_value)] + self.choices = blank_choice + self.choices def _get_choices(self): return self._choices @@ -1024,9 +1032,9 @@ class SerializerMethodField(Field): A field that gets its value by calling a method on the serializer it's attached to. """ - def __init__(self, method_name): + def __init__(self, method_name, *args, **kwargs): self.method_name = method_name - super(SerializerMethodField, self).__init__() + super(SerializerMethodField, self).__init__(*args, **kwargs) def field_to_native(self, obj, field_name): value = getattr(self.parent, self.method_name)(obj) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7bac510f7..7fc9db364 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -90,8 +90,8 @@ class GenericAPIView(views.APIView): 'view': self } - def get_serializer(self, instance=None, data=None, - files=None, many=False, partial=False): + def get_serializer(self, instance=None, data=None, files=None, many=False, + partial=False, allow_add_remove=False): """ Return the serializer instance that should be used for validating and deserializing input, and for serializing output. @@ -99,7 +99,9 @@ class GenericAPIView(views.APIView): serializer_class = self.get_serializer_class() context = self.get_serializer_context() return serializer_class(instance, data=data, files=files, - many=many, partial=partial, context=context) + many=many, partial=partial, + allow_add_remove=allow_add_remove, + context=context) def get_pagination_serializer(self, page): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8aedfbdfb..ac86d5aee 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -22,6 +22,7 @@ from django.db import models from django.forms import widgets from django.utils.datastructures import SortedDict from django.utils.translation import ugettext_lazy as _ +from django.core.exceptions import ObjectDoesNotExist from rest_framework.compat import get_concrete_model, six from rest_framework.settings import api_settings @@ -33,8 +34,8 @@ from rest_framework.settings import api_settings # This helps keep the separation between model fields, form fields, and # serializer fields more explicit. -from rest_framework.relations import * -from rest_framework.fields import * +from rest_framework.relations import * # NOQA +from rest_framework.fields import * # NOQA def _resolve_model(obj): @@ -49,7 +50,7 @@ def _resolve_model(obj): String representations should have the format: 'appname.ModelName' """ - if type(obj) == str and len(obj.split('.')) == 2: + if isinstance(obj, six.string_types) and len(obj.split('.')) == 2: app_name, model_name = obj.split('.') return models.get_model(app_name, model_name) elif inspect.isclass(obj) and issubclass(obj, models.Model): @@ -345,7 +346,7 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): if field.read_only and obj is None: - continue + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) @@ -759,9 +760,9 @@ class ModelSerializer(Serializer): field.read_only = True ret[accessor_name] = field - + # Ensure that 'read_only_fields' is an iterable - assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have been specified # in the `read_only_fields` option @@ -776,10 +777,10 @@ class ModelSerializer(Serializer): "on serializer '%s'." % (field_name, self.__class__.__name__)) ret[field_name].read_only = True - + # Ensure that 'write_only_fields' is an iterable - assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' - + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' + for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( "field '%s' on serializer '%s' specified in " @@ -790,7 +791,7 @@ class ModelSerializer(Serializer): "Non-existant field '%s' specified in `write_only_fields` " "on serializer '%s'." % (field_name, self.__class__.__name__)) - ret[field_name].write_only = True + ret[field_name].write_only = True return ret diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index dff176d62..a155d8d25 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -122,7 +122,7 @@ def optional_login(request): except NoReverseMatch: return '' - snippet = "Log in" % (login_url, request.path) + snippet = "Log in" % (login_url, escape(request.path)) return snippet @@ -136,7 +136,7 @@ def optional_logout(request): except NoReverseMatch: return '' - snippet = "Log out" % (logout_url, request.path) + snippet = "Log out" % (logout_url, escape(request.path)) return snippet diff --git a/rest_framework/test.py b/rest_framework/test.py index df5a5b3b3..284bcee07 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory): """ if not data: - return ('', None) + return ('', content_type) assert format is None or content_type is None, ( 'You may not set both `format` and `content_type`.' diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index e171d3bd9..fba3f8f7c 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -105,6 +105,7 @@ class Album(RESTFrameworkModel): title = models.CharField(max_length=100, unique=True) ref = models.CharField(max_length=10, unique=True, null=True, blank=True) + class Photo(RESTFrameworkModel): description = models.TextField() album = models.ForeignKey(Album) @@ -112,7 +113,8 @@ class Photo(RESTFrameworkModel): # Model for issue #324 class BlankFieldModel(RESTFrameworkModel): - title = models.CharField(max_length=100, blank=True, null=False) + title = models.CharField(max_length=100, blank=True, null=False, + default="title") # Model for issue #380 diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 3ae1c4388..b04b947f2 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -717,6 +717,15 @@ class ChoiceFieldTests(TestCase): f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) + def test_blank_choice_display(self): + blank = 'No Preference' + f = serializers.ChoiceField( + required=False, + choices=SAMPLE_CHOICES, + blank_display_value=blank, + ) + self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES) + def test_invalid_choice_model(self): s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'}) self.assertFalse(s.is_valid()) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index fa4e2cf8e..0428e7176 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1236,6 +1236,9 @@ class BlankFieldTests(TestCase): def test_create_model_null_field(self): serializer = self.model_serializer_class(data={'title': None}) self.assertEqual(serializer.is_valid(), True) + serializer.save() + self.assertIsNot(serializer.object.pk, None) + self.assertEqual(serializer.object.title, '') def test_create_not_blank_field(self): """ diff --git a/rest_framework/tests/test_serializers.py b/rest_framework/tests/test_serializers.py index 082a400ca..120510ace 100644 --- a/rest_framework/tests/test_serializers.py +++ b/rest_framework/tests/test_serializers.py @@ -3,6 +3,7 @@ from django.test import TestCase from rest_framework.serializers import _resolve_model from rest_framework.tests.models import BasicModel +from rest_framework.compat import six class ResolveModelTests(TestCase): @@ -19,6 +20,10 @@ class ResolveModelTests(TestCase): resolved_model = _resolve_model('tests.BasicModel') self.assertEqual(resolved_model, BasicModel) + def test_resolve_unicode_representation(self): + resolved_model = _resolve_model(six.text_type('tests.BasicModel')) + self.assertEqual(resolved_model, BasicModel) + def test_resolve_non_django_model(self): with self.assertRaises(ValueError): _resolve_model(TestCase)