Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a form with a varying number of repeated subform in Flask/WTForms

My model currently has three related objects (there are more, but only three are relevant to this problem). User, Network, and Email. What I want to be able to do is have a defined set of Networks, and to allow each User to have an Email address on each Network (these are slightly more complex, but I've 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 view:

@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')

The outer form's HTML:

{% 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 both Chrome and Firefox:

Screenshot of horrid-looking form

So I'm obviously doing something wrong, since:

  1. The sub-form's widgets look nothing like those of the outer form, and
  2. The sub-form keeps being shown over the top of the outer form.

Where have I gone wrong with this? I tried not using wtf.quick_form() but couldn't get it to look right manually, either. To do that, I replaced the {{ wtf.quick_form() }} with this:

        <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 render this, it appears as below in my browser:

Screenshot of bad form

This has the virtue of being consistent, but isn't the look I want to get using flask-bootstrap. I'm struggling to figure out which approach will get me where I want to go more easily.

SOLUTION

Changing the form html to this gave me the UI elements I was shooting for. The key was understanding that "class_" could be passed in and would be rendered 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>

Yielding this: Screenshot of correct form

like image 984
Bret Avatar asked Jun 05 '15 13:06

Bret


People also ask

What does form Validate_on_submit do flask?

Validating Forms Note that you don't have to pass request. form to Flask-WTF; it will load automatically. And the convenient validate_on_submit will check if it is a POST request and if it is valid. If your forms include validation, you'll need to add to your template to display any error messages.

What is form Hidden_tag ()?

The form. hidden_tag() template argument generates a hidden field that includes a token that is used to protect the form against CSRF attacks. All you need to do to have the form protected is include this hidden field and have the SECRET_KEY variable defined in the Flask configuration.

What is WTForms in flask?

WTForms is a Python library that provides flexible web form rendering. You can use it to render text fields, text areas, password fields, radio buttons, and others. WTForms also provides powerful data validation using different validators, which validate that the data the user submits meets certain criteria you define.

How do I import FlaskForm?

from flask_uploads import UploadSet, IMAGES from flask_wtf import FlaskForm from flask_wtf. file import FileField, FileAllowed, FileRequired images = UploadSet('images', IMAGES) class UploadForm(FlaskForm): upload = FileField('image', validators=[ FileRequired(), FileAllowed(images, 'Images only! ') ])


1 Answers

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

like image 69
Bret Avatar answered Sep 29 '22 06:09

Bret