How to better add input fields dynamically with wtfforms - python

I'd like to compose Form fields in code, dynamically, according to the type of data.
I have app.py file:
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField, FormField, FieldList
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secretomucho'
class StringForm(FlaskForm):
string = StringField()
class IntegerForm(FlaskForm):
integer = IntegerField()
class EntriesForm(FlaskForm):
strings = FieldList(FormField(StringForm))
integers = FieldList(FormField(IntegerForm))
#app.route('/', methods=['GET', 'POST'])
def index():
strs = [{"string": "First String"}, {"string": "Second String"}]
ints = [{"integer": 1}, {"integer": 2}]
form = EntriesForm(strings=strs, integers=ints)
return render_template('index.html', form=form)
and index.html file:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<form method="POST" action="">
{% for field in form %}
<td> {{ field.label }} </td>
<td> {{ field }} </td>
{% endfor %}
</form>
</body>
</html>
Which produces:
But I'd like to have a less grouped look, types of values mixed up, maybe some sections with different data types that can be toggled, etc. All that composed in code according to different data structures.
Something like:
form = MyForm()
form.addIntegerField(value)
form.addStringField(value2)
form.addIntegerField(value3)
return render_template('index.html', form=form)
Is this even possible?

I was able to achieve that with https://wtforms.readthedocs.io/en/2.3.x/specific_problems/#dynamic-form-composition
The key was to use an internal subclass:
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, IntegerField,
app = Flask(__name__)
app.config['SECRET_KEY'] = 'secretomucho'
#app.route('/', methods=['GET', 'POST'])
def index():
class MyForm(FlaskForm):
pass
setattr(MyForm, 'test1', StringField('StringField 1'))
setattr(MyForm, 'test2', IntegerField('IntegerField 1'))
setattr(MyForm, 'test3', IntegerField('IntegerField 2'))
setattr(MyForm, 'test4', StringField('StringField 2'))
form = MyForm()
return render_template('index.html', form=form)

Related

FlaskForm had to reload select choices

