diff --git a/django_select2/__init__.py b/django_select2/__init__.py index 5a60172..e0660fe 100644 --- a/django_select2/__init__.py +++ b/django_select2/__init__.py @@ -116,7 +116,7 @@ try: from .views import Select2View, NO_ERR_RESP if logger.isEnabledFor(logging.DEBUG): - logger.debug("Django found and fields and widgest loaded.") + logger.debug("Django found and fields and widgets 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 30e6dff..955cd5d 100644 --- a/django_select2/fields.py +++ b/django_select2/fields.py @@ -318,10 +318,30 @@ class ChoiceMixin(object): result._choices = copy.deepcopy(self._choices, memo) return result +class FilterableModelChoiceIterator(ModelChoiceIterator): + """ + Extends ModelChoiceIterator to add the capability to apply additional + filter on the passed queryset. + """ + + def set_extra_filter(self, **filter_map): + """ + Applies additional filter on the queryset. This can be called multiple times. + + :param kwargs: The ``**kwargs`` to pass to :py:meth:`django.db.models.query.QuerySet.filter`. + If this is not set then additional filter (if) applied before is removed. + """ + if not hasattr(self, '_original_queryset'): + import copy + self._original_queryset = copy.deepcopy(self.queryset) + if filter_map: + self.queryset = self._original_queryset.filter(**filter_map) + else: + self.queryset = self._original_queryset class QuerysetChoiceMixin(ChoiceMixin): """ - Overrides ``choices``' getter to return instance of :py:class:`.ModelChoiceIterator` + Overrides ``choices``' getter to return instance of :py:class:`.FilterableModelChoiceIterator` instead. """ @@ -338,12 +358,17 @@ class QuerysetChoiceMixin(ChoiceMixin): # accessed) so that we can ensure the QuerySet has not been consumed. This # construct might look complicated but it allows for lazy evaluation of # the queryset. - return ModelChoiceIterator(self) + return FilterableModelChoiceIterator(self) choices = property(_get_choices, ChoiceMixin._set_choices) + def __deepcopy__(self, memo): + result = super(QuerysetChoiceMixin, self).__deepcopy__(memo) + # Need to force a new ModelChoiceIterator to be created, bug #11183 + result.queryset = result.queryset + return result -class ModelChoiceFieldMixin(object): +class ModelChoiceFieldMixin(QuerysetChoiceMixin): def __init__(self, *args, **kwargs): queryset = kwargs.pop('queryset', None) @@ -370,6 +395,8 @@ class ModelChoiceFieldMixin(object): if hasattr(self, '_queryset'): return self._queryset + def get_pk_field_name(self): + return self.to_field_name or 'pk' ### Slightly altered versions of the Django counterparts with the same name in forms module. ### @@ -453,6 +480,9 @@ class HeavySelect2FieldBaseMixin(object): if hasattr(self, 'field_id'): self.widget.field_id = self.field_id + # Widget should have been instantiated by now. + self.widget.field = self + if logger.isEnabledFor(logging.DEBUG): t2 = util.timer_start('HeavySelect2FieldBaseMixin.__init__:choices initialization') diff --git a/django_select2/widgets.py b/django_select2/widgets.py index 220d99b..50c0994 100644 --- a/django_select2/widgets.py +++ b/django_select2/widgets.py @@ -4,6 +4,7 @@ Contains all the Django widgets for Select2. import logging from itertools import chain +import util from django import forms from django.utils.encoding import force_unicode @@ -202,6 +203,9 @@ class Select2Mixin(object): :return: The rendered markup. :rtype: :py:obj:`unicode` """ + if logger.isEnabledFor(logging.DEBUG): + t1 = util.timer_start('Select2Mixin.render') + args = [name, value, attrs] if choices: args.append(choices) @@ -214,6 +218,7 @@ class Select2Mixin(object): s += self.render_js_code(id_, name, value, attrs, choices) if logger.isEnabledFor(logging.DEBUG): + util.timer_end(t1) logger.debug("Generated widget code:-\n%s", s) return mark_safe(s) @@ -411,9 +416,16 @@ class HeavySelect2Mixin(Select2Mixin): txts = [] all_choices = choices if choices else [] choices_dict = dict() - for val, txt in chain(self.choices, all_choices): + self_choices = self.choices + + import fields + if isinstance(self_choices, fields.FilterableModelChoiceIterator): + self_choices.set_extra_filter(**{'%s__in' % self.field.get_pk_field_name(): selected_choices}) + + for val, txt in chain(self_choices, all_choices): val = force_unicode(val) choices_dict[val] = txt + for val in selected_choices: try: txts.append(choices_dict[val]) diff --git a/testapp/test.db b/testapp/test.db index ca95695..2f81331 100644 Binary files a/testapp/test.db and b/testapp/test.db differ diff --git a/testapp/testapp/templates/index.html b/testapp/testapp/templates/index.html index 2e199ac..79f6c63 100644 --- a/testapp/testapp/templates/index.html +++ b/testapp/testapp/templates/index.html @@ -11,5 +11,7 @@
  • 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
  • +
  • Test multi value auto model field.
  • +
  • Test performance. Issue#54.
  • diff --git a/testapp/testapp/testmain/forms.py b/testapp/testapp/testmain/forms.py index 8fd8d02..abe1e40 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, Tag, Question +from .models import Employee, Dept, ClassRoom, Lab, Word, School, Tag, Question, WordList from django.core.exceptions import ValidationError @@ -27,6 +27,10 @@ class WordChoices(AutoModelSelect2Field): queryset = Word.objects search_fields = ['word__icontains', ] +class MultiWordChoices(AutoModelSelect2MultipleField): + queryset = Word.objects + search_fields = ['word__icontains', ] + class TagField(AutoModelSelect2TagField): queryset = Tag.objects search_fields = ['tag__icontains', ] @@ -87,7 +91,6 @@ class SelfMultiChoices(AutoSelect2MultipleField): ########### Forms ##############] class SchoolForm(forms.ModelForm): - classes = ClassRoomChoices() class Meta: @@ -161,3 +164,10 @@ class QuestionForm(forms.ModelForm): class Meta: model = Question +class WordsForm(forms.ModelForm): + word = WordChoices() + words = MultiWordChoices() + + class Meta: + model = WordList + exclude = ['kind'] diff --git a/testapp/testapp/testmain/models.py b/testapp/testapp/testmain/models.py index 227ec88..104761a 100644 --- a/testapp/testapp/testmain/models.py +++ b/testapp/testapp/testmain/models.py @@ -52,3 +52,15 @@ class Question(models.Model): def __unicode__(self): return unicode(self.question) + +class KeyValueMap(models.Model): + key = models.CharField(max_length=200) + value = models.CharField(max_length=300) + + def __unicode__(self): + return u'%s=>%s' % (self.key, self.value) + +class WordList(models.Model): + kind = models.CharField(max_length=100) + word = models.ForeignKey(Word, null=True, blank=True, related_name='wordlist_word') + words = models.ManyToManyField(Word, null=True, blank=True, related_name='wordlist_words') diff --git a/testapp/testapp/testmain/urls.py b/testapp/testapp/testmain/urls.py index 47288ac..c2e2882 100644 --- a/testapp/testapp/testmain/urls.py +++ b/testapp/testapp/testmain/urls.py @@ -14,4 +14,8 @@ urlpatterns = patterns('testapp.testmain.views', 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'), + + url(r'auto_model/form/$', 'test_auto_multivalue_field', name='test_auto_multivalue_field'), + + url(r'auto_heavy/perf_test/$', 'test_auto_heavy_perf', name='test_auto_heavy_perf'), ) diff --git a/testapp/testapp/testmain/views.py b/testapp/testapp/testmain/views.py index 15d8589..7078c2d 100644 --- a/testapp/testapp/testmain/views.py +++ b/testapp/testapp/testmain/views.py @@ -2,8 +2,8 @@ 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, QuestionForm -from .models import Employee, Dept, Question +from .forms import EmployeeForm, DeptForm, MixedForm, InitialValueForm, QuestionForm, WordsForm, SchoolForm +from .models import Employee, Dept, Question, WordList, School def test_single_value_model_field(request): return render(request, 'list.html', { @@ -80,3 +80,32 @@ def test_tagging(request, id): form = QuestionForm(instance=question) return render(request, 'form.html', {'form': form}) +def test_auto_multivalue_field(request): + try: + s = School.objects.get(id=1) + except School.DoesNotExist: + s = School(id=1) + + if request.POST: + form = SchoolForm(data=request.POST, instance=s) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('home')) + else: + form = SchoolForm(instance=s) + return render(request, 'form.html', {'form': form}) + +def test_auto_heavy_perf(request): + try: + word = WordList.objects.get(kind='Word_Of_Day') + except WordList.DoesNotExist: + word = WordList(kind='Word_Of_Day') + + if request.POST: + form = WordsForm(data=request.POST, instance=word) + if form.is_valid(): + form.save() + return HttpResponseRedirect(reverse('home')) + else: + form = WordsForm(instance=word) + return render(request, 'form.html', {'form': form})