My project defines a sqlalchemy_utils.EmailType column and while it's listed as a column in the list view, it's not displayed as an editable field in the form view.
To fix this, I had to manually override scaffold_form() to include the field as a TextField, similar to the snippet in http://stackoverflow.com/a/13551851.
This took me a while to debug and work around, so I'm documenting it here in case anyone else runs into it.
I'm not really sure what an appropriate "fix" for this might be, if any exists, as I'm very new to flask-admin and not even very familiar with sqlalchemy-utils or WTForms. I guess it'd be nice if there were some logging output from flask-admin that might help developers understand why fields might not be showing up, and documentation on ways to work around it?
You can just add it to a model converter - https://github.com/flask-admin/flask-admin/blob/master/flask_admin/contrib/sqla/form.py#L273
Feel free to send pull request.
Hi, did this ever make it in? I'm having the same trouble with ChoiceType.
After making it work with:
# https://stackoverflow.com/a/13551851/450917
excluded_list_columns = ('type',)
def scaffold_form(self):
form_class = super().scaffold_form()
form_class.the_fieldname = Select2Field('the_fieldname', choices=TheModel.THE_ENUM)
return form_class
That seemed a bit clumsy, so replaced it with this I聽found in another answer:
form_choices = { 'type': TheModel.THE_ENUM }
Would be cool if it just worked though, so I didn't have to hook them up manually. :D
Btw, couldn't find form_choices in the docs.
Also, found that the ChoiceType keeps its internal enum object at self.choices:
(http://sqlalchemy-utils.readthedocs.io/en/latest/_modules/sqlalchemy_utils/types/choice.html#ChoiceType).
Same trouble with ChoiceType, I used a solution based on flask-admin-utils because I wanted a SelectField instead of a Select2Field:
class SelectForChoiceTypeField(SelectField):
def process_data(self, value):
if value is None:
self.data = None
else:
try:
if isinstance(value, Choice):
self.data = self.coerce(value.code)
else:
self.data = self.coerce(value)
except (ValueError, TypeError):
self.data = None
class YourModelView(ModelView):
...
form_overrides = {
'the_fieldname': SelectForChoiceTypeField,
}
form_args = {
'the_fieldname': {
'choices': TheModel.THE_ENUM,
},
}
...
This strategy worked for me. Note, I implemented my own ChoiceType.
from sqlalchemy.types import TypeDecorator
from sqlalchemy import Column, String
from sqlalchemy.types import TypeDecorator
from sqlalchemy_utils import EmailType, EncryptedType, URLType
from flask_admin.contrib.sqla.form import AdminModelConverter, validators
from wtforms import SelectField, StringField, ValidationError
class ChoiceType(TypeDecorator):
impl = String
def __init__(self, choices, **kw):
if len(choices) == 0:
raise ValueError("No choices provided!")
if isinstance(choices, list) or isinstance(choices, tuple):
if isinstance(choices[0], str):
choices = [(s,s) for s in choices]
self.choices = OrderedDict(choices)
num_choices = len(self.choices)
assert num_choices == len(set(self.choices.keys())), "Choice keys must be unique"
assert num_choices == len(set(self.choices.values())), "Choice values must be unique"
self.choices_rev = {v: k for k, v in self.choices.items()}
super(ChoiceType, self).__init__(**kw)
def process_bind_param(self, value, dialect):
if value in self.choices_rev:
return self.choices_rev[value]
if value in self.choices:
return value
raise KeyError("Value not found in choices: %s" % value)
def process_result_value(self, value, dialect):
return self.choices[value]
class CustomConverter(AdminModelConverter):
def __init__(self, *args, **kwargs):
super(CustomConverter, self).__init__(*args, **kwargs)
def get_converter(self, column):
if isinstance(column.type, EmailType):
return self.conv_email
elif isinstance(column.type, ChoiceType):
return self.conv_choice
elif isinstance(column.type, EncryptedType):
return self.get_converter(Column(String))
elif isinstance(column.type, URLType):
return self.conv_url
else:
return super(CustomConverter, self).get_converter(column)
def conv_email(self, column, field_args, **extra):
field_args["validators"].append(validators.Email())
return StringField(**field_args)
def conv_url(self, column, field_args, **extra):
field_args["validators"].append(validators.URL())
return StringField(**field_args)
def conv_choice(self, column, field_args, **extra):
field = column.name
choices = [(k, v) for k, v in column.type.choices.items()]
return SelectField(field, choices=choices)
class MyModelView(ModelView):
model_form_converter = CustomConverter
Most helpful comment
Hi, did this ever make it in? I'm having the same trouble with ChoiceType.
After making it work with:
That seemed a bit clumsy, so replaced it with this I聽found in another answer:
Would be cool if it just worked though, so I didn't have to hook them up manually. :D
Btw, couldn't find
form_choicesin the docs.Also, found that the ChoiceType keeps its internal enum object at self.choices:
(http://sqlalchemy-utils.readthedocs.io/en/latest/_modules/sqlalchemy_utils/types/choice.html#ChoiceType).