Skip to content

Instantly share code, notes, and snippets.

@kageurufu
Created October 3, 2013 17:42
Show Gist options
  • Star 58 You must be signed in to star a gist
  • Fork 14 You must be signed in to fork a gist
  • Save kageurufu/6813878 to your computer and use it in GitHub Desktop.
Save kageurufu/6813878 to your computer and use it in GitHub Desktop.
Flask-WTF FieldLists with Dynamic Entries
from flask import Flask
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.wtf import Form
from flask.ext.babel import gettext
from wtforms import SelectField, TelField, TextField, FormField, Fieldlist, SubmitField
from wtforms.validators import Optional, Required
app = Flask(__name__)
db = SQLAlchemy(app)
class User(db.Model):
id = db.Column(db.Integer(), primary_key=True)
username = db.Column(db.String(50))
phone = db.relationship(lambda: PhoneNumber)
class PhoneNumber(db.Model):
id = db.Column(db.Integer(), primary_key=True)
user_id = db.Column(db.Integer(), db.ForeignKey(User.id))
phonetype = db.Column(db.String(10))
number = db.Column(db.String(20))
ext = db.Column(db.String(10))
class PhoneNumberForm(Form):
phonetype = SelectField(gettext("Type"), choices=[(c, c) for c in ['Mobile', 'Home', 'Work', 'Fax', 'Other']])
number = TelField(gettext("Number"), validators=[Required()])
ext = TextField(gettext("Notes"), validators=[Optional()])
def __init__(self, csrf_enabled=False, *args, **kwargs):
super(PhoneNumberForm, self).__init__(csrf_enabled=False, *args, **kwargs)
class ModelFieldList(FieldList):
def __init__(self, *args, **kwargs):
self.model = kwargs.pop("model", None)
super(ModelFieldList, self).__init__(*args, **kwargs)
if not self.model:
raise ValueError("ModelFieldList requires model to be set")
def populate_obj(self, obj, name):
while len(getattr(obj, name)) < len(self.entries):
newModel = self.model()
db.session.add(newModel)
getattr(obj, name).append(newModel)
while len(getattr(obj, name)) > len(self.entries):
db.session.delete(getattr(obj, name).pop())
super(ModelFieldList, self).populate_obj(obj, name)
class UserForm(Form):
username = TextField(gettext("Username"), validators=[Required()])
phone = ModelFieldList(FormField(PhoneNumberForm), model=PhoneNumber)
submit = SubmitField(gettext("Submit"))
@app.route("/")
def index()
user = User.query.first()
form = UserForm(obj = user)
form.phone.min_entries=3
if form.validate_on_submit():
form.populate_obj(user)
db.session.commit()
return render_template("page.html", form = form)
if __name__ == '__main__':
db.drop_all()
db.create_all()
db.session.add(User(username="Frank"))
db.session.commit()
app.run(debug=True)
<!doctype html>
<html>
<head>
<title>User Form Demo</title>
</head>
<body>
{{ form.username.label }}: {{ form.username }}
<div data-toggle="fieldset" id="phone-fieldset">
{{ form.phone.label }} <button type="button" data-toggle="fieldset-add-row" data-target="#phone-fieldset">+</button>
<table>
<tr>
<th>Type</th>
<th>Number</th>
<th>Extension</th>
<th></th>
</tr>
{% for phone in form.phone %}
<tr data-toggle="fieldset-entry">
<td>{{ phone.phonetype }}</td>
<td>{{ phone.number }}</td>
<td>{{ phone.ext }}</td>
<td><button type="button" data-toggle="fieldset-remove-row" id="phone-{{loop.index0}}-remove">-</button></td>
</tr>
{% endfor %}
</table>
</div>
{{ form.submit }}
<script src="{{ url_for("static", filename="js/jquery-1.10.2.min.js") }}"></script>
<script src="{{ url_for("static", filename="js/page.js") }}"></script>
</body>
</html>
$(function() {
$("div[data-toggle=fieldset]").each(function() {
var $this = $(this);
//Add new entry
$this.find("button[data-toggle=fieldset-add-row]").click(function() {
var target = $($(this).data("target"))
console.log(target);
var oldrow = target.find("div[data-toggle=fieldset-entry]:last");
var row = oldrow.clone(true, true);
console.log(row.find(":input")[0]);
var elem_id = row.find(":input")[0].id;
var elem_num = parseInt(elem_id.replace(/.*-(\d{1,4})-.*/m, '$1')) + 1;
row.attr('data-id', elem_num);
row.find(":input").each(function() {
console.log(this);
var id = $(this).attr('id').replace('-' + (elem_num - 1) + '-', '-' + (elem_num) + '-');
$(this).attr('name', id).attr('id', id).val('').removeAttr("checked");
});
oldrow.after(row);
}); //End add new entry
//Remove row
$this.find("button[data-toggle=fieldset-remove-row]").click(function() {
if($this.find("div[data-toggle=fieldset-entry]").length > 1) {
var thisRow = $(this).closest("div[data-toggle=fieldset-entry]");
thisRow.remove();
}
}); //End remove row
});
});
Copy link

ghost commented Jun 24, 2015

I suggest the following processes before the code sample above can work:

  • Code change
import os

from flask import Flask, render_template
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.wtf import Form
from flask.ext.babel import gettext
from wtforms.fields import SelectField, TextField, FormField, SubmitField
from flask.ext.wtf.html5 import TelField
from wtforms import FieldList
from wtforms.validators import Optional, Required

app = Flask(__name__)
app.config.from_pyfile('app.cfg')
db = SQLAlchemy(app)

where app.cfg contains:

import os

basedir = os.path.abspath(os.path.dirname(__file__))
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db')

SQLALCHEMY_ECHO = True                                                                               
SECRET_KEY = os.urandom(24)
# Disable debugging
DEBUG = True
  • File name renaming from flask.py to any other name, e.g. app.py
  • The Javascript isn't working and may need a retouching- I've not totally verified this.

@sebkouba
Copy link

sebkouba commented Jan 3, 2016

Thanks for providing this code. It was a good starting point for me.
I've created a version of this that works out of the box: https://github.com/sebkouba/dynamic-flask-form

@rienafairefr
Copy link

I've tried to expand this a bit

https://github.com/rienafairefr/dynamic-flask-form

In this case, there is a hidden first entry, that way we can show a list of field that is empty, without losing the ability to add a new row (this code adds a copy of the last row, in JS)

@afro-coder
Copy link

Has anyone tried getting a Dynamic output as such:
1.Question
RadioField1 RadioField1
2.Question
RadioField2 RadioField2

I tried FieldList but couldn't render a unique id for each pair.

@paulegradie
Copy link

I've been looking everywhere for an answer to this question -- seems like you need to programatically add the new row in the backend and then redirect the same/refresh the page with the new updated form each time you click the button :/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment