-
-
Save giacomomarchioro/f6f3d80d7d48bf8bc8e92486966b7ef3 to your computer and use it in GitHub Desktop.
Dynamic Flask-WTF fields more than one dynamic form JQuery function parametrized with arguments (problem for removing items!)
This file contains 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, SelectField, \ | |
StringField, TextAreaField, SubmitField | |
from wtforms import validators | |
globalvar = [] | |
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', | |
validators=[validators.InputRequired(), validators.Length(max=100)] | |
) | |
lap_time = IntegerField( | |
'Lap time', | |
validators=[validators.InputRequired(), validators.NumberRange(min=1)] | |
) | |
category = SelectField( | |
'Category', | |
choices=[('cat1', 'Category 1'), ('cat2', 'Category 2')] | |
) | |
notes = TextAreaField( | |
'Notes', | |
validators=[validators.Length(max=255)] | |
) | |
class MainForm(FlaskForm): | |
"""Parent form.""" | |
Segnatura = StringField("Segnatura", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
datazione = StringField("datazione", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
tipo_di_supporto_e_qualita = StringField("tipo di supporto e qualita", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
consistenza = StringField("consistenza", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
numerazione_carte = StringField("numerazione carte", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
carte_di_guardia = StringField("carte di guardia", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
prospetto_fascicolazione = StringField("prospetto fascicolazione", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
arrangiamento_fogli_gregory = StringField("arrangiamento fogli gregory", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
dimensioni = StringField("dimensioni", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
rigatura = StringField("rigatura", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
foratura = StringField("foratura", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
legatura = StringField("legatura", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
utenti_email = StringField("utenti email", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
Descrizione_Esterna_Segnatura = StringField("Descrizione Esterna Segnatura", | |
validators=[validators.InputRequired(), validators.Length(max=500)] | |
) | |
laps = FieldList( | |
FormField(LapForm), | |
min_entries=1, | |
max_entries=20 | |
) | |
laps2 = 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) | |
category = db.Column(db.String(4)) | |
notes = db.Column(db.String(255)) | |
# Relationship | |
race = db.relationship( | |
'Race', | |
backref=db.backref('laps', lazy='dynamic', collection_class=list) | |
) | |
# Initialize app | |
app = Flask(__name__) | |
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 | |
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() | |
template_form = LapForm(prefix='laps-_-') | |
template_form2 = LapForm(prefix='laps2-_-') | |
if form.validate_on_submit(): | |
# Create race | |
new_race = Race() | |
globalvar.append(form) | |
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() | |
print("Valido") | |
races = Race.query | |
return render_template( | |
'index.html', | |
form=form, | |
races=races, | |
_template=template_form, | |
_template2 = template_form2 | |
) | |
@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 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 #} | |
{% import "macros.html" as macros %} | |
<html> | |
<head> | |
<title>Lap logging</title> | |
<!-- Required meta tags --> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<!-- Bootstrap CSS --> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous"> | |
{# Import JQuery #} | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> | |
<script> | |
const ID_RE = /(-)_(-)/; | |
/** | |
* Replace the template index of an element (-_-) with the | |
* given index. | |
*/ | |
function replaceTemplateIndex(value, index) { | |
return value.replace(ID_RE, '$1'+index+'$2'); | |
} | |
/** | |
* Adjust the indices of form fields when removing items. | |
*/ | |
function adjustIndices(removedIndex,tgsubf) { | |
var $forms = $(tgsubf); | |
$forms.each(function(i) { | |
var $form = $(this); | |
var index = parseInt($form.data('index')); | |
var newIndex = index - 1; | |
if (index < removedIndex) { | |
// Skip | |
return true; | |
} | |
// This will replace the original index with the new one | |
// only if it is found in the format -num-, preventing | |
// accidental replacing of fields that may have numbers | |
// intheir names. | |
var regex = new RegExp('(-)'+index+'(-)'); | |
var repVal = '$1'+newIndex+'$2'; | |
// Change ID in form itself | |
$form.attr('id', $form.attr('id').replace(index, newIndex)); | |
$form.data('index', newIndex); | |
// Change IDs in form fields | |
$form.find('label, input, select, textarea, h5').each(function(j) { | |
var $item = $(this); | |
if ($item.is('label')) { | |
// Update labels | |
$item.attr('for', $item.attr('for').replace(regex, repVal)); | |
return; | |
} | |
if ($item.is('h5')) { | |
// Update labels | |
$item.attr('id', $item.attr('id').replace(regex, repVal)); | |
$item.text(newIndex); | |
console.log(newIndex); | |
return; | |
} | |
// Update other fields | |
$item.attr('id', $item.attr('id').replace(regex, repVal)); | |
$item.attr('name', $item.attr('name').replace(regex, repVal)); | |
}); | |
}); | |
} | |
/** | |
* Remove a form. | |
*/ | |
function removeForm(anchor,tgsubf) { | |
var $removedForm = $(anchor).closest(tgsubf); | |
var removedIndex = parseInt($removedForm.data('index')); | |
$removedForm.remove(); | |
// Update indices | |
adjustIndices(removedIndex,tgsubf); | |
} | |
/** | |
* Add a new form. | |
*/ | |
function addForm(idt,clst,tgsubf,rmvf) { | |
var $templateForm = $(idt); | |
if ($templateForm.length === 0) { | |
console.log('[ERROR] Cannot find template'); | |
return; | |
} | |
// Get Last index | |
var $lastForm = $('.'+tgsubf).last(); | |
var newIndex = 1; | |
if ($lastForm.length > 0) { | |
newIndex = parseInt($lastForm.data('index')) | |
if (newIndex == 0) { | |
newIndex = newIndex + 2; | |
}else{ | |
newIndex = newIndex +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', replaceTemplateIndex($newForm.attr('id'), newIndex)); | |
$newForm.data('index', newIndex); | |
$newForm.find('label, input, select, textarea,h5').each(function(idx) { | |
var $item = $(this); | |
if ($item.is('label')) { | |
// Update labels | |
$item.attr('for', replaceTemplateIndex($item.attr('for'), newIndex)); | |
return; | |
} | |
if ($item.is('h5')) { | |
// Update labels | |
$item.attr('id', replaceTemplateIndex($item.attr('id'), newIndex)); | |
$item.text(newIndex) | |
return; | |
} | |
// Update other fields | |
$item.attr('id', replaceTemplateIndex($item.attr('id'), newIndex)); | |
$item.attr('name', replaceTemplateIndex($item.attr('name'), newIndex)); | |
}); | |
// Append | |
$(clst).append($newForm); | |
$newForm.addClass(tgsubf); | |
$newForm.removeClass('is-hidden'); | |
$newForm.find(rmvf).click(function(e) {e.preventDefault();removeForm(this,'.'+tgsubf)}); | |
} | |
$(document).ready(function() { | |
$('#add').click(function(e) { | |
e.preventDefault();addForm('#lap-_-form','#subforms-container','subform','.remove_1')}); | |
$('#add_cp').click(function(e) { | |
e.preventDefault();addForm('#lap2-_-form','#subforms-container_cp','subform2','.remove_2')}); | |
$('.remove_1').click(function(e) {e.preventDefault();removeForm(this,'subform')}); | |
$('.remove_2').click(function(e) {e.preventDefault();removeForm(this,'subform2')}); | |
}); | |
</script> | |
<style> | |
.is-hidden { | |
display: none; | |
} | |
</style> | |
</head> | |
<body style="margin:10;padding:10;margin-top:20px;background-color:papayawhip;"> | |
<h3>Laps 1</h3> | |
<form id="lap-form" action="" method="POST" role="form"> | |
{{ form.hidden_tag() }} | |
<div> | |
{{ form.Segnatura.label }} | |
{{ form.Segnatura}} | |
</div> | |
<div> <h3>Descrizioni interne </h3> | |
</div> | |
<hr/> | |
{# Show all subforms #} | |
<div id="subforms-container"> | |
{% for subform in form.laps %} | |
{{ macros.render_lap_form(subform, loop.index0) }} | |
{% endfor %} | |
</div> | |
<a id="add" href="#subforms-container" class="btn btn-primary btn-sm" tabindex="-1" role="button" aria-disabled="true">Aggiungi descrizione interna</a> | |
<div> <h3>Laps 2 </h3> | |
</div> | |
<hr/> | |
{# Show all subforms #} | |
<div id="subforms-container_cp"> | |
{% for subform in form.laps2 %} | |
{{ macros.render_lap_form2(subform, loop.index0) }} | |
{% endfor %} | |
</div> | |
<a id="add_cp" href="#subforms-container_cp" class="btn btn-primary btn-sm" tabindex="-1" role="button" aria-disabled="true">Aggiungi copista</a> | |
<button type="submit">Send</button> | |
</form> | |
{% if form.errors %} | |
{{ form.errors }} | |
{% endif %} | |
{# Form template #} | |
{{ macros.render_lap_form(_template, '_') }} | |
{{ macros.render_lap_form2(_template2, '_') }} | |
{# Show races #} | |
{% for race in races %} | |
<p><a href="{{ url_for('show_race', race_id=race.id) }}">Race {{ race.id }}</a></p> | |
{% endfor %} | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script> | |
</body> | |
</html> |
This file contains 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/macros.html #} | |
{# Render lap form. | |
This macro is intended to render both regular lap subforms (received from the | |
server) and the template form used to dynamically add more forms. | |
Arguments: | |
- subform: Form object to render | |
- index: Index of the form. For proper subforms rendered in the form loop, | |
this should match `loop.index0`, and for the template it should be | |
'_' | |
#} | |
{%- macro render_lap_form(subform, index) %} | |
<div id="lap-{{ index }}-form" class="{% if index != '_' %}subform{% else %}is-hidden{% endif %}" data-index="{{ index }}"> | |
<div> | |
<h5 id="inc-0-">1</h5> | |
{{ subform.runner_name.label }} | |
{{ subform.runner_name }} | |
</div> | |
<div> | |
{{ subform.lap_time.label }} | |
{{ subform.lap_time}} | |
</div> | |
<div> | |
{{ subform.category.label }} | |
{{ subform.category }} | |
</div> | |
<div> | |
{{ subform.notes.label }} | |
{{ subform.notes }} | |
</div> | |
<a class="remove">Remove</a> | |
<hr/> | |
</div> | |
{%- endmacro %} | |
{%- macro render_lap_form2(subform, index) %} | |
<div id="lap-{{ index }}-form" class="{% if index != '_' %}subform{% else %}is-hidden{% endif %}" data-index="{{ index }}"> | |
<div>{{ index }} </div> | |
<div> | |
{{ subform.runner_name.label }} | |
{{ subform.runner_name }} | |
</div> | |
<div> | |
{{ subform.lap_time.label }} | |
{{ subform.lap_time}} | |
</div> | |
<div> | |
{{ subform.category.label }} | |
{{ subform.category }} | |
</div> | |
<div> | |
{{ subform.notes.label }} | |
{{ subform.notes }} | |
</div> | |
<a class="remove">Remove</a> | |
<hr/> | |
</div> | |
{%- endmacro %} |
This file contains 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> | |
<th>Category</th> | |
<th>Notes</th> | |
</tr> | |
</thead> | |
<tbody> | |
{% for lap in race.laps %} | |
<tr> | |
<td>{{ lap.runner_name }}</td> | |
<td>{{ lap.lap_time }}</td> | |
<td> | |
{%- if lap.category == 'cat1' %} | |
Category 1 | |
{%- elif lap.category == 'cat2' %} | |
Category 2 | |
{%- else %} | |
Unknown | |
{%- endif %} | |
</td> | |
<td>{{ lap.notes }}</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