dpgaspar / Flask-AppBuilder

Simple and rapid application development framework, built on top of Flask. includes detailed security, auto CRUD generation for your models, google charts and much more. Demo (login with guest/welcome) - http://flaskappbuilder.pythonanywhere.com/
BSD 3-Clause "New" or "Revised" License
4.7k stars 1.36k forks source link

custom form fails on pre_validation #865

Closed kurtforrester closed 5 years ago

kurtforrester commented 5 years ago

Environment

Flask-Appbuilder version:

Flask-AppBuilder==1.12.2

pip freeze output:

alabaster==0.7.10 anaconda-client==1.6.9 anaconda-navigator==1.7.0 anaconda-project==0.8.2 asn1crypto==0.24.0 astroid==1.6.1 astropy==2.0.3 attrs==17.4.0 Babel==2.5.3 backports.shutil-get-terminal-size==1.0.0 beautifulsoup4==4.6.0 bitarray==0.8.1 bkcharts==0.2 blaze==0.11.3 bleach==2.1.2 bokeh==0.12.13 boto==2.48.0 Bottleneck==1.2.1 cairocffi==0.9.0 CairoSVG==2.2.1 certifi==2018.10.15 cffi==1.11.4 chardet==3.0.4 click==6.7 cloudpickle==0.5.2 clyent==1.2.2 colorama==0.3.9 conda==4.5.11 conda-build==3.4.1 conda-verify==2.0.0 contextlib2==0.5.5 cryptography==2.1.4 cssselect2==0.2.1 cycler==0.10.0 Cython==0.27.3 cytoolz==0.9.0 dask==0.16.1 datashape==0.5.4 decorator==4.2.1 defusedxml==0.5.0 distributed==1.20.2 Django==2.1.1 docutils==0.14 entrypoints==0.2.3 et-xmlfile==1.0.1 fastcache==1.0.2 filelock==2.0.13 Flask==1.0.2 Flask-AppBuilder==1.12.2 Flask-Babel==0.12.2 Flask-Cors==3.0.3 Flask-Login==0.4.1 Flask-OpenID==1.2.5 Flask-SQLAlchemy==2.3.2 Flask-WTF==0.14.2 gevent==1.2.2 glob2==0.6 gmpy2==2.0.8 greenlet==0.4.12 h5py==2.7.1 heapdict==1.0.0 html5lib==1.0.1 idna==2.6 imageio==2.2.0 imagesize==0.7.1 ipykernel==4.8.0 ipython==6.2.1 ipython-genutils==0.2.0 ipywidgets==7.1.1 isort==4.2.15 itsdangerous==0.24 jdcal==1.3 jedi==0.11.1 jeepney==0.3.1 Jinja2==2.10 jsonschema==2.6.0 jupyter==1.0.0 jupyter-client==5.2.2 jupyter-console==5.2.0 jupyter-core==4.4.0 jupyterlab==0.31.5 jupyterlab-launcher==0.10.2 keyring==13.0.0 kiwisolver==1.0.1 lazy-object-proxy==1.3.1 llvmlite==0.24.0 locket==0.2.0 lxml==4.1.1 Mako==1.0.7 MarkupSafe==1.0 matplotlib==3.0.1 mccabe==0.6.1 mistune==0.8.3 mkl-fft==1.0.4 mkl-random==1.0.1 mpmath==1.0.0 msgpack-python==0.5.1 multipledispatch==0.4.9 navigator-updater==0.1.0 nbconvert==5.3.1 nbformat==4.4.0 networkx==2.1 nltk==3.2.5 nose==1.3.7 notebook==5.4.0 numba==0.39.0 numexpr==2.6.4 numpy==1.15.4 numpydoc==0.7.0 odo==0.5.1 olefile==0.45.1 openpyxl==2.4.10 packaging==16.8 pandas==0.23.4 pandocfilters==1.4.2 parso==0.1.1 partd==0.3.8 path.py==10.5 pathlib2==2.3.0 patsy==0.5.0 pdfrw==0.4 pep8==1.7.1 periodictable==1.5.0 pexpect==4.3.1 pickleshare==0.7.4 Pillow==5.2.0 pkginfo==1.4.1 pluggy==0.6.0 ply==3.10 prompt-toolkit==1.0.15 psutil==5.4.3 ptyprocess==0.5.2 py==1.5.2 pycodestyle==2.3.1 pycosat==0.6.3 pycparser==2.18 pycrypto==2.6.1 pycurl==7.43.0.1 pyflakes==1.6.0 Pygments==2.2.0 pylint==1.8.2 pyodbc==4.0.22 pyOpenSSL==17.5.0 pyparsing==2.2.0 Pyphen==0.9.5 PySocks==1.6.7 pytest==3.3.2 python-dateutil==2.6.1 python3-openid==3.1.0 pytz==2017.3 PyWavelets==0.5.2 PyYAML==3.12 pyzmq==16.0.3 QtAwesome==0.4.4 qtconsole==4.3.1 QtPy==1.5.2 requests==2.18.4 rope==0.10.7 ruamel-yaml==0.15.35 scikit-image==0.13.1 scikit-learn==0.19.1 scipy==1.1.0 seaborn==0.8.1 SecretStorage==3.0.1 Send2Trash==1.4.2 simplegeneric==0.8.1 singledispatch==3.4.0.3 six==1.11.0 snowballstemmer==1.2.1 sortedcollections==0.5.3 sortedcontainers==1.5.9 Sphinx==1.8.1 sphinxcontrib-websupport==1.0.1 spyder==3.3.2 spyder-kernels==0.2.4 SQLAlchemy==1.2.13 statsmodels==0.9.0 sympy==1.1.1 tables==3.4.2 tblib==1.3.2 terminado==0.8.1 testpath==0.3.1 tinycss2==0.6.1 toolz==0.9.0 tornado==4.5.3 traitlets==4.3.2 typing==3.6.2 unicodecsv==0.14.1 urllib3==1.22 wcwidth==0.1.7 WeasyPrint==0.42.3 webencodings==0.5.1 Werkzeug==0.14.1 widgetsnbextension==3.1.0 wrapt==1.10.11 WTForms==2.2.1 xarray==0.10.6 xlrd==1.1.0 XlsxWriter==1.1.2 xlwt==1.3.0 zict==0.1.3

