Radical removal of all unneeded code

This commit is contained in:
Johannes Hoppe 2015-08-06 11:58:27 +02:00
parent 33b7dffca1
commit 95297a362e
44 changed files with 282 additions and 905144 deletions

2
.gitignore vendored
View File

@ -8,6 +8,8 @@ Django_Select2_Py3.egg-info
dist
build
node_modules/
docs/_build
# Intellij

View File

@ -1,5 +1,7 @@
language: python
sudo: false
cache:
- pip
services:
- memcached
python:
@ -19,7 +21,7 @@ env:
matrix:
fast_finish: true
allow_failures:
- env: DJANGO="<1.7,>=1.6"
- env: DJANGO="Django<1.7,>=1.6"
- env: DJANGO="-e git+https://github.com/django/django.git@master#egg=Django"
- python: "pypy"
- python: "pypy3"
@ -33,6 +35,9 @@ install:
- pip install coveralls
- sh -e /etc/init.d/xvfb start
script:
- isort --check-only --recursive --diff .
- flake8 --jobs=2 .
- pep257 django_select2
- coverage run --source=django_select2 runtests.py
after_success:
coveralls
- coveralls

View File

@ -1,31 +0,0 @@
Creating bundle
===============
$ python setup.py sdist
Uploading bundle to PyPi
========================
$ python setup.py sdist upload
OR
$ python setup.py sdist upload -r pypi
This needs we have a ~/.pypi file. Its content should be like:-
[distutils] # this tells distutils what package indexes you can push to
index-servers =
pypi
pypitest #Optional
[pypi]
repository: https://pypi.python.org/pypi
username: <username>
password: <password>
[pypitest] #Optional
repository: https://testpypi.python.org/pypi
username: <username>
password: <password>

View File

@ -1,12 +0,0 @@
The following license applies to the wordlist (for model testmain.word) included in testapp/testmain/fixtures/initial_data.json.
Src: http://dreamsteep.com/projects/the-english-open-word-list.html
======================================================================================
UK Advanced Cryptics Dictionary Licensing Information:
Copyright © J Ross Beresford 1993-1999. All Rights Reserved.
The following restriction is placed on the use of this publication: if the UK Advanced Cryptics Dictionary is used in a software package or redistributed in any form, the copyright notice must be prominently displayed and the text of this document must be included verbatim.
There are no other restrictions: I would like to see the list distributed as widely as possible.

View File

@ -10,62 +10,63 @@ The app includes Select2 driven Django Widgets and Form Fields.
Widgets
-------
These components are responsible for rendering the necessary JavaScript and HTML markups. Since this whole
package is to render choices using Select2 JavaScript library, hence these components are meant to be used
These components are responsible for rendering
the necessary JavaScript and HTML markups. Since this whole
package is to render choices using Select2 JavaScript
library, hence these components are meant to be used
with choice fields.
Widgets are generally of two types :-
1. **Light** --
They are not meant to be used when there are too many options, say, in thousands. This
is because all those options would have to be pre-rendered onto the page and JavaScript would
be used to search through them. Said that, they are also one the most easiest to use. They are almost
drop-in-replacement for Django's default select widgets.
They are not meant to be used when there
are too many options, say, in thousands.
This is because all those options would
have to be pre-rendered onto the page
and JavaScript would be used to search
through them. Said that, they are also one
the most easiest to use. They are almost
drop-in-replacement for Django's default
select widgets.
2. **Heavy** --
They are suited for scenarios when the number of options are large and need complex queries
(from maybe different sources) to get the options. This dynamic fetching of options undoubtedly requires
Ajax communication with the server. Django-Select2 includes a helper JS file which is included automatically,
so you need not worry about writing any Ajax related JS code. Although on the server side you do need to
create a view specifically to respond to the queries.
They are suited for scenarios when the number of options
are large and need complex queries (from maybe different
sources) to get the options.
This dynamic fetching of options undoubtedly requires
Ajax communication with the server. Django-Select2 includes
a helper JS file which is included automatically,
so you need not worry about writing any Ajax related JS code.
Although on the server side you do need to create a view
specifically to respond to the queries.
Heavies have further specialized versions called -- **Auto Heavy**. These do not require views to serve Ajax
requests. When they are instantiated, they register themselves with one central view which handles Ajax requests
for them.
Heavies have further specialized versions called -- **Auto Heavy**.
These do not require views to serve Ajax requests.
When they are instantiated, they register themselves
with one central view which handles Ajax requests for them.
Heavy widgets have the word 'Heavy' in their name. Light widgets are normally named, i.e. there is no 'Light' word
in their names.
Heavy widgets have the word 'Heavy' in their name.
Light widgets are normally named, i.e. there is no
'Light' word in their names.
**Available widgets:**
:py:class:`.Select2Widget`, :py:class:`.Select2MultipleWidget`, :py:class:`.HeavySelect2Widget`, :py:class:`.HeavySelect2MultipleWidget`,
:py:class:`.AutoHeavySelect2Widget`, :py:class:`.AutoHeavySelect2MultipleWidget`, :py:class:`.HeavySelect2TagWidget`,
:py:class:`.Select2Widget`,
:py:class:`.Select2MultipleWidget`,
:py:class:`.HeavySelect2Widget`,
:py:class:`.HeavySelect2MultipleWidget`,
:py:class:`.AutoHeavySelect2Widget`,
:py:class:`.AutoHeavySelect2MultipleWidget`,
:py:class:`.HeavySelect2TagWidget`,
:py:class:`.AutoHeavySelect2TagWidget`
`Read more`_
Fields
------
These are pre-implemented choice fields which use the above widgets. It is highly recommended that you use them
instead of rolling your own.
The fields available are good for general purpose use, although more specialized versions too are available for
your ease.
**Available fields:**
:py:class:`.Select2ChoiceField`, :py:class:`.Select2MultipleChoiceField`, :py:class:`.HeavySelect2ChoiceField`,
:py:class:`.HeavySelect2MultipleChoiceField`, :py:class:`.HeavyModelSelect2ChoiceField`,
:py:class:`.HeavyModelSelect2MultipleChoiceField`, :py:class:`.ModelSelect2Field`, :py:class:`.ModelSelect2MultipleField`,
:py:class:`.AutoSelect2Field`, :py:class:`.AutoSelect2MultipleField`, :py:class:`.AutoModelSelect2Field`,
:py:class:`.AutoModelSelect2MultipleField`, :py:class:`.HeavySelect2TagField`, :py:class:`.AutoSelect2TagField`,
:py:class:`.HeavyModelSelect2TagField`, :py:class:`.AutoModelSelect2TagField`
Views
-----
The view - `Select2View`, exposed here is meant to be used with 'Heavy' fields and widgets.
The view - `Select2View`, exposed here is meant
to be used with 'Heavy' fields and widgets.
**Imported:**
@ -76,83 +77,5 @@ The view - `Select2View`, exposed here is meant to be used with 'Heavy' fields a
.. _Read more: http://blog.applegrew.com/2012/08/django-select2/
"""
from __future__ import absolute_import, unicode_literals
import logging
import warnings
from .types import NO_ERR_RESP # NOAQ
logger = logging.getLogger(__name__)
__version__ = "4.3.2"
__RENDER_SELECT2_STATICS = False
__BOOTSTRAP = False
# @todo: Deprecated and to be removed in v5
__ENABLE_MULTI_PROCESS_SUPPORT = False
__MEMCACHE_HOST = None
__MEMCACHE_PORT = None
__MEMCACHE_TTL = 900
__GENERATE_RANDOM_ID = False
try:
from django.conf import settings
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Django found.")
if settings.configured:
__RENDER_SELECT2_STATICS = getattr(settings, 'AUTO_RENDER_SELECT2_STATICS', True)
__ENABLE_MULTI_PROCESS_SUPPORT = getattr(settings, 'ENABLE_SELECT2_MULTI_PROCESS_SUPPORT', False)
__MEMCACHE_HOST = getattr(settings, 'SELECT2_MEMCACHE_HOST', None)
__MEMCACHE_PORT = getattr(settings, 'SELECT2_MEMCACHE_PORT', None)
__MEMCACHE_TTL = getattr(settings, 'SELECT2_MEMCACHE_TTL', 900)
__GENERATE_RANDOM_ID = getattr(settings, 'GENERATE_RANDOM_SELECT2_ID', False)
__BOOTSTRAP = getattr(settings, 'SELECT2_BOOTSTRAP', False)
if __GENERATE_RANDOM_ID:
msg = (
'Select2\'s setting "GENERATE_RANDOM_SELECT2_ID" has been deprecated.\n'
'Since version 4.4 all IDs will be encrypted by default.'
)
warnings.warn(msg, DeprecationWarning)
__ENABLE_MULTI_PROCESS_SUPPORT = False
if __ENABLE_MULTI_PROCESS_SUPPORT:
msg = (
'Select2\'s setting "ENABLE_SELECT2_MULTI_PROCESS_SUPPORT"'
' has been deprecated and will be removed in version 4.4.\n'
'Multiprocessing support is on by default.\n'
'If you seek multi machine support please review the latest documentation.'
)
warnings.warn(msg, DeprecationWarning)
if __MEMCACHE_HOST or __MEMCACHE_PORT or __MEMCACHE_TTL:
msg = (
'Select2\'s setting "SELECT2_MEMCACHE_HOST" has been deprecated'
' in favour of "SELECT2_CACHE_BACKEND".\n'
'The support for this setting will be removed in version 5.'
)
from .widgets import (
Select2Widget, Select2MultipleWidget,
HeavySelect2Widget, HeavySelect2MultipleWidget,
AutoHeavySelect2Widget, AutoHeavySelect2MultipleWidget,
HeavySelect2TagWidget, AutoHeavySelect2TagWidget
) # NOQA
from .fields import (
Select2ChoiceField, Select2MultipleChoiceField,
HeavySelect2ChoiceField, HeavySelect2MultipleChoiceField,
HeavyModelSelect2ChoiceField, HeavyModelSelect2MultipleChoiceField,
ModelSelect2Field, ModelSelect2MultipleField,
AutoSelect2Field, AutoSelect2MultipleField,
AutoModelSelect2Field, AutoModelSelect2MultipleField,
HeavySelect2TagField, AutoSelect2TagField,
HeavyModelSelect2TagField, AutoModelSelect2TagField
) # NOQA
from .views import Select2View
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Django found and fields and widgets loaded.")
except ImportError:
if logger.isEnabledFor(logging.INFO):
logger.info("Django not found.")
__version__ = "5.0.0"

View File

@ -14,19 +14,10 @@ It is advised to always setup a separate cache server for Select2.
"""
from __future__ import absolute_import, unicode_literals
from django.core.cache import _create_cache, caches
from django.core.cache import caches
from . import __MEMCACHE_HOST as MEMCACHE_HOST
from . import __MEMCACHE_PORT as MEMCACHE_PORT
from . import __MEMCACHE_TTL as MEMCACHE_TTL
from .conf import settings
__all__ = ('cache', )
if MEMCACHE_HOST and MEMCACHE_PORT:
# @todo: Deprecated and to be removed in v5
location = ':'.join((MEMCACHE_HOST, MEMCACHE_PORT))
cache = _create_cache('django.core.cache.backends.memcached.MemcachedCache',
LOCATION=MEMCACHE_HOST, TIMEOUT=MEMCACHE_TTL)
else:
cache = caches[settings.SELECT2_CACHE_BACKEND]
cache = caches[settings.SELECT2_CACHE_BACKEND]

