From ab57470b02197c4f3e12a970afbf47b2d071decd Mon Sep 17 00:00:00 2001
From: "AppleGrew (applegrew)"
Date: Mon, 16 Sep 2013 02:03:08 +0530
Subject: [PATCH] Adding Tagging support.
---
README | 3 +-
README.md | 3 +-
django_select2/__init__.py | 18 ++-
django_select2/fields.py | 161 ++++++++++++++++++++-
django_select2/static/js/heavy_data.js | 10 ++
django_select2/static/js/heavy_data.min.js | 2 +-
django_select2/widgets.py | 34 ++++-
testapp/test.db | Bin 2412544 -> 2441216 bytes
testapp/testapp/templates/index.html | 1 +
testapp/testapp/templates/list.html | 4 +
testapp/testapp/testmain/forms.py | 16 +-
testapp/testapp/testmain/models.py | 15 +-
testapp/testapp/testmain/urls.py | 4 +
testapp/testapp/testmain/views.py | 36 ++++-
14 files changed, 289 insertions(+), 18 deletions(-)
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=$("