我一直努力在flask-admin中实现的一个功能是当用户编辑表单时,一旦设置了字段1就限制字段2的值。
让我用文字举一个简化的例子(实际用例更复杂)。然后我将展示一个实现该示例的完整要点,减去“约束”功能。
假设我们有一个数据库,可以跟踪某些软件“配方”,以各种格式输出报告。我们的示例数据库的recipe
表有两个配方:“严肃报告”,“ASCII艺术”。
为了实现每个配方,我们从几种方法中选择一种。我们数据库的method
表有两种方法:“tabulate_results”,“pretty_print”。
每种方法都有参数。 methodarg
表有两个参数名称“tabulate_results”(“rows”,“display_total”)和两个参数“pretty_print”(“embellishment_character”,“lines_to_jump”)。
现在,对于每个食谱(“严肃报告”,“ASCII艺术”),我们需要提供各自方法的参数值(“tabulate_results”,“pretty_print”)。
对于每条记录,recipearg
表允许我们选择一个配方(即字段1,例如“严重报告”)和参数名称(即字段2)。问题是显示了所有可能的参数名称,而它们需要根据字段1的值进行约束。
我们可以实现哪种过滤/约束机制,一旦我们选择“严重报告”,我们知道我们将使用“tabulate_results”方法,以便只有“rows”和“display_total”参数可用?
我正在考虑一些AJAX魔法,它会检查字段1并为字段2值设置查询,但不知道如何继续。
您可以通过使用要点来看到这一点:点击Recipe Arg
标签。在第一行(“严重报告”)中,如果您尝试通过单击它来编辑“Methodarg”值,则所有四个参数名称都可用,而不是仅两个。
# 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)
答案 0 :(得分:7)
我看到了解决这个问题的两种方法:
1-当Flask-Admin生成表单时,在data
选择的每个mid
标记中添加methodArg
个option
methodArg
个属性。然后让一些JS代码根据所选的配方过滤option
标签。
修改强>
这是尝试在每个data-mid
上添加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
)。
在您的每个选项上获得data-mid
后,您可以继续仅对您的食谱中的change
进行约束并选择并显示methodarg option
匹配data-mid
。考虑到Flask-Admin使用select2
,您可能需要做一些JS调整(最简单的丑陋解决方案是清理窗口小部件并为触发的每个change
事件重新创建它)
总的来说,我觉得这个不如第二个解决方案强大。我保留了monkeypatch,以明确这不应该用于生产imho。 (第二种解决方案稍微不那么具有侵入性)
2-使用Flask-Admin中支持的ajax-completion来根据所选配方破解你想要的选项:
首先,创建一个自定义AjaxModelLoader,负责对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]的字符串。方式,你会保持自动完成)
data: function(term, page) {
return {
query: $('#recipe').val(),
offset: (page - 1) * 10,
limit: 10
};
},
此代码段取自Flask-Admin的form.js
[1]
显然,这需要一些调整和参数化(因为做这样一个hacky解决方案会阻止你在你的应用程序管理员的其他部分使用其他ajax填充的选择+ form.js
上的更新直接就像升级Flask-Admin
非常麻烦)
总的来说,我对这两个解决方案都不满意,并且这个展示说,无论何时你想要走出框架/工具的轨道,你都会陷入复杂的死胡同。对于愿意向Flask-Admin上游提供真实解决方案的人来说,这可能是一个有趣的功能请求/项目。