Flask-Admin - How can I create a custom InlineModelForm that generates a dynamic field per related db record

Viewed 207

I'm trying to create dynamic inline fields based on a model (Parameter) and populate the corresponding dynamically-created field with the value from the related ParameterValue model.

Where I'm stuck is I don't know how to intercept the ParameterValue instance during the inline form construction to only create a single field based on the values in the ParameterValue.parameter relationship ([name, type, display_name, is_required, help_text])

Parameter.name is the variable name for the form field
Parameter.type determines what type of field it should be
Parameter.display_name is the label used on the form
Parameter.is_required determines if the required property should be added to the field
Parameter.help_text is passed as the "description" kwarg and rendered below the input

With my current code, each of the inline form fields have the columns of the ParameterValue model (the typical behavior of an inline form field).

Can anyone provide some insight on how to make the below MWE code produce 2 fields (one string field and one boolean field) based on the Account.reporter_fields relationship?

MVE

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_admin.model.form import InlineFormAdmin
from flask_admin.form.fields import Select2TagsField
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.ext.mutable import MutableList
from wtforms.fields import BooleanField, IntegerField, StringField
from wtforms.validators import Optional, Required

app = Flask("StackOverflow MVE")
app.config.update({
    "SECRET_KEY": "NOTASECRET",
    "SQLALCHEMY_DATABASE_URI": "sqlite:///:memory:",
    "SQLALCHEMY_TRACK_MODIFICATIONS": False
})
db = SQLAlchemy(app)


FIELD_TYPES = {
    "str": StringField,
    "int": IntegerField,
    "bool": BooleanField,
    "list": Select2TagsField
}


def generate_field(parameter):
    """Generates a dynamic field based on properties stored in the parameter table.

    :param parameter: The :class:`app.models.Parameter` used to derive field properties.
    :return: A form field based on the properties of ``parameter``.
    """
    field = FIELD_TYPES[parameter.type]
    kwargs = dict(
        label=parameter.display_name,
        description=parameter.help_text or "",
        validators=[Required() if parameter.is_required else Optional()],
        filters=[lambda value: value or None]
    )

    if parameter.type == "bool":
        del kwargs["validators"]
        del kwargs["filters"]
        kwargs["default"] = parameter.is_required
    elif parameter.type == "list":
        del kwargs["filters"]

    return field(**kwargs)


class ParameterValue(db.Model):
    """Assigns a value to a parameter and assigns the parameter to an entity."""
    __tablename__ = "parameter_value"
    id = db.Column(db.Integer, primary_key=True)
    parameter_id = db.Column(db.Integer, db.ForeignKey("parameter.id"), nullable=False)
    account_id = db.Column(db.Integer, db.ForeignKey("account.id"), nullable=False)
    str_value = db.Column(db.String(100), nullable=True)
    int_value = db.Column(db.Integer, nullable=True)
    bool_value = db.Column(db.Boolean, nullable=True)
    list_value = db.Column(MutableList.as_mutable(db.JSON), nullable=True)

    def __init__(self, **kwargs):
        """Explicit __init__ added to accommodate hybrid_property."""
        self.parameter = kwargs.pop("parameter")
        for kwarg, value in kwargs.items():
            setattr(self, kwarg, value)

    @hybrid_property
    def value(self):
        return getattr(self, f"{self.parameter.type}_value")

    @value.setter
    def value(self, new_value):
        setattr(self, f"{self.parameter.type}_value", new_value)

    @value.deleter
    def value(self):
        setattr(self, f"{self.parameter.type}_value", None)


class Parameter(db.Model):
    """Defines the properties of a dynamic parameter."""
    __tablename__ = "parameter"
    id = db.Column(db.Integer, primary_key=True)
    reporter_id = db.Column(db.Integer, db.ForeignKey("reporter.id"), nullable=False)
    name = db.Column(db.String(45), nullable=False)
    type = db.Column(
        db.Enum("str", "int", "bool", "list", name="parameter_type_enum"),
        nullable=False,
        default="str"
    )
    display_name = db.Column(db.String(100), nullable=True, default=None)
    is_required = db.Column(db.Boolean, default=False)
    help_text = db.Column(db.String(100), nullable=True, default=None)

    values = db.relationship("ParameterValue", backref="parameter")


class Reporter(db.Model):
    """The parent entity."""
    __tablename__ = "reporter"
    id = db.Column(db.Integer, primary_key=True)
    module = db.Column(db.String(50), nullable=False)
    parameters = db.relationship("Parameter", backref="reporter", lazy="dynamic", cascade="all, delete")
    accounts = db.relationship("Account", backref="reporter", cascade="all, delete")


class Account(db.Model):
    __tablename__ = "account"
    id = db.Column(db.Integer, primary_key=True)
    reporter_id = db.Column(db.Integer, db.ForeignKey("reporter.id"), nullable=False)
    client_name = db.Column(db.String(50), nullable=True)
    username = db.Column(db.String(100), nullable=True)
    reporter_fields = db.relationship("ParameterValue", lazy="dynamic", backref="account")


class ParameterInlineForm(InlineFormAdmin):
    """Kind of stuck here.

       How do I intercept the incoming record and construct
       a single dynamic field based on
       `ParameterValue.parameter.[name, type, display_name, is_required, help_text]`?

       `Parameter.name` is the variable name for the form field
       `Parameter.type` determines what type of field it should be
       `Parameter.display_name` is what is label used on the form
       `Parameter.is_required` determines if the required property should be added to the field
       `Parameter.help_text` is passed as the "description" kwarg and rendered below the input
    """


class AccountView(ModelView):
    """
    I'm trying to create a single field per associated ParameterValue.
    """
    column_list = ("client_name", "username")
    form_columns = ("client_name", "username", "reporter_fields")
    inline_models = (ParameterInlineForm(ParameterValue),)


@app.before_first_request
def startup():
    db.create_all()
    rep = Reporter(module="test")
    param_str = Parameter(name="string", type="str", display_name="String Field", is_required=True,
                          help_text="Enter a string", reporter=rep)
    param_int = Parameter(name="integer", type="int", display_name="Integer Field", reporter=rep)
    param_bool = Parameter(name="boolean", type="bool", display_name="Boolean Field", reporter=rep)
    param_list = Parameter(name="alist", type="list", display_name="Tag Field", reporter=rep)
    act = Account(
        client_name="John Doe",
        username="jd123",
        reporter=rep,
        reporter_fields=[
            ParameterValue(parameter=param_str, value="I'm a string"),
            ParameterValue(parameter=param_bool, value=True)
        ]
    )
    db.session.add_all([rep, param_str, param_int, param_bool, param_list, act])
    db.session.commit()


admin = Admin(app, url="/", template_mode="bootstrap4")
admin.add_view(AccountView(Account, db.session, name="Accounts", endpoint="accounts"))


if __name__ == "__main__":
    app.run(debug=True)
0 Answers
Related