1

I am learning Flask and I have found various snippets which show how to define models with SQLAlchemy, REST API with Flask-restless and forms with Flask-wtf (I'm not very familiar with REST API). More precisely I took inspiration from:

However I have not been able to create a fully working example. Building on the bits you can find online, I want to create a model with 2 classes Person and Computer (a Person can be associated with several Computers) and a form to add a new Person. Here is the code I have assembled.

The layout is the following:

test_flask/
├── test_flask.py
├── config.py
└── templates
    └── new_person.html

The main file test_flask.py contains:

from flask import Flask, request, flash, redirect, render_template, url_for
import flask.ext.sqlalchemy
from wtforms.ext.sqlalchemy.orm import model_form
import flask.ext.restless
from flaskext.wtf import Form

# Create the Flask application and the Flask-SQLAlchemy object.
app = Flask(__name__)
app.config.from_object('config')
db = flask.ext.sqlalchemy.SQLAlchemy(app)

# Create the Flask-SQLALchemy models.
class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode, unique=True)
    birth_date = db.Column(db.Date)


class Computer(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode, unique=True)
    vendor = db.Column(db.Unicode)
    purchase_time = db.Column(db.DateTime)
    owner_id = db.Column(db.Integer, db.ForeignKey('person.id'))
    owner = db.relationship('Person', backref=db.backref('computers',
                                                         lazy='dynamic'))

# Create the database tables.
db.create_all()

# Create the Flask-Restless API manager.
manager = flask.ext.restless.APIManager(app, flask_sqlalchemy_db=db)

# Create API endpoints.
manager.create_api(Person, methods=['GET', 'POST', 'DELETE'])
manager.create_api(Computer, methods=['GET'])

# Create a form class for class Person.
PersonForm = model_form(Person, base_class=Form)


@app.route('/')
def hello():
    return 'Hello World'


@app.route("/api/new_person")
def new_person():
    # The new person
    person = Person()
    # Create a form
    form = PersonForm(request.form, person)
    if form.validate_on_submit():
        form.populate_obj(person)
        person.post()
        flash("new person %s inserted updated" % person)
        return redirect(url_for("new_person"))
    return render_template("new_person.html", form=form)

if __name__ == '__main__':
    # start the flask loop
    app.run()

The config.py file contains:

DEBUG = True
WTF_CSRF_SECRET_KEY = 'a random string'
SECRET_KEY = 'you-will-never-guess'
WTF_CSRF_ENABLED = True
SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/test.db'

The template new_person.html contains:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
    "http://www.w3.org/TR/html4/strict.dtd">
<html lang="en">
  <head>
    <meta http-equiv="content-type" content="application/json">
    <title>New person</title>
  </head>
{% block body %}
    <form action="new_person" method=post class=add-entry>
      <dl>
        <dt>Name:
        <dd>{{form.name}}
        <dt>Birth date:
        <dd>{{form.birth_date}}
        <dd><input type=submit value=submit>
      </dl>
    </form>
{% endblock %}
</html>

I can see the form to add a new person at http://127.0.0.1:5000/api/new_person/ but I got a "Method Not Allowed" error when submitting it.

Community
  • 1
  • 1