View File

@ -4,12 +4,13 @@ from __future__ import absolute_import, unicode_literals
from appconf import AppConf
from django.conf import settings # NOQA
__all__ = ['settings']
__all__ = ('settings',)
class Select2Conf(AppConf):
CACHE_BACKEND = 'default'
CACHE_PREFIX = 'select2_'
BOOTSTRAP = False
class Meta:
prefix = 'SELECT2'

View File

@ -1,789 +0,0 @@
# -*- coding: utf-8 -*-
"""
Contains all the Django fields for Select2.
"""
from __future__ import absolute_import, unicode_literals
import copy
import logging
import warnings
from functools import reduce
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.forms.models import ModelChoiceIterator
from django.utils.encoding import force_text, smart_text
from django.utils.translation import ugettext_lazy as _
from .types import NO_ERR_RESP
from .util import extract_some_key_val
from .widgets import AutoHeavySelect2Mixin # NOQA
from .widgets import (AutoHeavySelect2MultipleWidget,
AutoHeavySelect2TagWidget, AutoHeavySelect2Widget,
HeavySelect2MultipleWidget, HeavySelect2TagWidget,
HeavySelect2Widget, Select2MultipleWidget, Select2Widget)
logger = logging.getLogger(__name__)
class AutoViewFieldMixin(object):
"""
Registers itself with AutoResponseView.
All Auto fields must sub-class this mixin, so that they are registered.
.. warning:: Do not forget to include ``'django_select2.urls'`` in your url conf, else,
central view used to serve Auto fields won't be available.
"""
def __init__(self, *args, **kwargs):
kwargs.pop('auto_id', None)
super(AutoViewFieldMixin, self).__init__(*args, **kwargs)
def get_results(self, *args, **kwargs):
raise NotImplementedError
# ## Light general fields ##
class Select2ChoiceField(forms.ChoiceField):
"""
Drop-in Select2 replacement for :py:class:`forms.ChoiceField`.
"""
widget = Select2Widget
class Select2MultipleChoiceField(forms.MultipleChoiceField):
"""
Drop-in Select2 replacement for :py:class:`forms.MultipleChoiceField`.
"""
widget = Select2MultipleWidget
# ## Model fields related mixins ##
class ModelResultJsonMixin(object):
"""
Makes ``heavy_data.js`` parsable JSON response for queries on its model.
On query it uses :py:meth:`.prepare_qs_params` to prepare query attributes
which it then passes to ``self.queryset.filter()`` to get the results.
It is expected that sub-classes will defined a class field variable
``search_fields``, which should be a list of field names to search for.
.. note:: As of version 3.1.3, ``search_fields`` is optional if sub-class
overrides ``get_results``.
"""
max_results = 25
to_field_name = 'pk'
queryset = {}
search_fields = ()
def __init__(self, *args, **kwargs):
self.search_fields = kwargs.pop('search_fields', self.search_fields)
super(ModelResultJsonMixin, self).__init__(*args, **kwargs)
def get_search_fields(self):
if self.search_fields:
return self.search_fields
raise NotImplementedError('%s must implement "search_fields".' % self.__class__.__name__)
def get_queryset(self):
"""
Return the list of model choices.
The return value must be an iterable and may be an instance of
`QuerySet` in which case `QuerySet` specific behavior will be enabled.
"""
if self.queryset:
return self.queryset
raise NotImplementedError('%s must implement "queryset".' % self.__class__.__name__)
def label_from_instance(self, obj):
"""
Sub-classes should override this to generate custom label texts for values.
:param obj: The model object.
:type obj: :py:class:`django.model.Model`
:return: The label string.
:rtype: :py:obj:`unicode`
"""
warnings.warn(
'"label_from_instance" is deprecated and will be removed in version 5.',
DeprecationWarning
)
return smart_text(obj)
def extra_data_from_instance(self, obj):
"""
Sub-classes should override this to generate extra data for values. These are passed to
JavaScript and can be used for custom rendering.
:param obj: The model object.
:type obj: :py:class:`django.model.Model`
:return: The extra data dictionary.
:rtype: :py:obj:`dict`
"""
return {}
def prepare_qs_params(self, request, search_term, search_fields):
"""
Prepare queryset parameter to use for searching.
:param search_term: The search term.
:type search_term: :py:obj:`str`
:param search_fields: The list of search fields. This is same as ``self.search_fields``.
:type search_term: :py:obj:`list`
:return: A dictionary of parameters to 'or' and 'and' together. The output format should
be ::
{
'or': [
Q(attr11=term11) | Q(attr12=term12) | ...,
Q(attrN1=termN1) | Q(attrN2=termN2) | ...,
...],
'and': {
'attrX1': termX1,
'attrX2': termX2,
...
}
}
The above would then be coaxed into ``filter()`` as below::
queryset.filter(
Q(attr11=term11) | Q(attr12=term12) | ...,
Q(attrN1=termN1) | Q(attrN2=termN2) | ...,
...,
attrX1=termX1,
attrX2=termX2,
...
)
In this implementation, ``term11, term12, termN1, ...`` etc., all are actually ``search_term``.
Also then ``and`` part is always empty.
So, let's take an example.
| Assume, ``search_term == 'John'``
| ``self.search_fields == ['first_name__icontains', 'last_name__icontains']``
So, the prepared query would be::
{
'or': [
Q(first_name__icontains=search_term) | Q(last_name__icontains=search_term)
],
'and': {}
}
:rtype: :py:obj:`dict`
"""
q = reduce(lambda x, y: y | Q({x: search_term}), search_fields)
return {'or': [q], 'and': {}}
def filter_queryset(self, request, term):
"""
See :py:meth:`.views.Select2View.get_results`.
This implementation takes care of detecting if more results are available.
"""
qs = self.get_queryset()
params = self.prepare_qs_params(request, term, self.search_fields)
return qs.filter(*params['or'], **params['and']).distinct()
def get_results(self, request, term, page, context):
warnings.warn(
'"get_results" is deprecated and will be removed in version 5.',
DeprecationWarning
)
self.widget.queryset = self.get_queryset()
self.widget.search_fields = self.get_search_fields()
qs = self.widget.filter_queryset(term)
if self.max_results:
min_ = (page - 1) * self.max_results
max_ = min_ + self.max_results + 1 # fetching one extra row to check if it has more rows.
res = qs[min_:max_]
has_more = len(res) == (max_ - min_)
if has_more:
res = list(res)[:-1]
else:
res = qs
has_more = False
res = [
(
getattr(obj, self.to_field_name),
self.label_from_instance(obj),
self.extra_data_from_instance(obj)
)
for obj in res
]
return NO_ERR_RESP, has_more, res
class ChoiceMixin(object):
"""
Simple mixin which provides a property -- ``choices``. When ``choices`` is set,
then it sets that value to ``self.widget.choices`` too.
"""
def _get_choices(self):
if hasattr(self, '_choices'):
return self._choices
return []
def _set_choices(self, value):
# Setting choices also sets the choices on the widget.
# choices can be any iterable, but we call list() on it because
# it will be consumed more than once.
self._choices = self.widget.choices = list(value)
choices = property(_get_choices, _set_choices)
def __deepcopy__(self, memo):
result = super(ChoiceMixin, self).__deepcopy__(memo)
if hasattr(self, '_choices'):
result._choices = copy.deepcopy(self._choices, memo)
return result
class FilterableModelChoiceIterator(ModelChoiceIterator):
"""
Extends ModelChoiceIterator to add the capability to apply additional
filter on the passed queryset.
"""
def set_extra_filter(self, **filter_map):
"""
Applies additional filter on the queryset. This can be called multiple times.
:param filter_map: The ``**kwargs`` to pass to :py:meth:`django.db.models.query.QuerySet.filter`.
If this is not set then additional filter (if) applied before is removed.
"""
if not hasattr(self, '_original_queryset'):
import copy
self._original_queryset = copy.deepcopy(self.queryset)
if filter_map:
self.queryset = self._original_queryset.filter(**filter_map)
else:
self.queryset = self._original_queryset
class QuerysetChoiceMixin(ChoiceMixin):
"""
Overrides ``choices``' getter to return instance of :py:class:`.FilterableModelChoiceIterator`
instead.
"""
def _get_choices(self):
# If self._choices is set, then somebody must have manually set
# the property self.choices. In this case, just return self._choices.
if hasattr(self, '_choices'):
return self._choices
# Otherwise, execute the QuerySet in self.queryset to determine the
# choices dynamically. Return a fresh ModelChoiceIterator that has not been
# consumed. Note that we're instantiating a new ModelChoiceIterator *each*
# time _get_choices() is called (and, thus, each time self.choices is
# accessed) so that we can ensure the QuerySet has not been consumed. This
# construct might look complicated but it allows for lazy evaluation of
# the queryset.
return FilterableModelChoiceIterator(self)
choices = property(_get_choices, ChoiceMixin._set_choices)
def __deepcopy__(self, memo):
result = super(QuerysetChoiceMixin, self).__deepcopy__(memo)
# Need to force a new ModelChoiceIterator to be created, bug #11183
result.queryset = result.queryset
return result
class ModelChoiceFieldMixin(QuerysetChoiceMixin):
def __init__(self, queryset=None, **kwargs):
# This filters out kwargs not supported by Field but are still passed as it is required
# by other codes. If new args are added to Field then make sure they are added here too.
kargs = extract_some_key_val(kwargs, [
'empty_label', 'cache_choices', 'required', 'label', 'initial', 'help_text',
'validators', 'localize',
])
kargs['widget'] = kwargs.pop('widget', getattr(self, 'widget', None))
kargs['to_field_name'] = kwargs.pop('to_field_name', 'pk')
queryset = queryset or self.get_queryset()
# If it exists then probably it is set by HeavySelect2FieldBase.
# We are not gonna use that anyway.
if hasattr(self, '_choices'):
del self._choices
super(ModelChoiceFieldMixin, self).__init__(queryset, **kargs)
if hasattr(self, 'set_placeholder'):
self.widget.set_placeholder(self.empty_label)
def get_pk_field_name(self):
return self.to_field_name
# ## Slightly altered versions of the Django counterparts with the same name in forms module. ##
class ModelChoiceField(ModelChoiceFieldMixin, forms.ModelChoiceField):
def get_queryset(self):
if self.queryset:
return self.queryset
elif self.model:
return self.model._default_queryset
raise NotImplementedError('%s must implement "model" or "queryset".' % self.__class__.__name__)
class ModelMultipleChoiceField(ModelChoiceFieldMixin, forms.ModelMultipleChoiceField):
pass
# ## Light Fields specialized for Models ##
class ModelSelect2Field(ModelChoiceField):
"""
Light Select2 field, specialized for Models.
Select2 replacement for :py:class:`forms.ModelChoiceField`.
"""
widget = Select2Widget
class ModelSelect2MultipleField(ModelMultipleChoiceField):
"""
Light multiple-value Select2 field, specialized for Models.
Select2 replacement for :py:class:`forms.ModelMultipleChoiceField`.
"""
widget = Select2MultipleWidget
# ## Heavy fields ##
class HeavySelect2FieldBaseMixin(object):
"""
Base mixin field for all Heavy fields.
.. note:: Although Heavy fields accept ``choices`` parameter like all Django choice fields, but these
fields are backed by big data sources, so ``choices`` cannot possibly have all the values.
For Heavies, consider ``choices`` to be a subset of all possible choices. It is available because users
might expect it to be available.
"""
def __init__(self, *args, **kwargs):
"""
Class constructor.
:param data_view: A :py:class:`~.views.Select2View` sub-class which can respond to this widget's Ajax queries.
:type data_view: :py:class:`django.views.generic.base.View` or None
:param widget: A widget instance.
:type widget: :py:class:`django.forms.widgets.Widget` or None
.. warning:: Either of ``data_view`` or ``widget`` must be specified, else :py:exc:`ValueError` would
be raised.
"""
data_view = kwargs.pop('data_view', None)
choices = kwargs.pop('choices', [])
widget = kwargs.pop('widget', None)
widget = widget or self.widget
if isinstance(widget, type):
self.widget = widget(data_view=data_view)
else:
self.widget.data_view = data_view
super(HeavySelect2FieldBaseMixin, self).__init__(*args, **kwargs)
# Widget should have been instantiated by now.
self.widget.field = self
# ModelChoiceField will set self.choices to ModelChoiceIterator
if choices and not (hasattr(self, 'choices') and isinstance(self.choices, forms.models.ModelChoiceIterator)):
self.choices = choices
class HeavyChoiceField(ChoiceMixin, forms.Field):
"""
Reimplements :py:class:`django.forms.TypedChoiceField` in a way which suites the use of big data.
.. note:: Although this field accepts ``choices`` parameter like all Django choice fields, but these
fields are backed by big data sources, so ``choices`` cannot possibly have all the values. It is meant
to be a subset of all possible choices.
"""
default_error_messages = {
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
}
empty_value = ''
"Sub-classes can set this other value if needed."
def __init__(self, *args, **kwargs):
super(HeavyChoiceField, self).__init__(*args, **kwargs)
# Widget should have been instantiated by now.
self.widget.field = self
def to_python(self, value):
if value == self.empty_value or value in validators.EMPTY_VALUES:
return self.empty_value
try:
value = self.coerce_value(value)
except (ValueError, TypeError, ValidationError):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
return value
def validate(self, value):
super(HeavyChoiceField, self).validate(value)
if value and not self.valid_value(value):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': value})
def valid_value(self, value):
uvalue = smart_text(value)
for k, v in self.choices:
if uvalue == smart_text(k):
return True
return self.validate_value(value)
def coerce_value(self, value):
"""
Coerces ``value`` to a Python data type.
Sub-classes should override this if they do not want Unicode values.
"""
return smart_text(value)
def validate_value(self, value):
"""
Sub-classes can override this to validate the value entered against the big data.
:param value: Value entered by the user.
:type value: As coerced by :py:meth:`.coerce_value`.
:return: ``True`` means the ``value`` is valid.
"""
return True
def _get_val_txt(self, value):
try:
value = self.coerce_value(value)
self.validate_value(value)
except Exception:
logger.exception("Exception while trying to get label for value")
return None
return self.get_val_txt(value)
def get_val_txt(self, value):
"""
If Heavy widgets encounter any value which it can't find in ``choices`` then it calls
this method to get the label for the value.
:param value: Value entered by the user.
:type value: As coerced by :py:meth:`.coerce_value`.
:return: The label for this value.
:rtype: :py:obj:`unicode` or None (when no possible label could be found)
"""
return None
class HeavyMultipleChoiceField(HeavyChoiceField):
"""
Reimplements :py:class:`django.forms.TypedMultipleChoiceField` in a way which suites the use of big data.
.. note:: Although this field accepts ``choices`` parameter like all Django choice fields, but these
fields are backed by big data sources, so ``choices`` cannot possibly have all the values. It is meant
to be a subset of all possible choices.
"""
hidden_widget = forms.MultipleHiddenInput
default_error_messages = {
'invalid_choice': _('Select a valid choice. %(value)s is not one of the available choices.'),
'invalid_list': _('Enter a list of values.'),
}
def to_python(self, value):
if not value:
return []
elif not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['invalid_list'])
return [self.coerce_value(val) for val in value]
def validate(self, value):
if self.required and not value:
raise ValidationError(self.error_messages['required'])
# Validate that each value in the value list is in self.choices or
# the big data (i.e. validate_value() returns True).
for val in value:
if not self.valid_value(val):
raise ValidationError(self.error_messages['invalid_choice'] % {'value': val})
class HeavySelect2ChoiceField(HeavySelect2FieldBaseMixin, HeavyChoiceField):
"""Heavy Select2 Choice field."""
widget = HeavySelect2Widget
class HeavySelect2MultipleChoiceField(HeavySelect2FieldBaseMixin, HeavyMultipleChoiceField):
"""Heavy Select2 Multiple Choice field."""
widget = HeavySelect2MultipleWidget
class HeavySelect2TagField(HeavySelect2MultipleChoiceField):
"""
Heavy Select2 field for tagging.
.. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`create_new_value` is not implemented.
"""
widget = HeavySelect2TagWidget
def validate(self, value):
if self.required and not value:
raise ValidationError(self.error_messages['required'])
# Check if each value in the value list is in self.choices or
# the big data (i.e. validate_value() returns True).
# If not then calls create_new_value() to create the new value.
for i in range(0, len(value)):
val = value[i]
if not self.valid_value(val):
value[i] = self.create_new_value(val)
def create_new_value(self, value):
"""
This is called when the input value is not valid. This
allows you to add the value into the data-store. If that
is not done then eventually the validation will fail.
:param value: Invalid value entered by the user.
:type value: As coerced by :py:meth:`HeavyChoiceField.coerce_value`.
:return: The a new value, which could be the id (pk) of the created value.
:rtype: Any
"""
raise NotImplementedError
# ## Heavy field specialized for Models ##
class HeavyModelSelect2ChoiceField(HeavySelect2FieldBaseMixin, ModelChoiceField):
"""Heavy Select2 Choice field, specialized for Models."""
widget = HeavySelect2Widget
def __init__(self, *args, **kwargs):
kwargs.pop('choices', None)
super(HeavyModelSelect2ChoiceField, self).__init__(*args, **kwargs)
class HeavyModelSelect2MultipleChoiceField(HeavySelect2FieldBaseMixin, ModelMultipleChoiceField):
"""Heavy Select2 Multiple Choice field, specialized for Models."""
widget = HeavySelect2MultipleWidget
def __init__(self, *args, **kwargs):
kwargs.pop('choices', None)
super(HeavyModelSelect2MultipleChoiceField, self).__init__(*args, **kwargs)
class HeavyModelSelect2TagField(HeavySelect2FieldBaseMixin, ModelMultipleChoiceField):
"""
Heavy Select2 field for tagging, specialized for Models.
.. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`get_model_field_values` is not implemented.
"""
widget = HeavySelect2TagWidget
def __init__(self, *args, **kwargs):
kwargs.pop('choices', None)
super(HeavyModelSelect2TagField, self).__init__(*args, **kwargs)
def to_python(self, value):
if value in self.empty_values:
return None
try:
key = self.to_field_name or 'pk'
value = self.queryset.get(**{key: value})
except ValueError:
raise ValidationError(self.error_messages['invalid_choice'])
except self.queryset.model.DoesNotExist:
value = self.create_new_value(value)
return value
def clean(self, value):
if self.required and not value:
raise ValidationError(self.error_messages['required'])
elif not self.required and not value:
return []
if not isinstance(value, (list, tuple)):
raise ValidationError(self.error_messages['list'])
new_values = []
key = self.to_field_name or 'pk'
for pk in list(value):
try:
self.queryset.filter(**{key: pk})
except ValueError:
value.remove(pk)
new_values.append(pk)
for val in new_values:
value.append(self.create_new_value(force_text(val)))
# Usually new_values will have list of new tags, but if the tag is
# suppose of type int then that could be interpreted as valid pk
# value and ValueError above won't be triggered.
# Below we find such tags and create them, by check if the pk
# actually exists.
qs = self.queryset.filter(**{'%s__in' % key: value})
pks = set([force_text(getattr(o, key)) for o in qs])
for i in range(0, len(value)):
val = force_text(value[i])
if val not in pks:
value[i] = self.create_new_value(val)
# Since this overrides the inherited ModelChoiceField.clean
# we run custom validators here
self.run_validators(value)
return qs
def create_new_value(self, value):
"""
This is called when the input value is not valid. This
allows you to add the value into the data-store. If that
is not done then eventually the validation will fail.
:param value: Invalid value entered by the user.
:type value: As coerced by :py:meth:`HeavyChoiceField.coerce_value`.
:return: The a new value, which could be the id (pk) of the created value.
:rtype: Any
"""
obj = self.queryset.create(**self.get_model_field_values(value))
return getattr(obj, self.to_field_name or 'pk')
def get_model_field_values(self, value):
"""
This is called when the input value is not valid and the field
tries to create a new model instance.
:param value: Invalid value entered by the user.
:type value: unicode
:return: Dict with attribute name - attribute value pair.
:rtype: dict
"""
raise NotImplementedError
# ## Heavy general field that uses central AutoView ##
class AutoSelect2Field(AutoViewFieldMixin, HeavySelect2ChoiceField):
"""
Auto Heavy Select2 field.
This needs to be subclassed. The first instance of a class (sub-class) is used to serve all incoming
json query requests for that type (class).
.. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`get_results` is not implemented.
"""
widget = AutoHeavySelect2Widget
class AutoSelect2MultipleField(AutoViewFieldMixin, HeavySelect2MultipleChoiceField):
"""
Auto Heavy Select2 field for multiple choices.
This needs to be subclassed. The first instance of a class (sub-class) is used to serve all incoming
json query requests for that type (class).
.. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`get_results` is not implemented.
"""
widget = AutoHeavySelect2MultipleWidget
class AutoSelect2TagField(AutoViewFieldMixin, HeavySelect2TagField):
"""
Auto Heavy Select2 field for tagging.
This needs to be subclassed. The first instance of a class (sub-class) is used to serve all incoming
json query requests for that type (class).
.. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`get_results` is not implemented.
"""
widget = AutoHeavySelect2TagWidget
# ## Heavy field, specialized for Model, that uses central AutoView ##
class AutoModelSelect2Field(ModelResultJsonMixin, AutoViewFieldMixin,
HeavyModelSelect2ChoiceField):
"""
Auto Heavy Select2 field, specialized for Models.
This needs to be subclassed. The first instance of a class (sub-class) is used to serve all incoming
json query requests for that type (class).
"""
widget = AutoHeavySelect2Widget
class AutoModelSelect2MultipleField(ModelResultJsonMixin, AutoViewFieldMixin,
HeavyModelSelect2MultipleChoiceField):
"""
Auto Heavy Select2 field for multiple choices, specialized for Models.
This needs to be subclassed. The first instance of a class (sub-class) is used to serve all incoming
json query requests for that type (class).
"""
widget = AutoHeavySelect2MultipleWidget
class AutoModelSelect2TagField(ModelResultJsonMixin, AutoViewFieldMixin,
HeavyModelSelect2TagField):
"""
Auto Heavy Select2 field for tagging, specialized for Models.
This needs to be subclassed. The first instance of a class (sub-class) is used to serve all incoming
json query requests for that type (class).
.. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`get_model_field_values` is not implemented.
Example::
class Tag(models.Model):
tag = models.CharField(max_length=10, unique=True)
def __str__(self):
return text_type(self.tag)
class TagField(AutoModelSelect2TagField):
queryset = Tag.objects
search_fields = ['tag__icontains', ]
def get_model_field_values(self, value):
return {'tag': value}
"""
widget = AutoHeavySelect2TagWidget

