flask-admin 表单:根据字段 1 的值约束字段 2 的值 [英] flask-admin form: Constrain Value of Field 2 depending on Value of Field 1

查看:41
本文介绍了flask-admin 表单:根据字段 1 的值约束字段 2 的值的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在flask-admin 中努力实现的一个功能是当用户编辑表单时,一旦设置了字段1,就限制字段2 的值.

One feature I have been struggling to implement in flask-admin is when the user edits a form, to constrain the value of Field 2 once Field 1 has been set.

让我用文字举一个简单的例子(实际用例更复杂).然后我将展示一个实现该示例的完整要点,减去约束"功能.

Let me give a simplified example in words (the actual use case is more convoluted). Then I will show a full gist that implements that example, minus the "constrain" feature.

假设我们有一个数据库,可以跟踪一些软件配方"以输出各种格式的报告.我们示例数据库的recipe表有两个recipe:Serious Report"、ASCII Art".

Let's say we have a database that tracks some software "recipes" to output reports in various formats. The recipe table of our sample database has two recipes: "Serious Report", "ASCII Art".

为了实现每个配方,我们从几种方法中选择一种.我们数据库的method表有两个方法:tabulate_results",pretty_print".

To implement each recipe, we choose one among several methods. The method table of our database has two methods: "tabulate_results", "pretty_print".

每个方法都有参数.methodarg 表有两个tabulate_results"参数名称(rows"、display_total")和两个pretty_print"参数名称(embellishment_character"、lines_to_jump").

Each method has parameters. The methodarg table has two parameter names for "tabulate_results" ("rows", "display_total") and two parameters for "pretty_print" ("embellishment_character", "lines_to_jump").

现在对于每个食谱(Serious Report"、ASCII Art"),我们需要提供它们各自方法(tabulate_results"、pretty_print")的参数值.

Now for each of the recipes ("Serious Report", "ASCII Art") we need to provide the value of the arguments of their respective methods ("tabulate_results", "pretty_print").

