Skip to content

Latest commit

 

History

History
1799 lines (1195 loc) · 42 KB

File metadata and controls

1799 lines (1195 loc) · 42 KB

Internet Programming with Python

img/bike.jpg

Week 5: Small Frameworks

"Reinventing the wheel is great
if your goal is to learn more about the wheel"

-- James Tauber, PyCon 2007

image: Britanglishman http://www.flickr.com/photos/britanglishman/5999131365/ - CC-BY

But First

Review from the Assignment

URL Mapping

Two basic approaches to solving the problem:

/books?id=id1
/books/id1

The first generally used environ['QUERY_STRING']. The second used environ['PATH_INFO']

Both are fine. Largely a matter of taste. I find the latter more common in daily work.

Regular Expressions

My personal approach to the url mapping problem was the second, which relies on regular expression mapping:

URLS = [(r'^$', 'books'),
        (r'^book/(id[\d]{1,2})$', 'book'), ]

Regular expressions should be as tight as possible, it's easy to over-match

Read the Python Regexp How-to and find a good Regular Expression Tester

String Formatting

This is awkward:

bob = {'a': 'things', 'b': 'stuff'}
"I have lots of " + bob['a'] + " and " + bob['b'] + "."

This is much less so:

bob = {'a': 'things', 'b': 'stuff'}
"I have lots of %(a)s and %(b)s." % bob

I am chastened. string.format() is the best (most flexible)

WSGIScriptAlias

CGI required a cgi directory. WSGI makes no such requirement.

You can use WSGIScriptAlias to point to a single file

Since a single file can often provide the entry point to an entire app, this allows you to mount entire apps at arbitrary path locations:

WSGIScriptAlias / /path/to/main/app/wsgi_app.py
WSGIScriptAlias /blog /path/to/blog/app/wsgi_app.py
WSGIScriptAlias /forum /path/to/forum/app/wsgi_app.py

Bad HTML

I know that web browsers are forgiving, but you should be less so.

These are not good HTML:

<p><a href = /book/id4 >foobar</p>
<P><A HREF='/book/id4'>foobar</A></P>

This is: <p><a href="https://github.com/book/id4">foobar</a></p>

The Mozilla Developer Network is a great resource for proper HTML. It also has great reference information on JavaScript. Shun the w3schools.

And Second

Questions from the Reading?

And Third

Class Project

  • Create a Website
  • It can do anything you want it to.
  • It should have some user interactions (forms users complete).
  • It should look nice-ish
  • It should show off some aspect of what you've learned
  • It should take you about 15-20 hours to create (so small)
  • It will be due Friday following the last day of class (March 15)
  • We will spend half of each of the last two class session working on it in class.
  • Questions?

And Now...

Small Frameworks

A Moment to Reflect

We've been at this for a while now. We've learned a great deal:

  • Sockets, the TCP/IP Stack and Basic Mechanics
  • Web Protocols and the Importance of Clear Communication
  • APIs and Consuming Data from The Web
  • CGI and WSGI and Getting Information to Your Dynamic Applications

This concludes the foundational part of the course.

Everything we do from here out will be based on tools built using what we've learned these first four weeks.

We've built

A full-featured web server

We've built

Data-driven applications using web-based APIs

We've built

CGI web pages

We've built

A simple wsgi application

Onward

We are moving up the stack

From Now On

Think of everything we do as sitting on top of WSGI

This may not actually be true

But we will always be working at that level of abstraction.

The Abstraction Stack

You can think of the libraries we use to write web applications as belonging to one of several levels:

plumbing

tools

small frameworks

full-stack frameworks

systems

Plumbing

We've done this part already:

Sockets

Protocols

CGI/WSGI

Tools

We've started to talk about these, we'll see more soon:

cgitb

wsgi middleware

werkzeug tools

WebOb

Small Frameworks

We're here today:

Flask

Bottle

CherryPy

Web.py

and many many more...

Full Stack Frameworks

We will visit this level next:

Django

Pyramid

web2py

Systems

We'll finish up here

Plone

Frameworks

From Wikipedia:

A web application framework (WAF) is a software framework that is designed to support the development of dynamic websites, web applications and web services. The framework aims to alleviate the overhead associated with common activities performed in Web development. For example, many frameworks provide libraries for database access, templating frameworks and session management, and they often promote code reuse

What Does That Mean?

You use a framework to build an application.

