From 6ffc1d48115be917d4a95b48a744af462a8c6d22 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Fri, 9 Mar 2018 16:43:49 -0500 Subject: [PATCH] Import newer version of django-selectable Sync up to the same version we have on the commitfest app, which will also be required for eventual django 1.11 support. --- dep/django-selectable/AUTHORS.txt | 5 + dep/django-selectable/LICENSE.txt | 2 +- dep/django-selectable/README.rst | 33 +++-- dep/django-selectable/docs/admin.rst | 119 ++++++++++++--- dep/django-selectable/docs/advanced.rst | 117 ++++++++++----- dep/django-selectable/docs/conf.py | 8 +- dep/django-selectable/docs/contribute.rst | 26 ++-- dep/django-selectable/docs/fields.rst | 33 +++-- dep/django-selectable/docs/lookups.rst | 39 ++--- dep/django-selectable/docs/quick-start.rst | 83 ++++++++--- dep/django-selectable/docs/releases.rst | 65 ++++++++- dep/django-selectable/docs/settings.rst | 2 - dep/django-selectable/docs/testing.rst | 12 +- dep/django-selectable/docs/widgets.rst | 8 +- dep/django-selectable/runtests.py | 9 +- dep/django-selectable/selectable/__init__.py | 2 +- dep/django-selectable/selectable/apps.py | 5 +- dep/django-selectable/selectable/base.py | 29 +--- dep/django-selectable/selectable/compat.py | 42 +----- dep/django-selectable/selectable/forms/base.py | 5 +- dep/django-selectable/selectable/forms/fields.py | 9 +- dep/django-selectable/selectable/forms/widgets.py | 161 +++++++++++++++------ dep/django-selectable/selectable/registry.py | 27 +--- .../static/selectable/js/jquery.dj.selectable.js | 7 +- .../selectable/templatetags/selectable_tags.py | 6 +- dep/django-selectable/selectable/tests/__init__.py | 13 +- dep/django-selectable/selectable/tests/base.py | 72 ++++++--- .../selectable/tests/qunit/jquery-loader.js | 4 +- .../selectable/tests/qunit/test-methods.js | 43 +++++- .../selectable/tests/test_fields.py | 2 +- .../selectable/tests/test_forms.py | 56 +++---- .../selectable/tests/test_functional.py | 59 ++++++-- .../selectable/tests/test_templatetags.py | 14 +- .../selectable/tests/test_views.py | 6 +- .../selectable/tests/test_widgets.py | 122 +++++++--------- dep/django-selectable/selectable/tests/views.py | 1 + dep/django-selectable/selectable/urls.py | 7 - dep/django-selectable/selectable/views.py | 2 +- dep/django-selectable/setup.cfg | 15 +- dep/django-selectable/setup.py | 16 +- 40 files changed, 812 insertions(+), 474 deletions(-) rewrite dep/django-selectable/selectable/compat.py (82%) diff --git a/dep/django-selectable/AUTHORS.txt b/dep/django-selectable/AUTHORS.txt index 1c0422f2..965073b0 100644 --- a/dep/django-selectable/AUTHORS.txt +++ b/dep/django-selectable/AUTHORS.txt @@ -23,5 +23,10 @@ Rebecca Lovewell Thomas Güttler Yuri Khrustalev @SaeX +Tam Huynh +Raphael Merx +Josh Addington +Tobias Zanke +Petr Dlouhy Thanks for all of your work! diff --git a/dep/django-selectable/LICENSE.txt b/dep/django-selectable/LICENSE.txt index c50fbf06..b06f6132 100644 --- a/dep/django-selectable/LICENSE.txt +++ b/dep/django-selectable/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2010-2014, Mark Lavin +Copyright (c) 2010-2018, Mark Lavin All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/dep/django-selectable/README.rst b/dep/django-selectable/README.rst index 0219c2cb..86f8aa08 100644 --- a/dep/django-selectable/README.rst +++ b/dep/django-selectable/README.rst @@ -6,6 +6,22 @@ Tools and widgets for using/creating auto-complete selection widgets using Djang .. 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 `_ + or `question `_ + 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 `_. I'm happy to work + with you to get you going on an issue. + Features ----------------------------------- @@ -17,10 +33,10 @@ Features Installation Requirements ----------------------------------- -- Python 2.6-2.7, 3.2+ -- `Django `_ >= 1.5 -- `jQuery `_ >= 1.7 -- `jQuery UI `_ >= 1.8 +- Python 2.7, 3.3+ +- `Django `_ >= 1.7, <= 1.11 +- `jQuery `_ >= 1.9, < 3.0 +- `jQuery UI `_ >= 1.10, < 1.12 To install:: @@ -40,16 +56,16 @@ Google CDN. Once installed you should add the urls to your root url patterns:: - urlpatterns = patterns('', + urlpatterns = [ # Other patterns go here - (r'^selectable/', include('selectable.urls')), - ) + url(r'^selectable/', include('selectable.urls')), + ] Documentation ----------------------------------- -Documentation for django-selectable is available on `Read The Docs `_. +Documentation for django-selectable is available on `Read The Docs `_. Additional Help/Support @@ -66,4 +82,3 @@ check out our `contributing guide `_. - diff --git a/dep/django-selectable/docs/admin.rst b/dep/django-selectable/docs/admin.rst index 07cba698..e0c02fa3 100644 --- a/dep/django-selectable/docs/admin.rst +++ b/dep/django-selectable/docs/admin.rst @@ -6,9 +6,9 @@ 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 `_ -and `ModelForms `_ particularly -on `overriding the default widgets `_. +documentation on the `ModelAdmin.form `_ +and `ModelForms `_ particularly +on `overriding the default widgets `_. As you will see integrating django-selectable in the adminis the same as working with regular forms. @@ -24,12 +24,17 @@ Django admin that means overriding You can include this media in the block name `extrahead` which is defined in `admin/base.html `_. - .. literalinclude:: ../example/example/templates/admin/base_site.html - :start-after: {% endblock title %} - :end-before: {% block branding %} + .. 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 `_. +`overriding admin templates `_. See the example project for the full template example. @@ -38,8 +43,6 @@ See the example project for the full template example. Using Grappelli -------------------------------------- -.. versionadded:: 0.7 - `Grappelli `_ 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`` @@ -52,19 +55,78 @@ and make use of them. Basic Example -------------------------------------- -In our sample project we have a ``Farm`` model with a foreign key to ``auth.User`` and +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. - .. literalinclude:: ../example/core/models.py - :pyobject: Farm + .. 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') + 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`. - .. literalinclude:: ../example/core/admin.py - :pyobject: FarmAdminForm + .. 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) - .. literalinclude:: ../example/core/admin.py - :pyobject: 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 @@ -76,7 +138,7 @@ 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 `_ +for the `ModelAdmin.raw_id_fields `_ when the default select box grows too long. @@ -87,13 +149,26 @@ Inline Example With our ``Farm`` model we can also associate the ``UserAdmin`` with a ``Farm`` by making use of the `InlineModelAdmin -`_. +`_. We can even make use of the same ``FarmAdminForm``. - .. literalinclude:: ../example/core/admin.py - :pyobject: FarmInline - .. literalinclude:: ../example/core/admin.py - :pyobject: NewUserAdmin + .. 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. diff --git a/dep/django-selectable/docs/advanced.rst b/dep/django-selectable/docs/advanced.rst index bbaa3df4..d3ffe2b5 100644 --- a/dep/django-selectable/docs/advanced.rst +++ b/dep/django-selectable/docs/advanced.rst @@ -2,7 +2,7 @@ Advanced Usage ========================== We've gone through the most command and simple use cases for django-selectable. Now -we'll take a lot at some of the more advanced features of this project. This assumes +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. @@ -16,7 +16,7 @@ The basic lookup is based on handling a search based on a single term string. If additional filtering is needed it can be inside the lookup ``get_query`` but you would need to define this when the lookup is defined. While this fits a fair number of use cases there are times when you need to define additional query -parameters that won't be know until the either the form is bound or until selections +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. @@ -24,8 +24,8 @@ cases. How Parameters are Passed _______________________________________ -As with the search term the additional parameters you define will be passed in -``request.GET``. Since ``get_query`` gets the current request so you will have access to +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. @@ -63,7 +63,7 @@ most common way to use this would be in the form ``__init__``. 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 -know when the form is defined or when an instance of the form is created. +known when the form is defined or when an instance of the form is created. .. _client-side-parameters: @@ -109,28 +109,90 @@ to write a little javascript. Suppose we have city model - .. literalinclude:: ../example/core/models.py - :pyobject: City + .. 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 - .. literalinclude:: ../example/core/forms.py - :pyobject: ChainedForm + .. 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: - .. literalinclude:: ../example/core/templates/advanced.html - :language: html - :start-after: {% block extra-js %} - :end-before: {% endblock %} - - -Then in our lookup we will grab the state value and filter our results on it: + .. code-block:: html - .. literalinclude:: ../example/core/lookups.py - :pyobject: CityLookup + And that's it! We now have a working chained selection example. The full source is included in the example project. @@ -153,12 +215,6 @@ expose the events defined by the plugin. - djselectableclose - djselectablechange -.. note:: - - Prior to v0.7 these event names were under the ``autocomplete`` namespace. If you - are upgrading from a previous version and had customizations using these events - you should be sure to update the names. - 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 `_. @@ -230,7 +286,7 @@ then you can make django-selectable work by passing ``bindSelectables`` to the Currently you must include the django-selectable javascript below this formset initialization -code for this to work. See django-selectable `issue #31 `_ +code for this to work. See django-selectable `issue #31 `_ for some additional detail on this problem. @@ -250,18 +306,9 @@ The item is a dictionary object matching what is returned by the lookup's :ref:`format_item `. ``formatLabel`` should return the string which should be used for the label. -.. note:: - - In v0.7 the scope of ``formatLabel`` was updated so that ``this`` refers to the - current ``djselectable`` plugin instance. Previously ``this`` refered to the - plugin ``options`` instance. - Going back to the ``CityLookup`` we can adjust the label to wrap the city and state portions with their own classes for additional styling: - .. literalinclude:: ../example/core/lookups.py - :pyobject: CityLookup - .. code-block:: html \ No newline at end of file + diff --git a/dep/django-selectable/docs/conf.py b/dep/django-selectable/docs/conf.py index 19349e43..73438490 100644 --- a/dep/django-selectable/docs/conf.py +++ b/dep/django-selectable/docs/conf.py @@ -11,7 +11,9 @@ # 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 @@ -41,16 +43,16 @@ master_doc = 'index' # General information about the project. project = u'Django-Selectable' -copyright = u'2011-2013, Mark Lavin' +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 = '0.7' +version = '.'.join(selectable.__version__.split('.')[0:2]) # The full version, including alpha/beta/rc tags. -release = '0.7.0' +release = selectable.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/dep/django-selectable/docs/contribute.rst b/dep/django-selectable/docs/contribute.rst index 513b981f..67bdba5a 100644 --- a/dep/django-selectable/docs/contribute.rst +++ b/dep/django-selectable/docs/contribute.rst @@ -12,10 +12,10 @@ you can add to our example project. Getting the Source -------------------------------------- -The source code is hosted on `Bitbucket `_. -You can download the full source by cloning the hg repo:: +The source code is hosted on `Github `_. +You can download the full source by cloning the git repo:: - hg clone https://bitbucket.org/mlavin/django-selectable + 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. @@ -24,7 +24,7 @@ be helpful for other then please submit a pull request to have it merged in. Submit an Issue -------------------------------------- -The issues are also managed on `Bitbucket issue page `_. +The issues are also managed on `Github issue page `_. 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. @@ -58,28 +58,26 @@ against multiple versions of Django/Python. With tox installed you can run:: to run all the version combinations. You can also run tox against a subset of supported environments:: - tox -e py26-1.4.X + tox -e py27-django15 -This example will run the test against the latest 1.5.X, 1.4.X, and 1.3.X releases -using Python 2.6 and 3.2 for 1.5+. For more information on running/installing tox please see the +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 `_. They can be found in ``selectable/tests/qunit/index.html``. The test suite also uses -`Grunt `_ and `PhantomJS `_ to -run the tests. You can install Grunt and PhantomJS from NPM:: +`PhantomJS `_ to +run the tests. You can install PhantomJS from NPM:: - # Install grunt requirements - npm install -g grunt phantomjs jshint - # Execute default grunt tasks - grunt + # Install requirements + npm install -g phantomjs jshint + make test-js Building the Documentation -------------------------------------- The documentation is built using `Sphinx `_ -and available on `Read the Docs `_. With +and available on `Read the Docs `_. With Sphinx installed you can build the documentation by running:: make html diff --git a/dep/django-selectable/docs/fields.rst b/dep/django-selectable/docs/fields.rst index 3b5ad438..60cae8ff 100644 --- a/dep/django-selectable/docs/fields.rst +++ b/dep/django-selectable/docs/fields.rst @@ -1,7 +1,7 @@ Fields ========== -Django-Selectable defines a number of fields for selecting either single or mutliple +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 @@ -20,18 +20,19 @@ Field tied to :ref:`AutoCompleteSelectWidget` to bind the selection to the form 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. - .. literalinclude:: ../example/core/forms.py - :start-after: # AutoCompleteSelectField (no new items) - :end-before: # AutoCompleteSelectField (allows new items) + .. code-block:: python + from django import forms -.. versionadded:: 0.7 + from selectable.forms import AutoCompleteSelectField -`lookup_class`` may also be a dotted path. + from .lookups import FruitLookup - .. code-block:: python - selectable.AutoCompleteSelectField(lookup_class='core.lookups.FruitLookup') + class FruitSelectionForm(forms.Form): + fruit = AutoCompleteSelectField(lookup_class=FruitLookup, label='Select a fruit') + +`lookup_class`` may also be a dotted path. .. _AutoCompleteSelectMultipleField: @@ -43,6 +44,16 @@ Field tied to :ref:`AutoCompleteSelectMultipleWidget` to bind the selection to t This field cleans to a list of items. :ref:`AutoCompleteSelectMultipleField` does not allow for the creation of new items. - .. literalinclude:: ../example/core/forms.py - :start-after: # AutoCompleteSelectMultipleField - :end-before: # AutoComboboxSelectMultipleField + + .. 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') diff --git a/dep/django-selectable/docs/lookups.rst b/dep/django-selectable/docs/lookups.rst index 20ba1c0d..5bb7cda5 100644 --- a/dep/django-selectable/docs/lookups.rst +++ b/dep/django-selectable/docs/lookups.rst @@ -25,7 +25,7 @@ you must register in with django-selectable. All lookups must extend from class MyLookup(LookupBase): def get_query(self, request, term): data = ['Foo', 'Bar'] - return filter(lambda x: x.startswith(term), data) + return [x for x in data if x.startswith(term)] registry.register(MyLookup) @@ -139,24 +139,14 @@ than jQuery UI. Most users will not need to override these methods. 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 `_ + `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 `_ + :return: The current `Page object `_ of results. -.. _lookup-serialize-results: - -.. py:method:: LookupBase.serialize_results(self, results) - - Returns serialized results for sending via http. You may choose to override - this if you are making use of - - :param results: a python structure to be serialized e.g. the one returned by :ref:`format_results` - :returns: JSON string. - .. _ModelLookup: @@ -167,11 +157,24 @@ Perhaps the most common use case is to define a lookup based on a given Django m For this you can extend ``selectable.base.ModelLookup``. To extend ``ModelLookup`` you should set two class attributes: ``model`` and ``search_fields``. - .. literalinclude:: ../example/core/lookups.py - :pyobject: FruitLookup + .. 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 `_. +`field lookup syntax `_. 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 @@ -183,7 +186,7 @@ User Lookup Example -------------------------------------- Below is a larger model lookup example using multiple search fields, filters -and display options for the `auth.User `_ +and display options for the `auth.User `_ model. .. code-block:: python @@ -218,8 +221,6 @@ model. Lookup Decorators -------------------------------------- -.. versionadded:: 0.5 - 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 diff --git a/dep/django-selectable/docs/quick-start.rst b/dep/django-selectable/docs/quick-start.rst index 7d14927c..d5a570d7 100644 --- a/dep/django-selectable/docs/quick-start.rst +++ b/dep/django-selectable/docs/quick-start.rst @@ -7,8 +7,8 @@ The workflow for using `django-selectable` involves two main parts: 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 `_ -and `using forms `_. +`defining models `_ +and `using forms `_. .. _start-include-jquery: @@ -16,11 +16,11 @@ Including jQuery & jQuery UI -------------------------------------- The widgets in django-selectable define the media they need as described in the -Django documentation on `Form Media `_. +Django documentation on `Form 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 `_. +please check out the `Django documentation `_. 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 @@ -31,17 +31,17 @@ the from the `Google CDN `_ >= 1.4.4 and -`jQuery UI `_ >= 1.8.13. +Django-Selectable should work with `jQuery `_ >= 1.9 and +`jQuery UI `_ >= 1.10. You must also include a `jQuery UI theme `_ stylesheet. There is also a template tag to easily add this style sheet from the Google CDN. @@ -51,13 +51,13 @@ is also a template tag to easily add this style sheet from the Google CDN. {% load selectable_tags %} {% include_ui_theme %} -By default this will use the `base `_ theme for jQuery UI v1.8.23. +By default this will use the `base `_ 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.8.13' %} + {% include_ui_theme 'ui-lightness' '1.11.4' %} Or only change the theme. @@ -72,11 +72,11 @@ Of course you can choose to include these rescources manually:: .. code-block:: html - - - - - + + + + + .. note:: @@ -91,17 +91,41 @@ 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: - .. literalinclude:: ../example/core/models.py - :pyobject: Fruit + .. 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: - .. literalinclude:: ../example/core/lookups.py - :pyobject: FruitLookup + .. 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 `_ +This syntax should look familiar as it is the same as the `field lookup syntax `_ for making queries in Django. Below this definition we will register our lookup class. @@ -128,9 +152,22 @@ Defining Forms Now that we have a working lookup we will define a form which uses it: - .. literalinclude:: ../example/core/forms.py - :pyobject: FruitForm - :end-before: newautocomplete + .. 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 diff --git a/dep/django-selectable/docs/releases.rst b/dep/django-selectable/docs/releases.rst index 925ebd66..6acd244b 100644 --- a/dep/django-selectable/docs/releases.rst +++ b/dep/django-selectable/docs/releases.rst @@ -1,10 +1,69 @@ Release Notes ================== -v0.7.1 (Released TBD) + +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) @@ -210,7 +269,7 @@ _________________ Bug Fixes _________________ -- Fixed issue with Enter key removing items from select multiple widgets `#24 `_ +- Fixed issue with Enter key removing items from select multiple widgets `#24 `_ Backwards Incompatible Changes @@ -241,7 +300,7 @@ v0.1.2 (Released 2011-05-25) Bug Fixes _________________ -- Fixed issue `#17 `_ +- Fixed issue `#17 `_ v0.1.1 (Release 2011-03-21) diff --git a/dep/django-selectable/docs/settings.rst b/dep/django-selectable/docs/settings.rst index ea20475a..80cf04b4 100644 --- a/dep/django-selectable/docs/settings.rst +++ b/dep/django-selectable/docs/settings.rst @@ -16,8 +16,6 @@ the query string to retrive more values. Default: ``25`` -.. versionadded:: 0.6 - .. _SELECTABLE_ESCAPED_KEYS: SELECTABLE_ESCAPED_KEYS diff --git a/dep/django-selectable/docs/testing.rst b/dep/django-selectable/docs/testing.rst index 37ab83a8..234b4ac2 100644 --- a/dep/django-selectable/docs/testing.rst +++ b/dep/django-selectable/docs/testing.rst @@ -8,7 +8,7 @@ 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 `_ module and -Django's `testing guide `_. +Django's `testing guide `_. Testing Forms with django-selectable @@ -17,7 +17,7 @@ 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 `_. +`MultiWidgets `_. The effect of this is that there are two names in the post rather than one. Take the below form for example. @@ -100,15 +100,19 @@ 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 `_, +`test client `_, it is slightly easier to use the -`request factory `_. +`request factory `_. A simple example is given below. .. code-block:: python diff --git a/dep/django-selectable/docs/widgets.rst b/dep/django-selectable/docs/widgets.rst index b9510c90..22354386 100644 --- a/dep/django-selectable/docs/widgets.rst +++ b/dep/django-selectable/docs/widgets.rst @@ -9,8 +9,6 @@ additional query parameters to the lookup search. See the section on :ref:`Adding Parameters on the Server Side ` for more information. -.. versionadded:: 0.7 - 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 `_ as well as the @@ -50,7 +48,7 @@ This widget should be used in conjunction with the :ref:`AutoCompleteSelectField 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 `_. +`ModelChoiceField `_. You can simply replace the widget without replacing the entire field. .. code-block:: python @@ -65,8 +63,6 @@ You can simply replace the widget without replacing the entire field. The one catch is that you must use ``allow_new=False`` which is the default. -.. versionadded:: 0.7 - ``lookup_class`` may also be a dotted path. .. code-block:: python @@ -82,7 +78,7 @@ AutoComboboxSelectWidget Similar to :ref:`AutoCompleteSelectWidget` but has a button to reveal all options. :ref:`AutoComboboxSelectWidget` works directly with Django's -`ModelChoiceField `_. +`ModelChoiceField `_. You can simply replace the widget without replacing the entire field. .. code-block:: python diff --git a/dep/django-selectable/runtests.py b/dep/django-selectable/runtests.py index dd91652d..82dc1590 100644 --- a/dep/django-selectable/runtests.py +++ b/dep/django-selectable/runtests.py @@ -13,19 +13,25 @@ if not settings.configured: 'NAME': ':memory:', } }, + MIDDLEWARE_CLASSES=(), INSTALLED_APPS=( 'selectable', ), SITE_ID=1, 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', ] @@ -35,4 +41,3 @@ def runtests(): if __name__ == '__main__': runtests() - diff --git a/dep/django-selectable/selectable/__init__.py b/dep/django-selectable/selectable/__init__.py index b4232fc2..0412b9b0 100644 --- a/dep/django-selectable/selectable/__init__.py +++ b/dep/django-selectable/selectable/__init__.py @@ -1,6 +1,6 @@ "Auto-complete selection widgets using Django and jQuery UI." -__version__ = '0.9.0' +__version__ = '1.1.0' default_app_config = 'selectable.apps.SelectableConfig' diff --git a/dep/django-selectable/selectable/apps.py b/dep/django-selectable/selectable/apps.py index f65c5e1a..579bf781 100644 --- a/dep/django-selectable/selectable/apps.py +++ b/dep/django-selectable/selectable/apps.py @@ -1,7 +1,4 @@ -try: - from django.apps import AppConfig -except ImportError: - AppConfig = object +from django.apps import AppConfig class SelectableConfig(AppConfig): diff --git a/dep/django-selectable/selectable/base.py b/dep/django-selectable/selectable/base.py index 8335ddd7..bc74204c 100644 --- a/dep/django-selectable/selectable/base.py +++ b/dep/django-selectable/selectable/base.py @@ -1,7 +1,6 @@ "Base classes for lookup creation." from __future__ import unicode_literals -import json import operator import re from functools import reduce @@ -9,13 +8,12 @@ from functools import reduce from django.conf import settings from django.core.paginator import Paginator, InvalidPage, EmptyPage from django.core.urlresolvers import reverse -from django.core.serializers.json import DjangoJSONEncoder -from django.http import HttpResponse -from django.db.models import Q +from django.http import JsonResponse +from django.db.models import Q, Model +from django.utils.encoding import smart_text from django.utils.html import conditional_escape from django.utils.translation import ugettext as _ -from selectable.compat import smart_text from selectable.forms import BaseLookupForm @@ -25,14 +23,6 @@ __all__ = ( ) -class JsonResponse(HttpResponse): - "HttpResponse subclass for returning JSON data." - - def __init__(self, *args, **kwargs): - kwargs['content_type'] = 'application/json' - super(JsonResponse, self).__init__(*args, **kwargs) - - class LookupBase(object): "Base class for all django-selectable lookups." @@ -100,8 +90,7 @@ class LookupBase(object): term = options.get('term', '') raw_data = self.get_query(request, term) results = self.format_results(raw_data, options) - content = self.serialize_results(results) - return self.response(content) + return self.response(results) def format_results(self, raw_data, options): ''' @@ -123,10 +112,6 @@ class LookupBase(object): results['meta'] = meta return results - def serialize_results(self, results): - "Returns serialized results for sending via http." - return json.dumps(results, cls=DjangoJSONEncoder, ensure_ascii=False) - class ModelLookup(LookupBase): "Lookup class for easily defining lookups based on Django models." @@ -146,10 +131,7 @@ class ModelLookup(LookupBase): return qs def get_queryset(self): - try: - qs = self.model._default_manager.get_queryset() - except AttributeError: # Django <= 1.5. - qs = self.model._default_manager.get_query_set() + qs = self.model._default_manager.get_queryset() if self.filters: qs = qs.filter(**self.filters) return qs @@ -160,6 +142,7 @@ class ModelLookup(LookupBase): 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): diff --git a/dep/django-selectable/selectable/compat.py b/dep/django-selectable/selectable/compat.py dissimilarity index 82% index a63ab6a1..448a7982 100644 --- a/dep/django-selectable/selectable/compat.py +++ b/dep/django-selectable/selectable/compat.py @@ -1,36 +1,6 @@ -"Compatibility utilites for Python/Django versions." -import sys - -try: - from urllib.parse import urlparse -except ImportError: - from urlparse import urlparse - -try: - from django.utils.encoding import smart_text, force_text -except ImportError: - from django.utils.encoding import smart_unicode as smart_text - from django.utils.encoding import force_unicode as force_text - -try: - from django.forms.utils import flatatt -except ImportError: - from django.forms.util import flatatt - -PY3 = sys.version_info[0] == 3 - -if PY3: - string_types = str, -else: - string_types = basestring, - -try: - from importlib import import_module -except ImportError: - from django.utils.importlib import import_module - -try: - from django.apps import AppConfig - LEGACY_AUTO_DISCOVER = False -except ImportError: - LEGACY_AUTO_DISCOVER = True +"Compatibility utilites for Python/Django versions." + +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse diff --git a/dep/django-selectable/selectable/forms/base.py b/dep/django-selectable/selectable/forms/base.py index c39c2f73..b2abb0de 100644 --- a/dep/django-selectable/selectable/forms/base.py +++ b/dep/django-selectable/selectable/forms/base.py @@ -1,9 +1,10 @@ from __future__ import unicode_literals +from importlib import import_module + from django import forms from django.conf import settings - -from selectable.compat import string_types, import_module +from django.utils.six import string_types __all__ = ( diff --git a/dep/django-selectable/selectable/forms/fields.py b/dep/django-selectable/selectable/forms/fields.py index 21109c41..6336cb12 100644 --- a/dep/django-selectable/selectable/forms/fields.py +++ b/dep/django-selectable/selectable/forms/fields.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django import forms +from django import forms, VERSION as DJANGO_VERSION from django.core.exceptions import ValidationError from django.core.validators import EMPTY_VALUES from django.utils.translation import ugettext_lazy as _ @@ -26,7 +26,7 @@ def model_vars(obj): class BaseAutoCompleteField(forms.Field): - def _has_changed(self, initial, data): + 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 @@ -41,6 +41,11 @@ class BaseAutoCompleteField(forms.Field): else: return data != initial + if DJANGO_VERSION < (1, 8): + def _has_changed(self, initial, data): + return self.has_changed(initial, data) + + class AutoCompleteSelectField(BaseAutoCompleteField): widget = AutoCompleteSelectWidget diff --git a/dep/django-selectable/selectable/forms/widgets.py b/dep/django-selectable/selectable/forms/widgets.py index f23332c6..5edc0c7d 100644 --- a/dep/django-selectable/selectable/forms/widgets.py +++ b/dep/django-selectable/selectable/forms/widgets.py @@ -1,14 +1,16 @@ from __future__ import unicode_literals +import inspect import json -from django import forms, VERSION as DJANGO_VERSION +from django import forms from django.conf import settings +from django.forms.utils import flatatt +from django.utils.encoding import force_text from django.utils.http import urlencode from django.utils.safestring import mark_safe from selectable import __version__ -from selectable.compat import force_text, flatatt from selectable.forms.base import import_lookup_class __all__ = ( @@ -33,7 +35,59 @@ class SelectableMediaMixin(object): js = ('%sjs/jquery.dj.selectable.js?v=%s' % (STATIC_PREFIX, __version__),) -class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin): +new_style_build_attrs = ( + 'base_attrs' in + inspect.getargs(forms.widgets.Widget.build_attrs.__code__).args) + + +class BuildAttrsCompat(object): + """ + Mixin to provide compatibility between old and new function + signatures for Widget.build_attrs, and a hook for adding our + own attributes. + """ + # These are build_attrs definitions that make it easier for + # us to override, without having to worry about the signature, + # by adding a standard hook, `build_attrs_extra`. + # It has a different signature when we are running different Django + # versions. + if new_style_build_attrs: + def build_attrs(self, base_attrs, extra_attrs=None): + attrs = super(BuildAttrsCompat, self).build_attrs( + base_attrs, extra_attrs=extra_attrs) + return self.build_attrs_extra(attrs) + else: + def build_attrs(self, extra_attrs=None, **kwargs): + attrs = super(BuildAttrsCompat, self).build_attrs( + extra_attrs=extra_attrs, **kwargs) + return self.build_attrs_extra(attrs) + + def build_attrs_extra(self, attrs): + # Default implementation, does nothing + return attrs + + # These provide a standard interface for when we want to call build_attrs + # in our own `render` methods. In both cases it is the same as the Django + # 1.11 signature, but has a different implementation for different Django + # versions. + if new_style_build_attrs: + def build_attrs_compat(self, base_attrs, extra_attrs=None): + return self.build_attrs(base_attrs, extra_attrs=extra_attrs) + + else: + def build_attrs_compat(self, base_attrs, extra_attrs=None): + # Implementation copied from Django 1.11, plus include our + # hook `build_attrs_extra` + attrs = base_attrs.copy() + if extra_attrs is not None: + attrs.update(extra_attrs) + return self.build_attrs_extra(attrs) + + +CompatMixin = BuildAttrsCompat + + +class AutoCompleteWidget(CompatMixin, forms.TextInput, SelectableMediaMixin): def __init__(self, lookup_class, *args, **kwargs): self.lookup_class = import_lookup_class(lookup_class) @@ -45,8 +99,8 @@ class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin): def update_query_parameters(self, qs_dict): self.qs.update(qs_dict) - def build_attrs(self, extra_attrs=None, **kwargs): - attrs = super(AutoCompleteWidget, self).build_attrs(extra_attrs, **kwargs) + def build_attrs_extra(self, attrs): + attrs = super(AutoCompleteWidget, self).build_attrs_extra(attrs) url = self.lookup_class.url() if self.limit and 'limit' not in self.qs: self.qs['limit'] = self.limit @@ -60,20 +114,11 @@ class AutoCompleteWidget(forms.TextInput, SelectableMediaMixin): return attrs -class SelectableMultiWidget(forms.MultiWidget): +class SelectableMultiWidget(CompatMixin, forms.MultiWidget): def update_query_parameters(self, qs_dict): self.widgets[0].update_query_parameters(qs_dict) - if DJANGO_VERSION < (1, 6): - def _has_changed(self, initial, data): - "Detects if the widget was changed. This is removed in Django 1.6." - if initial is None and data is None: - return False - if data and not hasattr(data, '__iter__'): - data = self.decompress(data) - return super(SelectableMultiWidget, self)._has_changed(initial, data) - def decompress(self, value): if value: lookup = self.lookup_class() @@ -144,8 +189,8 @@ class AutoCompleteSelectWidget(_BaseSingleSelectWidget): class AutoComboboxWidget(AutoCompleteWidget, SelectableMediaMixin): - def build_attrs(self, extra_attrs=None, **kwargs): - attrs = super(AutoComboboxWidget, self).build_attrs(extra_attrs, **kwargs) + def build_attrs_extra(self, attrs): + attrs = super(AutoComboboxWidget, self).build_attrs_extra(attrs) attrs['data-selectable-type'] = 'combobox' return attrs @@ -155,40 +200,75 @@ class AutoComboboxSelectWidget(_BaseSingleSelectWidget): primary_widget = AutoComboboxWidget -class LookupMultipleHiddenInput(forms.MultipleHiddenInput): +class LookupMultipleHiddenInput(CompatMixin, forms.MultipleHiddenInput): def __init__(self, lookup_class, *args, **kwargs): self.lookup_class = import_lookup_class(lookup_class) super(LookupMultipleHiddenInput, self).__init__(*args, **kwargs) + # This supports Django 1.11 and later + 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 + + # This supports Django 1.10 and earlier def render(self, name, value, attrs=None, choices=()): lookup = self.lookup_class() - if value is None: value = [] - final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) - id_ = final_attrs.get('id', None) + value = self._normalize_value(value) + + base_attrs = dict(self.attrs, type=self.input_type, name=name) + combined_attrs = self.build_attrs_compat(base_attrs, attrs) + id_ = combined_attrs.get('id', None) inputs = [] - model = getattr(self.lookup_class, 'model', None) for i, v in enumerate(value): - item = None - if model and isinstance(v, model): - item = v - v = lookup.get_item_id(item) - input_attrs = dict(value=force_text(v), **final_attrs) + input_attrs = combined_attrs.copy() + v_, title = self._lookup_value_and_title(lookup, v) + input_attrs.update( + value=v_, + title=title, + ) if id_: # An ID attribute was given. Add a numeric index as a suffix # so that the inputs don't all have the same ID attribute. input_attrs['id'] = '%s_%s' % (id_, i) - if v: - item = item or lookup.get_item(v) - input_attrs['title'] = lookup.get_item_value(item) inputs.append('' % flatatt(input_attrs)) return mark_safe('\n'.join(inputs)) - def build_attrs(self, extra_attrs=None, **kwargs): - attrs = super(LookupMultipleHiddenInput, self).build_attrs(extra_attrs, **kwargs) + # These are used by both paths + def build_attrs_extra(self, attrs): + attrs = super(LookupMultipleHiddenInput, self).build_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): """ @@ -225,23 +305,18 @@ class _BaseMultipleSelectWidget(SelectableMultiWidget, SelectableMediaMixin): value = self.get_compatible_postdata(data, name) return value + def build_attrs_extra(self, attrs): + attrs = super(_BaseMultipleSelectWidget, self).build_attrs_extra(attrs) + if 'required' in attrs: + attrs.pop('required') + return attrs + def render(self, name, value, attrs=None): if value and not hasattr(value, '__iter__'): value = [value] value = ['', value] return super(_BaseMultipleSelectWidget, self).render(name, value, attrs) - if DJANGO_VERSION < (1, 6): - def _has_changed(self, initial, data): - """" - Detects if the widget was changed. This is removed in Django 1.6. - - For the multi-select case we only care if the hidden inputs changed. - """ - initial = ['', initial] - data = ['', data] - return super(_BaseMultipleSelectWidget, self)._has_changed(initial, data) - class AutoCompleteSelectMultipleWidget(_BaseMultipleSelectWidget): diff --git a/dep/django-selectable/selectable/registry.py b/dep/django-selectable/selectable/registry.py index be1af0b8..5a475af7 100644 --- a/dep/django-selectable/selectable/registry.py +++ b/dep/django-selectable/selectable/registry.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals -from selectable.base import LookupBase, ModelLookup -from selectable.compat import force_text +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) @@ -37,26 +39,5 @@ registry = LookupRegistry() def autodiscover(): - - import copy - from django.conf import settings - - try: - from django.utils.module_loading import autodiscover_modules - except ImportError: - from django.utils.importlib import import_module - from django.utils.module_loading import module_has_submodule - - def autodiscover_modules(submod, **kwargs): - for app_name in settings.INSTALLED_APPS: - mod = import_module(app_name) - try: - before_import_registry = copy.copy(registry._registry) - import_module('%s.lookups' % app_name) - except: - registry._registry = before_import_registry - if module_has_submodule(mod, 'lookups'): - raise - # Attempt to import the app's lookups module. autodiscover_modules('lookups', register_to=registry) diff --git a/dep/django-selectable/selectable/static/selectable/js/jquery.dj.selectable.js b/dep/django-selectable/selectable/static/selectable/js/jquery.dj.selectable.js index 1de6c533..c16cc64d 100644 --- a/dep/django-selectable/selectable/static/selectable/js/jquery.dj.selectable.js +++ b/dep/django-selectable/selectable/static/selectable/js/jquery.dj.selectable.js @@ -71,7 +71,8 @@ icons: { primary: this.options.removeIcon }, - text: false + text: false, + disabled: this.disabled }, button = $('') .attr('href', '#') @@ -131,7 +132,8 @@ icons: { primary: this.options.comboboxIcon }, - text: false + text: false, + disabled: this.disabled }, button = $("") .html(" ") @@ -157,6 +159,7 @@ 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(""); diff --git a/dep/django-selectable/selectable/templatetags/selectable_tags.py b/dep/django-selectable/selectable/templatetags/selectable_tags.py index faa4f938..c380e530 100755 --- a/dep/django-selectable/selectable/templatetags/selectable_tags.py +++ b/dep/django-selectable/selectable/templatetags/selectable_tags.py @@ -1,17 +1,15 @@ from __future__ import unicode_literals from django import template -from django.conf import settings - register = template.Library() @register.inclusion_tag('selectable/jquery-js.html') -def include_jquery_libs(version='1.7.2', ui='1.8.23'): +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='base', version='1.8.23'): +def include_ui_theme(theme='smoothness', version='1.11.4'): return {'theme': theme, 'version': version} diff --git a/dep/django-selectable/selectable/tests/__init__.py b/dep/django-selectable/selectable/tests/__init__.py index afac57e9..3227bb50 100644 --- a/dep/django-selectable/selectable/tests/__init__.py +++ b/dep/django-selectable/selectable/tests/__init__.py @@ -13,6 +13,9 @@ class Thing(models.Model): def __str__(self): return self.name + class Meta: + ordering = ['id'] + @python_2_unicode_compatible class OtherThing(models.Model): @@ -38,13 +41,3 @@ class ThingLookup(ModelLookup): registry.register(ThingLookup) - - -from .test_base import * -from .test_decorators import * -from .test_fields import * -from .test_functional import * -from .test_forms import * -from .test_templatetags import * -from .test_views import * -from .test_widgets import * diff --git a/dep/django-selectable/selectable/tests/base.py b/dep/django-selectable/selectable/tests/base.py index fdb4f169..c77c9610 100644 --- a/dep/django-selectable/selectable/tests/base.py +++ b/dep/django-selectable/selectable/tests/base.py @@ -2,13 +2,14 @@ from __future__ import unicode_literals import random import string +from collections import defaultdict from xml.dom.minidom import parseString -from django.conf import settings -from django.test import TestCase -from ..base import ModelLookup +from django.test import TestCase, override_settings + from . import Thing +from ..base import ModelLookup def as_xml(html): @@ -28,22 +29,8 @@ def parsed_inputs(html): return inputs -class PatchSettingsMixin(object): - def setUp(self): - super(PatchSettingsMixin, self).setUp() - self.is_limit_set = hasattr(settings, 'SELECTABLE_MAX_LIMIT') - if self.is_limit_set: - self.original_limit = settings.SELECTABLE_MAX_LIMIT - settings.SELECTABLE_MAX_LIMIT = 25 - - def tearDown(self): - super(PatchSettingsMixin, self).tearDown() - if self.is_limit_set: - settings.SELECTABLE_MAX_LIMIT = self.original_limit - - +@override_settings(ROOT_URLCONF='selectable.tests.urls') class BaseSelectableTestCase(TestCase): - urls = 'selectable.tests.urls' def get_random_string(self, length=10): return ''.join(random.choice(string.ascii_letters) for x in range(length)) @@ -60,4 +47,51 @@ class BaseSelectableTestCase(TestCase): class SimpleModelLookup(ModelLookup): model = Thing - search_fields = ('name__icontains', ) \ No newline at end of file + 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 = as_xml(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.items(): + 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.childNodes: + for item in _walk_nodes(child): + yield item diff --git a/dep/django-selectable/selectable/tests/qunit/jquery-loader.js b/dep/django-selectable/selectable/tests/qunit/jquery-loader.js index 51160509..84ee0a98 100644 --- a/dep/django-selectable/selectable/tests/qunit/jquery-loader.js +++ b/dep/django-selectable/selectable/tests/qunit/jquery-loader.js @@ -3,8 +3,8 @@ var jqversion = location.search.match(/[?&]jquery=(.*?)(?=&|$)/); var uiversion = location.search.match(/[?&]ui=(.*?)(?=&|$)/); var path; - window.jqversion = jqversion && jqversion[1] || '1.7.2'; - window.uiversion = uiversion && uiversion[1] || '1.8.24'; + 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! diff --git a/dep/django-selectable/selectable/tests/qunit/test-methods.js b/dep/django-selectable/selectable/tests/qunit/test-methods.js index 7b6086fa..6720dd40 100644 --- a/dep/django-selectable/selectable/tests/qunit/test-methods.js +++ b/dep/django-selectable/selectable/tests/qunit/test-methods.js @@ -2,11 +2,16 @@ define(['selectable'], function ($) { - var expectedNamespace = 'djselectable'; + var expectedNamespace = 'djselectable', + useData = true; if (window.uiversion.lastIndexOf('1.10', 0) === 0) { // jQuery UI 1.10 introduces a namespace change to include ui-prefix expectedNamespace = 'ui-' + expectedNamespace; } + if (window.uiversion.lastIndexOf('1.11', 0) === 0) { + // jQuery UI 1.11 introduces an instance method to get the current instance + useData = false; + } module("Autocomplete Text Methods Tests"); @@ -16,7 +21,11 @@ define(['selectable'], function ($) { $('#qunit-fixture').append(input); bindSelectables('#qunit-fixture'); ok(input.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget"); - ok(input.data(expectedNamespace), "input should be bound with djselecable widget"); + if (useData) { + ok(input.data(expectedNamespace), "input should be bound with djselecable widget"); + } else { + ok(input.djselectable('instance'), "input should be bound with djselecable widget"); + } }); test("Manual Selection", function () { @@ -48,7 +57,11 @@ define(['selectable'], function ($) { bindSelectables('#qunit-fixture'); button = $('.ui-combo-button', '#qunit-fixture'); ok(input.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget"); - ok(input.data(expectedNamespace), "input should be bound with djselecable widget"); + if (useData) { + ok(input.data(expectedNamespace), "input should be bound with djselecable widget"); + } else { + ok(input.djselectable('instance'), "input should be bound with djselecable widget"); + } equal(button.length, 1, "combobox button should be created"); }); @@ -81,7 +94,11 @@ define(['selectable'], function ($) { $('#qunit-fixture').append(hiddenInput); bindSelectables('#qunit-fixture'); ok(textInput.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget"); - ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget"); + if (useData) { + ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget"); + } else { + ok(textInput.djselectable('instance'), "input should be bound with djselecable widget"); + } }); test("Manual Selection", function () { @@ -121,7 +138,11 @@ define(['selectable'], function ($) { bindSelectables('#qunit-fixture'); button = $('.ui-combo-button', '#qunit-fixture'); ok(textInput.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget"); - ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget"); + if (useData) { + ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget"); + } else { + ok(textInput.djselectable('instance'), "input should be bound with djselecable widget"); + } equal(button.length, 1, "combobox button should be created"); }); @@ -161,7 +182,11 @@ define(['selectable'], function ($) { bindSelectables('#qunit-fixture'); deck = $('.selectable-deck', '#qunit-fixture'); ok(textInput.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget"); - ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget"); + if (useData) { + ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget"); + } else { + ok(textInput.djselectable('instance'), "input should be bound with djselecable widget"); + } equal($('li', deck).length, 0, "no initial deck items"); }); @@ -204,7 +229,11 @@ define(['selectable'], function ($) { deck = $('.selectable-deck', '#qunit-fixture'); button = $('.ui-combo-button', '#qunit-fixture'); ok(textInput.hasClass('ui-autocomplete-input'), "input should be bound with djselecable widget"); - ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget"); + if (useData) { + ok(textInput.data(expectedNamespace), "input should be bound with djselecable widget"); + } else { + ok(textInput.djselectable('instance'), "input should be bound with djselecable widget"); + } equal($('li', deck).length, 0, "no initial deck items"); equal(button.length, 1, "combobox button should be created"); }); diff --git a/dep/django-selectable/selectable/tests/test_fields.py b/dep/django-selectable/selectable/tests/test_fields.py index e962003e..24d12cb6 100644 --- a/dep/django-selectable/selectable/tests/test_fields.py +++ b/dep/django-selectable/selectable/tests/test_fields.py @@ -42,7 +42,7 @@ class FieldTestMixin(object): An invalid lookup_class dotted path should raise an ImportError. """ with self.assertRaises(ImportError): - self.field_cls('this.is.an.invalid.path') + self.field_cls('that.is.an.invalid.path') def test_dotted_path_wrong_type(self): """ diff --git a/dep/django-selectable/selectable/tests/test_forms.py b/dep/django-selectable/selectable/tests/test_forms.py index 7bce96ee..ed6bac90 100644 --- a/dep/django-selectable/selectable/tests/test_forms.py +++ b/dep/django-selectable/selectable/tests/test_forms.py @@ -1,7 +1,5 @@ -from django.conf import settings - from ..forms import BaseLookupForm -from .base import BaseSelectableTestCase, PatchSettingsMixin +from .base import BaseSelectableTestCase __all__ = ( @@ -9,7 +7,7 @@ __all__ = ( ) -class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase): +class BaseLookupFormTestCase(BaseSelectableTestCase): def get_valid_data(self): data = { @@ -39,12 +37,13 @@ class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase): the form will return SELECTABLE_MAX_LIMIT. """ - data = self.get_valid_data() - if 'limit' in data: - del data['limit'] - form = BaseLookupForm(data) - self.assertTrue(form.is_valid(), "%s" % form.errors) - self.assertEqual(form.cleaned_data['limit'], settings.SELECTABLE_MAX_LIMIT) + with self.settings(SELECTABLE_MAX_LIMIT=25): + data = self.get_valid_data() + if 'limit' in data: + del data['limit'] + form = BaseLookupForm(data) + self.assertTrue(form.is_valid(), "%s" % form.errors) + self.assertEqual(form.cleaned_data['limit'], 25) def test_no_max_set(self): """ @@ -52,12 +51,12 @@ class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase): will return the given limit. """ - settings.SELECTABLE_MAX_LIMIT = None - data = self.get_valid_data() - form = BaseLookupForm(data) - self.assertTrue(form.is_valid(), "%s" % form.errors) - if 'limit' in data: - self.assertTrue(form.cleaned_data['limit'], data['limit']) + with self.settings(SELECTABLE_MAX_LIMIT=None): + data = self.get_valid_data() + form = BaseLookupForm(data) + self.assertTrue(form.is_valid(), "%s" % form.errors) + if 'limit' in data: + self.assertTrue(form.cleaned_data['limit'], data['limit']) def test_no_max_set_not_given(self): """ @@ -65,13 +64,13 @@ class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase): will return no limit. """ - settings.SELECTABLE_MAX_LIMIT = None - data = self.get_valid_data() - if 'limit' in data: - del data['limit'] - form = BaseLookupForm(data) - self.assertTrue(form.is_valid(), "%s" % form.errors) - self.assertFalse(form.cleaned_data.get('limit')) + with self.settings(SELECTABLE_MAX_LIMIT=None): + data = self.get_valid_data() + if 'limit' in data: + del data['limit'] + form = BaseLookupForm(data) + self.assertTrue(form.is_valid(), "%s" % form.errors) + self.assertFalse(form.cleaned_data.get('limit')) def test_over_limit(self): """ @@ -79,8 +78,9 @@ class BaseLookupFormTestCase(PatchSettingsMixin, BaseSelectableTestCase): the form will return SELECTABLE_MAX_LIMIT. """ - data = self.get_valid_data() - data['limit'] = settings.SELECTABLE_MAX_LIMIT + 100 - form = BaseLookupForm(data) - self.assertTrue(form.is_valid(), "%s" % form.errors) - self.assertEqual(form.cleaned_data['limit'], settings.SELECTABLE_MAX_LIMIT) + with self.settings(SELECTABLE_MAX_LIMIT=25): + data = self.get_valid_data() + data['limit'] = 125 + form = BaseLookupForm(data) + self.assertTrue(form.is_valid(), "%s" % form.errors) + self.assertEqual(form.cleaned_data['limit'], 25) diff --git a/dep/django-selectable/selectable/tests/test_functional.py b/dep/django-selectable/selectable/tests/test_functional.py index 01bf9bfc..ef393348 100644 --- a/dep/django-selectable/selectable/tests/test_functional.py +++ b/dep/django-selectable/selectable/tests/test_functional.py @@ -8,7 +8,7 @@ from django import forms from ..forms import AutoCompleteSelectField, AutoCompleteSelectMultipleField from ..forms import AutoCompleteSelectWidget, AutoComboboxSelectWidget from . import ManyThing, OtherThing, ThingLookup -from .base import BaseSelectableTestCase, parsed_inputs +from .base import BaseSelectableTestCase __all__ = ( @@ -26,6 +26,7 @@ class OtherThingForm(forms.ModelForm): class Meta(object): model = OtherThing + fields = ('name', 'thing', ) class FuncAutoCompleteSelectTestCase(BaseSelectableTestCase): @@ -77,32 +78,57 @@ class FuncAutoCompleteSelectTestCase(BaseSelectableTestCase): form = OtherThingForm(data=data) self.assertFalse(form.is_valid(), 'Form should not be valid') rendered_form = form.as_p() - inputs = parsed_inputs(rendered_form) # Selected text should be populated - thing_0 = inputs['thing_0'][0] - self.assertEqual(thing_0.attributes['value'].value, self.test_thing.name) + self.assertInHTML( + ''' + + '''.format(self.test_thing.name, + 'required' if hasattr(form, 'use_required_attribute') else ''), + rendered_form + ) # Selected pk should be populated - thing_1 = inputs['thing_1'][0] - self.assertEqual(int(thing_1.attributes['value'].value), self.test_thing.pk) + self.assertInHTML( + ''' + + '''.format(self.test_thing.pk, + 'required' if hasattr(form, 'use_required_attribute') else ''), + rendered_form, + ) def test_populate_from_model(self): "Populate from existing model." other_thing = OtherThing.objects.create(thing=self.test_thing, name='a') form = OtherThingForm(instance=other_thing) rendered_form = form.as_p() - inputs = parsed_inputs(rendered_form) # Selected text should be populated - thing_0 = inputs['thing_0'][0] - self.assertEqual(thing_0.attributes['value'].value, self.test_thing.name) + self.assertInHTML( + ''' + + '''.format(self.test_thing.name, + 'required' if hasattr(form, 'use_required_attribute') else ''), + rendered_form + ) # Selected pk should be populated - thing_1 = inputs['thing_1'][0] - self.assertEqual(int(thing_1.attributes['value'].value), self.test_thing.pk) + self.assertInHTML( + ''' + + '''.format(self.test_thing.pk, + 'required' if hasattr(form, 'use_required_attribute') else ''), + rendered_form + ) class SelectWidgetForm(forms.ModelForm): class Meta(object): model = OtherThing + fields = ('name', 'thing', ) widgets = { 'thing': AutoCompleteSelectWidget(lookup_class=ThingLookup) } @@ -166,6 +192,7 @@ class ComboboxSelectWidgetForm(forms.ModelForm): class Meta(object): model = OtherThing + fields = ('name', 'thing', ) widgets = { 'thing': AutoComboboxSelectWidget(lookup_class=ThingLookup) } @@ -231,6 +258,7 @@ class ManyThingForm(forms.ModelForm): class Meta(object): model = ManyThing + fields = ('name', 'things', ) class FuncManytoManyMultipleSelectTestCase(BaseSelectableTestCase): @@ -316,6 +344,15 @@ class FuncManytoManyMultipleSelectTestCase(BaseSelectableTestCase): form = ManyThingForm(data=data) self.assertTrue(form.is_valid(), str(form.errors)) + def test_render_form(self): + thing_1 = self.create_thing() + manything = ManyThing.objects.create(name='Foo') + manything.things.add(thing_1) + form = ManyThingForm(instance=manything) + rendered = form.as_p() + self.assertIn('title="{0}"'.format(thing_1.name), + rendered) + class SimpleForm(forms.Form): "Non-model form usage." diff --git a/dep/django-selectable/selectable/tests/test_templatetags.py b/dep/django-selectable/selectable/tests/test_templatetags.py index c0042fb0..74af539e 100644 --- a/dep/django-selectable/selectable/tests/test_templatetags.py +++ b/dep/django-selectable/selectable/tests/test_templatetags.py @@ -23,8 +23,8 @@ class JqueryTagTestCase(BaseSelectableTestCase): template = Template("{% load selectable_tags %}{% include_jquery_libs %}") context = Context({}) result = template.render(context) - self.assertJQueryVersion(result, '1.7.2') - self.assertUIVersion(result, '1.8.23') + self.assertJQueryVersion(result, '1.12.4') + self.assertUIVersion(result, '1.11.4') def test_render_jquery_version(self): "Render template tag with specified jQuery version." @@ -82,7 +82,7 @@ class ThemeTagTestCase(BaseSelectableTestCase): template = Template("{% load selectable_tags %}{% include_ui_theme %}") context = Context({}) result = template.render(context) - self.assertUICSS(result, 'base', '1.8.23') + self.assertUICSS(result, 'smoothness', '1.11.4') def test_render_version(self): "Render template tag with alternate version." @@ -90,7 +90,7 @@ class ThemeTagTestCase(BaseSelectableTestCase): context = Context({}) result = template.render(context) self.assertUICSS(result, 'base', '1.8.13') - + def test_variable_version(self): "Render using version from content variable." version = '1.8.13' @@ -104,12 +104,12 @@ class ThemeTagTestCase(BaseSelectableTestCase): template = Template("{% load selectable_tags %}{% include_ui_theme 'ui-lightness' %}") context = Context({}) result = template.render(context) - self.assertUICSS(result, 'ui-lightness', '1.8.23') - + self.assertUICSS(result, 'ui-lightness', '1.11.4') + def test_variable_theme(self): "Render using theme from content variable." theme = 'ui-lightness' template = Template("{% load selectable_tags %}{% include_ui_theme theme %}") context = Context({'theme': theme}) result = template.render(context) - self.assertUICSS(result, theme, '1.8.23') + self.assertUICSS(result, theme, '1.11.4') diff --git a/dep/django-selectable/selectable/tests/test_views.py b/dep/django-selectable/selectable/tests/test_views.py index 4dc20c41..0bb65597 100644 --- a/dep/django-selectable/selectable/tests/test_views.py +++ b/dep/django-selectable/selectable/tests/test_views.py @@ -4,9 +4,10 @@ import json from django.conf import settings from django.core.urlresolvers import reverse +from django.test import override_settings from . import ThingLookup -from .base import BaseSelectableTestCase, PatchSettingsMixin +from .base import BaseSelectableTestCase __all__ = ( @@ -14,7 +15,8 @@ __all__ = ( ) -class SelectableViewTest(PatchSettingsMixin, BaseSelectableTestCase): +@override_settings(SELECTABLE_MAX_LIMIT=25) +class SelectableViewTest(BaseSelectableTestCase): def setUp(self): super(SelectableViewTest, self).setUp() diff --git a/dep/django-selectable/selectable/tests/test_widgets.py b/dep/django-selectable/selectable/tests/test_widgets.py index 58a7d3ad..08e80246 100644 --- a/dep/django-selectable/selectable/tests/test_widgets.py +++ b/dep/django-selectable/selectable/tests/test_widgets.py @@ -3,11 +3,10 @@ import json from django import forms from django.utils.http import urlencode +from . import Thing, ThingLookup from ..compat import urlparse from ..forms import widgets -from . import Thing, ThingLookup -from .base import BaseSelectableTestCase, parsed_inputs - +from .base import BaseSelectableTestCase, parsed_inputs, parsed_widget_attributes __all__ = ( 'AutoCompleteWidgetTestCase', @@ -43,7 +42,7 @@ class WidgetTestMixin(object): An invalid lookup_class dotted path should raise an ImportError. """ with self.assertRaises(ImportError): - self.__class__.widget_cls('this.is.an.invalid.path') + self.__class__.widget_cls('that.is.an.invalid.path') def test_dotted_path_wrong_type(self): """ @@ -58,9 +57,9 @@ class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): widget_cls = widgets.AutoCompleteWidget lookup_cls = ThingLookup - def test_build_attrs(self): + def test_rendered_attrs(self): widget = self.get_widget_instance() - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) self.assertTrue('data-selectable-url' in attrs) self.assertTrue('data-selectable-type' in attrs) self.assertTrue('data-selectable-allow-new' in attrs) @@ -69,15 +68,15 @@ class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): params = {'active': 1} widget = self.get_widget_instance() widget.update_query_parameters(params) - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query self.assertEqual(query, urlencode(params)) - def test_limit_paramter(self): + def test_limit_parameter(self): widget = self.get_widget_instance(limit=10) - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -86,7 +85,7 @@ class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): def test_initial_query_parameters(self): params = {'active': 1} widget = self.get_widget_instance(query_params=params) - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -96,7 +95,7 @@ class AutoCompleteWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): "Serialize selectable options as json in data attribute." options = {'autoFocus': True} widget = self.get_widget_instance(attrs={'data-selectable-options': options}) - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) self.assertTrue('data-selectable-options' in attrs) self.assertEqual(attrs['data-selectable-options'], json.dumps(options)) @@ -115,8 +114,7 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): def test_hidden_type(self): widget = self.get_widget_instance() - sub_widget = widget.widgets[1] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[1]) self.assertTrue('data-selectable-type' in attrs) self.assertEqual(attrs['data-selectable-type'], 'hidden') @@ -124,17 +122,15 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): params = {'active': 1} widget = self.get_widget_instance() widget.update_query_parameters(params) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query self.assertEqual(query, urlencode(params)) - def test_limit_paramter(self): + def test_limit_parameter(self): widget = self.get_widget_instance(limit=10) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -143,8 +139,7 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): def test_initial_query_parameters(self): params = {'active': 1} widget = self.get_widget_instance(query_params=params) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -154,8 +149,7 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): "Serialize selectable options as json in data attribute." options = {'autoFocus': True} widget = self.get_widget_instance(attrs={'data-selectable-options': options}) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) self.assertTrue('data-selectable-options' in attrs) self.assertEqual(attrs['data-selectable-options'], json.dumps(options)) @@ -164,16 +158,16 @@ class AutoCompleteSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): postdata = {'fruit': '1'} widget = self.get_widget_instance() widget_val = widget.value_from_datadict(postdata, [], 'fruit') - self.assertEquals(widget_val, '1') + self.assertEqual(widget_val, '1') class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): widget_cls = widgets.AutoComboboxWidget lookup_cls = ThingLookup - def test_build_attrs(self): + def test_rendered_attrs(self): widget = self.get_widget_instance() - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) self.assertTrue('data-selectable-url' in attrs) self.assertTrue('data-selectable-type' in attrs) self.assertTrue('data-selectable-allow-new' in attrs) @@ -182,15 +176,15 @@ class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): params = {'active': 1} widget = self.get_widget_instance() widget.update_query_parameters(params) - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query self.assertEqual(query, urlencode(params)) - def test_limit_paramter(self): + def test_limit_parameter(self): widget = self.get_widget_instance(limit=10) - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -199,7 +193,7 @@ class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): def test_initial_query_parameters(self): params = {'active': 1} widget = self.get_widget_instance(query_params=params) - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -209,7 +203,7 @@ class AutoComboboxWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): "Serialize selectable options as json in data attribute." options = {'autoFocus': True} widget = self.get_widget_instance(attrs={'data-selectable-options': options}) - attrs = widget.build_attrs() + attrs = parsed_widget_attributes(widget) self.assertTrue('data-selectable-options' in attrs) self.assertEqual(attrs['data-selectable-options'], json.dumps(options)) @@ -228,8 +222,7 @@ class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): def test_hidden_type(self): widget = self.get_widget_instance() - sub_widget = widget.widgets[1] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[1]) self.assertTrue('data-selectable-type' in attrs) self.assertEqual(attrs['data-selectable-type'], 'hidden') @@ -237,17 +230,15 @@ class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): params = {'active': 1} widget = self.get_widget_instance() widget.update_query_parameters(params) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query self.assertEqual(query, urlencode(params)) - def test_limit_paramter(self): + def test_limit_parameter(self): widget = self.get_widget_instance(limit=10) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -256,8 +247,7 @@ class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): def test_initial_query_parameters(self): params = {'active': 1} widget = self.get_widget_instance(query_params=params) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -267,8 +257,7 @@ class AutoComboboxSelectWidgetTestCase(BaseSelectableTestCase, WidgetTestMixin): "Serialize selectable options as json in data attribute." options = {'autoFocus': True} widget = self.get_widget_instance(attrs={'data-selectable-options': options}) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) self.assertTrue('data-selectable-options' in attrs) self.assertEqual(attrs['data-selectable-options'], json.dumps(options)) @@ -283,8 +272,7 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes def test_multiple_attr(self): widget = self.get_widget_instance() - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) self.assertTrue('data-selectable-multiple' in attrs) self.assertEqual(attrs['data-selectable-multiple'], 'true') @@ -294,8 +282,7 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes def test_hidden_type(self): widget = self.get_widget_instance() - sub_widget = widget.widgets[1] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[1]) self.assertTrue('data-selectable-type' in attrs) self.assertEqual(attrs['data-selectable-type'], 'hidden-multiple') @@ -325,31 +312,32 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes widget = self.get_widget_instance() t1 = self.create_thing() t2 = self.create_thing() - qs_val = Thing.objects.filter(pk__in=[t1.pk, t2.pk]).values_list('pk', flat=True) + qs_val = Thing.objects.filter(pk__in=[t1.pk, t2.pk]) rendered_value = widget.render('field_name', qs_val) inputs = parsed_inputs(rendered_value) found_values = [] + found_titles = [] for field in inputs['field_name_1']: self.assertEqual(field.attributes['data-selectable-type'].value, 'hidden-multiple') self.assertEqual(field.attributes['type'].value, 'hidden') - found_values.append(int(field.attributes['value'].value)) - self.assertListEqual(found_values, [t1.pk, t2.pk]) + found_titles.append(field.attributes['title'].value) + found_values.append(field.attributes['value'].value) + self.assertListEqual(found_values, [str(t1.pk), str(t2.pk)]) + self.assertListEqual(found_titles, [t1.name, t2.name]) def test_update_query_parameters(self): params = {'active': 1} widget = self.get_widget_instance() widget.update_query_parameters(params) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query self.assertEqual(query, urlencode(params)) - def test_limit_paramter(self): + def test_limit_parameter(self): widget = self.get_widget_instance(limit=10) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -358,8 +346,7 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes def test_initial_query_parameters(self): params = {'active': 1} widget = self.get_widget_instance(query_params=params) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -369,8 +356,7 @@ class AutoCompleteSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes "Serialize selectable options as json in data attribute." options = {'autoFocus': True} widget = self.get_widget_instance(attrs={'data-selectable-options': options}) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) self.assertTrue('data-selectable-options' in attrs) self.assertEqual(attrs['data-selectable-options'], json.dumps(options)) @@ -385,8 +371,7 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes def test_multiple_attr(self): widget = self.get_widget_instance() - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) self.assertTrue('data-selectable-multiple' in attrs) self.assertEqual(attrs['data-selectable-multiple'], 'true') @@ -396,8 +381,7 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes def test_hidden_type(self): widget = self.get_widget_instance() - sub_widget = widget.widgets[1] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[1]) self.assertTrue('data-selectable-type' in attrs) self.assertEqual(attrs['data-selectable-type'], 'hidden-multiple') @@ -427,7 +411,7 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes widget = self.get_widget_instance() t1 = self.create_thing() t2 = self.create_thing() - qs_val = Thing.objects.filter(pk__in=[t1.pk, t2.pk]).values_list('pk', flat=True) + qs_val = Thing.objects.filter(pk__in=[t1.pk, t2.pk]) rendered_value = widget.render('field_name', qs_val) inputs = parsed_inputs(rendered_value) found_values = [] @@ -441,17 +425,15 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes params = {'active': 1} widget = self.get_widget_instance() widget.update_query_parameters(params) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query self.assertEqual(query, urlencode(params)) - def test_limit_paramter(self): + def test_limit_parameter(self): widget = self.get_widget_instance(limit=10) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -460,8 +442,7 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes def test_initial_query_parameters(self): params = {'active': 1} widget = self.get_widget_instance(query_params=params) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) url = attrs['data-selectable-url'] parse = urlparse(url) query = parse.query @@ -471,7 +452,6 @@ class AutoComboboxSelectMultipleWidgetTestCase(BaseSelectableTestCase, WidgetTes "Serialize selectable options as json in data attribute." options = {'autoFocus': True} widget = self.get_widget_instance(attrs={'data-selectable-options': options}) - sub_widget = widget.widgets[0] - attrs = sub_widget.build_attrs() + attrs = parsed_widget_attributes(widget.widgets[0]) self.assertTrue('data-selectable-options' in attrs) self.assertEqual(attrs['data-selectable-options'], json.dumps(options)) diff --git a/dep/django-selectable/selectable/tests/views.py b/dep/django-selectable/selectable/tests/views.py index f747967f..aa18f3e3 100644 --- a/dep/django-selectable/selectable/tests/views.py +++ b/dep/django-selectable/selectable/tests/views.py @@ -1,5 +1,6 @@ from django.http import HttpResponseNotFound, HttpResponseServerError + def test_404(request): return HttpResponseNotFound() diff --git a/dep/django-selectable/selectable/urls.py b/dep/django-selectable/selectable/urls.py index 02444b99..eee1b68a 100644 --- a/dep/django-selectable/selectable/urls.py +++ b/dep/django-selectable/selectable/urls.py @@ -1,13 +1,6 @@ from django.conf.urls import url from . import views -from .compat import LEGACY_AUTO_DISCOVER - -if LEGACY_AUTO_DISCOVER: - # Auto-discovery is now handled by the app configuration - from . import registry - - registry.autodiscover() urlpatterns = [ diff --git a/dep/django-selectable/selectable/views.py b/dep/django-selectable/selectable/views.py index ec4dc6d3..5d8ef623 100644 --- a/dep/django-selectable/selectable/views.py +++ b/dep/django-selectable/selectable/views.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.http import HttpResponse, Http404 +from django.http import Http404 from selectable.registry import registry diff --git a/dep/django-selectable/setup.cfg b/dep/django-selectable/setup.cfg index 6f08d0e3..d4d5fd84 100644 --- a/dep/django-selectable/setup.cfg +++ b/dep/django-selectable/setup.cfg @@ -1,8 +1,11 @@ -[bdist_wheel] -universal = 1 +[coverage:run] +branch = true +omit = */tests/*, example/*, .tox/*, setup.py, runtests.py +source = . + +[coverage:report] +show_missing = true -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 +[bdist_wheel] +universal = 1 diff --git a/dep/django-selectable/setup.py b/dep/django-selectable/setup.py index 77ed4575..fc2aa048 100644 --- a/dep/django-selectable/setup.py +++ b/dep/django-selectable/setup.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python import os from setuptools import setup, find_packages @@ -23,22 +24,21 @@ setup( license='BSD', description=' '.join(__import__('selectable').__doc__.splitlines()).strip(), classifiers=[ - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', + 'Framework :: Django', 'License :: OSI Approved :: BSD License', + 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.2', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', - 'Framework :: Django', - 'Development Status :: 5 - Production/Stable', - 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3.5', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', ], long_description=read_file('README.rst'), test_suite="runtests.runtests", - tests_require=['mock', ], - zip_safe=False, # because we're including media that Django needs + tests_require=['mock'], + zip_safe=False, # because we're including media that Django needs ) -- 2.11.4.GIT