Creating a form with a different number of repeating subformats in Flask / WTForms

There are currently three related objects in my model (there are more, but only three relate to this problem). User, network and email. What I want to do is to have a specific set of networks and allow each user to have an email address on each network (they are a bit more complicated, but I cut them down to what I think is relevant).

class User(UserMixin, db.Model): """ The User object. """ __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) # email = db.Column(db.String(64), unique=True, index=True) username = db.Column(db.String(64), unique=True, index=True) password_hash = db.Column(db.String(128)) firstname = db.Column(db.String(64)) lastname = db.Column(db.String(64), unique=False, index=True) email = db.relationship('Email', backref='user') class Network(db.Model): __tablename__ = 'networks' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), index=True) emails = db.relationship('Email', backref='network', lazy='dynamic') class Email(db.Model): __tablename__ = 'emails' id = db.Column(db.Integer, primary_key=True) network_id = db.Column(db.Integer, db.ForeignKey('networks.id')) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) address = db.Column(db.String(64)) 

My opinion:

 @main.route('/edit-profile', methods=['GET', 'POST']) @login_required def edit_profile(): form = EditProfileForm(obj=current_user) form.email.min_entries=Network.query.count() if form.validate_on_submit(): form.populate_obj(current_user) db.session.add(current_user) db.session.commit() flash("Your profile has been updated.") return redirect(url_for('.user', username=current_user.username)) return render_template('edit_profile.html', form=form) 

And forms:

 class EmailForm(Form): id = HiddenField('Id') address = StringField('Address', validators=[DataRequired(), Email()]) network = QuerySelectField(query_factory=get_networks) class EditProfileForm(Form): username = StringField('Username', validators=[Length(0, 64), Regexp('[A-Za-z0-9_\.\-]'), DataRequired()]) firstname = StringField('First name', validators=[Length(0, 64), DataRequired()]) lastname = StringField('Last name', validators=[Length(0, 64), DataRequired()]) email = ModelFieldList(FormField(EmailForm), model=Email) submit = SubmitField('Submit') 

External HTML Form:

 {% extends "base.html" %} {% import "bootstrap/wtf.html" as wtf %} {% block title %}Edit Profile{% endblock %} {% block page_content %} <div class="page-header"> <h1>Edit Your Profile</h1> </div> <div class="col-md-8"> {{ wtf.quick_form(form) }} </div> {% endblock %} 

Here's what it looks like in Chrome and Firefox:

Screenshot of horrid-looking form

Therefore, I obviously am doing something wrong, because:

  • Subform widgets are not like external forms and
  • The subform is stored in the upper part of the external form.

Where did I go wrong with that? I did not try to use wtf.quick_form (), but could not make it look right manually. To do this, I replaced {{wtf.quick_form ()}} as follows:

  <label>{{ form.username.label }}</label> {{ form.username }} <label>{{ form.firstname.label }}</label> {{ form.firstname }} <label>{{ form.lastname.label }}</label> {{ form.lastname }} <div data-toggle="fieldset" id="email-fieldset"> {{ form.email.label }} <table class="ui table"> <thead> <th>Network</th> <th>Address</th> <th> {{ form_button(url_for('main.add_email'), icon ('plus')) }} </th> </thead> <tbody> {% for e in form.email %} <tr data-toggle="fieldset-entry"> <td>{{ e.network }}</td> <td>{{ e.address }}</td> <td> {{ form_button(url_for('main.remove_email', id=loop.index), icon ('remove')) }} </td> </tr> {% endfor %} </tbody> </table> </div> {{ form.submit }} 

When I do this, it displays in my browser below:

Screenshots of bad form

This has the power to be consistent, but this is not the look I want using flash bootstrap. I'm struggling to figure out which approach will lead me to where I want to go easier.

Decision

Changing the html form to this gave me the user interface elements that I was filming. The key was understanding that "class_" could be passed and would be displayed in the output html as "class".

  <div class="form-group required"><label class="control-label">{{ form.username.label }}</label> {{ form.username(class_='form-control') }}</div> <div class="form-group required"><label class="control-label">{{ form.firstname.label }}</label> {{ form.firstname(class_='form-control') }}</div> <div class="form-group required"><label class="control-label">{{ form.lastname.label }}</label> {{ form.lastname(class_='form-control') }}</div> <div data-toggle="fieldset" id="email-fieldset" class="form-group"> {{ form.email.label }} <table class="ui table"> <thead> <th>Network</th> <th>Address</th> <th> {{ form_button(url_for('main.add_email'), icon ('plus')) }} </th> </thead> <tbody> {% for e in form.email %} <tr data-toggle="fieldset-entry"> <td>{{ e.network(class_='form-control') }}</td> <td>{{ e.address(class_='form-control') }}</td> <td> {{ form_button(url_for('main.remove_email', id=loop.index), icon ('remove')) }} </td> </tr> {% endfor %} </tbody> </table> </div> 

Simplification: Screenshot of correct form

+5
source share
1 answer

The answer was to simply pass "class_" to each field constructor in the .html form.

+2
source

All Articles