View File

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
"""
Contains all the Django widgets for Select2.
"""
"""Contains all the Django widgets for Select2."""
from __future__ import absolute_import, unicode_literals
import json
@ -14,17 +12,17 @@ from django import forms
from django.core import signing
from django.core.urlresolvers import reverse
from django.core.validators import EMPTY_VALUES
from django.db.models import Q, QuerySet
from django.db.models import Q
from django.utils.datastructures import MergeDict, MultiValueDict
from django.utils.encoding import force_text
from django.utils.safestring import mark_safe
from django.utils.six import text_type
from . import __RENDER_SELECT2_STATICS as RENDER_SELECT2_STATICS
from .cache import cache
from .conf import settings
from .media import (get_select2_css_libs, get_select2_heavy_js_libs,
get_select2_js_libs)
from .media import (
get_select2_css_libs, get_select2_heavy_js_libs, get_select2_js_libs
)
logger = logging.getLogger(__name__)
@ -33,10 +31,12 @@ logger = logging.getLogger(__name__)
class Select2Mixin(object):
"""
The base mixin of all Select2 widgets.
This mixin is responsible for rendering the necessary JavaScript and CSS codes which turns normal ``<select>``
This mixin is responsible for rendering the necessary
JavaScript and CSS codes which turns normal ``<select>``
markups into Select2 choice list.
The following Select2 options are added by this mixin:-
@ -86,18 +86,28 @@ class Select2Mixin(object):
})
}
.. tip:: You cannot introduce new options using this. For that you should sub-class and override
:py:meth:`.init_options`. The reason for this is, few options are not compatible with each other
or are not applicable in some scenarios. For example, when Select2 is attached to a ``<select>`` tag,
it can detect if it is being used with a single or multiple values from that tag itself. If you specified the
``multiple`` option in this case, it would not only be useless but an error from Select2 JS' point of view.
.. tip:: You cannot introduce new options using this.
For that you should sub-class and override
:py:meth:`.init_options`. The reason for this is,
few options are not compatible with each other
or are not applicable in some scenarios. For example,
when Select2 is attached to a ``<select>`` tag,
it can detect if it is being used with a single or
multiple values from that tag itself. If you specified the
``multiple`` option in this case, it would not only be
useless but an error from Select2 JS' point of view.
There are other such intricacies, based on which some options are removed. By enforcing this
restriction we make sure to not break the code by passing some wrong concoction of options.
There are other such intricacies, based on which
some options are removed. By enforcing this
restriction we make sure to not break the code by
passing some wrong concoction of options.
.. tip:: According to the select2 documentation, in order to get the ``placeholder`` and ``allowClear``
settings working, you have to specify an empty ``<option></option>`` as the first entry in your
``<select>`` list. Otherwise the field will be rendered without a placeholder and the clear feature
.. tip:: According to the select2 documentation, in order to
get the ``placeholder`` and ``allowClear``
settings working, you have to specify an empty
``<option></option>`` as the first entry in your
``<select>`` list. Otherwise the field will be
rendered without a placeholder and the clear feature
will stay disabled.
@ -116,6 +126,8 @@ class Select2Mixin(object):
def init_options(self):
"""
Initialize options.
Sub-classes can use this to suppress or override options passed to Select2 JS library.
Example::
@ -123,14 +135,16 @@ class Select2Mixin(object):
def init_options(self):
self.options['createSearchChoice'] = 'Your_js_function'
In the above example we are setting ``Your_js_function`` as Select2's ``createSearchChoice``
function.
In the above example we are setting ``Your_js_function``
as Select2's ``createSearchChoice`` function.
"""
pass
def set_placeholder(self, val):
"""
Placeholder is a value which Select2 JS library shows when nothing is selected. This should be string.
Placeholder is a value which Select2 JS library shows when nothing is selected.
This should be string.
:return: None
"""
@ -138,6 +152,8 @@ class Select2Mixin(object):
def get_options(self):
"""
Return select2 js options.
:return: Dictionary of options to be passed to Select2 JS.
:rtype: :py:obj:`dict`
@ -151,7 +167,7 @@ class Select2Mixin(object):
def render_js_code(self, id_, *args):
"""
Renders the ``<script>`` block which contains the JS code for this widget.
Render the ``<script>`` block which contains the JS code for this widget.
:return: The rendered JS code enclosed inside ``<script>`` block.
:rtype: :py:obj:`unicode`
@ -162,7 +178,9 @@ class Select2Mixin(object):
def render_js_script(self, inner_code):
"""
This wraps ``inner_code`` string inside the following code block::
Wrap ``inner_code`` string inside a code block.
Example::
<script type="text/javascript">
jQuery(function ($) {
@ -182,7 +200,7 @@ class Select2Mixin(object):
def render_inner_js_code(self, id_, *args):
"""
Renders all the JS code required for this widget.
Render all the JS code required for this widget.
:return: The rendered JS code which will be later enclosed inside ``<script>`` block.
:rtype: :py:obj:`unicode`
@ -194,13 +212,6 @@ class Select2Mixin(object):
return js
def render(self, name, value, attrs=None, choices=()):
"""
Renders this widget. HTML and JS code blocks all are rendered by this.
:return: The rendered markup.
:rtype: :py:obj:`unicode`
"""
args = [name, value, attrs]
if choices:
args.append(choices)
@ -215,7 +226,7 @@ class Select2Mixin(object):
def _get_media(self):
"""
Construct Media as a dynamic property
Construct Media as a dynamic property.
This is essential because we need to check RENDER_SELECT2_STATICS
before returning our assets.
@ -223,16 +234,15 @@ class Select2Mixin(object):
for more information:
https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property
"""
if RENDER_SELECT2_STATICS:
return forms.Media(
js=get_select2_js_libs(),
css={'screen': get_select2_css_libs(light=True)}
)
return forms.Media()
return forms.Media(
js=get_select2_js_libs(),
css={'screen': get_select2_css_libs(light=True)}
)
media = property(_get_media)
class Select2Widget(Select2Mixin, forms.Select):
"""
Drop-in Select2 replacement for :py:class:`forms.Select`.
@ -257,6 +267,7 @@ class Select2Widget(Select2Mixin, forms.Select):
class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple):
"""
Drop-in Select2 replacement for :py:class:`forms.SelectMultiple`.
@ -278,6 +289,7 @@ class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple):
class MultipleSelect2HiddenInput(forms.TextInput):
"""
Multiple hidden input for Select2.
@ -330,6 +342,7 @@ class MultipleSelect2HiddenInput(forms.TextInput):
# ## Heavy mixins and widgets ###
class HeavySelect2Mixin(Select2Mixin):
"""
The base mixin of all Heavy Select2 widgets. It sub-classes :py:class:`Select2Mixin`.
@ -343,8 +356,10 @@ class HeavySelect2Mixin(Select2Mixin):
* data: ``'django_select2.get_url_params'``
* results: ``'django_select2.process_results'``
.. tip:: You can override these options by passing ``select2_options`` kwarg to :py:meth:`.__init__`.
.. tip:: You can override these options by passing ``select2_options``
kwarg to :py:meth:`.__init__`.
"""
model = None
queryset = None
search_fields = []
@ -356,7 +371,8 @@ class HeavySelect2Mixin(Select2Mixin):
The following kwargs are allowed:-
:param data_view: A :py:class:`~.views.Select2View` sub-class which can respond to this widget's Ajax queries.
:param data_view: A :py:class:`~.views.Select2View` sub-class which
can respond to this widget's Ajax queries.
:type data_view: :py:class:`django.views.generic.base.View` or None
:param data_url: Url which will respond to Ajax queries with JSON object.
@ -365,37 +381,42 @@ class HeavySelect2Mixin(Select2Mixin):
.. tip:: When ``data_view`` is provided then it is converted into an URL using
:py:func:`~django.core.urlresolvers.reverse`.
.. warning:: Either of ``data_view`` or ``data_url`` must be specified, otherwise :py:exc:`ValueError` will
.. warning:: Either of ``data_view`` or ``data_url`` must be specified,
otherwise :py:exc:`ValueError` will
be raised.
:param choices: The list of available choices. If not provided then empty list is used instead. It
should be of the form -- ``[(val1, 'Label1'), (val2, 'Label2'), ...]``.
:param choices: The list of available choices.
If not provided then empty list is used instead.
It should be of the form -- ``[(val1, 'Label1'), (val2, 'Label2'), ...]``.
:type choices: :py:obj:`list` or :py:obj:`tuple`
:param userGetValTextFuncName: The name of the custom JS function which you want to use to convert
value to label.
:param userGetValTextFuncName: The name of the custom JS function which
you want to use to convert value to label.
In ``heavy_data.js``, ``django_select2.getValText()`` employs the following logic to convert value
to label :-
In ``heavy_data.js``, ``django_select2.getValText()`` employs
the following logic to convert value to label :-
1. First check if the Select2 input field has ``txt`` attribute set along with ``value``. If found
then use it.
1. First check if the Select2 input field has ``txt`` attribute
set along with ``value``. If found then use it.
2. Otherwise, check if user has provided any custom method for this. Then use that. If it returns a
label then use it.
2. Otherwise, check if user has provided any custom method for this.
Then use that. If it returns a label then use it.
3. Otherwise, check the cached results. When the user searches in the fields then all the returned
responses from server, which has the value and label mapping, are cached by ``heavy_data.js``.
3. Otherwise, check the cached results. When the user searches
in the fields then all the returned responses from server,
which has the value and label mapping, are cached by ``heavy_data.js``.
:type userGetValTextFuncName: :py:obj:`str`
.. tip:: Since version 3.2.0, cookies or localStorage are no longer checked or used. All
:py:class:`~.field.HeavyChoiceField` must override :py:meth:`~.fields.HeavyChoiceField.get_val_txt`.
If you are only using heavy widgets in your own fields then you should override :py:meth:`.render_texts`.
.. tip:: Since version 3.2.0, cookies or localStorage are no longer checked or used.
All :py:class:`~.field.HeavyChoiceField` must override
:py:meth:`~.fields.HeavyChoiceField.get_val_txt`.
If you are only using heavy widgets in your own fields
then you should override :py:meth:`.render_texts`.
"""
self.field = None
self.options = dict(self.options) # Making an instance specific copy
self.view = kwargs.pop('data_view', None)
self.view = kwargs.pop('data_view', 'django_select2_central_json')
self.url = kwargs.pop('data_url', None)
self.userGetValTextFuncName = kwargs.pop('userGetValTextFuncName', 'null')
self.choices = kwargs.pop('choices', [])
@ -408,8 +429,12 @@ class HeavySelect2Mixin(Select2Mixin):
self.options['ajax'] = {
'dataType': 'json',
'quietMillis': 100,
'data': '*START*django_select2.runInContextHelper(django_select2.get_url_params, selector)*END*',
'results': '*START*django_select2.runInContextHelper(django_select2.process_results, selector)*END*',
'data': ('*START*django_select2.runInContextHelper('
'django_select2.get_url_params, selector'
')*END*'),
'results': ('*START*django_select2.runInContextHelper('
'django_select2.process_results, selector'
')*END*'),
}
self.options['minimumInputLength'] = 2
self.options['initSelection'] = '*START*django_select2.onInit*END*'
@ -430,8 +455,6 @@ class HeavySelect2Mixin(Select2Mixin):
def get_queryset(self):
if self.queryset is not None:
queryset = self.queryset
if isinstance(queryset, QuerySet):
queryset = queryset.all()
elif self.model is not None:
queryset = self.model._default_manager.all()
else:
@ -451,7 +474,7 @@ class HeavySelect2Mixin(Select2Mixin):
def render_texts(self, selected_choices, choices):
"""
Renders a JS array with labels for the ``selected_choices``.
Render a JS array with labels for the ``selected_choices``.
:param selected_choices: List of selected choices' values.
:type selected_choices: :py:obj:`list` or :py:obj:`tuple`
@ -469,10 +492,6 @@ class HeavySelect2Mixin(Select2Mixin):
choices_dict = dict()
self_choices = self.choices
from . import fields
if isinstance(self_choices, fields.FilterableModelChoiceIterator):
self_choices.set_extra_filter(**{'%s__in' % self.field.get_pk_field_name(): selected_choices})
for val, txt in chain(self_choices, all_choices):
val = force_text(val)
choices_dict[val] = force_text(txt)
@ -503,8 +522,9 @@ class HeavySelect2Mixin(Select2Mixin):
def render_texts_for_value(self, id_, value, choices):
"""
Renders the JS code which sets the ``txt`` attribute on the field. It gets the array
of lables from :py:meth:`.render_texts`.
Render the JS code which sets the ``txt`` attribute on the field.
It gets the array of lables from :py:meth:`.render_texts`.
:param id_: Id of the field. This can be used to get reference of this field's DOM in JS.
:type id_: :py:obj:`str`
@ -550,7 +570,7 @@ class HeavySelect2Mixin(Select2Mixin):
def _get_media(self):
"""
Construct Media as a dynamic property
Construct Media as a dynamic property.
This is essential because we need to check RENDER_SELECT2_STATICS
before returning our assets.
@ -558,16 +578,15 @@ class HeavySelect2Mixin(Select2Mixin):
for more information:
https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property
"""
if RENDER_SELECT2_STATICS:
return forms.Media(
js=get_select2_heavy_js_libs(),
css={'screen': get_select2_css_libs()}
)
return forms.Media()
return forms.Media(
js=get_select2_heavy_js_libs(),
css={'screen': get_select2_css_libs()}
)
media = property(_get_media)
class HeavySelect2Widget(HeavySelect2Mixin, forms.TextInput):
"""
Single selection heavy widget.
@ -604,6 +623,7 @@ class HeavySelect2Widget(HeavySelect2Mixin, forms.TextInput):
class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput):
"""
Multiple selection heavy widget.
@ -627,8 +647,9 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput):
def render_texts_for_value(self, id_, value, choices):
"""
Renders the JS code which sets the ``txt`` attribute on the field. It gets the array
of lables from :py:meth:`.render_texts`.
Render the JS code which sets the ``txt`` attribute on the field.
It gets the array of lables from :py:meth:`.render_texts`.
:param id_: Id of the field. This can be used to get reference of this field's DOM in JS.
:type id_: :py:obj:`str`
@ -643,7 +664,8 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput):
:return: JS code which sets the ``txt`` attribute.
:rtype: :py:obj:`unicode`
"""
# Just like forms.SelectMultiple.render() it assumes that value will be multi-valued (list).
# Just like forms.SelectMultiple.render()
# it assumes that value will be multi-valued (list).
if value:
texts = self.render_texts(value, choices)
if texts:
@ -666,8 +688,11 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput):
class HeavySelect2TagWidget(HeavySelect2MultipleWidget):
"""
Heavy widget with tagging support. Based on :py:class:`HeavySelect2MultipleWidget`,
Heavy widget with tagging support.
Based on :py:class:`HeavySelect2MultipleWidget`,
unlike other widgets this allows users to create new options (tags).
Following Select2 options from :py:attr:`.Select2Mixin.options` are removed:-
@ -686,6 +711,7 @@ class HeavySelect2TagWidget(HeavySelect2MultipleWidget):
* minimumInputLength: ``1``
"""
def init_options(self):
super(HeavySelect2TagWidget, self).init_options()
self.options.pop('closeOnSelect', None)
@ -714,14 +740,15 @@ class HeavySelect2TagWidget(HeavySelect2MultipleWidget):
class AutoHeavySelect2Mixin(object):
"""
This mixin is needed for Auto heavy fields.
This mixin adds extra JS code to notify the field's DOM object of the generated id. The generated id
is not the same as the ``id`` attribute of the field's HTML markup. This id is generated by
:py:func:`~.util.register_field` when the Auto field is registered. The client side (DOM) sends this
id along with the Ajax request, so that the central view can identify which field should be used to
serve the request.
This mixin adds extra JS code to notify the field's DOM object of the generated id.
The generated id is not the same as the ``id`` attribute of the field's HTML markup.
This id is generated by :py:func:`~.util.register_field` when the Auto field is registered.
The client side (DOM) sends this id along with the Ajax request, so that the central
view can identify which field should be used to serve the request.
The js call to dynamically add the `django_select2` is as follows::
@ -739,15 +766,21 @@ class AutoHeavySelect2Mixin(object):
class AutoHeavySelect2Widget(AutoHeavySelect2Mixin, HeavySelect2Widget):
"""Auto version of :py:class:`.HeavySelect2Widget`"""
"""Auto version of :py:class:`.HeavySelect2Widget`."""
pass
class AutoHeavySelect2MultipleWidget(AutoHeavySelect2Mixin, HeavySelect2MultipleWidget):
"""Auto version of :py:class:`.HeavySelect2MultipleWidget`"""
"""Auto version of :py:class:`.HeavySelect2MultipleWidget`."""
pass
class AutoHeavySelect2TagWidget(AutoHeavySelect2Mixin, HeavySelect2TagWidget):
"""Auto version of :py:class:`.HeavySelect2TagWidget`"""
"""Auto version of :py:class:`.HeavySelect2TagWidget`."""
pass

