Skip to content

Commit

Permalink
Fix bugs
Browse files Browse the repository at this point in the history
Improve tests
  • Loading branch information
carlosgalvez-tiendeo committed Dec 18, 2020
1 parent 126b29f commit 46e82b1
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 127 deletions.
99 changes: 98 additions & 1 deletion tests/test_ndb.py
Expand Up @@ -15,7 +15,11 @@
RepeatedKeyPropertyField,\
PrefetchedKeyPropertyField,\
RepeatedPrefetchedKeyPropertyField,\
JsonPropertyField
JsonPropertyField, \
StringListPropertyField, \
GeoPtPropertyField, \
IntegerListPropertyField, \
ReferencePropertyField

from wtforms_appengine.ndb import model_form

Expand Down Expand Up @@ -376,3 +380,96 @@ def test_choices_override(self):
# For provided choices, they should be in the provided order
assert bound_form.genres.choices == expected
assert bound_form.name.choices == expected


class TestGeoFields:
class GeoTestForm(Form):
geo = GeoPtPropertyField()

def test_geopt_property(self):
form = self.GeoTestForm(DummyPostData(geo='5.0, -7.0'))
assert form.validate()
assert form.geo.data == '5.0,-7.0'
form = self.GeoTestForm(DummyPostData(geo='5.0,-f'))
assert not form.validate()


class TestReferencePropertyField:
nosegae_datastore_v3 = True

def build_form(self, reference_class=Author, **kw):
class BookForm(Form):
author = ReferencePropertyField(
reference_class=reference_class,
**kw)
return BookForm

def author_expected(self, selected_index=None, get_label=lambda x: x.name):
expected = set()
for i, author in enumerate(self.authors):
expected.add((author.key.urlsafe(),
get_label(author),
i == selected_index))
return expected

def setUp(self):
self.authors = fill_authors(Author)
self.author_names = set(x.name for x in self.authors)
self.author_ages = set(x.age for x in self.authors)

def test_basic(self, client):
with client.context():
self.setUp()
F = self.build_form(
get_label='name'
)
form = F()
assert set(form.author.iter_choices()) == self.author_expected()
assert not form.validate()

form = F(DummyPostData(author=str(self.authors[0].key)))
assert form.validate()
# What we want to validate here?
# assert set(form.author.iter_choices()) == self.author_expected(0)

def test_not_in_query(self, client):
with client.context():
self.setUp()
F = self.build_form()
new_author = Author(name='Jim', age=48)
new_author.put()
form = F(author=new_author)
form.author.query = Author.query().filter(Author.name != 'Jim')
assert form.author.data is new_author
assert not form.validate()

def test_get_label_func(self, client):
with client.context():
self.setUp()
get_age = lambda x: x.age
F = self.build_form(get_label=get_age)
form = F()
ages = set(x.label.text for x in form.author)
assert ages == self.author_ages

def test_allow_blank(self, client):
with client.context():
self.setUp()
F = self.build_form(allow_blank=True, get_label='name')
form = F(DummyPostData(author='__None'))
assert form.validate()
assert form.author.data is None
expected = self.author_expected()
expected.add(('__None', '', True))
assert set(form.author.iter_choices()) == expected


class TestStringListPropertyField:
class F(Form):
a = StringListPropertyField()

def test_basic(self, client):
with client.context():
form = self.F(DummyPostData(a='foo\nbar\nbaz'))
assert form.a.data == ['foo', 'bar', 'baz']
assert form.a._value() == 'foo\nbar\nbaz'
248 changes: 124 additions & 124 deletions wtforms_appengine/fields/ndb.py
Expand Up @@ -17,130 +17,6 @@
'ReferencePropertyField']


