django-select2/django_select2/forms.py

221 lines
7.1 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
2015-08-06 09:58:27 +00:00
"""Contains all the Django widgets for Select2."""
from __future__ import absolute_import, unicode_literals
import logging
from functools import reduce
from django import forms
from django.core import signing
2015-09-06 03:23:24 +00:00
from django.core.urlresolvers import reverse_lazy
2015-08-06 09:58:27 +00:00
from django.db.models import Q
2015-09-15 22:04:33 +00:00
from django.forms.models import ModelChoiceIterator
2015-08-10 12:19:46 +00:00
from django.utils.encoding import force_text
from .cache import cache
from .conf import settings
logger = logging.getLogger(__name__)
# ## Light mixin and widgets ##
class Select2Mixin(object):
2015-08-06 09:58:27 +00:00
"""
The base mixin of all Select2 widgets.
2015-08-06 09:58:27 +00:00
This mixin is responsible for rendering the necessary
2015-09-06 03:23:24 +00:00
data attributes for select2 as well as adding the static
form media.
"""
2015-09-06 03:23:24 +00:00
def build_attrs(self, extra_attrs=None, **kwargs):
attrs = super(Select2Mixin, self).build_attrs(extra_attrs=None, **kwargs)
2015-09-15 22:04:33 +00:00
if self.is_required:
attrs.setdefault('data-allow-clear', 'false')
else:
attrs.setdefault('data-allow-clear', 'true')
attrs.setdefault('data-placeholder', '')
attrs.setdefault('data-minimumInputLength', 0)
2015-09-06 03:23:24 +00:00
if 'class' in attrs:
attrs['class'] += ' django-select2'
else:
attrs['class'] = 'django-select2'
return attrs
2015-09-15 22:04:33 +00:00
def render_options(self, choices, selected_choices):
output = '<option></option>\n' if not self.is_required else ''
output += super(Select2Mixin, self).render_options(choices, selected_choices)
return output
Made widget media a dynamic property Symptoms: While using AutoModelSelect2Field in admin site, I noticed that css files are rendered only at the first request to the add or edit forms, after that the widget is rendered but without loading any css files from get_select2_css_libs. Solution: I changed assets loading from Assets as a static definition to Media as a dynamic property as described at Django docs: https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property Closed #179 Squashed commit of the following: commit f925ed4a118687b82b6f61d21a9104ccf00859c8 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:39:47 2015 +0300 remove trailing whitespace from docstrings commit b3f6553e422e19c8e065e026511c4ffd91ffee42 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:35:15 2015 +0300 Remove blank lines from docstrings commit 4490b78572a25069472933c88ebee48445b59972 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:19:38 2015 +0300 construct Media the right way Remove code that access private attributes of Media class. commit 6697cd734daca62ff099be73d0685ce6af32e53d Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:11:29 2015 +0300 fix change _media to _get_media I hope it is the final commit :) commit 59bda01aed44e5658825a1bfe37ad09c5760a3ad Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:09:35 2015 +0300 get_select2_heavy_js_libs change get_select2_js_libs to get_select2_heavy_js_libs commit 993e201355c9d5a6012e6f67cfa64462576c2570 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:07:57 2015 +0300 fix fix commit f66e5f40b9f4df295bc5aaa7f2aafe588b2c41bb Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:55:53 2015 +0300 fix fix commit d33d90278345bc32e80e20bdf782fd9ac5eb1800 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:47:25 2015 +0300 fix fix commit f6d956149d2e99ee504c7b2c110f959f7472a181 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:45:09 2015 +0300 fix fix commit 6d737a78cc07507c5b9b91736f6662b5f6f31c57 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:40:01 2015 +0300 global name 'Media' is not defined global name 'Media' is not defined commit 661a817b092dc72bfa8df1a5458d474599c0a50e Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:27:27 2015 +0300 Media as a dynamic property **Symptoms:** While using AutoModelSelect2Field in admin site, I noticed that css files are rendered only at the first request to the add or edit forms, after that the widget is rendered but without loading any css files from get_select2_css_libs. **Solution:** I changed assets loading from [Assets as a static definition](https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property) to [Media as a dynamic property](https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property) as described at Django docs.
2015-06-19 12:49:51 +00:00
def _get_media(self):
"""
2015-08-06 09:58:27 +00:00
Construct Media as a dynamic property.
Made widget media a dynamic property Symptoms: While using AutoModelSelect2Field in admin site, I noticed that css files are rendered only at the first request to the add or edit forms, after that the widget is rendered but without loading any css files from get_select2_css_libs. Solution: I changed assets loading from Assets as a static definition to Media as a dynamic property as described at Django docs: https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property Closed #179 Squashed commit of the following: commit f925ed4a118687b82b6f61d21a9104ccf00859c8 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:39:47 2015 +0300 remove trailing whitespace from docstrings commit b3f6553e422e19c8e065e026511c4ffd91ffee42 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:35:15 2015 +0300 Remove blank lines from docstrings commit 4490b78572a25069472933c88ebee48445b59972 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:19:38 2015 +0300 construct Media the right way Remove code that access private attributes of Media class. commit 6697cd734daca62ff099be73d0685ce6af32e53d Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:11:29 2015 +0300 fix change _media to _get_media I hope it is the final commit :) commit 59bda01aed44e5658825a1bfe37ad09c5760a3ad Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:09:35 2015 +0300 get_select2_heavy_js_libs change get_select2_js_libs to get_select2_heavy_js_libs commit 993e201355c9d5a6012e6f67cfa64462576c2570 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:07:57 2015 +0300 fix fix commit f66e5f40b9f4df295bc5aaa7f2aafe588b2c41bb Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:55:53 2015 +0300 fix fix commit d33d90278345bc32e80e20bdf782fd9ac5eb1800 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:47:25 2015 +0300 fix fix commit f6d956149d2e99ee504c7b2c110f959f7472a181 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:45:09 2015 +0300 fix fix commit 6d737a78cc07507c5b9b91736f6662b5f6f31c57 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:40:01 2015 +0300 global name 'Media' is not defined global name 'Media' is not defined commit 661a817b092dc72bfa8df1a5458d474599c0a50e Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:27:27 2015 +0300 Media as a dynamic property **Symptoms:** While using AutoModelSelect2Field in admin site, I noticed that css files are rendered only at the first request to the add or edit forms, after that the widget is rendered but without loading any css files from get_select2_css_libs. **Solution:** I changed assets loading from [Assets as a static definition](https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property) to [Media as a dynamic property](https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property) as described at Django docs.
2015-06-19 12:49:51 +00:00
This is essential because we need to check RENDER_SELECT2_STATICS
before returning our assets.
for more information:
https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property
"""
2015-08-06 09:58:27 +00:00
return forms.Media(
2015-09-06 03:23:24 +00:00
js=('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js',
'django_select2/django_select2.js'),
css={'screen': ('//cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css',)}
2015-08-06 09:58:27 +00:00
)
2015-09-06 03:23:24 +00:00
Made widget media a dynamic property Symptoms: While using AutoModelSelect2Field in admin site, I noticed that css files are rendered only at the first request to the add or edit forms, after that the widget is rendered but without loading any css files from get_select2_css_libs. Solution: I changed assets loading from Assets as a static definition to Media as a dynamic property as described at Django docs: https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property Closed #179 Squashed commit of the following: commit f925ed4a118687b82b6f61d21a9104ccf00859c8 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:39:47 2015 +0300 remove trailing whitespace from docstrings commit b3f6553e422e19c8e065e026511c4ffd91ffee42 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:35:15 2015 +0300 Remove blank lines from docstrings commit 4490b78572a25069472933c88ebee48445b59972 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Fri Jun 19 01:19:38 2015 +0300 construct Media the right way Remove code that access private attributes of Media class. commit 6697cd734daca62ff099be73d0685ce6af32e53d Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:11:29 2015 +0300 fix change _media to _get_media I hope it is the final commit :) commit 59bda01aed44e5658825a1bfe37ad09c5760a3ad Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:09:35 2015 +0300 get_select2_heavy_js_libs change get_select2_js_libs to get_select2_heavy_js_libs commit 993e201355c9d5a6012e6f67cfa64462576c2570 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Tue Jun 16 00:07:57 2015 +0300 fix fix commit f66e5f40b9f4df295bc5aaa7f2aafe588b2c41bb Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:55:53 2015 +0300 fix fix commit d33d90278345bc32e80e20bdf782fd9ac5eb1800 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:47:25 2015 +0300 fix fix commit f6d956149d2e99ee504c7b2c110f959f7472a181 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:45:09 2015 +0300 fix fix commit 6d737a78cc07507c5b9b91736f6662b5f6f31c57 Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:40:01 2015 +0300 global name 'Media' is not defined global name 'Media' is not defined commit 661a817b092dc72bfa8df1a5458d474599c0a50e Author: Razi Alsayyed <razi.sayed@gmail.com> Date: Mon Jun 15 23:27:27 2015 +0300 Media as a dynamic property **Symptoms:** While using AutoModelSelect2Field in admin site, I noticed that css files are rendered only at the first request to the add or edit forms, after that the widget is rendered but without loading any css files from get_select2_css_libs. **Solution:** I changed assets loading from [Assets as a static definition](https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property) to [Media as a dynamic property](https://docs.djangoproject.com/en/1.8/topics/forms/media/#media-as-a-dynamic-property) as described at Django docs.
2015-06-19 12:49:51 +00:00
media = property(_get_media)
class Select2Widget(Select2Mixin, forms.Select):
2015-09-06 03:23:24 +00:00
pass
2015-08-06 09:58:27 +00:00
2015-09-06 03:23:24 +00:00
class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple):
pass
2015-09-15 22:04:33 +00:00
class HeavySelect2Mixin(Select2Mixin):
2015-09-06 03:23:24 +00:00
"""Mixin that adds select2's ajax options and registers itself on django's cache."""
2015-09-06 03:23:24 +00:00
def __init__(self, **kwargs):
2015-09-15 22:04:33 +00:00
self.data_view = kwargs.pop('data_view', None)
self.data_url = kwargs.pop('data_url', None)
if not (self.data_view or self.data_url):
ValueError('You must ether specify "data_view" or "data_url".')
2015-09-06 03:23:24 +00:00
self.userGetValTextFuncName = kwargs.pop('userGetValTextFuncName', 'null')
super(HeavySelect2Mixin, self).__init__(**kwargs)
2015-09-06 03:23:24 +00:00
def get_url(self):
2015-09-15 22:04:33 +00:00
if self.data_url:
return self.data_url
2015-09-06 03:23:24 +00:00
return reverse_lazy(self.data_view)
2015-09-06 03:23:24 +00:00
def build_attrs(self, extra_attrs=None, **kwargs):
attrs = super(HeavySelect2Mixin, self).build_attrs(extra_attrs, **kwargs)
2015-08-06 09:58:27 +00:00
2015-09-06 03:23:24 +00:00
# encrypt instance Id
widget_id = signing.dumps(id(self))
# add widget object to cache
cache.set(self._get_cache_key(), self)
2015-09-06 03:23:24 +00:00
attrs['data-field_id'] = widget_id
attrs.setdefault('data-ajax--url', self.get_url())
attrs.setdefault('data-ajax--cache', "true")
attrs.setdefault('data-ajax--type', "GET")
attrs.setdefault('data-minimumInputLength', 2)
2015-09-15 22:04:33 +00:00
if 'class' in attrs:
attrs['class'] += ' django-select2-heavy'
else:
attrs['class'] = 'django-select2-heavy'
2015-09-06 03:23:24 +00:00
return attrs
2015-09-06 03:23:24 +00:00
def _get_cache_key(self):
return "%s%s" % (settings.SELECT2_CACHE_PREFIX, id(self))
2015-09-06 03:23:24 +00:00
def value_from_datadict(self, *args, **kwargs):
return super(HeavySelect2Mixin, self).value_from_datadict(*args, **kwargs)
2015-09-06 03:23:24 +00:00
def render_options(self, choices, selected_choices):
2015-09-15 22:04:33 +00:00
output = [super(HeavySelect2Mixin, self).render_options(choices, selected_choices)]
if isinstance(choices, ModelChoiceIterator):
selected_choices = set(choices.choice(obj)
for obj in choices.queryset.filter(pk__in=selected_choices))
else:
selected_choices = set((force_text(v), v) for v in choices if v in selected_choices)
for option_label, option_value in selected_choices:
2015-09-06 03:23:24 +00:00
output.append(self.render_option(selected_choices, option_value, option_label))
return '\n'.join(output)
2015-09-06 03:23:24 +00:00
class HeavySelect2Widget(HeavySelect2Mixin, forms.Select):
pass
2015-09-06 03:23:24 +00:00
class HeavySelect2MultipleWidget(HeavySelect2Mixin, forms.SelectMultiple):
pass
2015-08-06 09:58:27 +00:00
2015-09-06 03:23:24 +00:00
class HeavySelect2TagWidget(HeavySelect2MultipleWidget):
def build_attrs(self, extra_attrs=None, **kwargs):
attrs = super(HeavySelect2TagWidget, self).build_attrs(self, extra_attrs, **kwargs)
attrs['data-minimumInputLength'] = 1
attrs['data-tags'] = 'true'
attrs['data-tokenSeparators'] = [",", " "]
return attrs
2015-08-06 09:58:27 +00:00
2015-09-06 03:23:24 +00:00
# Auto Heavy widgets
2015-09-06 03:23:24 +00:00
class ModelSelect2Mixin(object):
"""Mixin for """
2015-08-06 09:58:27 +00:00
model = None
queryset = None
search_fields = []
max_results = 25
2015-09-06 03:23:24 +00:00
def __init__(self, *args, **kwargs):
self.model = kwargs.pop('model', self.model)
self.queryset = kwargs.pop('queryset', self.queryset)
self.search_fields = kwargs.pop('search_fields', self.search_fields)
self.max_results = kwargs.pop('max_results', self.max_results)
2015-09-15 22:04:33 +00:00
defaults = {'data_view': 'django_select2-json'}
2015-09-06 03:23:24 +00:00
defaults.update(kwargs)
super(ModelSelect2Mixin, self).__init__(*args, **defaults)
def filter_queryset(self, term):
qs = self.get_queryset()
search_fields = self.get_search_fields()
select = reduce(lambda x, y: Q(**{x: term}) | Q(**{y: term}), search_fields,
Q(**{search_fields.pop(): term}))
return qs.filter(select).distinct()
def get_queryset(self):
if self.queryset is not None:
queryset = self.queryset
elif self.model is not None:
queryset = self.model._default_manager.all()
else:
raise NotImplementedError(
"%(cls)s is missing a QuerySet. Define "
"%(cls)s.model, %(cls)s.queryset, or override "
"%(cls)s.get_queryset()." % {
'cls': self.__class__.__name__
}
)
return queryset
def get_search_fields(self):
if self.search_fields:
return self.search_fields
raise NotImplementedError('%s, must implement "search_fields".' % self.__class__.__name__)
2015-09-06 03:23:24 +00:00
class ModelSelect2Widget(ModelSelect2Mixin, HeavySelect2Widget):
2015-08-06 09:58:27 +00:00
"""Auto version of :py:class:`.HeavySelect2Widget`."""
pass
2015-09-06 03:23:24 +00:00
class ModelSelect2MultipleWidget(ModelSelect2Mixin, HeavySelect2MultipleWidget):
2015-08-06 09:58:27 +00:00
"""Auto version of :py:class:`.HeavySelect2MultipleWidget`."""
pass
2015-09-06 03:23:24 +00:00
class ModelSelect2TagWidget(ModelSelect2Mixin, HeavySelect2TagWidget):
2015-08-06 09:58:27 +00:00
"""Auto version of :py:class:`.HeavySelect2TagWidget`."""
pass