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=$("
").insertAfter(a).css({display:"none"});a.data("valContainer",b);a.change(django_select2.onMultipleHiddenChange);if(a.val()){a.change()}},convertArrToStr:function(a){return a.join(django_select2.MULTISEPARATOR)},runInContextHelper:function(a,b){return function(){var c=Array.prototype.slice.call(arguments,0);return a.apply($("#"+b).get(0),c)}},logErr:function(){if(console&&console.error){args=Array.prototype.slice.call(arguments);console.error.apply(console,args)}}};(function(b){if(b){for(var a in django_select2){var c=django_select2[a];if(typeof(c)=="function"){django_select2[a]=(function(d,e){return function(){console.log("Function "+d+" called for object: ",this);return e.apply(this,arguments)}}(a,c))}}}}(false));(function(a){a.fn.txt=function(b){if(typeof(b)!=="undefined"){if(b){if(b instanceof Array){if(this.attr("multiple")){b=django_select2.convertArrToStr(b)}else{b=b[0]}}this.attr("txt",b)}else{this.removeAttr("txt")}return this}else{b=this.attr("txt");if(this.attr("multiple")){if(b){b=b.split(django_select2.MULTISEPARATOR)}else{b=[]}}return b}}})(jQuery)}; \ No newline at end of file +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)},createSearchChoice:function(a,b){if(!b||$(b).filter(function(){return this.text.localeCompare(a)===0}).length===0){return{id:a,text:a}}},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=$("
").insertAfter(a).css({display:"none"});a.data("valContainer",b);a.change(django_select2.onMultipleHiddenChange);if(a.val()){a.change()}},convertArrToStr:function(a){return a.join(django_select2.MULTISEPARATOR)},runInContextHelper:function(a,b){return function(){var c=Array.prototype.slice.call(arguments,0);return a.apply($("#"+b).get(0),c)}},logErr:function(){if(console&&console.error){args=Array.prototype.slice.call(arguments);console.error.apply(console,args)}}};(function(b){if(b){for(var a in django_select2){var c=django_select2[a];if(typeof(c)=="function"){django_select2[a]=(function(d,e){return function(){console.log("Function "+d+" called for object: ",this);return e.apply(this,arguments)}}(a,c))}}}}(false));(function(a){a.fn.txt=function(b){if(typeof(b)!=="undefined"){if(b){if(b instanceof Array){if(this.attr("multiple")){b=django_select2.convertArrToStr(b)}else{b=b[0]}}this.attr("txt",b)}else{this.removeAttr("txt")}return this}else{b=this.attr("txt");if(this.attr("multiple")){if(b){b=b.split(django_select2.MULTISEPARATOR)}else{b=[]}}return b}}})(jQuery)}; \ No newline at end of file diff --git a/django_select2/widgets.py b/django_select2/widgets.py index a12f057..220d99b 100644 --- a/django_select2/widgets.py +++ b/django_select2/widgets.py @@ -504,7 +504,7 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput): Following Select2 options from :py:attr:`.Select2Mixin.options` are added or set:- - * multiple: ``False`` + * multiple: ``True`` * separator: ``JSVar('django_select2.MULTISEPARATOR')`` """ @@ -539,6 +539,34 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, MultipleSelect2HiddenInput): if texts: return u"$('#%s').txt(%s);" % (id_, texts) +class HeavySelect2TagWidget(HeavySelect2MultipleWidget): + """ + Heavy widget with tagging support. Based on :py:class:`HeavySelect2MultipleWidget`, + unlike other widgets this allows users to create new options (tags). + + Following Select2 options from :py:attr:`.Select2Mixin.options` are removed:- + + * allowClear + * minimumResultsForSearch + * closeOnSelect + + Following Select2 options from :py:attr:`.Select2Mixin.options` are added or set:- + + * multiple: ``True`` + * separator: ``JSVar('django_select2.MULTISEPARATOR')`` + * tags: ``True`` + * tokenSeparators: ``,`` and `` `` + * createSearchChoice: ``JSFunctionInContext('django_select2.createSearchChoice')`` + * minimumInputLength: ``1`` + + """ + def init_options(self): + super(HeavySelect2TagWidget, self).init_options() + self.options.pop('closeOnSelect', None) + self.options['minimumInputLength'] = 1 + self.options['tags'] = True + self.options['tokenSeparators'] = [",", " "] + self.options['createSearchChoice'] = JSFunctionInContext('django_select2.createSearchChoice') ### Auto Heavy widgets ### @@ -571,3 +599,7 @@ class AutoHeavySelect2Widget(AutoHeavySelect2Mixin, HeavySelect2Widget): class AutoHeavySelect2MultipleWidget(AutoHeavySelect2Mixin, HeavySelect2MultipleWidget): "Auto version of :py:class:`.HeavySelect2MultipleWidget`" pass + +class AutoHeavySelect2TagWidget(AutoHeavySelect2Mixin, HeavySelect2TagWidget): + "Auto version of :py:class:`.HeavySelect2TagWidget`" + pass diff --git a/testapp/test.db b/testapp/test.db index 4330de346d7931a2f7755ce64e7894830511bd7b..0c22100301b650a8cb65a41c2d800316869728f6 100644 GIT binary patch delta 2867 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 diff --git a/testapp/testapp/templates/index.html b/testapp/testapp/templates/index.html index 4eb95b4..2e199ac 100644 --- a/testapp/testapp/templates/index.html +++ b/testapp/testapp/templates/index.html @@ -10,5 +10,6 @@
  • Test multi selection model fields
  • Test mixed form. All fields' search must return their own results, not other fields'.
  • Test that initial values are honored in unbound form
  • +
  • Test tagging support
  • diff --git a/testapp/testapp/templates/list.html b/testapp/testapp/templates/list.html index 29f2dd1..f140bb2 100644 --- a/testapp/testapp/templates/list.html +++ b/testapp/testapp/templates/list.html @@ -5,6 +5,10 @@

    {{title}}

    +{% if create_new_href != '' %} + Create New +
    +{% endif %}
      {% for e in object_list %}
    • {{ e }}
    • diff --git a/testapp/testapp/testmain/forms.py b/testapp/testapp/testmain/forms.py index bec0fe2..8fd8d02 100644 --- a/testapp/testapp/testmain/forms.py +++ b/testapp/testapp/testmain/forms.py @@ -2,7 +2,7 @@ from django import forms from django_select2 import * -from .models import Employee, Dept, ClassRoom, Lab, Word, School +from .models import Employee, Dept, ClassRoom, Lab, Word, School, Tag, Question from django.core.exceptions import ValidationError @@ -27,6 +27,12 @@ class WordChoices(AutoModelSelect2Field): queryset = Word.objects search_fields = ['word__icontains', ] +class TagField(AutoModelSelect2TagField): + queryset = Tag.objects + search_fields = ['tag__icontains', ] + def get_model_field_values(self, value): + return {'tag': value} + class SelfChoices(AutoSelect2Field): def get_val_txt(self, value): if not hasattr(self, 'res_map'): @@ -147,3 +153,11 @@ class InitialValueForm(forms.Form): heavySelect2ChoiceWithQuotes = AutoSelect2Field(initial=2, choices=((1, "'Single-Quote'"), (2, "\"Double-Quotes\""), (3, "\"Mixed-Quotes'"), )) +class QuestionForm(forms.ModelForm): + question = forms.CharField() + description = forms.CharField(widget=forms.Textarea) + tags = TagField() + + class Meta: + model = Question + diff --git a/testapp/testapp/testmain/models.py b/testapp/testapp/testmain/models.py index ca84e80..227ec88 100644 --- a/testapp/testapp/testmain/models.py +++ b/testapp/testapp/testmain/models.py @@ -37,5 +37,18 @@ class Word(models.Model): class School(models.Model): - classes = models.ManyToManyField(ClassRoom) + +class Tag(models.Model): + tag = models.CharField(max_length=10, unique=True) + + def __unicode__(self): + return unicode(self.tag) + +class Question(models.Model): + question = models.CharField(max_length=200) + description = models.CharField(max_length=800) + tags = models.ManyToManyField(Tag) + + def __unicode__(self): + return unicode(self.question) diff --git a/testapp/testapp/testmain/urls.py b/testapp/testapp/testmain/urls.py index a8956d2..47288ac 100644 --- a/testapp/testapp/testmain/urls.py +++ b/testapp/testapp/testmain/urls.py @@ -10,4 +10,8 @@ urlpatterns = patterns('testapp.testmain.views', url(r'mixed/form/$', 'test_mixed_form', name='test_mixed_form'), url(r'initial/form/$', 'test_init_values', name='test_init_values'), + + url(r'question/$', 'test_list_questions', name='test_list_questions'), + url(r'question/form/([0-9]+)/$', 'test_tagging', name='test_tagging'), + url(r'question/form/$', 'test_tagging_new', name='test_tagging_new'), ) diff --git a/testapp/testapp/testmain/views.py b/testapp/testapp/testmain/views.py index eb7bbbd..15d8589 100644 --- a/testapp/testapp/testmain/views.py +++ b/testapp/testapp/testmain/views.py @@ -2,15 +2,16 @@ from django.core.urlresolvers import reverse from django.http import HttpResponseRedirect from django.shortcuts import render, get_object_or_404 -from .forms import EmployeeForm, DeptForm, MixedForm, InitialValueForm -from .models import Employee, Dept +from .forms import EmployeeForm, DeptForm, MixedForm, InitialValueForm, QuestionForm +from .models import Employee, Dept, Question def test_single_value_model_field(request): return render(request, 'list.html', { 'title': 'Employees', 'href': 'test_single_value_model_field1', - 'object_list': Employee.objects.all() - }) + 'object_list': Employee.objects.all(), + 'create_new_href': '' + }) def test_single_value_model_field1(request, id): emp = get_object_or_404(Employee, pk=id) @@ -28,7 +29,8 @@ def test_multi_values_model_field(request): return render(request, 'list.html', { 'title': 'Departments', 'href': 'test_multi_values_model_field1', - 'object_list': Dept.objects.all() + 'object_list': Dept.objects.all(), + 'create_new_href': '' }) def test_multi_values_model_field1(request, id): @@ -53,4 +55,28 @@ def test_mixed_form(request): def test_init_values(request): return render(request, 'form.html', {'form': InitialValueForm()}) +def test_list_questions(request): + return render(request, 'list.html', { + 'title': 'Questions', + 'href': 'test_tagging', + 'object_list': Question.objects.all(), + 'create_new_href': 'test_tagging_new' + }) + +def test_tagging_new(request): + return test_tagging(request, None) + +def test_tagging(request, id): + if id is None: + question = Question() + else: + question = get_object_or_404(Question, pk=id) + if request.POST: + form = QuestionForm(data=request.POST, instance=question) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('home')) + else: + form = QuestionForm(instance=question) + return render(request, 'form.html', {'form': form})