kvesteri / wtforms-alchemy

Tools for creating wtforms from sqlalchemy models
Other
244 stars 58 forks source link

enum TypeError: object of type 'enumVar' has no len() #162

Open adorsett opened 1 year ago

adorsett commented 1 year ago

Using Flask, SQLAlchemy, WTForms against an MySQL DB.

Imports

from flask import Flask, render_template, request, url_for, flash, redirect
from waitress import serve
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms_alchemy import model_form_factory, ModelFieldList
from wtforms.fields import FormField, SelectField
from sqlalchemy import Column, Integer, String, ForeignKey, Date, Enum, Table
from sqlalchemy.orm import declarative_base, relationship, sessionmaker, aliased, backref, Query
import enum

Have an Enumerated Type Field that's stored in MySQL as a native Enum Column

class Category(enum.Enum):
    Cat1 = 'Cat1'
    Cat2 = 'Cat2'
    Cat3 = 'Cat3'

Model Definition

class Label(Base):
    __tablename__ = "labels"
    id = Column(Integer, primary_key=True)
    abbreviation = Column(String(10), nullable=False)
    label = Column(String(50), nullable=False)
    category = Column(Enum(Category))

Setting up Globals

db = SQLAlchemy(app)
Base = db.Model
BaseModelForm = model_form_factory(FlaskForm)

class ModelForm(BaseModelForm):
    @classmethod
    def get_session(self):
        return db.session

Use ModelForm to Auto-Build Form

class LabelForm(ModelForm):
    class Meta:
        model = Label
        strip_string_fields = True
        include_foreign_keys = True

Flask Handler Snippet

    label = Label()
    success = False

    if request.method == 'POST':
        form = LabelForm(request.form, obj=label)
        print(request.form)
        if form.validate():
            form.populate_obj(label)
            db.session.add(label)
            db.session.commit()
            success = True
            flash("Label Saved", 'notice')
        else:
            flash(f"Form Failed Validation: {form.errors}", 'error')

The Form in the HTML Template

    <form method="post">
        {{ form.hidden_tag() }}
        {% for field in form %}
          {% if field.widget.input_type != 'hidden' %} {{ field.label }} {% endif %}
          {{ field }}
        {% endfor %}

        <input type="submit" value="Save">
        <input type="reset" value="Reset">

The Form will show perfectly using the above template when selecting a pre-existing DB entry by ID. If I try to "Edit" that entry by simply clicking "Save" I get the following traceback.

adminui  | Traceback (most recent call last):
adminui  |   File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 2528, in wsgi_app
adminui  |     response = self.full_dispatch_request()
adminui  |   File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1825, in full_dispatch_request
adminui  |     rv = self.handle_user_exception(e)
adminui  |   File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1823, in full_dispatch_request
adminui  |     rv = self.dispatch_request()
adminui  |   File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1799, in dispatch_request
adminui  |     return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
adminui  |   File "/src/src/app.py", line 246, in clearancedetail
adminui  |     if form.validate():
adminui  |   File "/usr/local/lib/python3.8/site-packages/wtforms/form.py", line 329, in validate
adminui  |     return super().validate(extra)
adminui  |   File "/usr/local/lib/python3.8/site-packages/wtforms/form.py", line 146, in validate
adminui  |     if not field.validate(self, extra):
adminui  |   File "/usr/local/lib/python3.8/site-packages/wtforms/fields/core.py", line 242, in validate
adminui  |     stop_validation = self._run_validation_chain(form, chain)
adminui  |   File "/usr/local/lib/python3.8/site-packages/wtforms/fields/core.py", line 262, in _run_validation_chain
adminui  |     validator(form, self)
adminui  |   File "/usr/local/lib/python3.8/site-packages/wtforms/validators.py", line 138, in __call__
adminui  |     length = field.data and len(field.data) or 0
adminui  | TypeError: object of type 'Category' has no len()

I added a len() method to the Enum object and that cleared the Traceback but then I get a data validation error when calling form.validate().

This works but I want my Enum definitions stored in a central place and not in the middle of my Model definition:

class Label(Base):
    # ...
    category = Column(Enum("Cat1", "Cat2", "Cat3", name="Category", default="Cat1"))
adorsett commented 1 year ago

This workaround worked. https://stackoverflow.com/a/51858425/15231085

Once I defined enum_field_options() I had to manually add the field using SelectField. category = SelectField("Category", **enum_field_options(Category))