"""
Contains all the Django widgets for Select2.
"""
import logging
from itertools import chain
from django import forms
from django.utils.encoding import force_unicode
from django.utils.safestring import mark_safe
from django.core.urlresolvers import reverse
from django.utils.datastructures import MultiValueDict, MergeDict
from .util import render_js_script, convert_to_js_string_arr, JSVar, JSFunction, JSFunctionInContext, \
convert_dict_to_js_map, convert_to_js_arr
logger = logging.getLogger(__name__)
def get_select2_js_path():
from django.conf import settings
if settings.configured and settings.DEBUG:
return 'js/select2.js'
else:
return 'js/select2.min.js'
### Light mixin and widgets ###
[docs]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>``
markups into Select2 choice list.
The following Select2 otions are added by this mixin:-
* minimumResultsForSearch: ``6``
* placeholder: ``''``
* allowClear: ``True``
* multiple: ``False``
* closeOnSelect: ``False``
.. note:: Many of them would be removed by sub-classes depending on requirements.
"""
# For details on these options refer: http://ivaynberg.github.com/select2/#documentation
options = {
'minimumResultsForSearch': 6, # Only applicable for single value select.
'placeholder': '', # Empty text label
'allowClear': True, # Not allowed when field is multiple since there each value has a clear button.
'multiple': False, # Not allowed when attached to <select>
'closeOnSelect': False,
}
"""
The options listed in this are rendered as JS map and passed to Select2 JS code.
The complete description of theses options are available in Select2_ JS' site.
.. _Select2: http://ivaynberg.github.com/select2/#documentation.
"""
[docs] def __init__(self, **kwargs):
"""
Constructor of the class.
The following additional kwarg is allowed:-
:param select2_options: This is similar to standard Django way to pass extra attributes to widgets.
This is meant to override values of existing :py:attr:`.options`.
Example::
class MyForm(ModelForm):
class Meta:
model = MyModel
widgets = {
'name': Select2WidgetName(select2_options={
'minimumResultsForSearch': 10,
'closeOnSelect': True,
})
}
.. tip:: You cannot introduce new options using this. For that you should sub-class and overried
: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 ``<select>`` tag,
it can get if it is multiple or single valued from that tag itself. In this case if you specify
``multiple`` option then not only it is useless but an error in 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 concotion of options.
:type select2_options: :py:obj:`dict` or None
"""
self.options = dict(self.options) # Making an instance specific copy
self.init_options()
select2_options = kwargs.pop('select2_options', None)
if select2_options:
for name in self.options:
val = self.options[name]
self.options[name] = select2_options.get(name, val)
super(Select2Mixin, self).__init__(**kwargs)
[docs] def init_options(self):
"""
Sub-classes can use this to pass additional options to Select2 JS library.
Example::
def init_options(self):
self.options['createSearchChoice'] = JSFunction('Your_js_function')
In the above example we are setting ``Your_js_function`` as Select2's ``createSearchChoice``
function.
.. tip:: If you want to run ``Your_js_function`` in the context of the Select2 DOM element,
i.e. ``this`` inside your JS function should point to the component instead of ``window``, then
use :py:class:`~.util.JSFunctionInContext` instead of :py:class:`~.util.JSFunction`.
"""
pass
[docs] def set_placeholder(self, val):
"""
Placeholder is a value which Select2 JS library shows when nothing is selected. This should be string.
:return: None
"""
self.options['placeholder'] = val
[docs] def get_options(self):
"""
:return: Dictionary of options to be passed to Select2 JS.
:rtype: :py:obj:`dict`
"""
options = dict(self.options)
if options.get('allowClear', None) is not None:
options['allowClear'] = not self.is_required
return options
[docs] def render_select2_options_code(self, options, id_):
"""
Renders options for Select2 JS.
:return: The rendered JS code.
:rtype: :py:obj:`unicode`
"""
return convert_dict_to_js_map(options, id_)
[docs] def render_js_code(self, id_, *args):
"""
Renders the ``<script>`` block which contains the JS code for this widget.
:return: The rendered JS code enclosed inside ``<script>`` block.
:rtype: :py:obj:`unicode`
"""
if id_:
return render_js_script(self.render_inner_js_code(id_, *args))
return u''
[docs] def render_inner_js_code(self, id_, *args):
"""
Renders 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`
"""
options = dict(self.get_options())
options = self.render_select2_options_code(options, id_)
return u'$("#%s").select2(%s);' % (id_, options)
[docs] 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)
s = unicode(super(Select2Mixin, self).render(*args)) # Thanks to @ouhouhsami Issue#1
s += self.media.render()
final_attrs = self.build_attrs(attrs)
id_ = final_attrs.get('id', None)
s += self.render_js_code(id_, name, value, attrs, choices)
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Generated widget code:-\n%s", s)
return mark_safe(s)
class Media:
js = (get_select2_js_path(), )
css = {'screen': ('css/select2.css', 'css/extra.css', )}
[docs]class HeavySelect2Mixin(Select2Mixin):
"""
The base mixin of all Heavy Select2 widgets. It sub-classes :py:class:`Select2Mixin`.
This mixin adds more Select2 options to :py:attr:`.Select2Mixin.options`. These are:-
* minimumInputLength: ``2``
* initSelection: ``JSFunction('django_select2.onInit')``
* ajax:
* dataType: ``'json'``
* quietMillis: ``100``
* data: ``JSFunctionInContext('django_select2.get_url_params')``
* results: ``JSFunctionInContext('django_select2.process_results')``
.. tip:: You can override these options by passing ``select2_options`` kwarg to :py:meth:`.__init__`.
"""
[docs] def __init__(self, **kwargs):
"""
Constructor of the class.
The following kwargs are allowed:-
: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.
:type data_url: :py:obj:`str` or None
.. tip:: When ``data_view`` is provided then it is converted into Url using
:py:func:`~django.core.urlresolvers.reverse`.
.. warning:: Either of ``data_view`` or ``data_url`` must be specified, else :py:exc:`ValueError` would
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'), ...]``.
: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.
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.
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``.
4. If we still do not have the label then check the cookies. When user selects some value then
``heavy_data.js`` stores the selected values and their labels in the cookies. These are cleared
when browser is closed.
"""
self.field = None
self.options = dict(self.options) # Making an instance specific copy
self.view = kwargs.pop('data_view', None)
self.url = kwargs.pop('data_url', None)
self.userGetValTextFuncName = kwargs.pop('userGetValTextFuncName', u'null')
self.choices = kwargs.pop('choices', [])
if not self.view and not self.url:
raise ValueError('data_view or data_url is required')
self.options['ajax'] = {
'dataType': 'json',
'quietMillis': 100,
'data': JSFunctionInContext('django_select2.get_url_params'),
'results': JSFunctionInContext('django_select2.process_results'),
}
self.options['minimumInputLength'] = 2
self.options['initSelection'] = JSFunction('django_select2.onInit')
super(HeavySelect2Mixin, self).__init__(**kwargs)
[docs] def render_texts(self, selected_choices, choices):
"""
Renders 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`
:param choices: Extra choices, if any. This is a list of tuples. In each tuple, the first
item is the choice value and the second item is choice label.
:type choices: :py:obj:`list` or :py:obj:`tuple`
:return: The rendered JS array code.
:rtype: :py:obj:`unicode`
"""
selected_choices = list(force_unicode(v) for v in selected_choices)
txts = []
all_choices = choices if choices else []
for val, txt in chain(self.choices, all_choices):
val = force_unicode(val)
if val in selected_choices:
selected_choices = [v for v in selected_choices if v != val]
txts.append(txt)
if hasattr(self.field, '_get_val_txt') and selected_choices:
for val in selected_choices:
txt = self.field._get_val_txt(val)
if txt is not None:
txts.append(txt)
if txts:
return convert_to_js_string_arr(txts)
def get_options(self):
if self.url is None:
self.url = reverse(self.view) # We lazy resolve the view. By this time Url conf would been loaded fully.
if self.options['ajax'].get('url', None) is None:
self.options['ajax']['url'] = self.url
return super(HeavySelect2Mixin, self).get_options()
[docs] 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`.
: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`
:param value: Currently set value on the field.
:type value: Any
:param choices: Extra choices, if any. This is a list of tuples. In each tuple, the first
item is the choice value and the second item is choice label.
:type choices: :py:obj:`list` or :py:obj:`tuple`
:return: JS code which sets the ``txt`` attribute.
:rtype: :py:obj:`unicode`
"""
if value is not None:
values = [value] # Just like forms.Select.render() it assumes that value will be single valued.
texts = self.render_texts(values, choices)
if texts:
return u"$('#%s').txt(%s);" % (id_, texts)
def render_inner_js_code(self, id_, name, value, attrs=None, choices=(), *args):
js = u"$('#%s').change(django_select2.onValChange).data('userGetValText', %s);" \
% (id_, self.userGetValTextFuncName)
texts = self.render_texts_for_value(id_, value, choices)
if texts:
js += texts
js += super(HeavySelect2Mixin, self).render_inner_js_code(id_, name, value, attrs, choices, *args)
return js
class Media:
js = (get_select2_js_path(), 'js/heavy_data.js', )
css = {'screen': ('css/select2.css', 'css/extra.css', )}
[docs]class AutoHeavySelect2Mixin(object):
"""
This mixin is needed for Auto heavy fields.
This mxin 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.
"""
def render_inner_js_code(self, id_, *args):
js = u"$('#%s').data('field_id', '%s');" % (id_, self.field_id)
js += super(AutoHeavySelect2Mixin, self).render_inner_js_code(id_, *args)
return js