"""
Contains all the Django fields for Select2.
"""
import logging
logger = logging.getLogger(__name__)
[docs]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.
"""
[docs] def __init__(self, *args, **kwargs):
"""
Class constructor.
:param auto_id: The key to use while registering this field. If it is not provided then
an auto generated key is used.
.. tip::
This mixin uses full class name of the field to register itself. This is
used like key in a :py:obj:`dict` by :py:func:`.util.register_field`.
If that key already exists then the instance is not registered again. So, eventually
all instances of an Auto field share one instance to respond to the Ajax queries for
its fields.
If for some reason any instance needs to be isolated then ``auto_id`` can be used to
provide a unique key which has never occured before.
:type auto_id: :py:obj:`unicode`
"""
name = kwargs.pop('auto_id', u"%s.%s" % (self.__module__, self.__class__.__name__))
if logger.isEnabledFor(logging.INFO):
logger.info("Registering auto field: %s", name)
from . import util
id_ = util.register_field(name, self)
self.field_id = id_
super(AutoViewFieldMixin, self).__init__(*args, **kwargs)
[docs] def security_check(self, request, *args, **kwargs):
"""
Returns ``False`` if security check fails.
:param request: The Ajax request object.
:type request: :py:class:`django.http.HttpRequest`
:param args: The ``*args`` passed to :py:meth:`django.views.generic.base.View.dispatch`.
:param kwargs: The ``**kwargs`` passed to :py:meth:`django.views.generic.base.View.dispatch`.
:return: A boolean value, signalling if check passed or failed.
:rtype: :py:obj:`bool`
.. warning:: Sub-classes should override this. You really do not want random people making
Http reqeusts to your server, be able to get access to sensitive information.
"""
return True
[docs] def get_results(self, request, term, page, context):
"See :py:meth:`.views.Select2View.get_results`."
raise NotImplementedError
import copy
from django import forms
from django.core import validators
from django.core.exceptions import ValidationError
from django.forms.models import ModelChoiceIterator
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import smart_unicode
from django.core.validators import EMPTY_VALUES
from .widgets import Select2Widget, Select2MultipleWidget,\
HeavySelect2Widget, HeavySelect2MultipleWidget, AutoHeavySelect2Widget, \
AutoHeavySelect2MultipleWidget
from .views import NO_ERR_RESP
from .util import extract_some_key_val
### Light general fields ###
[docs]class Select2ChoiceField(forms.ChoiceField):
"""
Drop-in Select2 replacement for :py:class:`forms.ChoiceField`.
"""
widget = Select2Widget
[docs]class Select2MultipleChoiceField(forms.MultipleChoiceField):
"""
Drop-in Select2 replacement for :py:class:`forms.MultipleChoiceField`.
"""
widget = Select2MultipleWidget
### Model fields related mixins ###
[docs]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.
"""
[docs] def __init__(self, *args, **kwargs):
"""
Class constructor.
:param queryset: This can be passed as kwarg here or defined as field variabel,
like ``search_fields``.
:type queryset: :py:class:`django.db.models.query.QuerySet` or None
:param max_results: Maximum number to results to return per Ajax query.
:type max_results: :py:obj:`int`
:param to_field_name: Which field's value should be returned as result tuple's
value. (Default is ``pk``, i.e. the id field of the model)
:type to_field_name: :py:obj:`str`
"""
if self.queryset is None and not kwargs.has_key('queryset'):
raise ValueError('queryset is required.')
if not self.search_fields:
raise ValueError('search_fields is required.')
self.max_results = getattr(self, 'max_results', None)
self.to_field_name = getattr(self, 'to_field_name', 'pk')
super(ModelResultJsonMixin, self).__init__(*args, **kwargs)
[docs] 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`
"""
return smart_unicode(obj)
[docs] def prepare_qs_params(self, request, search_term, search_fields):
"""
Prepares 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 = None
for field in search_fields:
kwargs = {}
kwargs[field] = search_term
if q is None:
q = Q(**kwargs)
else:
q = q | Q(**kwargs)
return {'or': [q], 'and': {},}
[docs] def get_results(self, request, term, page, context):
"""
See :py:meth:`.views.Select2View.get_results`.
This implementation takes care of detecting if more results are available.
"""
qs = copy.deepcopy(self.queryset)
params = self.prepare_qs_params(request, term, self.search_fields)
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 = list(qs.filter(*params['or'], **params['and'])[min_:max_])
has_more = len(res) == (max_ - min_)
if has_more:
res = res[:-1]
else:
res = list(qs.filter(*params['or'], **params['and']))
has_more = False
res = [ (getattr(obj, self.to_field_name), self.label_from_instance(obj), ) for obj in res ]
return (NO_ERR_RESP, has_more, res, )
[docs]class UnhideableQuerysetType(type):
"""
This does some pretty nasty hacky stuff, to make sure users can
also define ``queryset`` as class-level field variable, instead of
passing it to constructor.
"""
# TODO check for alternatives. Maybe this hack is not necessary.
def __new__(cls, name, bases, dct):
_q = dct.get('queryset', None)
if _q is not None and not isinstance(_q, property):
# This hack is needed since users are allowed to
# provide queryset in sub-classes by declaring
# class variable named - queryset, which will
# effectively hide the queryset declared in this
# mixin.
dct.pop('queryset') # Throwing away the sub-class queryset
dct['_subclass_queryset'] = _q
return type.__new__(cls, name, bases, dct)
def __call__(cls, *args, **kwargs):
queryset = kwargs.get('queryset', None)
if not queryset and hasattr(cls, '_subclass_queryset'):
kwargs['queryset'] = getattr(cls, '_subclass_queryset')
return type.__call__(cls, *args, **kwargs)
[docs]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)
result._choices = copy.deepcopy(self._choices, memo)
return result
[docs]class QuerysetChoiceMixin(ChoiceMixin):
"""
Overrides ``choices``' getter to return instance of :py:class:`.ModelChoiceIterator`
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 ModelChoiceIterator(self)
choices = property(_get_choices, ChoiceMixin._set_choices)
class ModelChoiceFieldMixin(object):
def __init__(self, *args, **kwargs):
queryset = kwargs.pop('queryset', None)
kargs = extract_some_key_val(kwargs, [
'empty_label', 'cache_choices', 'required', 'label', 'initial', 'help_text',
])
kargs['widget'] = kwargs.pop('widget', getattr(self, 'widget', None))
kargs['to_field_name'] = kwargs.pop('to_field_name', 'pk')
if hasattr(self, '_choices'): # If it exists then probably it is set by HeavySelect2FieldBase.
# We are not gonna use that anyway.
del self._choices
super(ModelChoiceFieldMixin, self).__init__(queryset, **kargs)
if hasattr(self, 'set_placeholder'):
self.widget.set_placeholder(self.empty_label)
def _get_queryset(self):
if hasattr(self, '_queryset'):
return self._queryset
### Slightly altered versions of the Django counterparts with the same name in forms module. ###
class ModelChoiceField(ModelChoiceFieldMixin, forms.ModelChoiceField):
queryset = property(ModelChoiceFieldMixin._get_queryset, forms.ModelChoiceField._set_queryset)
class ModelMultipleChoiceField(ModelChoiceFieldMixin, forms.ModelMultipleChoiceField):
queryset = property(ModelChoiceFieldMixin._get_queryset, forms.ModelMultipleChoiceField._set_queryset)
### Light Fileds specialized for Models ###
[docs]class ModelSelect2Field(ModelChoiceField) :
"""
Light Select2 field, specialized for Models.
Select2 replacement for :py:class:`forms.ModelChoiceField`.
"""
widget = Select2Widget
[docs]class ModelSelect2MultipleField(ModelMultipleChoiceField) :
"""
Light multiple-value Select2 field, specialized for Models.
Select2 replacement for :py:class:`forms.ModelMultipleChoiceField`.
"""
widget = Select2MultipleWidget
### Heavy fields ###
[docs]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.
"""
[docs] 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', [])
kargs = {}
if data_view is not None:
kargs['widget'] = self.widget(data_view=data_view)
elif kwargs.get('widget', None) is None:
raise ValueError('data_view is required else you need to provide your own widget instance.')
kargs.update(kwargs)
super(HeavySelect2FieldBaseMixin, self).__init__(*args, **kargs)
# By this time self.widget would have been instantiated.
# This piece of code is needed here since (God knows why) Django's Field class does not call
# super(); because of that __init__() of classes would get called after Field.__init__().
# If did had super() call there then we could have simply moved AutoViewFieldMixin at the
# end of the MRO list. This way it would have got widget instance instead of class and it
# could have directly set field_id on it.
if hasattr(self, 'field_id'):
self.widget.field_id = self.field_id
if not choices and hasattr(self, 'choices'): # ModelChoiceField will set this to ModelChoiceIterator
choices = self.choices
self.choices = choices
[docs]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': _(u'Select a valid choice. %(value)s is not one of the available choices.'),
}
empty_value = u''
"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_unicode(value)
for k, v in self.choices:
if uvalue == smart_unicode(k):
return True
return self.validate_value(value)
[docs] 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_unicode(value)
[docs] 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, e:
logger.exception("Exception while trying to get label for value")
return None
return self.get_val_txt(value)
[docs] 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
[docs]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': _(u'Select a valid choice. %(value)s is not one of the available choices.'),
'invalid_list': _(u'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})
[docs]class HeavySelect2ChoiceField(HeavySelect2FieldBaseMixin, HeavyChoiceField):
"Heavy Select2 Choice field."
widget = HeavySelect2Widget
[docs]class HeavySelect2MultipleChoiceField(HeavySelect2FieldBaseMixin, HeavyMultipleChoiceField):
"Heavy Select2 Multiple Choice field."
widget = HeavySelect2MultipleWidget
### Heavy field specialized for Models ###
[docs]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)
[docs]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)
### Heavy general field that uses central AutoView ###
[docs]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
def __init__(self, *args, **kwargs):
self.data_view = "django_select2_central_json"
kwargs['data_view'] = self.data_view
super(AutoSelect2Field, self).__init__(*args, **kwargs)
[docs]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
def __init__(self, *args, **kwargs):
self.data_view = "django_select2_central_json"
kwargs['data_view'] = self.data_view
super(AutoSelect2MultipleField, self).__init__(*args, **kwargs)
### Heavy field, specialized for Model, that uses central AutoView ###
[docs]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).
"""
__metaclass__ = UnhideableQuerysetType # Makes sure that user defined queryset class variable is replaced by
# queryset property (as it is needed by super classes).
widget = AutoHeavySelect2Widget
def __init__(self, *args, **kwargs):
self.data_view = "django_select2_central_json"
kwargs['data_view'] = self.data_view
super(AutoModelSelect2Field, self).__init__(*args, **kwargs)
[docs]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).
"""
__metaclass__ = UnhideableQuerysetType # Makes sure that user defined queryset class variable is replaced by
# queryset property (as it is needed by super classes).
widget = AutoHeavySelect2MultipleWidget
def __init__(self, *args, **kwargs):
self.data_view = "django_select2_central_json"
kwargs['data_view'] = self.data_view
super(AutoModelSelect2MultipleField, self).__init__(*args, **kwargs)