From 46e82b1cdc9f32ebe200db0ea5f8ab2bc14a30fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20G=C3=A1lvez=20Mart=C3=ADnez?= Date: Fri, 18 Dec 2020 11:42:12 +0100 Subject: [PATCH] Fix bugs Improve tests --- tests/test_ndb.py | 99 ++++++++++++- wtforms_appengine/fields/ndb.py | 248 ++++++++++++++++---------------- wtforms_appengine/ndb.py | 5 +- 3 files changed, 225 insertions(+), 127 deletions(-) diff --git a/tests/test_ndb.py b/tests/test_ndb.py index e78b45a..4eb0417 100644 --- a/tests/test_ndb.py +++ b/tests/test_ndb.py @@ -15,7 +15,11 @@ RepeatedKeyPropertyField,\ PrefetchedKeyPropertyField,\ RepeatedPrefetchedKeyPropertyField,\ - JsonPropertyField + JsonPropertyField, \ + StringListPropertyField, \ + GeoPtPropertyField, \ + IntegerListPropertyField, \ + ReferencePropertyField from wtforms_appengine.ndb import model_form @@ -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' diff --git a/wtforms_appengine/fields/ndb.py b/wtforms_appengine/fields/ndb.py index bd6a1d9..a740ecf 100644 --- a/wtforms_appengine/fields/ndb.py +++ b/wtforms_appengine/fields/ndb.py @@ -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. @@ -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')) diff --git a/wtforms_appengine/ndb.py b/wtforms_appengine/ndb.py index 2ca9057..fb2cd3a 100644 --- a/wtforms_appengine/ndb.py +++ b/wtforms_appengine/ndb.py @@ -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. @@ -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] | @@ -218,7 +219,7 @@ class ModelConverter(ModelConverterBase): +--------------------+-------------------+--------------+------------------+ | TextProperty | TextAreaField | unicode | | +--------------------+-------------------+--------------+------------------+ - | GeoPtProperty | StringField | db.GeoPt | | + | GeoPtProperty | StringField | db.GeoPt | | +--------------------+-------------------+--------------+------------------+ | KeyProperty | KeyProperyField | ndb.Key | | +--------------------+-------------------+--------------+------------------+