Ticket #14370: ticket-14730.diff

File ticket-14730.diff, 92.3 KB (added by istruble, 13 years ago)
  • 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
    a b  
    511511    border-top: 1px solid #ddd;
    512512}
    513513
     514/* AUTOCOMPLETE */
     515/* *** NOTE ***
     516   This is mostly culled from css generated by http://jqueryui.com/themeroller
     517   */
     518.ui-menu {
     519    display: block;
     520    float: left;
     521    list-style: none outside none;
     522    margin: 0;
     523    padding: 2px;
     524}
     525.ui-menu .ui-menu-item a {
     526    display: block;
     527    line-height: 1.5;
     528    padding: 0.2em 0.4em;
     529    text-decoration: none;
     530}
     531
     532.ui-autocomplete {
     533    cursor: default;
     534    position: absolute;
     535}
     536
     537form .aligned ul.ui-autocomplete {
     538    padding: 2px;
     539}
     540.ui-widget-content {
     541    background: #fcfdfd;
     542    border: 1px solid #a6c9e2;
     543    color: #222222;
     544}
     545.ui-menu .ui-menu-item {
     546    background: #fcfdfd;
     547    clear: left;
     548    float: left;
     549    margin: 0;
     550    padding: 0;
     551    width: 100%;
     552}
     553.ui-menu .ui-menu-item a {
     554    border: 1px solid #fcfdfd;
     555}
     556ul.ui-menu li,
     557.ui-autocomplete-value {
     558    list-style-type: none;
     559}
     560.ui-state-hover, .ui-state-focus {
     561    color: #fff;
     562    background: #417690;
     563    border: 1px solid #417690;
     564}
     565
     566.ui-autocomplete-value a {
     567    margin-left: 0.5em;
     568}
     569.djangoautocomplete-wrapper > * {
     570    float: left;
     571}
     572.djangoautocomplete-wrapper > ul {
     573    clear: left;
     574}
     575.related-lookup {
     576    position: relative;
     577    top: 4px;
     578    left: 2px;
     579}
  • django/contrib/admin/media/js/admin/RelatedObjectLookups.js

    diff --git a/django/contrib/admin/media/js/admin/RelatedObjectLookups.js b/django/contrib/admin/media/js/admin/RelatedObjectLookups.js
    a b  
    4444function dismissRelatedLookupPopup(win, chosenId) {
    4545    var name = windowname_to_id(win.name);
    4646    var elem = document.getElementById(name);
    47     if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
     47    var $ = django && django.jQuery;
     48    var autocomplete_elem = !!$ && $(elem);
     49    if (!!autocomplete_elem && !!autocomplete_elem.data('djangoautocomplete')) {
     50        $.getJSON(
     51                autocomplete_elem.data('djangoautocomplete').options.source,
     52                {term: chosenId, by_id: 1},
     53                function (data) {
     54                    // Pass the returned item to the normal
     55                    // autocomplete onSelect handler.
     56                    autocomplete_elem.data('autocomplete')
     57                        .options.select({}, {item: data[0]});
     58                });
     59    }
     60    else if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
    4861        elem.value += ',' + chosenId;
    4962    } else {
    50         document.getElementById(name).value = chosenId;
     63        elem.value = chosenId;
    5164    }
    5265    win.close();
    5366}
     
    7992            elem.options[elem.options.length] = o;
    8093            o.selected = true;
    8194        } else if (elem.nodeName == 'INPUT') {
    82             if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
     95            var autocomplete_elem = django && django.jQuery && django.jQuery(elem);
     96            if (!!autocomplete_elem && !!autocomplete_elem.data('djangoautocomplete')) {
     97                django.jQuery.getJSON(
     98                    autocomplete_elem.data('djangoautocomplete').options.source,
     99                    {term: newId, by_id: 1},
     100                    function (data) {
     101                        // Pass the returned item to the normal
     102                        // autocomplete onSelect handler.
     103                        autocomplete_elem.data('autocomplete')
     104                            .options.select({}, {item: data[0]});
     105                    });
     106            } else if (elem.className.indexOf('vManyToManyRawIdAdminField') != -1 && elem.value) {
    83107                elem.value += ',' + newId;
    84108            } else {
    85109                elem.value = newId;
  • new file django/contrib/admin/media/js/admin/autocomplete.js

    diff --git a/django/contrib/admin/media/js/admin/autocomplete.js b/django/contrib/admin/media/js/admin/autocomplete.js
    new file mode 100644
    - +  
     1(function ($) {
     2"use strict";
     3
     4$.widget("ui.djangoautocomplete", {
     5    options: {
     6        source: "../autocomplete/$name/",
     7        multiple: false,
     8        force_selection: true,
     9        renderItem: function (ul, item) {
     10            return $("<li></li>")
     11                .data("item.autocomplete", item)
     12                .append($("<a></a>").append(item.label))
     13                .appendTo(ul);
     14        },
     15      is_djangoautocomplete: true
     16    },
     17    _create: function () {
     18        var self = this;
     19        this.hidden_input = this.element.prev("input[type=hidden]");
     20        this.name = this.hidden_input.attr("name");
     21        this.element.autocomplete({
     22            appendTo: this.element.parent(),
     23            select: function (event, ui) {
     24                if (!ui.item) { return; /* unexpected result */ }
     25                var item = ui.item.data && ui.item.data("item.autocomplete") || ui.item;
     26                self.lastSelected = item;
     27                if (self.options.is_djangoautocomplete === true) {
     28                    if (self.options.multiple) {
     29                        if ($.inArray(item.id, self.values) < 0) {
     30                            $('<li></li>')
     31                                .addClass("ui-autocomplete-value")
     32                                .data("value.autocomplete", item.id)
     33                                .append(item.label + '<a href="#">x</a>')
     34                                .appendTo(self.values_ul);
     35                            self.values.push(item.id);
     36                        }
     37                    } else {
     38                        self.term = item.value;
     39                        self.element.val(item.value);
     40                    }
     41                    return false;
     42                }
     43            }
     44        }).data("autocomplete")._renderItem = this.options.renderItem;
     45        this._initSource();
     46        if (this.options.multiple) {
     47            this._initManyToMany();
     48        } else {
     49            this.lastSelected = {
     50                id: this.hidden_input.val(),
     51                value: this.element.val()
     52            };
     53        }
     54        if (this.options.force_selection) {
     55            this.element.focusout(function () {
     56                if (self.element.val() !== self.lastSelected.value) {
     57                    self.element.val("");
     58                }
     59            });
     60        }
     61        this.element.closest("form").submit(function () {
     62            if (self.options.multiple) {
     63                self.hidden_input.val(self.values.join(","));
     64            } else if (self.options.force_selection) {
     65                self.hidden_input.val(self.element.val() ? self.lastSelected.id : "");
     66            } else {
     67                self.hidden_input.val(self.element.val());
     68            }
     69        });
     70    },
     71   
     72    destroy: function () {
     73        this.element.autocomplete("destroy");
     74        if (this.options.multiple) {
     75            this.values_ul.remove();
     76        }
     77                $.Widget.prototype.destroy.call(this);
     78    },
     79
     80    _setOption: function (key, value) {
     81                $.Widget.prototype._setOption.apply(this, arguments);
     82        if (key === "source") {
     83            this._initSource();
     84        }
     85    },
     86
     87    _initSource: function () {
     88        var source = typeof this.options.source === "string" ?
     89            this.options.source.replace("$name", this.hidden_input.attr("name")) :
     90            this.options.source;
     91        this.element.autocomplete("option", "source", source);
     92    },
     93
     94    _initManyToMany: function () {
     95        var self = this;
     96        this.element.bind("autocompleteclose", function (event, ui) {
     97            self.element.val("");
     98        });
     99        this.values = [];
     100        if (this.hidden_input.val() !== "") {
     101            $.each(this.hidden_input.val().split(","), function (i, id) {
     102                self.values.push(parseInt(id, 10));
     103            });
     104        }
     105        this.values_ul = this.element.nextAll("ul.ui-autocomplete-values");
     106        this.lastSelected = { id: null, value: null };
     107        if (this.values.length && this.values_ul[0]) {
     108            this.values_ul.children().each(function (i) {
     109                $(this)
     110                    .addClass("ui-autocomplete-value")
     111                    .data("value.autocomplete", self.values[i])
     112                    .append('<a href="#">x</a>');
     113            });
     114        } else {
     115            this.values_ul = $("<ul></ul>").insertAfter(this.element);
     116        }
     117        this.values_ul.addClass("ui-autocomplete-values");
     118        $(".ui-autocomplete-value a", this.values_ul[0]).live("click", function () {
     119            var span = $(this).parent(),
     120                id = span.data("value.autocomplete");
     121            $.each(self.values, function (i, v) {
     122                if (v === id) {
     123                    self.values.splice(i, 1);
     124                }
     125            });
     126            span.remove();
     127        });
     128    }
     129});
     130
     131})(django.jQuery);
  • new file django/contrib/admin/media/js/jquery-ui.js

    diff --git a/django/contrib/admin/media/js/jquery-ui.js b/django/contrib/admin/media/js/jquery-ui.js
    new file mode 100644
    - +  
     1/*!
     2 * jQuery UI 1.8.5
     3 *
     4 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     5 * Dual licensed under the MIT or GPL Version 2 licenses.
     6 * http://jquery.org/license
     7 *
     8 * http://docs.jquery.com/UI
     9 */
     10(function( $, undefined ) {
     11
     12// prevent duplicate loading
     13// this is only a problem because we proxy existing functions
     14// and we don't want to double proxy them
     15$.ui = $.ui || {};
     16if ( $.ui.version ) {
     17        return;
     18}
     19
     20$.extend( $.ui, {
     21        version: "1.8.5",
     22
     23        keyCode: {
     24                ALT: 18,
     25                BACKSPACE: 8,
     26                CAPS_LOCK: 20,
     27                COMMA: 188,
     28                COMMAND: 91,
     29                COMMAND_LEFT: 91, // COMMAND
     30                COMMAND_RIGHT: 93,
     31                CONTROL: 17,
     32                DELETE: 46,
     33                DOWN: 40,
     34                END: 35,
     35                ENTER: 13,
     36                ESCAPE: 27,
     37                HOME: 36,
     38                INSERT: 45,
     39                LEFT: 37,
     40                MENU: 93, // COMMAND_RIGHT
     41                NUMPAD_ADD: 107,
     42                NUMPAD_DECIMAL: 110,
     43                NUMPAD_DIVIDE: 111,
     44                NUMPAD_ENTER: 108,
     45                NUMPAD_MULTIPLY: 106,
     46                NUMPAD_SUBTRACT: 109,
     47                PAGE_DOWN: 34,
     48                PAGE_UP: 33,
     49                PERIOD: 190,
     50                RIGHT: 39,
     51                SHIFT: 16,
     52                SPACE: 32,
     53                TAB: 9,
     54                UP: 38,
     55                WINDOWS: 91 // COMMAND
     56        }
     57});
     58
     59// plugins
     60$.fn.extend({
     61        _focus: $.fn.focus,
     62        focus: function( delay, fn ) {
     63                return typeof delay === "number" ?
     64                        this.each(function() {
     65                                var elem = this;
     66                                setTimeout(function() {
     67                                        $( elem ).focus();
     68                                        if ( fn ) {
     69                                                fn.call( elem );
     70                                        }
     71                                }, delay );
     72                        }) :
     73                        this._focus.apply( this, arguments );
     74        },
     75
     76        scrollParent: function() {
     77                var scrollParent;
     78                if (($.browser.msie && (/(static|relative)/).test(this.css('position'))) || (/absolute/).test(this.css('position'))) {
     79                        scrollParent = this.parents().filter(function() {
     80                                return (/(relative|absolute|fixed)/).test($.curCSS(this,'position',1)) && (/(auto|scroll)/).test($.curCSS(this,'overflow',1)+$.curCSS(this,'overflow-y',1)+$.curCSS(this,'overflow-x',1));
     81                        }).eq(0);
     82                } else {
     83                        scrollParent = this.parents().filter(function() {
     84                                return (/(auto|scroll)/).test($.curCSS(this,'overflow',1)+$.curCSS(this,'overflow-y',1)+$.curCSS(this,'overflow-x',1));
     85                        }).eq(0);
     86                }
     87
     88                return (/fixed/).test(this.css('position')) || !scrollParent.length ? $(document) : scrollParent;
     89        },
     90
     91        zIndex: function( zIndex ) {
     92                if ( zIndex !== undefined ) {
     93                        return this.css( "zIndex", zIndex );
     94                }
     95
     96                if ( this.length ) {
     97                        var elem = $( this[ 0 ] ), position, value;
     98                        while ( elem.length && elem[ 0 ] !== document ) {
     99                                // Ignore z-index if position is set to a value where z-index is ignored by the browser
     100                                // This makes behavior of this function consistent across browsers
     101                                // WebKit always returns auto if the element is positioned
     102                                position = elem.css( "position" );
     103                                if ( position === "absolute" || position === "relative" || position === "fixed" ) {
     104                                        // IE returns 0 when zIndex is not specified
     105                                        // other browsers return a string
     106                                        // we ignore the case of nested elements with an explicit value of 0
     107                                        // <div style="z-index: -10;"><div style="z-index: 0;"></div></div>
     108                                        value = parseInt( elem.css( "zIndex" ) );
     109                                        if ( !isNaN( value ) && value != 0 ) {
     110                                                return value;
     111                                        }
     112                                }
     113                                elem = elem.parent();
     114                        }
     115                }
     116
     117                return 0;
     118        },
     119       
     120        disableSelection: function() {
     121                return this.bind(
     122                        "mousedown.ui-disableSelection selectstart.ui-disableSelection",
     123                        function( event ) {
     124                                event.preventDefault();
     125                        });
     126        },
     127
     128        enableSelection: function() {
     129                return this.unbind( ".ui-disableSelection" );
     130        }
     131});
     132
     133$.each( [ "Width", "Height" ], function( i, name ) {
     134        var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
     135                type = name.toLowerCase(),
     136                orig = {
     137                        innerWidth: $.fn.innerWidth,
     138                        innerHeight: $.fn.innerHeight,
     139                        outerWidth: $.fn.outerWidth,
     140                        outerHeight: $.fn.outerHeight
     141                };
     142
     143        function reduce( elem, size, border, margin ) {
     144                $.each( side, function() {
     145                        size -= parseFloat( $.curCSS( elem, "padding" + this, true) ) || 0;
     146                        if ( border ) {
     147                                size -= parseFloat( $.curCSS( elem, "border" + this + "Width", true) ) || 0;
     148                        }
     149                        if ( margin ) {
     150                                size -= parseFloat( $.curCSS( elem, "margin" + this, true) ) || 0;
     151                        }
     152                });
     153                return size;
     154        }
     155
     156        $.fn[ "inner" + name ] = function( size ) {
     157                if ( size === undefined ) {
     158                        return orig[ "inner" + name ].call( this );
     159                }
     160
     161                return this.each(function() {
     162                        $.style( this, type, reduce( this, size ) + "px" );
     163                });
     164        };
     165
     166        $.fn[ "outer" + name] = function( size, margin ) {
     167                if ( typeof size !== "number" ) {
     168                        return orig[ "outer" + name ].call( this, size );
     169                }
     170
     171                return this.each(function() {
     172                        $.style( this, type, reduce( this, size, true, margin ) + "px" );
     173                });
     174        };
     175});
     176
     177// selectors
     178function visible( element ) {
     179        return !$( element ).parents().andSelf().filter(function() {
     180                return $.curCSS( this, "visibility" ) === "hidden" ||
     181                        $.expr.filters.hidden( this );
     182        }).length;
     183}
     184
     185$.extend( $.expr[ ":" ], {
     186        data: function( elem, i, match ) {
     187                return !!$.data( elem, match[ 3 ] );
     188        },
     189
     190        focusable: function( element ) {
     191                var nodeName = element.nodeName.toLowerCase(),
     192                        tabIndex = $.attr( element, "tabindex" );
     193                if ( "area" === nodeName ) {
     194                        var map = element.parentNode,
     195                                mapName = map.name,
     196                                img;
     197                        if ( !element.href || !mapName || map.nodeName.toLowerCase() !== "map" ) {
     198                                return false;
     199                        }
     200                        img = $( "img[usemap=#" + mapName + "]" )[0];
     201                        return !!img && visible( img );
     202                }
     203                return ( /input|select|textarea|button|object/.test( nodeName )
     204                        ? !element.disabled
     205                        : "a" == nodeName
     206                                ? element.href || !isNaN( tabIndex )
     207                                : !isNaN( tabIndex ))
     208                        // the element and all of its ancestors must be visible
     209                        && visible( element );
     210        },
     211
     212        tabbable: function( element ) {
     213                var tabIndex = $.attr( element, "tabindex" );
     214                return ( isNaN( tabIndex ) || tabIndex >= 0 ) && $( element ).is( ":focusable" );
     215        }
     216});
     217
     218// support
     219$(function() {
     220        var div = document.createElement( "div" ),
     221                body = document.body;
     222
     223        $.extend( div.style, {
     224                minHeight: "100px",
     225                height: "auto",
     226                padding: 0,
     227                borderWidth: 0
     228        });
     229
     230        $.support.minHeight = body.appendChild( div ).offsetHeight === 100;
     231        // set display to none to avoid a layout bug in IE
     232        // http://dev.jquery.com/ticket/4014
     233        body.removeChild( div ).style.display = "none";
     234});
     235
     236
     237
     238
     239
     240// deprecated
     241$.extend( $.ui, {
     242        // $.ui.plugin is deprecated.  Use the proxy pattern instead.
     243        plugin: {
     244                add: function( module, option, set ) {
     245                        var proto = $.ui[ module ].prototype;
     246                        for ( var i in set ) {
     247                                proto.plugins[ i ] = proto.plugins[ i ] || [];
     248                                proto.plugins[ i ].push( [ option, set[ i ] ] );
     249                        }
     250                },
     251                call: function( instance, name, args ) {
     252                        var set = instance.plugins[ name ];
     253                        if ( !set || !instance.element[ 0 ].parentNode ) {
     254                                return;
     255                        }
     256       
     257                        for ( var i = 0; i < set.length; i++ ) {
     258                                if ( instance.options[ set[ i ][ 0 ] ] ) {
     259                                        set[ i ][ 1 ].apply( instance.element, args );
     260                                }
     261                        }
     262                }
     263        },
     264       
     265        // will be deprecated when we switch to jQuery 1.4 - use jQuery.contains()
     266        contains: function( a, b ) {
     267                return document.compareDocumentPosition ?
     268                        a.compareDocumentPosition( b ) & 16 :
     269                        a !== b && a.contains( b );
     270        },
     271       
     272        // only used by resizable
     273        hasScroll: function( el, a ) {
     274       
     275                //If overflow is hidden, the element might have extra content, but the user wants to hide it
     276                if ( $( el ).css( "overflow" ) === "hidden") {
     277                        return false;
     278                }
     279       
     280                var scroll = ( a && a === "left" ) ? "scrollLeft" : "scrollTop",
     281                        has = false;
     282       
     283                if ( el[ scroll ] > 0 ) {
     284                        return true;
     285                }
     286       
     287                // TODO: determine which cases actually cause this to happen
     288                // if the element doesn't have the scroll set, see if it's possible to
     289                // set the scroll
     290                el[ scroll ] = 1;
     291                has = ( el[ scroll ] > 0 );
     292                el[ scroll ] = 0;
     293                return has;
     294        },
     295       
     296        // these are odd functions, fix the API or move into individual plugins
     297        isOverAxis: function( x, reference, size ) {
     298                //Determines when x coordinate is over "b" element axis
     299                return ( x > reference ) && ( x < ( reference + size ) );
     300        },
     301        isOver: function( y, x, top, left, height, width ) {
     302                //Determines when x, y coordinates is over "b" element
     303                return $.ui.isOverAxis( y, top, height ) && $.ui.isOverAxis( x, left, width );
     304        }
     305});
     306
     307})( jQuery );
     308/*!
     309 * jQuery UI Widget 1.8.5
     310 *
     311 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     312 * Dual licensed under the MIT or GPL Version 2 licenses.
     313 * http://jquery.org/license
     314 *
     315 * http://docs.jquery.com/UI/Widget
     316 */
     317(function( $, undefined ) {
     318
     319// jQuery 1.4+
     320if ( $.cleanData ) {
     321        var _cleanData = $.cleanData;
     322        $.cleanData = function( elems ) {
     323                for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
     324                        $( elem ).triggerHandler( "remove" );
     325                }
     326                _cleanData( elems );
     327        };
     328} else {
     329        var _remove = $.fn.remove;
     330        $.fn.remove = function( selector, keepData ) {
     331                return this.each(function() {
     332                        if ( !keepData ) {
     333                                if ( !selector || $.filter( selector, [ this ] ).length ) {
     334                                        $( "*", this ).add( [ this ] ).each(function() {
     335                                                $( this ).triggerHandler( "remove" );
     336                                        });
     337                                }
     338                        }
     339                        return _remove.call( $(this), selector, keepData );
     340                });
     341        };
     342}
     343
     344$.widget = function( name, base, prototype ) {
     345        var namespace = name.split( "." )[ 0 ],
     346                fullName;
     347        name = name.split( "." )[ 1 ];
     348        fullName = namespace + "-" + name;
     349
     350        if ( !prototype ) {
     351                prototype = base;
     352                base = $.Widget;
     353        }
     354
     355        // create selector for plugin
     356        $.expr[ ":" ][ fullName ] = function( elem ) {
     357                return !!$.data( elem, name );
     358        };
     359
     360        $[ namespace ] = $[ namespace ] || {};
     361        $[ namespace ][ name ] = function( options, element ) {
     362                // allow instantiation without initializing for simple inheritance
     363                if ( arguments.length ) {
     364                        this._createWidget( options, element );
     365                }
     366        };
     367
     368        var basePrototype = new base();
     369        // we need to make the options hash a property directly on the new instance
     370        // otherwise we'll modify the options hash on the prototype that we're
     371        // inheriting from
     372//      $.each( basePrototype, function( key, val ) {
     373//              if ( $.isPlainObject(val) ) {
     374//                      basePrototype[ key ] = $.extend( {}, val );
     375//              }
     376//      });
     377        basePrototype.options = $.extend( true, {}, basePrototype.options );
     378        $[ namespace ][ name ].prototype = $.extend( true, basePrototype, {
     379                namespace: namespace,
     380                widgetName: name,
     381                widgetEventPrefix: $[ namespace ][ name ].prototype.widgetEventPrefix || name,
     382                widgetBaseClass: fullName
     383        }, prototype );
     384
     385        $.widget.bridge( name, $[ namespace ][ name ] );
     386};
     387
     388$.widget.bridge = function( name, object ) {
     389        $.fn[ name ] = function( options ) {
     390                var isMethodCall = typeof options === "string",
     391                        args = Array.prototype.slice.call( arguments, 1 ),
     392                        returnValue = this;
     393
     394                // allow multiple hashes to be passed on init
     395                options = !isMethodCall && args.length ?
     396                        $.extend.apply( null, [ true, options ].concat(args) ) :
     397                        options;
     398
     399                // prevent calls to internal methods
     400                if ( isMethodCall && options.substring( 0, 1 ) === "_" ) {
     401                        return returnValue;
     402                }
     403
     404                if ( isMethodCall ) {
     405                        this.each(function() {
     406                                var instance = $.data( this, name );
     407                                if ( !instance ) {
     408                                        throw "cannot call methods on " + name + " prior to initialization; " +
     409                                                "attempted to call method '" + options + "'";
     410                                }
     411                                if ( !$.isFunction( instance[options] ) ) {
     412                                        throw "no such method '" + options + "' for " + name + " widget instance";
     413                                }
     414                                var methodValue = instance[ options ].apply( instance, args );
     415                                if ( methodValue !== instance && methodValue !== undefined ) {
     416                                        returnValue = methodValue;
     417                                        return false;
     418                                }
     419                        });
     420                } else {
     421                        this.each(function() {
     422                                var instance = $.data( this, name );
     423                                if ( instance ) {
     424                                        instance.option( options || {} )._init();
     425                                } else {
     426                                        $.data( this, name, new object( options, this ) );
     427                                }
     428                        });
     429                }
     430
     431                return returnValue;
     432        };
     433};
     434
     435$.Widget = function( options, element ) {
     436        // allow instantiation without initializing for simple inheritance
     437        if ( arguments.length ) {
     438                this._createWidget( options, element );
     439        }
     440};
     441
     442$.Widget.prototype = {
     443        widgetName: "widget",
     444        widgetEventPrefix: "",
     445        options: {
     446                disabled: false
     447        },
     448        _createWidget: function( options, element ) {
     449                // $.widget.bridge stores the plugin instance, but we do it anyway
     450                // so that it's stored even before the _create function runs
     451                $.data( element, this.widgetName, this );
     452                this.element = $( element );
     453                this.options = $.extend( true, {},
     454                        this.options,
     455                        $.metadata && $.metadata.get( element )[ this.widgetName ],
     456                        options );
     457
     458                var self = this;
     459                this.element.bind( "remove." + this.widgetName, function() {
     460                        self.destroy();
     461                });
     462
     463                this._create();
     464                this._init();
     465        },
     466        _create: function() {},
     467        _init: function() {},
     468
     469        destroy: function() {
     470                this.element
     471                        .unbind( "." + this.widgetName )
     472                        .removeData( this.widgetName );
     473                this.widget()
     474                        .unbind( "." + this.widgetName )
     475                        .removeAttr( "aria-disabled" )
     476                        .removeClass(
     477                                this.widgetBaseClass + "-disabled " +
     478                                "ui-state-disabled" );
     479        },
     480
     481        widget: function() {
     482                return this.element;
     483        },
     484
     485        option: function( key, value ) {
     486                var options = key,
     487                        self = this;
     488
     489                if ( arguments.length === 0 ) {
     490                        // don't return a reference to the internal hash
     491                        return $.extend( {}, self.options );
     492                }
     493
     494                if  (typeof key === "string" ) {
     495                        if ( value === undefined ) {
     496                                return this.options[ key ];
     497                        }
     498                        options = {};
     499                        options[ key ] = value;
     500                }
     501
     502                $.each( options, function( key, value ) {
     503                        self._setOption( key, value );
     504                });
     505
     506                return self;
     507        },
     508        _setOption: function( key, value ) {
     509                this.options[ key ] = value;
     510
     511                if ( key === "disabled" ) {
     512                        this.widget()
     513                                [ value ? "addClass" : "removeClass"](
     514                                        this.widgetBaseClass + "-disabled" + " " +
     515                                        "ui-state-disabled" )
     516                                .attr( "aria-disabled", value );
     517                }
     518
     519                return this;
     520        },
     521
     522        enable: function() {
     523                return this._setOption( "disabled", false );
     524        },
     525        disable: function() {
     526                return this._setOption( "disabled", true );
     527        },
     528
     529        _trigger: function( type, event, data ) {
     530                var callback = this.options[ type ];
     531
     532                event = $.Event( event );
     533                event.type = ( type === this.widgetEventPrefix ?
     534                        type :
     535                        this.widgetEventPrefix + type ).toLowerCase();
     536                data = data || {};
     537
     538                // copy original event properties over to the new event
     539                // this would happen if we could call $.event.fix instead of $.Event
     540                // but we don't have a way to force an event to be fixed multiple times
     541                if ( event.originalEvent ) {
     542                        for ( var i = $.event.props.length, prop; i; ) {
     543                                prop = $.event.props[ --i ];
     544                                event[ prop ] = event.originalEvent[ prop ];
     545                        }
     546                }
     547
     548                this.element.trigger( event, data );
     549
     550                return !( $.isFunction(callback) &&
     551                        callback.call( this.element[0], event, data ) === false ||
     552                        event.isDefaultPrevented() );
     553        }
     554};
     555
     556})( jQuery );
     557/*
     558 * jQuery UI Position 1.8.5
     559 *
     560 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     561 * Dual licensed under the MIT or GPL Version 2 licenses.
     562 * http://jquery.org/license
     563 *
     564 * http://docs.jquery.com/UI/Position
     565 */
     566(function( $, undefined ) {
     567
     568$.ui = $.ui || {};
     569
     570var horizontalPositions = /left|center|right/,
     571        verticalPositions = /top|center|bottom/,
     572        center = "center",
     573        _position = $.fn.position,
     574        _offset = $.fn.offset;
     575
     576$.fn.position = function( options ) {
     577        if ( !options || !options.of ) {
     578                return _position.apply( this, arguments );
     579        }
     580
     581        // make a copy, we don't want to modify arguments
     582        options = $.extend( {}, options );
     583
     584        var target = $( options.of ),
     585                targetElem = target[0],
     586                collision = ( options.collision || "flip" ).split( " " ),
     587                offset = options.offset ? options.offset.split( " " ) : [ 0, 0 ],
     588                targetWidth,
     589                targetHeight,
     590                basePosition;
     591
     592        if ( targetElem.nodeType === 9 ) {
     593                targetWidth = target.width();
     594                targetHeight = target.height();
     595                basePosition = { top: 0, left: 0 };
     596        } else if ( targetElem.scrollTo && targetElem.document ) {
     597                targetWidth = target.width();
     598                targetHeight = target.height();
     599                basePosition = { top: target.scrollTop(), left: target.scrollLeft() };
     600        } else if ( targetElem.preventDefault ) {
     601                // force left top to allow flipping
     602                options.at = "left top";
     603                targetWidth = targetHeight = 0;
     604                basePosition = { top: options.of.pageY, left: options.of.pageX };
     605        } else {
     606                targetWidth = target.outerWidth();
     607                targetHeight = target.outerHeight();
     608                basePosition = target.offset();
     609        }
     610
     611        // force my and at to have valid horizontal and veritcal positions
     612        // if a value is missing or invalid, it will be converted to center
     613        $.each( [ "my", "at" ], function() {
     614                var pos = ( options[this] || "" ).split( " " );
     615                if ( pos.length === 1) {
     616                        pos = horizontalPositions.test( pos[0] ) ?
     617                                pos.concat( [center] ) :
     618                                verticalPositions.test( pos[0] ) ?
     619                                        [ center ].concat( pos ) :
     620                                        [ center, center ];
     621                }
     622                pos[ 0 ] = horizontalPositions.test( pos[0] ) ? pos[ 0 ] : center;
     623                pos[ 1 ] = verticalPositions.test( pos[1] ) ? pos[ 1 ] : center;
     624                options[ this ] = pos;
     625        });
     626
     627        // normalize collision option
     628        if ( collision.length === 1 ) {
     629                collision[ 1 ] = collision[ 0 ];
     630        }
     631
     632        // normalize offset option
     633        offset[ 0 ] = parseInt( offset[0], 10 ) || 0;
     634        if ( offset.length === 1 ) {
     635                offset[ 1 ] = offset[ 0 ];
     636        }
     637        offset[ 1 ] = parseInt( offset[1], 10 ) || 0;
     638
     639        if ( options.at[0] === "right" ) {
     640                basePosition.left += targetWidth;
     641        } else if (options.at[0] === center ) {
     642                basePosition.left += targetWidth / 2;
     643        }
     644
     645        if ( options.at[1] === "bottom" ) {
     646                basePosition.top += targetHeight;
     647        } else if ( options.at[1] === center ) {
     648                basePosition.top += targetHeight / 2;
     649        }
     650
     651        basePosition.left += offset[ 0 ];
     652        basePosition.top += offset[ 1 ];
     653
     654        return this.each(function() {
     655                var elem = $( this ),
     656                        elemWidth = elem.outerWidth(),
     657                        elemHeight = elem.outerHeight(),
     658                        marginLeft = parseInt( $.curCSS( this, "marginLeft", true ) ) || 0,
     659                        marginTop = parseInt( $.curCSS( this, "marginTop", true ) ) || 0,
     660                        collisionWidth = elemWidth + marginLeft +
     661                                parseInt( $.curCSS( this, "marginRight", true ) ) || 0,
     662                        collisionHeight = elemHeight + marginTop +
     663                                parseInt( $.curCSS( this, "marginBottom", true ) ) || 0,
     664                        position = $.extend( {}, basePosition ),
     665                        collisionPosition;
     666
     667                if ( options.my[0] === "right" ) {
     668                        position.left -= elemWidth;
     669                } else if ( options.my[0] === center ) {
     670                        position.left -= elemWidth / 2;
     671                }
     672
     673                if ( options.my[1] === "bottom" ) {
     674                        position.top -= elemHeight;
     675                } else if ( options.my[1] === center ) {
     676                        position.top -= elemHeight / 2;
     677                }
     678
     679                // prevent fractions (see #5280)
     680                position.left = parseInt( position.left );
     681                position.top = parseInt( position.top );
     682
     683                collisionPosition = {
     684                        left: position.left - marginLeft,
     685                        top: position.top - marginTop
     686                };
     687
     688                $.each( [ "left", "top" ], function( i, dir ) {
     689                        if ( $.ui.position[ collision[i] ] ) {
     690                                $.ui.position[ collision[i] ][ dir ]( position, {
     691                                        targetWidth: targetWidth,
     692                                        targetHeight: targetHeight,
     693                                        elemWidth: elemWidth,
     694                                        elemHeight: elemHeight,
     695                                        collisionPosition: collisionPosition,
     696                                        collisionWidth: collisionWidth,
     697                                        collisionHeight: collisionHeight,
     698                                        offset: offset,
     699                                        my: options.my,
     700                                        at: options.at
     701                                });
     702                        }
     703                });
     704
     705                if ( $.fn.bgiframe ) {
     706                        elem.bgiframe();
     707                }
     708                elem.offset( $.extend( position, { using: options.using } ) );
     709        });
     710};
     711
     712$.ui.position = {
     713        fit: {
     714                left: function( position, data ) {
     715                        var win = $( window ),
     716                                over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft();
     717                        position.left = over > 0 ? position.left - over : Math.max( position.left - data.collisionPosition.left, position.left );
     718                },
     719                top: function( position, data ) {
     720                        var win = $( window ),
     721                                over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop();
     722                        position.top = over > 0 ? position.top - over : Math.max( position.top - data.collisionPosition.top, position.top );
     723                }
     724        },
     725
     726        flip: {
     727                left: function( position, data ) {
     728                        if ( data.at[0] === center ) {
     729                                return;
     730                        }
     731                        var win = $( window ),
     732                                over = data.collisionPosition.left + data.collisionWidth - win.width() - win.scrollLeft(),
     733                                myOffset = data.my[ 0 ] === "left" ?
     734                                        -data.elemWidth :
     735                                        data.my[ 0 ] === "right" ?
     736                                                data.elemWidth :
     737                                                0,
     738                                atOffset = data.at[ 0 ] === "left" ?
     739                                        data.targetWidth :
     740                                        -data.targetWidth,
     741                                offset = -2 * data.offset[ 0 ];
     742                        position.left += data.collisionPosition.left < 0 ?
     743                                myOffset + atOffset + offset :
     744                                over > 0 ?
     745                                        myOffset + atOffset + offset :
     746                                        0;
     747                },
     748                top: function( position, data ) {
     749                        if ( data.at[1] === center ) {
     750                                return;
     751                        }
     752                        var win = $( window ),
     753                                over = data.collisionPosition.top + data.collisionHeight - win.height() - win.scrollTop(),
     754                                myOffset = data.my[ 1 ] === "top" ?
     755                                        -data.elemHeight :
     756                                        data.my[ 1 ] === "bottom" ?
     757                                                data.elemHeight :
     758                                                0,
     759                                atOffset = data.at[ 1 ] === "top" ?
     760                                        data.targetHeight :
     761                                        -data.targetHeight,
     762                                offset = -2 * data.offset[ 1 ];
     763                        position.top += data.collisionPosition.top < 0 ?
     764                                myOffset + atOffset + offset :
     765                                over > 0 ?
     766                                        myOffset + atOffset + offset :
     767                                        0;
     768                }
     769        }
     770};
     771
     772// offset setter from jQuery 1.4
     773if ( !$.offset.setOffset ) {
     774        $.offset.setOffset = function( elem, options ) {
     775                // set position first, in-case top/left are set even on static elem
     776                if ( /static/.test( $.curCSS( elem, "position" ) ) ) {
     777                        elem.style.position = "relative";
     778                }
     779                var curElem   = $( elem ),
     780                        curOffset = curElem.offset(),
     781                        curTop    = parseInt( $.curCSS( elem, "top",  true ), 10 ) || 0,
     782                        curLeft   = parseInt( $.curCSS( elem, "left", true ), 10)  || 0,
     783                        props     = {
     784                                top:  (options.top  - curOffset.top)  + curTop,
     785                                left: (options.left - curOffset.left) + curLeft
     786                        };
     787               
     788                if ( 'using' in options ) {
     789                        options.using.call( elem, props );
     790                } else {
     791                        curElem.css( props );
     792                }
     793        };
     794
     795        $.fn.offset = function( options ) {
     796                var elem = this[ 0 ];
     797                if ( !elem || !elem.ownerDocument ) { return null; }
     798                if ( options ) {
     799                        return this.each(function() {
     800                                $.offset.setOffset( this, options );
     801                        });
     802                }
     803                return _offset.call( this );
     804        };
     805}
     806
     807}( jQuery ));
     808/*
     809 * jQuery UI Autocomplete 1.8.5
     810 *
     811 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     812 * Dual licensed under the MIT or GPL Version 2 licenses.
     813 * http://jquery.org/license
     814 *
     815 * http://docs.jquery.com/UI/Autocomplete
     816 *
     817 * Depends:
     818 *      jquery.ui.core.js
     819 *      jquery.ui.widget.js
     820 *      jquery.ui.position.js
     821 */
     822(function( $, undefined ) {
     823
     824$.widget( "ui.autocomplete", {
     825        options: {
     826                appendTo: "body",
     827                delay: 300,
     828                minLength: 1,
     829                position: {
     830                        my: "left top",
     831                        at: "left bottom",
     832                        collision: "none"
     833                },
     834                source: null
     835        },
     836        _create: function() {
     837                var self = this,
     838                        doc = this.element[ 0 ].ownerDocument;
     839                this.element
     840                        .addClass( "ui-autocomplete-input" )
     841                        .attr( "autocomplete", "off" )
     842                        // TODO verify these actually work as intended
     843                        .attr({
     844                                role: "textbox",
     845                                "aria-autocomplete": "list",
     846                                "aria-haspopup": "true"
     847                        })
     848                        .bind( "keydown.autocomplete", function( event ) {
     849                                if ( self.options.disabled ) {
     850                                        return;
     851                                }
     852
     853                                var keyCode = $.ui.keyCode;
     854                                switch( event.keyCode ) {
     855                                case keyCode.PAGE_UP:
     856                                        self._move( "previousPage", event );
     857                                        break;
     858                                case keyCode.PAGE_DOWN:
     859                                        self._move( "nextPage", event );
     860                                        break;
     861                                case keyCode.UP:
     862                                        self._move( "previous", event );
     863                                        // prevent moving cursor to beginning of text field in some browsers
     864                                        event.preventDefault();
     865                                        break;
     866                                case keyCode.DOWN:
     867                                        self._move( "next", event );
     868                                        // prevent moving cursor to end of text field in some browsers
     869                                        event.preventDefault();
     870                                        break;
     871                                case keyCode.ENTER:
     872                                case keyCode.NUMPAD_ENTER:
     873                                        // when menu is open or has focus
     874                                        if ( self.menu.element.is( ":visible" ) ) {
     875                                                event.preventDefault();
     876                                        }
     877                                        //passthrough - ENTER and TAB both select the current element
     878                                case keyCode.TAB:
     879                                        if ( !self.menu.active ) {
     880                                                return;
     881                                        }
     882                                        self.menu.select( event );
     883                                        break;
     884                                case keyCode.ESCAPE:
     885                                        self.element.val( self.term );
     886                                        self.close( event );
     887                                        break;
     888                                default:
     889                                        // keypress is triggered before the input value is changed
     890                                        clearTimeout( self.searching );
     891                                        self.searching = setTimeout(function() {
     892                                                // only search if the value has changed
     893                                                if ( self.term != self.element.val() ) {
     894                                                        self.selectedItem = null;
     895                                                        self.search( null, event );
     896                                                }
     897                                        }, self.options.delay );
     898                                        break;
     899                                }
     900                        })
     901                        .bind( "focus.autocomplete", function() {
     902                                if ( self.options.disabled ) {
     903                                        return;
     904                                }
     905
     906                                self.selectedItem = null;
     907                                self.previous = self.element.val();
     908                        })
     909                        .bind( "blur.autocomplete", function( event ) {
     910                                if ( self.options.disabled ) {
     911                                        return;
     912                                }
     913
     914                                clearTimeout( self.searching );
     915                                // clicks on the menu (or a button to trigger a search) will cause a blur event
     916                                self.closing = setTimeout(function() {
     917                                        self.close( event );
     918                                        self._change( event );
     919                                }, 150 );
     920                        });
     921                this._initSource();
     922                this.response = function() {
     923                        return self._response.apply( self, arguments );
     924                };
     925                this.menu = $( "<ul></ul>" )
     926                        .addClass( "ui-autocomplete" )
     927                        .appendTo( $( this.options.appendTo || "body", doc )[0] )
     928                        // prevent the close-on-blur in case of a "slow" click on the menu (long mousedown)
     929                        .mousedown(function( event ) {
     930                                // clicking on the scrollbar causes focus to shift to the body
     931                                // but we can't detect a mouseup or a click immediately afterward
     932                                // so we have to track the next mousedown and close the menu if
     933                                // the user clicks somewhere outside of the autocomplete
     934                                var menuElement = self.menu.element[ 0 ];
     935                                if ( event.target === menuElement ) {
     936                                        setTimeout(function() {
     937                                                $( document ).one( 'mousedown', function( event ) {
     938                                                        if ( event.target !== self.element[ 0 ] &&
     939                                                                event.target !== menuElement &&
     940                                                                !$.ui.contains( menuElement, event.target ) ) {
     941                                                                self.close();
     942                                                        }
     943                                                });
     944                                        }, 1 );
     945                                }
     946
     947                                // use another timeout to make sure the blur-event-handler on the input was already triggered
     948                                setTimeout(function() {
     949                                        clearTimeout( self.closing );
     950                                }, 13);
     951                        })
     952                        .menu({
     953                                focus: function( event, ui ) {
     954                                        var item = ui.item.data( "item.autocomplete" );
     955                                        if ( false !== self._trigger( "focus", null, { item: item } ) ) {
     956                                                // use value to match what will end up in the input, if it was a key event
     957                                                if ( /^key/.test(event.originalEvent.type) ) {
     958                                                        self.element.val( item.value );
     959                                                }
     960                                        }
     961                                },
     962                                selected: function( event, ui ) {
     963                                        var item = ui.item.data( "item.autocomplete" ),
     964                                                previous = self.previous;
     965
     966                                        // only trigger when focus was lost (click on menu)
     967                                        if ( self.element[0] !== doc.activeElement ) {
     968                                                self.element.focus();
     969                                                self.previous = previous;
     970                                        }
     971
     972                                        if ( false !== self._trigger( "select", event, { item: item } ) ) {
     973                                                self.term = item.value;
     974                                                self.element.val( item.value );
     975                                        }
     976
     977                                        self.close( event );
     978                                        self.selectedItem = item;
     979                                },
     980                                blur: function( event, ui ) {
     981                                        // don't set the value of the text field if it's already correct
     982                                        // this prevents moving the cursor unnecessarily
     983                                        if ( self.menu.element.is(":visible") &&
     984                                                ( self.element.val() !== self.term ) ) {
     985                                                self.element.val( self.term );
     986                                        }
     987                                }
     988                        })
     989                        .zIndex( this.element.zIndex() + 1 )
     990                        // workaround for jQuery bug #5781 http://dev.jquery.com/ticket/5781
     991                        .css({ top: 0, left: 0 })
     992                        .hide()
     993                        .data( "menu" );
     994                if ( $.fn.bgiframe ) {
     995                         this.menu.element.bgiframe();
     996                }
     997        },
     998
     999        destroy: function() {
     1000                this.element
     1001                        .removeClass( "ui-autocomplete-input" )
     1002                        .removeAttr( "autocomplete" )
     1003                        .removeAttr( "role" )
     1004                        .removeAttr( "aria-autocomplete" )
     1005                        .removeAttr( "aria-haspopup" );
     1006                this.menu.element.remove();
     1007                $.Widget.prototype.destroy.call( this );
     1008        },
     1009
     1010        _setOption: function( key, value ) {
     1011                $.Widget.prototype._setOption.apply( this, arguments );
     1012                if ( key === "source" ) {
     1013                        this._initSource();
     1014                }
     1015                if ( key === "appendTo" ) {
     1016                        this.menu.element.appendTo( $( value || "body", this.element[0].ownerDocument )[0] )
     1017                }
     1018        },
     1019
     1020        _initSource: function() {
     1021                var self = this,
     1022                        array,
     1023                        url;
     1024                if ( $.isArray(this.options.source) ) {
     1025                        array = this.options.source;
     1026                        this.source = function( request, response ) {
     1027                                response( $.ui.autocomplete.filter(array, request.term) );
     1028                        };
     1029                } else if ( typeof this.options.source === "string" ) {
     1030                        url = this.options.source;
     1031                        this.source = function( request, response ) {
     1032                                if (self.xhr) {
     1033                                        self.xhr.abort();
     1034                                }
     1035                                self.xhr = $.getJSON( url, request, function( data, status, xhr ) {
     1036                                        if ( xhr === self.xhr ) {
     1037                                                response( data );
     1038                                        }
     1039                                        self.xhr = null;
     1040                                });
     1041                        };
     1042                } else {
     1043                        this.source = this.options.source;
     1044                }
     1045        },
     1046
     1047        search: function( value, event ) {
     1048                value = value != null ? value : this.element.val();
     1049
     1050                // always save the actual value, not the one passed as an argument
     1051                this.term = this.element.val();
     1052
     1053                if ( value.length < this.options.minLength ) {
     1054                        return this.close( event );
     1055                }
     1056
     1057                clearTimeout( this.closing );
     1058                if ( this._trigger("search") === false ) {
     1059                        return;
     1060                }
     1061
     1062                return this._search( value );
     1063        },
     1064
     1065        _search: function( value ) {
     1066                this.element.addClass( "ui-autocomplete-loading" );
     1067
     1068                this.source( { term: value }, this.response );
     1069        },
     1070
     1071        _response: function( content ) {
     1072                if ( content.length ) {
     1073                        content = this._normalize( content );
     1074                        this._suggest( content );
     1075                        this._trigger( "open" );
     1076                } else {
     1077                        this.close();
     1078                }
     1079                this.element.removeClass( "ui-autocomplete-loading" );
     1080        },
     1081
     1082        close: function( event ) {
     1083                clearTimeout( this.closing );
     1084                if ( this.menu.element.is(":visible") ) {
     1085                        this._trigger( "close", event );
     1086                        this.menu.element.hide();
     1087                        this.menu.deactivate();
     1088                }
     1089        },
     1090       
     1091        _change: function( event ) {
     1092                if ( this.previous !== this.element.val() ) {
     1093                        this._trigger( "change", event, { item: this.selectedItem } );
     1094                }
     1095        },
     1096
     1097        _normalize: function( items ) {
     1098                // assume all items have the right format when the first item is complete
     1099                if ( items.length && items[0].label && items[0].value ) {
     1100                        return items;
     1101                }
     1102                return $.map( items, function(item) {
     1103                        if ( typeof item === "string" ) {
     1104                                return {
     1105                                        label: item,
     1106                                        value: item
     1107                                };
     1108                        }
     1109                        return $.extend({
     1110                                label: item.label || item.value,
     1111                                value: item.value || item.label
     1112                        }, item );
     1113                });
     1114        },
     1115
     1116        _suggest: function( items ) {
     1117                var ul = this.menu.element
     1118                                .empty()
     1119                                .zIndex( this.element.zIndex() + 1 ),
     1120                        menuWidth,
     1121                        textWidth;
     1122                this._renderMenu( ul, items );
     1123                // TODO refresh should check if the active item is still in the dom, removing the need for a manual deactivate
     1124                this.menu.deactivate();
     1125                this.menu.refresh();
     1126                this.menu.element.show().position( $.extend({
     1127                        of: this.element
     1128                }, this.options.position ));
     1129
     1130                menuWidth = ul.width( "" ).outerWidth();
     1131                textWidth = this.element.outerWidth();
     1132                ul.outerWidth( Math.max( menuWidth, textWidth ) );
     1133        },
     1134
     1135        _renderMenu: function( ul, items ) {
     1136                var self = this;
     1137                $.each( items, function( index, item ) {
     1138                        self._renderItem( ul, item );
     1139                });
     1140        },
     1141
     1142        _renderItem: function( ul, item) {
     1143                return $( "<li></li>" )
     1144                        .data( "item.autocomplete", item )
     1145                        .append( $( "<a></a>" ).text( item.label ) )
     1146                        .appendTo( ul );
     1147        },
     1148
     1149        _move: function( direction, event ) {
     1150                if ( !this.menu.element.is(":visible") ) {
     1151                        this.search( null, event );
     1152                        return;
     1153                }
     1154                if ( this.menu.first() && /^previous/.test(direction) ||
     1155                                this.menu.last() && /^next/.test(direction) ) {
     1156                        this.element.val( this.term );
     1157                        this.menu.deactivate();
     1158                        return;
     1159                }
     1160                this.menu[ direction ]( event );
     1161        },
     1162
     1163        widget: function() {
     1164                return this.menu.element;
     1165        }
     1166});
     1167
     1168$.extend( $.ui.autocomplete, {
     1169        escapeRegex: function( value ) {
     1170                return value.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
     1171        },
     1172        filter: function(array, term) {
     1173                var matcher = new RegExp( $.ui.autocomplete.escapeRegex(term), "i" );
     1174                return $.grep( array, function(value) {
     1175                        return matcher.test( value.label || value.value || value );
     1176                });
     1177        }
     1178});
     1179
     1180}( jQuery ));
     1181
     1182/*
     1183 * jQuery UI Menu (not officially released)
     1184 *
     1185 * This widget isn't yet finished and the API is subject to change. We plan to finish
     1186 * it for the next release. You're welcome to give it a try anyway and give us feedback,
     1187 * as long as you're okay with migrating your code later on. We can help with that, too.
     1188 *
     1189 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     1190 * Dual licensed under the MIT or GPL Version 2 licenses.
     1191 * http://jquery.org/license
     1192 *
     1193 * http://docs.jquery.com/UI/Menu
     1194 *
     1195 * Depends:
     1196 *      jquery.ui.core.js
     1197 *  jquery.ui.widget.js
     1198 */
     1199(function($) {
     1200
     1201$.widget("ui.menu", {
     1202        _create: function() {
     1203                var self = this;
     1204                this.element
     1205                        .addClass("ui-menu ui-widget ui-widget-content ui-corner-all")
     1206                        .attr({
     1207                                role: "listbox",
     1208                                "aria-activedescendant": "ui-active-menuitem"
     1209                        })
     1210                        .click(function( event ) {
     1211                                if ( !$( event.target ).closest( ".ui-menu-item a" ).length ) {
     1212                                        return;
     1213                                }
     1214                                // temporary
     1215                                event.preventDefault();
     1216                                self.select( event );
     1217                        });
     1218                this.refresh();
     1219        },
     1220       
     1221        refresh: function() {
     1222                var self = this;
     1223
     1224                // don't refresh list items that are already adapted
     1225                var items = this.element.children("li:not(.ui-menu-item):has(a)")
     1226                        .addClass("ui-menu-item")
     1227                        .attr("role", "menuitem");
     1228               
     1229                items.children("a")
     1230                        .addClass("ui-corner-all")
     1231                        .attr("tabindex", -1)
     1232                        // mouseenter doesn't work with event delegation
     1233                        .mouseenter(function( event ) {
     1234                                self.activate( event, $(this).parent() );
     1235                        })
     1236                        .mouseleave(function() {
     1237                                self.deactivate();
     1238                        });
     1239        },
     1240
     1241        activate: function( event, item ) {
     1242                this.deactivate();
     1243                if (this.hasScroll()) {
     1244                        var offset = item.offset().top - this.element.offset().top,
     1245                                scroll = this.element.attr("scrollTop"),
     1246                                elementHeight = this.element.height();
     1247                        if (offset < 0) {
     1248                                this.element.attr("scrollTop", scroll + offset);
     1249                        } else if (offset >= elementHeight) {
     1250                                this.element.attr("scrollTop", scroll + offset - elementHeight + item.height());
     1251                        }
     1252                }
     1253                this.active = item.eq(0)
     1254                        .children("a")
     1255                                .addClass("ui-state-hover")
     1256                                .attr("id", "ui-active-menuitem")
     1257                        .end();
     1258                this._trigger("focus", event, { item: item });
     1259        },
     1260
     1261        deactivate: function() {
     1262                if (!this.active) { return; }
     1263
     1264                this.active.children("a")
     1265                        .removeClass("ui-state-hover")
     1266                        .removeAttr("id");
     1267                this._trigger("blur");
     1268                this.active = null;
     1269        },
     1270
     1271        next: function(event) {
     1272                this.move("next", ".ui-menu-item:first", event);
     1273        },
     1274
     1275        previous: function(event) {
     1276                this.move("prev", ".ui-menu-item:last", event);
     1277        },
     1278
     1279        first: function() {
     1280                return this.active && !this.active.prevAll(".ui-menu-item").length;
     1281        },
     1282
     1283        last: function() {
     1284                return this.active && !this.active.nextAll(".ui-menu-item").length;
     1285        },
     1286
     1287        move: function(direction, edge, event) {
     1288                if (!this.active) {
     1289                        this.activate(event, this.element.children(edge));
     1290                        return;
     1291                }
     1292                var next = this.active[direction + "All"](".ui-menu-item").eq(0);
     1293                if (next.length) {
     1294                        this.activate(event, next);
     1295                } else {
     1296                        this.activate(event, this.element.children(edge));
     1297                }
     1298        },
     1299
     1300        // TODO merge with previousPage
     1301        nextPage: function(event) {
     1302                if (this.hasScroll()) {
     1303                        // TODO merge with no-scroll-else
     1304                        if (!this.active || this.last()) {
     1305                                this.activate(event, this.element.children(":first"));
     1306                                return;
     1307                        }
     1308                        var base = this.active.offset().top,
     1309                                height = this.element.height(),
     1310                                result = this.element.children("li").filter(function() {
     1311                                        var close = $(this).offset().top - base - height + $(this).height();
     1312                                        // TODO improve approximation
     1313                                        return close < 10 && close > -10;
     1314                                });
     1315
     1316                        // TODO try to catch this earlier when scrollTop indicates the last page anyway
     1317                        if (!result.length) {
     1318                                result = this.element.children(":last");
     1319                        }
     1320                        this.activate(event, result);
     1321                } else {
     1322                        this.activate(event, this.element.children(!this.active || this.last() ? ":first" : ":last"));
     1323                }
     1324        },
     1325
     1326        // TODO merge with nextPage
     1327        previousPage: function(event) {
     1328                if (this.hasScroll()) {
     1329                        // TODO merge with no-scroll-else
     1330                        if (!this.active || this.first()) {
     1331                                this.activate(event, this.element.children(":last"));
     1332                                return;
     1333                        }
     1334
     1335                        var base = this.active.offset().top,
     1336                                height = this.element.height();
     1337                                result = this.element.children("li").filter(function() {
     1338                                        var close = $(this).offset().top - base + height - $(this).height();
     1339                                        // TODO improve approximation
     1340                                        return close < 10 && close > -10;
     1341                                });
     1342
     1343                        // TODO try to catch this earlier when scrollTop indicates the last page anyway
     1344                        if (!result.length) {
     1345                                result = this.element.children(":first");
     1346                        }
     1347                        this.activate(event, result);
     1348                } else {
     1349                        this.activate(event, this.element.children(!this.active || this.first() ? ":last" : ":first"));
     1350                }
     1351        },
     1352
     1353        hasScroll: function() {
     1354                return this.element.height() < this.element.attr("scrollHeight");
     1355        },
     1356
     1357        select: function( event ) {
     1358                this._trigger("selected", event, { item: this.active });
     1359        }
     1360});
     1361
     1362}(jQuery));
  • new file django/contrib/admin/media/js/jquery-ui.min.js

    diff --git a/django/contrib/admin/media/js/jquery-ui.min.js b/django/contrib/admin/media/js/jquery-ui.min.js
    new file mode 100644
    - +  
     1/*!
     2 * jQuery UI 1.8.5
     3 *
     4 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     5 * Dual licensed under the MIT or GPL Version 2 licenses.
     6 * http://jquery.org/license
     7 *
     8 * http://docs.jquery.com/UI
     9 */
     10(function(c,j){function k(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.5",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,
     11NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this,
     12"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position");
     13if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"));if(!isNaN(b)&&b!=0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind("mousedown.ui-disableSelection selectstart.ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,l,m){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(l)g-=parseFloat(c.curCSS(f,
     14"border"+this+"Width",true))||0;if(m)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c.style(this,h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c.style(this,
     15h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){var b=a.nodeName.toLowerCase(),d=c.attr(a,"tabindex");if("area"===b){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&k(a)}return(/input|select|textarea|button|object/.test(b)?!a.disabled:"a"==b?a.href||!isNaN(d):!isNaN(d))&&k(a)},tabbable:function(a){var b=c.attr(a,"tabindex");return(isNaN(b)||b>=0)&&c(a).is(":focusable")}});
     16c(function(){var a=document.createElement("div"),b=document.body;c.extend(a.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.appendChild(a).offsetHeight===100;b.removeChild(a).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=0;e<b.length;e++)a.options[b[e][0]]&&b[e][1].apply(a.element,
     17d)}},contains:function(a,b){return document.compareDocumentPosition?a.compareDocumentPosition(b)&16:a!==b&&a.contains(b)},hasScroll:function(a,b){if(c(a).css("overflow")==="hidden")return false;b=b&&b==="left"?"scrollLeft":"scrollTop";var d=false;if(a[b]>0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a<b+d},isOver:function(a,b,d,e,h,i){return c.ui.isOverAxis(a,d,h)&&c.ui.isOverAxis(b,e,i)}})}})(jQuery);
     18;/*!
     19 * jQuery UI Widget 1.8.5
     20 *
     21 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     22 * Dual licensed under the MIT or GPL Version 2 licenses.
     23 * http://jquery.org/license
     24 *
     25 * http://docs.jquery.com/UI/Widget
     26 */
     27(function(b,j){if(b.cleanData){var k=b.cleanData;b.cleanData=function(a){for(var c=0,d;(d=a[c])!=null;c++)b(d).triggerHandler("remove");k(a)}}else{var l=b.fn.remove;b.fn.remove=function(a,c){return this.each(function(){if(!c)if(!a||b.filter(a,[this]).length)b("*",this).add([this]).each(function(){b(this).triggerHandler("remove")});return l.call(b(this),a,c)})}}b.widget=function(a,c,d){var e=a.split(".")[0],f;a=a.split(".")[1];f=e+"-"+a;if(!d){d=c;c=b.Widget}b.expr[":"][f]=function(h){return!!b.data(h,
     28a)};b[e]=b[e]||{};b[e][a]=function(h,g){arguments.length&&this._createWidget(h,g)};c=new c;c.options=b.extend(true,{},c.options);b[e][a].prototype=b.extend(true,c,{namespace:e,widgetName:a,widgetEventPrefix:b[e][a].prototype.widgetEventPrefix||a,widgetBaseClass:f},d);b.widget.bridge(a,b[e][a])};b.widget.bridge=function(a,c){b.fn[a]=function(d){var e=typeof d==="string",f=Array.prototype.slice.call(arguments,1),h=this;d=!e&&f.length?b.extend.apply(null,[true,d].concat(f)):d;if(e&&d.substring(0,1)===
     29"_")return h;e?this.each(function(){var g=b.data(this,a);if(!g)throw"cannot call methods on "+a+" prior to initialization; attempted to call method '"+d+"'";if(!b.isFunction(g[d]))throw"no such method '"+d+"' for "+a+" widget instance";var i=g[d].apply(g,f);if(i!==g&&i!==j){h=i;return false}}):this.each(function(){var g=b.data(this,a);g?g.option(d||{})._init():b.data(this,a,new c(d,this))});return h}};b.Widget=function(a,c){arguments.length&&this._createWidget(a,c)};b.Widget.prototype={widgetName:"widget",
     30widgetEventPrefix:"",options:{disabled:false},_createWidget:function(a,c){b.data(c,this.widgetName,this);this.element=b(c);this.options=b.extend(true,{},this.options,b.metadata&&b.metadata.get(c)[this.widgetName],a);var d=this;this.element.bind("remove."+this.widgetName,function(){d.destroy()});this._create();this._init()},_create:function(){},_init:function(){},destroy:function(){this.element.unbind("."+this.widgetName).removeData(this.widgetName);this.widget().unbind("."+this.widgetName).removeAttr("aria-disabled").removeClass(this.widgetBaseClass+
     31"-disabled ui-state-disabled")},widget:function(){return this.element},option:function(a,c){var d=a,e=this;if(arguments.length===0)return b.extend({},e.options);if(typeof a==="string"){if(c===j)return this.options[a];d={};d[a]=c}b.each(d,function(f,h){e._setOption(f,h)});return e},_setOption:function(a,c){this.options[a]=c;if(a==="disabled")this.widget()[c?"addClass":"removeClass"](this.widgetBaseClass+"-disabled ui-state-disabled").attr("aria-disabled",c);return this},enable:function(){return this._setOption("disabled",
     32false)},disable:function(){return this._setOption("disabled",true)},_trigger:function(a,c,d){var e=this.options[a];c=b.Event(c);c.type=(a===this.widgetEventPrefix?a:this.widgetEventPrefix+a).toLowerCase();d=d||{};if(c.originalEvent){a=b.event.props.length;for(var f;a;){f=b.event.props[--a];c[f]=c.originalEvent[f]}}this.element.trigger(c,d);return!(b.isFunction(e)&&e.call(this.element[0],c,d)===false||c.isDefaultPrevented())}}})(jQuery);
     33;/*
     34 * jQuery UI Position 1.8.5
     35 *
     36 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     37 * Dual licensed under the MIT or GPL Version 2 licenses.
     38 * http://jquery.org/license
     39 *
     40 * http://docs.jquery.com/UI/Position
     41 */
     42(function(c){c.ui=c.ui||{};var n=/left|center|right/,o=/top|center|bottom/,t=c.fn.position,u=c.fn.offset;c.fn.position=function(b){if(!b||!b.of)return t.apply(this,arguments);b=c.extend({},b);var a=c(b.of),d=a[0],g=(b.collision||"flip").split(" "),e=b.offset?b.offset.split(" "):[0,0],h,k,j;if(d.nodeType===9){h=a.width();k=a.height();j={top:0,left:0}}else if(d.scrollTo&&d.document){h=a.width();k=a.height();j={top:a.scrollTop(),left:a.scrollLeft()}}else if(d.preventDefault){b.at="left top";h=k=0;j=
     43{top:b.of.pageY,left:b.of.pageX}}else{h=a.outerWidth();k=a.outerHeight();j=a.offset()}c.each(["my","at"],function(){var f=(b[this]||"").split(" ");if(f.length===1)f=n.test(f[0])?f.concat(["center"]):o.test(f[0])?["center"].concat(f):["center","center"];f[0]=n.test(f[0])?f[0]:"center";f[1]=o.test(f[1])?f[1]:"center";b[this]=f});if(g.length===1)g[1]=g[0];e[0]=parseInt(e[0],10)||0;if(e.length===1)e[1]=e[0];e[1]=parseInt(e[1],10)||0;if(b.at[0]==="right")j.left+=h;else if(b.at[0]==="center")j.left+=h/
     442;if(b.at[1]==="bottom")j.top+=k;else if(b.at[1]==="center")j.top+=k/2;j.left+=e[0];j.top+=e[1];return this.each(function(){var f=c(this),l=f.outerWidth(),m=f.outerHeight(),p=parseInt(c.curCSS(this,"marginLeft",true))||0,q=parseInt(c.curCSS(this,"marginTop",true))||0,v=l+p+parseInt(c.curCSS(this,"marginRight",true))||0,w=m+q+parseInt(c.curCSS(this,"marginBottom",true))||0,i=c.extend({},j),r;if(b.my[0]==="right")i.left-=l;else if(b.my[0]==="center")i.left-=l/2;if(b.my[1]==="bottom")i.top-=m;else if(b.my[1]===
     45"center")i.top-=m/2;i.left=parseInt(i.left);i.top=parseInt(i.top);r={left:i.left-p,top:i.top-q};c.each(["left","top"],function(s,x){c.ui.position[g[s]]&&c.ui.position[g[s]][x](i,{targetWidth:h,targetHeight:k,elemWidth:l,elemHeight:m,collisionPosition:r,collisionWidth:v,collisionHeight:w,offset:e,my:b.my,at:b.at})});c.fn.bgiframe&&f.bgiframe();f.offset(c.extend(i,{using:b.using}))})};c.ui.position={fit:{left:function(b,a){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();
     46b.left=d>0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0];
     47b.left+=a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d=
     48c(b),g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery);
     49;/*
     50 * jQuery UI Autocomplete 1.8.5
     51 *
     52 * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about)
     53 * Dual licensed under the MIT or GPL Version 2 licenses.
     54 * http://jquery.org/license
     55 *
     56 * http://docs.jquery.com/UI/Autocomplete
     57 *
     58 * Depends:
     59 *      jquery.ui.core.js
     60 *      jquery.ui.widget.js
     61 *      jquery.ui.position.js
     62 */
     63(function(e){e.widget("ui.autocomplete",{options:{appendTo:"body",delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},_create:function(){var a=this,b=this.element[0].ownerDocument;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!a.options.disabled){var d=e.ui.keyCode;switch(c.keyCode){case d.PAGE_UP:a._move("previousPage",
     64c);break;case d.PAGE_DOWN:a._move("nextPage",c);break;case d.UP:a._move("previous",c);c.preventDefault();break;case d.DOWN:a._move("next",c);c.preventDefault();break;case d.ENTER:case d.NUMPAD_ENTER:a.menu.element.is(":visible")&&c.preventDefault();case d.TAB:if(!a.menu.active)return;a.menu.select(c);break;case d.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!=a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay);
     65break}}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)};this.menu=e("<ul></ul>").addClass("ui-autocomplete").appendTo(e(this.options.appendTo||"body",b)[0]).mousedown(function(c){var d=a.menu.element[0];
     66c.target===d&&setTimeout(function(){e(document).one("mousedown",function(f){f.target!==a.element[0]&&f.target!==d&&!e.ui.contains(d,f.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,d){d=d.item.data("item.autocomplete");false!==a._trigger("focus",null,{item:d})&&/^key/.test(c.originalEvent.type)&&a.element.val(d.value)},selected:function(c,d){d=d.item.data("item.autocomplete");var f=a.previous;if(a.element[0]!==b.activeElement){a.element.focus();
     67a.previous=f}if(false!==a._trigger("select",c,{item:d})){a.term=d.value;a.element.val(d.value)}a.close(c);a.selectedItem=d},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");e.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup");
     68this.menu.element.remove();e.Widget.prototype.destroy.call(this)},_setOption:function(a,b){e.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(e(b||"body",this.element[0].ownerDocument)[0])},_initSource:function(){var a=this,b,c;if(e.isArray(this.options.source)){b=this.options.source;this.source=function(d,f){f(e.ui.autocomplete.filter(b,d.term))}}else if(typeof this.options.source==="string"){c=this.options.source;this.source=
     69function(d,f){a.xhr&&a.xhr.abort();a.xhr=e.getJSON(c,d,function(g,i,h){h===a.xhr&&f(g);a.xhr=null})}}else this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length<this.options.minLength)return this.close(b);clearTimeout(this.closing);if(this._trigger("search")!==false)return this._search(a)},_search:function(a){this.element.addClass("ui-autocomplete-loading");this.source({term:a},this.response)},_response:function(a){if(a.length){a=
     70this._normalize(a);this._suggest(a);this._trigger("open")}else this.close();this.element.removeClass("ui-autocomplete-loading")},close:function(a){clearTimeout(this.closing);if(this.menu.element.is(":visible")){this._trigger("close",a);this.menu.element.hide();this.menu.deactivate()}},_change:function(a){this.previous!==this.element.val()&&this._trigger("change",a,{item:this.selectedItem})},_normalize:function(a){if(a.length&&a[0].label&&a[0].value)return a;return e.map(a,function(b){if(typeof b===
     71"string")return{label:b,value:b};return e.extend({label:b.label||b.value,value:b.value||b.label},b)})},_suggest:function(a){var b=this.menu.element.empty().zIndex(this.element.zIndex()+1),c;this._renderMenu(b,a);this.menu.deactivate();this.menu.refresh();this.menu.element.show().position(e.extend({of:this.element},this.options.position));a=b.width("").outerWidth();c=this.element.outerWidth();b.outerWidth(Math.max(a,c))},_renderMenu:function(a,b){var c=this;e.each(b,function(d,f){c._renderItem(a,f)})},
     72_renderItem:function(a,b){return e("<li></li>").data("item.autocomplete",b).append(e("<a></a>").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});e.extend(e.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")},
     73filter:function(a,b){var c=new RegExp(e.ui.autocomplete.escapeRegex(b),"i");return e.grep(a,function(d){return c.test(d.label||d.value||d)})}})})(jQuery);
     74(function(e){e.widget("ui.menu",{_create:function(){var a=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(b){if(e(b.target).closest(".ui-menu-item a").length){b.preventDefault();a.select(b)}});this.refresh()},refresh:function(){var a=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex",
     75-1).mouseenter(function(b){a.activate(b,e(this).parent())}).mouseleave(function(){a.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.attr("scrollTop"),f=this.element.height();if(c<0)this.element.attr("scrollTop",d+c);else c>=f&&this.element.attr("scrollTop",d+c-f+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",a,{item:b})},
     76deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");this._trigger("blur");this.active=null}},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(this.active){a=this.active[a+"All"](".ui-menu-item").eq(0);
     77a.length?this.activate(c,a):this.activate(c,this.element.children(b))}else this.activate(c,this.element.children(b))},nextPage:function(a){if(this.hasScroll())if(!this.active||this.last())this.activate(a,this.element.children(":first"));else{var b=this.active.offset().top,c=this.element.height(),d=this.element.children("li").filter(function(){var f=e(this).offset().top-b-c+e(this).height();return f<10&&f>-10});d.length||(d=this.element.children(":last"));this.activate(a,d)}else this.activate(a,this.element.children(!this.active||
     78this.last()?":first":":last"))},previousPage:function(a){if(this.hasScroll())if(!this.active||this.first())this.activate(a,this.element.children(":last"));else{var b=this.active.offset().top,c=this.element.height();result=this.element.children("li").filter(function(){var d=e(this).offset().top-b+c-e(this).height();return d<10&&d>-10});result.length||(result=this.element.children(":first"));this.activate(a,result)}else this.activate(a,this.element.children(!this.active||this.first()?":last":":first"))},
     79hasScroll:function(){return this.element.height()<this.element.attr("scrollHeight")},select:function(a){this._trigger("selected",a,{item:this.active})}})})(jQuery);
     80;
     81 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
    a b  
     1import operator
    12from django import forms, template
    23from django.forms.formsets import all_valid
    34from django.forms.models import modelform_factory, modelformset_factory, inlineformset_factory
     
    1314from django.db.models.fields import BLANK_CHOICE_DASH
    1415from django.http import Http404, HttpResponse, HttpResponseRedirect
    1516from django.shortcuts import get_object_or_404, render_to_response
     17from django.utils import simplejson
    1618from django.utils.decorators import method_decorator
    1719from django.utils.datastructures import SortedDict
    1820from django.utils.functional import update_wrapper
     
    2123from django.utils.functional import curry
    2224from django.utils.text import capfirst, get_text_list
    2325from django.utils.translation import ugettext as _
    24 from django.utils.translation import ungettext
    25 from django.utils.encoding import force_unicode
     26from django.utils.translation import ungettext, ugettext_lazy
     27from django.utils.encoding import force_unicode, smart_str
    2628
    2729HORIZONTAL, VERTICAL = 1, 2
    2830# returns the <ul> class for a given radio_admin field
     
    5052    models.FileField:       {'widget': widgets.AdminFileWidget},
    5153}
    5254
     55AUTOCOMPLETE_FIELDS_DEFAULTS = {
     56    'limit': 5,
     57    'value': lambda o: unicode(o),
     58    'label': lambda o: unicode(o),
     59    'show_search': True,
     60}
     61
    5362csrf_protect_m = method_decorator(csrf_protect)
    5463
    5564class BaseModelAdmin(object):
     
    5766    __metaclass__ = forms.MediaDefiningClass
    5867
    5968    raw_id_fields = ()
     69    autocomplete_fields = {}
    6070    fields = None
    6171    exclude = None
    6272    fieldsets = None
     
    7484        overrides.update(self.formfield_overrides)
    7585        self.formfield_overrides = overrides
    7686
     87        def build_setting(value):
     88            if value in settings['queryset'].model._meta.get_all_field_names():
     89                return lambda m: getattr(m, value)
     90            return lambda m: value % vars(m)
     91
     92        autocomplete_fields = {}
     93        for (field, values) in self.autocomplete_fields.items():
     94            settings = autocomplete_fields[field] = AUTOCOMPLETE_FIELDS_DEFAULTS.copy()
     95            settings.update(values)
     96            if hasattr(self.model, field):
     97                rel = getattr(self.model, field).field.rel
     98                settings['id'] = settings.get('id', rel.get_related_field().name)
     99                if not settings.get('queryset'):
     100                    settings['queryset'] = rel.to._default_manager.complex_filter(rel.limit_choices_to)
     101            for option in ('value', 'label'):
     102                if isinstance(settings[option], (str, unicode)):
     103                    settings[option] = build_setting(settings[option])
     104
     105        self.autocomplete_fields = autocomplete_fields
     106
     107
    77108    def formfield_for_dbfield(self, db_field, **kwargs):
    78109        """
    79110        Hook for specifying the form Field instance for a given database Field
     
    107138            # extra HTML -- the "add other" interface -- to the end of the
    108139            # rendered output. formfield can be None if it came from a
    109140            # OneToOneField with parent_link=True or a M2M intermediary.
    110             if formfield and db_field.name not in self.raw_id_fields:
     141            if (formfield and db_field.name not in self.raw_id_fields
     142                and db_field.name not in self.autocomplete_fields):
    111143                related_modeladmin = self.admin_site._registry.get(
    112144                                                            db_field.rel.to)
    113145                can_add_related = bool(related_modeladmin and
     
    118150
    119151            return formfield
    120152
     153        elif db_field.name in self.autocomplete_fields:
     154            kwargs['widget'] = widgets.AutocompleteWidget(
     155                self.autocomplete_fields[db_field.name],
     156                using=kwargs.get('using'),
     157                force_selection=False)
     158
    121159        # If we've got overrides for the formfield defined, use 'em. **kwargs
    122160        # passed to formfield_for_dbfield override the defaults.
    123161        for klass in db_field.__class__.mro():
     
    153191        db = kwargs.get('using')
    154192        if db_field.name in self.raw_id_fields:
    155193            kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel, using=db)
     194        elif db_field.name in self.autocomplete_fields:
     195            kwargs['widget'] = widgets.AutocompleteWidget(
     196                self.autocomplete_fields[db_field.name], using=db)
    156197        elif db_field.name in self.radio_fields:
    157198            kwargs['widget'] = widgets.AdminRadioSelect(attrs={
    158199                'class': get_ul_class(self.radio_fields[db_field.name]),
     
    174215        if db_field.name in self.raw_id_fields:
    175216            kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db)
    176217            kwargs['help_text'] = ''
     218        elif db_field.name in self.autocomplete_fields:
     219            kwargs['widget'] = widgets.MultipleAutocompleteWidget(
     220                self.autocomplete_fields[db_field.name], using=db)
     221            kwargs['help_text'] = ''
    177222        elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
    178223            kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
    179224
     
    266311            url(r'^add/$',
    267312                wrap(self.add_view),
    268313                name='%s_%s_add' % info),
     314            url(r'^autocomplete/(?P<field>[\w]+)/$',
     315                wrap(self.autocomplete_view),
     316                name='%s_%s_autocomplete' % info),
    269317            url(r'^(.+)/history/$',
    270318                wrap(self.history_view),
    271319                name='%s_%s_history' % info),
     
    287335
    288336        js = ['js/core.js', 'js/admin/RelatedObjectLookups.js',
    289337              'js/jquery.min.js', 'js/jquery.init.js']
     338        if self.autocomplete_fields:
     339            js.insert(3, 'js/jquery-ui.min.js')
    290340        if self.actions is not None:
    291341            js.extend(['js/actions.min.js'])
    292342        if self.prepopulated_fields:
     
    771821            self.message_user(request, msg)
    772822            return None
    773823
     824    def autocomplete_view(self, request, field, extra_content=None):
     825        query = request.GET.get('term', None)
     826       
     827        if field not in self.autocomplete_fields or query is None:
     828            raise Http404
     829
     830        settings = self.autocomplete_fields[field]
     831        queryset = settings['queryset']
     832        search_fields = settings['fields']
     833        if request.GET.get('by_id', None) is not None:
     834            # lookup only via an exact match on id
     835            search_fields = ('=%s' % settings['id'],)
     836
     837        def construct_search(field_name):
     838            # use different lookup methods depending on the notation
     839            if field_name.startswith('^'):
     840                return "%s__istartswith" % field_name[1:]
     841            elif field_name.startswith('='):
     842                return "%s__iexact" % field_name[1:]
     843            elif field_name.startswith('@'):
     844                return "%s__search" % field_name[1:]
     845            else:
     846                return "%s__icontains" % field_name
     847       
     848        for bit in query.split():
     849            or_queries = [models.Q(**{construct_search(
     850                smart_str(field_name)): bit})
     851                    for field_name in search_fields]
     852            queryset = queryset.filter(reduce(operator.or_, or_queries))
     853       
     854        data = []
     855        for o in queryset[:settings['limit']]:
     856            data.append({
     857                'id': getattr(o, settings['id']),
     858                'value': settings['value'](o),
     859                'label': settings['label'](o),
     860            })
     861       
     862        return HttpResponse(simplejson.dumps(data))
     863   
    774864    @csrf_protect_m
    775865    @transaction.commit_on_success
    776866    def add_view(self, request, form_url='', extra_context=None):
  • django/contrib/admin/widgets.py

    diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
    a b  
    77from django import forms
    88from django.forms.widgets import RadioFieldRenderer
    99from django.forms.util import flatatt
     10from django.utils import simplejson
    1011from django.utils.html import escape
    1112from django.utils.text import truncate_words
    1213from django.utils.translation import ugettext as _
     
    9192    template_with_clear = (u'<span class="clearable-file-input">%s</span>'
    9293                           % forms.ClearableFileInput.template_with_clear)
    9394
     95def _get_search_icon(model, name, value, params, attrs):
     96    related_url = '../../../%s/%s/' % (model._meta.app_label, model._meta.object_name.lower())
     97    if params:
     98        url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
     99    else:
     100        url = ''
     101    # TODO: "id_" is hard-coded here. This should instead use the correct
     102    # API to determine the ID dynamically.
     103    output = []
     104    output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \
     105        (related_url, url, name))
     106    output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
     107    return output
    94108
    95109class ForeignKeyRawIdWidget(forms.TextInput):
    96110    """
     
    105119    def render(self, name, value, attrs=None):
    106120        if attrs is None:
    107121            attrs = {}
    108         related_url = '../../../%s/%s/' % (self.rel.to._meta.app_label, self.rel.to._meta.object_name.lower())
    109         params = self.url_parameters()
    110         if params:
    111             url = '?' + '&amp;'.join(['%s=%s' % (k, v) for k, v in params.items()])
    112         else:
    113             url = ''
    114122        if "class" not in attrs:
    115123            attrs['class'] = 'vForeignKeyRawIdAdminField' # The JavaScript looks for this hook.
    116124        output = [super(ForeignKeyRawIdWidget, self).render(name, value, attrs)]
    117         # TODO: "id_" is hard-coded here. This should instead use the correct
    118         # API to determine the ID dynamically.
    119         output.append('<a href="%s%s" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> ' % \
    120             (related_url, url, name))
    121         output.append('<img src="%simg/admin/selector-search.gif" width="16" height="16" alt="%s" /></a>' % (settings.ADMIN_MEDIA_PREFIX, _('Lookup')))
     125
     126        output += _get_search_icon(self.rel.to, name, value, self.url_parameters(), attrs)
    122127        if value:
    123128            output.append(self.label_for_value(value))
    124129        return mark_safe(u''.join(output))
     
    150155        except (ValueError, self.rel.to.DoesNotExist):
    151156            return ''
    152157
     158class AutocompleteWidget(forms.Widget):
     159   
     160    class Media:
     161        js = (
     162            settings.ADMIN_MEDIA_PREFIX + 'js/admin/autocomplete.js',
     163        )
     164
     165    def __init__(self, settings, attrs=None, using=None, **js_options):
     166        self.settings = settings
     167        self.db = using
     168        self.js_options = dict(
     169            source = settings.get('source'),
     170            multiple = settings.get('multiple', False),
     171            force_selection = settings.get('force_selection', True),
     172        )
     173        self.js_options.update(js_options)
     174        super(AutocompleteWidget, self).__init__(attrs)
     175
     176    def get_autocomplete_url(self, name):
     177        return '../autocomplete/%s/' % name
     178
     179    def render(self, name, value, attrs=None, hattrs=None, initial_objects=u''):
     180        if value is None:
     181            value = ''
     182        hidden_id = 'id_hidden_%s' % name
     183        hidden_attrs = self.build_attrs(type='hidden', name=name, value=value, id=hidden_id)
     184        normal_attrs = self.build_attrs(attrs, type='text')
     185        normal_attrs['value'] = self.label_for_value(value)
     186        if not self.js_options.get('source'):
     187            self.js_options['source'] = self.get_autocomplete_url(name)
     188        options = simplejson.dumps(self.js_options)
     189        if self.settings.get('show_search'):
     190            target_key = self.settings.get('id')
     191            search_icon = _get_search_icon(self.settings.get('queryset').model,
     192                                           name, value, {'t': target_key}, attrs)
     193            search_icon = u''.join(search_icon) + '\n'
     194        else:
     195            search_icon = u''
     196        return mark_safe(u''.join((
     197            u'<div class="djangoautocomplete-wrapper">\n',
     198            u'<input%s />\n' % flatatt(hidden_attrs),
     199            u'<input%s />\n' % flatatt(normal_attrs),
     200            initial_objects,
     201            u'</div>\n',
     202            search_icon,
     203            u'<script type="text/javascript">',
     204            u'django.jQuery("#id_%s").djangoautocomplete(%s);' % (name, options),
     205            u'</script>\n',
     206        )))
     207
     208    def label_for_value(self, value):
     209        qs, key, value_fmt = [self.settings[k] for k in ('queryset','id','value')]
     210        if not value:
     211            return value
     212        try:
     213            obj = qs.get(**{key: value})
     214            return value_fmt(obj)
     215        # XXX this shouldn't happen.
     216        except qs.model.DoesNotExist:
     217            return value
     218
     219
    153220class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
    154221    """
    155222    A Widget for displaying ManyToMany ids in the "raw_id" interface rather than
     
    188255                return True
    189256        return False
    190257
     258class MultipleAutocompleteWidget(AutocompleteWidget):
     259
     260    def __init__(self, settings, attrs=None, using=None, **js_options):
     261        js_options['multiple'] = True
     262        super(MultipleAutocompleteWidget, self).__init__(settings, attrs,
     263            using, **js_options)
     264
     265    def render(self, name, value, attrs=None, hattrs=None):
     266        if value:
     267            initial_objects = self.initial_objects(value)
     268            value = ','.join([str(v) for v in value])
     269        else:
     270            initial_objects = u''
     271        return super(MultipleAutocompleteWidget, self).render(
     272            name, value, attrs, hattrs, initial_objects)
     273
     274    def label_for_value(self, value):
     275        return ''
     276
     277    def value_from_datadict(self, data, files, name):
     278        value = data.get(name)
     279        if value:
     280            return value.split(',')
     281        return value
     282   
     283    def initial_objects(self, value):
     284        qs, key, label_fmt = [self.settings[k] for k in ('queryset','id','label')]
     285        output = [u'<ul class="ui-autocomplete-values">']
     286        for obj in qs.filter(**{'%s__in' % key: value}):
     287            output.append(u'<li>%s</li>' % label_fmt(obj))
     288        output.append(u'</ul>\n')
     289        return mark_safe(u'\n'.join(output))
     290
     291
    191292class RelatedFieldWidgetWrapper(forms.Widget):
    192293    """
    193294    This class is a wrapper to a given widget to add the add icon for the
  • docs/ref/contrib/admin/index.txt

    diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
    a b  
    9797    class AuthorAdmin(admin.ModelAdmin):
    9898        date_hierarchy = 'pub_date'
    9999
     100.. versionadded:: 1.3
     101
     102.. attribute:: ModelAdmin.autocomplete_fields
     103
     104By default, Django's admin uses a select-box interface (<select>) for fields
     105that are ``ForeignKey`` or ``ManyToMany``.  If you know that your users'
     106browsers will have javascript enabled you can give them autocomplete behavior.
     107
     108``autocomplete_fields`` is a list of fields you would like to change
     109into a smart ``Input`` widget for either a ``ForeignKey`` or ``ManyToManyField``::
     110
     111    class BookAdmin(admin.ModelAdmin):
     112        autocomplete_fields = {
     113            'author': { 'fields': ('name',) },
     114        }
     115
     116``autocomplete_fields`` is a dictionary that connects a ``field_name`` to
     117a dictionary of ``field_options``.  The ``field_options`` can have the
     118following keys:
     119
     120    * ``fields``
     121        A tuple of field names used to search for objects associated with
     122        ``field_name``. This key is required.
     123
     124        Example::
     125
     126            'fields': ('name', '^user__email',),
     127
     128    * ``label``
     129        A formatting string or subroutine that controls how each choice
     130        is displayed in the list of autocomplete choices.
     131
     132        Example::
     133
     134            'label': 'name'
     135            'label': '%(name)s [%(gender)s]'
     136            'label': lambda o: o.name.lower()
     137
     138    * ``limit``
     139        An integer that limits the size of the autocomplete choices displayed.
     140
     141    * ``show_search``
     142        A boolean that controls the rendering of a clickable search icon. 
     143        Defaults to `True`.
     144
     145    * ``value``
     146       A formatting string or subroutine that controls how a selected item
     147       is displayed.
     148
     149       Same syntax as the ``label`` option.
     150
    100151.. attribute:: ModelAdmin.date_hierarchy
    101152
    102153    Set ``date_hierarchy`` to the name of a ``DateField`` or ``DateTimeField``
  • tests/regressiontests/admin_widgets/models.py

    diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
    a b  
    99    name = models.CharField(max_length=100)
    1010    birthdate = models.DateTimeField(blank=True, null=True)
    1111    gender = models.CharField(max_length=1, blank=True, choices=[('M','Male'), ('F', 'Female')])
     12    user = models.ForeignKey(User, blank=True, null=True)
    1213
    1314    def __unicode__(self):
    1415        return self.name
  • tests/regressiontests/admin_widgets/tests.py

    diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
    a b  
    88from django.contrib.admin import widgets
    99from django.contrib.admin.widgets import (FilteredSelectMultiple,
    1010    AdminSplitDateTime, AdminFileWidget, ForeignKeyRawIdWidget, AdminRadioSelect,
    11     RelatedFieldWidgetWrapper, ManyToManyRawIdWidget)
     11    RelatedFieldWidgetWrapper, ManyToManyRawIdWidget,
     12    AutocompleteWidget, MultipleAutocompleteWidget)
    1213from django.core.files.storage import default_storage
    1314from django.core.files.uploadedfile import SimpleUploadedFile
    1415from django.db.models import DateField
    1516from django.test import TestCase as DjangoTestCase
     17from django.utils import simplejson
    1618from django.utils.html import conditional_escape
    1719from django.utils.translation import activate, deactivate
    1820from django.utils.unittest import TestCase
     
    323325        # Used to fail with a name error.
    324326        w = RelatedFieldWidgetWrapper(w, rel, admin.site)
    325327        self.assertFalse(w.can_add_related)
     328
     329
     330class AutocompleteWidgetTest(DjangoTestCase):
     331    def test_render(self):
     332        band = models.Band.objects.create(name='Linkin Park')
     333        band.album_set.create(
     334            name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg'
     335        )
     336
     337        widget_settings = {
     338            'fields': ('name',),
     339            'id': 'id',
     340            'limit': 5,
     341            'value': lambda o: unicode(o),
     342            'label': lambda o: unicode(o),
     343            'queryset': models.Band.objects.all(),
     344            }
     345        w = AutocompleteWidget(widget_settings)
     346        field_name = 'test'
     347        expected = "\n".join((
     348            '<div class="djangoautocomplete-wrapper">',
     349            '<input type="hidden" name="test" value="%(band_pk)s" id="id_hidden_%(field_name)s" />',
     350            '<input type="text" value="%(band_name)s" />',
     351            '</div>',
     352            '<script type="text/javascript">django.jQuery("#id_%(field_name)s").djangoautocomplete({"force_selection": true, "multiple": %(multiple)s, "source": "../autocomplete/%(field_name)s/"});</script>',
     353            ''
     354            )) % {'field_name': field_name,
     355                  'band_pk': band.pk,
     356                  'band_name': band.name,
     357                  'multiple': 'false',
     358                  }
     359        self.assertEqual(
     360            conditional_escape(w.render('test', band.pk, attrs={})),
     361            expected,
     362        )
     363
     364
     365class MultipleAutocompleteWidgetTest(DjangoTestCase):
     366    fixtures = ["admin-widgets-users.xml"]
     367    admin_root = '/widget_admin'
     368
     369    def setUp(self):
     370        band = models.Band.objects.create(name='Linkin Park')
     371        band.album_set.create(
     372            name='Hybrid Theory', cover_art=r'albums\hybrid_theory.jpg'
     373        )
     374        band2 = models.Band.objects.create(name='Johnny Cash')
     375        band2.album_set.create(
     376            name='At San Quentin'
     377        )
     378        self.bands = (band, band2)
     379   
     380    def _get_expected(self, name, bands, show_search=False):
     381        bands = [] if bands is None else bands
     382        expected = [
     383            '<div class="djangoautocomplete-wrapper">',
     384            '<input type="hidden" name="%(field_name)s" value="%(band_pks)s" id="id_hidden_%(field_name)s" />',
     385            '<input type="text" value="" />',  # input is for data entry only
     386            ]
     387        if bands:               
     388            expected += ['<ul class="ui-autocomplete-values">']
     389            expected += ['<li>%s</li>' % b.name for b in bands]
     390            expected += ['</ul>']
     391        expected += ['</div>']
     392        if show_search:
     393            expected += ['<a href="../../../admin_widgets/band/?t=id" class="related-lookup" id="lookup_id_%s" onclick="return showRelatedObjectLookupPopup(this);"> <img src="%simg/admin/selector-search.gif" width="16" height="16" alt="Lookup" /></a>' % (name, settings.ADMIN_MEDIA_PREFIX)]
     394        expected += [
     395            '<script type="text/javascript">django.jQuery("#id_%(field_name)s").djangoautocomplete({"force_selection": true, "multiple": %(multiple)s, "source": "../autocomplete/%(field_name)s/"});</script>',
     396            ''
     397            ]
     398        expected = "\n".join(expected) % {
     399            'field_name': name,
     400            'band_pks': ','.join([str(b.pk) for b in bands]),
     401            'multiple': 'true',
     402            }
     403        return expected
     404
     405    def test_render(self):
     406        bands = self.bands
     407
     408        widget_settings = {
     409            'fields': ('name',),
     410            'id': 'id',
     411            'limit': 5,
     412            'value': lambda o: unicode(o),
     413            'label': lambda o: unicode(o),
     414            'queryset': models.Band.objects.all(),
     415            'show_search': False,
     416            }
     417        with_search_settings = widget_settings.copy()
     418        with_search_settings['show_search'] = True
     419
     420        w = MultipleAutocompleteWidget(widget_settings)
     421        expected_multiple = self._get_expected('test_multiple', bands)
     422        self.assertEqual(
     423            conditional_escape(w.render('test_multiple', [b.pk for b in bands], attrs={})),
     424            expected_multiple,
     425        )
     426
     427        w = MultipleAutocompleteWidget(widget_settings)
     428        bands_just_one = bands[:1]
     429        expected_single = self._get_expected('test_single', bands_just_one)
     430        self.assertEqual(
     431            conditional_escape(w.render('test_single', [b.pk for b in bands_just_one], attrs={})),
     432            expected_single,
     433        )
     434
     435        w = MultipleAutocompleteWidget(widget_settings)
     436        bands_none = None
     437        expected_none = self._get_expected('test_none', bands_none)
     438        self.assertEqual(
     439            conditional_escape(w.render('test_none', bands_none, attrs={})),
     440            expected_none,
     441        )
     442
     443        w = MultipleAutocompleteWidget(with_search_settings)
     444        bands_none = None
     445        expected_none = self._get_expected('test_none', bands_none, show_search=True)
     446        self.assertEqual(
     447            conditional_escape(w.render('test_none', bands_none, attrs={})),
     448            expected_none,
     449        )
     450       
     451
     452    def test_lookup(self):
     453        band = self.bands[1]
     454        self.client.login(username="super", password="secret")
     455        response = self.client.get('%s/admin_widgets/album/autocomplete/band/?term=johnny' % self.admin_root )
     456        self.assertEqual(simplejson.loads(response.content),
     457                         [{'id': band.id,
     458                           'value': band.name,
     459                           'label': band.name}])
     460        response = self.client.get('%s/admin_widgets/album/autocomplete/band/?term=a' % self.admin_root )
     461        self.assertEqual(simplejson.loads(response.content),
     462                         [{'id': band.id,
     463                           'value': band.name,
     464                           'label': band.name} for band in self.bands])
     465        response = self.client.get('%s/admin_widgets/album/autocomplete/band/?term=%s&by_id=1' % (self.admin_root, band.id ))
     466        self.assertEqual(simplejson.loads(response.content),
     467                         [{'id': band.id,
     468                           'value': band.name,
     469                           'label': band.name}])
     470       
     471
     472    def test_different_id(self):
     473        self.client.login(username="super", password="secret")
     474        user = models.User.objects.get(username='testser')
     475        user.member_set.create(
     476            name='Man Named Sue',
     477            user=user,
     478        )
     479        expected = [{'id': user.email,
     480                     'value': user.username,
     481                     'label': user.username}]
     482        lookup_url = "%s/admin_widgets/member/autocomplete/user/?term=%%s" % self.admin_root
     483        lookup_by_id_url = "%s&by_id=1" % lookup_url
     484
     485        response = self.client.get(lookup_url % user.username[:2])
     486        self.assertEqual(simplejson.loads(response.content),
     487                         expected)
     488        response = self.client.get(lookup_url % user.email[:2])
     489        self.assertEqual(simplejson.loads(response.content),
     490                         expected)
     491        response = self.client.get(lookup_url % user.first_name[:2])
     492        self.assertEqual(simplejson.loads(response.content),
     493                         expected)
     494        response = self.client.get(lookup_by_id_url % user.email)
     495        self.assertEqual(simplejson.loads(response.content),
     496                         expected)
     497       
  • tests/regressiontests/admin_widgets/widgetadmin.py

    diff --git a/tests/regressiontests/admin_widgets/widgetadmin.py b/tests/regressiontests/admin_widgets/widgetadmin.py
    a b  
    2222class EventAdmin(admin.ModelAdmin):
    2323    raw_id_fields = ['band']
    2424
     25
     26class AlbumAdmin(admin.ModelAdmin):
     27    autocomplete_fields = {
     28        'band': { 'fields': ('name',) }
     29            }
     30
     31class MemberAdmin(admin.ModelAdmin):
     32    autocomplete_fields = {
     33        'user': {
     34            'fields': ('^email', '^username', '^first_name', ),
     35            'id': 'email',  # weird id for test_different_id
     36            }
     37        }
     38
     39
    2540site = WidgetAdmin(name='widget-admin')
    2641
    2742site.register(models.User)
     43site.register(models.Album, AlbumAdmin)
    2844site.register(models.Car, CarAdmin)
    2945site.register(models.CarTire, CarTireAdmin)
    3046site.register(models.Event, EventAdmin)
     47site.register(models.Member, MemberAdmin)
Back to Top