How to use a WTForms FieldList of FormFields? - python

I'm building a website using Flask in which I use WTForms. In a Form I now want to use a FieldList of FormFields as follows:
class LocationForm(Form):
location_id = StringField('location_id')
city = StringField('city')
class CompanyForm(Form):
company_name = StringField('company_name')
locations = FieldList(FormField(LocationForm))
so to give people the ability to enter a company with two locations (dynamic adding of locations comes later) I do this on the front side:
<form action="" method="post" role="form">
{{ companyForm.hidden_tag() }}
{{ companyForm.company_name() }}
{{ locationForm.location_id() }}
{{ locationForm.city() }}
{{ locationForm.location_id() }}
{{ locationForm.city() }}
<input type="submit" value="Submit!" />
</form>
So on submit I print the locations:
print companyForm.locations.data
but I get
[{'location_id': u'', 'city': u''}]
I can print the values of the first location using the locationForm (see below), but I still don't know how to get the data of the second location.
print locationForm.location_id.data
print locationForm.city.data
So the list of locations does have one dict with empty values, but:
Why does the list of locations have only one, and not two dicts?
And why are the values in the location dict empty?
Does anybody know what I'm doing wrong here? All tips are welcome!

For starters, there's an argument for the FieldList called min_entries, that will make space for your data:
class CompanyForm(Form):
company_name = StringField('company_name')
locations = FieldList(FormField(LocationForm), min_entries=2)
This will setup the list the way you need. Next you should render the fields directly from the locations property, so names are generated correctly:
<form action="" method="post" role="form">
{{ companyForm.hidden_tag() }}
{{ companyForm.company_name() }}
{{ companyForm.locations() }}
<input type="submit" value="Submit!" />
</form>
Look at the rendered html, the inputs should have names like locations-0-city, this way WTForms will know which is which.
Alternatively, for custom rendering of elements do
{% for l in companyForms.locations %}
{{ l.form.city }}
{% endfor %}
(in wtforms alone l.city is shorthand for l.form.city. However, that syntax seems to clash with Jinja, and there it is necessary to use the explicit l.form.city in the template.)
Now to ready the submitted data, just create the CompanyForm and iterate over the locations:
for entry in form.locations.entries:
print entry.data['location_id']
print entry.data['city']

This is an old question, but still a good one.
I'd like to add a working Flask based example of a toy database (just a list of strings) with focus on the Python part - how to initialize the form with variable number of subforms and how to process the posted data.
This is the example.py file:
import flask
import wtforms
import flask_wtf
app = flask.Flask(__name__)
app.secret_key = 'fixme!'
# not subclassing from flask_wtf.FlaskForm
# in order to avoid CSRF on subforms
class EntryForm(wtforms.Form):
city = wtforms.fields.StringField('city name:')
delete = wtforms.fields.BooleanField('delete?')
class MainForm(flask_wtf.FlaskForm):
entries = wtforms.fields.FieldList(wtforms.fields.FormField(EntryForm))
submit = wtforms.fields.SubmitField('SUBMIT')
city_db = "Graz Poprad Brno Basel Rosenheim Torino".split() # initial value
#app.route("/", methods=['POST'])
def demo_view_function_post():
global city_db
form = MainForm()
if form.validate_on_submit():
city_db = [
entry['city'] for entry in form.entries.data
if entry['city'] and not entry['delete']]
return flask.redirect(flask.url_for('demo_view_function_get'))
# handle the validation error, i.e. flash a warning
return flask.render_template('demo.html', form=form)
#app.route("/")
def demo_view_function_get():
form = MainForm()
entries_data = [{'city': city, 'delete': False} for city in city_db]
entries_data.append({'city': '', 'delete': False}) # "add new" placeholder
form.process(data={'entries': entries_data})
return flask.render_template('demo.html', form=form)
This is the demo.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Demo</title>
</head>
<body>
<h1>Subform demo</h1>
<p>Edit names / mark for deletion / add new</p>
<form method="post">
{{ form.csrf_token() }}
{% for entry in form.entries %}
{% if loop.last %}
<div>Add new:</div>
{% endif %}
<div>
{{ entry.city.label }} {{ entry.city() }}
{{ entry.delete() }} {{ entry.delete.label }}
</div>
{% endfor %}
{{ form.submit() }}
</form>
</body>
Run with: FLASK_APP=example flask run

