Ticket #25: select_filter.2.diff

File select_filter.2.diff, 12.1 KB (added by sebleier, 13 years ago)

fixed a bug. Development can be found at https://github.com/sebleier/django/tree/feature/select-filter

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