+++ /dev/null
-[run]
-source = selectable
-omit = */tests*,*/urls.py
+++ /dev/null
-[main]
-host = https://www.transifex.com
-
-[django-selectable.txo]
-file_filter = selectable/locale/<lang>/LC_MESSAGES/django.po
-source_file = selectable/locale/en/LC_MESSAGES/django.po
-source_lang = en
-type = PO
+++ /dev/null
-Primary author:
-
-Mark Lavin
-
-The following people who have contributed to django-selectable:
-
-Michael Manfre
-Luke Plant
-Augusto Men
-@dc
-Colin Copeland
-Sławomir Ehlert
-Dan Poirier
-Felipe Prenholato
-David Ray
-Rick Testore
-Karen Tracey
-Manuel Alvarez
-Ustun Ozgur
-@leo-the-manic
-Calvin Spealman
-Rebecca Lovewell
-Thomas Güttler
-Yuri Khrustalev
-@SaeX
-Tam Huynh
-Raphael Merx
-Josh Addington
-Tobias Zanke
-Petr Dlouhy
-Vinod Kurup
-
-Thanks for all of your work!
+++ /dev/null
-Copyright (c) 2010-201999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999, Mark Lavin
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without modification,
-are permitted provided that the following conditions are met:
-
-* Redistributions of source code must retain the above copyright notice,
-this list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above copyright notice,
-this list of conditions and the following disclaimer in the documentation
-and/or other materials provided with the distribution.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
-ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
-ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+++ /dev/null
-include AUTHORS.txt
-include README.rst
-include LICENSE.txt
-recursive-include selectable/locale *
-recursive-include selectable/static *
-recursive-include selectable/templates *
+++ /dev/null
-STATIC_DIR = ./selectable/static/selectable
-QUNIT_TESTS = file://`pwd`/selectable/tests/qunit/index.html
-
-test-js:
- # Run JS tests
- # Requires phantomjs
- phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.11.2&ui=1.11.4
- phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.11.2&ui=1.10.4
- phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.10.2&ui=1.11.4
- phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.10.2&ui=1.10.4
- phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.9.1&ui=1.11.4
- phantomjs run-qunit.js ${QUNIT_TESTS}?jquery=1.9.1&ui=1.10.4
-
-
-lint-js:
- # Check JS for any problems
- # Requires jshint
- jshint ${STATIC_DIR}/js/jquery.dj.selectable.js
-
-
-.PHONY: lint-js test-js
+++ /dev/null
-django-selectable
-===================
-
-Tools and widgets for using/creating auto-complete selection widgets using Django and jQuery UI.
-
-.. image:: https://travis-ci.org/mlavin/django-selectable.svg?branch=master
- :target: https://travis-ci.org/mlavin/django-selectable
-
-.. image:: https://codecov.io/github/mlavin/django-selectable/coverage.svg?branch=master
- :target: https://codecov.io/github/mlavin/django-selectable?branch=master
-
-
-.. note::
-
- This project is looking for additional maintainers to help with Django/jQuery compatibility
- issues as well as addressing support issues/questions. If you are looking to help out
- on this project and take a look at the open
- `help-wanted <https://github.com/mlavin/django-selectable/issues?q=is%3Aissue+is%3Aopen+label%3Ahelp-wanted>`_
- or `question <https://github.com/mlavin/django-selectable/issues?q=is%3Aissue+is%3Aopen+label%3Aquestion>`_
- and see if you can contribute a fix. Be bold! If you want to take a larger role on
- the project, please reach out on the
- `mailing list <http://groups.google.com/group/django-selectable>`_. I'm happy to work
- with you to get you going on an issue.
-
-
-Features
------------------------------------
-
-- Works with the latest jQuery UI Autocomplete library
-- Auto-discovery/registration pattern for defining lookups
-
-
-Installation Requirements
------------------------------------
-
-- Python 2.7, 3.4+
-- `Django <http://www.djangoproject.com/>`_ >= 1.11, <= 3.0
-- `jQuery <http://jquery.com/>`_ >= 1.9, < 3.0
-- `jQuery UI <http://jqueryui.com/>`_ >= 1.10
-
-To install::
-
- pip install django-selectable
-
-Next add `selectable` to your `INSTALLED_APPS` to include the related css/js::
-
- INSTALLED_APPS = (
- 'contrib.staticfiles',
- # Other apps here
- 'selectable',
- )
-
-The jQuery and jQuery UI libraries are not included in the distribution but must be included
-in your templates. See the example project for an example using these libraries from the
-Google CDN.
-
-Once installed you should add the urls to your root url patterns::
-
- urlpatterns = [
- # Other patterns go here
- url(r'^selectable/', include('selectable.urls')),
- ]
-
-
-Documentation
------------------------------------
-
-Documentation for django-selectable is available on `Read The Docs <http://django-selectable.readthedocs.io/en/latest/>`_.
-
-
-Additional Help/Support
------------------------------------
-
-You can find additional help or support on the mailing list: http://groups.google.com/group/django-selectable
-
-
-Contributing
---------------------------------------
-
-If you think you've found a bug or are interested in contributing to this project
-check out our `contributing guide <http://readthedocs.org/docs/django-selectable/en/latest/contribute.html>`_.
-
-If you are interested in translating django-selectable into your native language
-you can join the `Transifex project <https://www.transifex.com/projects/p/django-selectable/>`_.
+++ /dev/null
-# Makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS =
-SPHINXBUILD = sphinx-build
-PAPER =
-BUILDDIR = _build
-
-# Internal variables.
-PAPEROPT_a4 = -D latex_paper_size=a4
-PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
-
-.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
-
-help:
- @echo "Please use \`make <target>' where <target> is one of"
- @echo " html to make standalone HTML files"
- @echo " dirhtml to make HTML files named index.html in directories"
- @echo " singlehtml to make a single large HTML file"
- @echo " pickle to make pickle files"
- @echo " json to make JSON files"
- @echo " htmlhelp to make HTML files and a HTML help project"
- @echo " qthelp to make HTML files and a qthelp project"
- @echo " devhelp to make HTML files and a Devhelp project"
- @echo " epub to make an epub"
- @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
- @echo " latexpdf to make LaTeX files and run them through pdflatex"
- @echo " text to make text files"
- @echo " man to make manual pages"
- @echo " changes to make an overview of all changed/added/deprecated items"
- @echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if enabled)"
-
-clean:
- -rm -rf $(BUILDDIR)/*
-
-html:
- $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-
-dirhtml:
- $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
-
-singlehtml:
- $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
- @echo
- @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
-
-pickle:
- $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
- @echo
- @echo "Build finished; now you can process the pickle files."
-
-json:
- $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
- @echo
- @echo "Build finished; now you can process the JSON files."
-
-htmlhelp:
- $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
- @echo
- @echo "Build finished; now you can run HTML Help Workshop with the" \
- ".hhp project file in $(BUILDDIR)/htmlhelp."
-
-qthelp:
- $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
- @echo
- @echo "Build finished; now you can run "qcollectiongenerator" with the" \
- ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
- @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Django-Selectable.qhcp"
- @echo "To view the help file:"
- @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Django-Selectable.qhc"
-
-devhelp:
- $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
- @echo
- @echo "Build finished."
- @echo "To view the help file:"
- @echo "# mkdir -p $$HOME/.local/share/devhelp/Django-Selectable"
- @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Django-Selectable"
- @echo "# devhelp"
-
-epub:
- $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
- @echo
- @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
-
-latex:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo
- @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
- @echo "Run \`make' in that directory to run these through (pdf)latex" \
- "(use \`make latexpdf' here to do that automatically)."
-
-latexpdf:
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
- @echo "Running LaTeX files through pdflatex..."
- make -C $(BUILDDIR)/latex all-pdf
- @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
-
-text:
- $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
- @echo
- @echo "Build finished. The text files are in $(BUILDDIR)/text."
-
-man:
- $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
- @echo
- @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
-
-changes:
- $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
- @echo
- @echo "The overview file is in $(BUILDDIR)/changes."
-
-linkcheck:
- $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
- @echo
- @echo "Link check complete; look for any errors in the above output " \
- "or in $(BUILDDIR)/linkcheck/output.txt."
-
-doctest:
- $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
- @echo "Testing of doctests in the sources finished, look at the " \
- "results in $(BUILDDIR)/doctest/output.txt."
+++ /dev/null
-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/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.form>`_
-and `ModelForms <http://docs.djangoproject.com/en/stable/topics/forms/modelforms/>`_ particularly
-on `overriding the default widgets <http://docs.djangoproject.com/en/stable/topics/forms/modelforms/#overriding-the-default-field-types-or-widgets>`_.
-As you will see integrating django-selectable in the adminis the same as working with regular forms.
-
-
-.. _admin-jquery-include:
-
-Including jQuery & jQuery UI
---------------------------------------
-
-As noted :ref:`in the quick start guide <start-include-jquery>`, the jQuery and jQuery UI libraries
-are not included in the distribution but must be included in your templates. For the
-Django admin that means overriding
-`admin/base_site.html <https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/templates/admin/base_site.html>`_.
-You can include this media in the block name `extrahead` which is defined in
-`admin/base.html <https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/templates/admin/base.html>`_.
-
- .. code-block:: html
-
- {% block extrahead %}
- {% load selectable_tags %}
- {% include_ui_theme %}
- {% include_jquery_libs %}
- {{ block.super }}
- {% endblock %}
-
-See the Django documentation on
-`overriding admin templates <https://docs.djangoproject.com/en/stable/ref/contrib/admin/#overriding-admin-templates>`_.
-See the example project for the full template example.
-
-
-.. _admin-grappelli:
-
-Using Grappelli
---------------------------------------
-
-`Grappelli <https://django-grappelli.readthedocs.org>`_ is a popular customization of the Django
-admin interface. It includes a number of interface improvements which are also built on top of
-jQuery UI. When using Grappelli you do not need to make any changes to the ``admin/base_site.html``
-template. django-selectable will detect jQuery and jQuery UI versions included by Grappelli
-and make use of them.
-
-
-.. _admin-basic-example:
-
-Basic Example
---------------------------------------
-
-For example, we may have a ``Farm`` model with a foreign key to ``auth.User`` and
-a many to many relation to our ``Fruit`` model.
-
- .. code-block:: python
-
- from __future__ import unicode_literals
-
- from django.db import models
- from django.utils.encoding import python_2_unicode_compatible
-
-
- @python_2_unicode_compatible
- class Fruit(models.Model):
- name = models.CharField(max_length=200)
-
- def __str__(self):
- return self.name
-
-
- @python_2_unicode_compatible
- class Farm(models.Model):
- name = models.CharField(max_length=200)
- owner = models.ForeignKey('auth.User', related_name='farms', on_delete=models.CASCADE)
- fruit = models.ManyToManyField(Fruit)
-
- def __str__(self):
- return "%s's Farm: %s" % (self.owner.username, self.name)
-
-In `admin.py` we will define the form and associate it with the `FarmAdmin`.
-
- .. code-block:: python
-
- from django.contrib import admin
- from django.contrib.auth.admin import UserAdmin
- from django.contrib.auth.models import User
- from django import forms
-
- from selectable.forms import AutoCompleteSelectField, AutoCompleteSelectMultipleWidget
-
- from .models import Fruit, Farm
- from .lookups import FruitLookup, OwnerLookup
-
-
- class FarmAdminForm(forms.ModelForm):
- owner = AutoCompleteSelectField(lookup_class=OwnerLookup, allow_new=True)
-
- class Meta(object):
- model = Farm
- widgets = {
- 'fruit': 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.pk
-
- 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
-
-
- admin.site.register(Farm, FarmAdmin)
-
-
-You'll note this form also allows 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.
-
-The django-selectable widgets are compatitible with the add another popup in the
-admin. It's that little green plus sign that appears next to ``ForeignKey`` or
-``ManyToManyField`` items. This makes django-selectable a user friendly replacement
-for the `ModelAdmin.raw_id_fields <https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.raw_id_fields>`_
-when the default select box grows too long.
-
-
-.. _admin-inline-example:
-
-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/stable/ref/contrib/admin/#inlinemodeladmin-objects>`_.
-We can even make use of the same ``FarmAdminForm``.
-
- .. code-block:: python
-
- # continued from above
-
- class FarmInline(admin.TabularInline):
- model = Farm
- form = FarmAdminForm
-
-
- class NewUserAdmin(UserAdmin):
- inlines = [
- FarmInline,
- ]
-
-
- admin.site.unregister(User)
- admin.site.register(User, NewUserAdmin)
-
-The auto-complete functions will be bound as new forms are added dynamically.
+++ /dev/null
-Advanced Usage
-==========================
-
-We've gone through the most command and simple use cases for django-selectable. Now
-we'll take a look at some of the more advanced features of this project. This assumes
-that you are comfortable reading and writing a little bit of Javascript making
-use of jQuery.
-
-
-.. _additional-parameters:
-
-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 known until 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, you will have access to
-them. Since they can be manipulated on the client side, these parameters should be
-treated like all user input. It should be properly validated and sanitized.
-
-
-Limiting the Result Set
-_______________________________________
-
-The number of results are globally limited/paginated by the :ref:`SELECTABLE_MAX_LIMIT`
-but you can also lower this limit on the field or widget level. Each field and widget
-takes a ``limit`` argument in the ``__init__`` that will be passed back to the lookup
-through the ``limit`` query parameter. The result set will be automatically paginated
-for you if you use either this parameter or the global setting.
-
-
-.. _server-side-parameters:
-
-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'})
-
-You can also pass the query parameters into the widget using the ``query_params``
-keyword argument. It depends on your use case as to whether the parameters are
-known when the form is defined or when an instance of the form is created.
-
-
-.. _client-side-parameters:
-
-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>
-
-.. note::
-
- In v0.7 the scope of ``prepareQuery`` was updated so that ``this`` refers to the
- current ``djselectable`` plugin instance. Previously ``this`` refered to the
- plugin ``options`` instance.
-
-
-.. _chain-select-example:
-
-Chained Selection
---------------------------------------
-
-It's a fairly common pattern to have two or more inputs depend one another such City/State/Zip.
-In fact there are other Django apps dedicated to this purpose such as
-`django-smart-selects <https://github.com/digi604/django-smart-selects>`_ or
-`django-ajax-filtered-fields <http://code.google.com/p/django-ajax-filtered-fields/>`_.
-It's possible to handle this kind of selection with django-selectable if you are willing
-to write a little javascript.
-
-Suppose we have city model
-
- .. code-block:: python
-
- from __future__ import unicode_literals
-
- from django.db import models
- from django.utils.encoding import python_2_unicode_compatible
-
- from localflavor.us.models import USStateField
-
-
- @python_2_unicode_compatible
- class City(models.Model):
- name = models.CharField(max_length=200)
- state = USStateField()
-
- def __str__(self):
- return self.name
-
-Then in our lookup we will grab the state value and filter our results on it:
-
- .. code-block:: python
-
- from __future__ import unicode_literals
-
- from selectable.base import ModelLookup
- from selectable.registry import registry
-
- from .models import City
-
-
- class CityLookup(ModelLookup):
- model = City
- search_fields = ('name__icontains', )
-
- def get_query(self, request, term):
- results = super(CityLookup, self).get_query(request, term)
- state = request.GET.get('state', '')
- if state:
- results = results.filter(state=state)
- return results
-
- def get_item_label(self, item):
- return "%s, %s" % (item.name, item.state)
-
-
- registry.register(CityLookup)
-
-and a simple form
-
- .. code-block:: python
-
- from django import forms
-
- from localflavor.us.forms import USStateField, USStateSelect
-
- from selectable.forms import AutoCompleteSelectField, AutoComboboxSelectWidget
-
- from .lookups import CityLookup
-
-
- class ChainedForm(forms.Form):
- city = AutoCompleteSelectField(
- lookup_class=CityLookup,
- label='City',
- required=False,
- widget=AutoComboboxSelectWidget
- )
- state = USStateField(widget=USStateSelect, required=False)
-
-
-We want our users to select a city and if they choose a state then we will only
-show them cities in that state. To do this we will pass back chosen state as
-addition parameter with the following javascript:
-
- .. code-block:: html
-
- <script type="text/javascript">
- $(document).ready(function() {
- function newParameters(query) {
- query.state = $('#id_state').val();
- }
- $('#id_city_0').djselectable('option', 'prepareQuery', newParameters);
- });
- </script>
-
-And that's it! We now have a working chained selection example. The full source
-is included in the example project.
-
-.. _client-side-changes:
-
-Detecting Client Side Changes
-____________________________________________
-
-The previous example detected selection changes on the client side to allow passing
-parameters to the lookup. Since django-selectable is built on top of the jQuery UI
-`Autocomplete plug-in <http://jqueryui.com/demos/autocomplete/>`_, the widgets
-expose the events defined by the plugin.
-
- - djselectablecreate
- - djselectablesearch
- - djselectableopen
- - djselectablefocus
- - djselectableselect
- - djselectableclose
- - djselectablechange
-
-For the most part these event names should be self-explanatory. If you need additional
-detail you should refer to the `jQuery UI docs on these events <http://jqueryui.com/demos/autocomplete/#events>`_.
-
-The multiple select widgets include additional events which indicate when a new item is added
-or removed from the current list. These events are ``djselectableadd`` and ``djselectableremove``.
-These events pass a dictionary of data with the following keys
-
- - element: The original text input
- - input: The hidden input to be added for the new item
- - wrapper: The ``<li>`` element to be added to the deck
- - deck: The outer ``<ul>`` deck element
-
-You can use these events to prevent items from being added or removed from the deck by
-returning ``false`` in the handling function. A simple example is given below:
-
- .. code-block:: html
-
- <script type="text/javascript">
- $(document).ready(function() {
- $(':input[name=my_field_0]').bind('djselectableadd', function(event, item) {
- // Don't allow foo to be added
- if ($(item.input).val() === 'foo') {
- return false;
- }
- });
- });
- </script>
-
-
-Submit On Selection
---------------------------------------
-
-You might want to help your users by submitting the form once they have selected a valid
-item. To do this you simply need to listen for the ``djselectableselect`` event. This
-event is fired by the text input which has an index of 0. If your field is named ``my_field``
-then input to watch would be ``my_field_0`` such as:
-
- .. code-block:: html
-
- <script type="text/javascript">
- $(document).ready(function() {
- $(':input[name=my_field_0]').bind('djselectableselect', function(event, ui) {
- $(this).parents("form").submit();
- });
- });
- </script>
-
-
-Dynamically Added Forms
---------------------------------------
-
-django-selectable can work with dynamically added forms such as inlines in the admin.
-To make django-selectable work in the admin there is nothing more to do than include
-the necessary static media as described in the
-:ref:`Admin Integration <admin-jquery-include>` section.
-
-If you are making use of the popular `django-dynamic-formset <http://code.google.com/p/django-dynamic-formset/>`_
-then you can make django-selectable work by passing ``bindSelectables`` to the
-`added <http://code.google.com/p/django-dynamic-formset/source/browse/trunk/docs/usage.txt#259>`_ option:
-
- .. code-block:: html
-
- <script type="text/javascript">
- $(document).ready(function() {
- $('#my-formset').formset({
- added: bindSelectables
- });
- });
- </script>
-
-Currently you must include the django-selectable javascript below this formset initialization
-code for this to work. See django-selectable `issue #31 <https://github.com/mlavin/django-selectable/issues/31>`_
-for some additional detail on this problem.
-
-
-.. _advanced-label-formats:
-
-Label Formats on the Client Side
---------------------------------------
-
-The lookup label is the text which is shown in the list before it is selected.
-You can use the :ref:`get_item_label <lookup-get-item-label>` method in your lookup
-to do this on the server side. This works for most applications. However if you don't
-want to write your HTML in Python or need to adapt the format on the client side you
-can use the :ref:`formatLabel <javascript-formatLabel>` option.
-
-``formatLabel`` takes two paramaters the current label and the current selected item.
-The item is a dictionary object matching what is returned by the lookup's
-:ref:`format_item <lookup-format-item>`. ``formatLabel`` should return the string
-which should be used for the label.
-
-Going back to the ``CityLookup`` we can adjust the label to wrap the city and state
-portions with their own classes for additional styling:
-
- .. code-block:: html
-
- <script type="text/javascript">
- $(document).ready(function() {
- function formatLabel(label, item) {
- var data = label.split(',');
- return '<span class="city">' + data[0] + '</span>, <span class="state">' + data[1] + '</span>';
- }
- $('#id_city_0').djselectable('option', 'formatLabel', formatLabel);
- });
- </script>
-
-This is a rather simple example but you could also pass additional information in ``format_item``
-such as a flag of whether the city is the capital and render the state captials differently.
-
-.. _advanced-bootstrap:
-
-Using with Twitter Bootstrap
---------------------------------------
-
-django-selectable can work along side with Twitter Bootstrap but there are a few things to
-take into consideration. Both jQuery UI and Bootstrap define a ``$.button`` plugin. This
-plugin is used by default by django-selectable and expects the UI version. If the jQuery UI
-JS is included after the Bootstrap JS then this will work just fine but the Bootstrap
-button JS will not be available. This is the strategy taken by the `jQuery UI Bootstrap
-<http://addyosmani.github.com/jquery-ui-bootstrap/>`_ theme.
-
-Another option is to rename the Bootstrap plugin using the ``noConflict`` option.
-
- .. code-block:: html
-
- <!-- Include Bootstrap JS -->
- <script>$.fn.bootstrapBtn = $.fn.button.noConflict();</script>
- <!-- Include jQuery UI JS -->
-
-Even with this some might complain that it's too resource heavy to include all of
-jQuery UI when you just want the autocomplete to work with django-selectable. For
-this you can use the `Download Builder <http://jqueryui.com/download/>`_ to build
-a minimal set of jQuery UI widgets. django-selectable requires the UI core, autocomplete,
-menu and button widgets. None of the effects or interactions are needed. Minified
-this totals around 100 kb of JS, CSS and images (based on jQuery UI 1.10).
-
-.. note::
-
- For a comparison this is smaller than the minified Bootstrap 2.3.0 CSS
- which is 105 kb not including the responsive CSS or the icon graphics.
-
-It is possible to remove the dependency on the UI button plugin and instead
-use the Bootstrap button styles. This is done by overriding
-the ``_comboButtonTemplate`` and ``_removeButtonTemplate`` functions used to
-create the buttons. An example is given below.
-
- .. code-block:: html
-
- <script>
- $.ui.djselectable.prototype._comboButtonTemplate = function (input) {
- var icon = $("<i>").addClass("icon-chevron-down");
- // Remove current classes on the text input
- $(input).attr("class", "");
- // Wrap with input-append
- $(input).wrap('<div class="input-append" />');
- // Return button link with the chosen icon
- return $("<a>").append(icon).addClass("btn btn-small");
- };
- $.ui.djselectable.prototype._removeButtonTemplate = function (item) {
- var icon = $("<i>").addClass("icon-remove-sign");
- // Return button link with the chosen icon
- return $("<a>").append(icon).addClass("btn btn-small pull-right");
- };
- </script>
+++ /dev/null
-# -*- coding: utf-8 -*-
-#
-# Django-Selectable documentation build configuration file, created by
-# sphinx-quickstart on Sat Mar 12 14:14:16 2011.
-#
-# This file is execfile()d with the current directory set to its containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
-import datetime
-import sys, os
-import selectable
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.insert(0, os.path.abspath('.'))
-
-# -- General configuration -----------------------------------------------------
-
-# If your documentation needs a minimal Sphinx version, state it here.
-#needs_sphinx = '1.0'
-
-# Add any Sphinx extension module names here, as strings. They can be extensions
-# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
-extensions = []
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
-
-# The suffix of source filenames.
-source_suffix = '.rst'
-
-# The encoding of source files.
-#source_encoding = 'utf-8-sig'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General information about the project.
-project = u'Django-Selectable'
-copyright = u'2011-%s, Mark Lavin' % datetime.date.today().year
-
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-version = '.'.join(selectable.__version__.split('.')[0:2])
-# The full version, including alpha/beta/rc tags.
-release = selectable.__version__
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-#language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-#today = ''
-# Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-exclude_patterns = ['_build']
-
-# The reST default role (used for this markup: `text`) to use for all documents.
-#default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-#add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-#add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-#show_authors = False
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-# A list of ignored prefixes for module index sorting.
-#modindex_common_prefix = []
-
-
-# -- Options for HTML output ---------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages. See the documentation for
-# a list of builtin themes.
-html_theme = 'default'
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further. For a list of options available for each theme, see the
-# documentation.
-#html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-#html_theme_path = []
-
-# The name for this set of Sphinx documents. If None, it defaults to
-# "<project> v<release> documentation".
-#html_title = None
-
-# A shorter title for the navigation bar. Default is the same as html_title.
-#html_short_title = None
-
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-#html_logo = None
-
-# The name of an image file (within the static path) to use as favicon of the
-# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-#html_favicon = None
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-#html_static_path = ['_static']
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-#html_use_smartypants = True
-
-# Custom sidebar templates, maps document names to template names.
-#html_sidebars = {}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-#html_additional_pages = {}
-
-# If false, no module index is generated.
-#html_domain_indices = True
-
-# If false, no index is generated.
-#html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-#html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-#html_show_sourcelink = True
-
-# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-#html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-#html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it. The value of this option must be the
-# base URL from which the finished HTML is served.
-#html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = None
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'Django-Selectabledoc'
-
-
-# -- Options for LaTeX output --------------------------------------------------
-
-# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
-
-# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title, author, documentclass [howto/manual]).
-latex_documents = [
- ('index', 'Django-Selectable.tex', u'Django-Selectable Documentation',
- u'Mark Lavin', 'manual'),
-]
-
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-#latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-#latex_use_parts = False
-
-# If true, show page references after internal links.
-#latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-#latex_show_urls = False
-
-# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
-
-# Documents to append as an appendix to all manuals.
-#latex_appendices = []
-
-# If false, no module index is generated.
-#latex_domain_indices = True
-
-
-# -- Options for manual page output --------------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
- ('index', 'django-selectable', u'Django-Selectable Documentation',
- [u'Mark Lavin'], 1)
-]
+++ /dev/null
-.. _contributing-guide:
-
-Contributing
-==================
-
-There are plenty of ways to contribute to this project. If you think you've found
-a bug please submit an issue. If there is a feature you'd like to see then please
-open an ticket proposal for it. If you've come up with some helpful examples then
-you can add to our example project.
-
-
-Getting the Source
---------------------------------------
-
-The source code is hosted on `Github <https://github.com/mlavin/django-selectable>`_.
-You can download the full source by cloning the git repo::
-
- git clone git://github.com/mlavin/django-selectable.git
-
-Feel free to fork the project and make your own changes. If you think that it would
-be helpful for other then please submit a pull request to have it merged in.
-
-
-Submit an Issue
---------------------------------------
-
-The issues are also managed on `Github issue page <https://github.com/mlavin/django-selectable/issues>`_.
-If you think you've found a bug it's helpful if you indicate the version of django-selectable
-you are using the ticket version flag. If you think your bug is javascript related it is
-also helpful to know the version of jQuery, jQuery UI, and the browser you are using.
-
-Issues are also used to track new features. If you have a feature you would like to see
-you can submit a proposal ticket. You can also see features which are planned here.
-
-
-Submit a Translation
---------------------------------------
-
-We are working towards translating django-selectable into different languages. There
-are not many strings to be translated so it is a reasonably easy task and a great way
-to be involved with the project. The translations are managed through
-`Transifex <https://www.transifex.com/projects/p/django-selectable/>`_.
-
-Running the Test Suite
---------------------------------------
-
-There are a number of tests in place to test the server side code for this
-project. To run the tests you need Django and `mock <http://www.voidspace.org.uk/python/mock/>`_
-installed and run::
-
- python runtests.py
-
-`tox <http://tox.readthedocs.org/en/latest/index.html>`_ is used to test django-selectable
-against multiple versions of Django/Python. With tox installed you can run::
-
- tox
-
-to run all the version combinations. You can also run tox against a subset of supported
-environments::
-
- tox -e py27-django15
-
-For more information on running/installing tox please see the
-tox documentation: http://tox.readthedocs.org/en/latest/index.html
-
-Client side tests are written using `QUnit <http://docs.jquery.com/QUnit>`_. They
-can be found in ``selectable/tests/qunit/index.html``. The test suite also uses
-`PhantomJS <http://phantomjs.org/>`_ to
-run the tests. You can install PhantomJS from NPM::
-
- # Install requirements
- npm install -g phantomjs jshint
- make test-js
-
-
-Building the Documentation
---------------------------------------
-
-The documentation is built using `Sphinx <http://sphinx.pocoo.org/>`_
-and available on `Read the Docs <http://django-selectable.readthedocs.io/>`_. With
-Sphinx installed you can build the documentation by running::
-
- make html
-
-inside the docs directory. Documentation fixes and improvements are always welcome.
-
+++ /dev/null
-Fields
-==========
-
-Django-Selectable defines a number of fields for selecting either single or multiple
-lookup items. Item in this context corresponds to the object return by the underlying
-lookup ``get_item``. The single select select field :ref:`AutoCompleteSelectField`
-allows 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
-:ref:`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:
-
-AutoCompleteSelectField
---------------------------------------
-
-Field tied to :ref:`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.
-
- .. code-block:: python
-
- from django import forms
-
- from selectable.forms import AutoCompleteSelectField
-
- from .lookups import FruitLookup
-
-
- class FruitSelectionForm(forms.Form):
- fruit = AutoCompleteSelectField(lookup_class=FruitLookup, label='Select a fruit')
-
-`lookup_class`` may also be a dotted path.
-
-
-.. _AutoCompleteSelectMultipleField:
-
-AutoCompleteSelectMultipleField
---------------------------------------
-
-Field tied to :ref:`AutoCompleteSelectMultipleWidget` to bind the selection to the form.
-This field cleans to a list of items. :ref:`AutoCompleteSelectMultipleField` does not
-allow for the creation of new items.
-
-
- .. code-block:: python
-
- from django import forms
-
- from selectable.forms import AutoCompleteSelectMultipleField
-
- from .lookups import FruitLookup
-
-
- class FruitsSelectionForm(forms.Form):
- fruits = AutoCompleteSelectMultipleField(lookup_class=FruitLookup,
- label='Select your favorite fruits')
+++ /dev/null
-.. include:: ../README.rst
-
-Contents:
-
-.. toctree::
- :maxdepth: 2
-
- overview
- quick-start
- lookups
- advanced
- admin
- testing
- fields
- widgets
- settings
- contribute
- releases
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
-
+++ /dev/null
-Defining Lookups
-==================
-
-What are Lookups?
---------------------------------------
-
-Lookups define the corresponding ajax views used by the auto-completion
-fields and widgets. They take in the current request and return the JSON
-needed by the jQuery auto-complete plugin.
-
-
-Defining a Lookup
---------------------------------------
-
-django-selectable uses a registration pattern similar to the Django admin.
-Lookups should be defined in a `lookups.py` in your application's module. Once defined
-you must register in with django-selectable. All lookups must extend from
-``selectable.base.LookupBase`` which defines the API for every lookup.
-
- .. code-block:: python
-
- from selectable.base import LookupBase
- from selectable.registry import registry
-
- class MyLookup(LookupBase):
- def get_query(self, request, term):
- data = ['Foo', 'Bar']
- return [x for x in data if x.startswith(term)]
-
- registry.register(MyLookup)
-
-
-Lookup API
---------------------------------------
-
-.. py:method:: LookupBase.get_query(request, term)
-
- This is the main method which takes the current request
- from the user and returns the data which matches their search.
-
- :param request: The current request object.
- :param term: The search term from the widget input.
- :return: An iterable set of data of items matching the search term.
-
-.. _lookup-get-item-label:
-
-.. py:method:: LookupBase.get_item_label(item)
-
- This is first of three formatting methods. The label is shown in the
- drop down menu of search results. This defaults to ``item.__unicode__``.
-
- :param item: An item from the search results.
- :return: A string representation of the item to be shown in the search results.
- The label can include HTML. For changing the label format on the client side
- see :ref:`Advanced Label Formats <advanced-label-formats>`.
-
-
-.. py:method:: LookupBase.get_item_id(item)
-
- This is second of three formatting methods. The id is the value that will eventually
- be returned by the field/widget. This defaults to ``item.__unicode__``.
-
- :param item: An item from the search results.
- :return: A string representation of the item to be returned by the field/widget.
-
-
-.. py:method:: LookupBase.split_term(term)
-
- Split searching term into array of subterms that will be searched separately.
- You can override this function to achieve different splitting of the term.
-
- :param term: The search term.
- :return: Array with subterms
-
-.. py:method:: LookupBase.get_item_value(item)
-
- This is last of three formatting methods. The value is shown in the
- input once the item has been selected. This defaults to ``item.__unicode__``.
-
- :param item: An item from the search results.
- :return: A string representation of the item to be shown in the input.
-
-.. py:method:: LookupBase.get_item(value)
-
- ``get_item`` is the reverse of ``get_item_id``. This should take the value
- from the form initial values and return the current item. This defaults
- to simply return the value.
-
- :param value: Value from the form inital value.
- :return: The item corresponding to the initial value.
-
-.. py:method:: LookupBase.create_item(value)
-
- If you plan to use a lookup with a field or widget which allows the user
- to input new values then you must define what it means to create a new item
- for your lookup. By default this raises a ``NotImplemented`` error.
-
- :param value: The user given value.
- :return: The new item created from the item.
-
-.. _lookup-format-item:
-
-.. py:method:: LookupBase.format_item(item)
-
- By default ``format_item`` creates a dictionary with the three keys used by
- the UI plugin: id, value, label. These are generated from the calls to
- ``get_item_id``, ``get_item_value`` and ``get_item_label``. If you want to
- add additional keys you should add them here.
-
- The results of ``get_item_label`` is conditionally escaped to prevent
- Cross Site Scripting (XSS) similar to the templating language.
- If you know that the content is safe and you want to use these methods
- to include HTML should mark the content as safe with ``django.utils.safestring.mark_safe``
- inside the ``get_item_label`` method.
-
- ``get_item_id`` and ``get_item_value`` are not escapted by default. These are
- not a XSS vector with the built-in JS. If you are doing additional formating using
- these values you should be conscience of this fake and be sure to escape these
- values.
-
- :param item: An item from the search results.
- :return: A dictionary of information for this item to be sent back to the client.
-
-There are also some additional methods that you could want to use/override. These
-are for more advanced use cases such as using the lookups with JS libraries other
-than jQuery UI. Most users will not need to override these methods.
-
-.. _lookup-format-results:
-
-.. py:method:: LookupBase.format_results(self, raw_data, options)
-
- Returns a python structure that later gets serialized. This makes a call to
- :ref:`paginate_results<lookup-paginate-results>` prior to calling
- :ref:`format_item<lookup-format-item>` on each item in the current page.
-
- :param raw_data: The set of all matched results.
- :param options: Dictionary of ``cleaned_data`` from the lookup form class.
- :return: A dictionary with two keys ``meta`` and ``data``.
- The value of ``data`` is an iterable extracted from page_data.
- The value of ``meta`` is a dictionary. This is a copy of options with one additional element
- ``more`` which is a translatable "Show more" string
- (useful for indicating more results on the javascript side).
-
-.. _lookup-paginate-results:
-
-.. py:method:: LookupBase.paginate_results(results, options)
-
- If :ref:`SELECTABLE_MAX_LIMIT` is defined or ``limit`` is passed in request.GET
- then ``paginate_results`` will return the current page using Django's
- built in pagination. See the Django docs on
- `pagination <https://docs.djangoproject.com/en/stable/topics/pagination/>`_
- for more info.
-
- :param results: The set of all matched results.
- :param options: Dictionary of ``cleaned_data`` from the lookup form class.
- :return: The current `Page object <https://docs.djangoproject.com/en/stable/topics/pagination/#page-objects>`_
- of results.
-
-
-.. _ModelLookup:
-
-Lookups Based on Models
---------------------------------------
-
-Perhaps the most common use case is to define a lookup based on a given Django model.
-For this you can extend ``selectable.base.ModelLookup``. To extend ``ModelLookup`` you
-should set two class attributes: ``model`` and ``search_fields``.
-
- .. code-block:: python
-
- from __future__ import unicode_literals
-
- from selectable.base import ModelLookup
- from selectable.registry import registry
-
- from .models import Fruit
-
-
- class FruitLookup(ModelLookup):
- model = Fruit
- search_fields = ('name__icontains', )
-
- registry.register(FruitLookup)
-
-The syntax for ``search_fields`` is the same as the Django
-`field lookup syntax <http://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups>`_.
-Each of these lookups are combined as OR so any one of them matching will return a
-result. You may optionally define a third class attribute ``filters`` which is a dictionary of
-filters to be applied to the model queryset. The keys should be a string defining a field lookup
-and the value should be the value for the field lookup. Filters on the other hand are
-combined with AND.
-
-
-User Lookup Example
---------------------------------------
-
-Below is a larger model lookup example using multiple search fields, filters
-and display options for the `auth.User <https://docs.djangoproject.com/en/stable/topics/auth/#users>`_
-model.
-
- .. code-block:: python
-
- from django.contrib.auth.models import User
- from selectable.base import ModelLookup
- from selectable.registry import registry
-
-
- class UserLookup(ModelLookup):
- model = User
- search_fields = (
- 'username__icontains',
- 'first_name__icontains',
- 'last_name__icontains',
- )
- filters = {'is_active': True, }
-
- def get_item_value(self, item):
- # Display for currently selected item
- return item.username
-
- def get_item_label(self, item):
- # Display for choice listings
- return u"%s (%s)" % (item.username, item.get_full_name())
-
- registry.register(UserLookup)
-
-
-.. _lookup-decorators:
-
-Lookup Decorators
---------------------------------------
-
-Registering lookups with django-selectable creates a small API for searching the
-lookup data. While the amount of visible data is small there are times when you want
-to restrict the set of requests which can view the data. For this purpose there are
-lookup decorators. To use them you simply decorate your lookup class.
-
- .. code-block:: python
-
- from django.contrib.auth.models import User
- from selectable.base import ModelLookup
- from selectable.decorators import login_required
- from selectable.registry import registry
-
-
- @login_required
- class UserLookup(ModelLookup):
- model = User
- search_fields = ('username__icontains', )
- filters = {'is_active': True, }
-
- registry.register(UserLookup)
-
-.. note::
-
- The class decorator syntax was introduced in Python 2.6. If you are using
- django-selectable with Python 2.5 you can still make use of these decorators
- by applying the without the decorator syntax.
-
- .. code-block:: python
-
- class UserLookup(ModelLookup):
- model = User
- search_fields = ('username__icontains', )
- filters = {'is_active': True, }
-
- UserLookup = login_required(UserLookup)
-
- registry.register(UserLookup)
-
-Below are the descriptions of the available lookup decorators.
-
-
-ajax_required
-______________________________________
-
-The django-selectable javascript will always request the lookup data via
-XMLHttpRequest (AJAX) request. This decorator enforces that the lookup can only
-be accessed in this way. If the request is not an AJAX request then it will return
-a 400 Bad Request response.
-
-
-login_required
-______________________________________
-
-This decorator requires the user to be authenticated via ``request.user.is_authenticated``.
-If the user is not authenticated this will return a 401 Unauthorized response.
-``request.user`` is set by the ``django.contrib.auth.middleware.AuthenticationMiddleware``
-which is required for this decorator to work. This middleware is enabled by default.
-
-staff_member_required
-______________________________________
-
-This decorator builds from ``login_required`` and in addition requires that
-``request.user.is_staff`` is ``True``. If the user is not authenticatated this will
-continue to return at 401 response. If the user is authenticated but not a staff member
-then this will return a 403 Forbidden response.
+++ /dev/null
-@ECHO OFF
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
- set SPHINXBUILD=sphinx-build
-)
-set BUILDDIR=_build
-set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
-if NOT "%PAPER%" == "" (
- set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
-)
-
-if "%1" == "" goto help
-
-if "%1" == "help" (
- :help
- echo.Please use `make ^<target^>` where ^<target^> is one of
- echo. html to make standalone HTML files
- echo. dirhtml to make HTML files named index.html in directories
- echo. singlehtml to make a single large HTML file
- echo. pickle to make pickle files
- echo. json to make JSON files
- echo. htmlhelp to make HTML files and a HTML help project
- echo. qthelp to make HTML files and a qthelp project
- echo. devhelp to make HTML files and a Devhelp project
- echo. epub to make an epub
- echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
- echo. text to make text files
- echo. man to make manual pages
- echo. changes to make an overview over all changed/added/deprecated items
- echo. linkcheck to check all external links for integrity
- echo. doctest to run all doctests embedded in the documentation if enabled
- goto end
-)
-
-if "%1" == "clean" (
- for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
- del /q /s %BUILDDIR%\*
- goto end
-)
-
-if "%1" == "html" (
- %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The HTML pages are in %BUILDDIR%/html.
- goto end
-)
-
-if "%1" == "dirhtml" (
- %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
- goto end
-)
-
-if "%1" == "singlehtml" (
- %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
- goto end
-)
-
-if "%1" == "pickle" (
- %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can process the pickle files.
- goto end
-)
-
-if "%1" == "json" (
- %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can process the JSON files.
- goto end
-)
-
-if "%1" == "htmlhelp" (
- %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can run HTML Help Workshop with the ^
-.hhp project file in %BUILDDIR%/htmlhelp.
- goto end
-)
-
-if "%1" == "qthelp" (
- %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; now you can run "qcollectiongenerator" with the ^
-.qhcp project file in %BUILDDIR%/qthelp, like this:
- echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Django-Selectable.qhcp
- echo.To view the help file:
- echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Django-Selectable.ghc
- goto end
-)
-
-if "%1" == "devhelp" (
- %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished.
- goto end
-)
-
-if "%1" == "epub" (
- %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The epub file is in %BUILDDIR%/epub.
- goto end
-)
-
-if "%1" == "latex" (
- %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
- goto end
-)
-
-if "%1" == "text" (
- %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The text files are in %BUILDDIR%/text.
- goto end
-)
-
-if "%1" == "man" (
- %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
- if errorlevel 1 exit /b 1
- echo.
- echo.Build finished. The manual pages are in %BUILDDIR%/man.
- goto end
-)
-
-if "%1" == "changes" (
- %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
- if errorlevel 1 exit /b 1
- echo.
- echo.The overview file is in %BUILDDIR%/changes.
- goto end
-)
-
-if "%1" == "linkcheck" (
- %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
- if errorlevel 1 exit /b 1
- echo.
- echo.Link check complete; look for any errors in the above output ^
-or in %BUILDDIR%/linkcheck/output.txt.
- goto end
-)
-
-if "%1" == "doctest" (
- %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
- if errorlevel 1 exit /b 1
- echo.
- echo.Testing of doctests in the sources finished, look at the ^
-results in %BUILDDIR%/doctest/output.txt.
- goto end
-)
-
-:end
+++ /dev/null
-Overview
-==================
-
-Motivation
---------------------------------------
-
-There are many Django apps related to auto-completion why create another? One problem
-was varying support for the `jQuery UI auto-complete plugin <http://jqueryui.com/demos/autocomplete/>`_
-versus the now deprecated `bassistance version <http://bassistance.de/jquery-plugins/jquery-plugin-autocomplete/>`_.
-Another was support for combo-boxes and multiple selects. And lastly was a simple syntax for
-defining the related backend views for the auto-completion.
-
-This library aims to meet all of these goals:
- - Built on jQuery UI auto-complete
- - Fields and widgets for a variety of use-cases:
- - Text inputs and combo-boxes
- - Text selection
- - Value/ID/Foreign key selection
- - Multiple object selection
- - Allowing new values
- - Simple and extendable syntax for defining backend views
-
-
-Related Projects
---------------------------------------
-
-Much of the work here was inspired by things that I like (and things I don't like) about
-`django-ajax-selects <http://code.google.com/p/django-ajax-selects/>`_. To see some of the
-other Django apps for handling auto-completion see `Django-Packages <http://djangopackages.com/grids/g/auto-complete/>`_.
+++ /dev/null
-Getting Started
-==================
-
-The workflow for using `django-selectable` involves two main parts:
- - Defining your lookups
- - Defining your forms
-
-This guide assumes that you have a basic knowledge of creating Django models and
-forms. If not you should first read through the documentation on
-`defining models <http://docs.djangoproject.com/en/stable/topics/db/models/>`_
-and `using forms <http://docs.djangoproject.com/en/stable/topics/forms/>`_.
-
-.. _start-include-jquery:
-
-Including jQuery & jQuery UI
---------------------------------------
-
-The widgets in django-selectable define the media they need as described in the
-Django documentation on `Form Media <https://docs.djangoproject.com/en/stable/topics/forms/media/>`_.
-That means to include the javascript and css you need to make the widgets work you
-can include ``{{ form.media.css }}`` and ``{{ form.media.js }}`` in your template. This is
-assuming your form is called `form` in the template context. For more information
-please check out the `Django documentation <https://docs.djangoproject.com/en/stable/topics/forms/media/>`_.
-
-The jQuery and jQuery UI libraries are not included in the distribution but must be included
-in your templates. However there is a template tag to easily add these libraries from
-the from the `Google CDN <http://code.google.com/apis/libraries/devguide.html#jquery>`_.
-
- .. code-block:: html
-
- {% load selectable_tags %}
- {% include_jquery_libs %}
-
-By default these will use jQuery v1.11.2 and jQuery UI v1.11.3. You can customize the versions
-used by pass them to the tag. The first version is the jQuery version and the second is the
-jQuery UI version.
-
- .. code-block:: html
-
- {% load selectable_tags %}
- {% include_jquery_libs '1.11.2' '1.11.3' %}
-
-Django-Selectable should work with `jQuery <http://jquery.com/>`_ >= 1.9 and
-`jQuery UI <http://jqueryui.com/>`_ >= 1.10.
-
-You must also include a `jQuery UI theme <http://jqueryui.com/themeroller/>`_ stylesheet. There
-is also a template tag to easily add this style sheet from the Google CDN.
-
- .. code-block:: html
-
- {% load selectable_tags %}
- {% include_ui_theme %}
-
-By default this will use the `base <http://jqueryui.com/themeroller/>`_ theme for jQuery UI v1.11.4.
-You can configure the theme and version by passing them in the tag.
-
- .. code-block:: html
-
- {% load selectable_tags %}
- {% include_ui_theme 'ui-lightness' '1.11.4' %}
-
-Or only change the theme.
-
- .. code-block:: html
-
- {% load selectable_tags %}
- {% include_ui_theme 'ui-lightness' %}
-
-See the the jQuery UI documentation for a full list of available stable themes: http://jqueryui.com/download#stable-themes
-
-Of course you can choose to include these rescources manually::
-
- .. code-block:: html
-
- <link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/themes/base/jquery-ui.css" type="text/css">
- <link href="{% static 'selectable/css/dj.selectable.css' %}" type="text/css" media="all" rel="stylesheet">
- <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
- <script src="//ajax.googleapis.com/ajax/libs/jqueryui/1.11.3/jquery-ui.js"></script>
- <script type="text/javascript" src="{% static 'selectable/js/jquery.dj.selectable.js' %}"></script>
-
-.. note::
-
- jQuery UI shares a few plugin names with the popular Twitter Bootstrap framework. There
- are notes on using Bootstrap along with django-selectable in the :ref:`advanced usage
- section <advanced-bootstrap>`.
-
-
-Defining a Lookup
---------------------------------
-
-The lookup classes define the backend views. The most common case is defining a
-lookup which searchs models based on a particular field. Let's define a simple model:
-
- .. code-block:: python
-
- from __future__ import unicode_literals
-
- from django.db import models
- from django.utils.encoding import python_2_unicode_compatible
-
-
- @python_2_unicode_compatible
- class Fruit(models.Model):
- name = models.CharField(max_length=200)
-
- def __str__(self):
- return self.name
-
-In a `lookups.py` we will define our lookup:
-
- .. code-block:: python
-
- from __future__ import unicode_literals
-
- from selectable.base import ModelLookup
- from selectable.registry import registry
-
- from .models import Fruit
-
-
- class FruitLookup(ModelLookup):
- model = Fruit
- search_fields = ('name__icontains', )
-
-
-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.
-This syntax should look familiar as it is the same as the `field lookup syntax <http://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups>`_
-for making queries in Django.
-
-Below this definition we will register our lookup class.
-
- .. code-block:: python
-
- registry.register(FruitLookup)
-
-.. note::
-
- You should only register your lookup once. Attempting to register the same lookup class
- more than once will lead to ``LookupAlreadyRegistered`` errors. A common problem related to the
- ``LookupAlreadyRegistered`` error is related to inconsistant import paths in your project.
- Prior to Django 1.4 the default ``manage.py`` allows for importing both with and without
- the project name (i.e. ``from myproject.myapp import lookups`` or ``from myapp import lookups``).
- This leads to the ``lookup.py`` file being imported twice and the registration code
- executing twice. Thankfully this is no longer the default in Django 1.4. Keeping
- your import consistant to include the project name (when your app is included inside the
- project directory) will avoid these errors.
-
-
-Defining Forms
---------------------------------
-
-Now that we have a working lookup we will define a form which uses it:
-
- .. code-block:: python
-
- from django import forms
-
- from selectable.forms import AutoCompleteWidget
-
- from .lookups import FruitLookup
-
-
- class FruitForm(forms.Form):
- autocomplete = forms.CharField(
- label='Type the name of a fruit (AutoCompleteWidget)',
- widget=AutoCompleteWidget(FruitLookup),
- required=False,
- )
-
-
-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
-existing ``Fruit`` models.
-
-And that's pretty much it. Keep on reading if you want to learn about the other
-types of fields and widgets that are available as well as defining more complicated
-lookups.
+++ /dev/null
-Release Notes
-==================
-
-
-v1.2.1 (Released 2019-02-02)
---------------------------------------
-
-Fixed compatibility issue with jQuery UI 1.12. Thanks to Christian Klus (kluchrj) for the fix.
-
-
-v1.2.0 (Released 2018-10-13)
---------------------------------------
-
-Primarily a Django support related release. This version adds support for Django 2.0 and 2.1 while
-dropping support for Django versions below 1.11. A number of deprecation warnings for future Django
-versions have also been addressed.
-
-Added the ability to search on multiple terms split by whitespace.
-
-
-Backwards Incompatible Changes
-________________________________
-
-- Dropped support for Django versions below 1.11
-
-
-v1.1.0 (Released 2018-01-12)
---------------------------------------
-
-- Updated admin docs.
-- Added support for Django 1.11
-
-Special thanks to Luke Plant for contributing the fixes to support Django 1.11.
-
-
-v1.0.0 (Released 2017-04-14)
---------------------------------------
-
-This project has been stable for quite some time and finally declaring a 1.0 release. With
-that comes new policies on official supported versions for Django, Python, jQuery, and jQuery UI.
-
-- New translations for German and Czech.
-- Various bug and compatibility fixes.
-- Updated example project.
-
-Special thanks to Raphael Merx for helping track down issues related to this release
-and an updating the example project to work on Django 1.10.
-
-Backwards Incompatible Changes
-________________________________
-
-- Dropped support Python 2.6 and 3.2
-- Dropped support for Django < 1.7. Django 1.11 is not yet supported.
-- ``LookupBase.serialize_results`` had been removed. This is now handled by the built-in ``JsonResponse`` in Django.
-- jQuery and jQuery UI versions for the ``include_jquery_libs`` and ``include_ui_theme`` template tags have been increased to 1.12.4 and 1.11.4 respectively.
-- Dropped testing support for jQuery < 1.9 and jQuery UI < 1.10. Earlier versions may continue to work but it is recommended to upgrade.
-
-
-v0.9.0 (Released 2014-10-21)
---------------------------------------
-
-This release primarily addresses incompatibility with Django 1.7. The app-loading refactor both
-broke the previous registration and at the same time provided better utilities in Django core to
-make it more robust.
-
-- Compatibility with Django 1.7. Thanks to Calvin Spealman for the fixes.
-- Fixes for Python 3 support.
-
-Backwards Incompatible Changes
-________________________________
-
-- Dropped support for jQuery < 1.7
-
-
-v0.8.0 (Released 2014-01-20)
---------------------------------------
-
-- Widget media references now include a version string for cache-busting when upgrading django-selectable. Thanks to Ustun Ozgur.
-- Added compatibility code for \*SelectWidgets to handle POST data for the default SelectWidget. Thanks to leo-the-manic.
-- Development moved from Bitbucket to Github.
-- Update test suite compatibility with new test runner in Django 1.6. Thanks to Dan Poirier for the report and fix.
-- Tests now run on Travis CI.
-- Added French and Chinese translations.
-
-Backwards Incompatible Changes
-________________________________
-
-- Support for Django < 1.5 has been dropped. Most pieces should continue to work but there was an ugly JS hack to make django-selectable work nicely in the admin which too flakey to continue to maintain. If you aren't using the selectable widgets in inline-forms in the admin you can most likely continue to use Django 1.4 without issue.
-
-
-v0.7.0 (Released 2013-03-01)
---------------------------------------
-
-This release features a large refactor of the JS plugin used by the widgets. While this
-over makes the plugin more maintainable and allowed for some of the new features in this
-release, it does introduce a few incompatible changes. For the most part places where you
-might have previously used the ``autocomplete`` namespace/plugin, those references should
-be updated to reference the ``djselectable`` plugin.
-
-This release also adds experimental support for Python 3.2+ to go along with Django's support in 1.5.
-To use Python 3 with django-selectable you will need to use Django 1.5+.
-
-- Experimental Python 3.2+ support
-- Improved the scope of ``prepareQuery`` and ``formatLabel`` options. Not fully backwards compatible. Thanks to Augusto Men.
-- Allow passing the Python path string in place of the lookup class to the fields and widgets. Thanks to Michael Manfre.
-- Allow passing JS plugin options through the widget ``attrs`` option. Thanks to Felipe Prenholato.
-- Tests for compatibility with jQuery 1.6 through 1.9 and jQuery UI 1.8 through 1.10.
-- Added notes on Bootstrap compatibility.
-- Added compatibility with Grappelli in the admin.
-- Added Spanish translation thanks to Manuel Alvarez.
-- Added documentation notes on testing.
-
-Bug Fixes
-_________________
-
-- Fixed bug with matching hidden input when the name contains '_1'. Thanks to Augusto Men for the report and fix.
-- Fixed bug where the enter button would open the combobox options rather than submit the form. Thanks to Felipe Prenholato for the report.
-- Fixed bug with using ``allow_new=True`` creating items when no data was submitted. See #91.
-- Fixed bug with widget ``has_changed`` when there is no initial data. See #92.
-
-
-Backwards Incompatible Changes
-________________________________
-
-- The JS event namespace has changed from ``autocomplete`` to ``djselectable``.
-- ``data('autocomplete')`` is no longer available on the widgets on the client-side. Use ``data('djselectable')`` instead.
-- Combobox button was changed from a ``<button>`` to ``<a>``. Any customized styles you may have should be updated.
-- Combobox no longer changes the ``minLength`` or ``delay`` options.
-
-
-v0.6.2 (Released 2012-11-07)
---------------------------------------
-
-Bug Fixes
-_________________
-
-- Fixed bug with special characters when highlighting matches. Thanks to Chad Files for the report.
-- Fixed javascript bug with spaces in ``item.id``. Thanks to @dc for the report and fix.
-
-
-v0.6.1 (Released 2012-10-13)
---------------------------------------
-
-Features
-_________________
-
-- Added Polish translation. Thanks to Sławomir Ehlert.
-
-Bug Fixes
-_________________
-
-- Fixed incompatibility with jQuery UI 1.9.
-
-
-v0.6.0 (Released 2012-10-09)
---------------------------------------
-
-This release continues to clean up the API and JS. This was primarily motivated by
-Sławomir Ehlert (@slafs) who is working on an alternate implementation which
-uses Select2 rather than jQuery UI. This opens the door for additional apps
-which use the same lookup declaration API with a different JS library on the front
-end.
-
-Python 2.5 support has been dropped to work towards Python 3 support.
-This also drops Django 1.2 support which is no longer receiving security fixes.
-
-Features
-_________________
-
-- Initial translations (pt_BR). Thanks to Felipe Prenholato for the patch.
-- Upgraded default jQuery UI version included by the template tags from 1.8.18 to 1.8.23
-- Added ``djselectableadd`` and ``djselectableremove`` events fired when items are added or removed from a mutliple select
-
-Bug Fixes
-_________________
-
-- Cleaned up JS scoping problems when multiple jQuery versions are used on the page. Thanks Antti Kaihola for the report.
-- Fixed minor JS bug where text input was not cleared when selected via the combobox in the multiselect. Thanks Antti Kaihola for the report and Lukas Pirl for a hotfix.
-
-Backwards Incompatible Changes
-________________________________
-
-- ``get_item_value`` and ``get_item_id`` are no longer marked as safe by default.
-- Removed AutoComboboxSelectField and AutoComboboxSelectMultipleField. These were deprecated in 0.5.
-- Dropping official Python 2.5 support.
-- Dropping official Django 1.2 support.
-- ``paginate_results`` signature changed as part of the lookup refactor.
-- ``SELECTABLE_MAX_LIMIT`` can no longer be ``None``.
-
-
-v0.5.2 (Released 2012-06-27)
---------------------------------------
-
-Bug Fixes
-_________________
-
-- Fixed XSS flaw with lookup ``get_item_*`` methods. Thanks slafs for the report.
-- Fixed bug when passing widget instance rather than widget class to ``AutoCompleteSelectField`` or ``AutoCompleteSelectMultipleField``.
-
-
-v0.5.1 (Released 2012-06-08)
---------------------------------------
-
-Bug Fixes
-_________________
-
-- Fix for double ``autocompleteselect`` event firing.
-- Fix for broken pagination in search results. Thanks David Ray for report and fix.
-
-
-v0.4.2 (Released 2012-06-08)
---------------------------------------
-
-Bug Fixes
-_________________
-
-- Backported fix for double ``autocompleteselect`` event firing.
-- Backported fix for broken pagination in search results.
-
-
-v0.5.0 (Released 2012-06-02)
---------------------------------------
-
-Features
-_________________
-
-- Template tag to add necessary jQuery and jQuery UI libraries. Thanks to Rick Testore for the initial implementation
-- :ref:`Lookup decorators <lookup-decorators>` for requiring user authentication or staff access to use the lookup
-- Additional documentation
-- Minor updates to the example project
-
-Backwards Incompatible Changes
-________________________________
-
-- Previously the minimal version of jQuery was listed as 1.4.3 when it fact there was a bug a that made django-selectable require 1.4.4. Not a new incompatibility but the docs have now been updated and 1.4.3 compatibility will not be added. Thanks to Rick Testore for the report and the fix
-- Started deprecation path for AutoComboboxSelectField and AutoComboboxSelectMultipleField
-
-
-v0.4.1 (Released 2012-03-11)
---------------------------------------
-
-Bug Fixes
-_________________
-
-- Cleaned up whitespace in css/js. Thanks Dan Poirier for the report and fix.
-- Fixed issue with saving M2M field data with AutoCompleteSelectMultipleField. Thanks Raoul Thill for the report.
-
-
-v0.4.0 (Released 2012-02-25)
---------------------------------------
-
-Features
-_________________
-
-- Better compatibility with :ref:`AutoCompleteSelectWidget`/:ref:`AutoComboboxSelectWidget` and Django's ModelChoiceField
-- Better compatibility with the Django admin :ref:`add another popup <admin-basic-example>`
-- Easier passing of query parameters. See the :ref:`Additional Parameters <additional-parameters>` section
-- Additional documentation
-- QUnit tests for JS functionality
-
-
-Backwards Incompatible Changes
-________________________________
-
-- Support for ``ModelLookup.search_field`` string has been removed. You should use the ``ModelLookup.search_fields`` tuple instead.
-
-
-v0.3.1 (Released 2012-02-23)
---------------------------------------
-
-Bug Fixes
-_________________
-
-- Fixed issue with media urls when not using staticfiles.
-
-
-v0.3.0 (Released 2012-02-15)
---------------------------------------
-
-Features
-_________________
-
-- Multiple search fields for :ref:`model based lookups <ModelLookup>`
-- Support for :ref:`highlighting term matches <javascript-highlightMatch>`
-- Support for HTML in :ref:`result labels <lookup-get-item-label>`
-- Support for :ref:`client side formatting <advanced-label-formats>`
-- Additional documentation
-- Expanded examples in example project
-
-
-Bug Fixes
-_________________
-
-- Fixed issue with Enter key removing items from select multiple widgets `#24 <https://github.com/mlavin/django-selectable/issues/24>`_
-
-
-Backwards Incompatible Changes
-________________________________
-
-- The fix for #24 changed the remove items from a button to an anchor tag. If you were previously using the button tag for additional styling then you will need to adjust your styles.
-- The static resources were moved into a `selectable` sub-directory. This makes the media more in line with the template directory conventions. If you are using the widgets in the admin there is nothing to change. If you are using ``{{ form.media }}`` then there is also nothing to change. However if you were including static media manually then you will need to adjust them to include the selectable prefix.
-
-
-v0.2.0 (Released 2011-08-13)
---------------------------------------
-
-Features
-_________________
-
-- Additional documentation
-- :ref:`Positional configuration <AutoCompleteSelectMultipleWidget>` for multiple select fields/widgets
-- :ref:`Settings/configuration <SELECTABLE_MAX_LIMIT>` for limiting/paginating result sets
-- Compatibility and examples for :ref:`Admin inlines <admin-inline-example>`
-- JS updated for jQuery 1.6 compatibility
-- :ref:`JS hooks <client-side-parameters>` for updating query parameters
-- :ref:`Chained selection example <chain-select-example>`
-
-
-v0.1.2 (Released 2011-05-25)
---------------------------------------
-
-Bug Fixes
-_________________
-
-- Fixed issue `#17 <https://github.com/mlavin/django-selectable/issues/17>`_
-
-
-v0.1.1 (Release 2011-03-21)
---------------------------------------
-
-Bug Fixes
-_________________
-
-- Fixed/cleaned up multiple select fields and widgets
-- Added media definitions to widgets
-
-
-Features
-_________________
-
-- Additional documentation
-- Added `update_query_parameters` to widgets
-- Refactored JS for easier configuration
-
-
-v0.1 (Released 2011-03-13)
---------------------------------------
-
-Initial public release
+++ /dev/null
-Settings
-==================
-
-
-.. _SELECTABLE_MAX_LIMIT:
-
-SELECTABLE_MAX_LIMIT
---------------------------------------
-
-This setting is used to limit the number of results returned by the auto-complete fields.
-Each field/widget can individually lower this maximum. The result sets will be
-paginated allowing the client to ask for more results. The limit is passed as a
-query parameter and validated against this value to ensure the client cannot manipulate
-the query string to retrive more values.
-
-Default: ``25``
-
-
-.. _SELECTABLE_ESCAPED_KEYS:
-
-SELECTABLE_ESCAPED_KEYS
---------------------------------------
-
-The ``LookupBase.format_item`` will conditionally escape result keys based on this
-setting. The label is escaped by default to prevent a XSS flaw when using the
-jQuery UI autocomplete. If you are using the lookup responses for a different
-autocomplete plugin then you may need to esacpe more keys by default.
-
-Default: ``('label', )``
-
-.. note::
- You probably don't want to include ``id`` in this setting.
-
-
-.. _javascript-options:
-
-Javascript Plugin Options
---------------------------------------
-
-Below the options for configuring the Javascript behavior of the django-selectable
-widgets.
-
-
-.. _javascript-removeIcon:
-
-removeIcon
-______________________________________
-
-
-This is the class name used for the remove buttons for the multiple select widgets.
-The set of icon classes built into the jQuery UI framework can be found here:
-http://jqueryui.com/themeroller/
-
-Default: ``ui-icon-close``
-
-
-.. _javascript-comboboxIcon:
-
-comboboxIcon
-______________________________________
-
-
-This is the class name used for the combobox dropdown icon. The set of icon classes built
-into the jQuery UI framework can be found here: http://jqueryui.com/themeroller/
-
-Default: ``ui-icon-triangle-1-s``
-
-
-.. _javascript-prepareQuery:
-
-prepareQuery
-______________________________________
-
-
-``prepareQuery`` is a function that is run prior to sending the search request to
-the server. It is an oppotunity to add additional parameters to the search query.
-It takes one argument which is the current search parameters as a dictionary. For
-more information on its usage see :ref:`Adding Parameters on the Client Side <client-side-parameters>`.
-
-Default: ``null``
-
-
-.. _javascript-highlightMatch:
-
-highlightMatch
-______________________________________
-
-
-If true the portions of the label which match the current search term will be wrapped
-in a span with the class ``highlight``.
-
-Default: ``true``
-
-
-.. _javascript-formatLabel:
-
-formatLabel
-______________________________________
-
-
-``formatLabel`` is a function that is run prior to rendering the search results in
-the dropdown menu. It takes two arguments: the current item label and the item data
-dictionary. It should return the label which should be used. For more information
-on its usage see :ref:`Label Formats on the Client Side <advanced-label-formats>`.
-
-Default: ``null``
-
+++ /dev/null
-Testing Forms and Lookups
-====================================
-
-django-selectable has its own test suite for testing the rendering, validation
-and server-side logic it provides. However, depending on the additional customizations
-you add to your forms and lookups you most likely will want to include tests of your
-own. This section contains some tips or techniques for testing your lookups.
-
-This guide assumes that you are reasonable familiar with the concepts of unit testing
-including Python's `unittest <http://docs.python.org/2/library/unittest.html>`_ module and
-Django's `testing guide <https://docs.djangoproject.com/en/stable/topics/testing/>`_.
-
-
-Testing Forms with django-selectable
---------------------------------------------------
-
-For the most part testing forms which use django-selectable's custom fields
-and widgets is the same as testing any Django form. One point that is slightly
-different is that the select and multi-select widgets are
-`MultiWidgets <https://docs.djangoproject.com/en/stable/ref/forms/widgets/#django.forms.MultiWidget>`_.
-The effect of this is that there are two names in the post rather than one. Take the below
-form for example.
-
- .. code-block:: python
-
- # models.py
-
- from django.db import models
-
- class Thing(models.Model):
- name = models.CharField(max_length=100)
- description = models.CharField(max_length=100)
-
- def __unicode__(self):
- return self.name
-
- .. code-block:: python
-
- # lookups.py
-
- from selectable.base import ModelLookup
- from selectable.registry import registry
-
- from .models import Thing
-
- class ThingLookup(ModelLookup):
- model = Thing
- search_fields = ('name__icontains', )
-
- registry.register(ThingLookup)
-
- .. code-block:: python
-
- # forms.py
-
- from django import forms
-
- from selectable.forms import AutoCompleteSelectField
-
- from .lookups import ThingLookup
-
- class SimpleForm(forms.Form):
- "Basic form for testing."
- thing = AutoCompleteSelectField(lookup_class=ThingLookup)
-
-This form has a single field to select a ``Thing``. It does not allow
-new items. Let's write some simple tests for this form.
-
- .. code-block:: python
-
- # tests.py
-
- from django.test import TestCase
-
- from .forms import SimpleForm
- from .models import Thing
-
- class SimpleFormTestCase(TestCase):
-
- def test_valid_form(self):
- "Submit valid data."
- thing = Thing.objects.create(name='Foo', description='Bar')
- data = {
- 'thing_0': thing.name,
- 'thing_1': thing.pk,
- }
- form = SimpleForm(data=data)
- self.assertTrue(form.is_valid())
-
- def test_invalid_form(self):
- "Thing is required but missing."
- data = {
- 'thing_0': 'Foo',
- 'thing_1': '',
- }
- form = SimpleForm(data=data)
- self.assertFalse(form.is_valid())
-
-Here you will note that while there is only one field ``thing`` it requires
-two items in the POST the first is for the text input and the second is for
-the hidden input. This is again due to the use of MultiWidget for the selection.
-
-There is compatibility code in the widgets to lookup the original name
-from the POST. This makes it easier to transition to the the selectable widgets without
-breaking existing tests.
-
-
-Testing Lookup Results
---------------------------------------------------
-
-Testing the lookups used by django-selectable is similar to testing your Django views.
-While it might be tempting to use the Django
-`test client <https://docs.djangoproject.com/en/stable/topics/testing/#module-django.test.client>`_,
-it is slightly easier to use the
-`request factory <https://docs.djangoproject.com/en/stable/topics/testing/#the-request-factory>`_.
-A simple example is given below.
-
- .. code-block:: python
-
- # tests.py
-
- import json
-
- from django.test import TestCase
- from django.test.client import RequestFactory
-
- from .lookups import ThingLookup
- from .models import Thing
-
- class ThingLookupTestCase(TestCase):
-
- def setUp(self):
- self.factory = RequestFactory()
- self.lookup = ThingLookup()
- self.test_thing = Thing.objects.create(name='Foo', description='Bar')
-
- def test_results(self):
- "Test full response."
- request = self.factory.get("/", {'term': 'Fo'})
- response = self.lookup.results(request)
- data = json.loads(response.content)['data']
- self.assertEqual(1, len(data))
- self.assertEqual(self.test_thing.pk, data[1]['id'])
-
- def test_label(self):
- "Test item label."
- label = self.lookup.get_item_label(self.test_thing)
- self.assertEqual(self.test_thing.name, label)
-
-As shown in the ``test_label`` example it is not required to test the full
-request/response. You can test each of the methods in the lookup API individually.
-When testing your lookups you should focus on testing the portions which have been
-customized by your application.
\ No newline at end of file
+++ /dev/null
-Widgets
-==========
-
-Below are the custom widgets defined by Django-Selectable. All widgets take the
-lookup class as the first required argument.
-
-These widgets all support a ``query_params`` keyword argument which is used to pass
-additional query parameters to the lookup search. See the section on
-:ref:`Adding Parameters on the Server Side <server-side-parameters>` for more
-information.
-
-You can configure the plugin options by passing the configuration dictionary in the ``data-selectable-options``
-attribute. The set of options availble include those define by the base
-`autocomplete plugin <http://api.jqueryui.com/1.9/autocomplete/>`_ as well as the
-:ref:`javascript-removeIcon`, :ref:`javascript-comboboxIcon`, and :ref:`javascript-highlightMatch` options
-which are unique to django-selectable.
-
- .. code-block:: python
-
- attrs = {'data-selectable-options': {'highlightMatch': True, 'minLength': 5}}
- selectable.AutoCompleteSelectWidget(lookup_class=FruitLookup, attrs=attrs)
-
-
-.. _AutoCompleteWidget:
-
-AutoCompleteWidget
---------------------------------------
-
-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:
-
-AutoComboboxWidget
---------------------------------------
-
-Similar to :ref:`AutoCompleteWidget` but has a button to reveal all options.
-
-
-.. _AutoCompleteSelectWidget:
-
-AutoCompleteSelectWidget
---------------------------------------
-
-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 :ref:`AutoCompleteSelectField` as it will
-return both the text entered by the user and the id (if an item was selected/matched).
-
-:ref:`AutoCompleteSelectWidget` works directly with Django's
-`ModelChoiceField <https://docs.djangoproject.com/en/stable/ref/forms/fields/#modelchoicefield>`_.
-You can simply replace the widget without replacing the entire field.
-
- .. code-block:: python
-
- class FarmAdminForm(forms.ModelForm):
-
- class Meta(object):
- model = Farm
- widgets = {
- 'owner': selectable.AutoCompleteSelectWidget(lookup_class=FruitLookup),
- }
-
-The one catch is that you must use ``allow_new=False`` which is the default.
-
-``lookup_class`` may also be a dotted path.
-
- .. code-block:: python
-
- widget = selectable.AutoCompleteWidget(lookup_class='core.lookups.FruitLookup')
-
-
-.. _AutoComboboxSelectWidget:
-
-AutoComboboxSelectWidget
---------------------------------------
-
-Similar to :ref:`AutoCompleteSelectWidget` but has a button to reveal all options.
-
-:ref:`AutoComboboxSelectWidget` works directly with Django's
-`ModelChoiceField <https://docs.djangoproject.com/en/stable/ref/forms/fields/#modelchoicefield>`_.
-You can simply replace the widget without replacing the entire field.
-
- .. code-block:: python
-
- class FarmAdminForm(forms.ModelForm):
-
- class Meta(object):
- model = Farm
- widgets = {
- 'owner': selectable.AutoComboboxSelectWidget(lookup_class=FruitLookup),
- }
-
-The one catch is that you must use ``allow_new=False`` which is the default.
-
-
-.. _AutoCompleteSelectMultipleWidget:
-
-AutoCompleteSelectMultipleWidget
---------------------------------------
-
-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
-:ref:`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:
-
-AutoComboboxSelectMultipleWidget
---------------------------------------
-
-Same as :ref:`AutoCompleteSelectMultipleWidget` but with a combobox.
+++ /dev/null
-#!/usr/bin/env python
-import os
-import sys
-
-from django.conf import settings
-
-
-if not settings.configured:
- settings.configure(
- DATABASES={
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': ':memory:',
- }
- },
- MIDDLEWARE=(),
- INSTALLED_APPS=(
- 'selectable',
- ),
- SECRET_KEY='super-secret',
- ROOT_URLCONF='selectable.tests.urls',
- TEMPLATES=[{
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [os.path.join(os.path.normpath(os.path.join(
- os.path.dirname(__file__), 'selectable')), 'templates')]}])
-
-
-from django import setup
-from django.test.utils import get_runner
-
-
-def runtests():
- setup()
- TestRunner = get_runner(settings)
- test_runner = TestRunner(verbosity=1, interactive=True, failfast=False)
- args = sys.argv[1:] or ['selectable', ]
- failures = test_runner.run_tests(args)
- sys.exit(failures)
-
-
-if __name__ == '__main__':
- runtests()
+++ /dev/null
-"Auto-complete selection widgets using Django and jQuery UI."
-
-
-__version__ = '1.2.1'
-
-default_app_config = 'selectable.apps.SelectableConfig'
+++ /dev/null
-from django.apps import AppConfig
-
-
-class SelectableConfig(AppConfig):
- """App configuration for django-selectable."""
-
- name = 'selectable'
-
- def ready(self):
- from . import registry
- registry.autodiscover()
+++ /dev/null
-"Base classes for lookup creation."
-from __future__ import unicode_literals
-
-import operator
-import re
-from functools import reduce
-
-from django.conf import settings
-from django.core.paginator import Paginator, InvalidPage, EmptyPage
-from django.http import JsonResponse
-from django.db.models import Q, Model
-from django.urls import reverse
-from django.utils.encoding import smart_text
-from django.utils.html import conditional_escape
-from django.utils.translation import ugettext as _
-
-from .forms import BaseLookupForm
-
-__all__ = (
- 'LookupBase',
- 'ModelLookup',
-)
-
-
-class LookupBase(object):
- "Base class for all django-selectable lookups."
-
- form = BaseLookupForm
- response = JsonResponse
-
- def _name(cls):
- app_name = cls.__module__.split('.')[-2].lower()
- class_name = cls.__name__.lower()
- name = '%s-%s' % (app_name, class_name)
- return name
- name = classmethod(_name)
-
- def split_term(self, term):
- """
- Split searching term into array of subterms
- that will be searched separately.
- """
- return term.split()
-
- def _url(cls):
- return reverse('selectable-lookup', args=[cls.name()])
- url = classmethod(_url)
-
- def get_query(self, request, term):
- return []
-
- def get_item_label(self, item):
- return smart_text(item)
-
- def get_item_id(self, item):
- return smart_text(item)
-
- def get_item_value(self, item):
- return smart_text(item)
-
- def get_item(self, value):
- return value
-
- def create_item(self, value):
- raise NotImplemented()
-
- def format_item(self, item):
- "Construct result dictionary for the match item."
- result = {
- 'id': self.get_item_id(item),
- 'value': self.get_item_value(item),
- 'label': self.get_item_label(item),
- }
- for key in settings.SELECTABLE_ESCAPED_KEYS:
- if key in result:
- result[key] = conditional_escape(result[key])
- return result
-
- def paginate_results(self, results, options):
- "Return a django.core.paginator.Page of results."
- limit = options.get('limit', settings.SELECTABLE_MAX_LIMIT)
- paginator = Paginator(results, limit)
- page = options.get('page', 1)
- try:
- results = paginator.page(page)
- except (EmptyPage, InvalidPage):
- results = paginator.page(paginator.num_pages)
- return results
-
- def results(self, request):
- "Match results to given term and return the serialized HttpResponse."
- results = {}
- form = self.form(request.GET)
- if form.is_valid():
- options = form.cleaned_data
- term = options.get('term', '')
- raw_data = self.get_query(request, term)
- results = self.format_results(raw_data, options)
- return self.response(results)
-
- def format_results(self, raw_data, options):
- '''
- Returns a python structure that later gets serialized.
- raw_data
- full list of objects matching the search term
- options
- a dictionary of the given options
- '''
- page_data = self.paginate_results(raw_data, options)
- results = {}
- meta = options.copy()
- meta['more'] = _('Show more results')
- if page_data and page_data.has_next():
- meta['next_page'] = page_data.next_page_number()
- if page_data and page_data.has_previous():
- meta['prev_page'] = page_data.previous_page_number()
- results['data'] = [self.format_item(item) for item in page_data.object_list]
- results['meta'] = meta
- return results
-
-
-class ModelLookup(LookupBase):
- "Lookup class for easily defining lookups based on Django models."
-
- model = None
- filters = {}
- search_fields = ()
-
- def get_query(self, request, term):
- qs = self.get_queryset()
- if term:
- if self.search_fields:
- for t in self.split_term(term):
- search_filters = []
- for field in self.search_fields:
- search_filters.append(Q(**{field: t}))
- qs = qs.filter(reduce(operator.or_, search_filters))
- return qs
-
- def get_queryset(self):
- qs = self.model._default_manager.get_queryset()
- if self.filters:
- qs = qs.filter(**self.filters)
- return qs
-
- def get_item_id(self, item):
- return item.pk
-
- def get_item(self, value):
- item = None
- if value:
- value = value.pk if isinstance(value, Model) else value
- try:
- item = self.get_queryset().get(pk=value)
- except (ValueError, self.model.DoesNotExist):
- item = None
- return item
-
- def create_item(self, value):
- data = {}
- if self.search_fields:
- field_name = re.sub(r'__\w+$', '', self.search_fields[0])
- if field_name:
- data = {field_name: value}
- return self.model(**data)
+++ /dev/null
-"Compatibility utilites for Python versions."
-
-try:
- from urllib.parse import urlparse
-except ImportError:
- # This can be removed when Python 2.7 support is dropped
- from urlparse import urlparse
+++ /dev/null
-"Decorators for additional lookup functionality."
-from __future__ import unicode_literals
-
-from functools import wraps
-
-from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
-
-
-__all__ = (
- 'ajax_required',
- 'login_required',
- 'staff_member_required',
-)
-
-
-def results_decorator(func):
- """
- Helper for constructing simple decorators around Lookup.results.
-
- func is a function which takes a request as the first parameter. If func
- returns an HttpReponse it is returned otherwise the original Lookup.results
- is returned.
- """
- # Wrap function to maintian the original doc string, etc
- @wraps(func)
- def decorator(lookup_cls):
- # Construct a class decorator from the original function
- original = lookup_cls.results
- def inner(self, request):
- # Wrap lookup_cls.results by first calling func and checking the result
- result = func(request)
- if isinstance(result, HttpResponse):
- return result
- return original(self, request)
- # Replace original lookup_cls.results with wrapped version
- lookup_cls.results = inner
- return lookup_cls
- # Return the constructed decorator
- return decorator
-
-
-@results_decorator
-def ajax_required(request):
- "Lookup decorator to require AJAX calls to the lookup view."
- if not request.is_ajax():
- return HttpResponseBadRequest()
-
-
-@results_decorator
-def login_required(request):
- "Lookup decorator to require the user to be authenticated."
- user = getattr(request, 'user', None)
- if user is None or not user.is_authenticated:
- return HttpResponse(status=401) # Unauthorized
-
-
-@results_decorator
-def staff_member_required(request):
- "Lookup decorator to require the user is a staff member."
- user = getattr(request, 'user', None)
- if user is None or not user.is_authenticated:
- return HttpResponse(status=401) # Unauthorized
- elif not user.is_staff:
- return HttpResponseForbidden()
+++ /dev/null
-class LookupAlreadyRegistered(Exception):
- "Exception when trying to register a lookup which is already registered."
-
-
-class LookupNotRegistered(Exception):
- "Exception when trying use a lookup which is not registered."
-
-
-class LookupInvalid(Exception):
- "Exception when register an invalid lookup class."
+++ /dev/null
-from selectable.forms.base import *
-from selectable.forms.fields import *
-from selectable.forms.widgets import *
+++ /dev/null
-from __future__ import unicode_literals
-
-from importlib import import_module
-
-from django import forms
-from django.conf import settings
-from django.utils.six import string_types
-
-
-__all__ = (
- 'BaseLookupForm',
- 'import_lookup_class',
-)
-
-
-class BaseLookupForm(forms.Form):
- term = forms.CharField(required=False)
- limit = forms.IntegerField(required=False, min_value=1)
- page = forms.IntegerField(required=False, min_value=1)
-
- def clean_limit(self):
- "Ensure given limit is less than default if defined"
- limit = self.cleaned_data.get('limit', None)
- if (settings.SELECTABLE_MAX_LIMIT is not None and
- (not limit or limit > settings.SELECTABLE_MAX_LIMIT)):
- limit = settings.SELECTABLE_MAX_LIMIT
- return limit
-
- def clean_page(self):
- "Return the first page if no page or invalid number is given."
- return self.cleaned_data.get('page', 1) or 1
-
-
-def import_lookup_class(lookup_class):
- """
- Import lookup_class as a dotted base and ensure it extends LookupBase
- """
- from selectable.base import LookupBase
- if isinstance(lookup_class, string_types):
- mod_str, cls_str = lookup_class.rsplit('.', 1)
- mod = import_module(mod_str)
- lookup_class = getattr(mod, cls_str)
- if not issubclass(lookup_class, LookupBase):
- raise TypeError('lookup_class must extend from selectable.base.LookupBase')
- return lookup_class
+++ /dev/null
-from __future__ import unicode_literals
-
-from django import forms
-from django.core.exceptions import ValidationError
-from django.core.validators import EMPTY_VALUES
-from django.utils.translation import ugettext_lazy as _
-from django.db.models import Model
-
-from selectable.forms.base import import_lookup_class
-from selectable.forms.widgets import AutoCompleteSelectWidget
-from selectable.forms.widgets import AutoCompleteSelectMultipleWidget
-
-__all__ = (
- 'AutoCompleteSelectField',
- 'AutoCompleteSelectMultipleField',
-)
-
-
-def model_vars(obj):
- fields = dict(
- (field.name, getattr(obj, field.name))
- for field in obj._meta.fields
- )
- return fields
-
-
-class BaseAutoCompleteField(forms.Field):
-
- def has_changed(self, initial, data):
- "Detects if the data was changed. This is added in 1.6."
- if initial is None and data is None:
- return False
- if data and not hasattr(data, '__iter__'):
- data = self.widget.decompress(data)
- initial = self.to_python(initial)
- data = self.to_python(data)
- if hasattr(self, '_coerce'):
- data = self._coerce(data)
- if isinstance(data, Model) and isinstance(initial, Model):
- return model_vars(data) != model_vars(initial)
- else:
- return data != initial
-
-
-class AutoCompleteSelectField(BaseAutoCompleteField):
- widget = AutoCompleteSelectWidget
-
- default_error_messages = {
- 'invalid_choice': _('Select a valid choice. That choice is not one of the available choices.'),
- }
-
- def __init__(self, lookup_class, *args, **kwargs):
- self.lookup_class = import_lookup_class(lookup_class)
- self.allow_new = kwargs.pop('allow_new', False)
- self.limit = kwargs.pop('limit', None)
- widget = kwargs.get('widget', self.widget) or self.widget
- if isinstance(widget, type):
- kwargs['widget'] = widget(lookup_class, allow_new=self.allow_new, limit=self.limit)
- super(AutoCompleteSelectField, self).__init__(*args, **kwargs)
-
- def to_python(self, value):
- if value in EMPTY_VALUES:
- return None
- lookup = self.lookup_class()
- if isinstance(value, list):
- # Input comes from an AutoComplete widget. It's two
- # components: text and id
- if len(value) != 2:
- raise ValidationError(self.error_messages['invalid_choice'])
- label, pk = value
- if pk in EMPTY_VALUES:
- if not self.allow_new:
- if label:
- raise ValidationError(self.error_messages['invalid_choice'])
- else:
- return None
- if label in EMPTY_VALUES:
- return None
- value = lookup.create_item(label)
- else:
- value = lookup.get_item(pk)
- if value is None:
- raise ValidationError(self.error_messages['invalid_choice'])
- else:
- value = lookup.get_item(value)
- if value is None:
- raise ValidationError(self.error_messages['invalid_choice'])
- return value
-
-
-class AutoCompleteSelectMultipleField(BaseAutoCompleteField):
- widget = AutoCompleteSelectMultipleWidget
-
- default_error_messages = {
- 'invalid_choice': _('Select a valid choice. That choice is not one of the available choices.'),
- }
-
- def __init__(self, lookup_class, *args, **kwargs):
- self.lookup_class = import_lookup_class(lookup_class)
- self.limit = kwargs.pop('limit', None)
- widget = kwargs.get('widget', self.widget) or self.widget
- if isinstance(widget, type):
- kwargs['widget'] = widget(lookup_class, limit=self.limit)
- super(AutoCompleteSelectMultipleField, self).__init__(*args, **kwargs)
-
- def to_python(self, value):
- if value in EMPTY_VALUES:
- return []
- lookup = self.lookup_class()
- items = []
- for v in value:
- if v not in EMPTY_VALUES:
- item = lookup.get_item(v)
- if item is None:
- raise ValidationError(self.error_messages['invalid_choice'])
- items.append(item)
- return items
+++ /dev/null
-from __future__ import unicode_literals
-
-import json
-
-from django import forms
-from django.conf import settings
-from django.utils.encoding import force_text
-from django.utils.http import urlencode
-
-from selectable import __version__
-from selectable.forms.base import import_lookup_class
-
-__all__ = (
- 'AutoCompleteWidget',
- 'AutoCompleteSelectWidget',
- 'AutoComboboxWidget',
- 'AutoComboboxSelectWidget',
- 'AutoCompleteSelectMultipleWidget',
- 'AutoComboboxSelectMultipleWidget',
-)
-
-
-STATIC_PREFIX = '%sselectable/' % settings.STATIC_URL
-
-
-class SelectableMediaMixin(object):
-
- class Media(object):
- css = {
- 'all': ('%scss/dj.selectable.css?v=%s' % (STATIC_PREFIX, __version__),)
- }
- js = ('%sjs/jquery.dj.selectable.js?v=%s' % (STATIC_PREFIX, __version__),)
-
-
-class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin):
-
- def __init__(self, lookup_class, *args, **kwargs):
- self.lookup_class = import_lookup_class(lookup_class)
- self.allow_new = kwargs.pop('allow_new', False)
- self.qs = kwargs.pop('query_params', {})
- self.limit = kwargs.pop('limit', None)
- super(AutoCompleteWidget, self).__init__(*args, **kwargs)
-
- def update_query_parameters(self, qs_dict):
- self.qs.update(qs_dict)
-
- def build_attrs(self, base_attrs, extra_attrs=None):
- attrs = super(AutoCompleteWidget, self).build_attrs(base_attrs, extra_attrs)
- url = self.lookup_class.url()
- if self.limit and 'limit' not in self.qs:
- self.qs['limit'] = self.limit
- if self.qs:
- url = '%s?%s' % (url, urlencode(self.qs))
- if 'data-selectable-options' in attrs:
- attrs['data-selectable-options'] = json.dumps(attrs['data-selectable-options'])
- attrs['data-selectable-url'] = url
- attrs['data-selectable-type'] = 'text'
- attrs['data-selectable-allow-new'] = str(self.allow_new).lower()
- return attrs
-
-
-class SelectableMultiWidget(forms.MultiWidget):
-
- def update_query_parameters(self, qs_dict):
- self.widgets[0].update_query_parameters(qs_dict)
-
- def decompress(self, value):
- if value:
- lookup = self.lookup_class()
- model = getattr(self.lookup_class, 'model', None)
- if model and isinstance(value, model):
- item = value
- value = lookup.get_item_id(item)
- else:
- item = lookup.get_item(value)
- item_value = lookup.get_item_value(item)
- return [item_value, value]
- return [None, None]
-
- def get_compatible_postdata(self, data, name):
- """Get postdata built for a normal <select> element.
-
- Django MultiWidgets create post variables like ``foo_0`` and ``foo_1``,
- and this behavior is not cleanly overridable. Non-multiwidgets, like
- Select, get simple names like ``foo``. In order to keep this widget
- compatible with requests designed for traditional select widgets,
- search postdata for a name like ``foo`` and return that value.
-
- This will return ``None`` if a ``<select>``-compatibile post variable
- is not found.
- """
- return data.get(name, None)
-
-
-class _BaseSingleSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
- """
- Common base class for AutoCompleteSelectWidget and AutoComboboxSelectWidget
- each which use one widget (primary_widget) to select text and a single
- hidden input to hold the selected id.
- """
-
- primary_widget = None
-
- def __init__(self, lookup_class, *args, **kwargs):
- self.lookup_class = import_lookup_class(lookup_class)
- self.allow_new = kwargs.pop('allow_new', False)
- self.limit = kwargs.pop('limit', None)
- query_params = kwargs.pop('query_params', {})
- widgets = [
- self.primary_widget(
- lookup_class, allow_new=self.allow_new,
- limit=self.limit, query_params=query_params,
- attrs=kwargs.get('attrs'),
- ),
- forms.HiddenInput(attrs={'data-selectable-type': 'hidden'})
- ]
- super(_BaseSingleSelectWidget, self).__init__(widgets, *args, **kwargs)
-
- def value_from_datadict(self, data, files, name):
- value = super(_BaseSingleSelectWidget, self).value_from_datadict(data, files, name)
- if not value[1]:
- compatible_postdata = self.get_compatible_postdata(data, name)
- if compatible_postdata:
- value[1] = compatible_postdata
- if not self.allow_new:
- return value[1]
- return value
-
-
-class AutoCompleteSelectWidget(_BaseSingleSelectWidget):
-
- primary_widget = AutoCompleteWidget
-
-
-class AutoComboboxWidget(AutoCompleteWidget, SelectableMediaMixin):
-
- def build_attrs(self, base_attrs, extra_attrs=None):
- attrs = super(AutoComboboxWidget, self).build_attrs(base_attrs, extra_attrs)
- attrs['data-selectable-type'] = 'combobox'
- return attrs
-
-
-class AutoComboboxSelectWidget(_BaseSingleSelectWidget):
-
- primary_widget = AutoComboboxWidget
-
-
-class LookupMultipleHiddenInput(forms.MultipleHiddenInput):
-
- def __init__(self, lookup_class, *args, **kwargs):
- self.lookup_class = import_lookup_class(lookup_class)
- super(LookupMultipleHiddenInput, self).__init__(*args, **kwargs)
-
- def get_context(self, name, value, attrs):
- lookup = self.lookup_class()
- values = self._normalize_value(value)
- values = list(values) # force evaluation
-
- context = super(LookupMultipleHiddenInput, self).get_context(name, values, attrs)
-
- # Now override/add to what super() did:
- subwidgets = context['widget']['subwidgets']
- for widget_ctx, item in zip(subwidgets, values):
- input_value, title = self._lookup_value_and_title(lookup, item)
- widget_ctx['value'] = input_value # override what super() did
- if title:
- widget_ctx['attrs']['title'] = title
- return context
-
- def build_attrs(self, base_attrs, extra_attrs=None):
- attrs = super(LookupMultipleHiddenInput, self).build_attrs(base_attrs, extra_attrs)
- attrs['data-selectable-type'] = 'hidden-multiple'
- return attrs
-
- def _normalize_value(self, value):
- if value is None:
- value = []
- return value
-
- def _lookup_value_and_title(self, lookup, v):
- model = getattr(self.lookup_class, 'model', None)
- item = None
- if model and isinstance(v, model):
- item = v
- v = lookup.get_item_id(item)
- title = None
- if v:
- item = item or lookup.get_item(v)
- title = lookup.get_item_value(item)
- return force_text(v), title
-
-
-class _BaseMultipleSelectWidget(SelectableMultiWidget, SelectableMediaMixin):
- """
- Common base class for AutoCompleteSelectMultipleWidget and AutoComboboxSelectMultipleWidget
- each which use one widget (primary_widget) to select text and a multiple
- hidden inputs to hold the selected ids.
- """
-
- primary_widget = None
-
- def __init__(self, lookup_class, *args, **kwargs):
- self.lookup_class = import_lookup_class(lookup_class)
- self.limit = kwargs.pop('limit', None)
- position = kwargs.pop('position', 'bottom')
- attrs = {
- 'data-selectable-multiple': 'true',
- 'data-selectable-position': position
- }
- attrs.update(kwargs.get('attrs', {}))
- query_params = kwargs.pop('query_params', {})
- widgets = [
- self.primary_widget(
- lookup_class, allow_new=False,
- limit=self.limit, query_params=query_params, attrs=attrs
- ),
- LookupMultipleHiddenInput(lookup_class)
- ]
- super(_BaseMultipleSelectWidget, self).__init__(widgets, *args, **kwargs)
-
- def value_from_datadict(self, data, files, name):
- value = self.widgets[1].value_from_datadict(data, files, name + '_1')
- if not value:
- # Fall back to the compatible POST name
- value = self.get_compatible_postdata(data, name)
- return value
-
- def build_attrs(self, base_attrs, extra_attrs=None):
- attrs = super(_BaseMultipleSelectWidget, self).build_attrs(base_attrs, extra_attrs)
- if 'required' in attrs:
- attrs.pop('required')
- return attrs
-
- def render(self, name, value, attrs=None, renderer=None):
- if value and not hasattr(value, '__iter__'):
- value = [value]
- value = ['', value]
- return super(_BaseMultipleSelectWidget, self).render(name, value, attrs, renderer)
-
-
-class AutoCompleteSelectMultipleWidget(_BaseMultipleSelectWidget):
-
- primary_widget = AutoCompleteWidget
-
-
-class AutoComboboxSelectMultipleWidget(_BaseMultipleSelectWidget):
-
- primary_widget = AutoComboboxWidget
+++ /dev/null
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
-#
-#, fuzzy
-msgid ""
-msgstr ""
-"Project-Id-Version: PACKAGE VERSION\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2014-10-21 20:14-0400\n"
-"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
-"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
-"Language-Team: LANGUAGE <LL@li.org>\n"
-"Language: \n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-
-#: base.py:117
-msgid "Show more results"
-msgstr ""
-
-#: forms/fields.py:48 forms/fields.py:96
-msgid "Select a valid choice. That choice is not one of the available choices."
-msgstr ""
+++ /dev/null
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-#
-# Translators:
-msgid ""
-msgstr ""
-"Project-Id-Version: django-selectable\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2012-10-06 15:02-0400\n"
-"PO-Revision-Date: 2013-11-20 10:18+0000\n"
-"Last-Translator: Manuel Alvarez <escriva@gmail.com>\n"
-"Language-Team: Spanish (http://www.transifex.com/projects/p/django-selectable/language/es/)\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Language: es\n"
-"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-
-#: base.py:115
-msgid "Show more results"
-msgstr "Mostrar más resultados"
-
-#: forms/fields.py:19 forms/fields.py:63
-msgid ""
-"Select a valid choice. That choice is not one of the available choices."
-msgstr "Seleccione una opción válida. La opción seleccionada no está disponible."
+++ /dev/null
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-#
-# Translators:
-# Mark Lavin <markdlavin@gmail.com>, 2014
-msgid ""
-msgstr ""
-"Project-Id-Version: django-selectable\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2012-10-06 15:02-0400\n"
-"PO-Revision-Date: 2014-01-21 01:00+0000\n"
-"Last-Translator: Mark Lavin <markdlavin@gmail.com>\n"
-"Language-Team: French (http://www.transifex.com/projects/p/django-selectable/language/fr/)\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Language: fr\n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-
-#: base.py:115
-msgid "Show more results"
-msgstr "Afficher plus de résultats"
-
-#: forms/fields.py:19 forms/fields.py:63
-msgid ""
-"Select a valid choice. That choice is not one of the available choices."
-msgstr "Sélectionnez un choix valide. Ce choix ne fait pas partie de ceux disponibles."
+++ /dev/null
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-#
-# Translators:
-# slafs <slafs.e@gmail.com>, 2012
-msgid ""
-msgstr ""
-"Project-Id-Version: django-selectable\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2012-10-06 15:02-0400\n"
-"PO-Revision-Date: 2013-11-20 10:18+0000\n"
-"Last-Translator: slafs <slafs.e@gmail.com>\n"
-"Language-Team: Polish (http://www.transifex.com/projects/p/django-selectable/language/pl/)\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Language: pl\n"
-"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
-
-#: base.py:115
-msgid "Show more results"
-msgstr "Pokaż więcej wyników"
-
-#: forms/fields.py:19 forms/fields.py:63
-msgid ""
-"Select a valid choice. That choice is not one of the available choices."
-msgstr "Dokonaj poprawnego wyboru. Ten wybór nie jest jednym z dostępnych."
+++ /dev/null
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-#
-# Translators:
-msgid ""
-msgstr ""
-"Project-Id-Version: django-selectable\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2012-10-06 15:02-0400\n"
-"PO-Revision-Date: 2013-11-20 10:18+0000\n"
-"Last-Translator: Mark Lavin <markdlavin@gmail.com>\n"
-"Language-Team: Portuguese (Brazil) (http://www.transifex.com/projects/p/django-selectable/language/pt_BR/)\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Language: pt_BR\n"
-"Plural-Forms: nplurals=2; plural=(n > 1);\n"
-
-#: base.py:115
-msgid "Show more results"
-msgstr "Mostrar mais resultados"
-
-#: forms/fields.py:19 forms/fields.py:63
-msgid ""
-"Select a valid choice. That choice is not one of the available choices."
-msgstr "Selecione uma escolha valida. Esta escolha não é uma das disponíveis."
+++ /dev/null
-# SOME DESCRIPTIVE TITLE.
-# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
-# This file is distributed under the same license as the PACKAGE package.
-#
-# Translators:
-# mozillazg <opensource.mozillazg@gmail.com>, 2013
-msgid ""
-msgstr ""
-"Project-Id-Version: django-selectable\n"
-"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2012-10-06 15:02-0400\n"
-"PO-Revision-Date: 2013-11-21 05:08+0000\n"
-"Last-Translator: mozillazg <opensource.mozillazg@gmail.com>\n"
-"Language-Team: Chinese (China) (http://www.transifex.com/projects/p/django-selectable/language/zh_CN/)\n"
-"MIME-Version: 1.0\n"
-"Content-Type: text/plain; charset=UTF-8\n"
-"Content-Transfer-Encoding: 8bit\n"
-"Language: zh_CN\n"
-"Plural-Forms: nplurals=1; plural=0;\n"
-
-#: base.py:115
-msgid "Show more results"
-msgstr "显示更多结果"
-
-#: forms/fields.py:19 forms/fields.py:63
-msgid ""
-"Select a valid choice. That choice is not one of the available choices."
-msgstr "请选择一个有效的选项。当前选项无效。"
+++ /dev/null
-from __future__ import unicode_literals
-
-from django.conf import settings
-
-# Set default settings
-if not hasattr(settings, 'SELECTABLE_MAX_LIMIT'):
- settings.SELECTABLE_MAX_LIMIT = 25
-
-if not hasattr(settings, 'SELECTABLE_ESCAPED_KEYS'):
- settings.SELECTABLE_ESCAPED_KEYS = ('label', )
+++ /dev/null
-from __future__ import unicode_literals
-
-from django.utils.encoding import force_text
-from django.utils.module_loading import autodiscover_modules
-
-from selectable.base import LookupBase
-from selectable.exceptions import (LookupAlreadyRegistered, LookupNotRegistered,
- LookupInvalid)
-
-
-class LookupRegistry(object):
-
- def __init__(self):
- self._registry = {}
-
- def validate(self, lookup):
- if not issubclass(lookup, LookupBase):
- raise LookupInvalid('Registered lookups must inherit from the LookupBase class')
-
- def register(self, lookup):
- self.validate(lookup)
- name = force_text(lookup.name())
- if name in self._registry:
- raise LookupAlreadyRegistered('The name %s is already registered' % name)
- self._registry[name] = lookup
-
- def unregister(self, lookup):
- self.validate(lookup)
- name = force_text(lookup.name())
- if name not in self._registry:
- raise LookupNotRegistered('The name %s is not registered' % name)
- del self._registry[name]
-
- def get(self, key):
- return self._registry.get(key, None)
-
-
-registry = LookupRegistry()
-
-
-def autodiscover():
- # Attempt to import the app's lookups module.
- autodiscover_modules('lookups', register_to=registry)
+++ /dev/null
-/*
- * django-selectable UI widget CSS
- * Source: https://github.com/mlavin/django-selectable
- * Docs: http://django-selectable.readthedocs.org/
- *
- * Copyright 2010-2014, Mark Lavin
- * BSD License
- *
-*/
-ul.selectable-deck, ul.ui-autocomplete {
- list-style: none outside none;
-}
-ul.selectable-deck li.selectable-deck-item,
-ul.ui-autocomplete li.ui-menu-item {
- margin: 0;
- 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;
-}
-ul.ui-autocomplete li.ui-menu-item span.highlight {
- font-weight: bold;
-}
-input.ui-combo-input {
- margin-right: 0;
- line-height: 1.3;
-}
-a.ui-combo-button {
- margin-left: -1px;
-}
-a.ui-combo-button .ui-button-text {
- padding: 0;
-}
+++ /dev/null
-/*jshint trailing:true, indent:4*/
-/*
- * django-selectable UI widget
- * Source: https://github.com/mlavin/django-selectable
- * Docs: http://django-selectable.readthedocs.org/
- *
- * Depends:
- * - jQuery 1.7+
- * - jQuery UI 1.8 widget factory
- *
- * Copyright 2010-2014, Mark Lavin
- * BSD License
- *
-*/
-(function ($) {
-
- $.widget("ui.djselectable", $.ui.autocomplete, {
-
- options: {
- removeIcon: "ui-icon-close",
- comboboxIcon: "ui-icon-triangle-1-s",
- defaultClasses: {
- "text": "ui-widget ui-widget-content ui-corner-all",
- "combobox": "ui-widget ui-widget-content ui-corner-left ui-combo-input"
- },
- prepareQuery: null,
- highlightMatch: true,
- formatLabel: null
- },
-
- _initDeck: function () {
- /* Create list display for currently selected items for multi-select */
- var self = this;
- var data = $(this.element).data();
- var style = data.selectablePosition || 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);
- }
- $(self.hiddenMultipleSelector).each(function (i, input) {
- self._addDeckItem(input);
- });
- },
-
- _addDeckItem: function (input) {
- /* Add new deck list item from a given hidden input */
- var self = this,
- li = $('<li>').addClass('selectable-deck-item'),
- item = {element: self.element, input: input, wrapper: li, deck: self.deck},
- button;
- li.html($(input).attr('title'));
- if (self._trigger("add", null, item) === false) {
- input.remove();
- } else {
- button = this._removeButtonTemplate(item);
- button.click(function (e) {
- e.preventDefault();
- if (self._trigger("remove", e, item) !== false) {
- $(input).remove();
- li.remove();
- }
- });
- li.append(button).appendTo(this.deck);
- }
- },
-
- _removeButtonTemplate: function (item) {
- var options = {
- icons: {
- primary: this.options.removeIcon
- },
- text: false,
- disabled: this.disabled
- },
- button = $('<a>')
- .attr('href', '#')
- .addClass('selectable-deck-remove')
- .button(options);
- return button;
- },
-
- select: function (item, event) {
- /* Trigger selection of a given item.
- Item should contain two properties: id and value
- Event is the original select event if there is one.
- Event should not be passed if triggered manually.
- */
- var $input = $(this.element);
- $input.removeClass('ui-state-error');
- this._setHidden(item);
- if (item) {
- if (this.allowMultiple) {
- $input.val("");
- this.term = "";
- if ($(this.hiddenMultipleSelector + '[value="' + item.id + '"]').length === 0) {
- var newInput = $('<input />', {
- 'type': 'hidden',
- 'name': this.hiddenName,
- 'value': item.id,
- 'title': item.value,
- 'data-selectable-type': 'hidden-multiple'
- });
- $input.after(newInput);
- this._addDeckItem(newInput);
- }
- return false;
- } else {
- $input.val(item.value);
- var ui = {item: item};
- if (typeof(event) === 'undefined' || event.type !== "djselectableselect") {
- this.element.trigger("djselectableselect", [ui ]);
- }
- }
- }
- },
-
- _setHidden: function (item) {
- /* Set or clear single hidden input */
- var $elem = $(this.hiddenSelector);
- if (item && item.id) {
- $elem.val(item.id);
- } else {
- $elem.val("");
- }
- },
-
- _comboButtonTemplate: function (input) {
- // Add show all items button
- var options = {
- icons: {
- primary: this.options.comboboxIcon
- },
- text: false,
- disabled: this.disabled
- },
- button = $("<a>")
- .html(" ")
- .attr("tabIndex", -1)
- .attr("title", "Show All Items")
- .button(options)
- .removeClass("ui-corner-all")
- .addClass("ui-corner-right ui-button-icon ui-combo-button");
- return button;
- },
-
- _create: function () {
- /* Initialize a new selectable widget */
- var self = this,
- $input = $(this.element),
- data = $input.data(),
- options, button;
- this.url = data.selectableUrl || data['selectable-url'];
- this.allowNew = data.selectableAllowNew || data['selectable-allow-new'];
- this.allowMultiple = data.selectableMultiple || data['selectable-multiple'];
- this.textName = $input.attr('name');
- this.hiddenName = this.textName.replace(new RegExp('_0$'), '_1');
- this.hiddenSelector = ':input[data-selectable-type=hidden][name=' + this.hiddenName + ']';
- this.hiddenMultipleSelector = ':input[data-selectable-type=hidden-multiple][name=' + this.hiddenName + ']';
- this.selectableType = data.selectableType || data['selectable-type'];
- this.disabled = $input.prop('disabled');
- if (this.allowMultiple) {
- this.allowNew = false;
- $input.val("");
- this._initDeck();
- }
- options = data.selectableOptions || data['selectable-options'];
- if (options) {
- this._setOptions(options);
- }
- // Call super-create
- // This could be replaced by this._super() with jQuery UI 1.9
- $.ui.autocomplete.prototype._create.call(this);
- $input.addClass(this.options.defaultClasses[this.selectableType]);
- // Additional work for combobox widgets
- if (this.selectableType === 'combobox') {
- // Add show all items button
- button = this._comboButtonTemplate($input);
- button.insertAfter($input).click(function (e) {
- e.preventDefault();
- // close if already visible
- if (self.widget().is(":visible")) {
- self.close();
- }
- // pass empty string as value to search for, displaying all results
- self._search("");
- $input.focus();
- });
- }
- },
-
- // Override the default source creation
- _initSource: function () {
- var self = this,
- $input = $(this.element);
- this.source = function dataSource(request, response) {
- /* Custom data source to uses the lookup url with pagination
- Adds hook for adjusting query parameters.
- Includes timestamp to prevent browser caching the lookup. */
- var now = new Date().getTime(),
- query = {term: request.term, timestamp: now},
- page = $input.data("page");
- if (self.options.prepareQuery) {
- self.options.prepareQuery.apply(self, [query]);
- }
- if (page) {
- query.page = page;
- }
- function unwrapResponse(data) {
- var results = data.data,
- meta = data.meta;
- if (meta.next_page && meta.more) {
- results.push({
- id: '',
- value: '',
- label: meta.more,
- page: meta.next_page,
- term: request.term
- });
- }
- return response(results);
- }
- $.getJSON(self.url, query, unwrapResponse);
- };
- },
- // Override the default auto-complete render.
- _renderItem: function (ul, item) {
- /* Adds hook for additional formatting, allows HTML in the label,
- highlights term matches and handles pagination. */
- var label = item.label,
- self = this,
- $input = $(this.element),
- re, html, li;
- if (this.options.formatLabel && !item.page) {
- label = this.options.formatLabel.apply(this, [label, item]);
- }
- if (this.options.highlightMatch && this.term && !item.page) {
- re = new RegExp("(?![^&;]+;)(?!<[^<>]*)(" +
- $.ui.autocomplete.escapeRegex(this.term) +
- ")(?![^<>]*>)(?![^&;]+;)", "gi");
- if (label.html) {
- html = label.html();
- html = html.replace(re, "<span class='highlight'>$1</span>");
- label.html(html);
- } else {
- label = label.replace(re, "<span class='highlight'>$1</span>");
- }
- }
- li = $("<li></li>")
- .data("item.autocomplete", item)
- .append($("<a></a>").append(label))
- .appendTo(ul);
- if (item.page) {
- li.addClass('selectable-paginator');
- }
- return li;
- },
- // Override the default auto-complete suggest.
- _suggest: function (items) {
- /* Needed for handling pagination links */
- var $input = $(this.element),
- page = $input.data('page'),
- ul = this.menu.element;
- if (page) {
- $('.selectable-paginator', ul).remove();
- } else {
- ul.empty();
- }
- $input.data('page', null);
- ul.css("zIndex", $input.css("zIndex") + 1);
- this._renderMenu(ul, items);
- // jQuery UI menu does not define deactivate
- if (this.menu.deactivate) {
- this.menu.deactivate();
- }
- this.menu.refresh();
- // size and position menu
- ul.show();
- this._resizeMenu();
- ul.position($.extend({of: this.element}, this.options.position));
- if (this.options.autoFocus) {
- this.menu.next(new $.Event("mouseover"));
- } else if (page) {
- $input.focus();
- }
- },
- // Override default trigger for additional change/select logic
- _trigger: function (type, event, data) {
- var $input = $(this.element),
- self = this;
- if (type === "select") {
- $input.removeClass('ui-state-error');
- if (data.item.page) {
- $input.data("page", data.item.page);
- this._search(data.item.term);
- return false;
- }
- return this.select(data.item, event);
- } else if (type === "change") {
- $input.removeClass('ui-state-error');
- this._setHidden(data.item);
- if ($input.val() && !data.item) {
- if (!this.allowNew) {
- $input.addClass('ui-state-error');
- }
- }
- if (this.allowMultiple && !$input.hasClass('ui-state-error')) {
- $input.val("");
- this.term = "";
- }
- }
- // Call super trigger
- // This could be replaced by this._super() with jQuery UI 1.9
- return $.ui.autocomplete.prototype._trigger.apply(this, arguments);
- },
- close: function (event) {
- var page = $(this.element).data('page');
- if (page !== null) {
- return;
- }
- // Call super trigger
- // This could be replaced by this._super() with jQuery UI 1.9
- return $.ui.autocomplete.prototype.close.apply(this, arguments);
- }
- });
-
- window.bindSelectables = function (context) {
- /* Bind all selectable widgets in a given context.
- Automatically called on document.ready.
- Additional calls can be made for dynamically added widgets.
- */
- $(":input[data-selectable-type=text]", context).djselectable();
- $(":input[data-selectable-type=combobox]", context).djselectable();
- };
-
- function djangoAdminPatches() {
- /* Listen for new rows being added to the dynamic inlines.
- Requires Django 1.5+ */
- $('body').on('click', '.add-row', function (e) {
- var wrapper = $(this).parents('.inline-related'),
- newRow = $('.form-row:not(.empty-form)', wrapper).last();
- window.bindSelectables(newRow);
- });
-
- /* Monkey-patch Django's dismissAddAnotherPopup(), if defined */
- if (typeof(dismissAddAnotherPopup) !== "undefined" &&
- typeof(windowname_to_id) !== "undefined" &&
- typeof(html_unescape) !== "undefined") {
- var django_dismissAddAnotherPopup = dismissAddAnotherPopup;
- dismissAddAnotherPopup = function (win, newId, newRepr) {
- /* See if the popup came from a selectable field.
- If not, pass control to Django's code.
- If so, handle it. */
- var fieldName = windowname_to_id(win.name); /* e.g. "id_fieldname" */
- var field = $('#' + fieldName);
- var multiField = $('#' + fieldName + '_0');
- /* Check for bound selectable */
- var singleWidget = field.data('djselectable');
- var multiWidget = multiField.data('djselectable');
- if (singleWidget || multiWidget) {
- // newId and newRepr are expected to have previously been escaped by
- // django.utils.html.escape.
- var item = {
- id: html_unescape(newId),
- value: html_unescape(newRepr)
- };
- if (singleWidget) {
- field.djselectable('select', item);
- }
- if (multiWidget) {
- multiField.djselectable('select', item);
- }
- win.close();
- } else {
- /* Not ours, pass on to original function. */
- return django_dismissAddAnotherPopup(win, newId, newRepr);
- }
- };
- }
- }
-
- $(document).ready(function () {
- // Patch the django admin JS
- if (typeof(djselectableAdminPatch) === "undefined" || djselectableAdminPatch) {
- djangoAdminPatches();
- }
- // Bind existing widgets on document ready
- if (typeof(djselectableAutoLoad) === "undefined" || djselectableAutoLoad) {
- window.bindSelectables('body');
- }
- });
-})(jQuery || grp.jQuery);
+++ /dev/null
-<link rel="stylesheet" href="//ajax.googleapis.com/ajax/libs/jqueryui/{{ version }}/themes/{{ theme }}/jquery-ui.css" type="text/css">
+++ /dev/null
-{% if version %}<script src="//ajax.googleapis.com/ajax/libs/jquery/{{ version }}/jquery.min.js"></script>{% endif %}
-{% if ui %}<script src="//ajax.googleapis.com/ajax/libs/jqueryui/{{ ui }}/jquery-ui.js"></script>{% endif %}
+++ /dev/null
-from __future__ import unicode_literals
-
-from django import template
-
-register = template.Library()
-
-
-@register.inclusion_tag('selectable/jquery-js.html')
-def include_jquery_libs(version='1.12.4', ui='1.11.4'):
- return {'version': version, 'ui': ui}
-
-
-@register.inclusion_tag('selectable/jquery-css.html')
-def include_ui_theme(theme='smoothness', version='1.11.4'):
- return {'theme': theme, 'version': version}
+++ /dev/null
-from django.db import models
-from django.utils.encoding import python_2_unicode_compatible
-
-from ..base import ModelLookup
-from ..registry import registry
-
-
-@python_2_unicode_compatible
-class Thing(models.Model):
- name = models.CharField(max_length=100)
- description = models.CharField(max_length=100)
-
- def __str__(self):
- return self.name
-
- class Meta:
- ordering = ['id']
-
-
-@python_2_unicode_compatible
-class OtherThing(models.Model):
- name = models.CharField(max_length=100)
- thing = models.ForeignKey(Thing, on_delete=models.CASCADE)
-
- def __str__(self):
- return self.name
-
-
-@python_2_unicode_compatible
-class ManyThing(models.Model):
- name = models.CharField(max_length=100)
- things = models.ManyToManyField(Thing)
-
- def __str__(self):
- return self.name
-
-
-class ThingLookup(ModelLookup):
- model = Thing
- search_fields = ('name__icontains', )
-
-
-registry.register(ThingLookup)
+++ /dev/null
-from __future__ import unicode_literals
-
-import random
-import string
-from collections import defaultdict
-
-
-from django.test import TestCase, override_settings
-from django.test.html import parse_html
-
-from . import Thing
-from ..base import ModelLookup
-
-
-def parsed_inputs(html):
- "Returns a dictionary mapping name --> node of inputs found in the HTML."
- node = parse_html(html)
- inputs = {}
- for field in [c for c in node.children if c.name == 'input']:
- name = dict(field.attributes)['name']
- current = inputs.get(name, [])
- current.append(field)
- inputs[name] = current
- return inputs
-
-
-@override_settings(ROOT_URLCONF='selectable.tests.urls')
-class BaseSelectableTestCase(TestCase):
-
- def get_random_string(self, length=10):
- return ''.join(random.choice(string.ascii_letters) for x in range(length))
-
- def create_thing(self, data=None):
- data = data or {}
- defaults = {
- 'name': self.get_random_string(),
- 'description': self.get_random_string(),
- }
- defaults.update(data)
- return Thing.objects.create(**defaults)
-
-
-class SimpleModelLookup(ModelLookup):
- model = Thing
- search_fields = ('name__icontains', )
-
-
-def parsed_widget_attributes(widget):
- """
- Get a dictionary-like object containing all HTML attributes
- of the rendered widget.
-
- Lookups on this object raise ValueError if there is more than one attribute
- of the given name in the HTML, and they have different values.
- """
- # For the tests that use this, it generally doesn't matter what the value
- # is, so we supply anything.
- rendered = widget.render('a_name', 'a_value')
- return AttrMap(rendered)
-
-
-class AttrMap(object):
- def __init__(self, html):
- dom = parse_html(html)
- self._attrs = defaultdict(set)
- self._build_attr_map(dom)
-
- def _build_attr_map(self, dom):
- for node in _walk_nodes(dom):
- if node.attributes is not None:
- for (k, v) in node.attributes:
- self._attrs[k].add(v)
-
- def __contains__(self, key):
- return key in self._attrs and len(self._attrs[key]) > 0
-
- def __getitem__(self, key):
- if key not in self:
- raise KeyError(key)
- vals = self._attrs[key]
- if len(vals) > 1:
- raise ValueError("More than one value for attribute {0}: {1}".
- format(key, ", ".join(vals)))
- else:
- return list(vals)[0]
-
-
-def _walk_nodes(dom):
- yield dom
- for child in dom.children:
- for item in _walk_nodes(child):
- yield item
+++ /dev/null
-/* Test utility functions */
-(function ($) {
-
- window.createTextComplete = function (name, attrs) {
- var inputAttrs = {
- 'name': name,
- 'data-selectable-type': 'text',
- 'data-selectable-url': '/lookup/core-fruitlookup/',
- 'type': 'text'
- }, finalAttrs = $.extend({}, inputAttrs, attrs || {});
- return $('<input>', finalAttrs);
- };
-
- window.createTextCombobox = function (name, attrs) {
- // Force change of the name and type
- var inputAttrs = $.extend({
- 'data-selectable-type': 'combobox'
- }, attrs || {});
- return window.createTextComplete(name, inputAttrs);
- };
-
- window.createTextSelect = function (name, attrs) {
- var inputAttrs = $.extend({
- 'name': name + '_0'
- }, attrs || {}), textInput, hiddenInput,
- hiddenAttrs = {
- 'name': name + '_1',
- 'data-selectable-type': 'hidden',
- 'type': 'hidden'
- };
- textInput = window.createTextComplete(name, inputAttrs);
- hiddenInput = $('<input>', hiddenAttrs);
- return [textInput, hiddenInput];
- };
-
- window.createComboboxSelect = function (name, attrs) {
- var inputAttrs = $.extend({
- 'name': name + '_0'
- }, attrs || {}), textInput, hiddenInput,
- hiddenAttrs = {
- 'name': name + '_1',
- 'data-selectable-type': 'hidden',
- 'type': 'hidden'
- };
- textInput = window.createTextCombobox(name, inputAttrs);
- hiddenInput = $('<input>', hiddenAttrs);
- return [textInput, hiddenInput];
- };
-
- window.createTextSelectMultiple = function (name, attrs) {
- var inputAttrs = $.extend({
- 'name': name + '_0',
- 'data-selectable-multiple': true,
- 'data-selectable-allow-new': false
- }, attrs || {}), textInput, hiddenInput,
- hiddenAttrs = {
- 'name': name + '_1',
- 'data-selectable-type': 'hidden-multiple',
- 'type': 'hidden'
- };
- textInput = window.createTextComplete(name, inputAttrs);
- hiddenInput = $('<input>', hiddenAttrs);
- return [textInput, hiddenInput];
- };
-
- window.createComboboxSelectMultiple = function (name, attrs) {
- var inputAttrs = $.extend({
- 'name': name + '_0',
- 'data-selectable-multiple': true,
- 'data-selectable-allow-new': false
- }, attrs || {}), textInput, hiddenInput,
- hiddenAttrs = {
- 'name': name + '_1',
- 'data-selectable-type': 'hidden-multiple',
- 'type': 'hidden'
- };
- textInput = window.createTextCombobox(name, inputAttrs);
- hiddenInput = $('<input>', hiddenAttrs);
- return [textInput, hiddenInput];
- };
-
- window.simpleLookupResponse = function () {
- var meta = {
- "term": "ap",
- "limit": 25,
- "page": 1,
- "more": "Show more results"
- }, data = [
- {"id": 1, "value": "Apple", "label": "Apple"},
- {"id": 3, "value": "Grape", "label": "Grape"}
- ];
- return {"meta": meta, "data": data};
- };
-
- window.paginatedLookupResponse = function () {
- var meta = {
- "term": "ap",
- "limit": 2,
- "page": 1,
- "more": "Show more results"
- }, data = [
- {"id": 1, "value": "Apple", "label": "Apple"},
- {"id": 3, "value": "Grape", "label": "Grape"},
- {"id": null, "page": 2, "label": "Show more results"}
- ];
- return {"meta": meta, "data": data};
- };
-})(jQuery);
\ No newline at end of file
+++ /dev/null
-<!DOCTYPE html>
-<html>
-<head>
- <meta charset="utf-8">
- <title>Django Selectable Test Suite</title>
- <link rel="stylesheet" href="http://code.jquery.com/qunit/qunit-1.11.0.css" media="screen">
- <script src="jquery-loader.js"></script>
- <script src="http://code.jquery.com/qunit/qunit-1.11.0.js"></script>
- <script src="sinon-1.5.2.js"></script>
- <script src="helpers.js"></script>
- <script>QUnit.config.autostart = false;</script>
- <script data-main="main" src="http://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.4/require.min.js"></script>
-</head>
-<body>
- <h1 id="qunit-header">Django Selectable Test Suite</h1>
- <h2 id="qunit-banner"></h2>
- <div id="qunit-testrunner-toolbar"></div>
- <h2 id="qunit-userAgent"></h2>
- <ol id="qunit-tests"></ol>
- <div id="qunit-fixture"></div>
-</body>
-</html>
+++ /dev/null
-(function() {
- // Get any jquery=___ param from the query string.
- var jqversion = location.search.match(/[?&]jquery=(.*?)(?=&|$)/);
- var uiversion = location.search.match(/[?&]ui=(.*?)(?=&|$)/);
- var path;
- window.jqversion = jqversion && jqversion[1] || '1.11.2';
- window.uiversion = uiversion && uiversion[1] || '1.11.4';
- jqpath = 'http://code.jquery.com/jquery-' + window.jqversion + '.js';
- uipath = 'http://code.jquery.com/ui/' + window.uiversion + '/jquery-ui.js';
- // This is the only time I'll ever use document.write, I promise!
- document.write('<script src="' + jqpath + '"></script>');
- document.write('<script src="' + uipath + '"></script>');
-}());
+++ /dev/null
-/*global require, QUnit*/
-
-require.config({
- baseUrl: '../../static/selectable/js/',
- paths: {
- selectable: 'jquery.dj.selectable'
- },
- shim: {
- selectable: {
- exports: 'jQuery'
- }
- }
-});
-
-require(['test-methods.js', 'test-events.js', 'test-options.js'], function () {
- //Tests loaded, run Tests
- QUnit.load();
- QUnit.start();
-});
\ No newline at end of file
+++ /dev/null
-/**
- * Sinon.JS 1.5.2, 2012/11/27
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @author Contributors: https://github.com/cjohansen/Sinon.JS/blob/master/AUTHORS
- *
- * (The BSD License)
- *
- * Copyright (c) 2010-2012, Christian Johansen, christian@cjohansen.no
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- * * Redistributions of source code must retain the above copyright notice,
- * this list of conditions and the following disclaimer.
- * * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * * Neither the name of Christian Johansen nor the names of his contributors
- * may be used to endorse or promote products derived from this software
- * without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
- * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
- * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
- * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
- * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
- * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-var sinon = (function () {
-"use strict";
-
-var buster = (function (setTimeout, B) {
- var isNode = typeof require == "function" && typeof module == "object";
- var div = typeof document != "undefined" && document.createElement("div");
- var F = function () {};
-
- var buster = {
- bind: function bind(obj, methOrProp) {
- var method = typeof methOrProp == "string" ? obj[methOrProp] : methOrProp;
- var args = Array.prototype.slice.call(arguments, 2);
- return function () {
- var allArgs = args.concat(Array.prototype.slice.call(arguments));
- return method.apply(obj, allArgs);
- };
- },
-
- partial: function partial(fn) {
- var args = [].slice.call(arguments, 1);
- return function () {
- return fn.apply(this, args.concat([].slice.call(arguments)));
- };
- },
-
- create: function create(object) {
- F.prototype = object;
- return new F();
- },
-
- extend: function extend(target) {
- if (!target) { return; }
- for (var i = 1, l = arguments.length, prop; i < l; ++i) {
- for (prop in arguments[i]) {
- target[prop] = arguments[i][prop];
- }
- }
- return target;
- },
-
- nextTick: function nextTick(callback) {
- if (typeof process != "undefined" && process.nextTick) {
- return process.nextTick(callback);
- }
- setTimeout(callback, 0);
- },
-
- functionName: function functionName(func) {
- if (!func) return "";
- if (func.displayName) return func.displayName;
- if (func.name) return func.name;
- var matches = func.toString().match(/function\s+([^\(]+)/m);
- return matches && matches[1] || "";
- },
-
- isNode: function isNode(obj) {
- if (!div) return false;
- try {
- obj.appendChild(div);
- obj.removeChild(div);
- } catch (e) {
- return false;
- }
- return true;
- },
-
- isElement: function isElement(obj) {
- return obj && obj.nodeType === 1 && buster.isNode(obj);
- },
-
- isArray: function isArray(arr) {
- return Object.prototype.toString.call(arr) == "[object Array]";
- },
-
- flatten: function flatten(arr) {
- var result = [], arr = arr || [];
- for (var i = 0, l = arr.length; i < l; ++i) {
- result = result.concat(buster.isArray(arr[i]) ? flatten(arr[i]) : arr[i]);
- }
- return result;
- },
-
- each: function each(arr, callback) {
- for (var i = 0, l = arr.length; i < l; ++i) {
- callback(arr[i]);
- }
- },
-
- map: function map(arr, callback) {
- var results = [];
- for (var i = 0, l = arr.length; i < l; ++i) {
- results.push(callback(arr[i]));
- }
- return results;
- },
-
- parallel: function parallel(fns, callback) {
- function cb(err, res) {
- if (typeof callback == "function") {
- callback(err, res);
- callback = null;
- }
- }
- if (fns.length == 0) { return cb(null, []); }
- var remaining = fns.length, results = [];
- function makeDone(num) {
- return function done(err, result) {
- if (err) { return cb(err); }
- results[num] = result;
- if (--remaining == 0) { cb(null, results); }
- };
- }
- for (var i = 0, l = fns.length; i < l; ++i) {
- fns[i](makeDone(i));
- }
- },
-
- series: function series(fns, callback) {
- function cb(err, res) {
- if (typeof callback == "function") {
- callback(err, res);
- }
- }
- var remaining = fns.slice();
- var results = [];
- function callNext() {
- if (remaining.length == 0) return cb(null, results);
- var promise = remaining.shift()(next);
- if (promise && typeof promise.then == "function") {
- promise.then(buster.partial(next, null), next);
- }
- }
- function next(err, result) {
- if (err) return cb(err);
- results.push(result);
- callNext();
- }
- callNext();
- },
-
- countdown: function countdown(num, done) {
- return function () {
- if (--num == 0) done();
- };
- }
- };
-
- if (typeof process === "object" &&
- typeof require === "function" && typeof module === "object") {
- var crypto = require("crypto");
- var path = require("path");
-
- buster.tmpFile = function (fileName) {
- var hashed = crypto.createHash("sha1");
- hashed.update(fileName);
- var tmpfileName = hashed.digest("hex");
-
- if (process.platform == "win32") {
- return path.join(process.env["TEMP"], tmpfileName);
- } else {
- return path.join("/tmp", tmpfileName);
- }
- };
- }
-
- if (Array.prototype.some) {
- buster.some = function (arr, fn, thisp) {
- return arr.some(fn, thisp);
- };
- } else {
- // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/some
- buster.some = function (arr, fun, thisp) {
- if (arr == null) { throw new TypeError(); }
- arr = Object(arr);
- var len = arr.length >>> 0;
- if (typeof fun !== "function") { throw new TypeError(); }
-
- for (var i = 0; i < len; i++) {
- if (arr.hasOwnProperty(i) && fun.call(thisp, arr[i], i, arr)) {
- return true;
- }
- }
-
- return false;
- };
- }
-
- if (Array.prototype.filter) {
- buster.filter = function (arr, fn, thisp) {
- return arr.filter(fn, thisp);
- };
- } else {
- // https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/filter
- buster.filter = function (fn, thisp) {
- if (this == null) { throw new TypeError(); }
-
- var t = Object(this);
- var len = t.length >>> 0;
- if (typeof fn != "function") { throw new TypeError(); }
-
- var res = [];
- for (var i = 0; i < len; i++) {
- if (i in t) {
- var val = t[i]; // in case fun mutates this
- if (fn.call(thisp, val, i, t)) { res.push(val); }
- }
- }
-
- return res;
- };
- }
-
- if (isNode) {
- module.exports = buster;
- buster.eventEmitter = require("./buster-event-emitter");
- Object.defineProperty(buster, "defineVersionGetter", {
- get: function () {
- return require("./define-version-getter");
- }
- });
- }
-
- return buster.extend(B || {}, buster);
-}(setTimeout, buster));
-if (typeof buster === "undefined") {
- var buster = {};
-}
-
-if (typeof module === "object" && typeof require === "function") {
- buster = require("buster-core");
-}
-
-buster.format = buster.format || {};
-buster.format.excludeConstructors = ["Object", /^.$/];
-buster.format.quoteStrings = true;
-
-buster.format.ascii = (function () {
-
- var hasOwn = Object.prototype.hasOwnProperty;
-
- var specialObjects = [];
- if (typeof global != "undefined") {
- specialObjects.push({ obj: global, value: "[object global]" });
- }
- if (typeof document != "undefined") {
- specialObjects.push({ obj: document, value: "[object HTMLDocument]" });
- }
- if (typeof window != "undefined") {
- specialObjects.push({ obj: window, value: "[object Window]" });
- }
-
- function keys(object) {
- var k = Object.keys && Object.keys(object) || [];
-
- if (k.length == 0) {
- for (var prop in object) {
- if (hasOwn.call(object, prop)) {
- k.push(prop);
- }
- }
- }
-
- return k.sort();
- }
-
- function isCircular(object, objects) {
- if (typeof object != "object") {
- return false;
- }
-
- for (var i = 0, l = objects.length; i < l; ++i) {
- if (objects[i] === object) {
- return true;
- }
- }
-
- return false;
- }
-
- function ascii(object, processed, indent) {
- if (typeof object == "string") {
- var quote = typeof this.quoteStrings != "boolean" || this.quoteStrings;
- return processed || quote ? '"' + object + '"' : object;
- }
-
- if (typeof object == "function" && !(object instanceof RegExp)) {
- return ascii.func(object);
- }
-
- processed = processed || [];
-
- if (isCircular(object, processed)) {
- return "[Circular]";
- }
-
- if (Object.prototype.toString.call(object) == "[object Array]") {
- return ascii.array.call(this, object, processed);
- }
-
- if (!object) {
- return "" + object;
- }
-
- if (buster.isElement(object)) {
- return ascii.element(object);
- }
-
- if (typeof object.toString == "function" &&
- object.toString !== Object.prototype.toString) {
- return object.toString();
- }
-
- for (var i = 0, l = specialObjects.length; i < l; i++) {
- if (object === specialObjects[i].obj) {
- return specialObjects[i].value;
- }
- }
-
- return ascii.object.call(this, object, processed, indent);
- }
-
- ascii.func = function (func) {
- return "function " + buster.functionName(func) + "() {}";
- };
-
- ascii.array = function (array, processed) {
- processed = processed || [];
- processed.push(array);
- var pieces = [];
-
- for (var i = 0, l = array.length; i < l; ++i) {
- pieces.push(ascii.call(this, array[i], processed));
- }
-
- return "[" + pieces.join(", ") + "]";
- };
-
- ascii.object = function (object, processed, indent) {
- processed = processed || [];
- processed.push(object);
- indent = indent || 0;
- var pieces = [], properties = keys(object), prop, str, obj;
- var is = "";
- var length = 3;
-
- for (var i = 0, l = indent; i < l; ++i) {
- is += " ";
- }
-
- for (i = 0, l = properties.length; i < l; ++i) {
- prop = properties[i];
- obj = object[prop];
-
- if (isCircular(obj, processed)) {
- str = "[Circular]";
- } else {
- str = ascii.call(this, obj, processed, indent + 2);
- }
-
- str = (/\s/.test(prop) ? '"' + prop + '"' : prop) + ": " + str;
- length += str.length;
- pieces.push(str);
- }
-
- var cons = ascii.constructorName.call(this, object);
- var prefix = cons ? "[" + cons + "] " : ""
-
- return (length + indent) > 80 ?
- prefix + "{\n " + is + pieces.join(",\n " + is) + "\n" + is + "}" :
- prefix + "{ " + pieces.join(", ") + " }";
- };
-
- ascii.element = function (element) {
- var tagName = element.tagName.toLowerCase();
- var attrs = element.attributes, attribute, pairs = [], attrName;
-
- for (var i = 0, l = attrs.length; i < l; ++i) {
- attribute = attrs.item(i);
- attrName = attribute.nodeName.toLowerCase().replace("html:", "");
-
- if (attrName == "contenteditable" && attribute.nodeValue == "inherit") {
- continue;
- }
-
- if (!!attribute.nodeValue) {
- pairs.push(attrName + "=\"" + attribute.nodeValue + "\"");
- }
- }
-
- var formatted = "<" + tagName + (pairs.length > 0 ? " " : "");
- var content = element.innerHTML;
-
- if (content.length > 20) {
- content = content.substr(0, 20) + "[...]";
- }
-
- var res = formatted + pairs.join(" ") + ">" + content + "</" + tagName + ">";
-
- return res.replace(/ contentEditable="inherit"/, "");
- };
-
- ascii.constructorName = function (object) {
- var name = buster.functionName(object && object.constructor);
- var excludes = this.excludeConstructors || buster.format.excludeConstructors || [];
-
- for (var i = 0, l = excludes.length; i < l; ++i) {
- if (typeof excludes[i] == "string" && excludes[i] == name) {
- return "";
- } else if (excludes[i].test && excludes[i].test(name)) {
- return "";
- }
- }
-
- return name;
- };
-
- return ascii;
-}());
-
-if (typeof module != "undefined") {
- module.exports = buster.format;
-}
-/*jslint eqeqeq: false, onevar: false, forin: true, nomen: false, regexp: false, plusplus: false*/
-/*global module, require, __dirname, document*/
-/**
- * Sinon core utilities. For internal use only.
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2011 Christian Johansen
- */
-
-var sinon = (function (buster) {
- var div = typeof document != "undefined" && document.createElement("div");
- var hasOwn = Object.prototype.hasOwnProperty;
-
- function isDOMNode(obj) {
- var success = false;
-
- try {
- obj.appendChild(div);
- success = div.parentNode == obj;
- } catch (e) {
- return false;
- } finally {
- try {
- obj.removeChild(div);
- } catch (e) {
- // Remove failed, not much we can do about that
- }
- }
-
- return success;
- }
-
- function isElement(obj) {
- return div && obj && obj.nodeType === 1 && isDOMNode(obj);
- }
-
- function isFunction(obj) {
- return typeof obj === "function" || !!(obj && obj.constructor && obj.call && obj.apply);
- }
-
- function mirrorProperties(target, source) {
- for (var prop in source) {
- if (!hasOwn.call(target, prop)) {
- target[prop] = source[prop];
- }
- }
- }
-
- var sinon = {
- wrapMethod: function wrapMethod(object, property, method) {
- if (!object) {
- throw new TypeError("Should wrap property of object");
- }
-
- if (typeof method != "function") {
- throw new TypeError("Method wrapper should be function");
- }
-
- var wrappedMethod = object[property];
-
- if (!isFunction(wrappedMethod)) {
- throw new TypeError("Attempted to wrap " + (typeof wrappedMethod) + " property " +
- property + " as function");
- }
-
- if (wrappedMethod.restore && wrappedMethod.restore.sinon) {
- throw new TypeError("Attempted to wrap " + property + " which is already wrapped");
- }
-
- if (wrappedMethod.calledBefore) {
- var verb = !!wrappedMethod.returns ? "stubbed" : "spied on";
- throw new TypeError("Attempted to wrap " + property + " which is already " + verb);
- }
-
- // IE 8 does not support hasOwnProperty on the window object.
- var owned = hasOwn.call(object, property);
- object[property] = method;
- method.displayName = property;
-
- method.restore = function () {
- // For prototype properties try to reset by delete first.
- // If this fails (ex: localStorage on mobile safari) then force a reset
- // via direct assignment.
- if (!owned) {
- delete object[property];
- }
- if (object[property] === method) {
- object[property] = wrappedMethod;
- }
- };
-
- method.restore.sinon = true;
- mirrorProperties(method, wrappedMethod);
-
- return method;
- },
-
- extend: function extend(target) {
- for (var i = 1, l = arguments.length; i < l; i += 1) {
- for (var prop in arguments[i]) {
- if (arguments[i].hasOwnProperty(prop)) {
- target[prop] = arguments[i][prop];
- }
-
- // DONT ENUM bug, only care about toString
- if (arguments[i].hasOwnProperty("toString") &&
- arguments[i].toString != target.toString) {
- target.toString = arguments[i].toString;
- }
- }
- }
-
- return target;
- },
-
- create: function create(proto) {
- var F = function () {};
- F.prototype = proto;
- return new F();
- },
-
- deepEqual: function deepEqual(a, b) {
- if (sinon.match && sinon.match.isMatcher(a)) {
- return a.test(b);
- }
- if (typeof a != "object" || typeof b != "object") {
- return a === b;
- }
-
- if (isElement(a) || isElement(b)) {
- return a === b;
- }
-
- if (a === b) {
- return true;
- }
-
- if ((a === null && b !== null) || (a !== null && b === null)) {
- return false;
- }
-
- var aString = Object.prototype.toString.call(a);
- if (aString != Object.prototype.toString.call(b)) {
- return false;
- }
-
- if (aString == "[object Array]") {
- if (a.length !== b.length) {
- return false;
- }
-
- for (var i = 0, l = a.length; i < l; i += 1) {
- if (!deepEqual(a[i], b[i])) {
- return false;
- }
- }
-
- return true;
- }
-
- var prop, aLength = 0, bLength = 0;
-
- for (prop in a) {
- aLength += 1;
-
- if (!deepEqual(a[prop], b[prop])) {
- return false;
- }
- }
-
- for (prop in b) {
- bLength += 1;
- }
-
- if (aLength != bLength) {
- return false;
- }
-
- return true;
- },
-
- functionName: function functionName(func) {
- var name = func.displayName || func.name;
-
- // Use function decomposition as a last resort to get function
- // name. Does not rely on function decomposition to work - if it
- // doesn't debugging will be slightly less informative
- // (i.e. toString will say 'spy' rather than 'myFunc').
- if (!name) {
- var matches = func.toString().match(/function ([^\s\(]+)/);
- name = matches && matches[1];
- }
-
- return name;
- },
-
- functionToString: function toString() {
- if (this.getCall && this.callCount) {
- var thisValue, prop, i = this.callCount;
-
- while (i--) {
- thisValue = this.getCall(i).thisValue;
-
- for (prop in thisValue) {
- if (thisValue[prop] === this) {
- return prop;
- }
- }
- }
- }
-
- return this.displayName || "sinon fake";
- },
-
- getConfig: function (custom) {
- var config = {};
- custom = custom || {};
- var defaults = sinon.defaultConfig;
-
- for (var prop in defaults) {
- if (defaults.hasOwnProperty(prop)) {
- config[prop] = custom.hasOwnProperty(prop) ? custom[prop] : defaults[prop];
- }
- }
-
- return config;
- },
-
- format: function (val) {
- return "" + val;
- },
-
- defaultConfig: {
- injectIntoThis: true,
- injectInto: null,
- properties: ["spy", "stub", "mock", "clock", "server", "requests"],
- useFakeTimers: true,
- useFakeServer: true
- },
-
- timesInWords: function timesInWords(count) {
- return count == 1 && "once" ||
- count == 2 && "twice" ||
- count == 3 && "thrice" ||
- (count || 0) + " times";
- },
-
- calledInOrder: function (spies) {
- for (var i = 1, l = spies.length; i < l; i++) {
- if (!spies[i - 1].calledBefore(spies[i])) {
- return false;
- }
- }
-
- return true;
- },
-
- orderByFirstCall: function (spies) {
- return spies.sort(function (a, b) {
- // uuid, won't ever be equal
- var aCall = a.getCall(0);
- var bCall = b.getCall(0);
- var aId = aCall && aCall.callId || -1;
- var bId = bCall && bCall.callId || -1;
-
- return aId < bId ? -1 : 1;
- });
- },
-
- log: function () {},
-
- logError: function (label, err) {
- var msg = label + " threw exception: "
- sinon.log(msg + "[" + err.name + "] " + err.message);
- if (err.stack) { sinon.log(err.stack); }
-
- setTimeout(function () {
- err.message = msg + err.message;
- throw err;
- }, 0);
- },
-
- typeOf: function (value) {
- if (value === null) {
- return "null";
- }
- else if (value === undefined) {
- return "undefined";
- }
- var string = Object.prototype.toString.call(value);
- return string.substring(8, string.length - 1).toLowerCase();
- }
- };
-
- var isNode = typeof module == "object" && typeof require == "function";
-
- if (isNode) {
- try {
- buster = { format: require("buster-format") };
- } catch (e) {}
- module.exports = sinon;
- module.exports.spy = require("./sinon/spy");
- module.exports.stub = require("./sinon/stub");
- module.exports.mock = require("./sinon/mock");
- module.exports.collection = require("./sinon/collection");
- module.exports.assert = require("./sinon/assert");
- module.exports.sandbox = require("./sinon/sandbox");
- module.exports.test = require("./sinon/test");
- module.exports.testCase = require("./sinon/test_case");
- module.exports.assert = require("./sinon/assert");
- module.exports.match = require("./sinon/match");
- }
-
- if (buster) {
- var formatter = sinon.create(buster.format);
- formatter.quoteStrings = false;
- sinon.format = function () {
- return formatter.ascii.apply(formatter, arguments);
- };
- } else if (isNode) {
- try {
- var util = require("util");
- sinon.format = function (value) {
- return typeof value == "object" && value.toString === Object.prototype.toString ? util.inspect(value) : value;
- };
- } catch (e) {
- /* Node, but no util module - would be very old, but better safe than
- sorry */
- }
- }
-
- return sinon;
-}(typeof buster == "object" && buster));
-
-/* @depend ../sinon.js */
-/*jslint eqeqeq: false, onevar: false, plusplus: false*/
-/*global module, require, sinon*/
-/**
- * Match functions
- *
- * @author Maximilian Antoni (mail@maxantoni.de)
- * @license BSD
- *
- * Copyright (c) 2012 Maximilian Antoni
- */
-
-(function (sinon) {
- var commonJSModule = typeof module == "object" && typeof require == "function";
-
- if (!sinon && commonJSModule) {
- sinon = require("../sinon");
- }
-
- if (!sinon) {
- return;
- }
-
- function assertType(value, type, name) {
- var actual = sinon.typeOf(value);
- if (actual !== type) {
- throw new TypeError("Expected type of " + name + " to be " +
- type + ", but was " + actual);
- }
- }
-
- var matcher = {
- toString: function () {
- return this.message;
- }
- };
-
- function isMatcher(object) {
- return matcher.isPrototypeOf(object);
- }
-
- function matchObject(expectation, actual) {
- if (actual === null || actual === undefined) {
- return false;
- }
- for (var key in expectation) {
- if (expectation.hasOwnProperty(key)) {
- var exp = expectation[key];
- var act = actual[key];
- if (match.isMatcher(exp)) {
- if (!exp.test(act)) {
- return false;
- }
- } else if (sinon.typeOf(exp) === "object") {
- if (!matchObject(exp, act)) {
- return false;
- }
- } else if (!sinon.deepEqual(exp, act)) {
- return false;
- }
- }
- }
- return true;
- }
-
- matcher.or = function (m2) {
- if (!isMatcher(m2)) {
- throw new TypeError("Matcher expected");
- }
- var m1 = this;
- var or = sinon.create(matcher);
- or.test = function (actual) {
- return m1.test(actual) || m2.test(actual);
- };
- or.message = m1.message + ".or(" + m2.message + ")";
- return or;
- };
-
- matcher.and = function (m2) {
- if (!isMatcher(m2)) {
- throw new TypeError("Matcher expected");
- }
- var m1 = this;
- var and = sinon.create(matcher);
- and.test = function (actual) {
- return m1.test(actual) && m2.test(actual);
- };
- and.message = m1.message + ".and(" + m2.message + ")";
- return and;
- };
-
- var match = function (expectation, message) {
- var m = sinon.create(matcher);
- var type = sinon.typeOf(expectation);
- switch (type) {
- case "object":
- if (typeof expectation.test === "function") {
- m.test = function (actual) {
- return expectation.test(actual) === true;
- };
- m.message = "match(" + sinon.functionName(expectation.test) + ")";
- return m;
- }
- var str = [];
- for (var key in expectation) {
- if (expectation.hasOwnProperty(key)) {
- str.push(key + ": " + expectation[key]);
- }
- }
- m.test = function (actual) {
- return matchObject(expectation, actual);
- };
- m.message = "match(" + str.join(", ") + ")";
- break;
- case "number":
- m.test = function (actual) {
- return expectation == actual;
- };
- break;
- case "string":
- m.test = function (actual) {
- if (typeof actual !== "string") {
- return false;
- }
- return actual.indexOf(expectation) !== -1;
- };
- m.message = "match(\"" + expectation + "\")";
- break;
- case "regexp":
- m.test = function (actual) {
- if (typeof actual !== "string") {
- return false;
- }
- return expectation.test(actual);
- };
- break;
- case "function":
- m.test = expectation;
- if (message) {
- m.message = message;
- } else {
- m.message = "match(" + sinon.functionName(expectation) + ")";
- }
- break;
- default:
- m.test = function (actual) {
- return sinon.deepEqual(expectation, actual);
- };
- }
- if (!m.message) {
- m.message = "match(" + expectation + ")";
- }
- return m;
- };
-
- match.isMatcher = isMatcher;
-
- match.any = match(function () {
- return true;
- }, "any");
-
- match.defined = match(function (actual) {
- return actual !== null && actual !== undefined;
- }, "defined");
-
- match.truthy = match(function (actual) {
- return !!actual;
- }, "truthy");
-
- match.falsy = match(function (actual) {
- return !actual;
- }, "falsy");
-
- match.same = function (expectation) {
- return match(function (actual) {
- return expectation === actual;
- }, "same(" + expectation + ")");
- };
-
- match.typeOf = function (type) {
- assertType(type, "string", "type");
- return match(function (actual) {
- return sinon.typeOf(actual) === type;
- }, "typeOf(\"" + type + "\")");
- };
-
- match.instanceOf = function (type) {
- assertType(type, "function", "type");
- return match(function (actual) {
- return actual instanceof type;
- }, "instanceOf(" + sinon.functionName(type) + ")");
- };
-
- function createPropertyMatcher(propertyTest, messagePrefix) {
- return function (property, value) {
- assertType(property, "string", "property");
- var onlyProperty = arguments.length === 1;
- var message = messagePrefix + "(\"" + property + "\"";
- if (!onlyProperty) {
- message += ", " + value;
- }
- message += ")";
- return match(function (actual) {
- if (actual === undefined || actual === null ||
- !propertyTest(actual, property)) {
- return false;
- }
- return onlyProperty || sinon.deepEqual(value, actual[property]);
- }, message);
- };
- }
-
- match.has = createPropertyMatcher(function (actual, property) {
- if (typeof actual === "object") {
- return property in actual;
- }
- return actual[property] !== undefined;
- }, "has");
-
- match.hasOwn = createPropertyMatcher(function (actual, property) {
- return actual.hasOwnProperty(property);
- }, "hasOwn");
-
- match.bool = match.typeOf("boolean");
- match.number = match.typeOf("number");
- match.string = match.typeOf("string");
- match.object = match.typeOf("object");
- match.func = match.typeOf("function");
- match.array = match.typeOf("array");
- match.regexp = match.typeOf("regexp");
- match.date = match.typeOf("date");
-
- if (commonJSModule) {
- module.exports = match;
- } else {
- sinon.match = match;
- }
-}(typeof sinon == "object" && sinon || null));
-
-/**
- * @depend ../sinon.js
- * @depend match.js
- */
-/*jslint eqeqeq: false, onevar: false, plusplus: false*/
-/*global module, require, sinon*/
-/**
- * Spy functions
- *
- * @author Christian Johansen (christian@cjohansen.no)
- * @license BSD
- *
- * Copyright (c) 2010-2011 Christian Johansen
- */
-
-(function (sinon) {
- var commonJSModule = typeof module == "object" && typeof require == "function";
- var spyCall;
- var callId = 0;
- var push = [].push;
- var slice = Array.prototype.slice;
-
- if (!sinon && commonJSModule) {
- sinon = require("../sinon");
- }
-
- if (!sinon) {
- return;
- }
-
- function spy(object, property) {
- if (!property && typeof object == "function") {
- return spy.create(object);
- }
-
- if (!object && !property) {
- return spy.create(function () {});
- }
-
- var method = object[property];
- return sinon.wrapMethod(object, property, spy.create(method));
- }
-
- sinon.extend(spy, (function () {
-
- function delegateToCalls(api, method, matchAny, actual, notCalled) {
- api[method] = function () {
- if (!this.called) {
- if (notCalled) {
- return notCalled.apply(this, arguments);
- }
- return false;
- }
-
- var currentCall;
- var matches = 0;
-
- for (var i = 0, l = this.callCount; i < l; i += 1) {
- currentCall = this.getCall(i);
-
- if (currentCall[actual || method].apply(currentCall, arguments)) {
- matches += 1;
-
- if (matchAny) {
- return true;
- }
- }
- }
-
- return matches === this.callCount;
- };
- }
-
- function matchingFake(fakes, args, strict) {
- if (!fakes) {
- return;
- }
-
- var alen = args.length;
-
- for (var i = 0, l = fakes.length; i < l; i++) {
- if (fakes[i].matches(args, strict)) {
- return fakes[i];
- }
- }
- }
-
- function incrementCallCount() {
- this.called = true;
- this.callCount += 1;
- this.notCalled = false;
- this.calledOnce = this.callCount == 1;
- this.calledTwice = this.callCount == 2;
- this.calledThrice = this.callCount == 3;
- }
-
- function createCallProperties() {
- this.firstCall = this.getCall(0);
- this.secondCall = this.getCall(1);
- this.thirdCall = this.getCall(2);
- this.lastCall = this.getCall(this.callCount - 1);
- }
-
- var vars = "a,b,c,d,e,f,g,h,i,j,k,l";
- function createProxy(func) {
- // Retain the function length:
- var p;
- if (func.length) {
- eval("p = (function proxy(" + vars.substring(0, func.length * 2 - 1) +
- ") { return p.invoke(func, this, slice.call(arguments)); });");
- }
- else {
- p = function proxy() {
- return p.invoke(func, this, slice.call(arguments));
- };
- }
- return p;
- }
-
- var uuid = 0;
-
- // Public API
- var spyApi = {
- reset: function () {
- this.called = false;
- this.notCalled = true;
- this.calledOnce = false;
- this.calledTwice = false;
- this.calledThrice = false;
- this.callCount = 0;
- this.firstCall = null;
- this.secondCall = null;
- this.thirdCall = null;
- this.lastCall = null;
- this.args = [];
- this.returnValues = [];
- this.thisValues = [];
- this.exceptions = [];
- this.callIds = [];
- if (this.fakes) {
- for (var i = 0; i < this.fakes.length; i++) {
- this.fakes[i].reset();
- }
- }
- },
-
- create: function create(func) {
- var name;
-
- if (typeof func != "function") {
- func = function () {};
- } else {
- name = sinon.functionName(func);
- }
-
- var proxy = createProxy(func);
-
- sinon.extend(proxy, spy);
- delete proxy.create;
- sinon.extend(proxy, func);
-
- proxy.reset();
- proxy.prototype = func.prototype;
- proxy.displayName = name || "spy";
- proxy.toString = sinon.functionToString;
- proxy._create = sinon.spy.create;
- proxy.id = "spy#" + uuid++;
-
- return proxy;
- },
-
- invoke: function invoke(func, thisValue, args) {
- var matching = matchingFake(this.fakes, args);
- var exception, returnValue;
-
- incrementCallCount.call(this);
- push.call(this.thisValues, thisValue);
- push.call(this.args, args);
- push.call(this.callIds, callId++);
-
- try {
- if (matching) {
- returnValue = matching.invoke(func, thisValue, args);
- } else {
- returnValue = (this.func || func).apply(thisValue, args);
- }
- } catch (e) {
- push.call(this.returnValues, undefined);
- exception = e;
- throw e;
- } finally {
- push.call(this.exceptions, exception);
- }
-
- push.call(this.returnValues, returnValue);
-
- createCallProperties.call(this);
-
- return returnValue;
- },
-
- getCall: function getCall(i) {
- if (i < 0 || i >= this.callCount) {
- return null;
- }
-
- return spyCall.create(this, this.thisValues[i], this.args[i],
- this.returnValues[i], this.exceptions[i],
- this.callIds[i]);
- },
-
- calledBefore: function calledBefore(spyFn) {
- if (!this.called) {
- return false;
- }
-
- if (!spyFn.called) {
- return true;
- }
-
- return this.callIds[0] < spyFn.callIds[spyFn.callIds.length - 1];
- },
-
- calledAfter: function calledAfter(spyFn) {
- if (!this.called || !spyFn.called) {
- return false;
- }
-
- return this.callIds[this.callCount - 1] > spyFn.callIds[spyFn.callCount - 1];
- },
-
- withArgs: function () {
- var args = slice.call(arguments);
-
- if (this.fakes) {
- var match = matchingFake(this.fakes, args, true);
-
- if (match) {
- return match;
- }
- } else {
- this.fakes = [];
- }
-
- var original = this;
- var fake = this._create();
- fake.matchingAguments = args;
- push.call(this.fakes, fake);
-
- fake.withArgs = function () {
- return original.withArgs.apply(original, arguments);
- };
-
- for (var i = 0; i < this.args.length; i++) {
- if (fake.matches(this.args[i])) {
- incrementCallCount.call(fake);
- push.call(fake.thisValues, this.thisValues[i]);
- push.call(fake.args, this.args[i]);
- push.call(fake.returnValues, this.returnValues[i]);
- push.call(fake.exceptions, this.exceptions[i]);
- push.call(fake.callIds, this.callIds[i]);
- }
- }
- createCallProperties.call(fake);
-
- return fake;
- },
-
- matches: function (args, strict) {
- var margs = this.matchingAguments;
-
- if (margs.length <= args.length &&
- sinon.deepEqual(margs, args.slice(0, margs.length))) {
- return !strict || margs.length == args.length;
- }
- },
-
- printf: function (format) {
- var spy = this;
- var args = slice.call(arguments, 1);
- var formatter;
-
- return (format || "").replace(/%(.)/g, function (match, specifyer) {
- formatter = spyApi.formatters[specifyer];
-
- if (typeof formatter == "function") {
- return formatter.call(null, spy, args);
- } else if (!isNaN(parseInt(specifyer), 10)) {
- return sinon.format(args[specifyer - 1]);
- }
-
- return "%" + specifyer;
- });
- }
- };
-
- delegateToCalls(spyApi, "calledOn", true);
- delegateToCalls(spyApi, "alwaysCalledOn", false, "calledOn");
- delegateToCalls(spyApi, "calledWith", true);
- delegateToCalls(spyApi, "calledWithMatch", true);
- delegateToCalls(spyApi, "alwaysCalledWith", false, "calledWith");
- delegateToCalls(spyApi, "alwaysCalledWithMatch", false, "calledWithMatch");
- delegateToCalls(spyApi, "calledWithExactly", true);
- delegateToCalls(spyApi, "alwaysCalledWithExactly", false, "calledWithExactly");
- delegateToCalls(spyApi, "neverCalledWith", false, "notCalledWith",
- function () { return true; });
- delegateToCalls(spyApi, "neverCalledWithMatch", false, "notCalledWithMatch",
- function () { return true; });
- delegateToCalls(spyApi, "threw", true);
- delegateToCalls(spyApi, "alwaysThrew", false, "threw");
- delegateToCalls(spyApi, "returned", true);
- delegateToCalls(spyApi, "alwaysReturned", false, "returned");
- delegateToCalls(spyApi, "calledWithNew", true);
- delegateToCalls(spyApi, "alwaysCalledWithNew", false, "calledWithNew");
- delegateToCalls(spyApi, "callArg", false, "callArgWith", function () {
- throw new Error(this.toString() + " cannot call arg since it was not yet invoked.");
- });
- spyApi.callArgWith = spyApi.callArg;
- delegateToCalls(spyApi, "yield", false, "yield", function () {
- throw new Error(this.toString() + " cannot yield since it was not yet invoked.");
- });
- // "invokeCallback" is an alias for "yield" since "yield" is invalid in strict mode.
- spyApi.invokeCallback = spyApi.yield;
- delegateToCalls(spyApi, "yieldTo", false, "yieldTo", function (property) {
- throw new Error(this.toString() + " cannot yield to '" + property +
- "' since it was not yet invoked.");
- });
-
- spyApi.formatters = {
- "c": function (spy) {
- return sinon.timesInWords(spy.callCount);
- },
-
- "n": function (spy) {
- return spy.toString();
- },
-
- "C": function (spy) {
- var calls = [];
-
- for (var i = 0, l = spy.callCount; i < l; ++i) {
- push.call(calls, " " + spy.getCall(i).toString());
- }
-
- return calls.length > 0 ? "\n" + calls.join("\n") : "";
- },
-
- "t": function (spy) {
- var objects = [];
-
- for (var i = 0, l = spy.callCount; i < l; ++i) {
- push.call(objects, sinon.format(spy.thisValues[i]));
- }
-
- return objects.join(", ");
- },
-
- "*": function (spy, args) {
- var formatted = [];
-
- for (var i = 0, l = args.length; i < l; ++i) {
- push.call(formatted, sinon.format(args[i]));
- }
-
- return formatted.join(", ");
- }
- };
-
- return spyApi;
- }()));
-
- spyCall = (function () {
-
- function throwYieldError(proxy, text, args) {
- var msg = sinon.functionName(proxy) + text;
- if (args.length) {
- msg += " Received [" + slice.call(args).join(", ") + "]";
- }
- throw new Error(msg);
- }
-
- var callApi = {
- create: function create(spy, thisValue, args, returnValue, exception, id) {
- var proxyCall = sinon.create(spyCall);
- delete proxyCall.create;
- proxyCall.proxy = spy;
- proxyCall.thisValue = thisValue;
- proxyCall.args = args;
- proxyCall.returnValue = returnValue;
- proxyCall.exception = exception;
- proxyCall.callId = typeof id == "number" && id || callId++;
-
- return proxyCall;
- },
-
- calledOn: function calledOn(thisValue) {
- if (sinon.match && sinon.match.isMatcher(thisValue)) {
- return thisValue.test(this.thisValue);
- }
- return this.thisValue === thisValue;
- },
-
- calledWith: function calledWith() {
- for (var i = 0, l = arguments.length; i < l; i += 1) {
- if (!sinon.deepEqual(arguments[i], this.args[i])) {
- return false;
- }
- }
-
- return true;
- },
-
- calledWithMatch: function calledWithMatch() {
- for (var i = 0, l = arguments.length; i < l; i += 1) {
- var actual = this.args[i];
- var expectation = arguments[i];
- if (!sinon.match || !sinon.match(expectation).test(actual)) {
- return false;
- }
- }
- return true;
- },
-
- calledWithExactly: function calledWithExactly() {
- return arguments.length == this.args.length &&
- this.calledWith.apply(this, arguments);
- },
-
- notCalledWith: function notCalledWith() {
- return !this.calledWith.apply(this, arguments);
- },
-
- notCalledWithMatch: function notCalledWithMatch() {
- return !this.calledWithMatch.apply(this, arguments);
- },
-
- returned: function returned(value) {
- return sinon.deepEqual(value, this.returnValue);
- },
-
- threw: function threw(error) {
- &nb