class ReferencePropertyField(fields.SelectFieldBase):
"""
A field for ``db.ReferenceProperty``. The list items are rendered in a
select.
:param reference_class:
A db.Model class which will be used to generate the default query
to make the list of items. If this is not specified, The `query`
property must be overridden before validation.
:param get_label:
If a string, use this attribute on the model class as the label
associated with each option. If a one-argument callable, this callable
will be passed model instance and expected to return the label text.
Otherwise, the model object's `__str__` or `__unicode__` will be used.
:param allow_blank:
If set to true, a blank choice will be added to the top of the list
to allow `None` to be chosen.
:param blank_text:
Use this to override the default blank option's label.
"""
widget = widgets.Select()

def __init__(self, label=None, validators=None, reference_class=None,
get_label=None, allow_blank=False,
blank_text='', **kwargs):
super(ReferencePropertyField, self).__init__(label, validators,
**kwargs)
if get_label is None:
self.get_label = lambda x: x
elif isinstance(get_label, string_types):
self.get_label = operator.attrgetter(get_label)
else:
self.get_label = get_label

self.allow_blank = allow_blank
self.blank_text = blank_text
self._set_data(None)
if reference_class is not None:
self.query = reference_class.all()

def _get_data(self):
if self._formdata is not None:
for obj in self.query:
if str(obj.key()) == self._formdata:
self._set_data(obj)
break
return self._data

def _set_data(self, data):
self._data = data
self._formdata = None

data = property(_get_data, _set_data)

def iter_choices(self):
if self.allow_blank:
yield ('__None', self.blank_text, self.data is None)

for obj in self.query:
key = str(obj.key())
label = self.get_label(obj)
yield (key,
label,
(self.data.key() == obj.key()) if self.data else False)

def process_formdata(self, valuelist):
if valuelist:
if valuelist[0] == '__None':
self.data = None
else:
self._data = None
self._formdata = valuelist[0]

def pre_validate(self, form):
data = self.data
if data is not None:
s_key = str(data.key())
for obj in self.query:
if s_key == str(obj.key()):
break
else:
raise ValueError(self.gettext('Not a valid choice'))
elif not self.allow_blank:
raise ValueError(self.gettext('Not a valid choice'))


class StringListPropertyField(fields.TextAreaField):
"""
A field for ``db.StringListProperty``. The list items are rendered in a
textarea.
"""
def _value(self):
if self.raw_data:
return self.raw_data[0]
else:
return self.data and text_type("\n".join(self.data)) or ''

def process_formdata(self, valuelist):
if valuelist:
try:
self.data = valuelist[0].splitlines()
except ValueError:
raise ValueError(self.gettext('Not a valid list'))


class IntegerListPropertyField(fields.TextAreaField):
"""
A field for ``db.StringListProperty``. The list items are rendered in a
textarea.
"""
def _value(self):
if self.raw_data:
return self.raw_data[0]
else:
return text_type('\n'.join(self.data)) if self.data else ''

def process_formdata(self, valuelist):
if valuelist:
try:
self.data = [int(value) for value in valuelist[0].splitlines()]
except ValueError:
raise ValueError(self.gettext('Not a valid integer list'))