A framework allows you to build different kinds of applications.

A framework abstracts what needs to be abstracted, and allows control of the rest.

Think back over the last four weeks. What were your pain points? Which bits do you wish you didn't have to think about?

Level of Abstraction

This last part is important when it comes to choosing a framework

  • abstraction ∝ 1/freedom
  • The more they choose, the less you can
  • Every framework makes choices in what to abstract
  • Every framework makes different choices

Python Web Frameworks

There are scores of 'em (this is a partial list).

Django Grok Pylons TurboGears web2py
Zope CubicWeb Enamel Gizmo(QP) Glashammer
Karrigell Nagare notmm Porcupine QP
SkunkWeb Spyce Tipfy Tornado WebCore
web.py Webware Werkzeug WHIFF XPRESS
AppWsgi Bobo Bo7le CherryPy circuits.web
Paste PyWebLib WebStack Albatross Aquarium
Divmod Nevow Flask JOTWeb2 Python Servlet
Engine Pyramid Quixote Spiked weblayer

Choosing a Framework

Many folks will tell you "<XYZ> is the best framework".

In most cases, what they really mean is "I know how to use <XYZ>"

In some cases, what they really mean is "<XYZ> fits my brain the best"

What they usually forget is that everyone's brain (and everyone's use-case) is different.

Cris' First Law of Frameworks

Pick the Right Tool for the Job

First Corollary

The right tool is the tool that allows you to finish the job quickly and correctly.

But how do you know which that one is?

Cris' Second Law of Frameworks

You can't know unless you try

so let's try

Preparation

We proceed under the assumption that you have installed Flask into a virtualenv, either on your laptop or on your VM.

Start by activating the virtualenv with Flask installed. Mine is 'flaskenv'.

Next, create a new python source file: flask_intro.py

Finally, open that file in your text editor

Flask

Getting started with Flask is pretty straightforward. Here's a complete, simple app. Type it into flask_intro.py:

from flask import Flask
app = Flask(__name__)

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

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

Running our App

As you might expect by now, the last block in our flask_intro.py file allows us to run this as a python program. Save your file, and in your terminal try this:

(flaskenv)$ python flask_intro.py

Load http://localhost:5000 in your browser to see it in action.

Debugging our App

Last week, cgitb provided us with useful feedback when building an app. Flask has a similar tool. Make the following changes to your flask_intro.py file:

@app.route('/')
def hello_world():
    bar = 1 / 0
    return 'Hello World!'

if __name__ == '__main__':
    app.run(debug=True)

In your terminal, quit the app with ^C and then restart it. Then reload your browser and see what happens.

What's Happening Here?

Flask the framework provides a Python class called Flask. This class represents a single application in the WSGI sense.

  • You instantiate a Flask app with a name that represents the package or module containing the app.
  • If your application is a single module, this should be __name__
  • This is used to help the Flask app figure out where to look for resources
  • Resources can be static files (css, images, javascript), templates, or additional python modules you create and need to import.
  • You define a function and route a URL to call it

URL Routing

Remember our bookdb homework? How did you end up solving the problem of mapping an HTTP request to the right function?

Flask solves this problem by using the route decorator from your app.

A 'route' takes a URL rule (more on that in a minute) and maps it to an endpoint and a function.

When a request arrives at a URL that matches a known rule, the function is called.

Routes Can Be Dynamic

You can provide placeholders in dynamic urls. Each placeholder is then a named arg to your function (add these to flask_intro.py (and delete the 1/0 bit)):

@app.route('/profile/<username>')
def show_profile(username):
    return "My username is %s" % username

These placeholders can also include converters that will ensure the incoming argument is of the correct type.

@app.route('/div/<float:val>/')
def divide(val):
    return "%0.2f divided by 2 is %0.2f" % (val, val / 2)

Routes Can Be Filtered

You can also determine which HTTP methods a given route will accept:

@app.route('/blog/entry/<int:id>/', methods=['GET',])
def read_entry(id):
    return "reading entry %d" % id

@app.route('/blog/entry/<int:id>/', methods=['POST', ])
def write_entry(id):
    return 'writing entry %d' % id

After adding that to flask_intro.py and saving, try loading http://localhost:5000/blog/entry/23/ into your browser. Which was called?

Routes Can Be Reversed

Reversing a URL means the ability to generate the url that would result in a given endpoint being called.

