flask-admin 表单:根据字段 1 的值约束字段 2 的值 [英] flask-admin form: Constrain Value of Field 2 depending on Value of Field 1
问题描述
我一直在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<的每个
methodArg
的 mid
中添加 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屋!