mirror of
https://github.com/Hopiu/django-select2.git
synced 2026-03-16 21:40:23 +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 .fields import Select2ChoiceField, Select2MultipleChoiceField, HeavySelect2ChoiceField, HeavySelect2MultipleChoiceField
|
||||
from .views import Select2View
|
||||
from .widgets import Select2Widget, Select2MultipleWidget, HeavySelect2Widget, HeavySelect2MultipleWidget, AutoHeavySelect2Widget
|
||||
from .fields import Select2ChoiceField, Select2MultipleChoiceField, \
|
||||
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):
|
||||
widget = Select2Widget
|
||||
|
|
@ -27,5 +121,53 @@ class HeavySelect2ChoiceField(HeavySelect2FieldBase):
|
|||
class HeavySelect2MultipleChoiceField(HeavySelect2FieldBase):
|
||||
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 = {
|
||||
get_url_params: function (term, page, context) {
|
||||
return {
|
||||
'term': term,
|
||||
'page': page,
|
||||
'context': context
|
||||
};
|
||||
var field_id = $(this).data('field_id'),
|
||||
res = {
|
||||
'term': term,
|
||||
'page': page,
|
||||
'context': context
|
||||
};
|
||||
if (field_id) {
|
||||
res['field_id'] = field_id;
|
||||
}
|
||||
return res;
|
||||
},
|
||||
process_results: function (data, page, context) {
|
||||
var results;
|
||||
|
|
@ -22,6 +27,64 @@ var django_select2 = {
|
|||
} else {
|
||||
results = {'results':[]};
|
||||
}
|
||||
if (results.results) {
|
||||
$(this).data('results', results.results);
|
||||
} else {
|
||||
$(this).removeData('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
|
||||
|
||||
from django.http import HttpResponse
|
||||
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):
|
||||
"""
|
||||
|
|
@ -26,18 +32,31 @@ class JSONResponseMixin(object):
|
|||
class Select2View(JSONResponseMixin, View):
|
||||
|
||||
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)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.method == 'GET':
|
||||
term = request.GET.get('term', None)
|
||||
if term is None:
|
||||
return self.render_to_response(self._results_to_context(('missing term', False, [])))
|
||||
page = request.GET.get('page', None)
|
||||
return self.render_to_response(self._results_to_context(('missing term', False, [], )))
|
||||
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)
|
||||
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(
|
||||
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):
|
||||
err, has_more, results = output
|
||||
res = []
|
||||
if err == 'nil':
|
||||
if err == NO_ERR_RESP:
|
||||
for id_, text in results:
|
||||
res.append({'id': id_, 'text': text})
|
||||
return {
|
||||
'err': err,
|
||||
'more': has_more,
|
||||
'results': res
|
||||
'results': res,
|
||||
}
|
||||
|
||||
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.
|
||||
"""
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
class Select2Mixin(object):
|
||||
# For details on these options refer: http://ivaynberg.github.com/select2/#documentation
|
||||
options = {
|
||||
'minimumInputLength': 2,
|
||||
'minimumResultsForSearch': 6, # Only applicable for single value select.
|
||||
'placeholder': '',
|
||||
'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
|
||||
}
|
||||
|
||||
class Media:
|
||||
js = ('js/select2.min.js', )
|
||||
css = {'screen': ('css/select2.css', 'css/extra.css', )}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.options = dict(self.options) # Making an instance specific copy
|
||||
self.init_options()
|
||||
attrs = kwargs.pop('attrs', None)
|
||||
if attrs:
|
||||
|
|
@ -42,7 +50,7 @@ class Select2Mixin(object):
|
|||
options['allowClear'] = not self.is_required
|
||||
return options
|
||||
|
||||
def render_options(self, options):
|
||||
def render_options_code(self, options, id_):
|
||||
out = '{'
|
||||
is_first = True
|
||||
for name in options:
|
||||
|
|
@ -57,10 +65,15 @@ class Select2Mixin(object):
|
|||
out += 'true' if val else 'false'
|
||||
elif type(val) in [types.IntType, types.LongType, types.FloatType]:
|
||||
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):
|
||||
out += val # No quotes here
|
||||
elif isinstance(val, dict):
|
||||
out += self.render_options(val)
|
||||
out += self.render_options_code(val, id_)
|
||||
else:
|
||||
out += "'%s'" % val
|
||||
|
||||
|
|
@ -68,17 +81,20 @@ class Select2Mixin(object):
|
|||
|
||||
def render_js_code(self, id_):
|
||||
if id_:
|
||||
options = dict(self.get_options())
|
||||
options = self.render_options(options)
|
||||
|
||||
return u"""
|
||||
<script>
|
||||
$(function () {
|
||||
$("#%s").select2(%s);
|
||||
%s
|
||||
});
|
||||
</script>""" % (id_, options)
|
||||
</script>""" % self.render_inner_js_code(id_);
|
||||
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):
|
||||
s = str(super(Select2Mixin, self).render(name, value, attrs))
|
||||
s += self.media.render()
|
||||
|
|
@ -87,24 +103,26 @@ class Select2Mixin(object):
|
|||
s += self.render_js_code(id_)
|
||||
return mark_safe(s)
|
||||
|
||||
|
||||
class HeavySelect2Mixin(Select2Mixin):
|
||||
class Media:
|
||||
js = ('js/select2.min.js', 'js/heavy_data.js', )
|
||||
js = ('js/select2.min.js', )
|
||||
css = {'screen': ('css/select2.css', 'css/extra.css', )}
|
||||
|
||||
class HeavySelect2Mixin(Select2Mixin):
|
||||
def __init__(self, **kwargs):
|
||||
self.options = dict(self.options) # Making an instance specific copy
|
||||
self.view = kwargs.pop('data_view', 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')
|
||||
self.url = None
|
||||
self.options['ajax'] = {
|
||||
'dataType': 'json',
|
||||
'quietMillis': 100,
|
||||
'data': JSFunction('django_select2.get_url_params'),
|
||||
'results': JSFunction('django_select2.process_results')
|
||||
'data': JSFunctionInContext('django_select2.get_url_params'),
|
||||
'results': JSFunctionInContext('django_select2.process_results'),
|
||||
}
|
||||
self.options['minimumInputLength'] = 2
|
||||
self.options['initSelection'] = JSFunction('django_select2.onInit')
|
||||
super(HeavySelect2Mixin, self).__init__(**kwargs)
|
||||
|
||||
def get_options(self):
|
||||
|
|
@ -114,10 +132,31 @@ class HeavySelect2Mixin(Select2Mixin):
|
|||
self.options['ajax']['url'] = self.url
|
||||
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):
|
||||
def init_options(self):
|
||||
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):
|
||||
def init_options(self):
|
||||
self.options.pop('multiple', None)
|
||||
|
|
@ -136,3 +175,5 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, forms.TextInput):
|
|||
self.options.pop('allowClear', 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