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 - 每个请求都会修改(在进程级别共享)表单类,随意添加或覆盖字段。它似乎可以在具有单个并发用户的单进程单线程开发服务器上工作,但会因最不可预测的错误或(更糟糕的)错误结果而中断生产。

因此,您真正想知道的是如何动态地将字段添加到表单实例中——或者,作为替代方案,如何动态构建一个新的临时表单类(这并不难——记住 Python 类也是对象) .

于 2015-07-16T11:35:35.263 回答