-
-
Save LukasSliacky/50a709d211df0198a2406dbc6a2266fc to your computer and use it in GitHub Desktop.
Dynamic Flask-WTF fields
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{# 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{# 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