View File

@ -1,10 +1,6 @@
from django.conf import settings
from django.contrib.staticfiles.templatetags.staticfiles import static
from . import __BOOTSTRAP as BOOTSTRAP
# Local version of DEBUG
DEBUG = settings.configured and settings.DEBUG
from .conf import settings
def django_select2_static(file):
@ -12,7 +8,7 @@ def django_select2_static(file):
def get_select2_js_libs():
if DEBUG:
if settings.DEBUG:
js_file = 'js/select2.js'
else:
js_file = 'js/select2.min.js'
@ -22,7 +18,7 @@ def get_select2_js_libs():
def get_select2_heavy_js_libs():
libs = get_select2_js_libs()
if DEBUG:
if settings.DEBUG:
js_file = 'js/heavy_data.js'
else:
js_file = 'js/heavy_data.min.js'
@ -30,15 +26,15 @@ def get_select2_heavy_js_libs():
def get_select2_css_libs(light=False):
if DEBUG:
if settings.DEBUG:
if light:
css_files = 'css/select2.css',
else:
css_files = 'css/select2.css', 'css/extra.css'
if BOOTSTRAP:
if settings.SELECT2_BOOTSTRAP:
css_files += 'css/select2-bootstrap.css',
else:
if BOOTSTRAP:
if settings.SELECT2_BOOTSTRAP:
if light:
css_files = 'css/select2-bootstrapped.min.css',
else:

View File

@ -1,37 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from django import template
from django_select2.media import (get_select2_css_libs,
get_select2_heavy_js_libs,
get_select2_js_libs)
register = template.Library()
def link_tag(css_file):
return '<link href="{file}" rel="stylesheet">'.format(file=css_file)
def script_tag(script_file):
return '<script type="text/javascript" src="{file}"></script>'.format(file=script_file)
@register.simple_tag(name='import_django_select2_js')
def import_js(light=0):
if light:
js_files = get_select2_js_libs()
else:
js_files = get_select2_heavy_js_libs()
return '\n'.join(script_tag(js_file) for js_file in js_files)
@register.simple_tag(name='import_django_select2_css')
def import_css(light=0):
return '\n'.join(link_tag(css_file) for css_file in get_select2_css_libs(light=light))
@register.simple_tag(name='import_django_select2_js_css')
def import_all(light=0):
return import_css(light=light) + import_js(light=light)

View File

@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
def extract_some_key_val(dct, keys):
"""
Gets a sub-set of a :py:obj:`dict`.
:param dct: Source dictionary.
:type dct: :py:obj:`dict`
:param keys: List of subset keys, which to extract from ``dct``.
:type keys: :py:obj:`list` or any iterable.
:rtype: :py:obj:`dict`
"""
edct = {}
for k in keys:
v = dct.get(k, None)
if v is not None:
edct[k] = v
return edct

