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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(+) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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 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 --- rest_framework/viewsets.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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 --- rest_framework/filters.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'rest_framework') 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 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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 (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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(-) (limited to 'rest_framework') 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 @@
-