Mathieu Dubois
  • 1,054
  • 3
  • 14
  • 22
  • @Martijn Pieters I added the links to the tutorial I followed (I don't use `WTF-Alchemy` which is supposed to be slightly better). My problem is precisely to glue all the snippets together. – Mathieu Dubois Mar 30 '16 at 14:26
  • My gut feeling is that the `person.post()` method (which I believe is the one that insert the new `Person`) is in fact never called... I have tried to change the form `action` to `api/person` but without success. – Mathieu Dubois Mar 30 '16 at 14:44
  • Actually, where did `person.post()` come from? That won't add the new instance to your database.. – Martijn Pieters Mar 30 '16 at 14:49
  • I copied this part from [here](http://flask.pocoo.org/snippets/60/) (and renamed `model` to `person`) but I realize that they in fact use 'put()'... I don't think that changes anything since this code is never called. – Mathieu Dubois Mar 30 '16 at 14:55
  • Are you *certain* the code is never called? Though I suspect you need to tell the form that the `id` field is not required for the form to be valid. – Martijn Pieters Mar 30 '16 at 14:58
  • Hrm, if you were to use WTF-Alchemy then [primary key fields are automatically ignored by a form](https://wtforms-alchemy.readthedocs.org/en/latest/column_conversion.html). – Martijn Pieters Mar 30 '16 at 15:00
  • Another [perhaps useful snippet](http://flask.pocoo.org/docs/0.10/patterns/wtforms/) to ease the form rendering. And you can just loop over the `form` object to get each field in definition order: `{% for field in form %}{% render_field(field) %}{% endfor %}`. – Martijn Pieters Mar 30 '16 at 15:11
  • I have convert my code to use WTF-Alchemy and change the `new_person` function. Now `person.put()` is not defined... – Mathieu Dubois Mar 30 '16 at 15:29
  • Ignore the `person.put()` method. I'm not sure what that snippet is based on, but it is not based on the current Flask-SQLAlchemy models. See my answer for how to add objects to the database instead, I link to the right documentation. – Martijn Pieters Mar 30 '16 at 15:32
  • I'm going to have to revert your edit now; please don't treat questions on Stack Overflow as an ongoing debugging session. I addressed the issues in your original question (more than I should have really). Your updates show that you are expecting Flask-WTF to deal with JSON input, but it can only handle *form data* (e.g. what a browser sends, encoded to `application/x-www-form-urlencoded` or `multipart/form-data`). – Martijn Pieters Mar 30 '16 at 16:57
  • If you expect JSON, you can't use WTF to validate the incoming data. Look at Flask-RESTfull or Flask-RESTless or Flask API for REST frameworks that (perhaps?) can do model validation for you. – Martijn Pieters Mar 30 '16 at 16:59
  • If you want to support web forms in a browser, don't post JSON, but use `requests.post(url, data=..)` to post correctly encoded data instead. – Martijn Pieters Mar 30 '16 at 17:00
  • Thanks for your time, I recognize that the question was not very clear. I have found a way to send a post request (you need to use multi-threaded server and `requests.post(url, json=..)` so it works now. – Mathieu Dubois Mar 30 '16 at 21:31
  • You mean you were sending the request *from another view*? Then yes, the development server included with Flask is going to be a problem, as it doesn't enable threads by default. See [Flask broken pipe with requests](https://stackoverflow.com/q/12591760) – Martijn Pieters Mar 30 '16 at 21:44

1 Answers1

1

You are posting to the new_person route but haven't configured the route to accept a POST request. The default is to only allow GET and HEAD. Set the methods argument to the route() decorator:

@app.route("/api/new_person", methods=['GET', 'POST'])
def new_person():

See the HTTP Methods section of the Flask Quickstart.

Next, you'll need to commit the transaction if you want your changes to persist (like adding a new Person); add the new person object to your session and commit:

db.session.add(person)
db.session.commit()

See the Inserting Records section of the Flask-SQLAlchemy Select, Insert, Delete chapter.

Alternatively, set the SQLAlchemy session to auto-commit all changes:

db = flask.ext.sqlalchemy.SQLAlchemy(app, session_options={'autocommit': True})

You'll still need to add the new object to a session, however.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Hum, I have to `methods=['GET', 'POST']` (otherwise I can't see the form). However, I still don't see any `Person` when I go to `http://127.0.0.1:5000/api/person/` (the `GET` method for class `Person` define by `Flask-restless`). – Mathieu Dubois Mar 30 '16 at 14:14
  • @MathieuDubois: right, I only looked at the form, not what was serving the form. – Martijn Pieters Mar 30 '16 at 14:14
  • @Martjin Pieters I think that there is still no `Person` inserted in the DB. – Mathieu Dubois Mar 30 '16 at 14:17
  • @MathieuDubois: You'll need to commit the transaction. Let me look up the reference. – Martijn Pieters Mar 30 '16 at 14:29
  • @Martjin Pieters Can you provide a full snippet? – Mathieu Dubois Mar 30 '16 at 15:26
  • @MathieuDubois: sorry, this is not a code mentoring service. I can help you with specific problems, as we have done here, but I can't give you running updates for every new problem you find or write more code than is required for your answer, as I volunteer my time here and I don't have that much to give of that. – Martijn Pieters Mar 30 '16 at 15:31
  • What I mean is that I expected the POST method of the generated API to manage that. Therefore I can't see where to do this. – Mathieu Dubois Mar 30 '16 at 15:45
  • You mean committing the change? Right after you created the `person` instance and set its attributes from the form with `form.populate_obj(person)`. – Martijn Pieters Mar 30 '16 at 15:57
  • I will try to clarify the question a bit. – Mathieu Dubois Mar 30 '16 at 16:31