6

宣言型モデルPageSurveyの間に多対多の関係があります。これは、調査に表示されるページの順序が重要であるため、関連付けプロキシによって仲介されます。そのため、相互リンク テーブルには追加のフィールドがあります。

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

ここで、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))

これは機能しますが、私が望むことにはなりません (SurveyPage オブジェクトを調査に関連付けることはできますがordering、別のフォームでフィールドを編集する必要があります)。

class SurveyView (ModelView):
    form_columns = ('survey_pages',)
    # ...

et al.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
4

2 に答える 2

10

最終回答準備中

以下の最初の部分は元の回答です。回答を完成させる追加は最後に追加されます。

元の回答: 入力の保存

今では、私自身の質問に対する部分的な解決策があります。フォーム フィールドは希望どおりに機能し、入力はデータベースに正しく保存されます。欠けている側面が 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)

Select2MultipleFieldflask.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_viewflask.ext.admin.contrib.sqla.ModelViewedit_viewflask.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))
于 2014-07-24T08:21:01.350 に答える
-2

この回答は、フラスコ管理者の外部キーの動的フィルターに関連しています。フィールド B の選択リストが必要な場合、フィールド A などの値に依存する非常に一般的な状況だと思います。

このリンクには、役立つ情報も含まれている可能性があります: https://github.com/flask-admin/flask-admin/issues/797

on_form_prefill を query_factory と一緒に使用することで、これに対する解決策を見つけました。手順は次のとおりです。

admin 定義では、on_form_prefill のデフォルトの実装をオーバーライドします。そのメソッドでは、編集中の現在のオブジェクトを取得できるため、現在定義されているフィールドに基づいて別のフィールドの query_factory を定義できます。コードは次のとおりです。

class ReceivingAdmin(ModelView):
        def on_form_prefill(self, form, id):

        # Get field(purchase_order_id) from the current object being edited via form._obj.purchase_order_id
        if form is not None and form._obj is not None and form._obj.purchase_order_id is not None:
            po_id = form._obj.purchase_order_id
            # Define a dynamic query factory based on current data.
            # Please notice since the po_id parameter need to be passed to the function,
            # So functools.partial is used
            form.lines.form.purchase_order_line.kwargs['query_factory'] =\
                partial(PurchaseOrderLine.header_filter, po_id)

モデル内のクエリ ファクトリの定義は次のとおりです。

class PurchaseOrderLine(db.Model):
    @staticmethod
    def header_filter(po_id):
        return AppInfo.get_db().session.query(PurchaseOrderLine).filter_by(purchase_order_id=po_id)

このようにして、パラメータ po_id に基づいて発注明細リストに表示されるレコードを制御でき、po_id の値が on_form_prefill のクエリ ファクトリ関数に渡されます。

于 2015-07-07T14:54:32.950 に答える