Week 5: Small Frameworks
image: Britanglishman http://www.flickr.com/photos/britanglishman/5999131365/ - CC-BY
Review from the Assignment
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.
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
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." % bobI am chastened. string.format() is the best (most flexible)
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
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.
Questions from the Reading?
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?
Small Frameworks
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.
A full-featured web server
Data-driven applications using web-based APIs
CGI web pages
A simple wsgi application
We are moving up the stack
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.
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
We've done this part already:
Sockets
Protocols
CGI/WSGI
We've started to talk about these, we'll see more soon:
cgitb
wsgi middleware
werkzeug tools
WebOb
We're here today:
Flask
Bottle
CherryPy
Web.py
and many many more...
We will visit this level next:
Django
Pyramid
web2py
We'll finish up here
Plone
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
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?
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
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 |
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.
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?
You can't know unless you try
so let's try
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
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()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.
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.
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
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.
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" % usernameThese 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)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' % idAfter adding that to flask_intro.py and saving, try loading
http://localhost:5000/blog/entry/23/ into your browser. Which was called?
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
In Flask, you reverse a url with the url_for function.
url_forrequires an HTTP request context to work- You can fake an HTTP request when working in a terminal (or testing)
- Use the
test_request_contextmethod of your app object - This is a great chance to learn about the Python
withstatement - Don't type this
from flask import url_for
with app.test_request_context():
print url_for('endpoint', **kwargs)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/'I enjoy writing building HTML in Python strings
-- nobody, ever
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)
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.
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/
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 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
Open a terminal, change directories to the class repository, then to
assignments/week05/lab/book_app.
- You'll find a file
book_app.pywhich is all set up and ready to go - You'll also find a
templatesdirectory 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
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.
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.
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.
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
);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
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.
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.
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.appAdd 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'])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.
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)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)
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()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 endThen, re-run the tests (python flaskr_tests.py) and see what you get.
Wahoooo!
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()
^DOkay, 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.
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.
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.
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.
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.
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'])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 entriesRe-run your tests. You should now have four passing tests. Great Job!
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
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
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.
<!DOCTYPE html>
<html>
<head>
<title>Flaskr</title>
</head>
<body>
<h1>Flaskr</h1>
<div class="content">
{% block body %}{% endblock %}
</div>
</body>
</html>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 %}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.
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.
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')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'] = TrueLet'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)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.dataWe should be up to 9 tests, one failing
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 %}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@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'))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 -->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.dataWe'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'))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
{% 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 -->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
Now load http://localhost:5000/ in your browser and enjoy your reward.
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.
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>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
You'll need to make some changes to mod_wsgi configuration.
- Open
/etc/apache2/sites-available/defaultin 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
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
Finally, restart apache and bask in the glow:
$ sudo apache2ctl configtest $ sudo /etc/init.d/apache2 graceful
Load http://your_vm/
Wheeee!
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)
- ...
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?
- 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
athomefolder of week05. - Tell me about your new Framework. Discuss the points above regarding differences.
- Try to get your code running on your VM
- Add your source code, in it's entirety, to the
athomefolder 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
