Ticket #25: select_filter.diff

File select_filter.diff, 12.5 KB (added by sebleier@…, 13 years ago)

I modified the previous patch and made it more efficient. Still needs to be tested in browsers other than FF and Chrome.

  • AUTHORS

    diff --git a/AUTHORS b/AUTHORS
    index a19e49b..038b8f8 100644
    a b answer newbie questions, and generally made Django that much better:  
    8585    Natalia Bidart <nataliabidart@gmail.com>
    8686    Paul Bissex <http://e-scribe.com/>
    8787    Simon Blanchard
     88    Sean Bleier <sebleier@gmail.com>
    8889    David Blewett <david@dawninglight.net>
    8990    Matt Boersma <matt@sprout.org>
    9091    boobsd@gmail.com
    answer newbie questions, and generally made Django that much better:  
    215216    Scot Hacker <shacker@birdhouse.org>
    216217    dAniel hAhler
    217218    hambaloney
     219    Chuck Harmston <chuck@chuckharmston.com>
    218220    Brian Harring <ferringb@gmail.com>
    219221    Brant Harris
    220222    Ronny Haryanto <http://ronny.haryan.to/>
  • django/contrib/admin/media/css/widgets.css

    diff --git a/django/contrib/admin/media/css/widgets.css b/django/contrib/admin/media/css/widgets.css
    index 26400fa..870ffeb 100644
    a b  
    55    float: left;
    66}
    77
     8.selector-single{
     9        width: 282px !important;
     10}
     11
    812.selector select {
    913    width: 270px;
    1014    height: 17.2em;
  • new file django/contrib/admin/media/js/fkfilter.js

    diff --git a/django/contrib/admin/media/js/fkfilter.js b/django/contrib/admin/media/js/fkfilter.js
    new file mode 100644
    index 0000000..42af5a0
    - +  
     1(function($) {
     2    $.fn.fk_filter = function(verbose_name) {
     3        return this.each(function() {
     4            var excluded;;
     5            var j = 0;
     6            $(this).attr('size', 2); // Force a multi-row <select>
     7            $(this).find('option[value=""]').remove(); // Remove the '-----'
     8
     9            // Create the wrappers
     10            var outerwrapper = $('<div />', {
     11                'class': 'selector selector-single'
     12            });
     13            $(this).wrap(outerwrapper);
     14            var innerwrapper = $('<div />', {
     15                'class': 'selector-available'
     16            });
     17            $(this).wrap(innerwrapper);
     18
     19            // Creates Header
     20            var header = $('<h2 />', {
     21                'text': interpolate(gettext('Available %s'), [verbose_name])
     22            }).insertBefore(this);
     23
     24            // Creates search bar
     25            var searchbar = $('<p />', {
     26                'html': '<img src="' + window.__admin_media_prefix__ + 'img/admin/selector-search.gif"> <input type="text" id="' + $(this).attr('id') + '_input">',
     27                'class': 'selector-filter'
     28            }).insertBefore(this);
     29
     30            var select = $(this);
     31            var original = select.clone();
     32
     33            var filter = $('#' + $(this).attr('id') + '_input');
     34            var excluded = [];
     35            var lastPatternLength = 0;
     36
     37            filter.bind('keyup.fkfilter', function(evt) {
     38                /*
     39                Procedure for filtering options::
     40
     41                    * Detach the select from the DOM so each change doesn't
     42                      trigger the browser to re-render.
     43                    * If the pattern is longer than the previous pattern, then
     44                      we can skip any excluded select options and search the
     45                      included options for pattern matches.
     46                    * If the pattern is shorter, then we're forced to check
     47                      excluded select options for matches.
     48                    * ``matched`` is incremented whenever an excluded select
     49                      option is matched so that we don't have to search
     50                      recently appended options twice.
     51                    * Note the length of the pattern in ``lastPatternLength``.
     52                    * Append the select back into the DOM for rendering.
     53                */
     54                var i, size, option, options;
     55                var pattern = filter.val();
     56                var parent = select.parent();
     57                var matched = 0;
     58                select.detach();
     59                if (lastPatternLength > pattern.length) {
     60                    size = excluded.length;
     61                    for (i = 0; i < size; i++) {
     62                        option = excluded[i];
     63                        if (option.text().indexOf(pattern) !== -1) {
     64                            select.append(option);
     65                            matched++;
     66                        }
     67                    }
     68                }
     69                options = select.children();
     70                size = options.length - matched;
     71                for(i = 0; i < size; i++) {
     72                    if (options[i].text.indexOf(pattern) === -1) {
     73                        option = $(options[i]).detach();
     74                        excluded.push(option);
     75                    }
     76                }
     77                lastPatternLength = pattern.length;
     78                parent.append(select);
     79            });
     80        });
     81    };
     82})(django.jQuery);
     83
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index fbda8b7..3b23dec 100644
    a b class BaseModelAdmin(object):  
    159159                'class': get_ul_class(self.radio_fields[db_field.name]),
    160160            })
    161161            kwargs['empty_label'] = db_field.blank and _('None') or None
     162        elif db_field.name in self.filter_vertical:
     163            kwargs['widget'] = widgets.FilteredSelectSingle(db_field.verbose_name)
    162164
    163165        return db_field.formfield(**kwargs)
    164166
    class InlineModelAdmin(BaseModelAdmin):  
    13011303            js.append('js/urlify.js')
    13021304            js.append('js/prepopulate.min.js')
    13031305        if self.filter_vertical or self.filter_horizontal:
    1304             js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
     1306            js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js', 'js/fkfilter.js'])
    13051307        return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
    13061308    media = property(_media)
    13071309
  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    index 159afa4..d318bd3 100644
    a b def validate_base(cls, model):  
    296296        check_isseq(cls, 'filter_vertical', cls.filter_vertical)
    297297        for idx, field in enumerate(cls.filter_vertical):
    298298            f = get_field(cls, model, opts, 'filter_vertical', field)
    299             if not isinstance(f, models.ManyToManyField):
     299            if not isinstance(f, (models.ManyToManyField, models.ForeignKey)):
    300300                raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be "
    301                     "a ManyToManyField." % (cls.__name__, idx))
     301                    "a ManyToManyField or ForeignKey." % (cls.__name__, idx))
    302302
    303303    # filter_horizontal
    304304    if hasattr(cls, 'filter_horizontal'):
  • django/contrib/admin/widgets.py

    diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
    index f210d4e..f035586 100644
    a b from django.utils.encoding import force_unicode  
    1414from django.conf import settings
    1515from django.core.urlresolvers import reverse, NoReverseMatch
    1616
     17
     18class FilteredSelectSingle(forms.Select):
     19    """
     20    A Select with a JavaScript filter interface.
     21    """
     22    class Media:
     23        js = (settings.ADMIN_MEDIA_PREFIX + "js/fkfilter.js",)
     24
     25    def __init__(self, verbose_name, attrs=None, choices=()):
     26        self.verbose_name = verbose_name
     27        super(FilteredSelectSingle, self).__init__(attrs, choices)
     28
     29    def render(self, name, value, attrs={}, choices=()):
     30        attrs['class'] = 'selectfilter'
     31        output = [super(FilteredSelectSingle, self).render(
     32            name, value, attrs, choices)]
     33        output.append((
     34            '<script type="text/javascript">'
     35            'django.jQuery(document).ready(function(){'
     36            'django.jQuery("#id_%s").fk_filter("%s")'
     37            '});'
     38            '</script>'
     39        ) % (name, self.verbose_name.replace('"', '\\"'),));
     40        return mark_safe(u''.join(output))
     41
     42
    1743class FilteredSelectMultiple(forms.SelectMultiple):
    1844    """
    1945    A SelectMultiple with a JavaScript filter interface.
  • docs/ref/contrib/admin/index.txt

    diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
    index c28dc8b..20a3599 100644
    a b subclass::  
    276276
    277277    Same as :attr:`~ModelAdmin.filter_horizontal`, but uses a vertical display
    278278    of the filter interface with the box of unselected options appearing above
    279     the box of selected options.
     279    the box of selected options.  You can also add a
     280    :class:`~django.db.models.ForeignKey` to this list and a text box will
     281    allow you to narrow your search for a particular object through a
     282    potentially large list of options.
    280283
    281284.. attribute:: ModelAdmin.form
    282285
  • tests/regressiontests/admin_widgets/tests.py

    diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
    index 7ad74a3..2351eb0 100644
    a b from django.contrib import admin  
    88from django.contrib.admin import widgets
    99from django.contrib.admin.widgets import (FilteredSelectMultiple,
    1010    AdminSplitDateTime, AdminFileWidget, ForeignKeyRawIdWidget, AdminRadioSelect,
    11     RelatedFieldWidgetWrapper, ManyToManyRawIdWidget)
     11    RelatedFieldWidgetWrapper, ManyToManyRawIdWidget, FilteredSelectSingle)
    1212from django.core.files.storage import default_storage
    1313from django.core.files.uploadedfile import SimpleUploadedFile
    1414from django.db.models import DateField
    class AdminForeignKeyRawIdWidget(DjangoTestCase):  
    181181                'Select a valid choice. That choice is not one of the available choices.')
    182182
    183183
     184class FilteredSelectSingleWidgetTest(TestCase):
     185    def test_render(self):
     186        w = FilteredSelectSingle('test')
     187        self.assertEqual(
     188            conditional_escape(w.render('test', 'test')),
     189            '<select name="test" class="selectfilter">\n</select><script type="text/javascript">django.jQuery(document).ready(function(){django.jQuery("#id_test").fk_filter("test")});</script>'
     190        )
     191
     192
    184193class FilteredSelectMultipleWidgetTest(TestCase):
    185194    def test_render(self):
    186195        w = FilteredSelectMultiple('test', False)
  • tests/regressiontests/modeladmin/tests.py

    diff --git a/tests/regressiontests/modeladmin/tests.py b/tests/regressiontests/modeladmin/tests.py
    index a20e579..a011114 100644
    a b from django.contrib.admin.options import ModelAdmin, TabularInline, \  
    66    HORIZONTAL, VERTICAL
    77from django.contrib.admin.sites import AdminSite
    88from django.contrib.admin.validation import validate
    9 from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect
     9from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect, \
     10    FilteredSelectSingle
    1011from django.core.exceptions import ImproperlyConfigured
    1112from django.forms.models import BaseModelFormSet
    1213from django.forms.widgets import Select
    class ModelAdminTests(TestCase):  
    301302            list(ma.get_formsets(request))[0]().forms[0].fields.keys(),
    302303            ['extra', 'transport', 'id', 'DELETE', 'main_band'])
    303304
     305    def test_filter_vertical_foreignkey(self):
     306        class ConcertModelAdmin(ModelAdmin):
     307            filter_vertical = ('main_band',)
     308
     309        cma = ConcertModelAdmin(Concert, self.site)
     310        cmafa = cma.get_form(request)
     311
     312        self.assertEqual(type(cmafa.base_fields['main_band'].widget.widget),
     313            FilteredSelectSingle)
     314        self.assertEqual(type(cmafa.base_fields['opening_band'].widget.widget),
     315            Select)
     316
     317        self.assertEqual(
     318            list(cmafa.base_fields['main_band'].widget.choices),
     319            [(u'', u'---------'), (self.band.id, u'The Doors')])
     320
     321        self.assertEqual(
     322            type(cmafa.base_fields['opening_band'].widget.widget), Select)
     323        self.assertEqual(
     324            list(cmafa.base_fields['opening_band'].widget.choices),
     325            [(u'', u'---------'), (self.band.id, u'The Doors')])
     326
     327        self.assertEqual(type(cmafa.base_fields['day'].widget), Select)
     328        self.assertEqual(list(cmafa.base_fields['day'].widget.choices),
     329            [('', '---------'), (1, 'Fri'), (2, 'Sat')])
     330
     331        self.assertEqual(type(cmafa.base_fields['transport'].widget),
     332            Select)
     333        self.assertEqual(
     334            list(cmafa.base_fields['transport'].widget.choices),
     335            [('', '---------'), (1, 'Plane'), (2, 'Train'), (3, 'Bus')])
     336
    304337
    305338class ValidationTests(unittest.TestCase):
    306339    def test_validation_only_runs_in_debug(self):
Back to Top