mirror of
https://github.com/Hopiu/django-select2.git
synced 2026-05-28 13:58:20 +00:00
Enhanced Heavy fields. Added 'auto' fields. Some bug fixes.
This commit is contained in:
parent
243c614c3a
commit
d5aa9b5ddb
9 changed files with 533 additions and 37 deletions
14
README.md
14
README.md
|
|
@ -1,4 +1,14 @@
|
||||||
django-select2
|
Django-Select2
|
||||||
==============
|
==============
|
||||||
|
|
||||||
This is a [Django](https://www.djangoproject.com/) integration for [Select2](http://ivaynberg.github.com/select2/)
|
This is a [Django](https://www.djangoproject.com/) integration of [Select2](http://ivaynberg.github.com/select2/).
|
||||||
|
|
||||||
|
The app includes Select2 driven Django Widgets and Form Fields.
|
||||||
|
|
||||||
|
More details can be found on my blog at - [http://blog.applegrew.com/2012/08/django-select2/](http://blog.applegrew.com/2012/08/django-select2/).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
from .widgets import Select2Widget, Select2MultipleWidget, HeavySelect2Widget, HeavySelect2MultipleWidget
|
from .widgets import Select2Widget, Select2MultipleWidget, HeavySelect2Widget, HeavySelect2MultipleWidget, AutoHeavySelect2Widget
|
||||||
from .fields import Select2ChoiceField, Select2MultipleChoiceField, HeavySelect2ChoiceField, HeavySelect2MultipleChoiceField
|
from .fields import Select2ChoiceField, Select2MultipleChoiceField, \
|
||||||
from .views import Select2View
|
HeavySelect2ChoiceField, HeavySelect2MultipleChoiceField, \
|
||||||
|
ModelSelect2Field, AutoSelect2Field, AutoModelSelect2Field
|
||||||
|
from .views import Select2View, NO_ERR_RESP
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,100 @@
|
||||||
from django import forms
|
class AutoViewFieldMixin(object):
|
||||||
|
"""Registers itself with AutoResponseView."""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
name = self.__class__.__name__
|
||||||
|
from .util import register_field
|
||||||
|
if name not in ['AutoViewFieldMixin', 'AutoModelSelect2Field']:
|
||||||
|
id_ = register_field("%s.%s" % (self.__module__, name), self)
|
||||||
|
self.widget.field_id = id_
|
||||||
|
super(AutoViewFieldMixin, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
from .widgets import Select2Widget, Select2MultipleWidget, HeavySelect2Widget, HeavySelect2MultipleWidget
|
def security_check(self, request, *args, **kwargs):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_results(self, request, term, page, context):
|
||||||
|
raise NotImplemented
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
from django.utils.encoding import smart_unicode
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.core.validators import EMPTY_VALUES
|
||||||
|
|
||||||
|
from .widgets import Select2Widget, Select2MultipleWidget,\
|
||||||
|
HeavySelect2Widget, HeavySelect2MultipleWidget, AutoHeavySelect2Widget
|
||||||
|
from .views import NO_ERR_RESP
|
||||||
|
|
||||||
|
class ModelResultJsonMixin(object):
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
if self.queryset is None:
|
||||||
|
raise ValueError('queryset is required.')
|
||||||
|
|
||||||
|
if not self.search_fields:
|
||||||
|
raise ValueError('search_fields is required.')
|
||||||
|
|
||||||
|
self.max_results = getattr(self, 'max_results', None)
|
||||||
|
self.to_field_name = getattr(self, 'to_field_name', 'pk')
|
||||||
|
|
||||||
|
super(ModelResultJsonMixin, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def label_from_instance(self, obj):
|
||||||
|
return smart_unicode(obj)
|
||||||
|
|
||||||
|
def prepare_qs_params(self, request, search_term, search_fields):
|
||||||
|
q = None
|
||||||
|
for field in search_fields:
|
||||||
|
kwargs = {}
|
||||||
|
kwargs[field] = search_term
|
||||||
|
if q is None:
|
||||||
|
q = Q(**kwargs)
|
||||||
|
else:
|
||||||
|
q = q | Q(**kwargs)
|
||||||
|
return {'or': [q], 'and': {},}
|
||||||
|
|
||||||
|
def get_results(self, request, term, page, context):
|
||||||
|
qs = copy.deepcopy(self.queryset)
|
||||||
|
params = self.prepare_qs_params(request, term, self.search_fields)
|
||||||
|
|
||||||
|
if self.max_results:
|
||||||
|
min_ = (page - 1) * self.max_results
|
||||||
|
max_ = min_ + self.max_results + 1 # fetching one extra row to check if it has more rows.
|
||||||
|
res = list(qs.filter(*params['or'], **params['and'])[min_:max_])
|
||||||
|
has_more = len(res) == (max_ - min_)
|
||||||
|
if has_more:
|
||||||
|
res = res[:-1]
|
||||||
|
else:
|
||||||
|
res = list(qs.filter(*params['or'], **params['and']))
|
||||||
|
has_more = False
|
||||||
|
|
||||||
|
res = [ (getattr(obj, self.to_field_name), self.label_from_instance(obj), ) for obj in res ]
|
||||||
|
return (NO_ERR_RESP, has_more, res, )
|
||||||
|
|
||||||
|
class ModelValueMixin(object):
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid_choice': _(u'Select a valid choice. That choice is not one of'
|
||||||
|
u' the available choices.'),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
if self.queryset is None:
|
||||||
|
raise ValueError('queryset is required.')
|
||||||
|
|
||||||
|
self.to_field_name = getattr(self, 'to_field_name', 'pk')
|
||||||
|
|
||||||
|
super(ModelValueMixin, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if value in EMPTY_VALUES:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
key = self.to_field_name
|
||||||
|
value = self.queryset.get(**{key: value})
|
||||||
|
except (ValueError, self.queryset.model.DoesNotExist):
|
||||||
|
raise ValidationError(self.error_messages['invalid_choice'])
|
||||||
|
return value
|
||||||
|
|
||||||
class Select2ChoiceField(forms.ChoiceField):
|
class Select2ChoiceField(forms.ChoiceField):
|
||||||
widget = Select2Widget
|
widget = Select2Widget
|
||||||
|
|
@ -27,5 +121,53 @@ class HeavySelect2ChoiceField(HeavySelect2FieldBase):
|
||||||
class HeavySelect2MultipleChoiceField(HeavySelect2FieldBase):
|
class HeavySelect2MultipleChoiceField(HeavySelect2FieldBase):
|
||||||
widget = HeavySelect2MultipleWidget
|
widget = HeavySelect2MultipleWidget
|
||||||
|
|
||||||
|
class AutoSelect2Field(ModelResultJsonMixin, AutoViewFieldMixin, HeavySelect2ChoiceField):
|
||||||
|
"""
|
||||||
|
This needs to be subclassed. The first instance of a class (sub-class) is used to serve all incoming
|
||||||
|
json query requests for that type (class).
|
||||||
|
"""
|
||||||
|
|
||||||
|
widget = AutoHeavySelect2Widget
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.data_view = "django_select2_central_json"
|
||||||
|
kwargs['data_view'] = self.data_view
|
||||||
|
super(AutoSelect2Field, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
class AutoModelSelect2Field(ModelResultJsonMixin, AutoViewFieldMixin, ModelValueMixin, HeavySelect2ChoiceField):
|
||||||
|
"""
|
||||||
|
This needs to be subclassed. The first instance of a class (sub-class) is used to serve all incoming
|
||||||
|
json query requests for that type (class).
|
||||||
|
"""
|
||||||
|
|
||||||
|
widget = AutoHeavySelect2Widget
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.data_view = "django_select2_central_json"
|
||||||
|
kwargs['data_view'] = self.data_view
|
||||||
|
super(AutoModelSelect2Field, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
class ModelSelect2Field(ModelValueMixin, Select2ChoiceField):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.queryset = kwargs.pop('queryset', None)
|
||||||
|
self.to_field_name = kwargs.pop('to_field_name', 'pk')
|
||||||
|
|
||||||
|
choices = kwargs.pop('choices', None)
|
||||||
|
if choices is None:
|
||||||
|
choices = []
|
||||||
|
for obj in self.queryset.all():
|
||||||
|
choices.append((getattr(obj, self.to_field_name), smart_unicode(obj), ))
|
||||||
|
|
||||||
|
kwargs['choices'] = choices
|
||||||
|
|
||||||
|
super(ModelSelect2Field, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def valid_value(self, value):
|
||||||
|
val = getattr(value, self.to_field_name)
|
||||||
|
for k, v in self.choices:
|
||||||
|
if k == val:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
|
|
||||||
var django_select2 = {
|
var django_select2 = {
|
||||||
get_url_params: function (term, page, context) {
|
get_url_params: function (term, page, context) {
|
||||||
return {
|
var field_id = $(this).data('field_id'),
|
||||||
'term': term,
|
res = {
|
||||||
'page': page,
|
'term': term,
|
||||||
'context': context
|
'page': page,
|
||||||
};
|
'context': context
|
||||||
|
};
|
||||||
|
if (field_id) {
|
||||||
|
res['field_id'] = field_id;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
},
|
},
|
||||||
process_results: function (data, page, context) {
|
process_results: function (data, page, context) {
|
||||||
var results;
|
var results;
|
||||||
|
|
@ -22,6 +27,64 @@ var django_select2 = {
|
||||||
} else {
|
} else {
|
||||||
results = {'results':[]};
|
results = {'results':[]};
|
||||||
}
|
}
|
||||||
|
if (results.results) {
|
||||||
|
$(this).data('results', results.results);
|
||||||
|
} else {
|
||||||
|
$(this).removeData('results');
|
||||||
|
}
|
||||||
return results;
|
return results;
|
||||||
|
},
|
||||||
|
setCookie: function (c_name, value) {
|
||||||
|
document.cookie=c_name + "=" + escape(value);
|
||||||
|
},
|
||||||
|
getCookie: function (c_name) {
|
||||||
|
var i,x,y,ARRcookies=document.cookie.split(";");
|
||||||
|
for (i=0; i<ARRcookies.length; i++) {
|
||||||
|
x=ARRcookies[i].substr(0,ARRcookies[i].indexOf("="));
|
||||||
|
y=ARRcookies[i].substr(ARRcookies[i].indexOf("=")+1);
|
||||||
|
x=x.replace(/^\s+|\s+$/g,"");
|
||||||
|
if (x==c_name) {
|
||||||
|
return unescape(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delCookie: function (c_name) {
|
||||||
|
document.cookie = c_name + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||||
|
},
|
||||||
|
onValChange: function () {
|
||||||
|
var e = $(this), res = e.data('results'), val = e.val(), txt, id = e.attr('id');
|
||||||
|
|
||||||
|
if (res) {
|
||||||
|
for (var i in res) {
|
||||||
|
if (res[i].id == val) {
|
||||||
|
val = res[i].id; // To set it to correct data type.
|
||||||
|
txt = res[i].text;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (txt) {
|
||||||
|
// Cookies are used to persist selection's text. This needed
|
||||||
|
//when the form springs back if there is any validation failure.
|
||||||
|
django_select2.setCookie(id + '_heavy_val', val);
|
||||||
|
django_select2.setCookie(id + '_heavy_txt', txt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
django_select2.delCookie(id + '_heavy_val');
|
||||||
|
django_select2.delCookie(id + '_heavy_txt');
|
||||||
|
},
|
||||||
|
onInit: function (e) {
|
||||||
|
e = $(e);
|
||||||
|
var id = e.attr('id'),
|
||||||
|
val = django_select2.getCookie(id + '_heavy_val'),
|
||||||
|
txt = django_select2.getCookie(id + '_heavy_txt');
|
||||||
|
|
||||||
|
if (txt && e.val() == val) {
|
||||||
|
// Restores persisted value text.
|
||||||
|
return {'id': val, 'text': txt};
|
||||||
|
} else {
|
||||||
|
e.val(null);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
7
django_select2/urls.py
Normal file
7
django_select2/urls.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
|
from .views import AutoResponseView
|
||||||
|
|
||||||
|
urlpatterns = patterns("",
|
||||||
|
url(r"^fields/auto.json$", AutoResponseView.as_view(), name="django_select2_central_json"),
|
||||||
|
)
|
||||||
43
django_select2/util.py
Normal file
43
django_select2/util.py
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
def synchronized(f):
|
||||||
|
f.__lock__ = threading.Lock()
|
||||||
|
|
||||||
|
def synced_f(*args, **kwargs):
|
||||||
|
with f.__lock__:
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return synced_f
|
||||||
|
|
||||||
|
|
||||||
|
__id_store = {}
|
||||||
|
__field_store = {}
|
||||||
|
|
||||||
|
ID_PATTERN = r"[0-9_a-zA-Z.:+\- ]+"
|
||||||
|
|
||||||
|
def is_valid_id(val):
|
||||||
|
regex = "^%s$" % ID_PATTERN
|
||||||
|
if re.match(regex, val) is None:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
@synchronized
|
||||||
|
def register_field(name, field):
|
||||||
|
global __id_store, __field_store
|
||||||
|
|
||||||
|
if name not in __field_store:
|
||||||
|
# Generating id
|
||||||
|
id_ = "%d:%s" % (len(__id_store), str(datetime.datetime.now()))
|
||||||
|
|
||||||
|
__field_store[name] = id_
|
||||||
|
__id_store[id_] = field
|
||||||
|
else:
|
||||||
|
id_ = __field_store[name]
|
||||||
|
return id_
|
||||||
|
|
||||||
|
def get_field(id_):
|
||||||
|
return __id_store.get(id_, None)
|
||||||
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
from django.core.exceptions import PermissionDenied
|
||||||
|
from django.http import Http404
|
||||||
|
|
||||||
|
from .util import get_field, is_valid_id
|
||||||
|
|
||||||
|
NO_ERR_RESP = 'nil'
|
||||||
|
|
||||||
class JSONResponseMixin(object):
|
class JSONResponseMixin(object):
|
||||||
"""
|
"""
|
||||||
|
|
@ -26,18 +32,31 @@ class JSONResponseMixin(object):
|
||||||
class Select2View(JSONResponseMixin, View):
|
class Select2View(JSONResponseMixin, View):
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
self.check_all_permissions(request, *args, **kwargs)
|
try:
|
||||||
|
self.check_all_permissions(request, *args, **kwargs)
|
||||||
|
except Exception, e:
|
||||||
|
return self.respond_with_exception(e)
|
||||||
return super(Select2View, self).dispatch(request, *args, **kwargs)
|
return super(Select2View, self).dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
term = request.GET.get('term', None)
|
term = request.GET.get('term', None)
|
||||||
if term is None:
|
if term is None:
|
||||||
return self.render_to_response(self._results_to_context(('missing term', False, [])))
|
return self.render_to_response(self._results_to_context(('missing term', False, [], )))
|
||||||
page = request.GET.get('page', None)
|
if not term:
|
||||||
|
return self.render_to_response(self._results_to_context((NO_ERR_RESP, False, [], )))
|
||||||
|
|
||||||
|
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(('bade page no.', False, [], )))
|
||||||
context = request.GET.get('context', None)
|
context = request.GET.get('context', None)
|
||||||
else:
|
else:
|
||||||
return self.render_to_response(self._results_to_context(('not a get request', False, [])))
|
return self.render_to_response(self._results_to_context(('not a get request', False, [], )))
|
||||||
|
|
||||||
return self.render_to_response(
|
return self.render_to_response(
|
||||||
self._results_to_context(
|
self._results_to_context(
|
||||||
|
|
@ -45,16 +64,26 @@ class Select2View(JSONResponseMixin, View):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def respond_with_exception(self, e):
|
||||||
|
if isinstance(e, Http404):
|
||||||
|
status = 404
|
||||||
|
else:
|
||||||
|
status = getattr(e, 'status_code', 400)
|
||||||
|
return self.render_to_response(
|
||||||
|
self._results_to_context((str(e), False, [],)),
|
||||||
|
status=status
|
||||||
|
)
|
||||||
|
|
||||||
def _results_to_context(self, output):
|
def _results_to_context(self, output):
|
||||||
err, has_more, results = output
|
err, has_more, results = output
|
||||||
res = []
|
res = []
|
||||||
if err == 'nil':
|
if err == NO_ERR_RESP:
|
||||||
for id_, text in results:
|
for id_, text in results:
|
||||||
res.append({'id': id_, 'text': text})
|
res.append({'id': id_, 'text': text})
|
||||||
return {
|
return {
|
||||||
'err': err,
|
'err': err,
|
||||||
'more': has_more,
|
'more': has_more,
|
||||||
'results': res
|
'results': res,
|
||||||
}
|
}
|
||||||
|
|
||||||
def check_all_permissions(self, request, *args, **kwargs):
|
def check_all_permissions(self, request, *args, **kwargs):
|
||||||
|
|
@ -76,3 +105,26 @@ class Select2View(JSONResponseMixin, View):
|
||||||
`has_more` should be true if there are more rows.
|
`has_more` should be true if there are more rows.
|
||||||
"""
|
"""
|
||||||
raise NotImplemented
|
raise NotImplemented
|
||||||
|
|
||||||
|
|
||||||
|
class AutoResponseView(Select2View):
|
||||||
|
"""A central view meant to respond to Ajax queries for all Heavy fields. Although it is not mandatory to use. This is just a helper."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,23 @@ from django.utils.safestring import mark_safe
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
class JSFunction(str):
|
class JSFunction(str):
|
||||||
|
"""
|
||||||
|
Flags that the string is the name of a JS function. Used by Select2Mixin.render_options()
|
||||||
|
to make sure that this string is not quoted like other strings.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class JSFunctionInContext(str):
|
||||||
|
"""
|
||||||
|
Like JSFunction, this too flags the string as JS function, but with a special requirement.
|
||||||
|
The JS function needs to be invoked in the context of the current Select2 Html DOM,
|
||||||
|
such that 'this' inside the function refers to the source Select2 DOM.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Select2Mixin(object):
|
class Select2Mixin(object):
|
||||||
# For details on these options refer: http://ivaynberg.github.com/select2/#documentation
|
# For details on these options refer: http://ivaynberg.github.com/select2/#documentation
|
||||||
options = {
|
options = {
|
||||||
'minimumInputLength': 2,
|
|
||||||
'minimumResultsForSearch': 6, # Only applicable for single value select.
|
'minimumResultsForSearch': 6, # Only applicable for single value select.
|
||||||
'placeholder': '',
|
'placeholder': '',
|
||||||
'allowClear': True, # Not allowed when field is multiple since there each value has a clear button.
|
'allowClear': True, # Not allowed when field is multiple since there each value has a clear button.
|
||||||
|
|
@ -18,11 +29,8 @@ class Select2Mixin(object):
|
||||||
'closeOnSelect': False
|
'closeOnSelect': False
|
||||||
}
|
}
|
||||||
|
|
||||||
class Media:
|
|
||||||
js = ('js/select2.min.js', )
|
|
||||||
css = {'screen': ('css/select2.css', 'css/extra.css', )}
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self.options = dict(self.options) # Making an instance specific copy
|
||||||
self.init_options()
|
self.init_options()
|
||||||
attrs = kwargs.pop('attrs', None)
|
attrs = kwargs.pop('attrs', None)
|
||||||
if attrs:
|
if attrs:
|
||||||
|
|
@ -42,7 +50,7 @@ class Select2Mixin(object):
|
||||||
options['allowClear'] = not self.is_required
|
options['allowClear'] = not self.is_required
|
||||||
return options
|
return options
|
||||||
|
|
||||||
def render_options(self, options):
|
def render_options_code(self, options, id_):
|
||||||
out = '{'
|
out = '{'
|
||||||
is_first = True
|
is_first = True
|
||||||
for name in options:
|
for name in options:
|
||||||
|
|
@ -57,10 +65,15 @@ class Select2Mixin(object):
|
||||||
out += 'true' if val else 'false'
|
out += 'true' if val else 'false'
|
||||||
elif type(val) in [types.IntType, types.LongType, types.FloatType]:
|
elif type(val) in [types.IntType, types.LongType, types.FloatType]:
|
||||||
out += str(val)
|
out += str(val)
|
||||||
|
elif isinstance(val, JSFunctionInContext):
|
||||||
|
out += """function () {
|
||||||
|
var args = Array.prototype.slice.call(arguments);
|
||||||
|
return %s.apply($('#%s').get(0), args);
|
||||||
|
}""" % (val, id_)
|
||||||
elif isinstance(val, JSFunction):
|
elif isinstance(val, JSFunction):
|
||||||
out += val # No quotes here
|
out += val # No quotes here
|
||||||
elif isinstance(val, dict):
|
elif isinstance(val, dict):
|
||||||
out += self.render_options(val)
|
out += self.render_options_code(val, id_)
|
||||||
else:
|
else:
|
||||||
out += "'%s'" % val
|
out += "'%s'" % val
|
||||||
|
|
||||||
|
|
@ -68,17 +81,20 @@ class Select2Mixin(object):
|
||||||
|
|
||||||
def render_js_code(self, id_):
|
def render_js_code(self, id_):
|
||||||
if id_:
|
if id_:
|
||||||
options = dict(self.get_options())
|
|
||||||
options = self.render_options(options)
|
|
||||||
|
|
||||||
return u"""
|
return u"""
|
||||||
<script>
|
<script>
|
||||||
$(function () {
|
$(function () {
|
||||||
$("#%s").select2(%s);
|
%s
|
||||||
});
|
});
|
||||||
</script>""" % (id_, options)
|
</script>""" % self.render_inner_js_code(id_);
|
||||||
return u''
|
return u''
|
||||||
|
|
||||||
|
def render_inner_js_code(self, id_):
|
||||||
|
options = dict(self.get_options())
|
||||||
|
options = self.render_options_code(options, id_)
|
||||||
|
|
||||||
|
return '$("#%s").select2(%s);' % (id_, options)
|
||||||
|
|
||||||
def render(self, name, value, attrs=None):
|
def render(self, name, value, attrs=None):
|
||||||
s = str(super(Select2Mixin, self).render(name, value, attrs))
|
s = str(super(Select2Mixin, self).render(name, value, attrs))
|
||||||
s += self.media.render()
|
s += self.media.render()
|
||||||
|
|
@ -87,24 +103,26 @@ class Select2Mixin(object):
|
||||||
s += self.render_js_code(id_)
|
s += self.render_js_code(id_)
|
||||||
return mark_safe(s)
|
return mark_safe(s)
|
||||||
|
|
||||||
|
|
||||||
class HeavySelect2Mixin(Select2Mixin):
|
|
||||||
class Media:
|
class Media:
|
||||||
js = ('js/select2.min.js', 'js/heavy_data.js', )
|
js = ('js/select2.min.js', )
|
||||||
css = {'screen': ('css/select2.css', 'css/extra.css', )}
|
css = {'screen': ('css/select2.css', 'css/extra.css', )}
|
||||||
|
|
||||||
|
class HeavySelect2Mixin(Select2Mixin):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self.options = dict(self.options) # Making an instance specific copy
|
||||||
self.view = kwargs.pop('data_view', None)
|
self.view = kwargs.pop('data_view', None)
|
||||||
self.url = kwargs.pop('data_url', None)
|
self.url = kwargs.pop('data_url', None)
|
||||||
if not (self.view and self.url):
|
if not self.view and not self.url:
|
||||||
raise ValueError('data_view or data_url is required')
|
raise ValueError('data_view or data_url is required')
|
||||||
self.url = None
|
self.url = None
|
||||||
self.options['ajax'] = {
|
self.options['ajax'] = {
|
||||||
'dataType': 'json',
|
'dataType': 'json',
|
||||||
'quietMillis': 100,
|
'quietMillis': 100,
|
||||||
'data': JSFunction('django_select2.get_url_params'),
|
'data': JSFunctionInContext('django_select2.get_url_params'),
|
||||||
'results': JSFunction('django_select2.process_results')
|
'results': JSFunctionInContext('django_select2.process_results'),
|
||||||
}
|
}
|
||||||
|
self.options['minimumInputLength'] = 2
|
||||||
|
self.options['initSelection'] = JSFunction('django_select2.onInit')
|
||||||
super(HeavySelect2Mixin, self).__init__(**kwargs)
|
super(HeavySelect2Mixin, self).__init__(**kwargs)
|
||||||
|
|
||||||
def get_options(self):
|
def get_options(self):
|
||||||
|
|
@ -114,10 +132,31 @@ class HeavySelect2Mixin(Select2Mixin):
|
||||||
self.options['ajax']['url'] = self.url
|
self.options['ajax']['url'] = self.url
|
||||||
return super(HeavySelect2Mixin, self).get_options()
|
return super(HeavySelect2Mixin, self).get_options()
|
||||||
|
|
||||||
|
def render_inner_js_code(self, id_):
|
||||||
|
js = super(HeavySelect2Mixin, self).render_inner_js_code(id_)
|
||||||
|
js += "$('#%s').change(django_select2.onValChange);" % id_
|
||||||
|
return js
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
js = ('js/select2.min.js', 'js/heavy_data.js', )
|
||||||
|
css = {'screen': ('css/select2.css', 'css/extra.css', )}
|
||||||
|
|
||||||
|
class AutoHeavySelect2Mixin(HeavySelect2Mixin):
|
||||||
|
def render_inner_js_code(self, id_):
|
||||||
|
js = super(AutoHeavySelect2Mixin, self).render_inner_js_code(id_)
|
||||||
|
js += "$('#%s').data('field_id', '%s');" % (id_, self.field_id)
|
||||||
|
return js
|
||||||
|
|
||||||
class Select2Widget(Select2Mixin, forms.Select):
|
class Select2Widget(Select2Mixin, forms.Select):
|
||||||
def init_options(self):
|
def init_options(self):
|
||||||
self.options.pop('multiple', None)
|
self.options.pop('multiple', None)
|
||||||
|
|
||||||
|
def render_options(self, choices, selected_choices):
|
||||||
|
if not self.is_required:
|
||||||
|
choices = list(choices)
|
||||||
|
choices.append(('', '', )) # Adding an empty choice
|
||||||
|
return super(Select2Widget, self).render_options(choices, selected_choices)
|
||||||
|
|
||||||
class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple):
|
class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple):
|
||||||
def init_options(self):
|
def init_options(self):
|
||||||
self.options.pop('multiple', None)
|
self.options.pop('multiple', None)
|
||||||
|
|
@ -136,3 +175,5 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, forms.TextInput):
|
||||||
self.options.pop('allowClear', None)
|
self.options.pop('allowClear', None)
|
||||||
self.options.pop('minimumResultsForSearch', None)
|
self.options.pop('minimumResultsForSearch', None)
|
||||||
|
|
||||||
|
class AutoHeavySelect2Widget(AutoHeavySelect2Mixin, HeavySelect2Widget):
|
||||||
|
pass
|
||||||
|
|
|
||||||
136
setup.py
Normal file
136
setup.py
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import codecs
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from distutils.util import convert_path
|
||||||
|
from fnmatch import fnmatchcase
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
def read(fname):
|
||||||
|
return codecs.open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||||
|
|
||||||
|
|
||||||
|
# Provided as an attribute, so you can append to these instead
|
||||||
|
# of replicating them:
|
||||||
|
standard_exclude = ["*.py", "*.pyc", "*$py.class", "*~", ".*", "*.bak"]
|
||||||
|
standard_exclude_directories = [
|
||||||
|
".*", "CVS", "_darcs", "./build", "./dist", "EGG-INFO", "*.egg-info"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
|
||||||
|
# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
||||||
|
# Note: you may want to copy this into your setup.py file verbatim, as
|
||||||
|
# you can't import this from another package, when you don't know if
|
||||||
|
# that package is installed yet.
|
||||||
|
def find_package_data(
|
||||||
|
where=".",
|
||||||
|
package="",
|
||||||
|
exclude=standard_exclude,
|
||||||
|
exclude_directories=standard_exclude_directories,
|
||||||
|
only_in_packages=True,
|
||||||
|
show_ignored=False):
|
||||||
|
"""
|
||||||
|
Return a dictionary suitable for use in ``package_data``
|
||||||
|
in a distutils ``setup.py`` file.
|
||||||
|
|
||||||
|
The dictionary looks like::
|
||||||
|
|
||||||
|
{"package": [files]}
|
||||||
|
|
||||||
|
Where ``files`` is a list of all the files in that package that
|
||||||
|
don"t match anything in ``exclude``.
|
||||||
|
|
||||||
|
If ``only_in_packages`` is true, then top-level directories that
|
||||||
|
are not packages won"t be included (but directories under packages
|
||||||
|
will).
|
||||||
|
|
||||||
|
Directories matching any pattern in ``exclude_directories`` will
|
||||||
|
be ignored; by default directories with leading ``.``, ``CVS``,
|
||||||
|
and ``_darcs`` will be ignored.
|
||||||
|
|
||||||
|
If ``show_ignored`` is true, then all the files that aren"t
|
||||||
|
included in package data are shown on stderr (for debugging
|
||||||
|
purposes).
|
||||||
|
|
||||||
|
Note patterns use wildcards, or can be exact paths (including
|
||||||
|
leading ``./``), and all searching is case-insensitive.
|
||||||
|
"""
|
||||||
|
out = {}
|
||||||
|
stack = [(convert_path(where), "", package, only_in_packages)]
|
||||||
|
while stack:
|
||||||
|
where, prefix, package, only_in_packages = stack.pop(0)
|
||||||
|
for name in os.listdir(where):
|
||||||
|
fn = os.path.join(where, name)
|
||||||
|
if os.path.isdir(fn):
|
||||||
|
bad_name = False
|
||||||
|
for pattern in exclude_directories:
|
||||||
|
if (fnmatchcase(name, pattern)
|
||||||
|
or fn.lower() == pattern.lower()):
|
||||||
|
bad_name = True
|
||||||
|
if show_ignored:
|
||||||
|
print >> sys.stderr, (
|
||||||
|
"Directory %s ignored by pattern %s"
|
||||||
|
% (fn, pattern))
|
||||||
|
break
|
||||||
|
if bad_name:
|
||||||
|
continue
|
||||||
|
if (os.path.isfile(os.path.join(fn, "__init__.py"))
|
||||||
|
and not prefix):
|
||||||
|
if not package:
|
||||||
|
new_package = name
|
||||||
|
else:
|
||||||
|
new_package = package + "." + name
|
||||||
|
stack.append((fn, "", new_package, False))
|
||||||
|
else:
|
||||||
|
stack.append((fn, prefix + name + "/", package, only_in_packages))
|
||||||
|
elif package or not only_in_packages:
|
||||||
|
# is a file
|
||||||
|
bad_name = False
|
||||||
|
for pattern in exclude:
|
||||||
|
if (fnmatchcase(name, pattern)
|
||||||
|
or fn.lower() == pattern.lower()):
|
||||||
|
bad_name = True
|
||||||
|
if show_ignored:
|
||||||
|
print >> sys.stderr, (
|
||||||
|
"File %s ignored by pattern %s"
|
||||||
|
% (fn, pattern))
|
||||||
|
break
|
||||||
|
if bad_name:
|
||||||
|
continue
|
||||||
|
out.setdefault(package, []).append(prefix+name)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE = "django_select2"
|
||||||
|
NAME = "Django-Select2"
|
||||||
|
DESCRIPTION = "Select2 option fields for Django"
|
||||||
|
AUTHOR = "AppleGrew"
|
||||||
|
AUTHOR_EMAIL = "admin@applegrew.com"
|
||||||
|
URL = "https://github.com/applegrew/django-select2"
|
||||||
|
VERSION = __import__(PACKAGE).__version__
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name=NAME,
|
||||||
|
version=VERSION,
|
||||||
|
description=DESCRIPTION,
|
||||||
|
long_description=read("README.md"),
|
||||||
|
author=AUTHOR,
|
||||||
|
author_email=AUTHOR_EMAIL,
|
||||||
|
license="MIT",
|
||||||
|
url=URL,
|
||||||
|
packages=find_packages(exclude=["tests.*", "tests"]),
|
||||||
|
package_data=find_package_data(PACKAGE, only_in_packages=False),
|
||||||
|
include_package_data=True,
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 4 - Beta",
|
||||||
|
"Environment :: Web Environment",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: BSD License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Framework :: Django",
|
||||||
|
],
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue