From cc94db7e2bef1b2be24e70968629e6c1ba360658 Mon Sep 17 00:00:00 2001 From: "AppleGrew (applegrew)" Date: Tue, 17 Sep 2013 22:02:32 +0530 Subject: [PATCH] Issue 54. Widget performance fix. --- django_select2/__init__.py | 2 +- django_select2/fields.py | 36 ++++++++++++++++++++++++--- django_select2/widgets.py | 14 ++++++++++- testapp/test.db | Bin 2441216 -> 2469888 bytes testapp/testapp/templates/index.html | 2 ++ testapp/testapp/testmain/forms.py | 14 +++++++++-- testapp/testapp/testmain/models.py | 12 +++++++++ testapp/testapp/testmain/urls.py | 4 +++ testapp/testapp/testmain/views.py | 33 ++++++++++++++++++++++-- 9 files changed, 108 insertions(+), 9 deletions(-) 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 ca95695a7afdbc9e4b70b5e4701fc69c919fb916..2f81331f041b58844ca4684769a8f6c4d5da894d 100644 GIT binary patch delta 2953 zcmb_eYiv|S6uvX}*?rvEU0AjSwp`lVNG;OmmX?;ch(>G)0YOMC3%k@6`rH*T(R7N* zghWAbfonjN_`nd2s7<=Y5G98ANaXPYe@KENkVpdM5q$8_;LP0acDDthiTmT6J7>Q0 zm^0^`S&VlAd}bHeHr59nf&fYe72y43D~+cl!2~(Cfwed z4YdUsXmO1!l!FpxH5`d#nWU=>sG?dHdZvTQEZ=LR&l~H*>z<1mZQ)MupnodlGAgR5 zl7&Hy!4Psa2E)kGc(76yrrPIYk>tX|5qG2|3*0xp;DRF7I!`B6BkXcDxk9e-t~}>; z=jpyKBj)V!OP~x4PfM*W9hQNRPXZ;tmwn!vd@%M8sz zWnX0SzIQ~QE+^F!6D7Swm;1aGKf31^`eb23FqpZ)L^Pgg3&+~iH(278g$IJgIY~%k zveU5Or_aD7pT1Mj@1WD@5;~4PMLx6_ZABZr2X4y(jP;#<-bX|=pXKCAq!8rndf;iz~#oW?or zW)yaD+Q}$r=d`VVA^ZW^3NlxKTmf(ei7SX)0j3pJ!T~g@j%kYxoK9mD*7tV~+W>Qf;EEk2zC(cB-ll;n_!(_M6icoFTp;7M-e=lU_ZfQ2o4aOPjCUj zV+kHd@OXlQ1Q!xKfnbB+i3At*<6`NALqrz@bOT*Q7ih{(Ax#l^X~@>q7IXCQUDd84 zId!GO^cf-D68x}LkeikD%GYY6dRSYaUCv874mkdFTCOVBH*VlQDpjKo^^@YO@Cs}N z9l};x4a=%~pPsG;%FE%VSR%ZN&0f1(GXrvj06uAz?>5ItY#Lj=1XM_CW9^aTHKkBu zEtd>CiY(6okT;*Lii}A`u}D#=qqKK^qZ$Z>;HL6iKUoc#ag`;5OPIHSmKIspcAK-T z1KZ8fg?t_t(NWz^hgSEb>WYd|qp5kZ(X?d20!OLk-)>H6?3L%Jf!XD-w}i$R-I(!^ z9u6%vtLj6My84O{J8*@y?;r^Dj6fC{%}qwp2s}F}!?v*F_6BFGfhQVZ@AP}PWytuw zo%8I+;qtkQthWyW|4wH7qH1 zJZZIm6%VT$o}^6$wDKAn;7jv!D}veNAZ~nEC_ECGCU39|9YaW7j9OZo)}h}zvmdCr zRmoSoHiJNZ8pvXqYz*C9YRg_T4z15EbBk4AnQvQ(UFN%~7k8KuV0n|~K5PAJ<{Q@8 z?dA?^<_>cjtFxnZ|95fs+`qWdU&8eN*tByO&6~Y=p0RlLoZclf?T%l1_q?-=BlcqR zJ7@WY%kI_k9+pLxK_k|lh_*(%tvma{7(bH^wSLnKB@k|aNv}I#4ZH{P9?GPieZ*TT z?{>%RikH$SPU_!hN*+4?E(_=u`VC#C!|@E_@5y-j>aw>0ati3bP~mPcAg8V$H{G2M z_F@ViK|sG!8>dl807yb{a49=M^O}~2!_S9Tb;lyD(Fg-VbV)#epiA7Hp+P!}sjrLA zYE+4S317<0Uk<7;PhuDfg7y~$0cH!J0RatAOXtuiFLMgBoPx&v*z~w;YghRTK#}Ls cSXJZ>A>+&vLjZJBKsV|B6{IR61fUW80|5?p1poj5 delta 1038 zcmZ9LUuYCZ9LM)JGdp{Cd%JUYF}=jbG}?*eA2h8mwY1toZ3`y3Tyh}zVgf=9!Hs>4``F5d7(qAR?84{MSlJ*(`>8TRBD+4em>RZfqz}q8I%G(Tqw}vajFfTrA zG3}JLR|#!~gtSHnR6|ECI?5*4ZF`w- zh6=vQW)eg!T4Gr&iYNL~-4%A79+L%&yX2bHC8kR(To{&_rq}6ts}^dl*axwt_*V%# zal{>S?|47>6~V>V&LyXgf6BhGi!@C>hdH(tYKtSO2LVjV8^NZM{4ofQ%Tlm>OrB)y zOKU86=a6i(_YChP`QYlf%m&*h<-4w~us7f0+3n4wnD(;4qmpc3_MFAS;)EOmUtgd@ z`W?$QH)GA#UKT6+8b1*`n~%m4rY 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})