Search code examples
pythonflaskflask-wtformsflask-mongoengine

Flask-WTF SelectMultipleField populates with database IDs instead of names


I would like to preface this with saying that english is not my mother tongue, if any of my explanations are vague or don't make sense, please let me know and I will attempt to make them clearer.

I have an issue with populating a SelectMultipleField with the correct data in Flask-WTF. As can be seen in the code below, I pass the user object to the form. This works flawlessly as it fills in all fields when the page is rendered. The issue I run into is that the SelectMultipleField is populated with role IDs and not the names of the roles.

In the forms section I generate a list, choices, which hold all the available choices that should be displayed. I can invert this to choices.append((role.id, role.name)) and it will populate the field with names instead of IDs. However, if I do that the roles assigned to the user object (from the database) will not show up as preselected.

How can I make it so that the field is prepopulated with names and not IDs and keep the assigned roles preselected?

Any guidance or nudge in the right direction will be greatly appreciated. If any information is lacking, that I did not think to add, please let me know and I'll add it.

# Models
class Role(db.Document, RoleMixin):
    meta = {'collection': 'role'} 
    name = db.StringField(max_length=80, unique=True)
    description = db.StringField(max_length=255)

    def __repr__(self):
        return '<Role {}>'.format(self.name)

    def __str__(self):
        return self.name

class User(db.Document, UserMixin):
    meta = {'collection': 'User'}   
    first_name = db.StringField(max_length=30)
    last_name = db.StringField(max_length=30)
    email = db.StringField(max_length=120)
    password_hash = db.StringField(max_length=255)
    active = db.BooleanField(default=True)
    roles = db.ListField(db.ReferenceField(Role), default=[])

    def __repr__(self):
        return '<User {}>'.format(self.email)

    def __str__(self):
        return self.email

# Forms
class EditUserForm(FlaskForm):
    choices = []
    for role in Role.objects:
        choices.append((role.name, role.id))

    first_name = StringField('First name', validators=[DataRequired()])
    last_name = StringField('Last name', validators=[DataRequired()])
    email = StringField('Email', validators=[DataRequired(), Email()])
    password = PasswordField('Password')
    password2 = PasswordField('Repeat password', validators=[EqualTo('password')])
    roles = SelectMultipleField('User roles', choices=choices)
    submit = SubmitField('Save')

# Routes
@app.route('/users/edit/<id>', methods=['GET', 'POST'])
@roles_required('admin')
def edit_user(id):
    user = User.objects(id=id).first()
    form = EditUserForm(obj=user)

    if form.validate_on_submit() and request.method == 'POST':
        user.first_name=form.first_name.data
        user.last_name=form.last_name.data
        user.email=form.email.data
        user.roles = []

        for role in form.roles.data:
            r = Role.objects(name=role).first()
            user.set_role(r.id)

        if form.password.data:
            user.set_password(form.password.data)

        user.save()
        return redirect(url_for('users'))

    return render_template('edit_user.html', title='Edit user', user=user, form=form)

Template:

{% extends "layout.html" %}
{% set active_page = "edit_user" %}
{% block jumbotron %}

    <h1>{{ title }}</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.first_name.label }}<br>
            {{ form.first_name(size=32) }}<br>
            {% for error in form.first_name.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.last_name.label }}<br>
            {{ form.last_name(size=32) }}<br>
            {% for error in form.last_name.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.email.label }}<br>
            {{ form.email(size=32) }}<br>
            {% for error in form.email.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password2.label }}<br>
            {{ form.password2(size=32) }}<br>
            {% for error in form.password2.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.roles.label }}<br>
            {{ form.roles(size=32) }}<br>
            {% for error in form.roles.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>

{% endblock %}

I attempted to create my own object, with the code below, to pass to the form, however I was unsuccessful. When passing that object to the form, no fields were populated.

user = User.objects(id=id).first()
temps = []

u = user.to_mongo()
u['_id'] = {'$oid': str(u['_id'])}
temp_roles = []
for role in user.roles:
    r = role.to_mongo()
    r['_id'] = {'$oid': str(r['_id'])}
    temp_roles.append(r)

u['roles'] = temp_roles

Solution

  • I think PRMoureu is right.

    You could choices.append((role.name, role.name))

    FYI.

    The choices of SelectField is a list of tuple (value, label) pairs, please go to this link for more details.

    Select fields keep a choices property which is a sequence of (value, label) pairs. The value portion can be any type in theory, but as form data is sent by the browser as strings, you will need to provide a function which can coerce the string representation back to a comparable object.

    The default coerce is unicode, int if you use (id, name).