Ticket #25: 25_patch.diff

File 25_patch.diff, 8.5 KB (added by cpharmston, 5 years ago)

First run at it

  • 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 620e082..b1c7768 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..afe6fa5
    - +  
     1(function($){
     2       
     3        $.fn.fk_filter = function(verbose_name) {
     4               
     5                if(verbose_name === 'undefined') var verbose_name = '';
     6               
     7                return this.each(function(){
     8                       
     9                        $(this).attr('size', 2); // Force a multi-row <select>
     10                        $(this).find('option[value=""]').remove(); // Remove the '-----'
     11                       
     12                        // Create the wrappers
     13                        var outerwrapper = $('<div />', {
     14                                'class': 'selector selector-single'
     15                        });
     16                        $(this).wrap(outerwrapper);
     17                        var innerwrapper = $('<div />', {
     18                                'class': 'selector-available'
     19                        });
     20                        $(this).wrap(innerwrapper);
     21                       
     22                        // Creates Header
     23                        var header = $('<h2 />', {
     24                                'text': 'Available ' + verbose_name
     25                        }).insertBefore(this);
     26                       
     27                        // Creates search bar
     28                        var searchbar = $('<p />', {
     29                                'html': '<img src="/media/img/admin/selector-search.gif"> <input type="text" id="' + $(this).attr('id') + '_input">',
     30                                'class': 'selector-filter'
     31                        }).insertBefore(this);
     32                       
     33                        var select = $(this);
     34                        var filter = $('#' + $(this).attr('id') + '_input');
     35                       
     36                        // Creates cache
     37                        var cache = [];
     38                        select.find('option').each(function(index, element){
     39                                cache.push({
     40                                        'text': $(element).text(),
     41                                        'value': $(element).val(),
     42                                        'selected': $(element).attr('selected')
     43                                });
     44                        });
     45                        select.data('django-cache', cache);
     46                       
     47                        // Adjust cache on change
     48                        select.bind('change.fkfilter', function(evt){
     49                                var thisval = $(this).val();
     50                                var cache = select.data('django-cache');
     51                               
     52                                // for() used over $.each() for performance on long lists
     53                                for(var i = 0; i < cache.length; i++){
     54                                        cache[i]['selected'] = (cache[i]['value'] == thisval);
     55                                }
     56                        });
     57                       
     58                        // Repopulate from cache on keyup
     59                        filter.bind('keyup.fkfilter', function(evt){
     60                                select.children().remove();
     61                                var regex = new RegExp(filter.val(), 'gi');
     62                                var cache = select.data('django-cache');
     63                               
     64                                // for() used over $.each() for performance on long lists
     65                                for(var i = 0; i < cache.length; i++){
     66                                        if(cache[i]['text'].match(regex)){
     67                                                $('<option />', {
     68                                                        'selected': cache[i]['selected'],
     69                                                        'value': cache[i]['value'],
     70                                                        'text': cache[i]['text']
     71                                                }).appendTo(select);
     72                                        }
     73                                }
     74                        });
     75                       
     76                });
     77        };
     78       
     79})(django.jQuery);
     80 No newline at end of file
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index 3b6e2b7..1a0e7a6 100644
    a b class BaseModelAdmin(object): 
    6161    exclude = None
    6262    fieldsets = None
    6363    form = forms.ModelForm
     64    fk_filter = ()
    6465    filter_vertical = ()
    6566    filter_horizontal = ()
    6667    radio_fields = {}
    class BaseModelAdmin(object): 
    157158                'class': get_ul_class(self.radio_fields[db_field.name]),
    158159            })
    159160            kwargs['empty_label'] = db_field.blank and _('None') or None
     161        elif db_field.name in self.fk_filter:
     162            kwargs['widget'] = widgets.FilteredSelectSingle(
     163                db_field.verbose_name, (db_field.name in self.filter_vertical))
    160164
    161165        return db_field.formfield(**kwargs)
    162166
    class InlineModelAdmin(BaseModelAdmin): 
    12371241            js.append('js/prepopulate.min.js')
    12381242        if self.filter_vertical or self.filter_horizontal:
    12391243            js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js'])
     1244        if self.fk_filter:
     1245            js.append('js/fkfilter.js')
    12401246        return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js])
    12411247    media = property(_media)
    12421248
  • django/contrib/admin/widgets.py

    diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
    index 134effc..72e4dcb 100644
    a b from django.utils.encoding import force_unicode 
    1515from django.conf import settings
    1616from django.core.urlresolvers import reverse, NoReverseMatch
    1717
     18
     19class FilteredSelectSingle(forms.Select):
     20    """
     21    A Select with a JavaScript filter interface.
     22   
     23    Note that the resulting JavaScript assumes that the js18n catalog has been
     24    loaded in the page.
     25    """
     26    class Media:
     27        js = (settings.ADMIN_MEDIA_PREFIX + "js/fkfilter.js",)
     28
     29    def __init__(self, verbose_name, is_stacked, attrs=None, choices=()):
     30        self.verbose_name = verbose_name
     31        self.is_stacked = is_stacked
     32        super(FilteredSelectSingle, self).__init__(attrs, choices)
     33
     34    def render(self, name, value, attrs=None, choices=()):
     35        if attrs is None: attrs = {}
     36        attrs['class'] = 'selectfilter'
     37        if self.is_stacked: attrs['class'] += 'stacked'
     38        output = [super(FilteredSelectSingle, self).render(
     39            name, value, attrs, choices)]
     40        output.append((
     41            '<script type="text/javascript">'
     42            'django.jQuery(document).ready(function(){'
     43            'django.jQuery("#id_%s").fk_filter("%s")'
     44            '});'
     45            '</script>'
     46        ) % (name, self.verbose_name.replace('"', '\\"'),));
     47        return mark_safe(u''.join(output))
     48
     49
    1850class FilteredSelectMultiple(forms.SelectMultiple):
    1951    """
    2052    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 0550576..7c68806 100644
    a b Since the Author model only has three fields, ``name``, ``title``, and 
    247247``birth_date``, the forms resulting from the above declarations will contain
    248248exactly the same fields.
    249249
     250.. attribute:: ModelAdmin.fk_filter
     251
     252Use a nifty unobtrusive JavaScript "filter" interface instead of the usability-challenged ``<select>`` in the admin form. The value is a list of ForeignKey fields that should be displayed as a filter interface. See ``filter_horizontal`` and ``filter_vertical`` to use the same filtering interface on a ManyToManyField.
     253
    250254.. attribute:: ModelAdmin.filter_horizontal
    251255
    252 Use a nifty unobtrusive JavaScript "filter" interface instead of the
    253 usability-challenged ``<select multiple>`` in the admin form. The value is a
    254 list of fields that should be displayed as a horizontal filter interface. See
     256Uses the same JavaScript "filter" interface as ``fk_filter``, replacing the usability-challenged ``<select multiple>`` in the admin form. The value is a
     257list of ManyToMany fields that should be displayed as a horizontal filter interface. See
    255258``filter_vertical`` to use a vertical interface.
    256259
    257260.. attribute:: ModelAdmin.filter_vertical
  • tests/regressiontests/modeladmin/models.py

    diff --git a/tests/regressiontests/modeladmin/models.py b/tests/regressiontests/modeladmin/models.py
    index 36ea416..297ea1c 100644
    a b ImproperlyConfigured: 'ValidationTestModelAdmin.filter_vertical[0]' must be a Ma 
    516516...     filter_vertical = ("users",)
    517517>>> validate(ValidationTestModelAdmin, ValidationTestModel)
    518518
     519# fk_filter
     520
     521>>> class ValidationTestModelAdmin(ModelAdmin):
     522...     fk_filter = 42
     523>>> validate(ValidationTestModelAdmin, ValidationTestModel)
     524Traceback (most recent call last):
     525...
     526ImproperlyConfigured: 'ValidationTestModelAdmin.filter_horizontal' must be a list or tuple.
     527
     528>>> class ValidationTestModelAdmin(ModelAdmin):
     529...     fk_filter = ("non_existent_field",)
     530>>> validate(ValidationTestModelAdmin, ValidationTestModel)
     531Traceback (most recent call last):
     532...
     533ImproperlyConfigured: 'ValidationTestModelAdmin.fk_filter' refers to field 'non_existent_field' that is missing from model 'ValidationTestModel'.
     534
     535>>> class ValidationTestModelAdmin(ModelAdmin):
     536...     fk_filter = ("name",)
     537>>> validate(ValidationTestModelAdmin, ValidationTestModel)
     538Traceback (most recent call last):
     539...
     540ImproperlyConfigured: 'ValidationTestModelAdmin.fk_filter[0]' must be a ForeignKey.
     541
     542>>> class ValidationTestModelAdmin(ModelAdmin):
     543...     fk_filter = ("band",)
     544>>> validate(ValidationTestModelAdmin, ValidationTestModel)
     545
    519546# filter_horizontal
    520547
    521548>>> class ValidationTestModelAdmin(ModelAdmin):
Back to Top