class KeyPropertyField(fields.SelectFieldBase):
"""
A field for ``ndb.KeyProperty``. The list items are rendered in a select.
Expand Down Expand Up @@ -340,3 +216,127 @@ def process_formdata(self, valuelist):

def _value(self):
return json.dumps(self.data) if self.data is not None else ''


class ReferencePropertyField(KeyPropertyField):
"""
A field for ``db.ReferenceProperty``. The list items are rendered in a
select.
:param reference_class:
A db.Model class which will be used to generate the default query
to make the list of items. If this is not specified, The `query`
property must be overridden before validation.
:param get_label:
If a string, use this attribute on the model class as the label
associated with each option. If a one-argument callable, this callable
will be passed model instance and expected to return the label text.
Otherwise, the model object's `__str__` or `__unicode__` will be used.
:param allow_blank:
If set to true, a blank choice will be added to the top of the list
to allow `None` to be chosen.
:param blank_text:
Use this to override the default blank option's label.
"""
widget = widgets.Select()

def __init__(self, label=None, validators=None, reference_class=None,
get_label=None, allow_blank=False,
blank_text='', **kwargs):
super(ReferencePropertyField, self).__init__(label, validators,
**kwargs)
if get_label is None:
self.get_label = lambda x: x
elif isinstance(get_label, string_types):
self.get_label = operator.attrgetter(get_label)
else:
self.get_label = get_label

self.allow_blank = allow_blank
self.blank_text = blank_text
self._set_data(None)
if reference_class is not None:
self.query = reference_class.query()

def _get_data(self):
if self._formdata is not None:
for obj in self.query:
if str(obj.key) == self._formdata:
self._set_data(obj)
break
return self._data

def _set_data(self, data):
self._data = data
self._formdata = None

data = property(_get_data, _set_data)

def iter_choices(self):
if self.allow_blank:
yield ('__None', self.blank_text, self.data is None)

for obj in self.query:
key = self._key_value(obj.key)
label = self.get_label(obj)
yield (key,
label,
(self.data == obj.key) if self.data else False)

def process_formdata(self, valuelist):
if valuelist:
if valuelist[0] == '__None':
self.data = None
else:
self._data = None
self._formdata = valuelist[0]

def pre_validate(self, form):
data = self.data
if data is not None:
s_key = str(data.key)
for obj in self.query:
if s_key == str(obj.key):
break
else:
raise ValueError(self.gettext('Not a valid choice'))
elif not self.allow_blank:
raise ValueError(self.gettext('Not a valid choice'))


class StringListPropertyField(fields.TextAreaField):
"""
A field for ``db.StringListProperty``. The list items are rendered in a
textarea.
"""
def _value(self):
if self.raw_data:
return self.raw_data[0]
else:
return self.data and text_type("\n".join(self.data)) or ''

def process_formdata(self, valuelist):
if valuelist:
try:
self.data = valuelist[0].splitlines()
except ValueError:
raise ValueError(self.gettext('Not a valid list'))


class IntegerListPropertyField(fields.TextAreaField):
"""
A field for ``db.StringListProperty``. The list items are rendered in a
textarea.
"""
def _value(self):
if self.raw_data:
return self.raw_data[0]
else:
return text_type('\n'.join(self.data)) if self.data else ''

def process_formdata(self, valuelist):
if valuelist:
try:
self.data = [int(value) for value in valuelist[0].splitlines()]
except ValueError:
raise ValueError(self.gettext('Not a valid integer list'))
5 changes: 3 additions & 2 deletions wtforms_appengine/ndb.py
Expand Up @@ -190,6 +190,7 @@ def convert(self, model, prop, field_args):


class ModelConverter(ModelConverterBase):
from google.cloud.ndb import model
"""
Converts properties from a ``ndb.Model`` class to form fields.
Expand All @@ -205,7 +206,7 @@ class ModelConverter(ModelConverterBase):
+--------------------+-------------------+--------------+------------------+
| IntegerProperty | IntegerField | int or long | | repeated support
+--------------------+-------------------+--------------+------------------+
| FloatProperty | StringField | float | |
| FloatProperty | StringField | float | |
+--------------------+-------------------+--------------+------------------+
| DateTimeProperty | DateTimeField | datetime | skipped if |
| | | | auto_now[_add] |
Expand All @@ -218,7 +219,7 @@ class ModelConverter(ModelConverterBase):
+--------------------+-------------------+--------------+------------------+
| TextProperty | TextAreaField | unicode | |
+--------------------+-------------------+--------------+------------------+
| GeoPtProperty | StringField | db.GeoPt | |
| GeoPtProperty | StringField | db.GeoPt | |
+--------------------+-------------------+--------------+------------------+
| KeyProperty | KeyProperyField | ndb.Key | |
+--------------------+-------------------+--------------+------------------+
Expand Down

0 comments on commit 46e82b1

Please sign in to comment.