Jul*_*ian 5 sqlalchemy jinja2 flask jquery-select2 flask-admin
我在声明性模型页面和调查之间存在多对多关系,它由关联代理调解,因为调查中页面出现的顺序很重要,因此交叉链接表有一个额外的字段.
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')
Run Code Online (Sandbox Code Playgroud)
现在我想通过Flask-Admin提供一个表单,让用户将页面添加到调查中.理想情况下,用户将页面填充到表单中的顺序决定了它的值SurveyPage.ordering
.这不起作用(表单无法呈现,请参见帖子底部的最后一点回溯):
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))
Run Code Online (Sandbox Code Playgroud)
这可行,但它不能做我想要的(它让我将SurveyPage对象与调查相关联,但我必须以ordering
单独的形式编辑字段):
class SurveyView (ModelView):
form_columns = ('survey_pages',)
# ...
Run Code Online (Sandbox Code Playgroud)
我知道我可能不得不通过覆盖sqla.ModelView.form_rules
以及将一些HTML和Javascript插入到继承自admin/model/create.html
等人的模板中来做一些黑客攻击.不幸的是,我对Flask-Admin的经验很少,所以弄清楚如何解决这个问题需要花费太多时间.更糟糕的是,文档和示例代码似乎没有超出基础知识.将非常感谢帮助!
失败形式的最后一点追溯:
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
Run Code Online (Sandbox Code Playgroud)
最后答案准备好了
下面的第一部分是原始答案,最后附加完成答案的附加内容.
到现在为止,我对自己的问题有了部分解决方案.表单字段以我想要的方式工作,输入正确保存到数据库.缺少一个方面:当我打开预先存在的调查的编辑表单时,之前添加到调查中的页面不会显示在表单字段中(换句话说,该字段未预先填充) .
如果我自己找到最终的解决方案,我会编辑这篇文章.奖金将颁发给任何填补最后差距的人.如果你有金色的提示,请提交一个新答案!
令我惊讶的是,我还不需要对模板做任何事情.诀窍主要在于避免使用Survey.pages
和Survey.survey_pages
作为表单列,而是使用不同的名称作为具有自定义表单字段类型的"额外"字段.这是SurveyView
该类的新版本:
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)
Run Code Online (Sandbox Code Playgroud)
Select2MultipleField
是flask.ext.admin.form.fields.Select2Field
我通过简单地复制粘贴和修改代码来适应的变体.我感激地使用flask.ext.admin.form.widgets.Select2Widget
哪个已经允许多次选择,如果你传递正确的构造函数参数.我已经在这篇文章的底部包含了源代码,以便不分解文本的流程(编辑:此帖子底部的源代码现在更新以反映最终答案,不再使用Select2Widget
了).
SurveyView
该类的主体包含数据库查询,这意味着它需要具有实际数据库连接的应用程序上下文.在我的情况下,这是一个问题,因为我的Flask应用程序实现为具有多个模块和子包的包,并且我避免了循环依赖.我通过导入包含SurveyView
我的create_admin
函数中的类的模块解决了它:
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
Run Code Online (Sandbox Code Playgroud)
为了在编辑表单预填充领域,我怀疑我需要设置SurveyView.form_widget_args
一个'page_list'
字段.到目前为止,我仍然完全不知道该领域需要什么.任何帮助仍然非常感谢!
自动预填充Flask-Admin知道如何处理的表单字段flask.ext.admin.model.base.BaseModelView.edit_view
.不幸的是,开箱即用它没有提供任何挂钩on_model_change
来添加自定义预填充动作.作为一种解决方法,我创建了一个子类来覆盖edit_view
包含这样的钩子.插入只是一行,在上下文中显示:
@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)
# ...
Run Code Online (Sandbox Code Playgroud)
为了不对不使用钩子的模型视图造成问题,派生类显然也必须提供no-op作为默认值:
def on_form_prefill (self, form, id):
pass
Run Code Online (Sandbox Code Playgroud)
我已经为这些添加创建了一个补丁,并向Flask-Admin项目提交了一个pull请求.
然后我可以覆盖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()
)
Run Code Online (Sandbox Code Playgroud)
这就解决了这个问题的这一部分.(在解决方法中我实际上定义edit_view
了子类中的覆盖flask.ext.admin.contrib.sqla.ModelView
,因为我需要该类的附加功能,但edit_view
通常只在其中定义flask.ext.admin.model.base.BaseModelView
.)
但是,此时我发现了一个新问题:当输入完全存储到数据库中时,未保留页面添加到调查中的顺序.事实证明这是一个更多人使用Select2多个字段的问题.
事实证明,如果底层表单字段是a,则Select2无法保留顺序<select>
.Select2文档建议<input type="hidden">
使用可排序的多选,因此我定义了一个新的窗口小部件类型,wtforms.widgets.HiddenInput
并使用它来代替:
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) + ']'
Run Code Online (Sandbox Code Playgroud)
该data-*
属性是一个HTML5结构,用于传递元素属性中的任意数据.一旦被JQuery解析,这些属性就会变成$(element).data().*
.我在这里使用它将所有可用页面的列表传输到客户端.
为了确保隐藏的输入字段变得可见并且在页面加载时表现得像Select2字段,我需要扩展admin/model/edit.html
模板:
{% 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 %}
Run Code Online (Sandbox Code Playgroud)
作为额外的好处,这使用户能够通过拖放来订购代表所选页面的小部件.
在这一点上,我的问题终于得到了充分的回答.
代码Select2MultipleField
.我建议你运行差异flask.ext.admin.form.fields
来找到差异.
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))
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
3471 次 |
最近记录: |