Related

Single FlaskForm rendered multiple times on one page using FOR loop

I'm making an application where I need to render identical forms multiple times on one page (number of forms might be from 1 to 20). In the video below I'm using simple for loop over a range from 0 to 5, just for a test. When I input data into the first form and submit it, all data duplicates to other forms but I need the user to submit data of each form independently from others. In other words, the user can fill in data in some forms (in 1, in 2 or in all forms) and submit those data separately.
Youtube video showing my problem
I have tried to change submit buttons manually with unique ids and names for each form but this didn't help.
I also tried to use Field Enclosures from WTForms but this also didn't help.
Here is my FlaskForm I haven't included all fields to save space
class EnvelopeForm(FlaskForm):
vme_name = StringField('VME')
outside_diameter = DecimalField('OD', validators=[DataRequired()])
submit = SubmitField('build envelope')
Here is my view handler
#app.route('/bokeh', methods=['GET', 'POST'])
def nonuniform_ellipse():
form = EnvelopeForm()
if form.validate_on_submit():
envelope = Envelopes(vme_name=form.vme_name.data,
outside_diameter=form.outside_diameter.data)
db.session.add(envelope)
db.session.commit()
return render_template('vme.html', form=form)
Here is HTML I haven't included all fields to save space.
{% for i in range(0, 5) %}
<form method="POST" action="" name="{{ i }}">
{{ form.hidden_tag() }}
<div>
{{ form.vne_name.label }}
{{ form.vme_name }}
</div>
<div>
{{ form.outside_diameter.label }}
{{ form.outside_diameter }}
</div>
<div>
{{ form.submit) }}
</div>
</form>
{% endfor %}
Here is a solution as per #ser-zhm comment. Now I can render whatever forms I have in DB separately on each other as it is shown on this image.
But when I submit data from one of the forms it is always updates the last record in DB and not the particular row. Lets say I have three empty forms rendered on the page and three records (rows) in my DB with NULL data in each column name referring to each form fields like it is shown here. When I fill in form #2, all the data is saved in row #3 instead of the row #2. So whatever form I fill in it is always the last record is updated.
How can I updated the particular row which is referring to the form ID?
View handler
#app.route('/envelope_forms_test', methods=['GET', 'POST'])
def nonuniform_ellipse():
form_ids = list(map(str, [id[0] for id in
Envelopes.query.with_entities(Envelopes.id).all()]))
forms = []
for form_id in form_ids:
form = EnvelopeForm(prefix=form_id)
forms.append(form)
for form in forms:
if form.submit.data and form.validate_on_submit():
Envelopes.query.filter_by(id=form_id).\
update({'vme_name': form.vme_name.data,
'outside_diameter': form.outside_diameter.data})
db.session.commit()
Here is HTML
{% for form in forms %}
<form method="POST" action="">
{{ form.csrf_token() }}
<div>
{{ form.vne_name.label }}
{{ form.vme_name }}
</div>
<div>
{{ form.outside_diameter.label }}
{{ form.outside_diameter }}
</div>
<div>
{{ form.submit) }}
</div>
</form>
{% endfor %}

Multiple form on a page validation errors WTForms