Describe the expected results

I am trying to build a custom form which will find its way into a structured multi-model query which will eventually produce a PDF report... The form will take query criteria via form fields to construct the query. I can build the custom form and then populate some of the select fields with values from the database models but when I submit I get a validation error. This is some non-specific code which produces the error which is largely from the sqlalchemy and flask app-builder sites.

models.py


from flask_appbuilder import Model
from sqlalchemy import Column, Integer, Unicode, ForeignKey, Table
from sqlalchemy.orm import relationship

association_table = Table('association', Model.metadata, Column('left_id', Integer, ForeignKey('left.id')), Column('right_id', Integer, ForeignKey('right.id')) )

class Person(Model):

__tablename__ = 'person'
id = Column(Integer, primary_key=True)
name = Column(Unicode(32), nullable=False)
type_ = Column(Unicode(32), nullable=False, index=True)

__mapper_args__ = {
    'polymorphic_identity': 'person',
    'polymorphic_on': type_
}

def __repr__(self):
    return "{0}".format(self.name)

class Parent(Person): tablename = 'left' id = Column(Integer, ForeignKey('person.id'), primary_key=True) children = relationship( "Child", secondary=association_table, back_populates="parents")

__mapper_args__ = {
    'polymorphic_identity': 'parent',
}

class Child(Person): tablename = 'right' id = Column(Integer, ForeignKey('person.id'), primary_key=True) parents = relationship( "Parent", secondary=association_table, back_populates="children")

