Multi value bu fixes

Many bugs ironed out of Multi value fields. Added many new tests for
multi value fields.
This commit is contained in:
AppleGrew (applegrew) 2012-08-21 23:21:41 +05:30
parent 8d42210053
commit 691fa14e2e
10 changed files with 204 additions and 67 deletions

View file

@ -13,10 +13,9 @@ 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.
=================
Changelog Summary
=================
v2.0
----
### v2.0
Mostly major bug fixes in code and design. The changes were many, raising the possibility of backward incompatibilty. However, the backward incompatibilty would be subtle.

View file

@ -2,8 +2,10 @@ __version__ = "2.0"
from django.conf import settings
if settings.configured:
from .widgets import Select2Widget, Select2MultipleWidget, HeavySelect2Widget, HeavySelect2MultipleWidget, AutoHeavySelect2Widget
from .fields import Select2ChoiceField, Select2MultipleChoiceField, \
HeavySelect2ChoiceField, HeavySelect2MultipleChoiceField, \
ModelSelect2Field, AutoSelect2Field, AutoModelSelect2Field, ModelMultipleSelect2Field
from .widgets import Select2Widget, Select2MultipleWidget, HeavySelect2Widget, HeavySelect2MultipleWidget, \
AutoHeavySelect2Widget, AutoHeavySelect2MultipleWidget
from .fields import Select2ChoiceField, Select2MultipleChoiceField, HeavySelect2ChoiceField, \
HeavySelect2MultipleChoiceField, HeavyModelSelect2ChoiceField, HeavyModelSelect2MultipleChoiceField, \
ModelSelect2Field, ModelSelect2MultipleField, AutoSelect2Field, AutoSelect2MultipleField, \
AutoModelSelect2Field, AutoModelSelect2MultipleField
from .views import Select2View, NO_ERR_RESP

View file

