Ticket #25: select_filter.3.diff
File select_filter.3.diff, 12.1 KB (added by , 14 years ago) |
---|
-
AUTHORS
diff --git a/AUTHORS b/AUTHORS index a19e49b..038b8f8 100644
a b answer newbie questions, and generally made Django that much better: 85 85 Natalia Bidart <nataliabidart@gmail.com> 86 86 Paul Bissex <http://e-scribe.com/> 87 87 Simon Blanchard 88 Sean Bleier <sebleier@gmail.com> 88 89 David Blewett <david@dawninglight.net> 89 90 Matt Boersma <matt@sprout.org> 90 91 boobsd@gmail.com … … answer newbie questions, and generally made Django that much better: 215 216 Scot Hacker <shacker@birdhouse.org> 216 217 dAniel hAhler 217 218 hambaloney 219 Chuck Harmston <chuck@chuckharmston.com> 218 220 Brian Harring <ferringb@gmail.com> 219 221 Brant Harris 220 222 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 5 5 float: left; 6 6 } 7 7 8 .selector-single{ 9 width: 282px !important; 10 } 11 8 12 .selector select { 9 13 width: 270px; 10 14 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): 159 159 'class': get_ul_class(self.radio_fields[db_field.name]), 160 160 }) 161 161 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) 162 164 163 165 return db_field.formfield(**kwargs) 164 166 … … class InlineModelAdmin(BaseModelAdmin): 1301 1303 js.append('js/urlify.js') 1302 1304 js.append('js/prepopulate.min.js') 1303 1305 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']) 1305 1307 return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js]) 1306 1308 media = property(_media) 1307 1309 -
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): 296 296 check_isseq(cls, 'filter_vertical', cls.filter_vertical) 297 297 for idx, field in enumerate(cls.filter_vertical): 298 298 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)): 300 300 raise ImproperlyConfigured("'%s.filter_vertical[%d]' must be " 301 "a ManyToManyField ." % (cls.__name__, idx))301 "a ManyToManyField or ForeignKey." % (cls.__name__, idx)) 302 302 303 303 # filter_horizontal 304 304 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 14 14 from django.conf import settings 15 15 from django.core.urlresolvers import reverse, NoReverseMatch 16 16 17 18 class 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 17 43 class FilteredSelectMultiple(forms.SelectMultiple): 18 44 """ 19 45 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:: 276 276 277 277 Same as :attr:`~ModelAdmin.filter_horizontal`, but uses a vertical display 278 278 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. 280 283 281 284 .. attribute:: ModelAdmin.form 282 285 -
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 8 8 from django.contrib.admin import widgets 9 9 from django.contrib.admin.widgets import (FilteredSelectMultiple, 10 10 AdminSplitDateTime, AdminFileWidget, ForeignKeyRawIdWidget, AdminRadioSelect, 11 RelatedFieldWidgetWrapper, ManyToManyRawIdWidget )11 RelatedFieldWidgetWrapper, ManyToManyRawIdWidget, FilteredSelectSingle) 12 12 from django.core.files.storage import default_storage 13 13 from django.core.files.uploadedfile import SimpleUploadedFile 14 14 from django.db.models import DateField … … class AdminForeignKeyRawIdWidget(DjangoTestCase): 181 181 'Select a valid choice. That choice is not one of the available choices.') 182 182 183 183 184 class 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 184 193 class FilteredSelectMultipleWidgetTest(TestCase): 185 194 def test_render(self): 186 195 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, \ 6 6 HORIZONTAL, VERTICAL 7 7 from django.contrib.admin.sites import AdminSite 8 8 from django.contrib.admin.validation import validate 9 from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect 9 from django.contrib.admin.widgets import AdminDateWidget, AdminRadioSelect, \ 10 FilteredSelectSingle 10 11 from django.core.exceptions import ImproperlyConfigured 11 12 from django.forms.models import BaseModelFormSet 12 13 from django.forms.widgets import Select … … class ModelAdminTests(TestCase): 301 302 list(ma.get_formsets(request))[0]().forms[0].fields.keys(), 302 303 ['extra', 'transport', 'id', 'DELETE', 'main_band']) 303 304 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 304 337 305 338 class ValidationTests(unittest.TestCase): 306 339 def test_validation_only_runs_in_debug(self):