Question

There are many questions here on SO with titles that sound similar to what I'm about to describe but as far as I can tell from literally hours of research, this question is unique. So here goes!

I'm writing my first Flask app. I'm using SQLAlchemy for the model layer and WTForms to handle forms. The app is going to be a lightweight personal finance manager that I probably will not actually use for for serious biz. I have one table for a list of all transactions and another for all expense categories (groceries, clothing, etc). The transaction table has a column ("category") which references the Category table. In the view, I represent the list of categories with a element.

My issue is that when editing a transaction, I can't figure out how to tell WTForms to set the element to a specific pre-defined value. (Yes, I know that you can set a default value at the time that the form is defined, this is not what I am asking.)

The model looks like this (with irrelevant fields removed):

class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), nullable=False, unique=True)
    # ...

class Trans(db.Model):
    # ...
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
    category = db.relationship('Category',
                                backref=db.backref('trans', lazy='dynamic'))
    # ...

forms.py:

def category_choices():
    return [('0', '')] + [(c.id, c.name) for c in Category.query.all()]

class TransactionForm(Form):
    # ...
    category = SelectField('Category', coerce=int, validators=[InputRequired()])
    # ...

The route (POST not yet implemented):

@app.route('/transactions/edit/<trans_id>', methods=['GET', 'POST'])
def trans_edit(trans_id):
    transaction = Trans.query.get(trans_id)
    form = forms.TransactionForm(obj=transaction)
    form.category.choices = forms.category_choices()
    form.category.default = str(transaction.category.id)
    #raise Exception # for debugging
    return render_template('trans.html',
                           title='Edit Transaction',
                           form=form)

And finally, the template (Jinja2):

{{ form.category(class='trans-category input-medium') }}

As you can see in the route, I set form.category.default from the transaction.category.id, but this doesn't work. I think my issue is that I'm setting "default" after the form has been created. Which I'm rather forced to because the model comes from the database via SQLAlchemy. The root cause seems to be that form.category is an object (due to the relationship), which WTForms can't seem to handle easily. I can't have been the first one to come across this... Do I need to rework the model to be more WTForms compatible? What are my options?

Thanks!

Was it helpful?

Solution

I alluded to this in my comment. It sounds like you might benefit from using WTForm's SQLAlchemy extension. This will create a dropdown list for categories in your trans form.

My example's use case is slightly different. I am relating blog post's to topic. That is, many posts share one topic. I image in your case, many transactions share one category.

Form

from wtforms.ext.sqlalchemy.fields import QuerySelectField  #import the ext.

def enabled_topics(): # query the topics (a.k.a categories)
    return Topic.query.all()

class PostForm(Form):  # create your form
    title = StringField(u'title', validators=[DataRequired()])
    body = StringField(u'Text', widget=TextArea())
    topic = QuerySelectField(query_factory=enabled_topics, allow_blank=True)

models The important part here is a) making sure you have the relationship correctly defined, and b.) adding topic to your init since you use it to create a new entry.

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80))
    body = db.Column(db.Text)
    # one-to-many with Topic
    topic = db.relationship('Topic', backref=db.backref('post', lazy='dynamic'))

def __init__(self, title, body, topic):
        self.title = title
        self.body = body
        self.topic = topic

class Topic(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50))

    def __init__(self, name):
        self.name = name

view Nothing special here. Just a regular view that generates a form and processes submitted results.

@app.route('/create', methods=['GET', 'POST'])
@login_required
def create_post():
    form = PostForm()
    if form.validate_on_submit():
        post = Post(title=form.title.data, body=form.body.data,
                    topic=form.topic.data)
        db.session.add(post)
        db.session.commit()
        Topic.update_counts()
        flash('Your post has been published.')
        return redirect(url_for('display_post', url=url))
    posts = Post.query.all()
    return render_template('create_post.html', form=form, posts=posts)

template Nothing fancy here either. Just be sure to render the field in the template like you would a basic field. No fancy loop required since WTForms Sqlalchemy extensions does all that for you.

{% extends "base.html" %}
{% block title %}Create/Edit New Post{% endblock %}
{% block content %}
<H3>Create/Edit Post</H3>
<form action="" method=post>
   {{form.hidden_tag()}}
   <dl>
      <dt>Title:
      <dd>{{ form.title }}
      <dt>Post:
      <dd>{{ form.body(cols="35", rows="20") }}
      <dt>Topic:
      <dd>{{ form.topic }}
   </dl>
   <p>
      <input type=submit value="Publish">
</form>
{% endblock %}

That's It! Now my post form has a topic dropdown list. To use your terminology, when you load a transaction the default category for that transaction will be highlighted in the dropdown list. The correct way to state this is to say that the category associated with the transaction is loaded via the relationship defined in the trans model.

Also note, there is also a multisellect SQLAlchemy extension in case one transaction has many 'default' categories.

Now, my issue is how to deal with many-to-many relationships.... I'm trying to pass a string of tags that are stored in a many-to-many table to a TextArea field. No SQLAlchemy extension for that!

I posted that question here

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top