@ -2,8 +2,10 @@ class AutoViewFieldMixin(object):
"""Registers itself with AutoResponseView."""
def __init__(self, *args, **kwargs):
name = self.__class__.__name__
print '<><><><><><>', self.__module__, ' :::: ', name,
from .util import register_field
if name not in ['AutoViewFieldMixin', 'AutoSelect2Field', 'AutoModelSelect2Field']:
if name not in ['AutoViewFieldMixin', 'AutoSelect2Field', 'AutoModelSelect2Field',
'AutoSelect2MultipleField', 'AutoModelSelect2MultipleField']:
id_ = register_field("%s.%s" % (self.__module__, name), self)
self.widget.field_id = id_
super(AutoViewFieldMixin, self).__init__(*args, **kwargs)
@ -24,7 +26,8 @@ from django.utils.encoding import smart_unicode
from django.core.validators import EMPTY_VALUES
from .widgets import Select2Widget, Select2MultipleWidget,\
HeavySelect2Widget, HeavySelect2MultipleWidget, AutoHeavySelect2Widget
HeavySelect2Widget, HeavySelect2MultipleWidget, AutoHeavySelect2Widget, \
AutoHeavySelect2MultipleWidget
from .views import NO_ERR_RESP
from .util import extract_some_key_val
@ -173,7 +176,7 @@ class ModelSelect2Field(ModelChoiceField) :
"Light Model Select2 field"
widget = Select2Widget
class ModelMultipleSelect2Field(ModelMultipleChoiceField) :
class ModelSelect2MultipleField(ModelMultipleChoiceField) :
"Light multiple-value Model Select2 field"
widget = Select2MultipleWidget
@ -199,16 +202,21 @@ class HeavySelect2ChoiceField(HeavySelect2FieldBase):
class HeavySelect2MultipleChoiceField(HeavySelect2FieldBase):
widget = HeavySelect2MultipleWidget
### Heavy field specialized for Models (Single valued) ###
### Heavy field specialized for Models ###
class HeavyModelSelect2ChoiceField(QuerysetChoiceMixin, HeavySelect2ChoiceField, ModelChoiceField):
def __init__(self, *args, **kwargs):
kwargs.pop('choices', None)
super(HeavyModelSelect2ChoiceField, self).__init__(*args, **kwargs)
class HeavyModelSelect2MultipleChoiceField(QuerysetChoiceMixin, HeavySelect2MultipleChoiceField, ModelMultipleChoiceField):
def __init__(self, *args, **kwargs):
kwargs.pop('choices', None)
super(HeavyModelSelect2MultipleChoiceField, self).__init__(*args, **kwargs)
### Heavy general field that uses central AutoView ###
class AutoSelect2Field(ModelResultJsonMixin, AutoViewFieldMixin, HeavySelect2ChoiceField):
class AutoSelect2Field(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).
@ -221,6 +229,19 @@ class AutoSelect2Field(ModelResultJsonMixin, AutoViewFieldMixin, HeavySelect2Cho
kwargs['data_view'] = self.data_view
super(AutoSelect2Field, self).__init__(*args, **kwargs)
class AutoSelect2MultipleField(AutoViewFieldMixin, HeavySelect2MultipleChoiceField):
"""
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 = AutoHeavySelect2MultipleWidget
def __init__(self, *args, **kwargs):
self.data_view = "django_select2_central_json"
kwargs['data_view'] = self.data_view
super(AutoSelect2MultipleField, self).__init__(*args, **kwargs)
### Heavy field, specialized for Model, that uses central AutoView ###
class AutoModelSelect2Field(ModelResultJsonMixin, AutoViewFieldMixin, HeavyModelSelect2ChoiceField):
@ -237,3 +258,20 @@ class AutoModelSelect2Field(ModelResultJsonMixin, AutoViewFieldMixin, HeavyModel
self.data_view = "django_select2_central_json"
kwargs['data_view'] = self.data_view
super(AutoModelSelect2Field, self).__init__(*args, **kwargs)
class AutoModelSelect2MultipleField(ModelResultJsonMixin, AutoViewFieldMixin, HeavyModelSelect2MultipleChoiceField):
"""
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).
"""
__metaclass__ = UnhideableQuerysetType # Makes sure that user defined queryset class variable is replaced by
# queryset property (as it is needed by super classes).
widget = AutoHeavySelect2MultipleWidget
def __init__(self, *args, **kwargs):
self.data_view = "django_select2_central_json"
kwargs['data_view'] = self.data_view
super(AutoModelSelect2MultipleField, self).__init__(*args, **kwargs)

View file

@ -1,6 +1,6 @@
var django_select2 = {
MULTISEPARATOR: String.fromCharCode(0),
MULTISEPARATOR: String.fromCharCode(0), // We use this unprintable char as separator, since this can't be entered by user.
get_url_params: function (term, page, context) {
var field_id = $(this).data('field_id'),
res = {
@ -163,6 +163,10 @@ var django_select2 = {
e = $(e);
var id = e.attr('id'), data = null, val = e.select2('val');
if (!val && val !== 0) {
val = e.data('initVal');
}
if (val || val === 0) {
// Value is set so need to get the text.
data = django_select2.getValText(e);
@ -176,14 +180,16 @@ var django_select2 = {
callback(data); // Change for 2.3.x
},
onMultipleHiddenChange: function () {
var $e = $(this), valContainer = $e.data('valContainer'), name = $e.data('name');
var $e = $(this), valContainer = $e.data('valContainer'), name = $e.data('name'), vals = $e.val();
valContainer.empty();
$($e.val()).each(function () {
var inp = $('<input>').appendTo(valContainer);
inp.attr('type', 'hidden');
inp.attr('name', name);
inp.val(this);
});
if (vals) {
vals = vals.split(django_select2.MULTISEPARATOR);
$(vals).each(function () {
var inp = $('<input type="hidden">').appendTo(valContainer);
inp.attr('name', name);
inp.val(this);
});
}
},
initMultipleHidden: function ($e) {
var valContainer;
@ -191,18 +197,41 @@ var django_select2 = {
$e.data('name', $e.attr('name'));
$e.attr('name', '');
valContainer = $e.after('<div>').css({'display': 'none'});
valContainer = $('<div>').insertAfter($e).css({'display': 'none'});
$e.data('valContainer', valContainer);
$e.change(django_select2.onMultipleHiddenChange);
if ($e.val()) {
$e.change();
}
},
convertArrToStr: function (arr) {
return arr.join(django_select2.MULTISEPARATOR);
},
runInContextHelper: function (f, id) {
return function () {
var args = Array.prototype.slice.call(arguments);
return f.apply($('#' + id).get(0), args);
}
}
};
(function( $ ){
$.fn.txt = function() {
return this.attr('txt');
// This sets or gets the text lables for an element. It merely takes care returing array or single
// value, based on if element is multiple type.
$.fn.txt = function(val) {
if (typeof(val) !== 'undefined') {
if (val instanceof Array) {
val = django_select2.convertArrToStr(val);
}
this.attr('txt', val);
return this;
} else {
val = this.attr('txt');
if (val && this.attr('multiple')) {
val = val.split(django_select2.MULTISEPARATOR);
}
return val;
}
};
})( jQuery );

View file

@ -1,6 +1,4 @@
def convert_to_js_string_arr(lst):
lst = ['"%s"' % l for l in lst]
return u"[%s]" % (",".join(lst))
import types
def render_js_script(inner_code):
return u"""
@ -37,6 +35,53 @@ def extract_some_key_val(dct, keys):
edct[k] = v
return edct
def convert_py_to_js_data(val, id_):
if type(val) == types.BooleanType:
return u'true' if val else u'false'
elif type(val) in [types.IntType, types.LongType, types.FloatType]:
return unicode(val)
elif isinstance(val, JSFunctionInContext):
return u"django_select2.runInContextHelper(%s, '%s')" % (val, id_)
elif isinstance(val, JSVar):
return val # No quotes here
elif isinstance(val, dict):
return convert_dict_to_js_map(val, id_)
elif isinstance(val, list):
return convert_to_js_arr(val, id_)
else:
return u"'%s'" % unicode(val)
def convert_dict_to_js_map(dct, id_):
out = u'{'
is_first = True
for name in dct:
if not is_first:
out += u", "
else:
is_first = False
out += u"'%s': " % name
out += convert_py_to_js_data(dct[name], id_)
return out + u'}'
def convert_to_js_arr(lst, id_):
out = u'['
is_first = True
for val in lst:
if not is_first:
out += u", "
else:
is_first = False
out += convert_py_to_js_data(val, id_)
return out + u']'
def convert_to_js_string_arr(lst):
lst = ['"%s"' % l for l in lst]
return u"[%s]" % (",".join(lst))
### Auto view helper utils ###
import re