对于每条记录,recipearg 表让我们可以选择一个配方(即字段 1,例如严重报告")和参数名称(即字段 2).问题是显示了所有可能的参数名称,而它们需要根据字段 1 的值进行约束.

For each record, the recipearg table lets us select a recipe (that's Field 1, for instance "Serious Report") and an argument name (that's Field 2). The problem is that all possible argument names are shown, whereas they need to be constrained based on the value of Field 1.

我们可以实施什么过滤/约束机制,以便一旦我们选择严重报告",我们就知道我们将使用tabulate_results"方法,因此只有rows"和display_total"参数可用?

What filtering / constraining mechanism can we implement such that once we select "Serious Report", we know we will be using the "tabulate_results" method, so that only the "rows" and "display_total" arguments are available?

我正在考虑使用一些 AJAX 魔法来检查字段 1 并设置字段 2 值的查询,但不知道如何继续.

I'm thinking some AJAX wizardry that checks Field 1 and sets a query for Field 2 values, but have no idea how to proceed.

您可以通过玩弄要点来看到这一点:单击Recipe Arg 选项卡.在第一行(严重报告")中,如果您尝试通过单击来编辑Methodarg"值,则所有四个参数名称都可用,而不仅仅是两个.

You can see this by playing with the gist: click on the Recipe Arg tab. In the first row ("Serious Report"), if you try to edit the "Methodarg" value by clicking on it, all four argument names are available, instead of just two.

# full gist: please run this

from flask import Flask
from flask_admin import Admin
from flask_admin.contrib import sqla
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship

# Create application
app = Flask(__name__)

# Create dummy secrey key so we can use sessions
app.config['SECRET_KEY'] = '123456790'

app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///a_sample_database.sqlite'
app.config['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)

# Create admin app
admin = Admin(app, name="Constrain Values", template_mode='bootstrap3')

# Flask views
@app.route('/')
def index():
    return '<a href="/admin/">Click me to get to Admin!</a>'


class Method(db.Model):
    __tablename__ = 'method'
    mid = Column(Integer, primary_key=True)
    method = Column(String(20), nullable=False, unique=True)
    methodarg = relationship('MethodArg', backref='method')
    recipe = relationship('Recipe', backref='method')


    def __str__(self):
        return self.method


class MethodArg(db.Model):
    __tablename__ = 'methodarg'
    maid = Column(Integer, primary_key=True)
    mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
    methodarg = Column(String(20), nullable=False, unique=True)
    recipearg = relationship('RecipeArg', backref='methodarg')
    inline_models = (Method,)


    def __str__(self):
        return self.methodarg


class Recipe(db.Model):
    __tablename__ = 'recipe'
    rid = Column(Integer, primary_key=True)
    mid = Column(ForeignKey('method.mid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
    recipe = Column(String(20), nullable=False, index=True)
    recipearg = relationship('RecipeArg', backref='recipe')
    inline_models = (Method,)

    def __str__(self):
        return self.recipe


class RecipeArg(db.Model):
    __tablename__ = 'recipearg'

    raid = Column(Integer, primary_key=True)
    rid = Column(ForeignKey('recipe.rid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
    maid = Column(ForeignKey('methodarg.maid', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
    strvalue = Column(String(80), nullable=False)
    inline_models = (Recipe, MethodArg)


    def __str__(self):
        return self.strvalue


class MethodArgAdmin(sqla.ModelView):
    column_list = ('method', 'methodarg')
    column_editable_list = column_list



class RecipeAdmin(sqla.ModelView):
    column_list = ('recipe', 'method')
    column_editable_list = column_list



class RecipeArgAdmin(sqla.ModelView):
    column_list = ('recipe', 'methodarg', 'strvalue')
    column_editable_list = column_list


admin.add_view(RecipeArgAdmin(RecipeArg, db.session))

# More submenu
admin.add_view(sqla.ModelView(Method, db.session, category='See Other Tables'))
admin.add_view(MethodArgAdmin(MethodArg, db.session, category='See Other Tables'))
admin.add_view(RecipeAdmin(Recipe, db.session, category='See Other Tables'))


if __name__ == '__main__':

    db.drop_all()
    db.create_all()
    db.session.add(Method(mid=1, method='tabulate_results'))
    db.session.add(Method(mid=2, method='pretty_print'))
    db.session.commit()
    db.session.add(MethodArg(maid=1, mid=1, methodarg='rows'))
    db.session.add(MethodArg(maid=2, mid=1, methodarg='display_total'))
    db.session.add(MethodArg(maid=3, mid=2, methodarg='embellishment_character'))
    db.session.add(MethodArg(maid=4, mid=2, methodarg='lines_to_jump'))
    db.session.add(Recipe(rid=1, mid=1, recipe='Serious Report'))
    db.session.add(Recipe(rid=2, mid=2, recipe='ASCII Art'))
    db.session.commit()
    db.session.add(RecipeArg(raid=1, rid=1, maid=2, strvalue='true' ))
    db.session.add(RecipeArg(raid=2, rid=1, maid=1, strvalue='12' ))
    db.session.add(RecipeArg(raid=3, rid=2, maid=4, strvalue='3' ))
    db.session.commit()

    # Start app
    app.run(debug=True)

推荐答案

我认为有两种方法可以解决这个问题:

I see two ways of tacking this problem:

1- 当 Flask-Admin 生成表单时,在每个 option<的每个 methodArgmid 中添加 data 属性methodArg 选择中的/code> 标记.然后让一些 JS 代码根据选择的配方过滤 option 标签.

1- When Flask-Admin generate the form, add data attributes with the mid of each methodArg on each option tag in the methodArg select. Then have some JS code filter the option tags based on the recipe selected.

编辑

这是在每个 option 上放置 data-mid 属性的尝试性尝试:

Here is a tentative try at putting a data-mid attribute on each option:

def monkeypatched_call(self, field, **kwargs):
    kwargs.setdefault('id', field.id)
    if self.multiple:
        kwargs['multiple'] = True
    html = ['<select %s>' % html_params(name=field.name, **kwargs)]
    for (val, label, selected), (_, methodarg) in zip(field.iter_choices(), field._get_object_list()):
        html.append(self.render_option(val, label, selected, **{'data-mid': methodarg.mid}))
    html.append('</select>')
    return HTMLString(''.join(html))

Select.__call__ = monkeypatched_call

阻碍在于这些渲染调用是从 jinja 模板触发的,因此您几乎无法更新小部件(Select 是 WTForms 中最底层的,并且是用作 Flask-Admin 的 Select2Field 的基础.

The blocker is in the fact that those render calls are triggered from the jinja templates, so you are pretty much stuck updating a widget (Select being the most low-level one in WTForms, and is used as a base for Flask-Admin's Select2Field).

在获得每个选项的 data-mid 后,您可以继续在配方的选择上绑定一个 change 并显示 methodarg 的 option 具有匹配的 data-mid.考虑到 Flask-Admin 使用 select2,你可能需要做一些 JS 调整(最简单丑陋的解决方案是清理小部件并为每个触发的 change 事件重新创建它)

After getting those data-mid on each of your options, you can proceed with just binding an change on your recipe's select and display the methodarg's option that have a matching data-mid. Considering Flask-Admin uses select2, you might have to do some JS tweaking (easiest ugly solution would be to clean up the widget and re-create it for each change event triggered)

总的来说,我发现这个解决方案不如第二个解决方案强大.我保留了monkeypatch 以明确这不应该在生产中使用恕我直言.(第二种解决方案的侵入性稍低)

Overall, I find this one less robust than the second solution. I kept the monkeypatch to make it clear this should not be used in production imho. (the second solution is slightly less intrusive)

2- 使用 Flask-Admin 中支持的 ajax-completion 来破解根据所选配方获取所需选项的方法:

2- Use the supported ajax-completion in Flask-Admin to hack your way into getting the options that you want based on the selected recipe:

首先,创建一个自定义的 AjaxModelLoader,它将负责对数据库执行正确的选择查询:

First, create a custom AjaxModelLoader that will be responsible for executing the right selection query to the DB:

class MethodArgAjaxModelLoader(sqla.ajax.QueryAjaxModelLoader):
    def get_list(self, term, offset=0, limit=10):
        query = self.session.query(self.model).filter_by(mid=term)
        return query.offset(offset).limit(limit).all()

class RecipeArgAdmin(sqla.ModelView):
    column_list = ('recipe', 'methodarg', 'strvalue')
    form_ajax_refs = {
        'methodarg': MethodArgAjaxModelLoader('methodarg', db.session, MethodArg, fields=['methodarg'])
    }
    column_editable_list = column_list

然后,更新 Flask-Admin 的 form.js 以让浏览器向您发送配方信息,而不是需要自动完成的 methodArg 名称.(或者你可以在 query 中发送两者并在你的 AjaxLoader 中做一些 arg 解析,因为 Flask-Admin 不会在 query 上做任何解析,期望它是一个我想的字符串 [0].这样,你会保持自动完成)

Then, update Flask-Admin's form.js to get the browser to send you the recipe information instead of the methodArg name that needs to be autocompleted. (or you could send both in query and do some arg parsing in your AjaxLoader since Flask-Admin does no parsing whatsoever on query, expecting it to be a string I suppose [0]. That way, you would keep the auto-completion)

data: function(term, page) {
    return {
        query: $('#recipe').val(),
        offset: (page - 1) * 10,
        limit: 10
    };
},

这个片段取自 Flask-Admin 的 form.js [1]

This snippet is taken from Flask-Admin's form.js [1]

显然,这需要一些调整和参数设置(因为这样做会阻止您在应用管理的其余部分中使用其他 ajax 填充的选择 + 直接在 form.js 上的更新那样会使升级 Flask-Admin 变得极其麻烦)

Obviously, this needs some tweaking and parametrising (because doing such a hacky solution would block you from using other ajax-populated select in the rest of your app admin + the update on form.js directly like that would make upgrading Flask-Admin extremely cumbersome)

总的来说,我对这两种解决方案都不满意,而且这个展示无论何时你想脱离框架/工具的轨道,你最终都可能陷入复杂的死胡同.不过,对于愿意为 Flask-Admin 上游提供真实解决方案的人来说,这可能是一个有趣的功能请求/项目.

Overall, I am unsatisfied with both solutions and this showcase that whenever you want to go out of the tracks of a framework / tool, you can end up in complex dead ends. This might be an interesting feature request / project for someone willing to contribute a real solution upstream to Flask-Admin though.

这篇关于flask-admin 表单:根据字段 1 的值约束字段 2 的值的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