__mapper_args__ = {
    'polymorphic_identity': 'child',
}
> forms.py
```python3

from wtforms import StringField, SelectField
from wtforms.validators import DataRequired
from flask_appbuilder.fieldwidgets import (BS3TextFieldWidget,
                                           Select2Widget,
                                           Select2ManyWidget)
from flask_appbuilder.forms import DynamicForm

class MyForm(DynamicForm):

    field1 = StringField(('Field1'),
                         description=('Your field number one!'),
                         widget=BS3TextFieldWidget(),
                         validators=[DataRequired()]
                         )
    field2 = SelectField(('Field2'),
                         description=('Your field number two!'),
                         widget=Select2Widget(),
                         )
    field3 = SelectField(('Field3'),
                         description=('Your field number three!'),
                         widget=Select2ManyWidget(),
                         )

views.py


from flask import render_template, flash
from flask_appbuilder.models.sqla.interface import SQLAInterface
from flask_appbuilder import ModelView, MasterDetailView, SimpleFormView
from app import appbuilder, db

from .models import Parent, Child from .forms import MyForm

@appbuilder.app.errorhandler(404) def page_not_found(e): return render_template('404.html', base_template=appbuilder.base_template, appbuilder=appbuilder), 404

class ParentModelView(ModelView): datamodel = SQLAInterface(Parent) list_columns = ['name', 'children'] show_columns = list_columns edit_columns = list_columns

edit_exclude_columns = ['type_']
add_exclude_columns = edit_exclude_columns

class ChildModelView(ModelView): datamodel = SQLAInterface(Child) list_columns = ['name', 'parents'] show_columns = list_columns edit_columns = list_columns

edit_exclude_columns = ['type_']
add_exclude_columns = edit_exclude_columns

class ParentRelatedModelView(ModelView): datamodel = SQLAInterface(Parent) related_views = [ChildModelView] list_columns = ['name', 'children'] show_columns = list_columns edit_columns = list_columns

edit_exclude_columns = ['type_']
add_exclude_columns = edit_exclude_columns

show_template = 'appbuilder/general/model/show_cascade.html'
edit_template = 'appbuilder/general/model/edit_cascade.html'

class ChildRelatedModelView(ModelView): datamodel = SQLAInterface(Child) related_views = [ParentModelView] list_columns = ['name', 'parents'] show_columns = list_columns edit_columns = list_columns

edit_exclude_columns = ['type_']
add_exclude_columns = edit_exclude_columns

show_template = 'appbuilder/general/model/show_cascade.html'
edit_template = 'appbuilder/general/model/edit_cascade.html'

class MasterGroupChildView(MasterDetailView): datamodel = SQLAInterface(Parent) related_views = [ChildModelView]

list_columns = ['name']

class MyFormView(SimpleFormView):

form = MyForm
form_title = 'This is my first form view'
message = 'My form submitted'
session = appbuilder.get_session()

def form_get(self, form):
    form.field1.data = 'This was prefilled'
    form.field2.choices = appbuilder.get_session().query(Child.id,
                                                         Child.name).all()
    form.field3.choices = appbuilder.get_session().query(Parent.id,
                                                         Parent.name).all()

def form_post(self, form):
    # post process form
    flash(self.message, 'info')

db.create_all()

appbuilder.add_view(MyFormView, "MyFormView", icon="fa-search", label='Form', category="Form", category_icon="fa-group")

appbuilder.add_view(ParentModelView, "ParentModelView", icon="fa-folder-open-o", category="Category", category_icon='fa-envelope') appbuilder.add_view(ChildModelView, "ChildModelView", icon="fa-folder-open-o", category="Category", category_icon='fa-envelope') appbuilder.add_separator("Category") appbuilder.add_view(ParentRelatedModelView, "ParentRelatedModelView", icon="fa-cogs", category="Category", category_icon='fa-envelope') appbuilder.add_view(ChildRelatedModelView, "ChildRelatedModelView", icon="fa-cogs", category="Category", category_icon='fa-envelope') appbuilder.add_separator("Category") appbuilder.add_view(MasterGroupChildView, "MasterGroupChildView", icon="fa-cogs", category="Category", category_icon='fa-envelope')

> testdata.py
```python3

import logging
from app import appbuilder
from app.models import Child, Parent

import random

from sqlalchemy import exc

log = logging.getLogger(__name__)

def populate():
    children = ['Child {0}'.format(n) for n in range(30)]
    parents = ['Parent {0}'.format(n) for n in range(10)]

    try:
        session = appbuilder.get_session()
        session.add_all([Child(name=c) for c in children])
        session.add_all([Parent(name=p) for p in parents])

        session.commit()

        parents = session.query(Parent).all()
        children = session.query(Child).all()

        for c in children:
            c.parents = random.sample(parents, k=random.choice([1, 2]))
            session.commit()
    except exc.IntegrityError:
        session.rollback()

populate()

Describe the actual results

When the form is loaded the choices are populated as expected. Navigating the form and selecting/filling as expected. When save is pressed there is an exception in the form pre_validation.

Traceback (most recent call last):
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/app.py", line 2309, in __call__
    return self.wsgi_app(environ, start_response)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/app.py", line 2295, in wsgi_app
    response = self.handle_exception(e)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/app.py", line 1741, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/app.py", line 2292, in wsgi_app
    response = self.full_dispatch_request()
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/app.py", line 1815, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/app.py", line 1718, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/_compat.py", line 35, in reraise
    raise value
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/app.py", line 1813, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask/app.py", line 1799, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask_appbuilder/security/decorators.py", line 26, in wraps
    return f(self, *args, **kwargs)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask_appbuilder/views.py", line 75, in this_form_post
    if form.validate_on_submit():
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/flask_wtf/form.py", line 101, in validate_on_submit
    return self.is_submitted() and self.validate()
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/wtforms/form.py", line 310, in validate
    return super(Form, self).validate(extra)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/wtforms/form.py", line 152, in validate
    if not field.validate(self, extra):
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/wtforms/fields/core.py", line 195, in validate
    self.pre_validate(form)
  File "/home/kforrester/anaconda3/lib/python3.6/site-packages/wtforms/fields/core.py", line 471, in pre_validate
    for v, _ in self.choices:
TypeError: 'NoneType' object is not iterable

Steps to reproduce

Create a typical FAB project (+admin +testdata). Navigate to the form and populate the fields followed by press Save button.

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. Feel free to reopen it if it's still relevant to you. Thank you