From 9ae0ca1caeb7d195719b9544da2a3a7c4fc85b26 Mon Sep 17 00:00:00 2001 From: Michal Dvorak (cen38289) Date: Mon, 3 Dec 2012 17:26:01 +0100 Subject: #467 Added label and help_text to Field --- rest_framework/fields.py | 23 +++++++++++++---------- rest_framework/serializers.py | 6 ++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 482a3d48..907bab74 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -28,7 +28,7 @@ def is_simple_callable(obj): return ( (inspect.isfunction(obj) and not inspect.getargspec(obj)[0]) or (inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1) - ) + ) class Field(object): @@ -38,13 +38,15 @@ class Field(object): _use_files = None form_field_class = forms.CharField - def __init__(self, source=None): + def __init__(self, source=None, label=None, help_text=None): self.parent = None self.creation_counter = Field.creation_counter Field.creation_counter += 1 self.source = source + self.label = label + self.help_text = help_text def initialize(self, parent, field_name): """ @@ -123,11 +125,11 @@ class WritableField(Field): widget = widgets.TextInput default = None - def __init__(self, source=None, read_only=False, required=None, + def __init__(self, source=None, label=None, help_text=None, + read_only=False, required=None, validators=[], error_messages=None, widget=None, default=None, blank=None): - - super(WritableField, self).__init__(source=source) + super(WritableField, self).__init__(source=source, label=label, help_text=help_text) self.read_only = read_only if required is None: @@ -215,6 +217,7 @@ class ModelField(WritableField): """ A generic field that can be used against an arbitrary model field. """ + def __init__(self, *args, **kwargs): try: self.model_field = kwargs.pop('model_field') @@ -222,9 +225,9 @@ class ModelField(WritableField): raise ValueError("ModelField requires 'model_field' kwarg") self.min_length = kwargs.pop('min_length', - getattr(self.model_field, 'min_length', None)) + getattr(self.model_field, 'min_length', None)) self.max_length = kwargs.pop('max_length', - getattr(self.model_field, 'max_length', None)) + getattr(self.model_field, 'max_length', None)) super(ModelField, self).__init__(*args, **kwargs) @@ -434,7 +437,7 @@ class PrimaryKeyRelatedField(RelatedField): # RelatedObject (reverse relationship) obj = getattr(obj, self.source or field_name) return self.to_native(obj.pk) - # Forward relationship + # Forward relationship return self.to_native(pk) @@ -469,7 +472,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): # RelatedManager (reverse relationship) queryset = getattr(obj, self.source or field_name) return [self.to_native(item.pk) for item in queryset.all()] - # Forward relationship + # Forward relationship return [self.to_native(item.pk) for item in queryset.all()] def from_native(self, data): @@ -913,7 +916,7 @@ class DateTimeField(WritableField): # call stack. warnings.warn(u"DateTimeField received a naive datetime (%s)" u" while time zone support is active." % value, - RuntimeWarning) + RuntimeWarning) default_timezone = timezone.get_default_timezone() value = timezone.make_aware(value, default_timezone) return value diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4519ab05..2dab7914 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -428,6 +428,12 @@ class ModelSerializer(Serializer): if max_length: kwargs['max_length'] = max_length + if model_field.verbose_name: + kwargs['label'] = model_field.verbose_name + + if model_field.help_text: + kwargs['help_text'] = model_field.help_text + field_mapping = { models.FloatField: FloatField, models.IntegerField: IntegerField, -- cgit v1.2.3 From ad01fa0eae990ca1607d44cbabba5425c9d0b9a4 Mon Sep 17 00:00:00 2001 From: Michal Dvorak Date: Mon, 3 Dec 2012 19:07:07 +0100 Subject: #467 Added unit test --- rest_framework/serializers.py | 8 ++++---- rest_framework/tests/models.py | 3 ++- rest_framework/tests/serializer.py | 27 +++++++++++++++++++++++++-- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 2dab7914..e4fcbd67 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -428,11 +428,11 @@ class ModelSerializer(Serializer): if max_length: kwargs['max_length'] = max_length - if model_field.verbose_name: - kwargs['label'] = model_field.verbose_name + if model_field.verbose_name is not None: + kwargs['label'] = smart_unicode(model_field.verbose_name) - if model_field.help_text: - kwargs['help_text'] = model_field.help_text + if model_field.help_text is not None: + kwargs['help_text'] = smart_unicode(model_field.help_text) field_mapping = { models.FloatField: FloatField, diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index c35861c6..a13f1ef3 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -1,6 +1,7 @@ from django.db import models from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.generic import GenericForeignKey, GenericRelation +from django.utils.translation import ugettext_lazy as _ # from django.contrib.auth.models import Group @@ -56,7 +57,7 @@ class Anchor(RESTFrameworkModel): class BasicModel(RESTFrameworkModel): - text = models.CharField(max_length=100) + text = models.CharField(max_length=100, verbose_name=_("Text"), help_text=_("Text description.")) class SlugBasedModel(RESTFrameworkModel): diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 61a05da1..cc83a740 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1,6 +1,6 @@ import datetime from django.test import TestCase -from rest_framework import serializers +from rest_framework import serializers, fields from rest_framework.tests.models import (ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel) @@ -48,7 +48,7 @@ class BookSerializer(serializers.ModelSerializer): class ActionItemSerializer(serializers.ModelSerializer): - + class Meta: model = ActionItem @@ -641,3 +641,26 @@ class BlankFieldTests(TestCase): """ serializer = self.not_blank_model_serializer_class(data=self.data) self.assertEquals(serializer.is_valid(), False) + + +# Test for issue #467 +class FieldLabelTest(TestCase): + def setUp(self): + class LabelModelSerializer(serializers.ModelSerializer): + # This is check that ctor supports both fields + additional = fields.CharField(label='Label', help_text='Help') + + class Meta: + model = BasicModel + + self.serializer_class = LabelModelSerializer + + def test_label_from_model(self): + """ + Validates that label and help_text are correctly copied from the model class. + """ + serializer = self.serializer_class() + text_field = serializer.fields['text'] + + self.assertEquals('Text', text_field.label) + self.assertEquals('Text description.', text_field.help_text) -- cgit v1.2.3 From dea0f9129c770b6a9ccebce7296235b529fa59e7 Mon Sep 17 00:00:00 2001 From: Michal Dvorak Date: Mon, 3 Dec 2012 19:10:57 +0100 Subject: Fixed screwed formatting --- rest_framework/fields.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 907bab74..74b4cb7c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -28,7 +28,7 @@ def is_simple_callable(obj): return ( (inspect.isfunction(obj) and not inspect.getargspec(obj)[0]) or (inspect.ismethod(obj) and len(inspect.getargspec(obj)[0]) <= 1) - ) + ) class Field(object): @@ -217,7 +217,6 @@ class ModelField(WritableField): """ A generic field that can be used against an arbitrary model field. """ - def __init__(self, *args, **kwargs): try: self.model_field = kwargs.pop('model_field') @@ -225,9 +224,9 @@ class ModelField(WritableField): raise ValueError("ModelField requires 'model_field' kwarg") self.min_length = kwargs.pop('min_length', - getattr(self.model_field, 'min_length', None)) + getattr(self.model_field, 'min_length', None)) self.max_length = kwargs.pop('max_length', - getattr(self.model_field, 'max_length', None)) + getattr(self.model_field, 'max_length', None)) super(ModelField, self).__init__(*args, **kwargs) @@ -437,7 +436,7 @@ class PrimaryKeyRelatedField(RelatedField): # RelatedObject (reverse relationship) obj = getattr(obj, self.source or field_name) return self.to_native(obj.pk) - # Forward relationship + # Forward relationship return self.to_native(pk) @@ -472,7 +471,7 @@ class ManyPrimaryKeyRelatedField(ManyRelatedField): # RelatedManager (reverse relationship) queryset = getattr(obj, self.source or field_name) return [self.to_native(item.pk) for item in queryset.all()] - # Forward relationship + # Forward relationship return [self.to_native(item.pk) for item in queryset.all()] def from_native(self, data): @@ -916,7 +915,7 @@ class DateTimeField(WritableField): # call stack. warnings.warn(u"DateTimeField received a naive datetime (%s)" u" while time zone support is active." % value, - RuntimeWarning) + RuntimeWarning) default_timezone = timezone.get_default_timezone() value = timezone.make_aware(value, default_timezone) return value -- cgit v1.2.3 From a7849157bcfb8eb07b0ac934ae7c49a965bf6875 Mon Sep 17 00:00:00 2001 From: Michal Dvorak (cen38289) Date: Tue, 4 Dec 2012 10:00:14 +0100 Subject: Moved ctor test to separate unit test --- rest_framework/tests/serializer.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index cc83a740..76c9c465 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -647,9 +647,6 @@ class BlankFieldTests(TestCase): class FieldLabelTest(TestCase): def setUp(self): class LabelModelSerializer(serializers.ModelSerializer): - # This is check that ctor supports both fields - additional = fields.CharField(label='Label', help_text='Help') - class Meta: model = BasicModel @@ -664,3 +661,11 @@ class FieldLabelTest(TestCase): self.assertEquals('Text', text_field.label) self.assertEquals('Text description.', text_field.help_text) + + def test_field_ctor(self): + """ + This is check that ctor supports both label and help_text. + """ + fields.Field(label='Label', help_text='Help') + fields.CharField(label='Label', help_text='Help') + fields.ManyHyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help') -- cgit v1.2.3 From 2a82b64963792b353a7a2634c003692bd4957c9f Mon Sep 17 00:00:00 2001 From: Michal Dvorak (cen38289) Date: Tue, 4 Dec 2012 14:16:45 +0100 Subject: Moved smart_unicode to Field ctor, to mimic Django Forms behavior. --- rest_framework/fields.py | 8 ++++++-- rest_framework/serializers.py | 4 ++-- rest_framework/tests/serializer.py | 10 +++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 74b4cb7c..f57dc57f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -45,8 +45,12 @@ class Field(object): Field.creation_counter += 1 self.source = source - self.label = label - self.help_text = help_text + + if label is not None: + self.label = smart_unicode(label) + + if help_text is not None: + self.help_text = smart_unicode(help_text) def initialize(self, parent, field_name): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e4fcbd67..37496be3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -429,10 +429,10 @@ class ModelSerializer(Serializer): kwargs['max_length'] = max_length if model_field.verbose_name is not None: - kwargs['label'] = smart_unicode(model_field.verbose_name) + kwargs['label'] = model_field.verbose_name if model_field.help_text is not None: - kwargs['help_text'] = smart_unicode(model_field.help_text) + kwargs['help_text'] = model_field.help_text field_mapping = { models.FloatField: FloatField, diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 76c9c465..44adf92e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -659,13 +659,13 @@ class FieldLabelTest(TestCase): serializer = self.serializer_class() text_field = serializer.fields['text'] - self.assertEquals('Text', text_field.label) - self.assertEquals('Text description.', text_field.help_text) + self.assertEquals(u'Text', text_field.label) + self.assertEquals(u'Text description.', text_field.help_text) def test_field_ctor(self): """ This is check that ctor supports both label and help_text. """ - fields.Field(label='Label', help_text='Help') - fields.CharField(label='Label', help_text='Help') - fields.ManyHyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help') + self.assertEquals(u'Label', fields.Field(label='Label', help_text='Help').label) + self.assertEquals(u'Help', fields.CharField(label='Label', help_text='Help').help_text) + self.assertEquals(u'Label', fields.ManyHyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help').label) -- cgit v1.2.3 From 84be169353f0dd2ceb06fe459b72aa2452fcbeb5 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 1 Mar 2013 16:13:04 +1300 Subject: fix function names and dotted lookups for use in PrimaryKeyRelatedField.field_to_native (they work in RelatedField.field_to_native already) --- rest_framework/relations.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 0c108717..ef465b3c 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -215,12 +215,20 @@ class PrimaryKeyRelatedField(RelatedField): def field_to_native(self, obj, field_name): if self.many: # To-many relationship - try: + + queryset = None + if not self.source: # Prefer obj.serializable_value for performance reasons - queryset = obj.serializable_value(self.source or field_name) - except AttributeError: + try: + queryset = obj.serializable_value(field_name) + except AttributeError: + pass + if queryset is None: # RelatedManager (reverse relationship) - queryset = getattr(obj, self.source or field_name) + source = self.source or field_name + queryset = obj + for component in source.split('.'): + queryset = get_component(queryset, component) # Forward relationship return [self.to_native(item.pk) for item in queryset.all()] -- cgit v1.2.3 From 0081d744b9f530b2418d1e82d7ad94a2ebc31c9c Mon Sep 17 00:00:00 2001 From: Matteo Suppo Date: Sat, 23 Mar 2013 14:18:11 +0100 Subject: Added tests for issue 747 in serializer.py --- rest_framework/tests/serializer.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 05217f35..0386ca76 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -1082,3 +1082,32 @@ class DeserializeListTestCase(TestCase): self.assertFalse(serializer.is_valid()) expected = [{}, {'email': ['This field is required.']}, {}] self.assertEqual(serializer.errors, expected) + + +# test for issue 747 + +class LazyStringModel(object): + def __init__(self, lazystring): + self.lazystring = lazystring + + +class LazyStringSerializer(serializers.Serializer): + lazystring = serializers.Field() + + def restore_object(self, attrs, instance=None): + if instance is not None: + instance.lazystring = attrs.get('lazystring', instance.lazystring) + return instance + return Comment(**attrs) + + +class LazyStringsTestCase(TestCase): + + def setUp(self): + from django.utils.translation import ugettext_lazy as _ + + self.model = LazyStringModel(lazystring=_("lazystring")) + + def test_lazy_strings_are_translated(self): + serializer = LazyStringSerializer(self.model) + self.assertEqual(type(serializer.data['lazystring']), type("lazystring")) -- cgit v1.2.3 From b5640bb77843c50f42a649982b9b9592113c6f59 Mon Sep 17 00:00:00 2001 From: Matteo Suppo Date: Sat, 23 Mar 2013 14:18:55 +0100 Subject: Forcing translations of lazy translatable strings in Field to_native method --- rest_framework/fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f3496b53..09f076ab 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -18,7 +18,7 @@ from rest_framework import ISO_8601 from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time from rest_framework.compat import BytesIO from rest_framework.compat import six -from rest_framework.compat import smart_text +from rest_framework.compat import smart_text, force_text from rest_framework.settings import api_settings @@ -165,7 +165,7 @@ class Field(object): return [self.to_native(item) for item in value] elif isinstance(value, dict): return dict(map(self.to_native, (k, v)) for k, v in value.items()) - return smart_text(value) + return force_text(value) def attributes(self): """ -- cgit v1.2.3 From 3737e17d7c29fbc958d6e56c29a641dd6ec26af8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 9 May 2013 13:10:45 +0100 Subject: Added 'Customizing the generic views' section. Closes #816 --- docs/api-guide/generic-views.md | 120 ++++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 42 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index a30bfb21..80df2f40 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -164,6 +164,52 @@ You won't typically need to override the following methods, although you might n --- +# Mixins + +The mixin classes provide the actions that are used to provide the basic view behavior. 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 behavior. + +## ListModelMixin + +Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset. + +If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated. + +If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`. + +## CreateModelMixin + +Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. + +If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value. + +If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. + +## RetrieveModelMixin + +Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response. + +If an object can be retrieved this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`. + +## UpdateModelMixin + +Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance. + +Also provides a `.partial_update(request, *args, **kwargs)` method, which is similar to the `update` method, except that all fields for the update will be optional. This allows support for HTTP `PATCH` requests. + +If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response. + +If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response. + +If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. + +## DestroyModelMixin + +Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance. + +If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`. + +--- + # Concrete View Classes The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior. @@ -242,59 +288,49 @@ Extends: [GenericAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyMod --- -# Mixins - -The mixin classes provide the actions that are used to provide the basic view behavior. 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 behavior. - -## ListModelMixin - -Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset. - -If the queryset is populated, this returns a `200 OK` response, with a serialized representation of the queryset as the body of the response. The response data may optionally be paginated. - -If the queryset is empty this returns a `200 OK` response, unless the `.allow_empty` attribute on the view is set to `False`, in which case it will return a `404 Not Found`. - -Should be mixed in with [MultipleObjectAPIView]. +# Customizing the generic views -## CreateModelMixin +Often you'll want to use the existing generic views, but use some slightly customized behavior. If you find yourself reusing some bit of customized behavior in multiple places, you might want to refactor the behavior into a mixin class that you can then just apply to any view or viewset as needed. -Provides a `.create(request, *args, **kwargs)` method, that implements creating and saving a new model instance. +## Creating custom mixins -If an object is created this returns a `201 Created` response, with a serialized representation of the object as the body of the response. If the representation contains a key named `url`, then the `Location` header of the response will be populated with that value. - -If the request data provided for creating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. - -Should be mixed in with any [GenericAPIView]. - -## RetrieveModelMixin +For example, if you need to lookup objects based on multiple fields in the URL conf, you could create a mixin class like the following: -Provides a `.retrieve(request, *args, **kwargs)` method, that implements returning an existing model instance in a response. - -If an object can be retrieved this returns a `200 OK` response, with a serialized representation of the object as the body of the response. Otherwise it will return a `404 Not Found`. - -Should be mixed in with [SingleObjectAPIView]. - -## UpdateModelMixin - -Provides a `.update(request, *args, **kwargs)` method, that implements updating and saving an existing model instance. - -Also provides a `.partial_update(request, *args, **kwargs)` method, which is similar to the `update` method, except that all fields for the update will be optional. This allows support for HTTP `PATCH` requests. - -If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response. + class MultipleFieldLookupMixin(object): + """ + Apply this mixin to any view or viewset to get multiple field filtering + based on a `lookup_fields` attribute, instead of the default single field filtering. + """ + def get_object(self): + queryset = self.get_queryset() # Get the base queryset + queryset = self.filter_queryset(queryset) # Apply any filter backends + filter = {} + for field in self.lookup_fields: + filter[field] = self.kwargs[field] + return get_object_or_404(queryset, **filter) # Lookup the object -If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response. +You can then simply apply this mixin to a view or viewset anytime you need to apply the custom behavior. -If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. + class RetrieveUserView(MultipleFieldLookupMixin, generics.RetrieveAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer + lookup_fields = ('account', 'username') -Should be mixed in with [SingleObjectAPIView]. +Using custom mixins is a good option if you have custom behavior that needs to be used -## DestroyModelMixin +## Creating custom base classes -Provides a `.destroy(request, *args, **kwargs)` method, that implements deletion of an existing model instance. +If you are using a mixin across multiple views, you can take this a step further and create your own set of base views that can then be used throughout your project. For example: -If an object is deleted this returns a `204 No Content` response, otherwise it will return a `404 Not Found`. + class BaseRetrieveView(MultipleFieldLookupMixin, + generics.RetrieveAPIView): + pass + + class BaseRetrieveUpdateDestroyView(MultipleFieldLookupMixin, + generics.RetrieveUpdateDestroyAPIView): + pass -Should be mixed in with [SingleObjectAPIView]. +Using custom base classes is a good option if you have custom behavior that consistently needs to be repeated across a large number of views throughout your project. [cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views -- cgit v1.2.3 From f2466418dd325ed1353d4e0056411c16e96c2073 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 9 May 2013 13:14:20 +0100 Subject: Tweak doc text slightly --- docs/api-guide/generic-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 80df2f40..3651dc40 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -290,7 +290,7 @@ Extends: [GenericAPIView], [RetrieveModelMixin], [UpdateModelMixin], [DestroyMod # Customizing the generic views -Often you'll want to use the existing generic views, but use some slightly customized behavior. If you find yourself reusing some bit of customized behavior in multiple places, you might want to refactor the behavior into a mixin class that you can then just apply to any view or viewset as needed. +Often you'll want to use the existing generic views, but use some slightly customized behavior. If you find yourself reusing some bit of customized behavior in multiple places, you might want to refactor the behavior into a common class that you can then just apply to any view or viewset as needed. ## Creating custom mixins -- cgit v1.2.3 From 31f94ab409f1d5f41982a5946b980cf3ad8e3ba9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 9 May 2013 13:31:42 +0100 Subject: Added GenericViewSet and docs tweaking --- docs/api-guide/viewsets.md | 25 ++++++++++++++++--------- rest_framework/viewsets.py | 9 +++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index d98f37d8..e354a43a 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -136,9 +136,15 @@ The `ViewSet` class inherits from `APIView`. You can use any of the standard at The `ViewSet` class does not provide any implementations of actions. In order to use a `ViewSet` class you'll override the class and define the action implementations explicitly. +## GenericViewSet + +The `GenericViewSet` class inherits from `GenericAPIView`, and provides the default set of `get_object`, `get_queryset` methods and other generic view base behavior, but does not include any actions by default. + +In order to use a `GenericViewSet` class you'll override the class and either mixin the required mixin classes, or define the action implementations explicitly. + ## ModelViewSet -The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the +The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the various mixin classes. The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, and `.destroy()`. @@ -188,17 +194,18 @@ Again, as with `ModelViewSet`, you can use any of the standard attributes and me # Custom ViewSet base classes -Any standard `View` class can be turned into a `ViewSet` class by mixing in `ViewSetMixin`. You can use this to define your own base classes. +You may need to provide custom `ViewSet` classes that do not have the full set of `ModelViewSet` actions, or that customize the behavior in some other way. ## Example -For example, we can create a base viewset class that provides `retrieve`, `update` and `list` operations: +To create a base viewset class that provides `create`, `list` and `retrieve` operations, inherit from `GenericViewSet`, and mixin the required actions: + + class CreateListRetrieveViewSet(mixins.CreateMixin, + mixins.ListMixin, + mixins.RetrieveMixin, + viewsets.GenericViewSet): + pass - class RetrieveUpdateListViewSet(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.ListModelMixin, - viewsets.ViewSetMixin, - generics.GenericAPIView): """ A viewset that provides `retrieve`, `update`, and `list` actions. @@ -207,6 +214,6 @@ For example, we can create a base viewset class that provides `retrieve`, `updat """ pass -By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple views across your API. +By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple viewsets across your API. [cite]: http://guides.rubyonrails.org/routing.html diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 0eb3e86d..7c820091 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -108,6 +108,15 @@ class ViewSet(ViewSetMixin, views.APIView): pass +class GenericViewSet(ViewSetMixin, generics.GenericAPIView): + """ + The GenericViewSet class does not provide any actions by default, + but does include the base set of generic view behavior, such as + the `get_object` and `get_queryset` methods. + """ + pass + + class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, ViewSetMixin, -- cgit v1.2.3 From 939cc5adba6f5a95aac317134eb841838a0bff3f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 9 May 2013 13:35:01 +0100 Subject: Tweak inheritance --- rest_framework/viewsets.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 7c820091..d91323f2 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -119,8 +119,7 @@ class GenericViewSet(ViewSetMixin, generics.GenericAPIView): class ReadOnlyModelViewSet(mixins.RetrieveModelMixin, mixins.ListModelMixin, - ViewSetMixin, - generics.GenericAPIView): + GenericViewSet): """ A viewset that provides default `list()` and `retrieve()` actions. """ @@ -132,8 +131,7 @@ class ModelViewSet(mixins.CreateModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, - ViewSetMixin, - generics.GenericAPIView): + GenericViewSet): """ A viewset that provides default `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` and `list()` actions. -- cgit v1.2.3 From 0176a5391b5d0c5c5dd61133f17b9b68840d6e1a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 9 May 2013 17:09:40 +0100 Subject: Fix HyperlinkedModelSerializer not respecting lookup_fields --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ea5175e2..d7a4c9ef 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -827,7 +827,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions): def __init__(self, meta): super(HyperlinkedModelSerializerOptions, self).__init__(meta) self.view_name = getattr(meta, 'view_name', None) - self.lookup_field = getattr(meta, 'slug_field', None) + self.lookup_field = getattr(meta, 'lookup_field', None) class HyperlinkedModelSerializer(ModelSerializer): -- cgit v1.2.3 From 5c8356d51d48f758136f5019bfbbef3858c6f5fe Mon Sep 17 00:00:00 2001 From: Hamish Campbell Date: Fri, 10 May 2013 13:28:50 +1200 Subject: Fix minor code error in Generic Views documentation - missing `if` statement. --- docs/api-guide/generic-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 3651dc40..1a060a32 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -34,7 +34,7 @@ For more complex cases you might also want to override various methods on the vi """ Use smaller pagination for HTML representations. """ - self.request.accepted_renderer.format == 'html': + if self.request.accepted_renderer.format == 'html': return 20 return 100 -- cgit v1.2.3 From 2e3032ff8cf1fe172e5ac38dc4320f1191fba340 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 May 2013 14:14:42 +0200 Subject: Added @hamishcampbell for docs fix #818. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 13f673c9..9871c64e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -120,6 +120,7 @@ The following people have helped make REST framework great. * Jerome Chen - [chenjyw] * Andrew Hughes - [eyepulp] * Daniel Hepper - [dhepper] +* Hamish Campbell - [hamishcampbell] Many thanks to everyone who's contributed to the project. @@ -275,3 +276,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [chenjyw]: https://github.com/chenjyw [eyepulp]: https://github.com/eyepulp [dhepper]: https://github.com/dhepper +[hamishcampbell]: https://github.com/hamishcampbell -- cgit v1.2.3 From fd84cf7f10bf703c5daae4a5f6a7dee0c22471dd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 May 2013 21:56:33 +0100 Subject: Docs tweaks --- docs/topics/2.3-announcement.md | 4 ++-- docs/tutorial/2-requests-and-responses.md | 2 +- docs/tutorial/6-viewsets-and-routers.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/topics/2.3-announcement.md b/docs/topics/2.3-announcement.md index 6677c800..4df9c819 100644 --- a/docs/topics/2.3-announcement.md +++ b/docs/topics/2.3-announcement.md @@ -30,8 +30,8 @@ As an example of just how simple REST framework APIs can now be, here's an API w # Routers provide an easy way of automatically determining the URL conf router = routers.DefaultRouter() - router.register(r'users', views.UserViewSet) - router.register(r'groups', views.GroupViewSet) + router.register(r'users', UserViewSet) + router.register(r'groups', GroupViewSet) # Wire up our API using automatic URL routing. diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 3a002cb0..260c4d83 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -8,7 +8,7 @@ Let's introduce a couple of essential building blocks. REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs. request.POST # Only handles form data. Only works for 'POST' method. - request.DATA # Handles arbitrary data. Works any HTTP request with content. + request.DATA # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods. ## Response objects diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 4b01d3e0..277804e2 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -119,7 +119,7 @@ Registering the viewsets with the router is similar to providing a urlpattern. The `DefaultRouter` class we're using also automatically creates the API root view for us, so we can now delete the `api_root` method from our `views` module. -## Trade-offs between views vs viewsets. +## Trade-offs between views vs viewsets Using viewsets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf. -- cgit v1.2.3 From 773a92eab3ac4b635511483ef906b3b8de9dedc9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 May 2013 21:57:05 +0100 Subject: Move models into test modules, out of models module --- rest_framework/tests/models.py | 7 ------- rest_framework/tests/pagination.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index f2117538..40e41a64 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -58,13 +58,6 @@ class ReadOnlyManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) -# Model to test filtering. -class FilterableItem(RESTFrameworkModel): - text = models.CharField(max_length=100) - decimal = models.DecimalField(max_digits=4, decimal_places=2) - date = models.DateField() - - # Model for regression test for #285 class Comment(RESTFrameworkModel): diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 6b8ef02f..894d53d6 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -1,18 +1,24 @@ from __future__ import unicode_literals import datetime from decimal import Decimal -import django +from django.db import models 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, serializers from rest_framework.compat import django_filters -from rest_framework.tests.models import BasicModel, FilterableItem +from rest_framework.tests.models import BasicModel factory = RequestFactory() +class FilterableItem(models.Model): + text = models.CharField(max_length=100) + decimal = models.DecimalField(max_digits=4, decimal_places=2) + date = models.DateField() + + class RootView(generics.ListCreateAPIView): """ Example description for OPTIONS. -- cgit v1.2.3 From 8ce36d2bf1a899683208dc7de425a238ab27d0b3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 May 2013 21:57:20 +0100 Subject: SearchFilter and tests --- rest_framework/filters.py | 9 ++++- rest_framework/tests/filterset.py | 81 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index f2163f6f..54cbbde3 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -74,6 +74,8 @@ class DjangoFilterBackend(BaseFilterBackend): class SearchFilter(BaseFilterBackend): + search_param = 'search' + def construct_search(self, field_name): if field_name.startswith('^'): return "%s__istartswith" % field_name[1:] @@ -90,10 +92,13 @@ class SearchFilter(BaseFilterBackend): if not search_fields: return None + search_terms = request.QUERY_PARAMS.get(self.search_param) orm_lookups = [self.construct_search(str(search_field)) - for search_field in self.search_fields] - for bit in self.query.split(): + for search_field in search_fields] + + for bit in search_terms.split(): or_queries = [models.Q(**{orm_lookup: bit}) for orm_lookup in orm_lookups] queryset = queryset.filter(reduce(operator.or_, or_queries)) + return queryset diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 023bd016..7865fedd 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -1,17 +1,24 @@ from __future__ import unicode_literals import datetime from decimal import Decimal +from django.db import models from django.core.urlresolvers import reverse from django.test import TestCase from django.test.client import RequestFactory from django.utils import unittest from rest_framework import generics, serializers, status, filters from rest_framework.compat import django_filters, patterns, url -from rest_framework.tests.models import FilterableItem, BasicModel +from rest_framework.tests.models import BasicModel factory = RequestFactory() +class FilterableItem(models.Model): + text = models.CharField(max_length=100) + decimal = models.DecimalField(max_digits=4, decimal_places=2) + date = models.DateField() + + if django_filters: # Basic filter on a list view. class FilterFieldsRootView(generics.ListCreateAPIView): @@ -256,3 +263,75 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date)) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, valid_item_data) + + +class SearchFilterModel(models.Model): + title = models.CharField(max_length=20) + text = models.CharField(max_length=100) + + +class SearchFilterTests(TestCase): + def setUp(self): + # Sequence of title/text is: + # + # z abc + # zz bcd + # zzz cde + # ... + for idx in range(10): + title = 'z' * (idx + 1) + text = ( + chr(idx + ord('a')) + + chr(idx + ord('b')) + + chr(idx + ord('c')) + ) + SearchFilterModel(title=title, text=text).save() + + def test_search(self): + class SearchListView(generics.ListAPIView): + model = SearchFilterModel + filter_backends = (filters.SearchFilter,) + search_fields = ('title', 'text') + + view = SearchListView.as_view() + request = factory.get('?search=b') + response = view(request) + self.assertEqual( + response.data, + [ + {u'id': 1, 'title': u'z', 'text': u'abc'}, + {u'id': 2, 'title': u'zz', 'text': u'bcd'} + ] + ) + + def test_exact_search(self): + class SearchListView(generics.ListAPIView): + model = SearchFilterModel + filter_backends = (filters.SearchFilter,) + search_fields = ('=title', 'text') + + view = SearchListView.as_view() + request = factory.get('?search=zzz') + response = view(request) + self.assertEqual( + response.data, + [ + {u'id': 3, 'title': u'zzz', 'text': u'cde'} + ] + ) + + def test_startswith_search(self): + class SearchListView(generics.ListAPIView): + model = SearchFilterModel + filter_backends = (filters.SearchFilter,) + search_fields = ('title', '^text') + + view = SearchListView.as_view() + request = factory.get('?search=b') + response = view(request) + self.assertEqual( + response.data, + [ + {u'id': 2, 'title': u'zz', 'text': u'bcd'} + ] + ) -- cgit v1.2.3 From 293dc3e6d8071fb464a63593831309468e457d6b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 May 2013 22:33:11 +0100 Subject: Added SearchFilter --- docs/api-guide/filtering.md | 94 ++++++++++++++++++++++++++++++++------------- rest_framework/filters.py | 7 ++-- 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 50bc6f05..7d8c39e2 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -77,20 +77,61 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/ # Generic Filtering -As well as being able to override the default queryset, REST framework also includes support for generic filtering backends that allow you to easily construct complex filters that can be specified by the client using query parameters. +As well as being able to override the default queryset, REST framework also includes support for generic filtering backends that allow you to easily construct complex searches and filters. + +## Setting filter backends + +The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example. + + REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',) + } + +You can also set the authentication policy on a per-view, or per-viewset basis, +using the `GenericAPIView` class based views. + + class UserListView(generics.ListAPIView): + queryset = User.objects.all() + serializer = UserSerializer + filter_backends = (filters.DjangoFilterBackend,) + +## Filtering and object lookups + +Note that if a filter backend is configured for a view, then as well as being used to filter list views, it will also be used to filter the querysets used for returning a single object. + +For instance, given the previous example, and a product with an id of `4675`, the following URL would either return the corresponding object, or return a 404 response, depending on if the filtering conditions were met by the given product instance: + + http://example.com/api/products/4675/?category=clothing&max_price=10.00 + +## Overriding the initial queryset + +Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this: + + class PurchasedProductsList(generics.ListAPIView): + """ + Return a list of all the products that the authenticated + user has ever purchased, with optional filtering. + """ + model = Product + serializer_class = ProductSerializer + filter_class = ProductFilter + + def get_queryset(self): + user = self.request.user + return user.purchase_set.all() + +--- + +# API Guide ## DjangoFilterBackend +The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter]. + To use REST framework's `DjangoFilterBackend`, first install `django-filter`. pip install django-filter -You must also set the filter backend to `DjangoFilterBackend` in your settings: - - REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ['rest_framework.filters.DjangoFilterBackend'] - } - #### Specifying filter fields @@ -137,30 +178,30 @@ For more details on using filter sets see the [django-filter documentation][djan --- -## Filtering and object lookups +## SearchFilter -Note that if a filter backend is configured for a view, then as well as being used to filter list views, it will also be used to filter the querysets used for returning a single object. +The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. -For instance, given the previous example, and a product with an id of `4675`, the following URL would either return the corresponding object, or return a 404 response, depending on if the filtering conditions were met by the given product instance: +The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text fields on the model. - http://example.com/api/products/4675/?category=clothing&max_price=10.00 + class UserListView(generics.ListAPIView): + queryset = User.objects.all() + serializer = UserSerializer + filter_backends = (filters.SearchFilter,) + search_fields = ('username', 'email') -## Overriding the initial queryset - -Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this: +This will allow the client to filter the itemss in the list by making queries such as: + + http://example.com/api/users?search=russell + +You can also perform a related lookup on a ForeignKey or ManyToManyField with the lookup API double-underscore notation: + + search_fields = ('username', 'email', 'profile__profession') + +By default, searches will use case-insensitive partial matches. If the search parameter contains multiple whitespace seperated words, then objects will be returned in the list only if all the provided words are matched. + +For more details, see the [Django documentation][search-django-admin]. - class PurchasedProductsList(generics.ListAPIView): - """ - Return a list of all the products that the authenticated - user has ever purchased, with optional filtering. - """ - model = Product - serializer_class = ProductSerializer - filter_class = ProductFilter - - def get_queryset(self): - user = self.request.user - return user.purchase_set.all() --- # Custom generic filtering @@ -181,3 +222,4 @@ For example: [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py +[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 54cbbde3..3edef30d 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -74,7 +74,8 @@ class DjangoFilterBackend(BaseFilterBackend): class SearchFilter(BaseFilterBackend): - search_param = 'search' + search_param = 'search' # The URL query parameter used for the search. + delimiter = None # For example, set to ',' for comma delimited searchs. def construct_search(self, field_name): if field_name.startswith('^'): @@ -96,8 +97,8 @@ class SearchFilter(BaseFilterBackend): orm_lookups = [self.construct_search(str(search_field)) for search_field in search_fields] - for bit in search_terms.split(): - or_queries = [models.Q(**{orm_lookup: bit}) + for search_term in search_terms.split(self.delimiter): + or_queries = [models.Q(**{orm_lookup: search_term}) for orm_lookup in orm_lookups] queryset = queryset.filter(reduce(operator.or_, or_queries)) -- cgit v1.2.3 From 260a8125c58d76c947f864e738b0a8c35da02d9d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 May 2013 22:56:23 +0100 Subject: Improve custom filtering example --- docs/api-guide/filtering.md | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 7d8c39e2..b5dfc68e 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -180,9 +180,9 @@ For more details on using filter sets see the [django-filter documentation][djan ## SearchFilter -The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. +The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. -The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text fields on the model. +The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text fields on the model. class UserListView(generics.ListAPIView): queryset = User.objects.all() @@ -210,13 +210,20 @@ You can also provide your own generic filtering backend, or write an installable To do so override `BaseFilterBackend`, and override the `.filter_queryset(self, request, queryset, view)` method. The method should return a new, filtered queryset. -To install the filter backend, set the `'DEFAULT_FILTER_BACKENDS'` key in your `'REST_FRAMEWORK'` setting, using the dotted import path of the filter backend class. +As well as allowing clients to perform searches and filtering, generic filter backends can be useful for restricting which objects should be visible to any given request or user. -For example: +## Example - REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ['custom_filters.CustomFilterBackend'] - } +For example, you might need to restrict users to only being able to see objects they created. + + class IsOwnerFilterBackend(filters.BaseFilterBackend): + """ + Filter that only allows users to see their own objects. + """ + def filter_queryset(self, request, queryset, view): + return queryset.filter(owner=request.user) + +We could do the same thing by overriding `get_queryset` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API. [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter -- cgit v1.2.3 From dd51d369c8228f3add37cc639702097b0df9cd90 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 May 2013 23:02:24 +0100 Subject: Unicode string fix --- rest_framework/tests/filterset.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 7865fedd..e5414232 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -299,8 +299,8 @@ class SearchFilterTests(TestCase): self.assertEqual( response.data, [ - {u'id': 1, 'title': u'z', 'text': u'abc'}, - {u'id': 2, 'title': u'zz', 'text': u'bcd'} + {'id': 1, 'title': 'z', 'text': 'abc'}, + {'id': 2, 'title': 'zz', 'text': 'bcd'} ] ) @@ -316,7 +316,7 @@ class SearchFilterTests(TestCase): self.assertEqual( response.data, [ - {u'id': 3, 'title': u'zzz', 'text': u'cde'} + {'id': 3, 'title': 'zzz', 'text': 'cde'} ] ) @@ -332,6 +332,6 @@ class SearchFilterTests(TestCase): self.assertEqual( response.data, [ - {u'id': 2, 'title': u'zz', 'text': u'bcd'} + {'id': 2, 'title': 'zz', 'text': 'bcd'} ] ) -- cgit v1.2.3 From fd4a66cfc7888775d20b18665d63156cf3dae13a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 10 May 2013 23:06:42 +0100 Subject: Fix py3k compat with functools.reduce --- rest_framework/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 3edef30d..57f0f7c8 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -3,9 +3,9 @@ Provides generic filtering backends that can be used to filter the results returned by list views. """ from __future__ import unicode_literals - from django.db import models from rest_framework.compat import django_filters +from functools import reduce import operator FilterSet = django_filters and django_filters.FilterSet or None -- cgit v1.2.3 From 9d2580dccfe23e113221c7e150bddebb95d98214 Mon Sep 17 00:00:00 2001 From: Marlon Bailey Date: Sat, 11 May 2013 22:26:34 -0400 Subject: added support for multiple @action and @link decorators on a viewset, along with a router testcase illustrating the failure against the master code base --- rest_framework/routers.py | 6 +++--- rest_framework/tests/routers.py | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 rest_framework/tests/routers.py diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 0707635a..ebdf2b2a 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -127,18 +127,18 @@ class SimpleRouter(BaseRouter): """ # Determine any `@action` or `@link` decorated methods on the viewset - dynamic_routes = {} + dynamic_routes = [] for methodname in dir(viewset): attr = getattr(viewset, methodname) httpmethod = getattr(attr, 'bind_to_method', None) if httpmethod: - dynamic_routes[httpmethod] = methodname + dynamic_routes.append((httpmethod, methodname)) ret = [] for route in self.routes: if route.mapping == {'{httpmethod}': '{methodname}'}: # Dynamic routes (@link or @action decorator) - for httpmethod, methodname in dynamic_routes.items(): + for httpmethod, methodname in dynamic_routes: initkwargs = route.initkwargs.copy() initkwargs.update(getattr(viewset, methodname).kwargs) ret.append(Route( diff --git a/rest_framework/tests/routers.py b/rest_framework/tests/routers.py new file mode 100644 index 00000000..138d13d7 --- /dev/null +++ b/rest_framework/tests/routers.py @@ -0,0 +1,46 @@ +from __future__ import unicode_literals +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework import status +from rest_framework.response import Response +from rest_framework import viewsets +from rest_framework.decorators import link, action +from rest_framework.routers import SimpleRouter +import copy + +factory = RequestFactory() + + +class BasicViewSet(viewsets.ViewSet): + def list(self, request, *args, **kwargs): + return Response({'method': 'list'}) + + @action() + def action1(self, request, *args, **kwargs): + return Response({'method': 'action1'}) + + @action() + def action2(self, request, *args, **kwargs): + return Response({'method': 'action2'}) + + @link() + def link1(self, request, *args, **kwargs): + return Response({'method': 'link1'}) + + @link() + def link2(self, request, *args, **kwargs): + return Response({'method': 'link2'}) + + +class TestSimpleRouter(TestCase): + def setUp(self): + self.router = SimpleRouter() + + def test_link_and_action_decorator(self): + routes = self.router.get_routes(BasicViewSet) + # Should be 2 by default, and then four from the @action and @link combined + #self.assertEqual(len(routes), 6) + # + decorator_routes = routes[2:] + for i, method in enumerate(['action1', 'action2', 'link1', 'link2']): + self.assertEqual(decorator_routes[i].mapping.values()[0], method) -- cgit v1.2.3 From 5e2d8052d4bf87c81cc9807c96c933ca975cc483 Mon Sep 17 00:00:00 2001 From: Marlon Bailey Date: Sun, 12 May 2013 09:22:14 -0400 Subject: fix test case to work with Python 3 and make it more explicit --- rest_framework/tests/routers.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/rest_framework/tests/routers.py b/rest_framework/tests/routers.py index 138d13d7..4e4765cb 100644 --- a/rest_framework/tests/routers.py +++ b/rest_framework/tests/routers.py @@ -38,9 +38,18 @@ class TestSimpleRouter(TestCase): def test_link_and_action_decorator(self): routes = self.router.get_routes(BasicViewSet) - # Should be 2 by default, and then four from the @action and @link combined - #self.assertEqual(len(routes), 6) - # decorator_routes = routes[2:] - for i, method in enumerate(['action1', 'action2', 'link1', 'link2']): - self.assertEqual(decorator_routes[i].mapping.values()[0], method) + # Make sure all these endpoints exist and none have been clobbered + for i, endpoint in enumerate(['action1', 'action2', 'link1', 'link2']): + route = decorator_routes[i] + # check url listing + self.assertEqual(route.url, + '^{{prefix}}/{{lookup}}/{0}/$'.format(endpoint)) + # check method to function mapping + if endpoint.startswith('action'): + method_map = 'post' + else: + method_map = 'get' + self.assertEqual(route.mapping[method_map], endpoint) + + -- cgit v1.2.3 From 5074bbe4b21a0fc116e4288743fb78314a76a33b Mon Sep 17 00:00:00 2001 From: James Summerfield Date: Mon, 13 May 2013 07:51:23 +0200 Subject: Remove trailing unmatched in login_base.html template. Reformat indentation and label all closing tags for consistency. --- .../templates/rest_framework/login_base.html | 68 ++++++++++------------ 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html index 380d5820..a3e73b6b 100644 --- a/rest_framework/templates/rest_framework/login_base.html +++ b/rest_framework/templates/rest_framework/login_base.html @@ -12,44 +12,40 @@
-