I am new to Python and Flask. I am trying to use WTForm FlaskForm, and having problems with choices in SelectField. I am building my forms like this.
class FightForm(FlaskForm):
fight_id = StringField('fight_id', render_kw={'readonly': True})
fight_type_id = SelectField('fight_type_id',
choices=[(t.fight_type_id, t.type_name) for t in fight_type.query.all()], validate_choice=False, validators=[DataRequired()])
These choices appear to only load 1 time. If I go add a new fight_type, I have to stop the application and restart if for it to refresh.
Also, I found this answer, flask wtforms selectfield choices not update, but FlaskForm does not trigger the __init__ function this suggests.
I changed it temporarily to Form anyway and got an error saying fight_type_id does not belong to form (paraphrasing).
I would like for these to refresh every time I call the page.
I think you should take a look at the WTForms-SQLAlchemy extension.
The QuerySelectField dynamically loads all database entries based on a submitted database query. A column can be defined for the label of an entry. When the form is submitted, the selected object is automatically queried from the database and is returned by the field.
Flask (app.py)
from flask import (
Flask,
redirect,
render_template,
request,
url_for
)
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from flask_wtf.csrf import CSRFProtect
from wtforms.validators import InputRequired
from wtforms_sqlalchemy.fields import QuerySelectField
app = Flask(__name__)
app.secret_key = b'your secret here'
db = SQLAlchemy(app)
csrf = CSRFProtect(app)
class FightType(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, nullable=False, unique=True)
class Fight(db.Model):
id = db.Column(db.Integer, primary_key=True)
type_id = db.Column(db.Integer, db.ForeignKey('fight_type.id'))
type = db.relationship('FightType', backref='fights')
class FightForm(FlaskForm):
type = QuerySelectField(
get_label='name',
query_factory=lambda: FightType.query.all(),
validators=[InputRequired()]
)
with app.app_context():
db.drop_all()
db.create_all()
types = [FightType(name=f'type-{i}') for i in range(10)]
db.session.add_all(types)
db.session.commit()
#app.route('/')
def index():
fights = Fight.query.all()
return render_template('index.html', **locals())
#app.route('/fight/new', methods=['GET', 'POST'])
def fight_create():
form = FightForm(request.form)
if form.validate_on_submit():
fight = Fight()
form.populate_obj(fight)
db.session.add(fight)
db.session.commit()
return redirect(url_for('index'))
return render_template('create.html', **locals())
#app.route('/fight/<int:fight_id>/edit', methods=['GET', 'POST'])
def fight_update(fight_id):
fight = Fight.query.get_or_404(fight_id)
form = FightForm(request.form, obj=fight)
if form.validate_on_submit():
form.populate_obj(fight)
db.session.commit()
return redirect(url_for('index'))
return render_template('update.html', **locals())
HTML (templated/index.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Index</title>
</head>
<body>
New
<ul>
{% for f in fights -%}
<li>
<a href="{{ url_for('fight_update', fight_id=f.id) }}">
{{ f.id }} - {{ f.type.name }}
</a>
</li>
{% endfor -%}
</ul>
</body>
</html>
HTML (templates/create.html, templates/update.html)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fight</title>
</head>
<body>
<form method="post">
{{ form.csrf_token }}
{{ form.type.label }} {{ form.type() }}
<input type="submit" value="Go">
</form>
</body>
</html>

Validation on the length of the string in WTform does not work

Validation on the length of the string in Wtform does not work, I tried to use "if form.validate_on_submit ():" in the post method, but it gives the value:False regardless of the parameters in the input field.
main.py
from flask import Flask, render_template, request, redirect, url_for
from forms import Article
from models import db, Library
app = Flask(__name__)
app.config["SECRET_KEY"] = "hard_string_security"
#app.route('/', methods=["GET", "POST"])
def index():
form = Article()
date_db = Library.select()
# if form.validate_on_submit():
if request.method == "POST":
db.connect()
new_article = Library(titleArticle=form.title, contentArticle=form.content)
new_article.save()
db.close()
print(form.validate_on_submit())
return redirect(url_for(".index"))
return render_template("index.html", date_db=date_db, form=form)
forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, Length
class Article(FlaskForm):
title = StringField("Input title arcicle", validators=[Length(min=1, max=5),DataRequired()])
content = TextAreaField("Input content arcicle", validators=[DataRequired()])
submit = SubmitField("Submit")
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index</title>
</head>
<body>
<h2>Create Article</h2>
<form method="post" action="/">
{{ form.title.label }}{{ form.title }}<br>
{{ form.content.label }}{{ form.content }}<br>
{{ form.submit }}<br><br>
</form>
</body>
</html>

Flask form POST request to populate rest of jinja2 template

I have a Flask web form that "searches for members" using a free-text input field. I want to programmatically load-test this website using some command-line tool like curl, instead of mocking user behavior with something like Selenium.
Here is the Python backend:
# test.py
from time import sleep
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class Config:
SECRET_KEY = 'SUPER_SECRET_VALUE'
app = Flask(__name__)
app.config.from_object(Config)
class TestForm(FlaskForm):
member_id = StringField('Member ID', validators=[DataRequired()])
submit = SubmitField('Submit')
#app.route('/test', methods=['GET', 'POST'])
def test():
form = TestForm()
if form.validate_on_submit():
member_id_field = form.member_id
member_id = member_id_field.data
# simulate doing something with `member_id`
sleep(2)
result = 'some result'
return render_template(
'test.html',
title='test',
form=form,
result=result
)
else:
return render_template(
'test.html',
title='test',
form=form
)
if __name__ == '__main__':
app.run()
And here is the HTML form:
<h1>Search for a member</h1>
<form action="" method="post" novalidate>
{{ form.hidden_tag() }}
<p>
{{ form.member_id.label }}
<br>
{{ form.member_id(size=32) }}
</p>
<p>{{ form.submit() }}</p>
{% if result is defined %}
<h1>Result is: {{ result }}</h1>
{% endif %}
</form>
First, I start the server with python3 test.py and then use curl -X POST -H "Content-Type: application/json" -d '{"member_id":"123"}' http://127.0.0.1:5000/test/ to try to send a member ID to the form to trigger the searching logic.
If I go to the website in a browser, I'm able to type in an ID and the Result is: section populates. However, using curl, I don't get anything back:
<h1>Search for a member</h1>
<form action="" method="post" novalidate>
<input id="csrf_token" name="csrf_token" type="hidden" value="BASDHSAHSADHASHDASHDAHSDH">
<p>
<label for="member_id">Member ID</label>
<br>
<input id="member_id" name="member_id" required size="32" type="text" value="123">
</p>
<p><input id="submit" name="submit" type="submit" value="Submit"></p>
If Result is: populates, I know my form is working. So, the using-a-website approach works, but the command line does not. What could the issue be?
This isn't the most optimal solution as it doesn't account for the jinja rendering time, but you could add a layer of flask_restful, developing a REST API for the core data processing piece:
from time import sleep
from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from flask_restful import Api, Resource
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class Config:
SECRET_KEY = 'SUPER_SECRET_VALUE'
def get_data(member_id):
sleep(2)
return f'getting data for {member_id}'
class TestForm(FlaskForm):
member_id = StringField('Member ID', validators=[DataRequired()])
submit = SubmitField('Submit')
class TestApi(Resource):
def get(self):
data = request.get_json(force=True)
get_data(data)
return data, 200
def post(self):
data = request.get_json(force=True)
get_data(data)
return data, 200
app = Flask(__name__)
app.config.from_object(Config)
api_manager = Api(app)
api_manager.add_resource(TestApi, '/api/test')
#app.route('/test', methods=['GET', 'POST'])
def test():
form = TestForm()
if form.validate_on_submit():
member_id_field = form.member_id
member_id = member_id_field.data
# simulate doing something with `member_id`
data = get_data(member_id)
return render_template(
'test.html',
form=form,
result=data
)
else:
return render_template(
'test.html',
form=form
)
if __name__ == '__main__':
app.run()
And then use:
curl -X POST -H "Content-Type: application/json" -d '{"member_id":"123"}' http://127.0.0.1:5000/api/test

Python Flask: Sending a form class argument from another function to prepopulate default

I need to prepopulate form fields with database (dataset) values.
The problem is that I don't know how to send the argument to the form class.
forms.py
# coding: utf-8
from db import produtosalca as produtos ##dataset imports db['table']
from flask_wtf import FlaskForm
from wtforms import TextField, BooleanField, SubmitField, TextAreaField, validators, ValidationError
class ProductForm(FlaskForm,produto):
descricao = TextField("Nome", default=produto.Descricao)
classificacaoFiscal = TextField("NCM", default=produto.ClassificacaoFiscal)
mva = TextField("MVA",default=produto.MVA)
app.py
# coding: utf-8
import os
from werkzeug import secure_filename
from flask import (
Flask, request, send_from_directory, render_template, current_app, flash
)
from db import produtosalca
from forms import ContactForm, ProductForm
app = Flask("alcax")
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))
app.config['MEDIA_ROOT'] = os.path.join(PROJECT_ROOT, 'media_files')
app.secret_key = 'development key'
#app.route('/productform', methods=['GET','POST'])
def productform(product):
form = ProductForm(request.form,product) ## here i was gonna send product
if request.method == 'POST':
'posted'
elif request.method == 'GET':
return render_template('productform.html', form=form.content())
Well everything I tried always get me the error
'produto' is not defined
I'm a beginner in py. Have been researching all night long. Thanks in advance.
Note that class ProductForm(FlaskForm) means ProductForm inherets from FlaskForm. A simple solution for your case would be as follows:
forms.py:
from flask_wtf import FlaskForm
from wtforms import TextField
class ProductForm(FlaskForm):
descricao = TextField("Nome")
classificacaoFiscal = TextField("NCM")
mva = TextField("MVA")
app.py:
from flask import Flask, render_template, redirect, request
from forms import ProductForm
app = Flask(__name__)
app.secret_key = 'development key'
#app.route('/productform', methods=['GET','POST'])
def productform():
form = ProductForm(descricao='default_descricao', classificacaoFiscal='default_classificacaoFiscal', mva='default_mva')
if form.validate_on_submit():
form = ProductForm(request.form)
# Do something with your form and redirect the user to some page.
return render_template('productform.html', form=form)
if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
templates/_formhelpers.html
{% macro render_field(field) %}
<dt>{{ field.label }}
<dd>{{ field(**kwargs)|safe }}
{% if field.errors %}
<ul class=errors>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</dd>
{% endmacro %}
templates/productform.html
<html !DOCTYPE>
<head>
<title>Flask WTF</title>
</head>
<body>
{% from "_formhelpers.html" import render_field %}
<form method="POST" action="/submit">
<dl>
{{ form.hidden_tag() }}
{{ render_field(form.descricao, size=20) }}
{{ render_field(form.classificacaoFiscal, size=20) }}
{{ render_field(form.mva, size=20) }}
</dl>
<input type="submit" value="Go">
</form>
</body>
</html>
Edit to take into account the new queries in the comments below:
forms.py:
class ProductForm(FlaskForm):
descricao = TextField("Nome")
classificacaoFiscal = TextField("NCM")
mva = TextField("MVA")
# ...
def __init__(self, *args, **kwargs):
super(ProductForm, self).__init__(*args, **kwargs)
if 'product' in kwargs:
self.descricao.data = kwargs['product'][0]
self.classificacaoFiscal.data = kwargs['product'][1]
self.mva.data = kwargs['product'][2]
# ...
app.py:
#app.route('/productform', methods=['GET','POST'])
def productform():
# product to be retrieved from your database
form = ProductForm(product=product)
if form.validate_on_submit():
form = ProductForm(request.form)
# Do something with your form and redirect the user to some page.
return render_template('productform.html', form=form)