View File

@ -13,43 +13,23 @@ from .types import NO_ERR_RESP
class AutoResponseView(BaseListView):
"""
A central view meant to respond to Ajax queries for all Heavy widgets/fields.
Although it is not mandatory to use, but is immensely helpful.
.. tip:: Fields which want to use this view must sub-class :py:class:`~.widgets.AutoViewFieldMixin`.
"""
def get(self, request, *args, **kwargs):
self.widget = self.get_widget_or_404()
self.term = kwargs.get('term', request.GET.get('term', ''))
try:
self.object_list = self.get_queryset()
except NotImplementedError:
self.object_list = []
context = self.get_context_data()
results = self.widget.field.get_results(
self.request,
self.term,
int(self.request.GET.get('page', 1)),
context
)
return JsonResponse(self._results_to_context(results))
else:
context = self.get_context_data()
return JsonResponse({
'results': [
{
'text': smart_text(obj),
'id': obj.pk,
}
for obj in context['object_list']
],
'err': NO_ERR_RESP,
'more': context['is_paginated']
})
self.object_list = self.get_queryset()
context = self.get_context_data()
return JsonResponse({
'results': [
{
'text': smart_text(obj),
'id': obj.pk,
}
for obj in context['object_list']
],
'err': NO_ERR_RESP,
'more': context['is_paginated']
})
def get_queryset(self):
return self.widget.filter_queryset(self.term)
@ -71,20 +51,3 @@ class AutoResponseView(BaseListView):
if field is None:
raise Http404('field_id not found')
return field
def _results_to_context(self, output):
err, has_more, results = output
res = []
if err == NO_ERR_RESP:
for result in results:
id_, text = result[:2]
if len(result) > 2:
extra_data = result[2]
else:
extra_data = {}
res.append(dict(id=id_, text=text, **extra_data))
return {
'err': err,
'more': has_more,
'results': res,
}

View File

@ -1,10 +0,0 @@
[pytest]
norecursedirs=env testapp docs
addopts = --tb=short --pep8 --flakes --isort -rxs
DJANGO_SETTINGS_MODULE=tests.testapp.settings
pep8maxlinelength=139
pep8ignore=
runtests.py ALL
flakes-ignore=
django_select2/__init__.py UnusedImport
django_select2/fields.py UnusedImport

View File

@ -1,10 +1,9 @@
pytest
pytest-pep8
pytest-flakes
pytest-django
pytest-isort
pep257
selenium
model_mommy
isort
requests
flake8
pep8-naming

23
setup.cfg Normal file
View File

@ -0,0 +1,23 @@
[pytest]
addopts = --tb=short -rxs
DJANGO_SETTINGS_MODULE=tests.testapp.settings
[flake8]
max-line-length = 120
max-complexity = 10
statistics = true
show-source = true
exclude = docs,runtests.py,setup.py,env
[pep257]
ignore = D100,D101,D102,D103
explain = true
count = true
[isort]
atomic = true
multi_line_output = 5
line_length = 79
skip = manage.py,docs
known_first_party = django_select2
combine_as_imports = true

View File

@ -1,10 +0,0 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

Binary file not shown.

View File

@ -1,191 +0,0 @@
# Django settings for testapp project.
import os.path
import posixpath
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
import os, sys
# Including the great parent so that django_select2 can be found.
parent_folder = PROJECT_ROOT
parent_folder = parent_folder.split('/')[:-2]
parent_folder = '/'.join(parent_folder)
if parent_folder not in sys.path:
sys.path.insert(0, parent_folder)
###
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@example.com'),
)
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'test.db', # Or path to database file if using sqlite3.
'USER': '', # Not used with sqlite3.
'PASSWORD': '', # Not used with sqlite3.
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
'PORT': '', # Set to empty string for default. Not used with sqlite3.
}
}
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'UTC'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/home/media/media.lawrence.com/media/"
MEDIA_ROOT = os.path.join(PROJECT_ROOT, "site_media", "media")
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
MEDIA_URL = "/site_media/media/"
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/home/media/media.lawrence.com/static/"
STATIC_ROOT = os.path.join(PROJECT_ROOT, "site_media", "static")
# URL prefix for static files.
# Example: "http://media.lawrence.com/static/"
STATIC_URL = '/site_media/static/'
# Additional locations of static files
STATICFILES_DIRS = (
os.path.join(PROJECT_ROOT, "static"),
)
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'ps&amp;l59kx$8%&amp;a1vjcj9sim-k^)g9gca0+a@j7o#_ln$(w%-#+k'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'testapp.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'testapp.wsgi.application'
TEMPLATE_DIRS = (
os.path.join(PROJECT_ROOT, "templates"),
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# Uncomment the next line to enable the admin:
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'django_select2',
'testapp.testmain',
)
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
},
'console':{
'level':'DEBUG',
'class':'logging.StreamHandler'
},
},
'loggers': {
'django_select2': {
'handlers':['console'],
'propagate': True,
'level':'DEBUG',
},
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
}
AUTO_RENDER_SELECT2_STATICS = False
#GENERATE_RANDOM_SELECT2_ID = True
##
# To test for multiple processes in development system w/o WSGI, runserver at
# different ports. Use $('#select2_field_html_id').data('field_id') to get the id
# in one process. Now switch to another port and use
# $('#select2_field_html_id').data('field_id', "id from previous process") to set
# id from last process. Now try to use that field. Its ajax should still work and
# you should see a message like - "Id 7:2013-03-01 14:49:18.490212 not found in
# this process. Looking up in remote server.", in console if you have debug enabled.
##
#ENABLE_SELECT2_MULTI_PROCESS_SUPPORT = True
#SELECT2_MEMCACHE_HOST = '127.0.0.1' # Uncomment to use memcached too
#SELECT2_MEMCACHE_PORT = 11211 # Uncomment to use memcached too
#SELECT2_MEMCACHE_TTL = 9 # Default 900

File diff suppressed because one or more lines are too long

View File

@ -1,7 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
Error 500
</body>

View File

