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.