2014-01-07

Flask TodoMVC 2: Backbone Sync

This is the second article in the Flask TodoMVC tutorial, a series that creates a Backbone.js backend with Flask for the TodoMVC app. In the first article, we created a Flask app using the Backbone.js example as a starting point. In this article, we will replace the localStorage persistence with server side synchronization. A future article will modify this basic support to use a database backend.

We will begin with where we left off in the previous article. If you would like to follow along, but do not have the code, it is available on GitHub.

# Optional, use your own code if you followed part 1
$ git clone https://github.com/kevinbeaty/flask-todomvc
$ cd flask-todomvc
$ git checkout -b backbone-sync part1

Now that we have the code, we are ready to begin.

Remove local storage persistence

First, a little cleanup. Let's remove the Backbone localStorage initialization in static/js/collections/todos.js.

--- a/static/js/collections/todos.js
+++ b/static/js/collections/todos.js
        // Reference to this collection's model.
        model: app.Todo,

-       // Save all of the todo items under the `"todos"` namespace.
-       localStorage: new Backbone.LocalStorage('todos-backbone'),
-
        // Filter down the list of all todo items that are finished.
        completed: function () {
            return this.filter(function (todo) {

To avoid one network round trip when the page is loaded we are going to remove the automatic fetch of todos during the initialization of the app view. Later, we will revisit this by resetting the todo items within the template.

--- a/static/js/views/app-view.js
+++ b/static/js/views/app-view.js
            this.listenTo(app.todos, 'change:completed', this.filterOne);
            this.listenTo(app.todos, 'filter', this.filterAll);
            this.listenTo(app.todos, 'all', this.render);
-
-           // Suppresses 'add' events with {reset: true} and prevents the app view
-           // from being re-rendered for every model. Only renders when the 'reset'
-           // event is triggered at the end of the fetch.
-           app.todos.fetch({reset: true});
        },

Also, remove the backbone.localStorage.js and todomvc-common/base.js script tags in templates/index.html.

--- a/templates/index.html
+++ b/templates/index.html
-       <script src="bower_components/todomvc-common/base.js"></script>
        <script src="bower_components/jquery/jquery.js"></script>
        <script src="bower_components/underscore/underscore.js"></script>
        <script src="bower_components/backbone/backbone.js"></script>
-       <script src="bower_components/backbone.localStorage/backbone.localStorage.js"></script>
        <script src="js/models/todo.js"></script>

Finally, remove the scripts that we are no longer using.

$ rm -r static/bower_components/backbone.localStorage
$ rm static/bower_components/todomvc-common/base.js

Now that we've done some housekeeping we are ready to support synchronization within our Flask app.

Add backend synchronization

We need to add the CRUD to REST routes to our Flask app to support synchronization.

ActionMethodRoute
createPOST/todos
readGET/todos/<int:id>
updatePUT, PATCH/todos/<int:id>
deleteDELETE/todos/<int:id>

For now, we are simply going to store the todos in a list. Modify server.py to add these routes, and necessary imports.

""" server.py """
from flask import (
    Flask,
    abort,
    jsonify,
    render_template,
    request)

TODOS = []

app = Flask(__name__, static_url_path='')
app.debug = True


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/todos/', methods=['POST'])
def todo_create():
    todo = request.get_json()
    todo['id'] = len(TODOS)
    TODOS.append(todo)
    return _todo_response(todo)


@app.route('/todos/<int:id>')
def todo_read(id):
    todo = _todo_get_or_404(id)
    return _todo_response(todo)


@app.route('/todos/<int:id>', methods=['PUT', 'PATCH'])
def todo_update(id):
    todo = _todo_get_or_404(id)
    updates = request.get_json()
    todo.update(updates)
    return _todo_response(todo)


@app.route('/todos/<int:id>', methods=['DELETE'])
def todo_delete(id):
    todo = _todo_get_or_404(id)
    TODOS[id] = None
    return _todo_response(todo)


def _todo_get_or_404(id):
    if not (0 <= id < len(TODOS)):
        abort(404)
    todo = TODOS[id]
    if todo is None:
        abort(404)
    return todo


def _todo_response(todo):
    return jsonify(**todo)


if __name__ == '__main__':
    app.run(port=8000)

We added a route for each CRUD action modifying the TODOS list appropriately. In todo_create we add a new item to the list from the JSON body of the request. We set the id to the new list index to identify in later requests. We then return the todo as a JSON response using _todo_response. This convenience method is reused in all other CRUD routes and returns a response using jsonify, unpacking the dict to kwargs.

All other routes lookup the todo item based on the the id identified by the route. The _todo_get_or_404 method returns the identified todo, while aborting with a 404 if not found. The check for None is necessary as it indicates deletion.

We also enabled the debug flag to assist during development as it will attempt to reload our app on changes and print stack traces to the browser. Note that you do not want to leave this flag enabled in production.

Run the app and view it in a browser. You should be able to add, update and delete todos as before. If you view the network requests using a developer tool, such as Firebug, you will see the appropriate requests to your app.

Initialize on load

If you reload your browser, you will notice that the todo items disappear. This is because we have not initialized the list of todos on page load. We could add a route to retrieve the list of todos using a Backbone fetch, but we removed this call earlier during cleanup to save a network request. Instead, we are going to reset the list when rendering the template.

""" server.py """

@app.route('/')
def index():
    todos = filter(None, TODOS)
    return render_template('index.html', todos=todos)

We also need to configure the url for the todo items and add the initialization in a new inline script towards the end of the template. We could do this by modifying the collection initialization in JavaScript, but we will instead use url_for in the template so we do not hardcode the URL.

<!-- index.html -->
        <script>
            app.todos.url = '{{ url_for("todo_create") }}';
            app.todos.reset({{ todos | tojson }});
        </script>
    </body>
</html>

Add a few items to your list again and reload the browser. The todo items should now be persisted across a reload.

Conclusion

In this article, we replaced the localStorage persistence with backend synchronization with our Flask app. Although this is a good start, our todos are only stored in memory and will not survive a server restart. In the next article, we will replace the crude list persistence with a database backend.

The code is available on GitHub with tag part2 or compared to previous article.