@ -1,20 +0,0 @@
{% load staticfiles %}
{% load django_select2_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="{{ STATIC_URL }}jquery-1.7.2.min.js"></script>
{% import_django_select2_js %}
{% import_django_select2_css %}
<!-- For testing importing it again, but with another tag and light=1 -->
{% import_django_select2_js_css light=1 %}
</head>
<body>
<form method="post" action="">
{% csrf_token %}
<table>
{{form}}
</table>
<input type="submit" value="Submit Form"/>
</form>
</body>

View File

@ -1,22 +0,0 @@
{% load staticfiles %}
{% load django_select2_tags %}
<!DOCTYPE html>
<html lang="en">
<head>
<script src="{{ STATIC_URL }}jquery-1.7.2.min.js"></script>
{% import_django_select2_js %}
{% import_django_select2_css %}
<!-- For testing importing it again, but with another tag and light=1 -->
{% import_django_select2_js_css light=1 %}
</head>
<body>
<form method="get" action="">
<table>
{{form}}
</table>
<input type="submit" value="Submit Form"/>
</form>
{% for result in results %}
<p> {{ result.name}} </p>
{% endfor %}
</body>

View File

@ -1,19 +0,0 @@
{% load url from future %}
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<h1>Manual Tests</h1>
<ul>
<li><a href="{% url 'test_single_value_model_field' %}">Test single selection model fields</a></li>
<li><a href="{% url 'test_multi_values_model_field' %}">Test multi selection model fields</a></li>
<li><a href="{% url 'test_mixed_form' %}">Test mixed form. All fields' search must return their own results, not other fields'.</a></li>
<li><a href="{% url 'test_init_values' %}">Test that initial values are honored in unbound form</a></li>
<li><a href="{% url 'test_list_questions' %}">Test tagging support</a></li>
<li><a href="{% url 'test_auto_multivalue_field' %}">Test multi value auto model field.</a></li>
<li><a href="{% url 'test_auto_heavy_perf' %}">Test performance. Issue#54.</a></li>
<li><a href="{% url 'test_get_search_form' %}">Test a search form using GET. Issue#66.</a></li>
<li><a href="{% url 'test_issue_73' %}">Test issue#73.</a></li>
</ul>
</body>

View File

@ -1,26 +0,0 @@
{% load url from future %}
<!DOCTYPE html>
<html lang="en">
<head>
</head>
<body>
<h2>{{title}}</h2>
{% if create_new_href != '' %}
<a href="{% url create_new_href %}">Create New</a>
<br/>
{% endif %}
<p>Auto Tags</p>
<ul>
{% for e in object_list %}
<li><a href="{% url href e.id %}">{{ e }}</a></li>
{% endfor %}
</ul>
{% if href_non_auto %}
<p>Non-Auto Tags</p>
<ul>
{% for e in object_list %}
<li><a href="{% url href_non_auto e.id %}">{{ e }}</a></li>
{% endfor %}
</ul>
{% endif %}
</body>

View File

@ -1,19 +0,0 @@
from django.contrib import admin
from .models import ClassRoom, Lab, Dept, Employee, Word, School
from .forms import SchoolForm
class SchoolAdmin(admin.ModelAdmin):
form = SchoolForm
class Media:
js = ['jquery-1.7.2.min.js']
admin.site.register(ClassRoom)
admin.site.register(Lab)
admin.site.register(Dept)
admin.site.register(Employee)
admin.site.register(Word)
admin.site.register(School, SchoolAdmin)

View File

@ -1,40 +0,0 @@
from django_select2 import AutoSelect2MultipleField, AutoModelSelect2MultipleField
from django_select2 import NO_ERR_RESP
from .models import Dept
class GetSearchTestField(AutoSelect2MultipleField):
"""
Selects an employee.
This field does not render the form value on search results presentation.
"""
def security_check(self, request, *args, **kwargs):
return True
def get_results(self, request, term, page, context):
"""
Just a trivial example, with fixed values.
"""
res = [('Green Gold','Green Gold'),('Hulk','Hulk'),]
return (NO_ERR_RESP, False, res)
def get_val_txt(self, value):
"""
The problem of issue #66 was here. I was not overriding this.
When using AutoSelect2MultipleField you should implement get_val_txt in this case.
I think that this is because there should be an unique correspondence between
the referenced value and the shown value
In this particular example, the referenced value and the shown value are the same
"""
return unicode(value)
class GetModelSearchTestField(AutoModelSelect2MultipleField):
"""
Selects a department.
This field does render the form value on search results presentation. Works OK.
"""
queryset = Dept.objects.all()
search_fields = ['name__icontains']
to_field = 'name'
def security_check(self, request, *args, **kwargs):
return True

File diff suppressed because it is too large Load Diff

View File

@ -1,229 +0,0 @@
from django import forms
from django_select2 import *
from .models import Employee, Dept, ClassRoom, Lab, Word, School, Tag, Question, WordList
from .fields import GetSearchTestField, GetModelSearchTestField
from django.core.exceptions import ValidationError
def validate_fail_always(value):
raise ValidationError(u'%s not valid. In fact nothing is valid!' % value)
# Choice fields
class EmployeeChoices(AutoModelSelect2Field):
queryset = Employee.objects
search_fields = ['name__icontains', ]
class ClassRoomChoices(AutoModelSelect2MultipleField):
queryset = ClassRoom.objects
search_fields = ['number__icontains', ]
class ClassRoomSingleChoices(AutoModelSelect2Field):
queryset = ClassRoom.objects
search_fields = ['number__icontains', ]
class WordChoices(AutoModelSelect2Field):
queryset = Word.objects
search_fields = ['word__icontains', ]
class MultiWordChoices(AutoModelSelect2MultipleField):
queryset = Word.objects
search_fields = ['word__icontains', ]
class TagField(AutoModelSelect2TagField):
queryset = Tag.objects
search_fields = ['tag__icontains', ]
def get_model_field_values(self, value):
return {'tag': value}
class TagNAField(HeavyModelSelect2TagField):
def get_model_field_values(self, value):
return {'tag': value}
class SelfChoices(AutoSelect2Field):
def get_val_txt(self, value):
if not hasattr(self, 'res_map'):
self.res_map = {}
return self.res_map.get(value, None)
def get_results(self, request, term, page, context):
if not hasattr(self, 'res_map'):
self.res_map = {}
mlen = len(self.res_map)
res = []
for i in range(1, 6):
idx = i + mlen
res.append((idx, term * i,))
self.res_map[idx] = term * i
self.choices = res
return NO_ERR_RESP, False, res
class SelfMultiChoices(AutoSelect2MultipleField):
big_data = {
1: u"First", 2: u"Second", 3: u"Third",
}
def validate_value(self, value):
if value in [v for v in self.big_data]:
return True
else:
return False
def coerce_value(self, value):
return int(value)
def get_val_txt(self, value):
if not hasattr(self, '_big_data'):
self._big_data = dict(self.big_data)
return self._big_data.get(value, None)
def get_results(self, request, term, page, context):
if not hasattr(self, '_big_data'):
self._big_data = dict(self.big_data)
res = [(v, self._big_data[v]) for v in self._big_data]
blen = len(res)
for i in range(1, 6):
idx = i + blen
res.append((idx, term * i,))
self._big_data[idx] = term * i
self.choices = res
return NO_ERR_RESP, False, res
# Forms
class SchoolForm(forms.ModelForm):
classes = ClassRoomChoices()
class Meta:
model = School
fields = ('classes', )
class EmployeeForm(forms.ModelForm):
manager = EmployeeChoices(required=False)
dept = ModelSelect2Field(queryset=Dept.objects)
class Meta:
model = Employee
fields = ('name', 'salary', 'dept', 'manager')
class DeptForm(forms.ModelForm):
allotted_rooms = ClassRoomChoices()
allotted_labs = ModelSelect2MultipleField(queryset=Lab.objects, required=False)
class Meta:
model = Dept
fields = ('name', 'allotted_rooms', 'allotted_labs')
class MixedForm(forms.Form):
emp1 = EmployeeChoices()
rooms1 = ClassRoomChoices()
emp2 = EmployeeChoices()
rooms2 = ClassRoomChoices()
rooms3 = ClassRoomSingleChoices()
any_word = WordChoices()
self_choices = SelfChoices(label='Self copy choices')
self_multi_choices = SelfMultiChoices(label='Self copy multi-choices')
issue11_test = EmployeeChoices(
label='Issue 11 Test (Employee)',
widget=AutoHeavySelect2Widget(
select2_options={
'width': '32em',
'placeholder': u"Search foo"
}
)
)
always_fail_rooms = ClassRoomSingleChoices(validators=[validate_fail_always])
always_fail_rooms_multi = ClassRoomChoices(validators=[validate_fail_always])
always_fail_self_choice = SelfChoices(validators=[validate_fail_always], auto_id='always_fail_self_choice')
always_fail_self_choice_multi = SelfMultiChoices(validators=[validate_fail_always],
auto_id='always_fail_self_choice_multi')
model_with_both_required_and_empty_label_false = ModelSelect2Field(
queryset=Employee.objects, empty_label=None, required=False) # issue#26
# These are just for testing Auto registration of fields
EmployeeChoices() # Should already be registered
EmployeeChoices(auto_id="EmployeeChoices_CustomAutoId") # Should get registered
class InitialValueForm(forms.Form):
select2Choice = Select2ChoiceField(initial=2,
choices=((1, "First"), (2, "Second"), (3, "Third"), ))
select2MultipleChoice = Select2MultipleChoiceField(initial=[2, 3],
choices=((1, "First"), (2, "Second"), (3, "Third"), ))
heavySelect2Choice = AutoSelect2Field(initial=2,
choices=((1, "First"), (2, "Second"), (3, "Third"), ))
heavySelect2MultipleChoice = AutoSelect2MultipleField(initial=[1, 3],
choices=((1, "First"), (2, "Second"), (3, "Third"), ))
self_choices = SelfChoices(label='Self copy choices', initial=2,
choices=((1, "First"), (2, "Second"), (3, "Third"), ))
self_multi_choices = SelfMultiChoices(label='Self copy multi-choices', initial=[2, 3])
select2ChoiceWithQuotes = Select2ChoiceField(initial=2,
choices=((1, "'Single-Quote'"), (2, "\"Double-Quotes\""),
(3, "\"Mixed-Quotes'"), ))
heavySelect2ChoiceWithQuotes = AutoSelect2Field(initial=2,
choices=((1, "'Single-Quote'"), (2, "\"Double-Quotes\""),
(3, "\"Mixed-Quotes'"), ))
class QuestionForm(forms.ModelForm):
question = forms.CharField()
description = forms.CharField(widget=forms.Textarea)
tags = TagField()
class Meta:
model = Question
fields = ('question', 'description', 'tags')
class QuestionNonAutoForm(forms.ModelForm):
question = forms.CharField()
description = forms.CharField(widget=forms.Textarea)
tags = TagNAField(queryset=Tag.objects,
search_fields=['tag__icontains'],
widget=HeavySelect2TagWidget(data_view='test_tagging_tags'))
class Meta:
model = Question
fields = ('question', 'description', 'tags')
class WordsForm(forms.ModelForm):
word = WordChoices()
words = MultiWordChoices()
class Meta:
model = WordList
exclude = ['kind']
class GetSearchTestForm(forms.Form):
name = GetSearchTestField(required=False, label='Name')
dept = GetModelSearchTestField(required=False, label='Department')
class AnotherWordForm(forms.ModelForm):
word = WordChoices(widget=AutoHeavySelect2Widget())
class Meta:
model = WordList
exclude = ['kind', 'words']

View File

@ -1,66 +0,0 @@
from django.db import models
class ClassRoom(models.Model):
number = models.CharField(max_length=4)
def __unicode__(self):
return unicode(self.number)
class Lab(models.Model):
name = models.CharField(max_length=10)
def __unicode__(self):
return unicode(self.name)
class Dept(models.Model):
name = models.CharField(max_length=10)
allotted_rooms = models.ManyToManyField(ClassRoom)
allotted_labs = models.ManyToManyField(Lab)
def __unicode__(self):
return unicode(self.name)
class Employee(models.Model):
name = models.CharField(max_length=30)
salary = models.FloatField()
dept = models.ForeignKey(Dept)
manager = models.ForeignKey('Employee', null=True, blank=True)
def __unicode__(self):
return unicode(self.name)
class Word(models.Model):
word = models.CharField(max_length=15)
def __unicode__(self):
return unicode(self.word)
class School(models.Model):
classes = models.ManyToManyField(ClassRoom)
class Tag(models.Model):
tag = models.CharField(max_length=10, unique=True)
def __unicode__(self):
return unicode(self.tag)
class Question(models.Model):
question = models.CharField(max_length=200)
description = models.CharField(max_length=800)
tags = models.ManyToManyField(Tag)
def __unicode__(self):
return unicode(self.question)
class KeyValueMap(models.Model):
key = models.CharField(max_length=200)
value = models.CharField(max_length=300)
def __unicode__(self):
return u'%s=>%s' % (self.key, self.value)
class WordList(models.Model):
kind = models.CharField(max_length=100)
word = models.ForeignKey(Word, null=True, blank=True, related_name='wordlist_word')
words = models.ManyToManyField(Word, null=True, blank=True, related_name='wordlist_words')

View File

@ -1,27 +0,0 @@
from django.conf.urls import patterns, url
urlpatterns = patterns('testapp.testmain.views',
url(r'single/model/field/$', 'test_single_value_model_field', name='test_single_value_model_field'),
url(r'single/model/field/([0-9]+)/$', 'test_single_value_model_field1', name='test_single_value_model_field1'),
url(r'multi/model/field/$', 'test_multi_values_model_field', name='test_multi_values_model_field'),
url(r'multi/model/field/([0-9]+)/$', 'test_multi_values_model_field1', name='test_multi_values_model_field1'),
url(r'mixed/form/$', 'test_mixed_form', name='test_mixed_form'),
url(r'initial/form/$', 'test_init_values', name='test_init_values'),
url(r'question/$', 'test_list_questions', name='test_list_questions'),
url(r'question/form/([0-9]+)/$', 'test_tagging', name='test_tagging'),
url(r'question/form/([0-9]+)/na/$', 'test_tagging_non_auto', name='test_tagging_non_auto'),
url(r'question/form/$', 'test_tagging_new', name='test_tagging_new'),
url(r'question/tags/$', 'test_tagging_tags', name='test_tagging_tags'),
url(r'auto_model/form/$', 'test_auto_multivalue_field', name='test_auto_multivalue_field'),
url(r'auto_heavy/perf_test/$', 'test_auto_heavy_perf', name='test_auto_heavy_perf'),
url(r'get_search/get_search_test/$', 'test_get_search_form', name='test_get_search_form'),
url(r'issue76/$', 'test_issue_73', name='test_issue_73'),
)

View File

@ -1,165 +0,0 @@
import json
from django.core.urlresolvers import reverse
from django.http import HttpResponseRedirect, HttpResponse
from django.shortcuts import render, get_object_or_404
from .forms import EmployeeForm, DeptForm, MixedForm, InitialValueForm, QuestionForm, QuestionNonAutoForm, WordsForm, SchoolForm, \
GetSearchTestForm, AnotherWordForm
from .models import Employee, Dept, Question, WordList, School, Tag
def test_single_value_model_field(request):
return render(request, 'list.html', {
'title': 'Employees',
'href': 'test_single_value_model_field1',
'object_list': Employee.objects.all(),
'create_new_href': ''
})
def test_single_value_model_field1(request, id):
emp = get_object_or_404(Employee, pk=id)
if request.POST:
form = EmployeeForm(data=request.POST, instance=emp)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('home'))
else:
form = EmployeeForm(instance=emp)
return render(request, 'form.html', {'form': form})
def test_multi_values_model_field(request):
return render(request, 'list.html', {
'title': 'Departments',
'href': 'test_multi_values_model_field1',
'object_list': Dept.objects.all(),
'create_new_href': ''
})
def test_multi_values_model_field1(request, id):
dept = get_object_or_404(Dept, pk=id)
if request.POST:
form = DeptForm(data=request.POST, instance=dept)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('home'))
else:
form = DeptForm(instance=dept)
return render(request, 'form.html', {'form': form})
def test_mixed_form(request):
if request.POST:
form = MixedForm(request.POST)
form.is_valid()
else:
form = MixedForm()
return render(request, 'form.html', {'form': form})
def test_init_values(request):
return render(request, 'form.html', {'form': InitialValueForm()})
def test_list_questions(request):
return render(request, 'list.html', {
'title': 'Questions',
'href': 'test_tagging',
'href_non_auto': 'test_tagging_non_auto',
'object_list': Question.objects.all(),
'create_new_href': 'test_tagging_new'
})
def test_tagging_new(request):
return test_tagging(request, None)
def test_tagging(request, id):
if id is None:
question = Question()
else:
question = get_object_or_404(Question, pk=id)
if request.POST:
form = QuestionForm(data=request.POST, instance=question)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('home'))
else:
form = QuestionForm(instance=question)
return render(request, 'form.html', {'form': form})
def test_tagging_non_auto(request, id):
if id is None:
question = Question()
else:
question = get_object_or_404(Question, pk=id)
if request.POST:
form = QuestionNonAutoForm(data=request.POST, instance=question)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('home'))
else:
form = QuestionNonAutoForm(instance=question)
return render(request, 'form.html', {'form': form})
def test_tagging_tags(request):
tags = Tag.objects.all()
results = [{'id': t.id, 'text': t.tag} for t in tags]
return HttpResponse(json.dumps({'err': 'nil', 'results': results}), content_type='application/json')
def test_auto_multivalue_field(request):
try:
s = School.objects.get(id=1)
except School.DoesNotExist:
s = School(id=1)
if request.POST:
form = SchoolForm(data=request.POST, instance=s)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('home'))
else:
form = SchoolForm(instance=s)
return render(request, 'form.html', {'form': form})
def test_auto_heavy_perf(request):
try:
word = WordList.objects.get(kind='Word_Of_Day')
except WordList.DoesNotExist:
word = WordList(kind='Word_Of_Day')
if request.POST:
form = WordsForm(data=request.POST, instance=word)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('home'))
else:
form = WordsForm(instance=word)
return render(request, 'form.html', {'form': form})
def test_get_search_form(request):
"""
Test a search form using GET. Issue#66
"""
if request.GET:
form = GetSearchTestForm(request.GET)
if form.is_valid():
results = Employee.objects.all()
if form.cleaned_data['name'] != []:
results = results.filter(name__in = form.cleaned_data['name'])
if form.cleaned_data['dept'] != []:
results = results.filter(dept__in = form.cleaned_data['dept'])
else:
form = GetSearchTestForm()
results = Employee.objects.none()
return render(request, 'formget.html', {'form': form, 'results' : results})
def test_issue_73(request):
try:
word = WordList.objects.get(kind='Word_Of_Day')
except WordList.DoesNotExist:
word = WordList(kind='Word_Of_Day')
if request.POST:
form = AnotherWordForm(request.POST)
if form.is_valid():
form.save()
return HttpResponseRedirect(reverse('home'))
else:
form = AnotherWordForm(instance=word)
return render(request, 'form.html', {'form': form})

