如何处理Flask-Admin表单中的有序多对多关系(关联代理)? [英] How to handle ordered many-to-many relationship (association proxy) in Flask-Admin form?
问题描述
在声明性模型 Page 和调查之间存在多对多的关系,这是由关联代理调解的,因为页面在调查中出现的顺序是重要的是,所以交叉表有一个额外的字段。
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy.ext.associationproxy import association_proxy
db = SQLAlchemy()
$ b $ class Page(db.Model):
id = db.Column(db.Integer,primary_key = True)
survey = association_proxy('page_surveys','survey')
class Survey(db.Model):
id = db.Column(db.Integer,primary_key = True)
pages = association_proxy('survey_pages','page')
类SurveyPage(db.Model):
survey_id = db.Column(db.Integer,db.ForeignKey 'survey.id'),primary_key = True)
page_id = db.Column(db.Integer,db.ForeignKey('page.id'),primary_key = True)
ordering = db.Column(db.Integer)#1表示第一页
survey = db.relationship('Survey',backref ='survey_pages')
page = db.relationship('Page', backref ='page_surveys')
现在我想通过Flask-Admin提供一个表单,将页面添加到调查。理想情况下,用户填充表单的顺序决定了 SurveyPage.ordering
的值。这不是诀窍(表单不能被渲染,见最后一个回溯在帖子底部):
from flask.ext.admin.contrib.sqla从flask.ext.admin导入ModelView
import Admin
$ b admin = Admin(name ='Project')
class SurveyView(ModelView):
form_columns =('pages',)
def __init__(self,session,** kwargs):
super(SurveyView,self).__ init __(Survey,session, name ='Surveys',** kwargs)
admin.add_view(SurveyView(db.session))
这可以起作用,但是它不会做我想做的事情(它可以让我将SurveyPage对象与调查相关联,但是我将不得不编辑排序
$ b
类SurveyView(ModelView):
form_columns =('survey_pages', )
#...
据我所知,我可能不得不做一些黑客行为通过重写 sqla.ModelView.fo rm_rules
以及将一些HTML和Javascript插入到继承自 admin / model / create.html
等的模板中。不幸的是,我对Flask-Admin的经验很少,所以想办法解决这个问题需要花费太多的时间。更糟糕的是,文档和示例代码似乎没有涵盖太多的基础知识。帮助将非常感激!
$ b $ p
最后一点从失败的形式追溯:
在$ find
中的文件... / python2.7 / site-packages / flask_admin / contrib / sqla / form.py,第416行raise ValueError ('无效的模型属性名称%s。%s'%(model,name))
ValueError:无效的模型属性名称< class'project.models.Survey'> .pages
最终答案准备就绪
下面的第一部分是原来的答案,最后附加了完整的答案。
原始答案:存储输入
现在我已经部分解决了我自己的问题。表单域以我想要的方式工作,输入被正确保存到数据库。只有一个方面的缺失:当我打开一个预先存在的调查的编辑窗体,以前添加到调查的页面不显示在表单字段(换句话说,该字段没有预先填充) 。
如果我自己找到最终的解决方案,我将编辑这篇文章。奖金将颁发给谁填补最终的差距首先。请提交一个新的答案,如果你有金色的提示!
令我惊讶的是,我还没有需要使用模板做任何事情。诀窍主要在于避免 Survey.pages
和 Survey.survey_pages
作为表格列,而不是使用不同的名称带有自定义表单字段类型的额外字段。以下是 SurveyView
类的新版本:
类SurveyView ModelView):
form_columns =('page_list',)
form_extra_fields = {
''page_list'为避免与Survey $实际属性名称冲突而选择的名称b $ b'page_list':Select2MultipleField (
'Pages',
#options必须是(value,label)对的迭代
choices = db.session.query(Page.id,Page.name).all ),
coerce = int),
}
#手动处理表单域中提交的数据
def on_model_change(self,form,model,is_created = False ):
如果不是is_created:
self.session.query(SurveyPage).filter_by(survey = model).delete()
for index,id in enumerate(form.page_list.data) :
SurveyPage(survey = model,page_id = id,ordering = index)
$ b $ def __init__(self,session, ** kwargs):
super(SurveyView,self).__ init __(Survey,session,name ='Surveys',** kwargs)
$ b
Select2MultipleField
是的一个变种。flask.ext.admin.form.fields.Select2Field
,我通过简单地复制粘贴和修改代码来进行调整。我感激地使用 flask.ext.admin.form.widgets.Select2Widget
如果您传递正确的构造函数参数已经允许多选择。我已经在这个帖子的底部包含了源代码,以便不破坏文本的流动( edit:这个帖子底部的源代码现在被更新以反映最终答案,而不是使用 Select2Widget
)
$ b
SurveyView
class包含数据库查询,这意味着它需要具有实际数据库连接的应用程序上下文。在我的情况下,这是一个问题,因为我的Flask应用程序是作为包含多个模块和子包的包来实现的,我避免了循环依赖。我已经通过在 create_admin
函数中导入包含 SurveyView
类的模块解决了这个问题: from ..models导入数据库
def create_admin(app):
admin = Admin(name ='Project ',app = app)
with app.app_context():$ b $ from .views import SurveyView
admin.add_view(SurveyView(db.session))
return admin
为了在编辑表单中预填充字段,我怀疑我需要设置 SurveyView.form_widget_args
并带有'page_list'
字段。到目前为止,我还是完全不知道该领域需要做些什么。任何帮助仍然非常感激!
$ h
$ b
另外:预填充select2字段
自动预填充Flask-Admin知道如何处理的表单字段在 为了不会导致不使用钩子的模型视图的问题,派生类显然也必须提供一个no-op作为def 我为这些附加内容创建了一个补丁,并提交了一个 on_form_prefill flask.ext.admin.model.base.BaseModelView.edit_view
。不幸的是,开箱即不提供任何钩子< edit_view
的子类来包含这样一个钩子。插入只是一个单一的行,这里显示在上下文中:
$ p $ @expose('/ edit /',methods =(' GET','POST'))
def edit_view(self):
#...
$ b $如果validate_form_on_submit(form):
self.update_model(form如果在request.form中有'_continue_editing',
:
flash(gettext('Model was successfully saved。'))
return redirect(request.url)
else :
return redirect(return_url)
self.on_form_prefill(form,id)#< - 这是插入
form_opts = FormOpts(widget_args = self .form_widget_args,
form_rules = self._form_edit_rules)
#...
$ $ p $ def on_form_prefill(self,form,id):
pass
on_form_prefill
SurveyView
类中,如下所示:
def on_form_prefill(self,form,id):
form.page_list.process_data(
self.session.query(SurveyPage.page_id)
.filter(SurveyPage.survey_id == id)
.order_by(SurveyPage.ordering)
.all()
)
这就是解决这个问题的部分原因。 ()在变通方法中,我实际上在 flask.ext.admin.contrib.sqla.ModelView的子类中定义了
edit_view
code>,因为我需要该类的附加功能,但通常只在 flask.ext.admin.model中定义
edit_view
。然而,在这一点上,我发现了一个新的问题:当输入被完全存储到数据库中时数据库中,页面添加到调查的顺序不被保留。原来这是更多人使用Select2多个字段的问题。
另外:修改顺序
事实证明,Select2不能如果底层表单字段是< select>
,则保持订单。 Select2文档为可排序的多重选择建议< input type =hidden>
,所以我根据 wtforms.widgets定义了一个新的窗口小部件类型。 HiddenInput
并用它来代替:
来自wtforms导入小部件
属性是HTML5构造,用于传递元素属性中的任意数据。一旦JQuery解析,这些属性变成
class Select2MultipleWidget(widgets.HiddenInput):
(...)
默认情况下,将调用相关字段$ b的_value $ b提供``value =``HTML属性。
input_type ='select2multiple'
def __call __(self,field,* * kwargs):
kwargs.setdefault('data-choices',self.json_choices(field))
kwargs.setdefault('type','hidden')
return super(Select2MultipleWidget, self).__ call __(field,** kwargs)
@staticmethod
def json_choices(field):
objects =('{{id:{},text :{}}'。format(* c)for field.iter_choices())
'$'$'$'$'$'$'$' $ c> data - *$(element).data()。*
。我在这里使用它来将所有可用页面的列表传输到客户端。
为了确保隐藏的输入字段变得可见并且像Select2字段一样页面加载,我需要扩展
admin / model / edit.html
模板:
{%block tail%}
{{super()}}
< script src =// code.jquery.com/ui/1.11.0/jquery-ui.min.js\"></script>
< script>
$('input [data-choices]')。each(function(){
var self = $(this);
self.select2({
data:self .data()。options,
multiple:true,
sortable:true,
width:'220px'
});
self.on(change ,function(){
$(#+ self.id +_val)。html(self.val());
});
self.select2(container ).find(ul.select2-choices)。sortable({
containment:'parent',
start:function(){self.select2(onSortStart);},
update:function(){self.select2(onSortEnd);}
});
});
< / script>
{%endblock%}
作为额外的好处,这使用户能够在这一点上,我的问题终于得到了充分的回答。
Select2MultipleField
的代码。我建议你用flask.ext.admin.form.fields
运行一个差异来找出差异。来自wtforms导入字段
来自flask.ext.admin._compat import text_type,as_unicode
$ b $ class Select2MultipleField(fields.SelectMultipleField):
`Select2< https://github.com/ivaynberg/select2>`_ style选择部件
您必须包含select2.js,form.js和select2样式表to
work。
这是对原始Select2Field的一个稍微改变的派生。
widget = Select2MultipleWidget()
def __init __(self,label = None,validators = None,coerce = text_type,
choices = None,allow_blank = False,blank_text = None,** kwargs):
super(Select2MultipleField,self).__ init__ (
标签,验证器,强制,选择,** kwargs
)
self.allow_blank = allow_blank
self.blank_ text = blank_text或''
def iter_choices(self):
如果self.allow_blank:
yield(u'__ None',self.blank_text,self.data is [] )
为self.choices中的值,标签:
yield(value,label,self.coerce(value)in self.data)
def process_data自我价值):
如果不是价值:
self.data = []
else:
try:
self.data = []
for v值:
self.data.append(self.coerce(v [0]))
除了(ValueError,TypeError):
self.data = []
def process_formdata(self,valuelist):
如果是valuelist:
如果是valuelist [0] =='__None':
self.data = []
else:
尝试:
self.data = []
在valueelist [0] .split(',')中的值:
self.get(self.coerce(value))
ValueError:
raise ValueError(self.gettext(u'Invalid Choice:could not coerce {}'.format(value)))
$ b $ def pre_validate(self,form):
如果self.allow_blank和self.data是[]:
返回
super(Select2MultipleField,self) .pre_validate(form)
def _value(self):
return','。join(map(str,self.data))
I have a many-to-many relationship between declarative models Page and Survey, which is mediated by association proxies because the order in which pages appear in a survey is important, so the cross-linking table has an additional field.
from flask.ext.sqlalchemy import SQLAlchemy from sqlalchemy.ext.associationproxy import association_proxy db = SQLAlchemy() class Page (db.Model): id = db.Column(db.Integer, primary_key = True) surveys = association_proxy('page_surveys', 'survey') class Survey (db.Model): id = db.Column(db.Integer, primary_key = True) pages = association_proxy('survey_pages', 'page') class SurveyPage (db.Model): survey_id = db.Column(db.Integer, db.ForeignKey('survey.id'), primary_key = True) page_id = db.Column(db.Integer, db.ForeignKey('page.id'), primary_key = True) ordering = db.Column(db.Integer) # 1 means "first page" survey = db.relationship('Survey', backref = 'survey_pages') page = db.relationship('Page', backref = 'page_surveys')
Now I want to offer a form through Flask-Admin that lets the user add pages to a survey. Ideally, the order in which the user fills the pages into the form determines the value of
SurveyPage.ordering
. This doesn't do the trick (the form cannot be rendered, see last bit of traceback at bottom of post):from flask.ext.admin.contrib.sqla import ModelView from flask.ext.admin import Admin admin = Admin(name='Project') class SurveyView (ModelView): form_columns = ('pages',) def __init__ (self, session, **kwargs): super(SurveyView, self).__init__(Survey, session, name='Surveys', **kwargs) admin.add_view(SurveyView(db.session))
This works, but it doesn't do what I want (it lets me associate SurveyPage objects with the survey, but I would have to edit the
ordering
fields in a separate form):class SurveyView (ModelView): form_columns = ('survey_pages',) # ...
I understand that I'll probably have to do some hacking by overriding
sqla.ModelView.form_rules
as well as inserting some HTML and Javascript into templates that inherit fromadmin/model/create.html
et al. Unfortunately, I have very little experience with Flask-Admin, so figuring out how to tackle that on my own will take too much time. To make things worse, the documentation and example code don't seem to cover much beyond the basics. Help will be much appreciated!
Last bit of traceback from failing form:
File ".../python2.7/site-packages/flask_admin/contrib/sqla/form.py", line 416, in find raise ValueError('Invalid model property name %s.%s' % (model, name)) ValueError: Invalid model property name <class 'project.models.Survey'>.pages
解决方案Final answer ready
The first part below is the original answer, the additions that complete the answer are appended at the end.
Original answer: storing the input
By now I have a partial solution to my own question. The form field works in the way I want and the input is correctly saved to the database. There is just one aspect missing: when I open the edit form of a pre-existing Survey, the Pages that were previously added to the Survey do not show up in the form field (in other word, the field is not pre-populated).
I will edit this post if I find the final solution myself. The bounty will be awarded to anyone who fills the final gap first. Please submit a new answer if you have the golden hint!
To my own surprise, I have not needed to do anything with templates yet. The trick lies mostly in avoiding both
Survey.pages
andSurvey.survey_pages
as form columns, instead using a different name as an "extra" field with a custom form field type. Here is the new version of theSurveyView
class:class SurveyView (ModelView): form_columns = ('page_list',) form_extra_fields = { # 'page_list' name chosen to avoid name conflict with actual properties of Survey 'page_list': Select2MultipleField( 'Pages', # choices has to be an iterable of (value, label) pairs choices = db.session.query(Page.id, Page.name).all(), coerce = int ), } # handle the data submitted in the form field manually def on_model_change (self, form, model, is_created = False): if not is_created: self.session.query(SurveyPage).filter_by(survey=model).delete() for index, id in enumerate(form.page_list.data): SurveyPage(survey = model, page_id = id, ordering = index) def __init__ (self, session, **kwargs): super(SurveyView, self).__init__(Survey, session, name='Surveys', **kwargs)
Select2MultipleField
is a variant offlask.ext.admin.form.fields.Select2Field
which I adapted by simply copy-pasting and modifying the code. I gratefully useflask.ext.admin.form.widgets.Select2Widget
which already allows multiple selection if you pass the right constructor argument. I have included the source code at the bottom of this post in order to not break up the flow of the text (edit: the source code at the bottom of this post is now updated to reflect the final answer, which does not use theSelect2Widget
anymore).The body of the
SurveyView
class contains a database query, which means that it needs the application context with an actual database connection. In my case that is a problem because my Flask application is implemented as a package with multiple modules and sub-packages, and I avoid cyclical dependencies. I have solved it by importing the module that contains theSurveyView
class within mycreate_admin
function:from ..models import db def create_admin (app): admin = Admin(name='Project', app=app) with app.app_context(): from .views import SurveyView admin.add_view(SurveyView(db.session)) return admin
In order to pre-populate the field in the edit form, I suspect I'll need to set
SurveyView.form_widget_args
with a'page_list'
field. So far it's still completely obscure to me what needs to be in that field. Any help is still very much appreciated!
Addition: pre-populating the select2 field
Automatic pre-filling of form fields that Flask-Admin knows how to handle is done in
flask.ext.admin.model.base.BaseModelView.edit_view
. Unfortunately, out of the box it doesn't provide any hooks à laon_model_change
to add custom pre-filling actions. As a workaround, I made a subclass that overridesedit_view
to include such a hook. The insertion is just a single line, here shown in context:@expose('/edit/', methods=('GET', 'POST')) def edit_view(self): # ... if validate_form_on_submit(form): if self.update_model(form, model): if '_continue_editing' in request.form: flash(gettext('Model was successfully saved.')) return redirect(request.url) else: return redirect(return_url) self.on_form_prefill(form, id) # <-- this is the insertion form_opts = FormOpts(widget_args=self.form_widget_args, form_rules=self._form_edit_rules) # ...
In order to not cause problems for model views that don't use the hook, the derived class obviously also has to provide a no-op as the default:
def on_form_prefill (self, form, id): pass
I have created a patch for these additions and submitted a pull request to the Flask-Admin project.
Then I could override the
on_form_prefill
method in mySurveyView
class as follows:def on_form_prefill (self, form, id): form.page_list.process_data( self.session.query(SurveyPage.page_id) .filter(SurveyPage.survey_id == id) .order_by(SurveyPage.ordering) .all() )
And that was the solution to this part of this problem. (In the workaround I actually defined the override of
edit_view
in a subclass offlask.ext.admin.contrib.sqla.ModelView
, because I need the added functionality of that class, butedit_view
is normally only defined inflask.ext.admin.model.base.BaseModelView
.)However, at this point I discovered a new problem: while the input was completely stored into the database, the order in which the pages were added to the survey was not preserved. This turned out to be an issue more people walk into with Select2 multiple fields.
Addition: fixing the ordering
As it turns out, Select2 cannot preserve order if the underlying form field is a
<select>
. The Select2 documentation recommends<input type="hidden">
for sortable multiselects, so I defined a new widget type based onwtforms.widgets.HiddenInput
and used that instead:from wtforms import widgets class Select2MultipleWidget(widgets.HiddenInput): """ (...) By default, the `_value()` method will be called upon the associated field to provide the ``value=`` HTML attribute. """ input_type = 'select2multiple' def __call__(self, field, **kwargs): kwargs.setdefault('data-choices', self.json_choices(field)) kwargs.setdefault('type', 'hidden') return super(Select2MultipleWidget, self).__call__(field, **kwargs) @staticmethod def json_choices (field): objects = ('{{"id": {}, "text": "{}"}}'.format(*c) for c in field.iter_choices()) return '[' + ','.join(objects) + ']'
The
data-*
attribute is a HTML5 construct to pass arbitrary data in element attributes. Once parsed by JQuery such attributes become$(element).data().*
. I here use it to transfer the list of all available pages to the client side.In order to ensure that the hidden input field becomes visible and behaves like a Select2 field on page load, I needed to extend the
admin/model/edit.html
template:{% extends 'admin/model/edit.html' %} {% block tail %} {{ super() }} <script src="//code.jquery.com/ui/1.11.0/jquery-ui.min.js"></script> <script> $('input[data-choices]').each(function ( ) { var self = $(this); self.select2({ data:self.data().choices, multiple:true, sortable:true, width:'220px' }); self.on("change", function() { $("#" + self.id + "_val").html(self.val()); }); self.select2("container").find("ul.select2-choices").sortable({ containment: 'parent', start: function() { self.select2("onSortStart"); }, update: function() { self.select2("onSortEnd"); } }); }); </script> {% endblock %}
As an added bonus, this enables the user to order the widgets that represent the selected pages by drag-and-drop.
At this point, my question is finally fully answered.
The code for
Select2MultipleField
. I suggest you run a diff withflask.ext.admin.form.fields
to find the differences.from wtforms import fields from flask.ext.admin._compat import text_type, as_unicode class Select2MultipleField(fields.SelectMultipleField): """ `Select2 <https://github.com/ivaynberg/select2>`_ styled select widget. You must include select2.js, form.js and select2 stylesheet for it to work. This is a slightly altered derivation of the original Select2Field. """ widget = Select2MultipleWidget() def __init__(self, label=None, validators=None, coerce=text_type, choices=None, allow_blank=False, blank_text=None, **kwargs): super(Select2MultipleField, self).__init__( label, validators, coerce, choices, **kwargs ) self.allow_blank = allow_blank self.blank_text = blank_text or ' ' def iter_choices(self): if self.allow_blank: yield (u'__None', self.blank_text, self.data is []) for value, label in self.choices: yield (value, label, self.coerce(value) in self.data) def process_data(self, value): if not value: self.data = [] else: try: self.data = [] for v in value: self.data.append(self.coerce(v[0])) except (ValueError, TypeError): self.data = [] def process_formdata(self, valuelist): if valuelist: if valuelist[0] == '__None': self.data = [] else: try: self.data = [] for value in valuelist[0].split(','): self.data.append(self.coerce(value)) except ValueError: raise ValueError(self.gettext(u'Invalid Choice: could not coerce {}'.format(value))) def pre_validate(self, form): if self.allow_blank and self.data is []: return super(Select2MultipleField, self).pre_validate(form) def _value (self): return ','.join(map(str, self.data))
这篇关于如何处理Flask-Admin表单中的有序多对多关系(关联代理)?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!