I have multiple form on a page in modals: 1-st to create a new user address and n-forms with addresses which user created (created with loop). When i enter invalid data in fields with validators (e.g. datarequired), i have error messages in each form.
Here is the field render example which i use in every form:
{{ address_form.street.label(class_="form-label", for="InputStreet") }}
{{ address_form.street(class_="form-control", id="InputStreet") }}
{% for error in address_form.street.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
Part of the code from view.py:
#bp.route('/profile/address', methods=['GET', 'POST'])
#login_required
def address():
address_form = AddressForm()
if address_form.submit_address.data and address_form.validate():
address_to_add = Address(
street=address_form.street.data,
house=address_form.house.data,
building=address_form.building.data,
entrance=address_form.entrance.data,
floor=address_form.floor.data,
apartment=address_form.apartment.data,
additional_info=address_form.additional_info.data,
user=current_user)
db.session.add(address_to_add)
db.session.commit()
return redirect(url_for('profile.address'))
if address_form.edit_address.data and address_form.validate():
address_to_edit = Address.query.get(address_form.address_id.data) # Here is data from hidden field
# Editing data in DB
db.session.commit()
return redirect(url_for('profile.address'))
return render_template('profile/address.html', title='Адрес доставки', address_form=address_form)
Forms work fine with adding, editing and deleting data, but work incorrect with validation errors.
I think i need one more condition in if statenent related with hidden field or change something in my html file.
I've tried add an action attr in form like:
<form action="{{ url_for('profile.address', form_id=address.id) }}" method="post" novalidate>
And smth like this in view func but it doesn't work:
form_id = request.args.get('form_id', type=int)
if address_form.edit_address.data and address_form.validate() and form_id == address_form.address_id.data:
pass
Finally i found a very bad solution:
address_form = AddressForm()
form_id = request.args.get('form_id', 0, type=int)
For main form form_id always is 0.
Form tag for main form:
<form action="{{ url_for('profile.address', form_id=form_id) }}" method="post" novalidate>
And a field render for main form:
{{ address_form.street.label(class_="form-label", for="InputStreet") }}
{{ address_form.street(class_="form-control", id="InputStreet") }}
{% if form_id == 0 %}
{% for error in address_form.street.errors %}
<span style="color: red;">{{ error }}</span>
{% endfor %}
{% endif %}
If someone has a better solution about my problem it'd good. Now it's time to learn some JS and solve this problem with AJAX.

Simple dynamic forms with Flask/Flask-WTF

I need to build a simple form with a number of checkboxes. The problem is that I need the checkboxes to come from a csv file which would look like this:
data_so.csv
Name
A. Blabla
U. Blublu
I. Blibli
O. Bloblo
The form, right now, with some hard coded checkboxes, looks like this:
Instead of "Mr. 1", I'd need to have "A. Blabla", instead of "Ms. 2", I'd want "U. Blublu", etc. and instead of 3 checkboxes, I'd need 4, the number of entries in my csv file.
Here are my Flask files:
route_so.py
from flask import Flask, render_template, request, flash
from forms_so import ContactForm
import csv
app = Flask(__name__)
app.secret_key = 'development key'
#app.route('/', methods=['GET', 'POST'])
def home():
form = ContactForm()
if request.method == 'POST':
if form.validate() == False:
flash('All fields are required.')
return render_template('home_so.html', form=form)
else:
print(form.node_1.data,form.node_2.data,form.node_3.data)
return render_template('home_so.html', success=True)
elif request.method == 'GET':
return render_template('home_so.html', form=form)
if __name__ == '__main__':
app.run(debug=True)
form_so.py
from flask.ext.wtf import Form
import csv
from wtforms import TextField, RadioField, TextAreaField, SubmitField, validators, BooleanField
class ContactForm(Form):
# my attempt to force the creation of dynamic global variables
with open('/data_so.csv', 'rb') as f:
reader = csv.reader(f)
r = list(reader)
nodes = {}
for i in range(1,len(r)):
globals()[''.join("node_"+str(i))] = BooleanField(r[i][0])
# end of my attempt to force the creation of dynamic global variables
node_1 = BooleanField("Mr. 1")
node_2 = BooleanField("Ms. 2")
node_3 = BooleanField("Dr. 3")
# this needs to be dynamically set instead
submit = SubmitField("Send")
So I tried and created dynamic variables (in a dirty, hacky way). The problem now is that I don't know how to make the home_so.html work with an undifined number of variables...
home_so.html
{% extends "layout_so.html" %}
{% block content %}
{% if success %}
<p>Thank you for filling up our survey. We'll get back to you shortly.</p>
{% else %}
<form action="{{ url_for('home') }}" method=post>
{{ form.hidden_tag() }}
<h2>List of check boxes dynamically built from local csv file</h2>
#this needs to be dynamically set
{{ form.node_1.label }}
{{ form.node_1 }}
{{ form.node_2.label }}
{{ form.node_2 }}
{{ form.node_3.label }}
{{ form.node_3 }}
{{ form.submit }}
</form>
{% endif %}
{% endblock %}
Is there a way to accomplish this sort of things with a simple csv file? If not, what's the usual way to go about dynamically producing a form as it loads client-side?
{% for node in node_list_from_app %}
<p class="field"><label><input type="checkbox" name="node" value="{{ node }}"> {{ node }}</label></p>
{% endfor %}

Form with multiple identical lines

I am writing a Flask app in which the user will have to enter all of the university courses they are planning to take for a semester. The data required is a code (e.g. 'CMPT') and a number (e.g. '101').
I want the user to be able to input up to 10 courses. I would like to do this by reusing the CourseForm I created, instead of manually creating the fields for each course. I have played around with FieldList and FormField to create a new form that uses the CourseForm, but I can't get it to display properly.
How can I create a form that has 10 slots for courses?
class CourseForm(Form):
code = SelectField('Course Code', choices=CHOICES)
number = IntegerField('Course Number')
#app.route(...)
def index():
form = CourseForm()
return render_template('main.html', title='Main', form=form)
main.html:
...
<form action="" method="post" name="course">
{{ form.hidden_tag() }}
<p>{{ form.code() }} {{ form.number() }}</p>
<p><input type="submit" value="Submit"></p>
</form>
...
You're on the right track with FieldList and FormField.
Start with your CourseForm.
class CourseForm(Form):
code = SelectField('Course Code', choices=CHOICES)
number = IntegerField('Course Number')
Then you'll need to encapsulate it inside another form. For the sake of this example I'll call it RegisteredCoursesForm. You can use max_entries to limit the number of entries to 10. If you always want 10 entries to be available, even if they won't all be filled in, you can also include min_entries.
class RegisteredCoursesForm(Form):
courses = FieldList(FormField(CourseForm), min_entries=10, max_entries=10)
# Any other fields can go here (e.g., user_id).
You'll want to pass this new form to your template instead of CourseForm.
def index():
form = RegisteredCoursesForm()
return render_template('main.html', title='Main', form=form)
Finally you can iterate over the courses field to get all of the fields contained without CourseForm.
<form action="" method="post" name="course">
{{ form.hidden_tag() }}
{% for course_form in form.courses %}
<p>{{ course_form.code }} {{ course_form.number }}</p>
{% endfor %}
<p><input type="submit" value="Submit"></p>
</form>

Flask WTF – Forms always redirect to root

I have created a simple Flask WTF form
class SequenceForm(Form):
sequence = StringField('Please enter a sequence in FASTA format', validators=[Required()])
submit = SubmitField('Submit')
and I have set up a route to make it appear on a page
#main.route('/bioinformatics')
def bioinformatics():
form = SequenceForm()
return render_template('bioinformatics.html', form=form)
It all works great (so far). When I point my browser to foo/bioinformatics, I see a page with a SequenceForm rendered. However, when I hit the Submit button, I am always taken back to the root page defined by #main.route('/').
How can I make the Submit button take me somewhere else? I would like to use validate_on_submit() and do stuff with the data entered in the form.
Thanks!
/Michael Knudsen
UPDATE (Code from bioinformatics.html)
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Bioinformatics{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, Bioinformatics!</h1>
</div>
{{ wtf.quick_form(form) }}
{% endblock %}
You need to specify an action in the form in your html.
<form action="/url_which_handles_form_data" method="Post">
your code
</form>
make sure to give the correct path if you are using blueprints
Edit:
From https://github.com/mbr/flask-bootstrap/blob/master/flask_bootstrap/templates/bootstrap/wtf.html I found this part.
{% macro quick_form(form,
action="",
method="post",
extra_classes=None,
role="form",
form_type="basic",
horizontal_columns=('lg', 2, 10),
enctype=None,
button_map={},
id="") %}
So you can probably call
{{ wtf.quick_form(form, action="/fancy_url") }}
or
{{ wtf.quick_form(form, action=url_for("blueprint_name.fancy_url")) }}
Depending on where the view is located.
Thanks to Tim Rijavec and Zyber. I used a combination of your suggestions to come up with the following solution.
I added GET and POST to methods for the route
#main.route('/bioinformatics', methods=['GET', 'POST'])
def bioinformatics():
form = SequenceForm()
return render_template('bioinformatics.html', form=form)
and then I wrapped the wtf.quick_form call inside tags.
<form action="{{ url_for('main.bioinformatics') }}" method="POST">
{{ wtf.quick_form(form) }}
</form>
Now everything works beautifully. Thanks!

Categories

Resources