最終回答準備中
以下の最初の部分は元の回答です。回答を完成させる追加は最後に追加されます。
元の回答: 入力の保存
今では、私自身の質問に対する部分的な解決策があります。フォーム フィールドは希望どおりに機能し、入力はデータベースに正しく保存されます。欠けている側面が 1 つあります。既存の調査の編集フォームを開くと、以前に調査に追加されたページがフォーム フィールドに表示されません (つまり、フィールドに事前入力されていません)。 .
自分で最終的な解決策を見つけたら、この投稿を編集します。バウンティは、最後のギャップを最初に埋めた人に授与されます。黄金のヒントがある場合は、新しい回答を送信してください!
驚いたことに、テンプレートに関してはまだ何もする必要がありません。秘訣は、ほとんどの場合、カスタム フォーム フィールド タイプの「追加」フィールドとして別の名前を使用する代わりに、両方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)
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
編集フォームのフィールドに事前入力するには、フィールドを設定SurveyView.form_widget_args
する必要があると思います'page_list'
。これまでのところ、その分野に何が必要かはまだ完全にわかりません。どんな助けでも大歓迎です!
追加: select2 フィールドの事前設定
Flask-Admin が処理方法を知っているフォーム フィールドの自動事前入力は、 で行われflask.ext.admin.model.base.BaseModelView.edit_view
ます。on_model_change
残念ながら、すぐに使用できるフックは、カスタムの事前入力アクションを追加するためのものではありません。edit_view
回避策として、オーバーライドしてそのようなフックを含めるサブクラスを作成しました。挿入は 1 行だけです。ここではコンテキストで示します。
@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)
# ...
フックを使用しないモデル ビューで問題が発生しないようにするために、派生クラスは明らかにデフォルトとして何もしない必要があります。
def on_form_prefill (self, form, id):
pass
これらの追加用のパッチを作成し、Flask-Admin プロジェクトにプル リクエストを送信しました。
次に、次のようにクラスの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()
)
そして、それがこの問題のこの部分の解決策でした。(回避策として、実際には のサブクラスでのオーバーライドを定義しました。これは、そのクラスの追加機能が必要なためですが、通常は でのみ定義されています。edit_view
flask.ext.admin.contrib.sqla.ModelView
edit_view
flask.ext.admin.model.base.BaseModelView
)
しかし、この時点で新たな問題を発見しました。入力はデータベースに完全に保存されますが、調査にページが追加された順序は保持されませんでした。これは、Select2 の複数フィールドで多くの人が遭遇する問題であることが判明しました。
追加: 順序の修正
結局のところ、基になるフォーム フィールドが の場合、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) + ']'
この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 %}
追加のボーナスとして、これにより、ユーザーは選択したページを表すウィジェットをドラッグ アンド ドロップで並べ替えることができます。
この時点で、私の質問は最終的に完全に回答されました。
のコードSelect2MultipleField
。diff を実行し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))