Mark Lavin avatar Mark Lavin committed 2f5d4cf Merge

Merge in v0.1.2 bugfix.

Comments (0)

Files changed (19)

 db90eb16f083397a4d655313ce9106a02616c043 version-0.1
+3b971faaa99ceb46d5acc374cf4102662cedb2ee version-0.1.1
 c269d99ffb471f496ab43a3fab92dc6aea9a03ba version-0.1.2
 
 Once installed you should add the urls to your root url patterns::
 
-    .. code-block::
-
         urlpatterns = patterns('',
             # Other patterns go here
             (r'^selectable/', include('selectable.urls')),
+Admin Integration
+====================
+
+Overview
+--------------------------------------
+
+Django-Selectables will work in the admin. To get started on integrated the
+fields and widgets in the admin make sure you are familiar with the Django
+documentation on the `ModelAdmin.form <http://docs.djangoproject.com/en/1.3/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form>`_ 
+and `ModelForms <http://docs.djangoproject.com/en/1.3/topics/forms/modelforms/>`_ particularly
+on `overriding the default widgets <http://docs.djangoproject.com/en/1.3/topics/forms/modelforms/#overriding-the-default-field-types-or-widgets>`_. As you will see integrating Django-Selectables in the admin
+is the same as working with regular forms.
+
+
+Basic Example
+--------------------------------------
+
+In our sample project we have a `Farm` model with a foreign key to `auth.User` and 
+a many to many relation to our `Fruit` model.
+
+    .. literalinclude:: ../example/core/models.py
+       :pyobject: Farm
+
+In `admin.py` we will define the form and associate it with the `FarmAdmin`.
+
+    .. literalinclude:: ../example/core/admin.py
+        :pyobject: FarmAdminForm
+
+    .. literalinclude:: ../example/core/admin.py
+        :pyobject: FarmAdmin
+
+You'll note this form also for new users to be created and associated with the
+farm if no user is found matching the given name. To make use of this feature we
+need to add `owner` to the exclude so that it will pass model validation. Unfortunately
+that means we must set the owner manual in the save and in the initial data because
+the `ModelForm` will no longer do this for you. Since `fruit` does not allow new
+items you'll see these steps are not necessary.
+
+
+Inline Example
+--------------------------------------
+
+With our `Farm` model we can also associate the `UserAdmin` with a `Farm`
+by making use of the `InlineModelAdmin 
+<http://docs.djangoproject.com/en/1.3/ref/contrib/admin/#inlinemodeladmin-objects>`_.
+We can even make use of the same `FarmAdminForm`.
+
+    .. literalinclude:: ../example/core/admin.py
+        :pyobject: FarmInline
+    .. literalinclude:: ../example/core/admin.py
+        :pyobject: NewUserAdmin
+
+The auto-complete functions will be bound as new forms are added dynamically.
+
 # built documents.
 #
 # The short X.Y version.
-version = '0.1'
+version = '0.2'
 # The full version, including alpha/beta/rc tags.
-release = '0.1.2'
+release = '0.2.beta'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
 Fields
 ==========
 
+Django-Selectable defines a number of fields for selecting either single or mutliple
+lookup items. Item in this context corresponds to the object return by the underlying
+lookup `get_item`. The single select select fields (`AutoCompleteSelectField` and
+`AutoComboboxSelectField`) allow for the creation of new items. To use this feature the field's
+lookup class must define `create_item`. In the case of lookups extending from
+`ModelLookup` newly created items have not yet been saved into the database and saving
+should be handled by the form. All fields take the lookup class as the first required
+argument.
+
+
 AutoCompleteSelectField
 --------------------------------------
     
-Field tied to `AutoCompleteSelectWidget` to bind the selection to the form and  create new items, if allowed.
+Field tied to `AutoCompleteSelectWidget` to bind the selection to the form and  
+create new items, if allowed. The `allow_new` keyword argument (default: `False`)
+which determines if the field allows new items. This field cleans to a single item.
 
 
 AutoComboboxSelectField
 --------------------------------------
 
-Field tied to `AutoComboboxSelectWidget` to bind the selection to the form and create new items, if allowed.
+Field tied to `AutoComboboxSelectWidget` to bind the selection to the form and 
+create new items, if allowed. The `allow_new` keyword argument (default: `False`)
+which determines if the field allows new items. This field cleans to a single item.
 
 
 AutoCompleteSelectMultipleField
 --------------------------------------
 
 Field tied to `AutoCompleteSelectMultipleWidget` to bind the selection to the form.
+This field cleans to a list of items. `AutoCompleteSelectMultipleField` does not
+allow for the creation of new items.
 
 
 AutoComboboxSelectMultipleField
 --------------------------------------
 
 Field tied to `AutoComboboxSelectMultipleWidget` to bind the selection to the form.
+This field cleans to a list of items. `AutoComboboxSelectMultipleField` does not 
+allow for the creation of new items.
     overview
     quick-start
     lookups
+    parameters
+    admin    
     fields
     widgets
     releases
 should set two class attributes: `model` and `search_field`.
 
     .. literalinclude:: ../example/core/lookups.py
-        :lines: 1-10
+        :pyobject: FruitLookup
 
 The syntax for `search_field` is the same as the Django 
 `field lookup syntax <http://docs.djangoproject.com/en/1.2/ref/models/querysets/#field-lookups>`_. 

docs/parameters.rst

+Additional Parameters
+=========================
+
+The basic lookup is based on handling a search based on a single term string.
+If additional filtering is needed it can be inside the lookup `get_query` but
+you would need to define this when the lookup is defined. While this fits a fair
+number of use cases there are times when you need to define additional query
+parameters that won't be know until the either the form is bound or until selections
+are made on the client side. This section will detail how to handle both of these
+cases.
+
+
+How Parameters are Passed
+--------------------------------------
+
+As with the search term the additional parameters you define will be passed in
+`request.GET`. Since `get_query` gets the current request so you will have access to
+them.
+
+
+Adding Parameters on the Server Side
+--------------------------------------
+
+Each of the widgets define `update_query_parameters` which takes a dictionary. The
+most common way to use this would be in the form `__init__`.
+
+.. code-block:: python
+
+    class FruitForm(forms.Form):
+        autocomplete = forms.CharField(
+            label='Type the name of a fruit (AutoCompleteWidget)',
+            widget=selectable.AutoCompleteWidget(FruitLookup),
+            required=False,
+        )
+
+        def __init__(self, *args, **kwargs):
+            super(FruitForm, self).__init__(*args, **kwargs)
+            self.fields['autocomplete'].widget.update_query_parameters({'foo': 'bar'})
+
+
+Adding Parameters on the Client Side
+--------------------------------------
+
+There are times where you want to filter the result set based other selections
+by the user such as a filtering cities by a previously selected state. In this
+case you will need to bind a `prepareQuery` to the field. This function should accept the query dictionary. 
+You are free to make adjustments to  the query dictionary as needed.
+
+.. code-block:: html
+
+    <script type="text/javascript">
+        function newParameters(query) {
+            query.foo = 'bar';
+        }
+
+        $(document).ready(function() {
+            $('#id_autocomplete').djselectable('option', 'prepareQuery', newParameters);
+        });
+    </script>
+

docs/quick-start.rst

 lookup which searchs models based on a particular field. Let's define a simple model:
 
     .. literalinclude:: ../example/core/models.py
-        :lines: 1-10
+        :pyobject: Fruit
 
 In a `lookups.py` we will define our lookup:
 
     .. literalinclude:: ../example/core/lookups.py
-        :lines: 1-10
+        :pyobject: FruitLookup
 
 This lookups extends `selectable.base.ModelLookup` and defines two things: one is
 the model on which we will be searching and the other is the field which we are searching.
 
 Below this definition we will register our lookup class.
 
-    .. literalinclude:: ../example/core/lookups.py
-        :lines: 12
+    .. code-block:: python
+
+        registry.register(FruitLookup)
 
 
 Defining Forms
 Now that we have a working lookup we will define a form which uses it:
 
     .. literalinclude:: ../example/core/forms.py
-        :lines: 1-13
+        :pyobject: FruitForm
+        :end-before: newautocomplete
 
 This replaces the default widget for the `CharField` with the `AutoCompleteWidget`.
 This will allow the user to fill this field with values taken from the names of
 Widgets
 ==========
 
+Below are the custom widgets defined by Django-Selectable. All widgets take the 
+lookup class as the first required argument.
+
 
 AutoCompleteWidget
 --------------------------------------
 
-Basic widget for auto-completing text.
+Basic widget for auto-completing text. The widget returns the item value as defined
+by the lookup `get_item_value`. If the `allow_new` keyword argument is passed as
+true it will allow the user to type any text they wish.
 
 
 AutoComboboxWidget
 --------------------------------------
 
 Widget for selecting a value/id based on input text. Optionally allows selecting new items to be created.
+This widget should be used in conjunction with the `AutoCompleteSelectField` as it will
+return both the text entered by the user and the id (if an item was selected/matched).
 
 
 AutoComboboxSelectWidget
 AutoCompleteSelectMultipleWidget
 --------------------------------------
 
-Builds a list of selected items from auto-completion.
+Builds a list of selected items from auto-completion. This widget will return a list
+of item ids as defined by the lookup `get_item_id`. Using this widget with the
+`AutoCompleteSelectMultipleField` will clean the items to the item objects. This does
+not allow for creating new items. There is another optional keyword argument `postion`
+which can take four possible values: `bottom`, `bottom-inline`, `top` or `top-inline`.
+This determine the position of the deck list of currently selected items as well as
+whether this list is stacked or inline. The default is `bottom`.
 
 
 AutoComboboxSelectMultipleWidget

example/core/admin.py

+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin
+from django.contrib.auth.models import User
+from django import forms
+
+import selectable.forms as selectable
+
+from example.core.models import Fruit, Farm
+from example.core.lookups import FruitLookup, OwnerLookup
+
+
+class FarmAdminForm(forms.ModelForm):
+    owner = selectable.AutoComboboxSelectField(lookup_class=OwnerLookup, allow_new=True)
+
+    class Meta(object):
+        model = Farm
+        widgets = {
+            'fruit': selectable.AutoCompleteSelectMultipleWidget(lookup_class=FruitLookup),
+        }
+        exclude = ('owner', )
+
+    def __init__(self, *args, **kwargs):
+        super(FarmAdminForm, self).__init__(*args, **kwargs)
+        if self.instance and self.instance.pk and self.instance.owner:
+            self.initial['owner'] = self.instance.owner
+
+    def save(self, *args, **kwargs):
+        owner = self.cleaned_data['owner']
+        if owner and not owner.pk:
+            owner = User.objects.create_user(username=owner.username, email='')
+        self.instance.owner = owner
+        return super(FarmAdminForm, self).save(*args, **kwargs)
+
+
+class FarmAdmin(admin.ModelAdmin):
+    form = FarmAdminForm
+
+
+class FarmInline(admin.TabularInline):
+    model = Farm
+    form = FarmAdminForm
+
+
+class NewUserAdmin(UserAdmin):
+    inlines = [
+        FarmInline,
+    ]
+
+
+admin.site.unregister(User)
+admin.site.register(User, NewUserAdmin)
+admin.site.register(Fruit)
+admin.site.register(Farm, FarmAdmin)

example/core/lookups.py

+from django.contrib.auth.models import User
+
 from selectable.base import ModelLookup
 from selectable.registry import registry
 
 
 registry.register(FruitLookup)
 
+
+class OwnerLookup(ModelLookup):
+    model = User
+    search_field = 'username__icontains'
+
+
+registry.register(OwnerLookup)

example/core/models.py

-from django.contrib import admin
 from django.db import models
 
 
         return self.name
 
 
-admin.site.register(Fruit)
+class Farm(models.Model):
+    name = models.CharField(max_length=200)
+    owner = models.ForeignKey('auth.User', related_name='farms')
+    fruit = models.ManyToManyField(Fruit)
+
+    def __unicode__(self):
+        return u"%s's Farm: %s" % (self.owner.username, self.name)

example/settings.py

 # Django settings for example project.
+import os
+
+PROJECT_PATH = os.path.dirname(__file__)
 
 DEBUG = True
 TEMPLATE_DEBUG = DEBUG
     # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
     # Always use forward slashes, even on Windows.
     # Don't forget to use absolute paths, not relative paths.
+    os.path.join(PROJECT_PATH, 'templates'),
 )
 
 INSTALLED_APPS = (

example/templates/admin/base_site.html

+{% extends "admin/base.html" %}
+{% block title %}{{ title }} | Django-Selectable Example Site{% endblock %}
+{% block extrahead %}
+        <link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.10/themes/base/jquery-ui.css" type="text/css" media="screen">
+        <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.10/themes/ui-lightness/jquery-ui.css" type="text/css" media="screen">
+        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js"></script>
+        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.10/jquery-ui.min.js"></script>
+{% endblock %}
+{% block branding %}
+<h1 id="site-name">Django-Selectable Administration</h1>
+{% endblock %}
+{% block nav-global %}{% endblock %}

selectable/__init__.py

 
 __version_info__ = {
     'major': 0,
-    'minor': 1,
-    'micro': 2,
-    'releaselevel': 'final',
-    'serial': 1
+    'minor': 2,
+    'micro': 0,
+    'releaselevel': 'beta',
 }
 
 def get_version():
     if __version_info__['micro']:
         vers.append(".%(micro)i" % __version_info__)
     if __version_info__['releaselevel'] != 'final':
-        vers.append('%(releaselevel)s%(serial)i' % __version_info__)
+        vers.append('%(releaselevel)s' % __version_info__)
     return ''.join(vers)
 
 __version__ = get_version()

selectable/forms/widgets.py

 
     def __init__(self, lookup_class, *args, **kwargs):
         self.lookup_class = lookup_class
+        position = kwargs.pop('position', 'bottom')
+        attrs = {
+            u'data-selectable-multiple': 'true',
+            u'data-selectable-position': position
+        }
         widgets = [
-            AutoCompleteWidget(lookup_class, allow_new=False, attrs={u'data-selectable-multiple': 'true'}),
+            AutoCompleteWidget(lookup_class, allow_new=False, attrs=attrs),
             LookupMultipleHiddenInput(lookup_class)
         ]
         super(AutoCompleteSelectMultipleWidget, self).__init__(widgets, *args, **kwargs)
 
     def __init__(self, lookup_class, *args, **kwargs):
         self.lookup_class = lookup_class
+        position = kwargs.pop('position', 'bottom')
+        attrs = {
+            u'data-selectable-multiple': 'true',
+            u'data-selectable-position': position
+        }
         widgets = [
-            AutoComboboxWidget(lookup_class, allow_new=False, attrs={u'data-selectable-multiple': 'true'}),
+            AutoComboboxWidget(lookup_class, allow_new=False, attrs=attrs),
             LookupMultipleHiddenInput(lookup_class)
         ]
         super(AutoComboboxSelectMultipleWidget, self).__init__(widgets, *args, **kwargs)

selectable/static/css/dj.selectable.css

-ul.selectable-deck {
+ul.selectable-deck, ul.ui-autocomplete {
     list-style: none outside none;
 }
-ul.selectable-deck li.selectable-deck-item {
+ul.selectable-deck li.selectable-deck-item,
+ul.ui-autocomplete li.ui-menu-item {
     margin: 0;
-    padding: 0.4em;
+    list-style-type: none;
 }
 ul.selectable-deck li.selectable-deck-item .selectable-deck-remove {
     float: right;
 }
+ul.selectable-deck-bottom-inline,
+ul.selectable-deck-top-inline {
+    padding: 0;
+}
+ul.selectable-deck-bottom-inline li.selectable-deck-item,
+ul.selectable-deck-top-inline li.selectable-deck-item {
+    display: inline;
+}
+ul.selectable-deck-bottom-inline li.selectable-deck-item .selectable-deck-remove,
+ul.selectable-deck-top-inline li.selectable-deck-item .selectable-deck-remove {
+    margin-left: 0.4em;
+    display: inline;
+    float: none;
+}

selectable/static/js/jquery.dj.selectable.js

 
         options: {
             removeIcon: "ui-icon-close",
-            comboboxIcon: "ui-icon-triangle-1-s"
+            comboboxIcon: "ui-icon-triangle-1-s",
+            prepareQuery: null
         },
         
         _initDeck: function(hiddenInputs) {
             var self = this;
-            this.deck = $('<ul>').addClass('ui-widget selectable-deck');
-            $(this.element).after(this.deck);
+            var data = $(this.element).data();
+            var style = data['selectable-position'] || 'bottom';
+            this.deck = $('<ul>').addClass('ui-widget selectable-deck selectable-deck-' + style);
+            if (style === 'bottom' || style === 'bottom-inline') {
+                $(this.element).after(this.deck);
+            } else {
+                $(this.element).before(this.deck);
+            }
             $(hiddenInputs).each(function(i, input) {
                 self._addDeckItem(input);
             });
             function dataSource(request, response) {
                 var url = data['selectable-url'];
                 var now = new Date().getTime();
-				$.getJSON(url, {
-					term: request.term,
-                    timestamp: now
-				}, response);
+                var query = {term: request.term, timestamp: now};
+                if (self.options.prepareQuery) {
+                    self.options.prepareQuery(query);
+                }
+				$.getJSON(url, query, response);
             }
 
             $(input).autocomplete({
     });
 }
 
+if (typeof(django) != "undefined" && typeof(django.jQuery) != "undefined") {
+    if (django.jQuery.fn.formset) {
+        var oldformset = django.jQuery.fn.formset;
+	    django.jQuery.fn.formset = function(opts) {
+            var options = $.extend({}, opts);
+            var addedevent = function(row) {
+                bindSelectables($(row));
+            };
+            var added = null;
+            if (options.added) {
+                var oldadded = options.added;
+                added = function(row) { oldadded(row); addedevent(row); };
+            }
+            options.added = added || addedevent;
+            return oldformset.call(this, options);
+        }
+    }
+}
+
 $(document).ready(function() {
     bindSelectables('body');
 });
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.