Flask-admin: Flask-admin doesn't know what to do with sqlalchemy_utils.EmailType columns

Created on 10 Dec 2015  路  4Comments  路  Source: flask-admin/flask-admin

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?

Most helpful comment

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).

All 4 comments

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
Was this page helpful?
0 / 5 - 0 ratings

Related issues

btseytlin picture btseytlin  路  4Comments

iurisilvio picture iurisilvio  路  3Comments

lucasvo picture lucasvo  路  4Comments

rrad0812 picture rrad0812  路  4Comments

nMustaki picture nMustaki  路  3Comments