Skip to content

Instantly share code, notes, and snippets.

@rmed rmed/app.py
Last active Aug 13, 2019

Embed
What would you like to do?
Dynamic Flask-WTF fields
# -*- coding: utf-8 -*-
# app.py
from flask import Flask, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_wtf import FlaskForm
from wtforms import Form, FieldList, FormField, IntegerField, StringField, \
SubmitField
class LapForm(Form):
"""Subform.
CSRF is disabled for this subform (using `Form` as parent class) because
it is never used by itself.
"""
runner_name = StringField('Runner name')
lap_time = IntegerField('Lap time')
class MainForm(FlaskForm):
"""Parent form."""
laps = FieldList(
FormField(LapForm),
min_entries=1,
max_entries=20
)
# Create models
db = SQLAlchemy()
class Race(db.Model):
"""Stores races."""
__tablename__ = 'races'
id = db.Column(db.Integer, primary_key=True)
class Lap(db.Model):
"""Stores laps of a race."""
__tablename__ = 'laps'
id = db.Column(db.Integer, primary_key=True)
race_id = db.Column(db.Integer, db.ForeignKey('races.id'))
runner_name = db.Column(db.String(100))
lap_time = db.Column(db.Integer)
# Relationship
race = db.relationship(
'Race',
backref=db.backref('laps', lazy='dynamic', collection_class=list)
)
# Initialize app
app = Flask(__name__)
app.config['SECRET_KEY'] = 'sosecret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///test.db'
db.init_app(app)
db.create_all(app=app)
@app.route('/', methods=['GET', 'POST'])
def index():
form = MainForm()
if form.validate_on_submit():
# Create race
new_race = Race()
db.session.add(new_race)
for lap in form.laps.data:
new_lap = Lap(**lap)
# Add to race
new_race.laps.append(new_lap)
db.session.commit()
races = Race.query
return render_template(
'index.html',
form=form,
races=races
)
@app.route('/<race_id>', methods=['GET'])
def show_race(race_id):
"""Show the details of a race."""
race = Race.query.filter_by(id=race_id).first()
return render_template(
'show.html',
race=race
)
if __name__ == '__main__':
app.run()
{# templates/index.html #}
<html>
<head>
<title>Lap logging</title>
{# Import JQuery #}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
/**
* Adjust the indices of form fields when removing items.
*/
function adjustIndices(removedIndex) {
var $forms = $('.subform');
$forms.each(function(i) {
var $form = $(this);
var index = parseInt($form.data('index'));
var newIndex = index - 1;
if (index < removedIndex) {
// Skip
return true;
}
// Change ID in form itself
$form.attr('id', $form.attr('id').replace(index, newIndex));
$form.data('index', newIndex);
// Change IDs in form inputs
$form.find('input').each(function(j) {
var $item = $(this);
$item.attr('id', $item.attr('id').replace(index, newIndex));
$item.attr('name', $item.attr('name').replace(index, newIndex));
});
});
}
/**
* Remove a form.
*/
function removeForm() {
var $removedForm = $(this).closest('.subform');
var removedIndex = parseInt($removedForm.data('index'));
$removedForm.remove();
// Update indices
adjustIndices(removedIndex);
}
/**
* Add a new form.
*/
function addForm() {
var $templateForm = $('#lap-_-form');
if (!$templateForm) {
console.log('[ERROR] Cannot find template');
return;
}
// Get Last index
var $lastForm = $('.subform').last();
var newIndex = 0;
if ($lastForm.length > 0) {
newIndex = parseInt($lastForm.data('index')) + 1;
}
// Maximum of 20 subforms
if (newIndex > 20) {
console.log('[WARNING] Reached maximum number of elements');
return;
}
// Add elements
var $newForm = $templateForm.clone();
$newForm.attr('id', $newForm.attr('id').replace('_', newIndex));
$newForm.data('index', newIndex);
$newForm.find('input').each(function(idx) {
var $item = $(this);
$item.attr('id', $item.attr('id').replace('_', newIndex));
$item.attr('name', $item.attr('name').replace('_', newIndex));
});
// Append
$('#subforms-container').append($newForm);
$newForm.addClass('subform');
$newForm.removeClass('is-hidden');
$newForm.find('.remove').click(removeForm);
}
$(document).ready(function() {
$('#add').click(addForm);
$('.remove').click(removeForm);
});
</script>
<style>
.is-hidden {
display: none;
}
</style>
</head>
<body>
<a id="add" href="#">Add Lap</a>
{# Show all subforms #}
<form id="lap-form" action="" method="POST" role="form">
{{ form.hidden_tag() }}
<div id="subforms-container">
{% for subform in form.laps %}
<div id="lap-{{ loop.index0 }}-form" class="subform" data-index="{{ loop.index0 }}">
{{ subform.runner_name.label }}
{{ subform.runner_name }}
{{ subform.lap_time.label }}
{{ subform.lap_time}}
<a class="remove" href="#">Remove</a>
</div>
{% endfor %}
</div>
<button type="submit">Send</button>
</form>
{% if form.errors %}
{{ form.errors }}
{% endif %}
{# Form template #}
<div id="lap-_-form" class="is-hidden" data-index="_">
<label for="laps-_-runner_name">Runner name</label>
<input id="laps-_-runner_name" name="laps-_-runner_name" type="text" value="">
<label for="laps-_-lap_time">Lap time</label>
<input id="laps-_-lap_time" name="laps-_-lap_time" type="text">
<a class="remove" href="#">Remove</a>
</div>
{# Show submitted data #}
{% if data is defined %}
<p>Received data: {{ data }}</p>
{% endif %}
{# Show races #}
{% for race in races %}
<p><a href="{{ url_for('show_race', race_id=race.id) }}">Race {{ race.id }}</a></p>
{% endfor %}
</body>
</html>
{# templates/show.html #}
<html>
<head>
<title>Race details</title>
</head>
<body>
<a href="{{ url_for('index') }}">Back to index</a>
{% if not race %}
<p>Could not find race details</p>
{% else %}
<table>
<thead>
<tr>
<th>Runner name</th>
<th>Lap time</th>
</tr>
</thead>
<tbody>
{% for lap in race.laps %}
<tr>
<td>{{ lap.runner_name }}</td>
<td>{{ lap.lap_time }}</td>
</tr>
{% endfor%}
</tbody>
</table>
{% endif %}
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.