View file

@ -1,11 +1,12 @@
import types
from itertools import chain
from django import forms
from django.utils.safestring import mark_safe
from django.core.urlresolvers import reverse
from django.utils.datastructures import MultiValueDict, MergeDict
from .util import render_js_script, convert_to_js_string_arr, JSVar, JSFunction, JSFunctionInContext
from .util import render_js_script, convert_to_js_string_arr, JSVar, JSFunction, JSFunctionInContext, \
convert_dict_to_js_map, convert_to_js_arr
### Light mixin and widgets ###
@ -44,33 +45,7 @@ class Select2Mixin(object):
return options
def render_select2_options_code(self, options, id_):
out = '{'
is_first = True
for name in options:
if not is_first:
out += u", "
else:
is_first = False
out += u"'%s': " % name
val = options[name]
if type(val) == types.BooleanType:
out += u'true' if val else u'false'
elif type(val) in [types.IntType, types.LongType, types.FloatType]:
out += unicode(val)
elif isinstance(val, JSFunctionInContext):
out += u"""function () {
var args = Array.prototype.slice.call(arguments);
return %s.apply($('#%s').get(0), args);
}""" % (val, id_)
elif isinstance(val, JSVar):
out += val # No quotes here
elif isinstance(val, dict):
out += self.render_select2_options_code(val, id_)
else:
out += u"'%s'" % val
return out + u'}'
return convert_dict_to_js_map(options, id_)
def render_js_code(self, id_, *args):
if id_:
@ -129,12 +104,22 @@ class MultipleSelect2HiddenInput(forms.TextInput):
input_type = 'hidden' # We want it hidden but should be treated as if is_hidden is False
def render(self, name, value, attrs=None, choices=()):
attrs = self.build_attrs(attrs, multiple='multiple')
s = unicode(super(MultipleSelect2HiddenInput, self).render(name, value, attrs, choices))
s = unicode(super(MultipleSelect2HiddenInput, self).render(name, u"", attrs))
id_ = attrs.get('id', None)
if id_:
s += render_js_script(u"django_select2.initMultipleHidden($('#%s'));" % id_)
jscode = u''
if value:
jscode = u"$('#%s').val(django_select2.convertArrToStr(%s));" \
% (id_, convert_to_js_arr(value, id_))
jscode += u"django_select2.initMultipleHidden($('#%s'));" % id_
s += render_js_script(jscode)
return s
def value_from_datadict(self, data, files, name):
if isinstance(data, (MultiValueDict, MergeDict)):
return data.getlist(name)
return data.get(name, None)
### Heavy mixins and widgets ###
class HeavySelect2Mixin(Select2Mixin):
@ -146,7 +131,6 @@ class HeavySelect2Mixin(Select2Mixin):
self.choices = kwargs.pop('choices', [])
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,
@ -209,15 +193,18 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput):
if value: # Just like forms.SelectMultiple.render() it assumes that value will be multi-valued (list).
texts = self.render_texts(value, choices)
if texts:
return render_js_script(u"$('#%s').attr('txt', %s);" % (id_, texts))
return u"$('#%s').txt(%s);" % (id_, texts)
### Auto Heavy widgets ###
class AutoHeavySelect2Mixin(HeavySelect2Mixin):
def render_inner_js_code(self, id_, *args):
js = super(AutoHeavySelect2Mixin, self).render_inner_js_code(id_, *args)
js += u"$('#%s').data('field_id', '%s');" % (id_, self.field_id)
js = u"$('#%s').data('field_id', '%s');" % (id_, self.field_id)
js += super(AutoHeavySelect2Mixin, self).render_inner_js_code(id_, *args)
return js
class AutoHeavySelect2Widget(AutoHeavySelect2Mixin, HeavySelect2Widget):
pass
class AutoHeavySelect2MultipleWidget(AutoHeavySelect2Mixin, HeavySelect2MultipleWidget):
pass

