Skip to content

Commit

Permalink
Merge pull request #113 from adarshk7/features/grouped-query-select-m…
Browse files Browse the repository at this point in the history
…ultiple-field

Add GroupedQuerySelectMultipleField
  • Loading branch information
kvesteri committed Feb 28, 2017
2 parents b9387b3 + 26c1558 commit 6a1b80a
Show file tree
Hide file tree
Showing 3 changed files with 227 additions and 12 deletions.
105 changes: 93 additions & 12 deletions tests/test_query_select_field.py
Expand Up @@ -4,6 +4,8 @@

from wtforms_alchemy import (
GroupedQuerySelectField,
GroupedQuerySelectMultipleField,
ModelForm,
QuerySelectField,
QuerySelectMultipleField
)
Expand Down Expand Up @@ -223,17 +225,23 @@ def teardown_method(self, method):
self.base.metadata.drop_all(self.engine)
self.engine.dispose()


class TestGroupedQuerySelectField(DatabaseTestCase):
def create_models(self):
class City(self.base):
__tablename__ = 'city'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
country = sa.Column(sa.String)
state_id = sa.Column(sa.Integer, sa.ForeignKey('state.id'))

self.City = City

class State(self.base):
__tablename__ = 'state'
id = sa.Column(sa.Integer, primary_key=True)
cities = sa.orm.relationship('City')

self.State = State

def create_cities(self):
self.session.add_all([
self.City(name='Helsinki', country='Finland'),
Expand All @@ -243,6 +251,8 @@ def create_cities(self):
self.City(name='Stockholm', country='Sweden'),
])


class TestGroupedQuerySelectField(DatabaseTestCase):
def create_form(self, **kwargs):
query = self.session.query(self.City).order_by('name', 'country')

Expand All @@ -259,6 +269,19 @@ class MyForm(Form):

return MyForm

def test_custom_none_value(self):
self.create_cities()
MyForm = self.create_form(
allow_blank=True,
blank_text='Choose city...',
blank_value=''
)
form = MyForm(DummyPostData({'city': ''}))
assert form.validate(), form.errors
assert '<option selected value="">Choose city...</option>' in (
str(form.city)
)

def test_rendering(self):
MyForm = self.create_form()
self.create_cities()
Expand All @@ -277,15 +300,73 @@ def test_rendering(self):
'</select>'
)

def test_custom_none_value(self):

class TestGroupedQuerySelectMultipleField(DatabaseTestCase):
def create_form(self, **kwargs):
query = self.session.query(self.City).order_by('name', 'country')

class MyForm(ModelForm):
class Meta:
model = self.State

cities = GroupedQuerySelectMultipleField(
label=kwargs.get('label', 'City'),
query_factory=kwargs.get('query_factory', lambda: query),
get_label=kwargs.get('get_label', lambda c: c.name),
get_group=kwargs.get('get_group', lambda c: c.country),
blank_text=kwargs.get('blank_text', ''),
)

return MyForm

