Compare commits

...
5 Commits
Author SHA1 Message Date
Johannes Hoppe 4defb8112b Fixed #222 -- Display intital data for heavy widgets 2015-12-01 10:19:02 +01:00
Johannes Hoppe bb90be77b7 Fixed #231 -- Avoid fetching entire queryset from db
Patched ciritcal memory leak in ModelSelect2 widgets.
Boolean evaluation of queryset makes django fetch all
elements.

Thanks to @magicjohnson for discovering this issue
2015-11-30 13:17:02 +01:00
Johannes Hoppe 18647161a1 Fixed docs 2015-11-29 20:47:22 +01:00
Royendgel Silberie 3e6c1fc3ba fixing small typo
Closed #227
2015-11-29 20:03:01 +01:00
Johannes Hoppe 19a8d63bb1 Refactors tag widgets and adds light tag widget
Currently there was no light version of the tag widget.
A light version might come handy if you use a postgres
`ArrayField` and a corresponding `SimpleArrayField`.

Resolved #226
2015-11-23 11:54:51 +01:00
5 changed files with 70 additions and 18 deletions
+9
View File
@@ -1,6 +1,15 @@
Changelog Summary
=================
### v5.4.2
* Fixed initial data not being shown for heavy widgets.
### v5.4.1
* Fixed memory leak in `ModelSelect2Mixin` and subclasses
### v5.4.0
* Added `Select2TagWidget` a light widget with tagging support
### v5.3.0
* Added djangoSelect2 jQuery plugin to support
dynamic field initialisation
+1 -1
View File
@@ -9,4 +9,4 @@ The app includes Select2 driven Django Widgets and Form Fields.
"""
__version__ = "5.3.1"
__version__ = "5.4.2"
+41 -13
View File
@@ -49,6 +49,7 @@ Light widgets are normally named, i.e. there is no
from __future__ import absolute_import, unicode_literals
from functools import reduce
from itertools import chain
from django import forms
from django.core import signing
@@ -109,6 +110,18 @@ class Select2Mixin(object):
media = property(_get_media)
class Select2TagMixin(object):
"""Mixin to add select2 tag functionality."""
def build_attrs(self, extra_attrs=None, **kwargs):
"""Add select2's tag attributes."""
self.attrs.setdefault('data-minimum-input-length', 1)
self.attrs.setdefault('data-tags', 'true')
self.attrs.setdefault('data-token-separators', [",", " "])
return super(Select2TagMixin, self).build_attrs(extra_attrs, **kwargs)
class Select2Widget(Select2Mixin, forms.Select):
"""
@@ -145,6 +158,24 @@ class Select2MultipleWidget(Select2Mixin, forms.SelectMultiple):
pass
class Select2TagWidget(Select2TagMixin, Select2Mixin, forms.SelectMultiple):
"""
Select2 drop in widget for for tagging.
Example for :class:`.django.contrib.postgres.fields.ArrayField`::
class MyWidget(Select2TagWidget):
def value_from_datadict(self, data, files, name):
values = super(MyWidget, self).value_from_datadict(data, files, name):
return ",".join(values)
"""
pass
class HeavySelect2Mixin(Select2Mixin):
"""Mixin that adds select2's ajax options and registers itself on django's cache."""
@@ -203,6 +234,7 @@ class HeavySelect2Mixin(Select2Mixin):
def render_options(self, choices, selected_choices):
"""Render only selected options."""
choices = chain(choices, self.choices)
output = ['<option></option>' if not self.is_required else '']
choices = {(k, v) for k, v in choices if k in selected_choices}
selected_choices = {force_text(v) for v in selected_choices}
@@ -242,16 +274,11 @@ class HeavySelect2MultipleWidget(HeavySelect2Mixin, forms.SelectMultiple):
pass
class HeavySelect2TagWidget(HeavySelect2MultipleWidget):
class HeavySelect2TagWidget(Select2TagMixin, HeavySelect2MultipleWidget):
"""Mixin to add select2 tag functionality."""
"""Select2 tag widget."""
def build_attrs(self, extra_attrs=None, **kwargs):
"""Add select2's tag attributes."""
self.attrs.setdefault('data-minimum-input-length', 1)
self.attrs.setdefault('data-tags', 'true')
self.attrs.setdefault('data-token-separators', [",", " "])
return super(HeavySelect2TagWidget, self).build_attrs(extra_attrs, **kwargs)
pass
# Auto Heavy widgets
@@ -326,7 +353,7 @@ class ModelSelect2Mixin(object):
:return: Filtered queryset
:rtype: :class:`.django.db.models.QuerySet`
"""
if not queryset:
if queryset is None:
queryset = self.get_queryset()
search_fields = self.get_search_fields()
select = Q()
@@ -375,6 +402,7 @@ class ModelSelect2Mixin(object):
choices = {self.choices.choice(obj)
for obj in self.choices.queryset.filter(pk__in=selected_choices)}
else:
choices = chain(choices, self.choices)
choices = {(k, v) for k, v in choices if k in selected_choices}
selected_choices = {force_text(v) for v in selected_choices}
for option_value, option_label in choices:
@@ -391,7 +419,7 @@ class ModelSelect2Widget(ModelSelect2Mixin, HeavySelect2Widget):
class MyWidget(ModelSelect2Widget):
search_fields = [
'title__icontians',
'title__icontains',
]
class MyModelForm(forms.ModelForm):
@@ -432,10 +460,10 @@ class ModelSelect2MultipleWidget(ModelSelect2Mixin, HeavySelect2MultipleWidget):
pass
class ModelSelect2TagWidget(ModelSelect2Mixin, HeavySelect2TagWidget):
class ModelSelect2TagWidget(Select2TagMixin, ModelSelect2MultipleWidget):
"""
Select2 model field with tag support.
Select2 model widget with tag support.
This it not a simple drop in widget.
It requires to implement you own :func:`.value_from_datadict`
@@ -447,7 +475,7 @@ class ModelSelect2TagWidget(ModelSelect2Mixin, HeavySelect2TagWidget):
queryset = MyModel.objects.all()
def value_from_datadict(self, data, files, name):
values = super().value_from_datadict(self, data, files, name):
values = super().value_from_datadict(self, data, files, name)
qs = self.queryset.filter(**{'pk__in': list(values)})
pks = set(force_text(getattr(o, pk)) for o in qs)
cleaned_values = []
+14 -1
View File
@@ -30,6 +30,11 @@ class TestSelect2Mixin(object):
form = forms.AlbumSelect2WidgetForm()
widget_cls = Select2Widget
def test_initial_data(self, genres):
genre = genres[0]
form = self.form.__class__(initial={'primary_genre': genre.pk})
assert text_type(genre) in form.as_p()
def test_initial_form_class(self):
widget = self.widget_cls(attrs={'class': 'my-class'})
assert 'my-class' in widget.render('name', None)
@@ -81,9 +86,12 @@ class TestSelect2Mixin(object):
class TestHeavySelect2Mixin(TestSelect2Mixin):
url = reverse('heavy_select2_widget')
form = forms.HeavySelect2WidgetForm(initial={'primary_genre': [1]})
form = forms.HeavySelect2WidgetForm(initial={'primary_genre': 1})
widget_cls = HeavySelect2Widget
def test_initial_data(self):
assert 'One' in self.form.as_p()
def test_initial_form_class(self):
widget = self.widget_cls(data_view='heavy_data_1', attrs={'class': 'my-class'})
assert 'my-class' in widget.render('name', None)
@@ -129,6 +137,11 @@ class TestHeavySelect2Mixin(TestSelect2Mixin):
class TestModelSelect2Mixin(TestHeavySelect2Mixin):
form = forms.AlbumModelSelect2WidgetForm(initial={'primary_genre': 1})
def test_initial_data(self, genres):
genre = genres[0]
form = self.form.__class__(initial={'primary_genre': genre.pk})
assert text_type(genre) in form.as_p()
@pytest.fixture(autouse=True)
def genres(self, db):
return mommy.make(Genre, 100)
+5 -3
View File
@@ -114,11 +114,13 @@ class Select2WidgetForm(forms.Form):
class HeavySelect2WidgetForm(forms.Form):
artist = forms.ChoiceField(
widget=HeavySelect2Widget(data_view='heavy_data_1', choices=NUMBER_CHOICES)
widget=HeavySelect2Widget(data_view='heavy_data_1'),
choices=NUMBER_CHOICES
)
primary_genre = forms.ChoiceField(
widget=HeavySelect2Widget(data_view='heavy_data_2', choices=NUMBER_CHOICES),
required=False
widget=HeavySelect2Widget(data_view='heavy_data_2'),
required=False,
choices=NUMBER_CHOICES
)