Binary file not shown.

View file

@ -34,12 +34,34 @@
"number": "500"
}
},
{
"pk": 1,
"model": "testmain.lab",
"fields": {
"name": "Crimsion"
}
},
{
"pk": 2,
"model": "testmain.lab",
"fields": {
"name": "Saffron"
}
},
{
"pk": 3,
"model": "testmain.lab",
"fields": {
"name": "Lavender"
}
},
{
"pk": 1,
"model": "testmain.dept",
"fields": {
"name": "Chemistry",
"allotted_rooms": [1, 2, 3]
"allotted_rooms": [1, 2, 3],
"allotted_labs": [1]
}
},
{
@ -47,7 +69,8 @@
"model": "testmain.dept",
"fields": {
"name": "Biology",
"allotted_rooms": [3, 4]
"allotted_rooms": [3, 4],
"allotted_labs": [2]
}
},
{
@ -55,7 +78,8 @@
"model": "testmain.dept",
"fields": {
"name": "Physics",
"allotted_rooms": [1, 2, 5]
"allotted_rooms": [1, 2, 5],
"allotted_labs": [1, 3]
}
},
{

View file

@ -2,21 +2,27 @@ from django import forms
from django_select2 import *
from .models import Employee, Dept, ClassRoom
from .models import Employee, Dept, ClassRoom, Lab
class EmployeeChoices(AutoModelSelect2Field):
queryset = Employee.objects
search_fields = ['name__icontains', ]
class ClassRoomChoices(AutoModelSelect2MultipleField):
queryset = ClassRoom.objects
search_fields = ['number__icontains', ]
class EmployeeForm(forms.ModelForm):
manager = EmployeeChoices()
manager = EmployeeChoices(required=False)
dept = ModelSelect2Field(queryset=Dept.objects)
class Meta:
model = Employee
class DeptForm(forms.ModelForm):
allotted_rooms = ModelMultipleSelect2Field(queryset=ClassRoom.objects)
allotted_rooms = ClassRoomChoices()
allotted_labs = ModelSelect2MultipleField(queryset=Lab.objects, required=False)
class Meta:
model = Dept

View file

@ -6,9 +6,16 @@ class ClassRoom(models.Model):
def __unicode__(self):
return unicode(self.number)
class Lab(models.Model):
name = models.CharField(max_length=10)
def __unicode__(self):
return unicode(self.name)
class Dept(models.Model):
name = models.CharField(max_length=10)
allotted_rooms = models.ManyToManyField(ClassRoom)
allotted_labs = models.ManyToManyField(Lab)
def __unicode__(self):
return unicode(self.name)