This means you don't have to hard-code your URLs when building links

That means you can change the URLs for your app without changing code or templates

This is called decoupling and it is a good thing

Reversing URLs in Flask

In Flask, you reverse a url with the url_for function.

  • url_for requires an HTTP request context to work
  • You can fake an HTTP request when working in a terminal (or testing)
  • Use the test_request_context method of your app object
  • This is a great chance to learn about the Python with statement
  • Don't type this
from flask import url_for
with app.test_request_context():
  print url_for('endpoint', **kwargs)

Reversing in Action

Quit your Flask app with ^C. Then start a python interpreter in that same terminal and import your flask_intro.py module:

import flask_intro
from flask_intro import app
from flask import url_for
with app.test_request_context():
    print url_for('show_profile', username="cris")
    print url_for('divide', val=23.7)

'/profile/cris/'
'/div/23.7/'

Generating HTML

I enjoy writing building HTML in Python strings

-- nobody, ever

Templating

A good framework will provide some way of generating HTML with a templating system.

There are nearly as many templating systems as there are frameworks

Each has advantages and disadvantages

Flask includes the Jinja2 templating system (perhaps because it's built by the same folks)

Jinja2 Template Basics

There are a few basic things to know:

  • Variables in templates can be printed by surrounding the variable name with double curly braces: {{ name }}.
  • If a variable points to something like a dictionary or object, you can use either dot or subscript notation: {{ obj[attr] }}, {{ dict.key }}.
  • Variables in templates can be filtered: {{ name|capitalize }}. There is a list of builtin filters.
  • Logic can be put into templates using the processor marker: {% for x in y %}{{ x }}{% endfor %}
  • Logic comes in pairs. Any start must have an explicit end.

Advanced Jinja2

There is way too much about writing templates in Jinja2 for us to cover here today. Read more here:

http://jinja.pocoo.org/docs/templates/

Templates in Flask

Use the render_template function:

from flask import render_template

@app.route('/hello/')
@app.route('/hello/<name>')
def hello(name=None):
    return render_template('hello.html', name=name)

Flask looks for a templates directory in the same location as your app module (remember app = Flask(__name__)?).

Any extra variables you want to pass to the template should be keyword arguments to render_template

Flask Template Context

Flask adds a few things to the context of templates. You can use these

  • config: contains the current configuration object
  • request: contains the current request object
  • session: any session data that might be available
  • g: the request-local object to which global variables are bound
  • get_flashed_messages: a function that returns messages you flash to your users (more on this later).
  • url_for: so you can easily reverse urls from within your templates

Lab 1

Open a terminal, change directories to the class repository, then to assignments/week05/lab/book_app.

  • You'll find a file book_app.py which is all set up and ready to go
  • You'll also find a templates directory with some templates
  • Complete the functions to provide the right stuff to the templates
  • Complete the templates to display the data to the end-user
  • At the end you should have a reproduced version of last week's homework

GO

Lab 2 - Part 1

The rest of class today will be devoted to building and deploying a simple micro-blog app using flask.

This is based almost entirely on the Flaskr tutorial from the Flask website.

Data Persistence

There are many models for persistance of data.

  • Flat files
  • Relational Database (SQL RDBMs like PostgreSQL, MySQL, SQLServer, Oracle)
  • Object Stores (Pickle, ZODB)
  • NoSQL Databases (CouchDB, MongoDB, etc)

It's also one of the most contentious issues in app design.

For this reason, it's one of the things that most Small Frameworks leave undecided.

Simple SQL

For our second lab exercise today, we're going to use a simple SQL database.

Python PEP 249 describes a common API for database connections called DB API.

The Python Standard Library comes with an implementation of this for a common, light-weight sql database, sqlite3

I am not going to talk a lot about SQL. It's too deep a pool for us to get into. We'll concentrate only on those bits we need to get along.

Our Database

We're going to keep this really really simple.

In assignments/week05/lab/ find the flaskr_1 directory and open the schema.sql file in your editor. Add the following and save the file:

drop table if exists entries;
create table entries (
    id integer primary key autoincrement,
    title string not null,
    text string not null
);

Our App

We'll also need to do some configuration for our app.

In that same directory, find the file flaskr.py and open it in your editor. Add the following and save the file:

# configuration goes here
DATABASE = '/tmp/flaskr.db'
SECRET_KEY = 'development key'

app = Flask(__name__) # this is already in the file
app.config.from_object(__name__)

Windows users, you will need to create C:\tmp or change the pathname for DATABASE

Creating the Database

Still in flaskr.py let's add a function that will connect to our database:

# add this at the very top
import sqlite3

# add the rest of this below the app.config statement
def connect_db():
    return sqlite3.connect(app.config['DATABASE'])

This will be a convenience to us later on, and it will allow us to write our very first test.

Tests and TDD

If it isn't tested, it's broken

Test-Driven Development means writing the tests before writing the functions. As your tests pass, you know you're building what you want.

We are going to write tests at every step of this lab. Along the way, we'll learn a bit about the Python Standard Library module unittest.

You'll want to read more about this module. See our outline for reading suggestions.

Testing Setup

In the same flaskr_1 directory, find and open the flaskr_tests.py file in your editor. Edit it to look like this:

import os
import flaskr
import unittest
import tempfile

class FlaskrTestCase(unittest.TestCase):

    def setUp(self):
        db_fd = tempfile.mkstemp()
        self.db_fd, flaskr.app.config['DATABASE'] = db_fd
        flaskr.app.config['TESTING'] = True
        self.client = flaskr.app.test_client()
        self.app = flaskr.app

Testing Teardown

Add the following method to your test class:

class FlaskrTestCase(unittest.TestCase):
    ...

    def tearDown(self):
        os.close(self.db_fd)
        os.unlink(flaskr.app.config['DATABASE'])

Make Tests Runnable

And finally, add the following at the bottom of your flaskr_tests.py file:

if __name__ == '__main__':
    unittest.main()

Now, we're ready to add our first method.

Test Databse Setup

We'd like to test that our database is correctly initialized. The schema has one table with three columns. Let's test that.

Add the following method to your test class in flaskr_tests.py:

def test_database_setup(self):
    con = flaskr.connect_db()
    cur = con.execute('PRAGMA table_info(entries);')
    rows = cur.fetchall()
    self.assertEquals(len(rows), 3)

Run the Tests

Since we added that if __name__ == '__main__' block, we can simply run our tests with a flask-aware python executable:

(flaskenv)$ python flaskr_tests.py
F
======================================================================
FAIL: test_database_setup (__main__.FlaskrTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "flaskr_tests.py", line 23, in test_database_setup
    self.assertTrue(len(rows) == 3)
AssertionError: False is not True

----------------------------------------------------------------------
Ran 1 test in 0.011s

FAILED (failures=1)

Make the Test Pass

Our database hasn't actually be properly created. We have no table and so no rows are returned when we try to describe it. Let's fix that. Add the following to flaskr.py:

# add this import at the top
from contextlib import closing

# add this function after the connect_db function
def init_db():
    with closing(connect_db()) as db:
        with app.open_resource('schema.sql') as f:
            db.cursor().executescript(f.read())
        db.commit()

Initialize the DB in Tests

We also need to call that function in our flaskr_tests.py, in the setUp method of our test case.

Add the following line at the end of that setUp method:

def setUp(self):
    ...
    flaskr.init_db() # <- add this at the end

Then, re-run the tests (python flaskr_tests.py) and see what you get.

Wahoooo!

Initialize the DB IRL

Okay, so we know the init_db function we added sets up the database properly.

We still need to do this in real life, so that we can work against the database.

Start up a python interpreter in your flaskr_1 folder and do the following:

import flaskr
flaskr.init_db()
^D

Lab 2 - Part 2

Okay, we have a database. Now it's time to write stuff into it, and read it back.

Once again, we're going to start by writing tests.

If you've fallen behind, or if you just want to start fresh, you can find the base of what we've done so far in the flaskr_2 folder.

Managing DB Connections

Database connections should be bound to the borders of a request/response.

Flask provides decorators that mark functions to be run at these borders:

  • @before_request: any method decorated by this will be called before the cycle begins
  • @after_request: any method decorated by this will be called after the cycle is complete. If an unhandled exception occurs, these functions are skipped.
  • @teardown_request: any method decorated by this will be called at the end of the cycle, even if an unhandled exception occurs.

Manage our DB

Add the following code to our app (flaskr.py):

# add this import at the top:
from flask import g

# add these function after init_db
@app.before_request
def before_request():
    g.db = connect_db()

@app.teardown_request
def teardown_request(exception):
    g.db.close()

We bind our db connection to the 'g' object, which is a global context flask supplies to each request.

Test Writing Entries

We want to test that we can write an entry by providing a title and text. Add the following method to flaskr_tests.py:

def test_write_entry(self):
    expected = ("My Title", "My Text")
    with self.app.test_request_context('/'):
        self.app.preprocess_request()
        flaskr.write_entry(*expected)
        con = flaskr.connect_db()
        cur = con.execute("select * from entries;")
        rows = cur.fetchall()
    self.assertEquals(len(rows), 1)
    for val in expected:
        self.assertTrue(val in rows[0])

Note that we have to set up a request context, and preprocess it to get our @before_request method run.

Write an Entry

Now we are ready to write an entry to our database. Add this function to flaskr.py:

def write_entry(title, text):
    g.db.execute('insert into entries (title, text) values (?, ?)',
                 [title, text])
    g.db.commit()

When you're done, re-run your tests. You should now be two for two.

Test Reading Entries

def test_get_all_entries_empty(self):
    with self.app.test_request_context('/'):
        self.app.preprocess_request()
        entries = flaskr.get_all_entries()
        self.assertEquals(len(entries), 0)

def test_get_all_entries(self):
    expected = ("My Title", "My Text")
    with self.app.test_request_context('/'):
        self.app.preprocess_request()
        flaskr.write_entry(*expected)
        entries = flaskr.get_all_entries()
        self.assertEquals(len(entries), 1)
        for entry in entries:
            self.assertEquals(expected[0], entry['title'])
            self.assertEquals(expected[1], entry['text'])

Read Entries

Okay, so now we have 4 tests, and two fail, add this function to flaskr.py:

def get_all_entries():
    cur = g.db.execute('select title, text from entries order by id desc')
    entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
    return entries

Re-run your tests. You should now have four passing tests. Great Job!

Lab 2 - Part 3

Now we can read and write blog entries, let's add views so we can see what we're doing.

Again. Tests come first.

And again, if you've fallen behind or want to start clean, the completed code from our last step is in flaskr_3

Test the Front Page

Add the following tests to flaskr_tests.py:

def test_empty_listing(self):
    rv = self.client.get('/')
    assert 'No entries here so far' in rv.data

def test_listing(self):
    expected = ("My Title", "My Text")
    with self.app.test_request_context('/'):
        self.app.preprocess_request()
        flaskr.write_entry(*expected)
    rv = self.client.get('/')
    for value in expected:
        assert value in rv.data

Template Inheritance

One aspect of Jinja2 templates we haven't seen yet is that templates can inherit structure from other templates.

  • you can make replaceable blocks in templates with blocks: {% block foo %}{% endblock %}.
  • you can build on a template in a second template by extending: {% extends "layout.html" %} (this must be first)

We want the parts of our app to look alike, so let's create a basic layout first. Create a file layout.html in the templates directory.

Creating Layout

<!DOCTYPE html>
<html>
  <head>
    <title>Flaskr</title>
  </head>
  <body>
    <h1>Flaskr</h1>
    <div class="content">
    {% block body %}{% endblock %}
    </div>
  </body>
</html>

Extending Layout

Create a new file, show_entries.html in templates:

{% extends "layout.html" %}
{% block body %}
  <h2>Posts</h2>
  <ul class="entries">
  {% for entry in entries %}
    <li>
      <h2>{{ entry.title }}</h2>
      <div class="entry_body">
      {{ entry.text|safe }}
      </div>
    </li>
  {% else %}
    <li><em>No entries here so far</em></li>
  {% endfor %}
  </ul>
{% endblock %}

Creating a View

Now, we just need to hook up our entries to that template. In flaskr.py add the following code:

# at the top, import
from flask import render_template

# and after our last functions:
@app.route('/')
def show_entries():
    entries = get_all_entries()
    return render_template('show_entries.html', entries=entries)

Run our tests. Should be 6 for 6 now.

Authentication

We don't want just anyone to be able to add new entries. So we want to be able to authenticate a user.

We'll be using built-in functionality of Flask to do this, but this simplest-possible implementation should serve only as a guide.

We'll start with the tests, of course.

Test Authentication

Back in flaskr_tests.py add new test methods:

def test_login_passes(self):
    with self.app.test_request_context('/'):
        self.app.preprocess_request()
        flaskr.do_login(flaskr.app.config['USERNAME'],
                        flaskr.app.config['PASSWORD'])
        self.assertTrue(session.get('logged_in', False))

def test_login_fails(self):
    with self.app.test_request_context('/'):
        self.app.preprocess_request()
        self.assertRaises(ValueError, flaskr.do_login,
                          flaskr.app.config['USERNAME'],
                          'incorrectpassword')

Set Up Authentication

Now, let's add the code in flaskr.py to support this:

# add an import
from flask import session

# and configuration
USERNAME = 'admin'
PASSWORD = 'default'

# and a function
def do_login(usr, pwd):
    if usr != app.config['USERNAME']:
        raise ValueError
    elif pwd != app.config['PASSWORD']:
        raise ValueError
    else:
        session['logged_in'] = True

Login/Logout in Tests

Let's add tests for a view. We'll set up a form that redirects back to the main view on success. First, methods to actually do the login/logout (in flaskr_tests.py):

def login(self, username, password):
    return self.client.post('/login', data=dict(
        username=username,
        password=password
    ), follow_redirects=True)

def logout(self):
    return self.client.get('/logout',
                           follow_redirects=True)

Test Authentication

And now the test itself (again, flaskr_tests.py):

def test_login_logout(self):
    rv = self.login('admin', 'default')
    assert 'You were logged in' in rv.data
    rv = self.logout()
    assert 'You were logged out' in rv.data
    rv = self.login('adminx', 'default')
    assert 'Invalid Login' in rv.data
    rv = self.login('admin', 'defaultx')
    assert 'Invalid Login' in rv.data

We should be up to 9 tests, one failing

Add Login Template

Add login.html to templates:

{% extends "layout.html" %}
{% block body %}
  <h2>Login</h2>
  {% if error -%}
    <p class="error"><strong>Error</strong> {{ error }}
  {%- endif %}
  <form action="{{ url_for('login') }}" method="POST">
    <div class="field">
      <label for="username">Username</label>
      <input type="text" name="username" id="username"/>
    </div>
    <div class="field">
      <label for="password">Password</label>
      <input type="password" name="password" id="password"/>
    </div>
    <div class="control_row">
      <input type="submit" name="Login" value="Login"/>
    </div>
  </form>
{% endblock %}

Add Login/Logout Views

And back in flaskr.py add new code. Let's start with imports:

# at the top, new imports
from flask import request
from flask import redirect
from flask import flash
from flask import url_for

And the View Code

@app.route('/login', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        try:
            do_login(request.form['username'],
                     request.form['password'])
        except ValueError:
            error = "Invalid Login"
        else:
            flash('You were logged in')
            return redirect(url_for('show_entries'))
    return render_template('login.html', error=error)

@app.route('/logout')
def logout():
    session.pop('logged_in', None)
    flash('You were logged out')
    return redirect(url_for('show_entries'))

About Flash

Flask provides flash as a way of sending messages to the user from view code. We need a place to show these messages. Add it to layout.html (along with links to log in and out)

<h1>Flaskr</h1>       <!-- already there -->
<div class="metanav"> <!-- add all this -->
{% if not session.logged_in %}
  <a href="{{ url_for('login') }}">log in</a>
{% else %}
  <a href="{{ url_for('logout') }}">log_out</a>
{% endif %}
</div>
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
{% endfor %}
<div class="content"> <!-- already there -->

Adding an Entry

We still lack a way to add an entry. We need a view to do that. Again, tests first (in flaskr_tests.py):

def test_add_entries(self):
    self.login('admin', 'default')
    rv = self.client.post('/add', data=dict(
        title='Hello',
        text='This is a post'
    ), follow_redirects=True)
    assert 'No entries here so far' not in rv.data
    assert 'Hello' in rv.data
    assert 'This is a post' in rv.data

Add the View

We've already got all the stuff we need to write entries, we just need an endpoint that will do it via the web (in flaskr.py):

# add an import
from flask import abort

@app.route('/add', methods=['POST'])
def add_entry():
    if not session.get('logged_in'):
        abort(401)
    try:
        write_entry(request.form['title'], request.form['text'])
        flash('New entry was successfully posted')
    except sqlite3.Error as e:
        flash('There was an error: %s' % e.args[0])
    return redirect(url_for('show_entries'))

Where do Entries Come From

Finally, we're almost done. We can log in and log out. We can add entries and view them. But look at that last view. Do you see a call to render_template in there at all?

There isn't one. That's because that view is never meant to be be visible. Look carefully at the logic. What happens?

So where do the form values come from?

Let's add a form to the main view. Open show_entries.html

Provide a Form

{% block body %}  <!-- already there -->
{% if session.logged_in %}
<form action="{{ url_for('add_entry') }}" method="POST" class="add_entry">
  <div class="field">
    <label for="title">Title</label>
    <input type="text" size="30" name="title" id="title"/>
  </div>
  <div class="field">
    <label for="text">Text</label>
    <textarea name="text" id="text" rows="5" cols="80"></textarea>
  </div>
  <div class="control_row">
    <input type="submit" value="Share" name="Share"/>
  </div>
</form>
{% endif %}
<h2>Posts</h2>  <!-- already there -->

All Done

Okay. That's it. We've got an app all written.

So far, we haven't actually touched our browsers at all, but we have reasonable certainty that this works because of our tests. Let's try it.

In the terminal where you've been running tests, run our flaskr app:

(flaskenv)$ python flaskr.py
* Running on http://127.0.0.1:5000/
* Restarting with reloader

The Big Payoff

Now load http://localhost:5000/ in your browser and enjoy your reward.

Lab 2 - Part 4

On the other hand, what we've got here is pretty ugly. We could prettify it.

Again, if you want to start fresh or you fell behind you can find code completed to this point in flaskr_4.

In that directory inside the static directory you will find styles.css. Open it in your editor. It contains basic CSS for this app.

We'll need to include this file in our layout.html.

Static Files

Like page templates, Flask locates static resources like images, css and javascript by looking for a static directory next to the app module.

You can use the special url endpoint static to build urls that point here. Open layout.html and add the following:

<head>  <!-- you only need to add the <link> below -->
  <title>Flaskr</title>
  <link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet" type="text/css">
</head>

Deploying

First, move the source code to your VM:

(flaskenv)$ cd ../
(flaskenv)$ tar -czvf flaskr.tgz flaskr
(flaskenv)$ scp flaskr.tgz <your_vm>:~/
(flaskenv)$ ssh <your_vm>
$ tar -zxvf flaskr.tgz

Then, on your VM, set up a virtualenv with Flask installed

Deploying

You'll need to make some changes to mod_wsgi configuration.

  • Open /etc/apache2/sites-available/default in an editor (on the VM)
  • Add the following line at the top (outside the VirtualHost block): WSGIPythonHome /path/to/flaskenv
  • Delete all other lines refering to mod_wsgi configuration
  • Add the following in the VirtualHost block:
WSGIScriptAlias / /var/www/flaskr.wsgi

Deploying

Finally, you'll need to add the named wsgi file and edit it to match:

$ sudo touch /var/www/flaskr.wsgi
$ sudo vi /var/www/flasrk.wsgi


import sys
sys.path.insert(0, 'path/to/flaskr') # the flaskr app you uploaded

from flaskr import app as application

Deploying

Finally, restart apache and bask in the glow:

$ sudo apache2ctl configtest
$ sudo /etc/init.d/apache2 graceful

Load http://your_vm/

Wheeee!

Going Further

It's not too hard to see ways you could improve this.

  • For my part, I made a version using Bootstrap.js.
  • You could limit the number of posts shown on the front page.
  • You could add dates to the posts and provide archived views for older posts.
  • You could add the ability to edit existing posts (and add an updated date to the schema)
  • ...

But Instead

Instead of doing any of that, this week's assignment is a bit different.

You've implemented an app in one Small Framework. I want you to do it all again, in a different Small Framework.

While you're working on it, think about the differences between your new Framework and Flask. What do you like more? What do you like less? How might this influence your choice of Frameworks in the future?

Assignment

  • Re-implement the Flaskr app we built in class in a different Small Framework.
  • There are several named in the class outline, and in this presentation.
  • Pick one of them, or a different one of your choice. It must be Python.
  • When you are finished, add your source code and a README that talks about your experience to the athome folder of week05.
  • Tell me about your new Framework. Discuss the points above regarding differences.

Submitting The Assignment

  • Try to get your code running on your VM
  • Add your source code, in it's entirety, to the athome folder for week 5
  • Add a README.txt file that discusses the experience.
  • Commit your changes to your fork of the class repository and send me a pull request