wtforms hidden field value

I am using WTForms, and I have a problem with hidden fields not returning values, whereas the docs say they should. Here's a simple example:
forms.py:
from wtforms import (Form, TextField, HiddenField)
class TestForm(Form):
fld1 = HiddenField("Field 1")
fld2 = TextField("Field 2")
experiment.html:
{% from "_formshelper.html" import render_field %}
<html>
<body>
<table>
<form method=post action="/exp">
{% for field in form %}
{{ render_field(field) }}
{% endfor %}
<input type=submit value="Post">
</form>
</table>
</body>
</html>
(render_field just puts the label, field and errors in td tags)
experiment.py:
from flask import Flask, request, render_template
from templates.forms import *
from introspection import *
app = Flask(\__name__)
app.config.from_object(\__name__)
db_session = loadSession()
#app.route('/exp', methods=['POST', 'GET'])
def terms():
mydata = db_session.query(Peter).one()
form = TestForm(request.form, mydata)
if request.method == 'POST' and form.validate():
return str(form.data)
return render_template('experiment.html', form = form)
if __name__ == '__main__':
app.run(debug = True)
mydata returns the only row from a table that has 2 fields, fld1 and fld2. fld1 is an integer autoincrement field. The form is populated with that data, so if I run experiment.py, when I submit the form I get:
{'fld2': u'blah blah blah', 'fld1': u'1'}
But if I change fld1 to HiddenField, when I hit submit, I get:
{'fld2': u'blah blah blah', 'fld1': u''}
What am I doing wrong?
I suspect your hidden field is either (1) not getting a value set, or (2) the render_field macro isn't building it correctly. If I had to bet, I'd say your "mydata" object doesn't have the values you expect.
I stripped your code down to the bare minimum, and this works for me. Note I am explicitly giving a value to both fields:
from flask import Flask, render_template, request
from wtforms import Form, TextField, HiddenField
app = Flask(__name__)
class TestForm(Form):
fld1 = HiddenField("Field 1")
fld2 = TextField("Field 2")
#app.route('/', methods=["POST", "GET"])
def index():
form = TestForm(request.values, fld1="foo", fld2="bar")
if request.method == 'POST' and form.validate():
return str(form.data)
return render_template('experiment.html', form = form)
if __name__ == '__main__':
app.run()
and
<html>
<body>
<table>
<form method=post action="/exp">
{% for field in form %}
{{field}}
{% endfor %}
<input type=submit value="Post">
</form>
</table>
</body>
</html>
This gives me {'fld2': u'bar', 'fld1': u'foo'} as I would expect.
Check that mydata has an attribute "fld1" and it has a value. I might set it explicitly like form = TestForm(request.values, obj=mydata) - it doesn't look like WTForms would care, but I've gotten burned by it being weirdly picky sometimes.
If that doesn't work for you, come back and post your HTML and what values mydata has.

Categories