Mark Lavin avatar Mark Lavin committed 5a2573e Merge

Merging in multi-field lookups. Fixes #36.

Comments (0)

Files changed (7)

 django-selectable uses a registration pattern similar to the Django admin.
 Lookups should be defined in a `lookups.py` in your application's module. Once defined
 you must register in with django-selectable. All lookups must extend from 
-`selectable.base.LookupBase` which defines the API for every lookup.
+``selectable.base.LookupBase`` which defines the API for every lookup.
 
     .. code-block:: python
 
 .. py:method:: LookupBase.get_item_label(item)
 
     This is first of three formatting methods. The label is shown in the
-    drop down menu of search results. This defaults to `item.__unicode__`.
+    drop down menu of search results. This defaults to ``item.__unicode__``.
 
     :param item: An item from the search results.
     :return: A string representation of the item to be shown in the search results.
 .. py:method:: LookupBase.get_item_id(item)
 
     This is second of three formatting methods. The id is the value that will eventually
-    be returned by the field/widget. This defaults to `item.__unicode__`.
+    be returned by the field/widget. This defaults to ``item.__unicode__``.
 
     :param item: An item from the search results.
     :return: A string representation of the item to be returned by the field/widget.
 .. py:method:: LookupBase.get_item_value(item)
 
     This is last of three formatting methods. The value is shown in the
-    input once the item has been selected. This defaults to `item.__unicode__`.
+    input once the item has been selected. This defaults to ``item.__unicode__``.
 
     :param item: An item from the search results.
     :return: A string representation of the item to be shown in the input.
 
 .. py:method:: LookupBase.get_item(value)
 
-    `get_item` is the reverse of `get_item_id`. This should take the value
+    ``get_item`` is the reverse of ``get_item_id``. This should take the value
     from the form initial values and return the current item. This defaults
     to simply return the value.
 
 
     If you plan to use a lookup with a field or widget which allows the user
     to input new values then you must define what it means to create a new item
-    for your lookup. By default this raises a `NotImplemented` error.
+    for your lookup. By default this raises a ``NotImplemented`` error.
 
     :param value: The user given value.
     :return: The new item created from the item.
 
 .. py:method:: LookupBase.format_item(item)
 
-    By default `format_item` creates a dictionary with the three keys used by
+    By default ``format_item`` creates a dictionary with the three keys used by
     the UI plugin: id, value, label. These are generated from the calls to
-    `get_item_id`, `get_item_value`, and `get_item_label`. If you want to
+    ``get_item_id``, ``get_item_value``, and ``get_item_label``. If you want to
     add additional keys you should add them here.
 
     :param item: An item from the search results.
 
 .. py:method:: LookupBase.paginate_results(request, results, limit)
 
-    If :ref:`SELECTABLE_MAX_LIMIT` is defined or `limit` is passed in request.GET
-    then `paginate_results` will return the current page using Django's
+    If :ref:`SELECTABLE_MAX_LIMIT` is defined or ``limit`` is passed in request.GET
+    then ``paginate_results`` will return the current page using Django's
     built in pagination. See the Django docs on `pagination <https://docs.djangoproject.com/en/1.3/topics/pagination/>`_
     for more info.
 
 --------------------------------------
 
 Perhaps the most common use case is to define a lookup based on a given Django model.
-For this you can extend `selectable.base.ModelLookup`. To extend `ModelLookup` you
-should set two class attributes: `model` and `search_field`.
+For this you can extend ``selectable.base.ModelLookup``. To extend ``ModelLookup`` you
+should set two class attributes: ``model`` and ``search_fields``.
 
     .. literalinclude:: ../example/core/lookups.py
         :pyobject: FruitLookup
 
-The syntax for `search_field` is the same as the Django 
+The syntax for ``search_fields`` is the same as the Django 
 `field lookup syntax <http://docs.djangoproject.com/en/1.3/ref/models/querysets/#field-lookups>`_. 
-You may optionally define a third class attribute `filters` which is a dictionary of
+Each of these lookups are combined as OR so any one of them matching will return a
+result. You may optionally define a third class attribute ``filters`` which is a dictionary of
 filters to be applied to the model queryset. The keys should be a string defining a field lookup
-and the value should be the value for the field lookup.
+and the value should be the value for the field lookup. Filters on the other hand are
+combined with AND.
 
+.. versionadded:: 0.3
+
+Prior to version 0.3 the model based lookups used a single string ``search_field``. This
+will continue to work in v0.3 but will raise a DeprecationWarning. This support will
+be removed in v0.4.
+
+
+User Lookup Example
+--------------------------------------
+
+Below is a larger model lookup example using multiple search fields, filters 
+and display options for the `auth.User <https://docs.djangoproject.com/en/1.3/topics/auth/#users>`_ 
+model.
+
+    .. code-block:: python
+
+        from django.contrib.auth.models import User
+        from selectable.base import ModelLookup
+        from selectable.registry import registry
+
+
+        class UserLookup(ModelLookup):
+            model = User
+            search_fields = (
+                'username__icontains',
+                'first_name__icontains',
+                'last_name__icontains',
+            )
+            filters = {'is_active': True, }
+
+            def get_item_value(self, item):
+                # Display for currently selected item
+                return item.username
+
+            def get_item_label(self, item):
+                # Display for choice listings
+                return u"%s (%s)" % (item.username, item.get_full_name())
+
+        registry.register(UserLookup)
+

docs/releases.rst

 Features
 _________________
 