View File

@ -1,12 +0,0 @@
from django.conf.urls import patterns, include, url
from django.views.generic import TemplateView
from django.contrib import admin
admin.autodiscover()
urlpatterns = patterns('',
url(r'^admin/', include(admin.site.urls)),
url(r'^$', TemplateView.as_view(template_name="index.html"), name='home'),
url(r'^test/', include('testapp.testmain.urls')),
url(r'^ext/', include('django_select2.urls')),
)

View File

@ -1,28 +0,0 @@
"""
WSGI config for testapp project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testapp.settings")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)

View File

@ -4,12 +4,13 @@ from __future__ import absolute_import, print_function, unicode_literals
import os
import pytest
from model_mommy import mommy
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
browsers = {
'firefox': webdriver.Firefox,
'chrome': webdriver.Chrome,
# 'firefox': webdriver.Firefox,
# 'chrome': webdriver.Chrome,
'phantomjs': webdriver.PhantomJS,
}
@ -28,3 +29,13 @@ def driver(request):
b.set_window_size(1200, 800)
request.addfinalizer(lambda *args: b.quit())
return b
@pytest.fixture
def genres(db):
return mommy.make('testapp.Genre', _quantity=100)
@pytest.fixture
def artists(db):
return mommy.make('testapp.Artist', _quantity=100)

View File

@ -1,11 +1,16 @@
# -*- coding:utf-8 -*-
from __future__ import print_function, unicode_literals
import json
import pytest
from django.core.urlresolvers import reverse
from model_mommy import mommy
from django.utils.encoding import smart_text
from selenium.common.exceptions import NoSuchElementException
from django_select2.types import NO_ERR_RESP
from tests.testapp.forms import AlbumForm, ArtistForm
class ViewTestMixin(object):
url = ''
@ -15,11 +20,6 @@ class ViewTestMixin(object):
assert response.status_code == 200
@pytest.fixture
def genres(db):
mommy.make('testapp.Genre', _quantity=100)
class TestAutoModelSelect2TagField(object):
url = reverse('single_value_model_field')
@ -28,3 +28,23 @@ class TestAutoModelSelect2TagField(object):
with pytest.raises(NoSuchElementException):
error = driver.find_element_by_xpath('//body[@JSError]')
pytest.fail(error.get_attribute('JSError'))
def test_form(self):
form = ArtistForm()
assert form
class TestAutoModelSelect2Field(object):
def test_form(self, client, artists):
artist = artists[0]
form = AlbumForm()
assert form.as_p()
field_id = form.fields['artist'].widget.widget_id
url = reverse('django_select2_central_json')
response = client.get(url, {'field_id': field_id, 'term': artist.title})
assert response.status_code == 200
data = json.loads(response.content.decode('utf-8'))
assert data['results']
assert {'id': artist.pk, 'text': smart_text(artist)} in data['results']
assert data['more'] is False
assert data['err'] == NO_ERR_RESP

View File

@ -12,8 +12,8 @@ from model_mommy import mommy
from selenium.common.exceptions import NoSuchElementException
from six import text_type
from django_select2 import AutoHeavySelect2Widget
from django_select2.cache import cache
from django_select2.forms import AutoHeavySelect2Widget
from tests.testapp.models import Genre
@ -21,12 +21,12 @@ class TestWidgets(object):
url = ""
def test_is_hidden_multiple(self):
from django_select2.widgets import HeavySelect2MultipleWidget
from django_select2.forms import HeavySelect2MultipleWidget
new_widget = HeavySelect2MultipleWidget(data_url="/")
assert new_widget.is_hidden is False
def test_is_hidden(self):
from django_select2.widgets import HeavySelect2Widget
from django_select2.forms import HeavySelect2Widget
new_widget = HeavySelect2Widget(data_url="/")
assert new_widget.is_hidden is False

View File

@ -3,11 +3,11 @@ from __future__ import absolute_import, unicode_literals
from django import forms
from django_select2.fields import Select2MultipleWidget
from django_select2.widgets import Select2Widget, HeavySelect2Widget, HeavySelect2MultipleWidget
from django_select2.forms import (
HeavySelect2MultipleWidget, HeavySelect2Widget, Select2MultipleWidget,
Select2Widget
)
from tests.testapp import models
from . import fields
class GenreModelForm(forms.ModelForm):
@ -24,6 +24,7 @@ class GenreForm(forms.Form):
class ArtistModelForm(forms.ModelForm):
test = forms.BooleanField('asdf')
class Meta:
model = models.Artist
fields = (
@ -37,7 +38,10 @@ class ArtistModelForm(forms.ModelForm):
class ArtistForm(forms.Form):
title = forms.CharField(max_length=50)
genres = fields.GenreTagField()
genres = forms.ModelMultipleChoiceField(widget=HeavySelect2MultipleWidget(
queryset=models.Genre.objects.all(),
search_fields=['title'],
), queryset=models.Genre.objects.all())
class AlbumModelForm(forms.ModelForm):
@ -51,14 +55,19 @@ class AlbumModelForm(forms.ModelForm):
class AlbumForm(forms.Form):
title = forms.CharField(max_length=255)
artist = fields.ArtistField()
artist = forms.ModelChoiceField(widget=HeavySelect2Widget(
model=models.Artist,
search_fields=['title']
), queryset=models.Artist.objects.all())
class Select2WidgetForm(forms.Form):
NUMBER_CHOICES = [ (1, 'One'),
(2, 'Two'),
(3, 'Three'),
(4, 'Four') ]
NUMBER_CHOICES = [
(1, 'One'),
(2, 'Two'),
(3, 'Three'),
(4, 'Four'),
]
number = forms.ChoiceField(widget=Select2Widget(), choices=NUMBER_CHOICES)

View File

@ -1,14 +0,0 @@
# -*- coding:utf-8 -*-
from __future__ import absolute_import, unicode_literals
from django_select2.fields import (AutoModelSelect2Field,
AutoModelSelect2TagField)
from tests.testapp import models
class GenreTagField(AutoModelSelect2TagField):
queryset = models.Genre
class ArtistField(AutoModelSelect2Field):
queryset = models.Artist

View File

@ -1,5 +1,4 @@
{% load staticfiles %}
{% load django_select2_tags %}
<!DOCTYPE html>
<html>
<head>
@ -9,8 +8,7 @@
$("body").attr("JSError", msg);
}
</script>
{% import_django_select2_js %}
{% import_django_select2_css %}
{{ form.media }}
</head>
<body>
<form method="post" action="">

View File

@ -3,8 +3,10 @@ from __future__ import absolute_import, unicode_literals
from django.conf.urls import include, patterns, url
from .forms import (ArtistForm, HeavySelect2MultipleWidgetForm,
HeavySelect2WidgetForm, Select2WidgetForm)
from .forms import (
ArtistForm, HeavySelect2MultipleWidgetForm, HeavySelect2WidgetForm,
Select2WidgetForm
)
from .views import TemplateFormView, heavy_data
urlpatterns = patterns(