Flask Automatic Forms

Automatically create your database and HTML forms from your model. Short, convenient, but a bit abstract. Short notes.

Flask Templates in Firefox

This is an intermediate article. If you're new to Flask, you might want to start from Hello Flask

Writing HTML form can feel tedious. Luckily, WTForms can automatically:

  • Generate your form (from ORM database model)
  • Validate submitted from on server side
  • Show user errors on form (e.g. "Not a valid email")

Caveats: These are just short code examples. For troughout explanations, read my earlier articles on Flask. This is a demonstration of creating forms automatically. For example, CSRF protecation is not tested.

autoformed.py

As the backend is abstracted with SQLAlchemy, this exact same code works with PostgreSQL just by changing SQLALCHEMY_DATABASE_URI. As there is less setup with SQLite, it's a good idea to test with that first.

When you start using the script, you should provide your own SECRET_KEY.

$ pwgen -s 30 1
QFLl4I17MpsBebNB18nrKPP6aVnf7M
#!/usr/bin/python3
"RSVP autoform"
# Copyright 2020 Tero Karvinen http://TeroKarvinen.com

from flask import Flask, render_template, flash, redirect
from flask_sqlalchemy import SQLAlchemy
from wtforms.ext.sqlalchemy.orm import model_form
from flask_wtf import FlaskForm
import wtforms

app = Flask(__name__)
db = SQLAlchemy(app)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///autoformed.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.config["SECRET_KEY"] = "dUCF)mtd9MAoZ?;R|8*iB^.+TCV//0"

class Reply(db.Model):
	id = db.Column(db.Integer, primary_key=True)
	coming = db.Column(db.Boolean, nullable=False)
	email = db.Column(db.String, nullable=False)
	name = db.Column(db.String, nullable=False)

field_args = { "email": {"validators": [wtforms.validators.Email()]} }
ReplyForm = model_form(model=Reply, base_class=FlaskForm, db_session=db.session, field_args=field_args)

@app.before_first_request
def beforeFirstRequest():
	db.create_all()

@app.route("/", methods=["GET", "POST"])
def index():
	form = ReplyForm()

	if form.validate_on_submit():
		reply = Reply()
		form.populate_obj(reply)
		db.session.add(reply)
		db.session.commit()
		flash("Your reply has been added. Welcome!")
		return redirect("/")
	replies = db.session.query(Reply)
	return render_template("replies.html", form=form, replies=replies)

def main():
	app.run(debug=True)

if __name__ == "__main__":
	main()

templates/base.html - Base template

<!doctype html>
<html lang=en>
	<head>
		<title>Joining an Event Example</title>
		<meta charset="utf-8">
	</head>
	<body>
		{% with messages = get_flashed_messages() %}
		{% if messages %}
		<ul class=flashes>
			{% for message in messages %}
			<li>{{ message }}</li>
			{% endfor %}
		</ul>
		{% endif %}
		{% endwith %}

		{% block body %}
		<h1>Hello Tero</h1>
		{% endblock %}
		<p>Learn Flask automatic forms at <a href="TeroKarvinen.com">TeroKarvinen.com</a>
	</body>

</html>

templates/replies.html - Form and List Template

{% extends "base.html" %}
{% block body %}
<h1>Replies</h1>
<form method=post action="/">
	{{ form.csrf_token }}
	{% for field in form if not field.name in ["csrf_token"] %}
		<p>{{ field.label }}: {{ field }} <b>{{ " ".join(field.errors) }}</b></p>
	{% endfor %}
	<input type="submit">
</form>

<h2>Replies</h2>
{% for reply in replies %}
<p>{{ reply.name }}</p>
{% endfor %}

{% endblock %}

Adminstrivia

Updated: Fixed "reply.html" -> "replies.html" filename, thanks Santtu for spotting that.

This article was tested with Ubuntu Linux 18.04 LTS.