def test_unpopulated_default(self):
MyForm = self.create_form()
self.create_cities()
MyForm = self.create_form(
allow_blank=True,
blank_text='Choose city...',
blank_value=''
)
form = MyForm(DummyPostData({'city': ''}))
assert form.validate(), form.errors
assert '<option selected value="">Choose city...</option>' in (
str(form.city)
assert MyForm().cities.data == []

def test_single_value_without_factory(self):
obj = self.State()
MyForm = self.create_form()
self.create_cities()
form = MyForm(DummyPostData(cities=['1']), obj=obj)
assert [1] == [v.id for v in form.cities.data]
assert form.validate()
form.populate_obj(obj)
assert [city.id for city in obj.cities] == [1]

def test_multiple_values_without_query_factory(self):
obj = self.State()
MyForm = self.create_form()
self.create_cities()
form = MyForm(DummyPostData(cities=['1', '2']), obj=obj)
form.cities.query = self.session.query(self.City)

assert [1, 2] == [v.id for v in form.cities.data]
assert form.validate()
form.populate_obj(obj)
assert [city.id for city in obj.cities] == [1, 2]

form = MyForm(DummyPostData(cities=['1', '666']))
form.cities.query = self.session.query(self.City)
assert [x.id for x in form.cities.data] == [1]
assert not form.validate()
form.populate_obj(obj)
assert [city.id for city in obj.cities] == [1]

def test_rendering(self):
MyForm = self.create_form()
self.create_cities()
assert str(MyForm().cities).replace('\n', '') == (
'<select id="cities" multiple name="cities">'
'<optgroup label="Finland">'
'<option value="1">Helsinki</option>'
'<option value="2">Vantaa</option>'
'</optgroup><optgroup label="Sweden">'
'<option value="5">Stockholm</option>'
'</optgroup>'
'<optgroup label="USA">'
'<option value="3">New York</option>'
'<option value="4">Washington</option>'
'</optgroup>'
'</select>'
)
2 changes: 2 additions & 0 deletions wtforms_alchemy/__init__.py
Expand Up @@ -21,6 +21,7 @@
from .fields import ( # noqa
CountryField,
GroupedQuerySelectField,
GroupedQuerySelectMultipleField,
ModelFieldList,
ModelFormField,
PhoneNumberField,
Expand Down Expand Up @@ -115,6 +116,7 @@ def __init__(cls, *args, **kwargs):
generator.create_form(cls)
return ModelFormMeta


ModelFormMeta = model_form_meta_factory()


Expand Down
132 changes: 132 additions & 0 deletions wtforms_alchemy/fields.py
Expand Up @@ -445,6 +445,138 @@ def pre_validate(self, form):
raise ValidationError('Not a valid choice')


class GroupedQuerySelectMultipleField(SelectField):
widget = SelectWidget(multiple=True)

def __init__(
self,
label=None,
validators=None,
query_factory=None,
get_pk=None,
get_label=None,
get_group=None,
blank_text='',
default=None,
**kwargs
):
if default is None:
default = []
super(GroupedQuerySelectMultipleField, self).__init__(
label,
validators,
default=default,
coerce=lambda x: x,
**kwargs
)
if kwargs.get('allow_blank', False):
import warnings
warnings.warn(
'allow_blank=True does not do anything for '
'GroupedQuerySelectMultipleField.'
)

self.query = None
self.query_factory = query_factory

if get_pk is None:
self.get_pk = get_pk_from_identity
else:
self.get_pk = get_pk

self.get_label = get_label
self.get_group = get_group

self.blank_text = blank_text

self._choices = None
self._invalid_formdata = False

def _get_object_list(self):
query = self.query or self.query_factory()
return list((six.text_type(self.get_pk(obj)), obj) for obj in query)

def _pre_process_object_list(self, object_list):
return sorted(
object_list,
key=lambda x: (x[1] or u'', self.get_label(x[2]) or u'')
)

@property
def choices(self):
if not self._choices:
object_list = map(
lambda x: (x[0], self.get_group(x[1]), x[1]),
self._get_object_list()
)
# object_list is (key, group, value) tuple
choices = []
object_list = self._pre_process_object_list(object_list)
for group, data in groupby(object_list, key=lambda x: x[1]):
if group is not None:
group_items = []
for key, _, value in data:
group_items.append((key, self.get_label(value)))
choices.append((group, group_items))
else:
for key, group, value in data:
choices.append((key, self.get_label(value)))
self._choices = choices
return self._choices

@choices.setter
def choices(self, value):
pass

@property
def data(self):
formdata = self._formdata
if formdata is not None:
data = []
for pk, obj in self._get_object_list():
if not formdata:
break
elif self.coerce(pk) in formdata:
formdata.remove(self.coerce(pk))
data.append(obj)
if formdata:
self._invalid_formdata = True
self.data = data
return self._data

@data.setter
def data(self, valuelist):
self._data = valuelist
self._formdata = None

def iter_choices(self):
"""
We should update how choices are iter to make sure that value from
internal list or tuple should be selected.
"""
for value, label in self.concrete_choices:
yield (
value,
label,
(
self.coerce,
[self.get_pk(obj) for obj in self.data or []]
)
)

def process_formdata(self, valuelist):
self._formdata = set(valuelist)

def pre_validate(self, form):
if self._invalid_formdata:
raise ValidationError(self.gettext('Not a valid choice'))
elif self.data:
obj_list = list(x[1] for x in self._get_object_list())
for v in self.data:
if v not in obj_list:
raise ValidationError(self.gettext('Not a valid choice'))


class WeekDaysField(SelectMultipleField):
widget = ListWidget(prefix_label=False)
option_widget = CheckboxInput()
Expand Down

0 comments on commit 6a1b80a

Please sign in to comment.