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:
Johannes Hoppe 2015-07-17 20:10:16 +02:00
parent 29c74ae63e
commit 33b7dffca1
30 changed files with 457 additions and 697 deletions

View File

@ -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
View File

@ -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
===================

View File

@ -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
===================

View File

@ -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.")

32
django_select2/cache.py Normal file
View File

@ -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]

15
django_select2/conf.py Normal file
View File

@ -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'

View File

@ -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

View File

@ -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.

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from django import template

9
django_select2/types.py Normal file
View File

@ -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.
"""

View File

@ -1,4 +1,4 @@
# -*- coding:utf-8 -*-
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
from django.conf.urls import patterns, url

View File

@ -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

View File

@ -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)

View File

@ -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`"""

View File

@ -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.

View File

@ -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

View File

@ -2,7 +2,9 @@ pytest
pytest-pep8
pytest-flakes
pytest-django
pytest-isort
pep257
selenium
model_mommy
isort
requests
requests

View File

@ -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

View File

@ -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},
)

View File

@ -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,
}

10
tests/test_cache.py Normal file
View File

@ -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'

View File

@ -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']]

View File

@ -3,7 +3,6 @@ from __future__ import absolute_import, unicode_literals
from django_select2.fields import (AutoModelSelect2Field,
AutoModelSelect2TagField)
from tests.testapp import models

View File

@ -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

View File

@ -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(

View File

@ -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):