1

I am trying to create a form in python / Flask that will add some dynamic slider inputs to a set of standard fields. I am struggling to get it to work properly, though.

Most of the web forms in my app are static, created through wtforms as in:

    class CritiqueForm(Form):

        rating = IntegerField('Rating')
        comment = TextAreaField('Comments')
        submit = SubmitField('Save Critique')

When I am explicit like that, I can get the expected results by using the CritiqueForm() in the view and passing the form object to render in the template.

However, I have a critique form that needs to dynamically include some sliders for rating criteria specific to a particular record. The number of sliders can vary form one record to the next, as will the text and IDs that come from the record's associated criteria.

When I looked for some ways to handle this, I found a possible solution from dezza (Dynamic forms from variable length elements: wtforms) by creating a class method in the form, which I could then call before instantiating the form I want to render. As in:

    class CritiqueForm(Form):

        rating = IntegerField('Rating')
        comment = TextAreaField('Comments')
        submit = SubmitField('Save Critique')

        @classmethod
        def append_slider(cls, name, label):
            setattr(cls, name, IntegerField(label))
            return cls

where 'append_slider' is always an IntegerField with a label I provide. This works enough to allow me to populate the criteria sliders in the view, as in:

    @app.route('/critique/<url_id>/edit', methods=['GET', 'POST'])
    def edit_critique(url_id):
        from app.models import RecordModel
        from app.models.forms import CritiqueForm

        record = RecordModel.get_object_by_url_id(url_id)
        if not record: abort(404)

        # build editing form
        ratings = list()
        for i, criterium in enumerate(record.criteria):
            CritiqueForm.append_slider('rating_' + str(i+1),criterium.name)
            ratings.append('form.rating_' + str(i+1))
        form = CritiqueForm(request.form)

        # Process valid POST
        if request.method=='POST' and form.validate():
           # Process the submitted form and show updated read-only record        
            return render_template('critique.html')

        # Display edit form
        return render_template('edit_critique.html',
            form=form,
            ratings=ratings,
            )

The ratings list is built to give the template an easy way to reference the dynamic fields:

    {% for rating_field in ratings %}
        {{ render_slider_field(rating_field, label_visible=True, default_value=0) }} 
    {% endfor %}

where render_slider_field is a macro to turn the IntegerField into a slider.

With form.rating—an integer field explicitly defined in CritiqueForm—there is no problem and the slider is generated with a label, as expected. With the dynamic integer fields, however, I cannot reference the label value in the integer field. The last part of the stack trace looks like:

    File "/home/vagrant/msp/app/templates/edit_critique.html", line 41, in block "content"
    {{ render_slider_field(rating_field, label_visible=True, default_value=0) }}

    File "/home/vagrant/msp/app/templates/common/form_macros.html", line 49, in template
    {% set label = kwargs.pop('label', field.label.text) %}

    File "/home/vagrant/.virtualenvs/msp/lib/python2.7/site-packages/jinja2/environment.py", line 397, in getattr
    return getattr(obj, attribute)

    UndefinedError: 'str object' has no attribute 'label'

Through some debugging, I have confirmed that none of the expected field properties (e.g., name, short_name, id ...) are showing up. When the dust settles, I just want this:

        CritiqueForm.append_slider('rating', 'Rating')

to be equivalent to this:

        rating = IntegerField('Rating')

Is the setattr() technique inherently limiting in what information can be included in the form, or am I just initializing or referencing the field properties incorrectly?

EDIT: Two changes allowed my immediate blockers to be removed.

1) I was improperly referencing the form field in the template. The field parameters (e.g., label) appeared where expected with this change:

    {% for rating_field in ratings %}
            {{ render_slider_field(form[rating_field], label_visible=True, default_value=0) }} 
    {% endfor %}

where I replace the string rating_field with form[rating_field].

2) To address the problem of dynamically changing a base class from the view, a new form class ThisForm() is created to extend my base CritiqueForm, and then the dynamic appending is done there:

    class ThisForm(CritiqueForm):
        pass

    # build criteria form fields
    ratings = list()
    for i, criterium in enumerate(record.criteria):
        setattr(ThisForm, 'rating_' + str(i+1), IntegerField(criterium.name))
        ratings.append('rating_' + str(i+1))

    form = ThisForm(request.form)

I don't know if this addresses the anticipated performance and data integrity problems noted in the comments, but it at least seems a step in the right direction.

4

1 に答える 1

2

setattr(obj, name, value)obj.name = valueは- の構文糖衣である -とまったく同じですobj.__setattr__(name, value)。したがって、問題は「何らかの制限」ではsetattr()なく、最初にどのように機能するかwtform.Formです。ソースコードを見ると、フィールドをクラス属性として宣言するだけでなく、フィールドとフォームを連携させる方法がたくさんあることがわかります (メタクラスの魔法が関係しています...)。IOW、フォームにフィールドを動的に追加する方法を見つけるには、ソース コードを調べる必要があります。

また、コードはクラス自体に新しいフィールドを設定しようとします。これは、同時アクセスを伴うマルチプロセス/マルチスレッド/長時間実行プロセス環境では大きなNO NOです。各リクエストは(プロセスレベルで共有されている)フォームクラスを変更し、フィールドを無計画に追加またはオーバーライドします。1 人の同時ユーザーがいる単一プロセスの単一スレッドの開発サーバーで動作するように見えるかもしれませんが、予測不可能なエラーまたは (さらに悪い) 間違った結果で運用環境が中断されます。

つまり、フォームインスタンスにフィールドを動的に追加する方法、または別の方法として、新しい一時的なフォーム クラスを動的に作成する方法 (これは決して難しいことではありません。Python クラスもオブジェクトであることを思い出してください)。 .

于 2015-07-16T11:35:35.263 に答える