Removed own caching solution in favour of django caching
The old multiprocessing support was hard to maintain. Since signing and caching are part of `django.core` there is really no need to stick to our own solution. As a result multimachine support and security are now always in place. Fields are stored in Django's cache. The default cache used by select2 is called 'default' but can be cachanged overwriting the setting `SELECT2_CACHE_BACKEND`. Recommended cache backends are memcached, redis or a DB-cache. Refactored AutoResponseView The main reason for this refactoring is the fact that the pagingnation was slow. I dropped major parts of the initial code and wrote a more django-like-approach. Noteabley: - get_results now retuns a QuerySet - This commit drops django 1.6 support in favour of the JsonResponse (Backporting is possible).
This commit is contained in:
parent
29c74ae63e
commit
33b7dffca1
|
@ -1,5 +1,7 @@
|
|||
language: python
|
||||
sudo: false
|
||||
services:
|
||||
- memcached
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.3"
|
||||
|
@ -25,6 +27,8 @@ install:
|
|||
- pip install --upgrade pip
|
||||
- pip install -e .
|
||||
- pip install -r requirements_dev.txt
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 2* ]]; then pip install python-memcached; fi
|
||||
- if [[ $TRAVIS_PYTHON_VERSION == 3* ]]; then pip install python3-memcached; fi
|
||||
- pip install $DJANGO
|
||||
- pip install coveralls
|
||||
- sh -e /etc/init.d/xvfb start
|
||||
|
|
2
README
2
README
|
@ -45,7 +45,7 @@ External Dependencies
|
|||
|
||||
* Django - This is obvious.
|
||||
* jQuery - This is not included in the package since it is expected that in most scenarios this would already be available.
|
||||
* Memcached (python-memcached) - If you plan on running multiple python processes with `GENERATE_RANDOM_SELECT2_ID` enabled, then you need to turn on `ENABLE_SELECT2_MULTI_PROCESS_SUPPORT`. In that mode it is highly recommended that you use Memcached, to minimize DB hits.
|
||||
* Memcached or Redis - If you run more than one node, you'll need a shared memory.
|
||||
|
||||
Example Application
|
||||
===================
|
||||
|
|
|
@ -45,7 +45,8 @@ External Dependencies
|
|||
|
||||
* Django - This is obvious.
|
||||
* jQuery - This is not included in the package since it is expected that in most scenarios this would already be available.
|
||||
* Memcached (python-memcached) - If you plan on running multiple python processes with `GENERATE_RANDOM_SELECT2_ID` enabled, then you need to turn on `ENABLE_SELECT2_MULTI_PROCESS_SUPPORT`. In that mode it is highly recommended that you use Memcached, to minimize DB hits.
|
||||
* Memcached or Redis - If you run more than one node, you'll need a shared memory.
|
||||
|
||||
|
||||
Example Application
|
||||
===================
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This is a Django_ integration of Select2_.
|
||||
|
||||
|
@ -79,18 +79,24 @@ The view - `Select2View`, exposed here is meant to be used with 'Heavy' fields a
|
|||
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
|
||||
__SECRET_SALT = ''
|
||||
__BOOTSTRAP = False
|
||||
|
||||
|
||||
try:
|
||||
from django.conf import settings
|
||||
|
@ -103,12 +109,29 @@ try:
|
|||
__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)
|
||||
__SECRET_SALT = getattr(settings, 'SECRET_KEY', '')
|
||||
__BOOTSTRAP = getattr(settings, 'SELECT2_BOOTSTRAP', False)
|
||||
|
||||
if not __GENERATE_RANDOM_ID and __ENABLE_MULTI_PROCESS_SUPPORT:
|
||||
logger.warn("You need not turn on ENABLE_SELECT2_MULTI_PROCESS_SUPPORT when GENERATE_RANDOM_SELECT2_ID is disabled.")
|
||||
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,
|
||||
|
@ -126,7 +149,7 @@ try:
|
|||
HeavySelect2TagField, AutoSelect2TagField,
|
||||
HeavyModelSelect2TagField, AutoModelSelect2TagField
|
||||
) # NOQA
|
||||
from .views import Select2View, NO_ERR_RESP
|
||||
from .views import Select2View
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Django found and fields and widgets loaded.")
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Shared memory across multiple machines to the heavy ajax lookups.
|
||||
|
||||
Select2 uses django.core.cache_ to share fields across
|
||||
multiple threads and even machines.
|
||||
|
||||
Select2 uses the cabhe backend defind in the setting
|
||||
``SELECT2_CACHE_BACKEND`` [default=``default``].
|
||||
|
||||
It is advised to always setup a separate cache server for Select2.
|
||||
|
||||
.. _django.core.cache: https://docs.djangoproject.com/en/dev/topics/cache/
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.core.cache import _create_cache, 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]
|
|
@ -0,0 +1,15 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from appconf import AppConf
|
||||
from django.conf import settings # NOQA
|
||||
|
||||
__all__ = ['settings']
|
||||
|
||||
|
||||
class Select2Conf(AppConf):
|
||||
CACHE_BACKEND = 'default'
|
||||
CACHE_PREFIX = 'select2_'
|
||||
|
||||
class Meta:
|
||||
prefix = 'SELECT2'
|
|
@ -1,29 +0,0 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from .models import KeyMap
|
||||
|
||||
|
||||
class Client(object):
|
||||
def set(self, key, value):
|
||||
"""
|
||||
This method is used to set a new value
|
||||
in the db.
|
||||
"""
|
||||
o = self.get(key)
|
||||
if o is None:
|
||||
o = KeyMap()
|
||||
o.key = key
|
||||
|
||||
o.value = value
|
||||
o.save()
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
This method is used to retrieve a value
|
||||
from the db.
|
||||
"""
|
||||
try:
|
||||
return KeyMap.objects.get(key=key).value
|
||||
except KeyMap.DoesNotExist:
|
||||
return None
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Contains all the Django fields for Select2.
|
||||
"""
|
||||
|
@ -6,30 +6,25 @@ 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 import six
|
||||
from django.utils.encoding import force_text, smart_text
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import util
|
||||
from .types import NO_ERR_RESP
|
||||
from .util import extract_some_key_val
|
||||
from .views import NO_ERR_RESP
|
||||
from .widgets import AutoHeavySelect2Mixin # NOQA
|
||||
from .widgets import (AutoHeavySelect2MultipleWidget,
|
||||
AutoHeavySelect2TagWidget, AutoHeavySelect2Widget,
|
||||
HeavySelect2MultipleWidget, HeavySelect2TagWidget,
|
||||
HeavySelect2Widget, Select2MultipleWidget, Select2Widget)
|
||||
|
||||
try:
|
||||
from django.forms.fields import RenameFieldMethods as UnhideableQuerysetTypeBase
|
||||
except ImportError:
|
||||
UnhideableQuerysetTypeBase = type
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -42,57 +37,12 @@ class AutoViewFieldMixin(object):
|
|||
.. 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):
|
||||
"""
|
||||
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', "%s.%s" % (self.__module__, self.__class__.__name__))
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("Registering auto field: %s", name)
|
||||
|
||||
rf = util.register_field
|
||||
|
||||
id_ = rf(name, self)
|
||||
self.field_id = id_
|
||||
kwargs.pop('auto_id', None)
|
||||
super(AutoViewFieldMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
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 requests to your server, be able to get access to sensitive information.
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_results(self, request, term, page, context):
|
||||
"""See :py:meth:`.views.Select2View.get_results`."""
|
||||
def get_results(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
@ -130,41 +80,30 @@ class ModelResultJsonMixin(object):
|
|||
overrides ``get_results``.
|
||||
"""
|
||||
|
||||
max_results = 25
|
||||
to_field_name = 'pk'
|
||||
queryset = {}
|
||||
search_fields = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Class constructor.
|
||||
|
||||
:param queryset: This can be passed as kwarg here or defined as field variable,
|
||||
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`
|
||||
"""
|
||||
self.max_results = getattr(self, 'max_results', None)
|
||||
self.to_field_name = getattr(self, 'to_field_name', 'pk')
|
||||
|
||||
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):
|
||||
"""
|
||||
Returns the queryset.
|
||||
Return the list of model choices.
|
||||
|
||||
The default implementation returns the ``self.queryset``, which is usually the
|
||||
one set by sub-classes at class-level. However, if that is ``None``
|
||||
then ``ValueError`` is thrown.
|
||||
|
||||
:return: queryset
|
||||
:rtype: :py:class:`django.db.models.query.QuerySet`
|
||||
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 is None:
|
||||
raise ValueError('queryset is required.')
|
||||
|
||||
return self.queryset
|
||||
if self.queryset:
|
||||
return self.queryset
|
||||
raise NotImplementedError('%s must implement "queryset".' % self.__class__.__name__)
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
"""
|
||||
|
@ -176,6 +115,10 @@ class ModelResultJsonMixin(object):
|
|||
: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):
|
||||
|
@ -193,7 +136,7 @@ class ModelResultJsonMixin(object):
|
|||
|
||||
def prepare_qs_params(self, request, search_term, search_fields):
|
||||
"""
|
||||
Prepares queryset parameter to use for searching.
|
||||
Prepare queryset parameter to use for searching.
|
||||
|
||||
:param search_term: The search term.
|
||||
:type search_term: :py:obj:`str`
|
||||
|
@ -246,37 +189,38 @@ class ModelResultJsonMixin(object):
|
|||
}
|
||||
: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)
|
||||
q = reduce(lambda x, y: y | Q({x: search_term}), search_fields)
|
||||
return {'or': [q], 'and': {}}
|
||||
|
||||
def get_results(self, request, term, page, context):
|
||||
def filter_queryset(self, request, term):
|
||||
"""
|
||||
See :py:meth:`.views.Select2View.get_results`.
|
||||
|
||||
This implementation takes care of detecting if more results are available.
|
||||
"""
|
||||
if not hasattr(self, 'search_fields') or not self.search_fields:
|
||||
raise ValueError('search_fields is required.')
|
||||
|
||||
qs = copy.deepcopy(self.get_queryset())
|
||||
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 = list(qs.filter(*params['or'], **params['and']).distinct()[min_:max_])
|
||||
res = qs[min_:max_]
|
||||
has_more = len(res) == (max_ - min_)
|
||||
if has_more:
|
||||
res = res[:-1]
|
||||
res = list(res)[:-1]
|
||||
else:
|
||||
res = list(qs.filter(*params['or'], **params['and']).distinct())
|
||||
res = qs
|
||||
has_more = False
|
||||
|
||||
res = [
|
||||
|
@ -290,40 +234,12 @@ class ModelResultJsonMixin(object):
|
|||
return NO_ERR_RESP, has_more, res
|
||||
|
||||
|
||||
class UnhideableQuerysetType(UnhideableQuerysetTypeBase):
|
||||
"""
|
||||
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 queryset is None and hasattr(cls, '_subclass_queryset'):
|
||||
kwargs['queryset'] = getattr(cls, '_subclass_queryset')
|
||||
return type.__call__(cls, *args, **kwargs)
|
||||
|
||||
|
||||
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
|
||||
|
@ -359,6 +275,7 @@ class FilterableModelChoiceIterator(ModelChoiceIterator):
|
|||
"""
|
||||
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)
|
||||
|
@ -397,45 +314,46 @@ class QuerysetChoiceMixin(ChoiceMixin):
|
|||
|
||||
|
||||
class ModelChoiceFieldMixin(QuerysetChoiceMixin):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
queryset = kwargs.pop('queryset', None)
|
||||
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_queryset(self):
|
||||
if hasattr(self, '_queryset'):
|
||||
return self._queryset
|
||||
|
||||
def get_pk_field_name(self):
|
||||
return self.to_field_name or 'pk'
|
||||
return self.to_field_name
|
||||
|
||||
|
||||
# ## 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)
|
||||
|
||||
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):
|
||||
queryset = property(ModelChoiceFieldMixin._get_queryset, forms.ModelMultipleChoiceField._set_queryset)
|
||||
pass
|
||||
|
||||
|
||||
# ## Light Fields specialized for Models ##
|
||||
|
@ -473,6 +391,7 @@ class HeavySelect2FieldBaseMixin(object):
|
|||
might expect it to be available.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""
|
||||
Class constructor.
|
||||
|
@ -490,23 +409,14 @@ class HeavySelect2FieldBaseMixin(object):
|
|||
data_view = kwargs.pop('data_view', None)
|
||||
choices = kwargs.pop('choices', [])
|
||||
|
||||
kargs = {}
|
||||
if kwargs.get('widget', None) is None:
|
||||
kargs['widget'] = self.widget(data_view=data_view)
|
||||
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
|
||||
|
||||
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 it 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
|
||||
self.widget.attrs['data-select2-id'] = self.field_id
|
||||
super(HeavySelect2FieldBaseMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
# Widget should have been instantiated by now.
|
||||
self.widget.field = self
|
||||
|
@ -781,6 +691,7 @@ class HeavyModelSelect2TagField(HeavySelect2FieldBaseMixin, ModelMultipleChoiceF
|
|||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# ## Heavy general field that uses central AutoView ##
|
||||
|
||||
|
||||
|
@ -826,10 +737,8 @@ class AutoSelect2TagField(AutoViewFieldMixin, HeavySelect2TagField):
|
|||
# ## Heavy field, specialized for Model, that uses central AutoView ##
|
||||
|
||||
|
||||
class AutoModelSelect2Field(six.with_metaclass(UnhideableQuerysetType,
|
||||
ModelResultJsonMixin,
|
||||
AutoViewFieldMixin,
|
||||
HeavyModelSelect2ChoiceField)):
|
||||
class AutoModelSelect2Field(ModelResultJsonMixin, AutoViewFieldMixin,
|
||||
HeavyModelSelect2ChoiceField):
|
||||
"""
|
||||
Auto Heavy Select2 field, specialized for Models.
|
||||
|
||||
|
@ -840,10 +749,8 @@ class AutoModelSelect2Field(six.with_metaclass(UnhideableQuerysetType,
|
|||
widget = AutoHeavySelect2Widget
|
||||
|
||||
|
||||
class AutoModelSelect2MultipleField(six.with_metaclass(UnhideableQuerysetType,
|
||||
ModelResultJsonMixin,
|
||||
AutoViewFieldMixin,
|
||||
HeavyModelSelect2MultipleChoiceField)):
|
||||
class AutoModelSelect2MultipleField(ModelResultJsonMixin, AutoViewFieldMixin,
|
||||
HeavyModelSelect2MultipleChoiceField):
|
||||
"""
|
||||
Auto Heavy Select2 field for multiple choices, specialized for Models.
|
||||
|
||||
|
@ -854,10 +761,8 @@ class AutoModelSelect2MultipleField(six.with_metaclass(UnhideableQuerysetType,
|
|||
widget = AutoHeavySelect2MultipleWidget
|
||||
|
||||
|
||||
class AutoModelSelect2TagField(six.with_metaclass(UnhideableQuerysetType,
|
||||
ModelResultJsonMixin,
|
||||
AutoViewFieldMixin,
|
||||
HeavyModelSelect2TagField)):
|
||||
class AutoModelSelect2TagField(ModelResultJsonMixin, AutoViewFieldMixin,
|
||||
HeavyModelSelect2TagField):
|
||||
"""
|
||||
Auto Heavy Select2 field for tagging, specialized for Models.
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ def get_select2_js_libs():
|
|||
js_file = 'js/select2.js'
|
||||
else:
|
||||
js_file = 'js/select2.min.js'
|
||||
return (django_select2_static(js_file), )
|
||||
return django_select2_static(js_file),
|
||||
|
||||
|
||||
def get_select2_heavy_js_libs():
|
||||
|
@ -30,27 +30,23 @@ def get_select2_heavy_js_libs():
|
|||
|
||||
|
||||
def get_select2_css_libs(light=False):
|
||||
css_files = []
|
||||
if BOOTSTRAP:
|
||||
if DEBUG:
|
||||
if light:
|
||||
css_files = ('css/select2.css', 'css/select2-bootstrap.css', )
|
||||
else:
|
||||
css_files = ('css/select2.css', 'css/extra.css', 'css/select2-bootstrap.css')
|
||||
if DEBUG:
|
||||
if light:
|
||||
css_files = 'css/select2.css',
|
||||
else:
|
||||
if light:
|
||||
css_files = ('css/select2-bootstrapped.min.css',)
|
||||
else:
|
||||
css_files = ('css/all-bootstrapped.min.css',)
|
||||
css_files = 'css/select2.css', 'css/extra.css'
|
||||
if BOOTSTRAP:
|
||||
css_files += 'css/select2-bootstrap.css',
|
||||
else:
|
||||
if settings.configured and settings.DEBUG:
|
||||
if BOOTSTRAP:
|
||||
if light:
|
||||
css_files = ('css/select2.css',)
|
||||
css_files = 'css/select2-bootstrapped.min.css',
|
||||
else:
|
||||
css_files = ('css/select2.css', 'css/extra.css')
|
||||
css_files = 'css/all-bootstrapped.min.css',
|
||||
else:
|
||||
if light:
|
||||
css_files = ('css/select2.min.css',)
|
||||
css_files = 'css/select2.min.css',
|
||||
else:
|
||||
css_files = ('css/all.min.css',)
|
||||
css_files = 'css/all.min.css',
|
||||
|
||||
return [django_select2_static(f) for f in css_files]
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.utils.six import binary_type
|
||||
|
||||
import memcache
|
||||
|
||||
|
||||
class Client(object):
|
||||
host = ""
|
||||
server = ""
|
||||
expiry = 900
|
||||
|
||||
def __init__(self, hostname="127.0.0.1", port="11211", expiry=900):
|
||||
self.host = "%s:%s" % (hostname, port)
|
||||
self.server = memcache.Client([self.host])
|
||||
self.expiry = expiry
|
||||
|
||||
def set(self, key, value):
|
||||
"""
|
||||
This method is used to set a new value
|
||||
in the memcache server.
|
||||
"""
|
||||
self.server.set(self.normalize_key(key), value, self.expiry)
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
This method is used to retrieve a value
|
||||
from the memcache server.
|
||||
"""
|
||||
return self.server.get(self.normalize_key(key))
|
||||
|
||||
def normalize_key(self, key):
|
||||
key = binary_type(key)
|
||||
key = key.replace(' ', '-')
|
||||
return key
|
|
@ -1,42 +0,0 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
class Client(object):
|
||||
cache = None
|
||||
db = None
|
||||
|
||||
def __init__(self, hostname="127.0.0.1", port="11211", expiry=900):
|
||||
if hostname and port:
|
||||
from . import memcache_client
|
||||
|
||||
self.cache = memcache_client.Client(hostname, port, expiry)
|
||||
|
||||
from . import db_client
|
||||
|
||||
self.db = db_client.Client()
|
||||
|
||||
def set(self, key, value):
|
||||
"""
|
||||
This method is used to set a new value
|
||||
in the memcache server and the db.
|
||||
"""
|
||||
self.db.set(key, value)
|
||||
if self.cache:
|
||||
self.cache.set(key, value)
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
This method is used to retrieve a value
|
||||
from the memcache server, if found, else it
|
||||
is fetched from db.
|
||||
"""
|
||||
if self.cache:
|
||||
v = self.cache.get(key)
|
||||
if v is None:
|
||||
v = self.db.get(key)
|
||||
if v is not None:
|
||||
self.cache.set(key, v)
|
||||
else:
|
||||
v = self.db.get(key)
|
||||
return v
|
|
@ -1,15 +1 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_text, python_2_unicode_compatible
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class KeyMap(models.Model):
|
||||
key = models.CharField(max_length=40, unique=True)
|
||||
value = models.CharField(max_length=100)
|
||||
accessed_on = models.DateTimeField(auto_now=True)
|
||||
|
||||
def __str__(self):
|
||||
return force_text("%s => %s" % (self.key, self.value))
|
||||
# Django legacy file
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django import template
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
NO_ERR_RESP = 'nil'
|
||||
"""
|
||||
Equals to 'nil' constant.
|
||||
|
||||
Use this in :py:meth:`.Select2View.get_results` to mean no error, instead of hardcoding 'nil' value.
|
||||
"""
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf.urls import patterns, url
|
||||
|
|
|
@ -1,23 +1,6 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import datetime
|
||||
import hashlib
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
|
||||
from django.utils.six import binary_type, text_type
|
||||
|
||||
from . import __ENABLE_MULTI_PROCESS_SUPPORT as ENABLE_MULTI_PROCESS_SUPPORT
|
||||
from . import __GENERATE_RANDOM_ID as GENERATE_RANDOM_ID
|
||||
from . import __MEMCACHE_HOST as MEMCACHE_HOST
|
||||
from . import __MEMCACHE_PORT as MEMCACHE_PORT
|
||||
from . import __MEMCACHE_TTL as MEMCACHE_TTL
|
||||
from . import __SECRET_SALT as SECRET_SALT
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def extract_some_key_val(dct, keys):
|
||||
"""
|
||||
|
@ -37,120 +20,3 @@ def extract_some_key_val(dct, keys):
|
|||
if v is not None:
|
||||
edct[k] = v
|
||||
return edct
|
||||
|
||||
|
||||
# ## Auto view helper utils ##
|
||||
|
||||
|
||||
def synchronized(f):
|
||||
"""Decorator to synchronize multiple calls to a functions."""
|
||||
f.__lock__ = threading.Lock()
|
||||
|
||||
def synced_f(*args, **kwargs):
|
||||
with f.__lock__:
|
||||
return f(*args, **kwargs)
|
||||
|
||||
synced_f.__doc__ = f.__doc__
|
||||
return synced_f
|
||||
|
||||
# Generated Id to field instance mapping.
|
||||
__id_store = {}
|
||||
# Field's key to generated Id mapping.
|
||||
__field_store = {}
|
||||
|
||||
ID_PATTERN = r"[0-9_a-zA-Z.:+\- ]+"
|
||||
|
||||
|
||||
def is_valid_id(val):
|
||||
"""
|
||||
Checks if ``val`` is a valid generated Id.
|
||||
|
||||
:param val: The value to check.
|
||||
:type val: :py:obj:`str`
|
||||
|
||||
:rtype: :py:obj:`bool`
|
||||
"""
|
||||
regex = "^%s$" % ID_PATTERN
|
||||
if re.match(regex, val) is None:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
if ENABLE_MULTI_PROCESS_SUPPORT:
|
||||
from .memcache_wrapped_db_client import Client
|
||||
|
||||
remote_server = Client(MEMCACHE_HOST, binary_type(MEMCACHE_PORT), MEMCACHE_TTL)
|
||||
|
||||
|
||||
@synchronized
|
||||
def register_field(key, field):
|
||||
"""
|
||||
Registers an Auto field for use with :py:class:`.views.AutoResponseView`.
|
||||
|
||||
:param key: The key to use while registering this field.
|
||||
:type key: :py:obj:`unicode`
|
||||
|
||||
:param field: The field to register.
|
||||
:type field: :py:class:`AutoViewFieldMixin`
|
||||
|
||||
:return: The generated Id for this field. If given ``key`` was already registered then the
|
||||
Id generated that time, would be returned.
|
||||
:rtype: :py:obj:`unicode`
|
||||
"""
|
||||
global __id_store, __field_store
|
||||
|
||||
from .fields import AutoViewFieldMixin
|
||||
|
||||
if not isinstance(field, AutoViewFieldMixin):
|
||||
raise ValueError('Field must extend AutoViewFieldMixin')
|
||||
|
||||
if key not in __field_store:
|
||||
# Generating id
|
||||
if GENERATE_RANDOM_ID:
|
||||
id_ = "%d:%s" % (len(__id_store), text_type(datetime.datetime.now()))
|
||||
else:
|
||||
id_ = text_type(hashlib.sha1(":".join((key, SECRET_SALT)).encode('utf-8')).hexdigest())
|
||||
|
||||
__field_store[key] = id_
|
||||
__id_store[id_] = field
|
||||
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("Registering new field: %s; With actual id: %s", key, id_)
|
||||
|
||||
if ENABLE_MULTI_PROCESS_SUPPORT:
|
||||
logger.info(
|
||||
"Multi process support is enabled. Adding id-key mapping to remote server.")
|
||||
remote_server.set(id_, key)
|
||||
else:
|
||||
id_ = __field_store[key]
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("Field already registered: %s; With actual id: %s", key, id_)
|
||||
return id_
|
||||
|
||||
|
||||
def get_field(id_):
|
||||
"""
|
||||
Returns an Auto field instance registered with the given Id.
|
||||
|
||||
:param id_: The generated Id the field is registered with.
|
||||
:type id_: :py:obj:`unicode`
|
||||
|
||||
:rtype: :py:class:`AutoViewFieldMixin` or None
|
||||
"""
|
||||
field = __id_store.get(id_, None)
|
||||
if field is None and ENABLE_MULTI_PROCESS_SUPPORT:
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug('Id "%s" not found in this process. Looking up in remote server.', id_)
|
||||
key = remote_server.get(id_)
|
||||
if key is not None:
|
||||
id_in_current_instance = __field_store[key]
|
||||
if id_in_current_instance:
|
||||
field = __id_store.get(id_in_current_instance, None)
|
||||
if field:
|
||||
__id_store[id_] = field
|
||||
else:
|
||||
logger.error('Unknown id "%s".', id_in_current_instance)
|
||||
else:
|
||||
logger.error('Unknown id "%s".', id_)
|
||||
return field
|
||||
|
|
|
@ -1,98 +1,76 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import json
|
||||
from django.core import signing
|
||||
from django.core.signing import BadSignature
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.utils.encoding import smart_text
|
||||
from django.views.generic.list import BaseListView
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.utils.six import binary_type
|
||||
from django.views.generic import View
|
||||
|
||||
from .util import get_field, is_valid_id
|
||||
|
||||
NO_ERR_RESP = 'nil'
|
||||
"""
|
||||
Equals to 'nil' constant.
|
||||
|
||||
Use this in :py:meth:`.Select2View.get_results` to mean no error, instead of hardcoding 'nil' value.
|
||||
"""
|
||||
from .cache import cache
|
||||
from .conf import settings
|
||||
from .types import NO_ERR_RESP
|
||||
|
||||
|
||||
class JSONResponseMixin(object):
|
||||
class AutoResponseView(BaseListView):
|
||||
"""
|
||||
A mixin that can be used to render a JSON response.
|
||||
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`.
|
||||
"""
|
||||
response_class = HttpResponse
|
||||
|
||||
def render_to_response(self, context, **response_kwargs):
|
||||
"""
|
||||
Returns a JSON response, transforming 'context' to make the payload.
|
||||
"""
|
||||
response_kwargs['content_type'] = 'application/json'
|
||||
return self.response_class(
|
||||
self.convert_context_to_json(context),
|
||||
**response_kwargs
|
||||
)
|
||||
|
||||
def convert_context_to_json(self, context):
|
||||
"""Convert the context dictionary into a JSON object"""
|
||||
return json.dumps(context)
|
||||
|
||||
|
||||
class Select2View(JSONResponseMixin, View):
|
||||
"""
|
||||
Base view which is designed to respond with JSON to Ajax queries from heavy widgets/fields.
|
||||
|
||||
Although the widgets won't enforce the type of data_view it gets, but it is recommended to
|
||||
sub-class this view instead of creating a Django view from scratch.
|
||||
|
||||
.. note:: Only `GET <http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.3>`_ Http requests are supported.
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
try:
|
||||
self.check_all_permissions(request, *args, **kwargs)
|
||||
except Exception as e:
|
||||
return self.respond_with_exception(e)
|
||||
return super(Select2View, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
term = request.GET.get('term', None)
|
||||
if term is None:
|
||||
return self.render_to_response(self._results_to_context(('missing term', False, [], )))
|
||||
self.widget = self.get_widget_or_404()
|
||||
self.term = kwargs.get('term', request.GET.get('term', ''))
|
||||
|
||||
try:
|
||||
page = int(request.GET.get('page', None))
|
||||
if page <= 0:
|
||||
page = -1
|
||||
except ValueError:
|
||||
page = -1
|
||||
if page == -1:
|
||||
return self.render_to_response(self._results_to_context(('bad page no.', False, [], )))
|
||||
context = request.GET.get('context', None)
|
||||
|
||||
return self.render_to_response(
|
||||
self._results_to_context(
|
||||
self.get_results(request, term, page, context)
|
||||
)
|
||||
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
|
||||
)
|
||||
|
||||
def respond_with_exception(self, e):
|
||||
"""
|
||||
:param e: Exception object.
|
||||
:type e: Exception
|
||||
:return: Response with status code of 404 if e is ``Http404`` object,
|
||||
else 400.
|
||||
:rtype: HttpResponse
|
||||
"""
|
||||
if isinstance(e, Http404):
|
||||
status = 404
|
||||
return JsonResponse(self._results_to_context(results))
|
||||
else:
|
||||
status = getattr(e, 'status_code', 400)
|
||||
return self.render_to_response(
|
||||
self._results_to_context((binary_type(e), False, [],)),
|
||||
status=status
|
||||
)
|
||||
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)
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
return self.widget.max_results
|
||||
|
||||
def get_widget_or_404(self):
|
||||
field_id = self.kwargs.get('field_id', self.request.GET.get('field_id', None))
|
||||
if not field_id:
|
||||
raise Http404('No "field_id" provided.')
|
||||
try:
|
||||
key = signing.loads(field_id)
|
||||
except BadSignature:
|
||||
raise Http404('Invalid "field_id".')
|
||||
else:
|
||||
cache_key = '%s%s' % (settings.SELECT2_CACHE_PREFIX, key)
|
||||
field = cache.get(cache_key)
|
||||
if field is None:
|
||||
raise Http404('field_id not found')
|
||||
return field
|
||||
|
||||
def _results_to_context(self, output):
|
||||
err, has_more, results = output
|
||||
|
@ -110,85 +88,3 @@ class Select2View(JSONResponseMixin, View):
|
|||
'more': has_more,
|
||||
'results': res,
|
||||
}
|
||||
|
||||
def check_all_permissions(self, request, *args, **kwargs):
|
||||
"""
|
||||
Sub-classes can use this to raise exception on permission check failures,
|
||||
or these checks can be placed in ``urls.py``, e.g. ``login_required(SelectClass.as_view())``.
|
||||
|
||||
:param request: The Ajax request object.
|
||||
:type request: :py:class:`django.http.HttpRequest`
|
||||
|
||||
:param args: The ``*args`` passed to :py:meth:`django.views.generic.View.dispatch`.
|
||||
:param kwargs: The ``**kwargs`` passed to :py:meth:`django.views.generic.View.dispatch`.
|
||||
|
||||
.. warning:: Sub-classes should override this. You really do not want random people making
|
||||
Http requests to your server, be able to get access to sensitive information.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_results(self, request, term, page, context):
|
||||
"""
|
||||
Returns the result for the given search ``term``.
|
||||
|
||||
:param request: The Ajax request object.
|
||||
:type request: :py:class:`django.http.HttpRequest`
|
||||
|
||||
:param term: The search term.
|
||||
:type term: :py:obj:`str`
|
||||
|
||||
:param page: The page number. If in your last response you had signalled that there are more results,
|
||||
then when user scrolls more a new Ajax request would be sent for the same term but with next page
|
||||
number. (Page number starts at 1)
|
||||
:type page: :py:obj:`int`
|
||||
|
||||
:param context: Can be anything which persists across the lifecycle of queries for the same search term.
|
||||
It is reset to ``None`` when the term changes.
|
||||
|
||||
.. note:: Currently this is not used by ``heavy_data.js``.
|
||||
:type context: :py:obj:`str` or None
|
||||
|
||||
Expected output is of the form::
|
||||
|
||||
(err, has_more, [results])
|
||||
|
||||
Where ``results = [(id1, text1), (id2, text2), ...]``
|
||||
|
||||
For example::
|
||||
|
||||
('nil', False,
|
||||
[
|
||||
(1, 'Value label1'),
|
||||
(20, 'Value label2'),
|
||||
])
|
||||
|
||||
When everything is fine then the `err` must be 'nil'.
|
||||
`has_more` should be true if there are more rows.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AutoResponseView(Select2View):
|
||||
"""
|
||||
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 check_all_permissions(self, request, *args, **kwargs):
|
||||
id_ = request.GET.get('field_id', None)
|
||||
if id_ is None or not is_valid_id(id_):
|
||||
raise Http404('field_id not found or is invalid')
|
||||
field = get_field(id_)
|
||||
if field is None:
|
||||
raise Http404('field_id not found')
|
||||
|
||||
if not field.security_check(request, *args, **kwargs):
|
||||
raise PermissionDenied('permission denied')
|
||||
|
||||
request.__django_select2_local = field
|
||||
|
||||
def get_results(self, request, term, page, context):
|
||||
field = request.__django_select2_local
|
||||
del request.__django_select2_local
|
||||
return field.get_results(request, term, page, context)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Contains all the Django widgets for Select2.
|
||||
"""
|
||||
|
@ -7,21 +7,24 @@ from __future__ import absolute_import, unicode_literals
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
from functools import reduce
|
||||
from itertools import chain
|
||||
|
||||
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.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 django_select2.media import (get_select2_css_libs,
|
||||
get_select2_heavy_js_libs,
|
||||
get_select2_js_libs)
|
||||
|
||||
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)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -342,6 +345,10 @@ class HeavySelect2Mixin(Select2Mixin):
|
|||
|
||||
.. tip:: You can override these options by passing ``select2_options`` kwarg to :py:meth:`.__init__`.
|
||||
"""
|
||||
model = None
|
||||
queryset = None
|
||||
search_fields = []
|
||||
max_results = 25
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
|
@ -393,8 +400,10 @@ class HeavySelect2Mixin(Select2Mixin):
|
|||
self.userGetValTextFuncName = kwargs.pop('userGetValTextFuncName', 'null')
|
||||
self.choices = kwargs.pop('choices', [])
|
||||
|
||||
if not self.view and not self.url:
|
||||
raise ValueError('data_view or data_url is required')
|
||||
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)
|
||||
|
||||
self.options['ajax'] = {
|
||||
'dataType': 'json',
|
||||
|
@ -406,6 +415,40 @@ class HeavySelect2Mixin(Select2Mixin):
|
|||
self.options['initSelection'] = '*START*django_select2.onInit*END*'
|
||||
super(HeavySelect2Mixin, self).__init__(**kwargs)
|
||||
|
||||
def filter_queryset(self, term):
|
||||
"""
|
||||
See :py:meth:`.views.Select2View.get_results`.
|
||||
|
||||
This implementation takes care of detecting if more results are available.
|
||||
"""
|
||||
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
|
||||
if isinstance(queryset, QuerySet):
|
||||
queryset = queryset.all()
|
||||
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__)
|
||||
|
||||
def render_texts(self, selected_choices, choices):
|
||||
"""
|
||||
Renders a JS array with labels for the ``selected_choices``.
|
||||
|
@ -492,6 +535,19 @@ class HeavySelect2Mixin(Select2Mixin):
|
|||
js += super(HeavySelect2Mixin, self).render_inner_js_code(id_, name, value, attrs, choices, *args)
|
||||
return js
|
||||
|
||||
def _get_cache_key(self):
|
||||
return "%s%s" % (settings.SELECT2_CACHE_PREFIX, id(self))
|
||||
|
||||
def render(self, name, value, attrs={}, choices=()):
|
||||
self.widget_id = signing.dumps(id(self))
|
||||
cache.set(self._get_cache_key(), self)
|
||||
attrs.setdefault('data-field_id', self.widget_id)
|
||||
output = super(HeavySelect2Mixin, self).render(name, value, attrs, choices)
|
||||
return output
|
||||
|
||||
def value_from_datadict(self, *args, **kwargs):
|
||||
return super(HeavySelect2Mixin, self).value_from_datadict(*args, **kwargs)
|
||||
|
||||
def _get_media(self):
|
||||
"""
|
||||
Construct Media as a dynamic property
|
||||
|
@ -532,7 +588,7 @@ class HeavySelect2Widget(HeavySelect2Mixin, forms.TextInput):
|
|||
return False
|
||||
|
||||
def render_inner_js_code(self, id_, *args):
|
||||
field_id = self.field_id if hasattr(self, 'field_id') else id_
|
||||
field_id = self.widget_id
|
||||
fieldset_id = re.sub(r'-\d+-', '_', id_).replace('-', '_')
|
||||
if '__prefix__' in id_:
|
||||
return ''
|
||||
|
@ -540,7 +596,6 @@ class HeavySelect2Widget(HeavySelect2Mixin, forms.TextInput):
|
|||
js = '''
|
||||
window.django_select2.%s = function (selector, fieldID) {
|
||||
var hashedSelector = "#" + selector;
|
||||
$(hashedSelector).data("field_id", fieldID);
|
||||
''' % (fieldset_id)
|
||||
js += super(HeavySelect2Widget, self).render_inner_js_code(id_, *args)
|
||||
js += '};'
|
||||
|
@ -595,7 +650,7 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput):
|
|||
return '$("#%s").txt(%s);' % (id_, texts)
|
||||
|
||||
def render_inner_js_code(self, id_, *args):
|
||||
field_id = self.field_id if hasattr(self, 'field_id') else id_
|
||||
field_id = self.widget_id
|
||||
fieldset_id = re.sub(r'-\d+-', '_', id_).replace('-', '_')
|
||||
if '__prefix__' in id_:
|
||||
return ''
|
||||
|
@ -603,7 +658,6 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput):
|
|||
js = '''
|
||||
window.django_select2.%s = function (selector, fieldID) {
|
||||
var hashedSelector = "#" + selector;
|
||||
$(hashedSelector).data("field_id", fieldID);
|
||||
''' % (fieldset_id)
|
||||
js += super(HeavySelect2MultipleWidget, self).render_inner_js_code(id_, *args)
|
||||
js += '};'
|
||||
|
@ -641,7 +695,7 @@ class HeavySelect2TagWidget(HeavySelect2MultipleWidget):
|
|||
self.options['createSearchChoice'] = '*START*django_select2.createSearchChoice*END*'
|
||||
|
||||
def render_inner_js_code(self, id_, *args):
|
||||
field_id = self.field_id if hasattr(self, 'field_id') else id_
|
||||
field_id = self.widget_id
|
||||
fieldset_id = re.sub(r'-\d+-', '_', id_).replace('-', '_')
|
||||
if '__prefix__' in id_:
|
||||
return ''
|
||||
|
@ -649,7 +703,6 @@ class HeavySelect2TagWidget(HeavySelect2MultipleWidget):
|
|||
js = '''
|
||||
window.django_select2.%s = function (selector, fieldID) {
|
||||
var hashedSelector = "#" + selector;
|
||||
$(hashedSelector).data("field_id", fieldID);
|
||||
''' % (fieldset_id)
|
||||
js += super(HeavySelect2TagWidget, self).render_inner_js_code(id_, *args)
|
||||
js += '};'
|
||||
|
@ -684,21 +737,6 @@ class AutoHeavySelect2Mixin(object):
|
|||
kwargs['data_view'] = "django_select2_central_json"
|
||||
super(AutoHeavySelect2Mixin, self).__init__(*args, **kwargs)
|
||||
|
||||
def render_inner_js_code(self, id_, *args):
|
||||
fieldset_id = re.sub(r'-\d+-', '_', id_).replace('-', '_')
|
||||
if '__prefix__' in id_:
|
||||
return ''
|
||||
else:
|
||||
js = '''
|
||||
window.django_select2.%s = function (selector, fieldID) {
|
||||
var hashedSelector = "#" + selector;
|
||||
$(hashedSelector).data("field_id", fieldID);
|
||||
''' % (fieldset_id)
|
||||
js += super(AutoHeavySelect2Mixin, self).render_inner_js_code(id_, *args)
|
||||
js += '};'
|
||||
js += 'django_select2.%s("%s", "%s");' % (fieldset_id, id_, self.field_id)
|
||||
return js
|
||||
|
||||
|
||||
class AutoHeavySelect2Widget(AutoHeavySelect2Mixin, HeavySelect2Widget):
|
||||
"""Auto version of :py:class:`.HeavySelect2Widget`"""
|
||||
|
|
|
@ -46,41 +46,60 @@ When this settings is ``False`` then you are responsible for including the JS an
|
|||
.. tip:: Make sure to include them at the top of the page, preferably in ``<head>...</head>``.
|
||||
|
||||
.. note:: (Since version 3.3.1) The above template tags accept one argument ``light``. Default value for that is ``0``.
|
||||
If that is set to ``1`` then only the JS and CSS libraries needed by Select2Widget (Light fields) are rendered.
|
||||
That effectively leaves out ``heavy.js`` and ``extra.css``.
|
||||
If that is set to ``1`` then only the JS and CSS libraries needed by Select2Widget (Light fields) are rendered.
|
||||
That effectively leaves out ``heavy.js`` and ``extra.css``.
|
||||
|
||||
``GENERATE_RANDOM_SELECT2_ID`` [Default ``False``]
|
||||
..................................................
|
||||
``SELECT2_CACHE_BACKEND`` [Default ``default``]
|
||||
...............................................
|
||||
|
||||
As of version 4.0.0 the field's Ids are their paths which have been hashed by SHA1. This Id generation scheme should be sufficient for most applications.
|
||||
Django-Select2 uses Django's cache to sure a consistent state across multiple machines.
|
||||
|
||||
However, if you have a secret government project and fear that SHA1 hashes could be cracked (which is not impossible) to reveal the path and names of your fields then you can enable this mode. This will use timestamps as Ids which have no correlation to the field's name or path.
|
||||
Example of settings.py::
|
||||
|
||||
.. tip:: The field's paths are first salted with Django generated ``SECRET_KEY`` before hashing them.
|
||||
CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": "redis://127.0.0.1:6379/1",
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient",
|
||||
}
|
||||
},
|
||||
'select2': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': '127.0.0.1:11211',
|
||||
}
|
||||
}
|
||||
|
||||
# Set the cache backend to select2
|
||||
SELECT2_CACHE_BACKEND = 'select2'
|
||||
|
||||
.. tip:: To ensure a consistent state across all you machines you need to user
|
||||
a consistent external cache backend like Memcached, Redis or a database.
|
||||
|
||||
.. note:: Select2 requires the cache to never expire. Therefore you should avoid clearing the cache.
|
||||
As third party apps might add unpredictable behavior we recommend to always use an separate cache server.
|
||||
|
||||
``ENABLE_SELECT2_MULTI_PROCESS_SUPPORT`` [Default ``False``]
|
||||
............................................................
|
||||
|
||||
This setting cannot be enabled as it is not required when ``GENERATE_RANDOM_SELECT2_ID`` is ``False``.
|
||||
.. warning:: Deprecated in favour of ``SELECT2_CACHE_BACKEND``. Will be removed in version 5.
|
||||
|
||||
In production servers usually multiple server processes are run to handle the requests. This poses a problem for Django Select2's Auto fields since they generate unique Id at runtime when ``GENERATE_RANDOM_SELECT2_ID`` is enabled. The clients can identify the fields in Ajax query request using only these generated ids. In multi-processes scenario there is no guarantee that the process which rendered the page is the one which will respond to Ajax queries.
|
||||
|
||||
When this mode is enabled then Django Select2 maintains an id to field key mapping in DB for all processes. Whenever a process does not find an id in its internal map it looks-up in the central DB. From DB it finds the field key. Using the key, the process then looks-up a field instance with that key, since all instances with same key are assumed to be equivalent.
|
||||
|
||||
.. tip:: Make sure to run ``python manage.py syncdb`` to create the ``KeyMap`` table.
|
||||
|
||||
.. warning:: You need to write your own script to periodically purge old data from ``KeyMap`` table. You can take help of ``accessed_on`` column. You need to decide the criteria on which basis you will purge the rows.
|
||||
Since version 4.3 django-select2 supports multiprocessing support out of the box.
|
||||
If you want to have multiple machine support take a look at ``SELECT2_CACHE_BACKEND``.
|
||||
|
||||
|
||||
``SELECT2_MEMCACHE_HOST`` [Default ``None``], ``SELECT2_MEMCACHE_PORT`` [Default ``None``], ``SELECT2_MEMCACHE_TTL`` [Default ``900``]
|
||||
.......................................................................................................................................
|
||||
``SELECT2_MEMCACHE_HOST`` [Default ``None``], ``SELECT2_MEMCACHE_PORT`` [Default ``None``]
|
||||
..........................................................................................
|
||||
|
||||
When ``ENABLE_SELECT2_MULTI_PROCESS_SUPPORT`` is enabled then all processes will hit DB to get the mapping for the ids they are not aware of. For performance reasons it is recommended that you install Memcached and set the above settings appropriately.
|
||||
.. warning:: Deprecated in favour of ``SELECT2_CACHE_BACKEND``. Will be removed in version 5.
|
||||
|
||||
Also note that, when you set the above you need to install ``python-memcached`` library too.
|
||||
Since version 4.3 dajngo-select2 uses Django's own caching solution.
|
||||
The hostname and port will be used to create a new django cache backend.
|
||||
|
||||
.. note:: It is recommended to upgrade to ``SELECT2_CACHE_BACKEND`` to avoid cache consistency issues.
|
||||
|
||||
``SELECT2_BOOTSTRAP`` [Default ``False``]
|
||||
............................................................
|
||||
.........................................
|
||||
|
||||
Setting to True will include the CSS for making Select2 fit in with Bootstrap a bit better using the css found here https://github.com/fk/select2-bootstrap-css.
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
[pytest]
|
||||
norecursedirs=env testapp docs
|
||||
addopts = --tb=short --pep8 --flakes -rxs
|
||||
addopts = --tb=short --pep8 --flakes --isort -rxs
|
||||
DJANGO_SETTINGS_MODULE=tests.testapp.settings
|
||||
pep8maxlinelength=139
|
||||
pep8ignore=
|
||||
runtests.py ALL
|
||||
|
|
|
@ -2,7 +2,9 @@ pytest
|
|||
pytest-pep8
|
||||
pytest-flakes
|
||||
pytest-django
|
||||
pytest-isort
|
||||
pep257
|
||||
selenium
|
||||
model_mommy
|
||||
isort
|
||||
requests
|
||||
requests
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
#! /usr/bin/env python
|
||||
|
||||
import sys
|
||||
import base64
|
||||
import sys
|
||||
import zlib
|
||||
|
||||
|
||||
# Hi There!
|
||||
# You may be wondering what this giant blob of binary data here is, you might
|
||||
# even be worried that we're up to something nefarious (good for you for being
|
||||
|
|
6
setup.py
6
setup.py
|
@ -6,7 +6,7 @@ import codecs
|
|||
import os
|
||||
import sys
|
||||
|
||||
from setuptools import setup, find_packages, Command
|
||||
from setuptools import Command, find_packages, setup
|
||||
|
||||
|
||||
def read(file_name):
|
||||
|
@ -104,7 +104,9 @@ setup(
|
|||
"Programming Language :: Python",
|
||||
"Framework :: Django",
|
||||
],
|
||||
install_requires=[],
|
||||
install_requires=[
|
||||
'django-appconf>=0.6.0',
|
||||
],
|
||||
zip_safe=False,
|
||||
cmdclass={'test': PyTest},
|
||||
)
|
||||
|
|
|
@ -4,33 +4,13 @@ from __future__ import absolute_import, print_function, unicode_literals
|
|||
import os
|
||||
|
||||
import pytest
|
||||
from django import conf
|
||||
from selenium import webdriver
|
||||
from selenium.common.exceptions import WebDriverException
|
||||
|
||||
|
||||
def pytest_configure():
|
||||
os.environ[conf.ENVIRONMENT_VARIABLE] = "tests.testapp.settings"
|
||||
|
||||
try:
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
from django.test.utils import setup_test_environment
|
||||
|
||||
setup_test_environment()
|
||||
|
||||
from django.db import connection
|
||||
|
||||
connection.creation.create_test_db()
|
||||
|
||||
|
||||
browsers = {
|
||||
'firefox': webdriver.Firefox,
|
||||
'chrome': webdriver.Chrome,
|
||||
'phantomjs': webdriver.PhantomJS,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
def test_default_cache():
|
||||
from django_select2.cache import cache
|
||||
|
||||
cache.set('key', 'value')
|
||||
|
||||
assert cache.get('key') == 'value'
|
|
@ -1,9 +1,20 @@
|
|||
# -*- coding:utf-8 -*-
|
||||
from __future__ import print_function, unicode_literals
|
||||
|
||||
import json
|
||||
from collections import Iterable
|
||||
|
||||
import pytest
|
||||
from django.core import signing
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db.models import QuerySet
|
||||
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 tests.testapp.models import Genre
|
||||
|
||||
|
||||
class TestWidgets(object):
|
||||
|
@ -63,3 +74,64 @@ class TestHeavySelect2MultipleWidget(object):
|
|||
with pytest.raises(NoSuchElementException):
|
||||
error = driver.find_element_by_xpath('//body[@JSError]')
|
||||
pytest.fail(error.get_attribute('JSError'))
|
||||
|
||||
|
||||
class TestHeavySelect2Mixin(object):
|
||||
@pytest.fixture(autouse=True)
|
||||
def genres(self, db):
|
||||
mommy.make(Genre, 100)
|
||||
|
||||
def test_get_queryset(self):
|
||||
widget = AutoHeavySelect2Widget()
|
||||
with pytest.raises(NotImplementedError):
|
||||
widget.get_queryset()
|
||||
widget.model = Genre
|
||||
assert isinstance(widget.get_queryset(), QuerySet)
|
||||
widget.model = None
|
||||
widget.queryset = Genre.objects.all()
|
||||
assert isinstance(widget.get_queryset(), QuerySet)
|
||||
|
||||
def test_get_search_fields(self):
|
||||
widget = AutoHeavySelect2Widget()
|
||||
with pytest.raises(NotImplementedError):
|
||||
widget.get_search_fields()
|
||||
|
||||
widget.search_fields = ['title__icontains']
|
||||
assert isinstance(widget.get_search_fields(), Iterable)
|
||||
assert all(isinstance(x, text_type) for x in widget.get_search_fields())
|
||||
|
||||
def test_model_kwarg(self):
|
||||
widget = AutoHeavySelect2Widget(model=Genre, search_fields=['title__icontains'])
|
||||
genre = Genre.objects.last()
|
||||
result = widget.filter_queryset(genre.title)
|
||||
assert result.exists()
|
||||
|
||||
def test_queryset_kwarg(self):
|
||||
widget = AutoHeavySelect2Widget(queryset=Genre.objects, search_fields=['title__icontains'])
|
||||
genre = Genre.objects.last()
|
||||
result = widget.filter_queryset(genre.title)
|
||||
assert result.exists()
|
||||
|
||||
def test_widget_id(self):
|
||||
widget = AutoHeavySelect2Widget()
|
||||
widget.render('name', 'value')
|
||||
assert widget.widget_id
|
||||
assert signing.loads(widget.widget_id) == id(widget)
|
||||
|
||||
def test_render(self):
|
||||
widget = AutoHeavySelect2Widget()
|
||||
widget.render('name', 'value')
|
||||
cached_widget = cache.get(widget._get_cache_key())
|
||||
assert isinstance(cached_widget, AutoHeavySelect2Widget)
|
||||
assert cached_widget.widget_id == widget.widget_id
|
||||
|
||||
def test_ajax_view_registration(self, client):
|
||||
widget = AutoHeavySelect2Widget(queryset=Genre.objects, search_fields=['title__icontains'])
|
||||
widget.render('name', 'value')
|
||||
url = reverse('django_select2_central_json')
|
||||
genre = Genre.objects.last()
|
||||
response = client.get(url, data=dict(field_id=widget.widget_id, term=genre.title))
|
||||
assert response.status_code == 200, response.content
|
||||
data = json.loads(response.content.decode('utf-8'))
|
||||
assert data['results']
|
||||
assert genre.pk in [result['id'] for result in data['results']]
|
||||
|
|
|
@ -3,7 +3,6 @@ from __future__ import absolute_import, unicode_literals
|
|||
|
||||
from django_select2.fields import (AutoModelSelect2Field,
|
||||
AutoModelSelect2TagField)
|
||||
|
||||
from tests.testapp import models
|
||||
|
||||
|
||||
|
|
|
@ -40,4 +40,12 @@ SECRET_KEY = '123456'
|
|||
|
||||
USE_L10N = True
|
||||
|
||||
if os.environ.get('TRAVIS'):
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
|
||||
'LOCATION': 'localhost:11211',
|
||||
}
|
||||
}
|
||||
|
||||
AUTO_RENDER_SELECT2_STATICS = False
|
||||
|
|
|
@ -3,7 +3,8 @@ from __future__ import absolute_import, unicode_literals
|
|||
|
||||
from django.conf.urls import include, patterns, url
|
||||
|
||||
from .forms import ArtistForm, Select2WidgetForm, HeavySelect2WidgetForm, HeavySelect2MultipleWidgetForm
|
||||
from .forms import (ArtistForm, HeavySelect2MultipleWidgetForm,
|
||||
HeavySelect2WidgetForm, Select2WidgetForm)
|
||||
from .views import TemplateFormView, heavy_data
|
||||
|
||||
urlpatterns = patterns(
|
||||
|
|
|
@ -2,8 +2,9 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
from django.views.generic import FormView
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.views.generic import FormView
|
||||
|
||||
|
||||
class TemplateFormView(FormView):
|
||||
|
|
Loading…
Reference in New Issue