Radical removal of all unneeded code
This commit is contained in:
parent
33b7dffca1
commit
95297a362e
|
@ -8,6 +8,8 @@ Django_Select2_Py3.egg-info
|
|||
dist
|
||||
build
|
||||
|
||||
node_modules/
|
||||
|
||||
docs/_build
|
||||
|
||||
# Intellij
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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.
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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,
|
||||
}
|
||||
|
|
10
pytest.ini
10
pytest.ini
|
@ -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
|
|
@ -1,10 +1,9 @@
|
|||
pytest
|
||||
pytest-pep8
|
||||
pytest-flakes
|
||||
pytest-django
|
||||
pytest-isort
|
||||
pep257
|
||||
selenium
|
||||
model_mommy
|
||||
isort
|
||||
requests
|
||||
flake8
|
||||
pep8-naming
|
|
@ -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
|
|
@ -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)
|
BIN
testapp/test.db
BIN
testapp/test.db
Binary file not shown.
|
@ -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&l59kx$8%&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
|
@ -1,7 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
</head>
|
||||
<body>
|
||||
Error 500
|
||||
</body>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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)
|
|
@ -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
|
@ -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']
|
|
@ -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')
|
|
@ -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'),
|
||||
)
|
|
@ -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})
|
|
@ -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')),
|
||||
)
|
|
@ -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)
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
|
@ -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="">
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue