diff --git a/README b/README index f2b4e0b..0a461d6 100644 --- a/README +++ b/README @@ -49,9 +49,10 @@ Special Thanks Changelog Summary ================= -### v4.1.1 +### v4.2.0 * Updated Select2 to version 3.4.2. **Please note**, that if you need any of the Select2 locale files, then you need to download them yourself from http://ivaynberg.github.com/select2/ and add to your project. +* Tagging support added. See [Field API reference](http://django-select2.readthedocs.org/en/latest/ref_fields.html) in documentation. ### v4.1.0 diff --git a/README.md b/README.md index f2b4e0b..0a461d6 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,10 @@ Special Thanks Changelog Summary ================= -### v4.1.1 +### v4.2.0 * Updated Select2 to version 3.4.2. **Please note**, that if you need any of the Select2 locale files, then you need to download them yourself from http://ivaynberg.github.com/select2/ and add to your project. +* Tagging support added. See [Field API reference](http://django-select2.readthedocs.org/en/latest/ref_fields.html) in documentation. ### v4.1.0 diff --git a/django_select2/__init__.py b/django_select2/__init__.py index cb42bd5..b3050a4 100644 --- a/django_select2/__init__.py +++ b/django_select2/__init__.py @@ -38,7 +38,8 @@ in their names. **Available widgets:** :py:class:`.Select2Widget`, :py:class:`.Select2MultipleWidget`, :py:class:`.HeavySelect2Widget`, :py:class:`.HeavySelect2MultipleWidget`, -:py:class:`.AutoHeavySelect2Widget`, :py:class:`.AutoHeavySelect2MultipleWidget` +:py:class:`.AutoHeavySelect2Widget`, :py:class:`.AutoHeavySelect2MultipleWidget`, :py:class:`.HeavySelect2TagWidget`, +:py:class:`.AutoHeavySelect2TagWidget` `Read more`_ @@ -57,7 +58,8 @@ your ease. :py:class:`.HeavySelect2MultipleChoiceField`, :py:class:`.HeavyModelSelect2ChoiceField`, :py:class:`.HeavyModelSelect2MultipleChoiceField`, :py:class:`.ModelSelect2Field`, :py:class:`.ModelSelect2MultipleField`, :py:class:`.AutoSelect2Field`, :py:class:`.AutoSelect2MultipleField`, :py:class:`.AutoModelSelect2Field`, -:py:class:`.AutoModelSelect2MultipleField` +:py:class:`.AutoModelSelect2MultipleField`, :py:class:`.HeavySelect2TagField`, :py:class:`.AutoSelect2TagField`, +:py:class:`.HeavyModelSelect2TagField`, :py:class:`.AutoModelSelect2TagField` Views ----- @@ -77,7 +79,7 @@ The view - `Select2View`, exposed here is meant to be used with 'Heavy' fields a import logging logger = logging.getLogger(__name__) -__version__ = "4.1.1" +__version__ = "4.2.0" __RENDER_SELECT2_STATICS = False __ENABLE_MULTI_PROCESS_SUPPORT = False @@ -89,6 +91,8 @@ __SECRET_SALT = '' try: from django.conf import settings + if logger.isEnabledFor(logging.INFO): + logger.info("Django found.") if settings.configured: __RENDER_SELECT2_STATICS = getattr(settings, 'AUTO_RENDER_SELECT2_STATICS', True) __ENABLE_MULTI_PROCESS_SUPPORT = getattr(settings, 'ENABLE_SELECT2_MULTI_PROCESS_SUPPORT', False) @@ -103,12 +107,16 @@ try: __ENABLE_MULTI_PROCESS_SUPPORT = False from .widgets import Select2Widget, Select2MultipleWidget, HeavySelect2Widget, HeavySelect2MultipleWidget, \ - AutoHeavySelect2Widget, AutoHeavySelect2MultipleWidget + AutoHeavySelect2Widget, AutoHeavySelect2MultipleWidget, HeavySelect2TagWidget, AutoHeavySelect2TagWidget from .fields import Select2ChoiceField, Select2MultipleChoiceField, HeavySelect2ChoiceField, \ HeavySelect2MultipleChoiceField, HeavyModelSelect2ChoiceField, HeavyModelSelect2MultipleChoiceField, \ ModelSelect2Field, ModelSelect2MultipleField, AutoSelect2Field, AutoSelect2MultipleField, \ - AutoModelSelect2Field, AutoModelSelect2MultipleField + AutoModelSelect2Field, AutoModelSelect2MultipleField, HeavySelect2TagField, AutoSelect2TagField, \ + HeavyModelSelect2TagField, AutoModelSelect2TagField from .views import Select2View, NO_ERR_RESP + + if logger.isEnabledFor(logging.INFO): + logger.info("Django found and fields and widgest loaded.") except ImportError: if logger.isEnabledFor(logging.INFO): logger.info("Django not found.") diff --git a/django_select2/fields.py b/django_select2/fields.py index 70e88e3..ceb2a3c 100644 --- a/django_select2/fields.py +++ b/django_select2/fields.py @@ -82,11 +82,12 @@ from django.core.exceptions import ValidationError from django.forms.models import ModelChoiceIterator from django.db.models import Q from django.utils.translation import ugettext_lazy as _ -from django.utils.encoding import smart_unicode +from django.utils.encoding import smart_unicode, force_unicode from .widgets import Select2Widget, Select2MultipleWidget,\ HeavySelect2Widget, HeavySelect2MultipleWidget, AutoHeavySelect2Widget, \ - AutoHeavySelect2MultipleWidget, AutoHeavySelect2Mixin + AutoHeavySelect2MultipleWidget, AutoHeavySelect2Mixin, AutoHeavySelect2TagWidget, \ + HeavySelect2TagWidget from .views import NO_ERR_RESP from .util import extract_some_key_val @@ -585,6 +586,38 @@ class HeavySelect2MultipleChoiceField(HeavySelect2FieldBaseMixin, HeavyMultipleC "Heavy Select2 Multiple Choice field." widget = HeavySelect2MultipleWidget +class HeavySelect2TagField(HeavySelect2MultipleChoiceField): + """ + Heavy Select2 field for tagging. + + .. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`create_new_value` is not implemented. + """ + widget = HeavySelect2TagWidget + + def validate(self, value): + if self.required and not value: + raise ValidationError(self.error_messages['required']) + # Check if each value in the value list is in self.choices or + # the big data (i.e. validate_value() returns True). + # If not then calls create_new_value() to create the new value. + for i in range(0, len(value)): + val = value[i] + if not self.valid_value(val): + value[i] = self.create_new_value(val) + + def create_new_value(self, value): + """ + This is called when the input value is not valid. This + allows you to add the value into the data-store. If that + is not done then eventually the validation will fail. + + :param value: Invalid value entered by the user. + :type value: As coerced by :py:meth:`HeavyChoiceField.coerce_value`. + + :return: The a new value, which could be the id (pk) of the created value. + :rtype: Any + """ + raise NotImplementedError ### Heavy field specialized for Models ### @@ -605,6 +638,92 @@ class HeavyModelSelect2MultipleChoiceField(HeavySelect2FieldBaseMixin, ModelMult kwargs.pop('choices', None) super(HeavyModelSelect2MultipleChoiceField, self).__init__(*args, **kwargs) +class HeavyModelSelect2TagField(HeavySelect2FieldBaseMixin, ModelMultipleChoiceField): + """ + Heavy Select2 field for tagging, specialized for Models. + + .. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`get_model_field_values` is not implemented. + """ + widget = HeavySelect2TagWidget + + def __init__(self, *args, **kwargs): + kwargs.pop('choices', None) + super(HeavyModelSelect2TagField, self).__init__(*args, **kwargs) + + def to_python(self, value): + if value in EMPTY_VALUES: + return None + try: + key = self.to_field_name or 'pk' + value = self.queryset.get(**{key: value}) + except ValueError, e: + raise ValidationError(self.error_messages['invalid_choice']) + except self.queryset.model.DoesNotExist: + value = self.create_new_value(value) + return value + + def clean(self, value): + if self.required and not value: + raise ValidationError(self.error_messages['required']) + elif not self.required and not value: + return [] + if not isinstance(value, (list, tuple)): + raise ValidationError(self.error_messages['list']) + new_values = [] + key = self.to_field_name or 'pk' + for pk in list(value): + try: + self.queryset.filter(**{key: pk}) + except ValueError: + value.remove(pk) + new_values.append(pk) + + for val in new_values: + value.append(self.create_new_value(force_unicode(val))) + + # Usually new_values will have list of new tags, but if the tag is + # suppose of type int then that could be interpreted as valid pk + # value and ValueError above won't be triggered. + # Below we find such tags and create them, by check if the pk + # actually exists. + qs = self.queryset.filter(**{'%s__in' % key: value}) + pks = set([force_unicode(getattr(o, key)) for o in qs]) + for i in range(0, len(value)): + val = force_unicode(value[i]) + if val not in pks: + value[i] = self.create_new_value(val) + # Since this overrides the inherited ModelChoiceField.clean + # we run custom validators here + self.run_validators(value) + return qs + + def create_new_value(self, value): + """ + This is called when the input value is not valid. This + allows you to add the value into the data-store. If that + is not done then eventually the validation will fail. + + :param value: Invalid value entered by the user. + :type value: As coerced by :py:meth:`HeavyChoiceField.coerce_value`. + + :return: The a new value, which could be the id (pk) of the created value. + :rtype: Any + """ + obj = self.queryset.create(**self.get_model_field_values(value)) + return getattr(obj, self.to_field_name or 'pk') + + def get_model_field_values(self, value): + """ + This is called when the input value is not valid and the field + tries to create a new model instance. + + :param value: Invalid value entered by the user. + :type value: unicode + + :return: Dict with attribute name - attribute value pair. + :rtype: dict + """ + raise NotImplementedError ### Heavy general field that uses central AutoView ### @@ -633,6 +752,17 @@ class AutoSelect2MultipleField(AutoViewFieldMixin, HeavySelect2MultipleChoiceFie widget = AutoHeavySelect2MultipleWidget +class AutoSelect2TagField(AutoViewFieldMixin, HeavySelect2TagField): + """ + Auto Heavy Select2 field for tagging. + + 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). + + .. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`get_results` is not implemented. + """ + + widget = AutoHeavySelect2TagWidget ### Heavy field, specialized for Model, that uses central AutoView ### @@ -663,3 +793,30 @@ class AutoModelSelect2MultipleField(ModelResultJsonMixin, AutoViewFieldMixin, He widget = AutoHeavySelect2MultipleWidget +class AutoModelSelect2TagField(ModelResultJsonMixin, AutoViewFieldMixin, HeavyModelSelect2TagField): + """ + Auto Heavy Select2 field for tagging, specialized for Models. + + 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). + + .. warning:: :py:exc:`NotImplementedError` would be thrown if :py:meth:`get_model_field_values` is not implemented. + + Example:: + class Tag(models.Model): + tag = models.CharField(max_length=10, unique=True) + def __unicode__(self): + return unicode(self.tag) + + class TagField(AutoModelSelect2TagField): + queryset = Tag.objects + search_fields = ['tag__icontains', ] + def get_model_field_values(self, value): + return {'tag': value} + + """ + # Makes sure that user defined queryset class variable is replaced by + # queryset property (as it is needed by super classes). + __metaclass__ = UnhideableQuerysetType + + widget = AutoHeavySelect2TagWidget diff --git a/django_select2/static/js/heavy_data.js b/django_select2/static/js/heavy_data.js index 40cd2af..0d95d27 100644 --- a/django_select2/static/js/heavy_data.js +++ b/django_select2/static/js/heavy_data.js @@ -150,6 +150,16 @@ if (!window['django_select2']) { callback(data); // Change for 2.3.x django_select2.updateText(e); }, + createSearchChoice: function(term, data) { + if (!data || $(data).filter(function () { + return this.text.localeCompare(term) === 0; + }).length === 0) { + return { + id: term, + text: term + }; + } + }, onMultipleHiddenChange: function () { var $e = $(this), valContainer = $e.data('valContainer'), name = $e.data('name'), vals = $e.val(); valContainer.empty(); diff --git a/django_select2/static/js/heavy_data.min.js b/django_select2/static/js/heavy_data.min.js index 48af976..11f0850 100644 --- a/django_select2/static/js/heavy_data.min.js +++ b/django_select2/static/js/heavy_data.min.js @@ -1 +1 @@ -if(!window.django_select2){var django_select2={MULTISEPARATOR:String.fromCharCode(0),get_url_params:function(c,e,b){var d=$(this).data("field_id"),a={term:c,page:e,context:b};if(d){a.field_id=d}return a},process_results:function(d,c,b){var a;if(d.err&&d.err.toLowerCase()==="nil"){a={results:d.results};if(b){a.context=b}if(d.more===true||d.more===false){a.more=d.more}}else{a={results:[]}}if(a.results){$(this).data("results",a.results)}else{$(this).removeData("results")}return a},onValChange:function(){django_select2.updateText($(this))},prepareValText:function(d,a,c){var b=[];$(d).each(function(e){b.push({id:this,text:a[e]})});if(c){return b}else{if(b.length>0){return b[0]}else{return null}}},updateText:function(b){var f=b.select2("val"),d=b.select2("data"),a=b.txt(),c=!!b.attr("multiple"),e;if(f||f===0){if(c){if(f.length!==a.length){a=[];$(f).each(function(g){var h,j=this,k;for(h in d){k=d[h].id;if(k instanceof String){k=k.valueOf()}if(k==j){a.push(d[h].text)}}})}}else{a=d.text}b.txt(a)}else{b.txt("")}},getValText:function(b){var g=b.select2("val"),c=b.data("results"),a=b.txt(),e=!!b.attr("multiple"),d,h=b.attr("id");if(g||g===0){if(!e){g=[g];if(a||a===0){a=[a]}}if(a===0||(a&&g.length===a.length)){return[g,a]}d=b.data("userGetValText");if(d){a=d(b,g,e);if(a||a===0){return[g,a]}}if(c){a=[];$(g).each(function(f){var j,k=this;for(j in c){if(c[j].id==k){g[f]=c[j].id;a.push(c[j].text)}}});if(a||a===0){return[g,a]}}}return null},onInit:function(b,f){b=$(b);var d=b.attr("id"),a=null,c=b.select2("val");if(!c&&c!==0){c=b.data("initVal")}if(c||c===0){a=django_select2.getValText(b);if(a&&a[0]){a=django_select2.prepareValText(a[0],a[1],!!b.attr("multiple"))}}if(!a){b.val(null)}f(a);django_select2.updateText(b)},onMultipleHiddenChange:function(){var b=$(this),d=b.data("valContainer"),a=b.data("name"),c=b.val();d.empty();if(c){c=c.split(django_select2.MULTISEPARATOR);$(c).each(function(){var e=$('').appendTo(d);e.attr("name",a);e.val(this)})}},initMultipleHidden:function(a){var b;a.data("name",a.attr("name"));a.attr("name","");b=$("