flask-admin form:根据字段1的值限制字段2的值 [英] flask-admin form: Constrain Value of Field 2 depending on Value of Field 1

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

问题描述

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

假设我们有一个跟踪某些软件食谱的数据库来输出报告以各种格式。我们的示例数据库的配方表有两个配方:严重报告,ASCII艺术。

实施每个配方,我们选择几种方法之一。我们的数据库的方法表有两个方法:tabulate_results,pretty_print。

每个方法都有参数。 methodarg 表有两个tabulate_results(rows,display_total)参数名和两个pretty_print(embellishment_character,lines_to_jump)参数。现在对于每一个食谱(严重报告,ASCII艺术),我们需要提供它们各自方法的参数值(tabulate_results,对于每个记录, recipearg 表让我们选择一个配方(即字段1,例如严重报告)和参数名称(即字段2)。问题是显示所有可能的参数名称,而它们需要根据字段1的值进行约束。



我们可以实现哪种过滤/约束机制,一旦我们选择严重报告,我们知道我们将使用tabulate_results方法,所以只有rows和display_total参数是可用的?

我正在考虑一些AJAX巫术,检查字段1,并设置字段2值的查询,但不知道如何进行。



你可以通过玩要点:点击 Recipe Arg 标签。在第一行(严重报告)中,如果您尝试单击编辑Methodarg值,则所有四个参数名称都可用,而不是仅两个。

 #full gist:请运行这个

从flask导入Flask
from flask_admin import Admin $ b $ from flask_admin.contrib import sqla
from flask_sqlalchemy从sqlalchemy导入SQLAlchemy
从sqlalchemy.orm导入Column,ForeignKey,Integer和String
导入关系

#创建应用程序
app = Flask( __name__)

#创建虚拟秘密密钥,以便我们可以使用会话
app.config ['SECRET_KEY'] ='123456790'

app.config ['SQLALCHEMY_DATABASE_URI '] ='sqlite:///a_sample_database.sqlite'
app.config ['SQLALCHEMY_ECHO'] = True
db = SQLAlchemy(app)
$ b $创建admin app
admin = Admin(app,name =Constrain Values,template_mode ='bootstrap3')

#Flask views
@ app.route('/')
def index():
返回'< a href =/ admin />点击我进入管理员!< / a>'


类方法(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')

$ b def __str __(self):
返回self.method


类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 =(方法)

$ b def __str __(self):
返回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 =关系('RecipeArg '),
inline_models =(方法)
$ b $ def __str __(self):
return self.recipe


class RecipeArg(db.Model):
__tablename__ ='recipearg'
$ b raid = Column(Integer,primary_key = True)
rid = Column(ForeignKey('recipe。摆脱,ondelete = 'CASCADE',的onupdate = 'CASCADE'),可为空=假)
女佣=柱(ForeignKey的( 'methodarg.maid',ondelete = 'CASCADE',的onupdate = 'CASCADE'),可为空= False)
strvalue = Column(String(80),nullable = False)
inline_models =(Recipe,MethodArg)

$ b def __str __(self):
返回self.strvalue


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

#更多子菜单
admin.add_view(sqla.ModelView(Method,db.session,category ='See其他表')
admin.add_view(MethodArgAdmin(MethodArg,db.session,category ='See Other Tables'))
admin.add_view(RecipeAdmin(Recipe,db.session,category ='See其他表'))


if __name__ =='__main__':

db.drop_all()
db.create_all()
db.session.add(方法(mid = 1,method ='tabulate_results'))
db.session.add(Method(mid = 2,method ='pretty_print'))
db.session.commit()$ b $ (方法arg(女佣= 1,mid = 1,methodarg ='行'))
db.session.add(方法arg(女佣= 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 = 'recipe_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, )
db.session.commit()

#启动应用程序
app.run(debug = True)



1-当Flask-Admin生成表单时,添加<$ c每个 methodArg 上的$ c> data 属性与 mid methodArg 中选择c> option 标签。然后让一些JS代码根据所选的配方过滤选项标签。

编辑



下面试着在每个选项中加入 data-mid 属性

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

选择.__ call__ = monkeypatched_call

拦截器是这样的事实,那些渲染调用是从jinja模板触发的,所以你a (选择是WTForms中最低层的一个),并用作Flask-Admin的 Select2Field )。



在每个选项中获得 data-mid 继续在配方的选择上绑定 change ,并显示匹配选项 >数据中间。考虑到Flask-Admin使用 select2 ,你可能需要做一些JS调整(最简单的丑陋的解决方案是清理小部件并为每个<$ c $重新创建c>更改事件触发)

总的来说,我发现这个比第二个解决方案更不稳健。我保持monkeypatch明确这不应该用于生产恕我直言。 (第二个解决方案稍微少一点侵入性)



在Flask-Admin中使用支持的ajax-completion来破解你想要的选项,首先,创建一个自定义的AjaxModelLoader,负责执行对DB的正确的选择查询:


def get_list(self,term,offset = 0,limit = 10):
query = self。 session.query(self.model).filter_by(mid = term)
返回query.offset(offset).limit(limit).all()

类RecipeArgAdmin(sqla.ModelView) :
column_list中=( '食谱', 'methodarg', 'strValue中')
form_ajax_refs = {
'methodarg':MethodArgAjaxModelLoader( 'methodarg',db.session,methodArg,字段= [ 'methodarg'])
}
column_editable_list = column_list

然后,更新Flask -admin s form.js 来让浏览器发送配方信息,而不是需要自动完成的 methodArg 。 (或者你可以发送 query ),并在你的AjaxLoader中进行一些arg解析,因为Flask-Admin不会解析查询,期待它是一个字符串我想 [b]
$ b $ $ $ $ $ $ $ $ c $ data>:function(term,page) ){
return {
query:$('#recipe')。val(),
offset:(page - 1)* 10,
limit:10
};
},

这个片段来自Flask-Admin的表单.js [1] 显然,这需要一些调整和参数化(因为做这样一个hacky解决方案会阻止你使用其他Ajax人口在你的应用程序的其余部分选择admin +更新 form.js 直接就可以升级 Flask-Admin 非常麻烦)总而言之,我对这两种解决方案都不满意,而且这个展示只要你想走出一个框架/工具的轨道,就可以结束复杂的死胡同。这可能是一个有趣的功能请求/项目,有人愿意为Flask-Admin贡献一个真实解决方案上游。


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.

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

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

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

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

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.

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?

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.

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

EDIT

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

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

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)

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- Use the supported ajax-completion in Flask-Admin to hack your way into getting the options that you want based on the selected recipe:

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

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
    };
},

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

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)

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 form:根据字段1的值限制字段2的值的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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