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.68k stars 1.36k forks source link

ModelRestApi failed when relationship model contain Enum fields #1325

Open foxluqi opened 4 years ago

foxluqi commented 4 years ago

Environment

Flask-Appbuilder version: Flask-AppBuilder 2.3.0 pip freeze output: alembic==1.3.2 apispec==1.3.3 apply-defaults==0.1.4 attrs==19.3.0 Babel==2.7.0 blinker==1.4 certifi==2019.11.28 chardet==3.0.4 click==6.7 colorama==0.4.1 defusedxml==0.6.0 Flask==1.1.1 Flask-AppBuilder==2.3.0 Flask-Babel==1.0.0 Flask-Cors==3.0.8 Flask-JWT-Extended==3.24.1 Flask-Login==0.4.1 Flask-Mail==0.9.1 Flask-Migrate==2.5.2 Flask-OpenID==1.2.5 Flask-SQLAlchemy==2.4.1 Flask-WTF==0.14.2 idna==2.8 importlib-metadata==0.23 itsdangerous==1.1.0 Jinja2==2.10.3 jsonschema==3.2.0 Mako==1.1.0 MarkupSafe==1.1.1 marshmallow==2.19.5 marshmallow-enum==1.5.1 marshmallow-sqlalchemy==0.19.0 more-itertools==7.2.0 prison==0.1.2 PyJWT==1.7.1 PyMySQL==0.9.3 pyrsistent==0.15.6 python-dateutil==2.8.1 python-editor==1.0.4 python3-openid==3.1.0 pytz==2019.3 PyYAML==5.1.2 requests==2.22.0 six==1.13.0 SQLAlchemy==1.3.11 SQLAlchemy-Utils==0.35.0 urllib3==1.25.8 Werkzeug==0.16.0 WTForms==2.2.1 zipp==0.6.0

Describe the expected results

Tell us what should happen.

expect normal JSON reply of the requested domain lists

Describe the actual results

Tell us what happens instead.

in console

2020-03-23 17:04:45,932:ERROR:root:Object of type StatusEnum is not JSON serializable Traceback (most recent call last): File "/Users/alexlu/Developer/YY_DW_MMS/venv/lib/python3.7/site-packages/flask_appbuilder/api/__init__.py", line 83, in wraps return f(self, *args, **kwargs) File "/Users/alexlu/Developer/YY_DW_MMS/venv/lib/python3.7/site-packages/flask_appbuilder/api/__init__.py", line 153, in wraps return f(self, *args, **kwargs) File "/Users/alexlu/Developer/YY_DW_MMS/venv/lib/python3.7/site-packages/flask_appbuilder/api/__init__.py", line 1468, in get_list return self.get_list_headless(**kwargs) File "/Users/alexlu/Developer/YY_DW_MMS/venv/lib/python3.7/site-packages/flask_appbuilder/api/__init__.py", line 1409, in get_list_headless return self.response(200, **_response) File "/Users/alexlu/Developer/YY_DW_MMS/venv/lib/python3.7/site-packages/flask_appbuilder/api/__init__.py", line 671, in response _ret_json = jsonify(kwargs) File "/Users/alexlu/Developer/YY_DW_MMS/venv/lib/python3.7/site-packages/flask/json/__init__.py", line 370, in jsonify dumps(data, indent=indent, separators=separators) + "\n", File "/Users/alexlu/Developer/YY_DW_MMS/venv/lib/python3.7/site-packages/flask/json/__init__.py", line 211, in dumps rv = _json.dumps(obj, **kwargs) File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/__init__.py", line 238, in dumps **kw).encode(obj) File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 201, in encode chunks = list(chunks) File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 431, in _iterencode yield from _iterencode_dict(o, _current_indent_level) File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 405, in _iterencode_dict yield from chunks File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 325, in _iterencode_list yield from chunks File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 405, in _iterencode_dict yield from chunks File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 405, in _iterencode_dict yield from chunks File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 438, in _iterencode o = _default(o) File "/Users/alexlu/Developer/YY_DW_MMS/venv/lib/python3.7/site-packages/flask/json/__init__.py", line 100, in default return _json.JSONEncoder.default(self, o) File "/usr/local/Cellar/python/3.7.4_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/encoder.py", line 179, in default raise TypeError(f'Object of type {o.__class__.__name__} ' TypeError: Object of type StatusEnum is not JSON serializable

Steps to reproduce

Model

## define a enum type. And use it in both models.
class StatusEnum(enum.Enum):
    inactive = _("Inactive")
    active = _("Active")

class Business(Model, AuditMixin):
    id = Column(Integer, primary_key=True)
    name = Column(String(50), unique=True, nullable=False)
    database = Column(String(50), nullable=False, default="default")
    status = Column(Enum(StatusEnum), nullable=False, info={"enum_class": StatusEnum}, default=StatusEnum.active)
    # field_categories = Column(TEXT, nullable=True)
    remark = Column(String(255), nullable=True)

class Domain(Model, AuditMixin):
    id = Column(Integer, primary_key=True)
    name = Column(String(50), nullable=False)
    business_id = Column(Integer, ForeignKey('business.id'), nullable=False)
    business = relationship('Business')
    status = Column(Enum(StatusEnum), nullable=False, info={"enum_class": StatusEnum}, default=StatusEnum.active)
    remark = Column(String(255), nullable=True)

API

## define the ModelRestApi
class DomainModelApi(ModelRestApi):
    resource_name = 'domain'
    datamodel = SQLAInterface(Domain)

appbuilder.add_api(DomainModelApi)

when request the Domain list thru rest api, then server return fatal error message

{
  "message": "Fatal error"
}
alexandre-zia-ifood commented 4 years ago

Same here, exactly the same scenario.

alexandre-zia-ifood commented 4 years ago

You could do this monkeypatch

from marshmallow_enum import EnumField

string_types = (str, )
def _deserialize_by_name(self, value, attr, data):
        if not isinstance(value, string_types):
            self.fail('must_be_string', input=value, name=value)
        if self.enum.name == 'statusenum':
            return StatusEnum[value]
        else:
            try:
                return getattr(self.enum, value)
            except AttributeError:
                self.fail('by_name', input=value, name=value)
EnumField._deserialize_by_name = _deserialize_by_name