+- Multiple search fields for :ref:`model based lookups <ModelLookup>`
 - Support for :ref:`highlighting term matches <javascript-highlightMatch>`
 - Support for HTML in :ref:`result labels <lookup-get-item-label>`
 - Support for :ref:`client side formatting <advanaced-label-formats>`

example/core/lookups.py

 
 class FruitLookup(ModelLookup):
     model = Fruit
-    search_field = 'name__icontains'
+    search_fields = ('name__icontains', )
 
 registry.register(FruitLookup)
 
 
 class OwnerLookup(ModelLookup):
     model = User
-    search_field = 'username__icontains'
+    search_fields = ('username__icontains', )
 
 
 registry.register(OwnerLookup)
 
 class CityLookup(ModelLookup):
     model = City
-    search_field = 'name__icontains'
+    search_fields = ('name__icontains', )
 
     def get_query(self, request, term):
         results = super(CityLookup, self).get_query(request, term)

selectable/base.py

+import operator
 import re
 
 from django.conf import settings
 from django.core.paginator import Paginator, InvalidPage, EmptyPage
 from django.core.urlresolvers import reverse
 from django.core.serializers.json import DjangoJSONEncoder
+from django.db.models import Q
 from django.http import HttpResponse
 from django.utils import simplejson as json
 from django.utils.encoding import smart_unicode
     model = None
     filters = {}
     search_field = ''
+    search_fields = ()
+
+    def __init__(self):
+        super(ModelLookup, self).__init__()
+        if self.search_field and not self.search_fields:
+            self.search_fields = (self.search_field, )
 
     def get_query(self, request, term):
         qs = self.get_queryset()
-        if term and self.search_field:
-            qs = qs.filter(**{self.search_field: term})
+        if term:
+            search_filters = []
+            if self.search_fields:
+                for field in self.search_fields:
+                    search_filters.append(Q(**{field: term}))
+            qs = qs.filter(reduce(operator.or_, search_filters))
         return qs
 
     def get_queryset(self):
 
     def create_item(self, value):
         data = {}
-        if self.search_field:
-            field_name = re.sub(r'__\w+$', '',  self.search_field)
+        if self.search_fields:
+            field_name = re.sub(r'__\w+$', '',  self.search_fields[0])
             if field_name:
                 data = {field_name: value}
         return self.model(**data)

selectable/registry.py

 from django.utils.encoding import force_unicode
 
-from selectable.base import LookupBase
+from selectable.base import LookupBase, ModelLookup
 from selectable.exceptions import (LookupAlreadyRegistered, LookupNotRegistered,
                                     LookupInvalid)
 
     def validate(self, lookup):
         if not issubclass(lookup, LookupBase):
             raise LookupInvalid(u'Registered lookups must inherit from the LookupBase class')
+        if issubclass(lookup, ModelLookup) and getattr(lookup, 'search_field'):
+            import warnings
+            warnings.warn(
+                u"ModelLookup.search_field is deprecated; Use ModelLookup.search_fields instead.", 
+                DeprecationWarning
+            )
 
     def register(self, lookup):
 

selectable/tests/__init__.py

 
 class Thing(models.Model):
     name = models.CharField(max_length=100)
+    description = models.CharField(max_length=100)
 
     def __unicode__(self):
         return self.name

selectable/tests/base.py

 
 __all__ = (
     'ModelLookupTestCase',
+    'MultiFieldLookupTestCase',
+    'LegacyModelLookupTestCase',
 )
 
 
     def create_thing(self, data=None):
         data = data or {}
         defaults = {
-            'name': self.get_random_string()
+            'name': self.get_random_string(),
+            'description': self.get_random_string(),
         }
         defaults.update(data)
         return Thing.objects.create(**defaults)
 
 class SimpleModelLookup(ModelLookup):
     model = Thing
-    search_field = 'name__icontains'    
+    search_fields = ('name__icontains', )
 
 
 class ModelLookupTestCase(BaseSelectableTestCase):
         item = lookup.get_item(thing.pk)
         self.assertEqual(thing, item)
 
+
+class MultiFieldLookup(ModelLookup):
+    model = Thing
+    search_fields = ('name__icontains', 'description__icontains', )
+
+
+class MultiFieldLookupTestCase(ModelLookupTestCase):
+    lookup_cls = MultiFieldLookup
+
+    def test_get_name(self):
+        name = self.__class__.lookup_cls.name()
+        self.assertEqual(name, 'tests-multifieldlookup')
+
+    def test_get_url(self):
+        url = self.__class__.lookup_cls.url()
+        test_url = reverse('selectable-lookup', args=['tests-multifieldlookup'])
+        self.assertEqual(url, test_url)
+
+    def test_description_search(self):
+        lookup = self.get_lookup_instance()
+        thing = self.create_thing(data={'description': 'Thing'})
+        other_thing = self.create_thing(data={'description': 'Other Thing'})
+        qs = lookup.get_query(request=None, term='other')
+        self.assertTrue(thing.pk not in qs.values_list('id', flat=True))
+        self.assertTrue(other_thing.pk in qs.values_list('id', flat=True))
+
+
+class LegacyModelLookup(ModelLookup):
+    model = Thing
+    search_field = 'name__icontains'
+
+
+class LegacyModelLookupTestCase(ModelLookupTestCase):
+    lookup_cls = LegacyModelLookup
+
+    def test_get_name(self):
+        name = self.__class__.lookup_cls.name()
+        self.assertEqual(name, 'tests-legacymodellookup')
+
+    def test_get_url(self):
+        url = self.__class__.lookup_cls.url()
+        test_url = reverse('selectable-lookup', args=['tests-legacymodellookup'])
+        self.assertEqual(url, test_url)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.