Herman Michael. - Real Python For The Web
Herman Michael. - Real Python For The Web
Release 2.0
Michael Herman
Acknowledgements
Writing is an intense, solitary activity that requires discipline and repetition.
Although much of what happens in that process is a mystery to me, I know that the
myriad of people I have chosen to surround myself with played a huge role in the
development of this course. I am immensely grateful to all those in my life for
providing feedback, pushing me when I needed to be pushed, and just listening
when I needed silent support.
At times I ignored many people close to me, despite their continuing support. You
know who you are. I promise I will do my best to make up for it.
For those who wish to write, know that it can be a painful process for those around
you. They make just as many sacrifices as you do, if not more. Be mindful of this.
Take the time to be in the moment with those people, in any way that you can. You
and your work will benefit from this.
Thank you
First and foremost, I'd like to thank Fletcher, author of the original Real Python
course, for believing in me even when I did not believe in myself. He's a talented
developer and a natural leader; I'm also proud to call him a friend. Thanks to all my
close friends and family (Mom, Dad, Jeff) for all your support and kindness. Derek,
Josh, Danielle, Lily, John (all three of you), and Travis - each of you helped in a
very special way that I am only beginning to understand.
Thank you also to the immense support from the Python community. Despite not
knowing much about me or my abilities, you welcomed me, supported me, and
shaped me into a much better programmer. I only hope that I can give back as
much as you have given me.
Thanks to all who read through drafts, helping to shape this course into something
accurate, readable, and, most importantly, useful. Nina, you are a wonderful
technical writer and editor. Stay true to your passions.
1
Real Python for the Web RealPython.com
For those who don't know, this course started as a KickStarter. To all my original
backers and supporters: You have lead me as much as I hope I am now leading
you. Keep asking for more. This is your course.
Finally, thank you to a very popular yet terrible API that forced me to develop my
own solution to a problem, pushing me back into development. Permanently.
2
Real Python for the Web RealPython.com
Python is his tool of choice. He's founded and co-founded several startups and has
written extensively on his experiences.
He loves libraries and other mediums for publicly available data. When not staring
at a computer screen, he enjoys running, writing flash fiction, and making people
feel uncomfortable with his dance moves.
3
Real Python for the Web RealPython.com
Massimo has a PhD in High Energy Theoretical Physics from the University of
Southampton (UK), and he has previously worked as an associate researcher for
Fermi National Accelerator Laboratory. Massimo is the author of a book on
web2py, and more than 50 publications in the fields of Physics and Computational
Finance, and he has contributed to many open source projects.
He started the web2py project in 2007, and is currently the lead developer.
4
Real Python for the Web RealPython.com
0) Introduction
This is not a reference book. Instead, I’ve put together a series of tutorials and
examples to highlight the power of using Python for web development. The
purpose is to open doors, to expose you to the various options available, so you
can decide the path to go down when you are ready to move beyond this course.
Whether its moving on to more advanced materials, becoming more engaged in
the Python development community, or building dynamic web applications of your
own - the choice is yours.
This course moves fast, focusing more on practical solutions than theory and
concepts. Take the time to go through each example. Ask questions on the forum.
Join a local Meetup group. Take advantage of the various online and offline
resources available to you. Engage.
Regardless of whether you have past experience with Python or web development,
I urge you to approach this course with a beginner’s mind. The best way to learn
this material is by challenging yourself. Take my examples further. Find errors in
my code. And if you run into a problem, use the "Google-it-first" approach to find a
relevant blog post or article to help answer your question. This is how "real"
developers solve "real" problems.
By learning through a series of exercises that are challenging, you will screw up at
times. Try not to beat yourself up. Instead, learn from your mistakes - and get
better.
5
Real Python for the Web RealPython.com
With regard to web development, Python powers some of the world's most popular
sites. Reddit, the Washington Post, Instagram, Quora, Pinterest, Mozilla, Dropbox,
Yelp, and YouTube are all powered by Python.
Unlike Ruby, Python offers a plethora of frameworks from which to choose from,
including bottle.py, Flask, CherryPy, Pyramid, Django, and web2py.1 This freedom
of choice allows you to decide which framework is best for your applications. You
can start with a lightweight framework to get off the ground quickly, adding
complexity as your site grows. Such frameworks are great for beginners who wish
to learn the nuts and bolts that underlie web frameworks. Or if you're building an
enterprise-level application, the higher-level frameworks bypass much of the
monotony inherent in web development, enabling you to get an application up
quickly and efficiently.
Please be aware that learning both the Python language and web development at
the same time will be confusing. Spend at least a week going through the original
course before moving on to web development. Combined with this course, you will
get up to speed with Python and web development more quickly and smoothly.
What's more, this book is built on the same principles of the original Real Python
course:
We will cover the commands and techniques used in the vast majority of cases and
focus on how to program real-world solutions to problems that ordinary people
actually want to solve. 2
6
Real Python for the Web RealPython.com
The first section runs through the basics that underlie web frameworks. The more
you work through the exercises and homework problems in that section, the better
off you will be when you start working with web frameworks. Much automation
happens behind the scenes with web frameworks. This will be confusing at first,
which is why the first section details much of this automation. Work hard to get
through the first section to really learn the material, and you will have a much
easier time learning the web frameworks in the subsequent sections.
Learning by doing
Since the underlying philosophy is learning by doing, do just that: Type in each and
every code snippet presented to you. Do not copy and paste. The lessons work
as follows: After I present the main theory, you will type out and then run a small
program. I will then provide feedback on how the program works, focusing
specifically on new concepts presented in the lesson.
Finish all review exercises and give each homework assignment and the larger
development projects a try on your own before getting help from outside resources.
You may struggle, but that is just part of the process. You will learn better that way.
If you get stuck and feel frustrated, take a break. Stretch. Re-adjust your seat. Go
for a walk. Eat something. Do a one-armed handstand. And if you get stuck for
more than a few hours, check out the support forum on the Real Python website.
There is no need to waste time. If you continue to work on each chapter for as long
7
Real Python for the Web RealPython.com
as it takes to at least finish the exercises, eventually something will click and
everything that seemed hard, confusing, and beyond comprehension will suddenly
seem easy.
With enough practice, you will learn this material - and hopefully have fun along the
way!
0.4) Errata
I welcome ideas, suggestions, feedback, and the occasional rant. Did you find a
topic confusing? Or did you find an error in the text or code? Did I omit a topic you
would love to know more about. Whatever the reason, good or bad, please send in
your feedback.
The code found in this course has been tested in Mac OS X v. 10.8, Windows
XP, Windows 7, and Linux Mint 14.
0.5) License
This e-book and course are copyrighted and licensed under a Creative Commons
Attribution-NonCommercial-NoDerivs 3.0 Unported License. This means that you
are welcome to share this book and use it for any non-commercial purposes so
long as the entire book remains intact and unaltered. That being said, if you have
received this copy for free and have found it helpful, I would very much appreciate
it if you purchased a copy of your own.
The example Python scripts associated with this book should be considered open
content. This means that anyone is welcome to use any portion of the code for any
purpose.
8
Real Python for the Web RealPython.com
↩
1. http://wiki.python.org/moin/WebFrameworks↩
↩
2. http://www.realpython.com↩
9
Real Python for the Web RealPython.com
1) Getting Started
If you do not have Python installed, please refer to Appendix A for a basic tutorial.
To get the most out of this course, I assume you have at least an understanding of
the basic building blocks of the Python language:
• Data Types
• Numbers
• Strings
• Lists
• Tuples
• Dictionaries
• Loops
• Functions
• Modules
Again, if you are completely new to Python or just need a brief review, please start
with the original Real Python course.
10
Real Python for the Web RealPython.com
Shell, and an advanced Text Editor called gedit. If you are already familiar with
these environments, you can skip ahead to the lesson on SQLite.
We will be using the command line, or terminal, extensively throughout this course.
If you've never used the command line before, please familiarize yourself with the
following commands:
For simplicity, all command line examples use the Unix-style prompt:
$ python big_snake.py
Windows equivalent:
11
Real Python for the Web RealPython.com
The Shell can be accessed through the terminal by typing python and then
pressing enter. The Shell is an interactive environment: You type code directly into
it, sending code directly to the Python Interpreter, which then reads and responds
to the entered code. The >>> symbols indicate that you're working within the
Shell.
Try accessing the Shell through the terminal and print something out:
>>> print "The bigger the snake, the bigger the prey"
The bigger the snake, the bigger the prey
>>>
To exit the Shell from the Terminal, press CTRL-Z + Enter within Windows, or
CTRL-D within Unix.
The Shell gives you an immediate response to the code you enter. It's great for
testing, but you can really only run one statement at a time. To write longer scripts,
we will be using gedit.
gedit
Again, for much of this course, we will be using a basic yet powerful text editor built
for writing code called gedit. Like Python, it's cross-compatible with many operating
systems. gedit works well with Python and offers excellent syntax highlighting -
applying colors to certain parts of programs such as comments, keywords, and
variables - making the code more readable.
If you're using Linux, chances are gedit is already installed. If not, use the package
manager to install it. You can download the latest versions for Windows and Mac
OS X here.
12
Real Python for the Web RealPython.com
It's common practice to use four spaces for whitespace within a code block rather
than a single tab. gedit actually treats tabs as individual spaces, so in the Editor
tab, within the Preferences menu, decrease the tab width to 4 and check all
options. Now instead of hitting the spacebar four times, you can just hit the tab
button.
Close the window, and we’re now ready to take gedit for a quick test drive. Open a
new file.
Now, change the syntax on the bottom drop-down list from "Plain Text" to "Python"
so that the correct syntax highlighting will be applied:
13
Real Python for the Web RealPython.com
Notice the color differences between each object. That's syntax highlighting. It
makes it easier to read the code and spot errors.
Save the file as gedit.py to your desktop. Get in the habit of saving your programs
with a .py extension. Most of the basic development environments and editors
won't automatically add the extension. If you don't save the file with the .py
extension, the Python interpreter may not recognize the file as a Python program.
Now to run it, open up your terminal. Navigate to your desktop and then type
python gedit.py . If you got an error, go back and correct the code. Save the
file, and then try to run it again. Once you get it to run, close your terminal. That's it.
Pretty simple, right? We're going to have lots of fun with gedit.
If you have your own text editor you currently use, feel free to continue using it.
Notepad++ for Windows and TextWrangler for Mac OS X are excellent as well. But,
if you're new to programming or just want to give something new a try, go with
gedit. It's powerful and easy to use. Just like Python.
14
Real Python for the Web RealPython.com
If you do decide use a different text editor, be aware that mixing tabs with
spaces can cause problems with your code, even if the spacing looks the
same.
Homework
15
Real Python for the Web RealPython.com
Regardless of the operating system, you can download the SQLite Database Driver
from Sourceforge. Installation for Windows and Mac OS X environments are
relatively the same. As for Linux, installation is again dependent upon which Linux
flavor you are using.
Homework
16
Real Python for the Web RealPython.com
easy_install
With easy_install you can simply use the filename of the package you wish to
download and install:
$ easy_install numpy
The above command installs numpy or if you already have it installed, it will
attempt to install the new version (if available). If you need a specific version of a
package, you can use the following command:
$ easy_install numpy==1.6.2
To delete packages, run the following command and then manually delete the
package directory from within your python directory:
pip
Pip, meanwhile, is similar to easy_install, but there are a few added features:
Also, while easy_install will start to install packages before it's done downloading,
pip waits until the download is complete. So, if your connection is lost during
download, you won't have a partially installed package.
17
Real Python for the Web RealPython.com
Now, since pip is a wrapper that relies on easy_install, you must have easy_install
setup and working first before you can install pip. Once easy_install is setup, run
the following command to install pip:
$ easy_install pip
To install a package:
To uninstall a package:
If you are in a Unix environment you will probably need to use sudo before
each command in order to execute the command as the root user: sudo
easy_install pip 2. You will have to enter your root password to install.
Video
Please watch the video here for assistance with installing setup_tools and pip.
It's best to work in a virtualenv so that you can keep all of your Python versions,
libraries, packages, and dependencies separate from one another.
18
Real Python for the Web RealPython.com
Some examples:
Python will work fine without virtualenv. But if you start working on a number of
projects with a number of different libraries installed, you will find virtualenv an
absolute necessity. Once you understand the concept behind it, virtualenv is easy
to use. It will save you time (and possibly prevent a huge headache) in the long
run.
Some Linux flavors come with virtualenv pre-installed. You can run the following
command in your terminal to check:
$ virtualenv --version
$ virtualenv --version
1.8.4
19
Real Python for the Web RealPython.com
This created a new directory, "chapter2", and set up a virtualenv within that
directory. The --no-site-packages flag truly isolates your work
environment from the rest of your system as it does not include any packages
or modules already installed on your system. Thus, you have a completely
isolated environment, free from any previously installed packages.
3. Now you just need to activate the virtualenv, enabling the isolated work
environment:
Unix:
source bin/activate
Windows:
scripts\activate
This changes the context of your terminal in that this folder, chapter2, acts as the
root of your system. Now you can work with Python in a completely isolated
environment. You can tell when you're working in a virtualenv by the directory
surrounded by parentheses to the left of the path in your command-line:
20
Real Python for the Web RealPython.com
When you're done working, you can then exit the virtual environment using the
deactivate command. And when you're ready to develop again, simply navigate
back to that same directory and reactivate the virtualenv.
Video
Homework
• Create separate virtualenvs for chapters two, three, and four of this course.
1.7) Firebug
You can use whichever web browser you'd like during this course; however, there
will be times that we'll need to use the Firebug add-on for Firefox. Firebug is a must
for any web developer. It allows you to inspect and analyze various elements that
make up a webpage. Other web browsers do come with similar development add-
ons, but Firebug is the best in terms of ease of use and available features.
21
Real Python for the Web RealPython.com
So, go ahead and download the latest version of Firefox if you don't already have
it. Then follow these steps to add Firebug:
22
Real Python for the Web RealPython.com
If you've never used an IDE before, I'd recommend starting with the cross-platform
tool, Sublime Text 2, which is part text editor, part IDE, after familiarizing yourself
with gedit. (You will be introduced to Sublime in later chapters.) From there, you
can move on to some of the more advanced Python IDEs, such as NetBeans,
Komodo, PyScripted, PyDev, and Wing.
Finally, the web hosting platform, PythonAnywhere also doubles as an IDE. This
cloud-based service allows you to write programs and web applications on their
web servers, which you can easily access through your desktop computer, laptop,
iPad, and iPhone. You can also collaborate with people on projects. It's a really
cool environment:
23
Real Python for the Web RealPython.com
That's it. Let's go learn how to use Python for web development!
↩
1. http://www.pip-installer.org/en/latest/index.html↩
↩
2. https://gist.github.com/mjhea0/5692708↩
↩
3. http://www.virtualenv.org/en/1.9/↩
↩
4. https://www.pythonanywhere.com/↩
24
Real Python for the Web RealPython.com
2) Fundamentals: Database
Programming
Most relational databases use the SQL (Structured Query Language) language to
communicate with the database. SQL is a fairly easy language to learn, and one
worth learning. In this course I will only be providing a high-level overview to get
you started. To achieve the goals of the course, you need to understand the four
basic SQL commands: SELECT, UPDATE, INSERT, and DELETE.
Command Action
Although SQL is a simple language, you will find an even easier way to interact
with databases when we start working with web frameworks. In essence, instead of
working with SQL directly, you will work with Python objects, which many Python
programmers are more comfortable with. We'll cover these methods in later
chapters. For now, we'll cover SQL, as it's important to understand how SQL works
for when you have to troubleshoot or conduct difficult queries that require SQL.
25
Real Python for the Web RealPython.com
Numerous libraries and modules are available for connecting to relational database
management systems. Such systems include SQLite, MySQL, PostgreSQL,
Microsoft Access, SQL Server, and Oracle. Since the language is essentially the
same across these systems, choosing the one which best suits the needs of your
application depends on the application’s current and expected size. In this chapter,
we will focus on SQLite, which is ideal for simple applications.
SQLite is great. It gives you most of the database structure of the larger, more
powerful relational database systems without having to actually use a server.
Again, it is ideal for simple applications as well as for testing out code. Lightweight
and fast, SQLite requires little administration. It's also already included in the
Python standard library. Thus, you can literally start creating and accessing
databases without downloading any additional dependencies. 1
Homework
• Spend thirty minutes reading more about the basic SQL commands
highlighted above from the official SQLite documentation. If you have time,
check out W3schools.com's Basic SQL Tutorial as well. This will set the
basic ground work for the rest of the chapter.
• Also, if you have access to the Real Python course, brush through chapter 9
again.
26
Real Python for the Web RealPython.com
# create a table
cursor.execute("""CREATE TABLE population
(city TEXT, state TEXT, population INT)
""")
Save the file as ex2.2a.py in Chapter 2's directory. Run the file from your terminal:
$ python ex2.2a.py
As long as you didn't receive an error, the database and table were created inside
a new file called new.db. You can verify this by launching the SQLite Database
Browser and then opening up the "new" database, which will be located in the
same directory where you saved the file. Under the Database Structure tab you
should see the "population" table. You can then expand the table and see the
"city", "state", and "population" fields (also called table columns):
27
Real Python for the Web RealPython.com
First, we imported the sqlite3 library to communicate with SQLite. Next, we created
a new database named new.db. This same command is also used to connect to an
existing database. Since a database didn't exist in this case, one was created for
us. We then created a cursor object, which lets us execute a SQL query or
command against data within the database. Finally, we created a table named
"population" using the SQL statement CREATE TABLE that has two text fields,
"city" and "state", and one integer field, "population".
You can also use the ":memory:" string to create a database in memory
only:
conn = sqlite3.connect(":memory:")
Keep in mind, though, that as soon as you close the connection the database
will disappear.
You can see that working with databases is simple! No matter what you are trying
to accomplish, you will usually follow this basic workflow:
What happens next depends on your end goal. You may insert new data
(INSERT), modify (UPDATE) or delete (DELETE) current data, or simply extract
28
Real Python for the Web RealPython.com
data in order to output it the screen or conduct analysis (SELECT). Go back and
look at the SQL statements, from the beginning of the chapter, for a basic review.
Homework
• Create a new database called "cars", and add a table called "inventory" that
includes the following fields: "Make", "Model", and "Quantity". Don't forget to
include the proper data-types.
# insert data
cursor.execute("INSERT INTO population VALUES('New York City',
'NY', 8200000)")
cursor.execute("INSERT INTO population VALUES('San Francisco',
'CA', 800000)")
29
Real Python for the Web RealPython.com
Save the file as ex2.3a.py and then run it. Again, if you did not receive an error,
then you can assume the code ran correctly. Open up the SQLite Database
Browser again to ensure that the data was added. After you load the database,
click the second tab, "browse data", and you should see the new values that were
inserted.
As in the example from Lesson 2.2, we imported the sqlite3 library, established the
database connection, and created the cursor object. We then used the INSERT
INTO SQL command to insert data into the "population" table. Note how each item
has single quotes around it, while the entire statement is enclosed in double
quotes. Many relational databases only allow objects to be enclosed in single
quotes rather than double quotes. This can get a bit more complicated when you
have items that include single quotes in them. There is a workaround, though - the
executemany() method which you will see in the next example.
The commit() method executes the SQL statements and inserts the data into the
table. Anytime you make changes to a table via the INSERT, UPDATE, or DELETE
commands, you need to run the commit() method before you close the database
connection. Otherwise, the values will only persist temporarily in memory.
That being said, if you rewrite your script using the with keyword, your changes
will automatically be saved without having to use the commit() method, making
your code more compact.
Let's look at the above code re-written using the with keyword:
import sqlite3
with sqlite3.connect("new.db") as connection:
c = connection.cursor()
c.execute("INSERT INTO population VALUES('New York City',
'NY', 8200000)")
c.execute("INSERT INTO population VALUES('San Francisco',
'CA', 800000)")
30
Real Python for the Web RealPython.com
If you need to run many of the same SQL statements you can use the
executemany() method to save time and eliminate unnecessary code. This can be
helpful when initially populating a database with data.
import sqlite3
Save the file then run it. Double check in the SQLite Database Browser that the
values were added.
In this example, the question marks (?) act as placeholders (called parameterized
statements) for the tuple instead of string substitution (%s). Parametrized
statements should always be used when communicating with a SQL database due
to potential SQL injections that could occur from using string substitutions.
Essentially, a SQL injection is a fancy term for when a user supplies a value that
looks like SQL code but really causes the SQL statement to behave in unexpected
ways. Whether accidental or malicious in intent, the statement fools the database
31
Real Python for the Web RealPython.com
into thinking it's a real SQL statement. In some cases, a SQL injection can reveal
sensitive information or even damage or destroy the database. Be careful.
In many cases, you may need to insert thousands of records into your database, in
which case the data is probably contained within an external CSV file – or possibly
even from a different database. Use the executemany() method again.
Use the employees.csv file for this exercise, which is located within the chapter two
directory in the exercises folder.
import sqlite3
2.4) Searching
Let's now look at how to retrieve data:
32
Real Python for the Web RealPython.com
import csv
import sqlite3
Notice in the output the u character. This just stands for a Unicode string. Unicode
is an international character encoding standard for displaying characters 2. This
outputted because we printed the entire string rather than just the values.
Let's look at how to output the data with just the values by removing the unicode
characters altogether:
import sqlite3
33
Real Python for the Web RealPython.com
First, the fetchall() method retrieved all records from the query and stored them as
a tuple; or, more precisely: tuples within a tuple. We then assigned the records to
the "rows" variable.
Finally, we printed the values using index notation, print r[0], r[1] .
import sqlite3
# update data
c.execute("UPDATE population SET population = 9000000 WHERE
city='New York City'")
# delete data
c.execute("DELETE FROM population WHERE city='Boston'")
rows = c.fetchall()
for r in rows:
print r[0], r[1], r[2]
In this example, we used the UPDATE command to change a specific field from a
record and the DELETE command to delete an entire record. We then displayed
the results using the SELECT command. We also introduced the WHERE clause,
34
Real Python for the Web RealPython.com
which is used to filter the results by a certain characteristic. You can also use this
clause with the SELECT statement.
For example:
This statement searches the database for cities where the state is CA. All other
states are excluded from the query.
Homework
We covered a lot of material in the past few lessons. Please be sure to go over it
as many times as necessary before moving on.
• Using the "inventory" table from the homework in Lesson 2.2, add 5 records
(rows of data) to the table. Make sure 3 of the vehicles are Fords while the
other 2 are Hondas. Use any model and quantity for each.
• Update the quantity on two of the records, and then output all of the records
from the table.
• Finally output only records that are for Ford vehicles.
35
Real Python for the Web RealPython.com
import sqlite3
rows = c.fetchall()
for r in rows:
print r[0], r[1], r[2]
36
Real Python for the Web RealPython.com
Did you notice the WHERE clause again? In this example, we chose to limit the
results by only outputting cities with populations greater than one million. Next, let's
create a new table to use:
37
Real Python for the Web RealPython.com
import sqlite3
rows = c.fetchall()
38
Real Python for the Web RealPython.com
for r in rows:
print r[0], r[1]
We created a new table called "regions" that displayed the same cities with their
respective regions. Notice how we used the ORDER BY clause in the SELECT
statement to display the data in ascending order by region.
Open up the SQLite Browser to double check that the new table was in fact created
and populated with data.
SQL Joins
Let's write some code that will use data from both the "population" and the
"regions" tables.
Code:
import sqlite3
# retrieve data
c.execute("""SELECT population.city, population.population,
regions.region FROM population, regions
WHERE population.city = regions.city""")
rows = c.fetchall()
for r in rows:
print r[0], r[1], r[2]
Take a look at the SELECT statement. Since we are using two tables, fields in the
SELECT statement must adhere to the following format: table_name.column_name
39
Real Python for the Web RealPython.com
Finally, let's organize the outputted results and clean up the code so it's more
compact:
import sqlite3
rows = c.fetchall()
for r in rows:
print "City: " + r[0]
print "Population: " + str(r[1])
print "Region: " + r[2]
print
Homework
40
Real Python for the Web RealPython.com
Rewrite the previous examples using the correct syntax. Verify that your
results are correct by comparing them to the previous examples' results.
• Add another table to accompany your "inventory" table called "orders". This
table should have the following fields: "make", "model", and "order_date".
Make sure to only include makes and models for the cars found in the
inventory table. Add 15 records (3 for each car), each with a separate order
date (YYY-MM-DD).
• Finally output the car's make and model on one line, the quantity on another
line, and then the order_dates on subsequent lines below that.
Function Result
41
Real Python for the Web RealPython.com
import sqlite3
# run sql
c.execute(values)
Homework
• Using the COUNT() function, calculate the total number of orders for each
make and model.
• Output the car's make and model on one line, the quantity on another line, and
then the order count on the next line. The latter is a bit difficult, but please try it
first before looking at the code. Remember: Google-it-first!
42
Real Python for the Web RealPython.com
Criteria:
1. Add 100 random integers, ranging from 0 to 100, to a new database called
"newnum.db".
2. Prompt the user whether he or she would like to perform an aggregation
(AVG, MAX, MIN, or SUM) or exit the program altogether.
Break this assignment into two scripts. Name them assignment2a.py and
assignment2b.py.
Now stop for a minute and think about how you would set this up. Take out a piece
of paper and actually write it out. Create a box for the first script and another box
for the second. Write the criteria at the top of the page, and then begin by writing
out exactly what the program should do in plain English in each box. These
sentences will become the comments for your program.
Script 1
1. Import libraries (we need the random library because of the random variable
piece):
import sqlite3
import random
43
Real Python for the Web RealPython.com
c = connection.cursor()
4. Create table, "numbers", with value "num" as an integer (the DROP TABLE
command will remove the entire table if it exists so we can create a new one):
5. Use a for loop and random function to insert 100 random values (0 to 100):
for i in range(100):
c.execute("INSERT INTO numbers
VALUES(?)",(random.randint(0,100),))
Full Code:
44
Real Python for the Web RealPython.com
Script 2
Code:
45
Real Python for the Web RealPython.com
prompt = """
Select the operation that you want to perform [1-5]:
1. Average
2. Max
3. Min
4. Sum
5. Exit
"""
# retrieve data
cursor.execute("SELECT {}(num) from
numbers".format(operation))
46
Real Python for the Web RealPython.com
# if user enters 5
elif x == "5":
print "Exit"
# exit loop
break
We asked the user to enter the operation they would like to perform (numbers 1-4),
which queried the database and displayed either the average, minimum, maximum
or sum (depending on the operation chosen). The loop continues forever until the
user chooses 5 to break the loop.
This chapter provided a brief summary of SQLite and how to use Python to interact
with relational databases. There's a lot more you can do with databases that are
not covered here. If you'd like to explore relational databases further, there are a
number of great resources online, like ZetCode and tutorialspoint's Python
MySQL Database Access.
1. http://www.sqlite.org/about.html ↩
↩
2. http://www.unicode.org/standard/standard.html↩
47
Real Python for the Web RealPython.com
3) Fundamentals: Client-Side
Programming
3.1) Introduction
This chapter focuses on web client programming. Web clients are simply web
browsers that access documents from other servers. Web server programming, on
the other hand - covered in the next chapter - deals with, well, web servers. Put
simply, when you browse the Internet, your web client (i.e., browser) sends a
request to a remote server, which responds back to the web client with the
requested information.
In this chapter, we will navigate the Internet using Python programs to:
• gather data,
• access and consume web services,
• scrape web pages, and
• interact with web pages.
I'm assuming you have some familiarity with HTML (HyperText Markup Language),
the primary language of the Internet. If you need a quick brush up, the first 17
chapters of W3schools.com's Basic HTML Tutorial will get you up to speed
quickly. They shouldn't take more than an hour to review. Make sure that at a
minimum, you understand the basic elements of an HTML page such as the
<head> and <body> as well as various HTML tags like <a> , <div> , <p> ,
<h1> , <img> , <center> , and <br> .
These tags are used to differentiate between each section of a web site or
application.
For example:
48
Real Python for the Web RealPython.com
<h1>This is a headline</h1>
<p>This is a paragraph.</p>
<a href="http://www.realpython.com">This is a link.</a>
Finally, to fully explore this topic, you can gain an understanding of everything from
sockets to various web protocols and become a real expert on how the Internet
works. We will not be going anywhere near that in-depth in this course. Our focus,
rather, will be on the higher-level functions, which are practical in nature and which
can be immediately useful to a web development project. I will provide the required
concepts, but it's more important to concern yourself with the actual programs and
coding.
Homework
• Do you know the difference between the Internet and the Web? Did you know
that there is a difference?
The Web is what most people think of as the Internet, which, now you know is
actually incorrect.
• Read more about the differences between the Internet and the Web via
Google. Look up any terminology that you have questions about, most of
which we will be covering in this Chapter.
49
Real Python for the Web RealPython.com
Let's start with a basic example. But first, don't forget to activate your virtualenv.
Code:
import requests
print r.content
As long as you are connected to the Internet this script will pull the HTML source
code from the Python Software Foundation's website and output it to the screen.
It's a mess, right? Can you recognize the header ( <head> </head> )? How about
some of the other basic HTML tags?
Let me show you an easier way to look at the full HTML output.
Code:
import requests
r = requests.get("http://www.python.org/")
Save the file as ex3.2b.py and run it. If you don't see an error, you can assume that
it ran correctly. Open the new file, test_requests.html in gedit. Now it's much easier
50
Real Python for the Web RealPython.com
to examine the actual HTML. Go through the file and find tags that you recognize.
Google any tags that you don't. This will help you later when we start web scraping.
"wb" stands for write binary, which downloads the raw bytes of the file. Thus,
the file is downloaded in its exact format.
Did you notice the get() function in those last two programs? Computers talk to one
another via HTTP methods. The two methods you will use the most are GET and
POST. When you view a web page, your browser uses GET to fetch that
information. When you submit a form online, your browser will POST information to
a web server. Make them your new best friends. More on this later.
Code:
import requests
url = 'http://httpbin.org/post'
data = {'fname': 'Michael', 'lname': 'Herman'}
Output:
<Response [200]>
Using the requests library, you created a dictionary with the field names as the
keys fname and lname , associated with values Michael and Herman
respectively.
51
Real Python for the Web RealPython.com
requests.post initiates the POST request. In this example, you used the
website httpbin.org, which is specifically designed to test HTTP requests and
received a response back in the form of a code, called a status code.
• 200 OK
• 300 Multiple Choices
• 301 Moved Permanently
• 302 Found
• 304 Not Modified
• 307 Temporary Redirect
• 400 Bad Request
• 401 Unauthorized
• 403 Forbidden
• 404 Not Found
• 410 Gone
• 500 Internal Server Error
• 501 Not Implemented
• 503 Service Unavailable
• 550 Permission denied
There are actually many more status codes that mean various things, depending
on the situation.
You should have received a "200" response to the POST request above.
Modify the script to see the entire response by appending .content to the end of
the print statement:
print r.content
You should see the data you sent within the response:
52
Real Python for the Web RealPython.com
"form": {
"lname": "Herman",
"fname": "Michael"
},
APIs
One example that you've already seen is the SQLite API, which defines the
SELECT, INSERT, UPDATE, and DELETE requests discussed in the last chapter.
The SQLite API allows the end user to perform certain tasks, which, in general, are
limited to those four functions.
HTTP APIs
HTTP APIs, also called web services and web APIs, are simply APIs made
available over the Internet, used for reading (GET) and writing (POST) data. GET
and POST, as well as UPDATE and DELETE, along with SELECT, INSERT,
UPDATE, and DELETE are all forms of CRUD:
53
Real Python for the Web RealPython.com
So, the SELECT command, for example, is equivalent to the GET HTTP method,
which corresponds to the Read CRUD Operation.
Anytime you browse the Internet, you are constantly sending HTTP requests. For
example, WordPress reads (GETs) data from Facebook to show how many people
have "liked" a blog article. Then, if you "liked" an article, data is sent (POST) to
Facebook (if you allow it), showing that you liked that article. Without web services,
these interactions between two independent applications would be impossible.
1. Open FireFox
2. Click on the drop down arrow in the top right of the screen, next to the Firebug
logo
54
Real Python for the Web RealPython.com
4. Then click on the Firebug logo itself to open the Firebug console
5. With the console open, click the Net panel, and set it to enabled.
6. Now, navigate in your browser to a site you need to login at. Watch the activity
in your console. Do you see the GET requests? The most important one is the
top one, which loaded the web page.
7. Now click the tab to Log In. If you're already logged in, go ahead and log out.
Enter your login credentials. Pay close attention to the console, and click "Log
In".
8. Did you see the POST request? It was only there for a seconds. Basically, you
sent a POST request with your login credentials to the server.
Check out some other web pages. Try logging in to some of your favorite sites to
see more POST requests. Perhaps POST a comment on a blog or message forum.
Applications can access APIs either directly, through the API itself, or indirectly,
through a client library. The best means of access depends on a number of factors.
Access through client libraries can be easier, especially for beginners, as the code
is already written. However, you still have to learn how the client library works and
integrate the library's code base into your overall code. Also, if you do not first take
55
Real Python for the Web RealPython.com
the time to learn how the client library works, it can be difficult to debug or
troubleshoot. Direct access provides greater control, but beginners may encounter
more problems understanding and interpreting the rules of the specific API.
Not all web services rely on HTTP requests to govern the allowed interaction.
Only RESTful APIs use POST, GET, PUT, and DELETE. This confuses a lot
of developers. Just remember that web RESTful APIs, or HTTP APIs, are just
one type of web service.
Summary
In summary, APIs:
Although web services have brought much order to the Internet, the services
themselves are still fairly chaotic. There are no standards besides a few high-level
rules, REST, associated with HTTP requests. Documentation is a big problem too,
as it is left to the individual developers to document how their web services work. If
you start working more with web services, which I encourage you to do so, you will
begin to see not only just how different each and every API is but also how terribly
documented many of them are.
If you'd like more information on web APIs, check out this great crash course from
Codeacademy.
56
Real Python for the Web RealPython.com
Homework
<?xml version="1.0"?>
<CARS>
<CAR>
<MAKE>Ford</MAKE>
<MODEL>Focus</MODEL>
<COST>15000</COST>
</CAR>
<CAR>
<MAKE>Honda</MAKE>
<MODEL>Civic</MODEL>
<COST>20000</COST>
</CAR>
<CAR>
<MAKE>Toyota</MAKE>
<MODEL>Camry</MODEL>
<COST>25000</COST>
</CAR>
<CAR>
<MAKE>Honda</MAKE>
<MODEL>Accord</MODEL>
<COST>22000</COST>
</CAR>
</CARS>
57
Real Python for the Web RealPython.com
There's a declaration at the top, and the data is surrounded by opening and closing
tags. One useful thing to remember is that the purpose of XML is much different
than HTML. While HTML is used for displaying data, XML is used for transferring
data. In itself, an XML document is purposeless until it is read, understood, and
parsed by an application. It's about what you do with the data that matters.
With that in mind, let's build a quick parser. There are quite a few libraries you can
use to read and parse XML files. One of the easiest libraries to work with is the
ElementTree library, which is part of Python's standard library. Use the cars.xml file
found in your Chapter 3 folder with this example.
Code:
Output:
Focus
In this program you read and parsed the file using the find function and then
outputted the data between the first <MODEL> </MODEL> tags. These tags are
called element nodes, and are organized in a tree-like structure and further
classified into parent and child relationships.
In the example above, the parent is <CARS> , and the child elements are <CAR> ,
<MAKE> , <MODEL> , and <COST> . The find function begins looking for elements
that are children of the parent node, which is why we started with the first child
when we outputted the data, rather than the parent element:
58
Real Python for the Web RealPython.com
print doc.find("CAR/MODEL").text
print doc.find("CAR[1]/MODEL").text
See what happens when you change the code in the program to:
print doc.find("CAR[2]/MODEL").text
Civic
See how easy that was. That's why XML is both machine and human readable.
Let's take it a step further and add a loop to extract all the data.
Code:
doc = et.parse("cars.xml")
# outputs the make, model and cost of each car to the screen
for element in doc.findall("CAR"):
print (element.find("MAKE").text + " " +
element.find("MODEL").text +
", $" + element.find("COST").text)
59
Real Python for the Web RealPython.com
This program follows the same logic as the previous one, but you just added a
FOR loop to iterate through the XML file, pulling all the data.
In this last example, we will use a GET request to access XML found on the web.
Code:
doc = et.parse("test.xml")
Again, this program follows the same logic. You just added an additional step by
importing the requests library and downloading the XML file before reading and
parsing the XML.
60
Real Python for the Web RealPython.com
{
"CARS":[
{
"MAKE":"Ford",
"MODEL":"Focus",
"COST":"15000"
},
{
"MAKE":"Honda",
"MODEL":"Civic",
"COST":"20000"
},
{
"MAKE":"Toyota",
"MODEL":"Camry",
"COST":"25000"
},
{
"MAKE":"Honda",
"MODEL":"Accord",
"COST":"22000"
}
]
}
61
Real Python for the Web RealPython.com
Although the data looks very similar to XML, there are many noticeable differences.
There's less code, no start or end tags, and it's easier to read. Also, because JSON
operates much like a Python dictionary, it is very easy to work with with Python.
JSON decoding is the act of taking a JSON file, parsing it, and turning it into
something usable.
Without further ado, let's look at how to decode and parse a JSON file. Use the
cars.json file found in your Chapter 3 folder for this example.
Code:
import json
Output:
62
Real Python for the Web RealPython.com
You see we have four dictionaries inside a list, enclosed within another dictionary,
which is finally enclosed within another list. Repeat that to yourself a few times.
Can you see that in the output?
If you want to print just the value "Focus" of the "MODEL" key within the first
dictionary in the list, you could run the following code:
import json
1. [0]["CAR"] - indicates that we want to find the first car dictionary. Since
there is only one, there can only be one value - 0 .
2. [0]["MODEL"] - indicates that we want to find the first instance of the
model key, and then extract the value associated with that key. If we
changed the number to 1 , it would find the second instance of model and
return the associated value: Civic .
Code:
63
Real Python for the Web RealPython.com
import json
import requests
url = "http://httpbin.org/post"
payload = {"colors":[
{"color": "red", "hex":"#f00"},
{"color": "green", "hex":"#0f0"},
{"color": "blue", "hex":"#00f"},
{"color": "cyan", "hex":"#0ff"},
{"color": "magenta", "hex":"#f0f"},
{"color": "yellow", "hex":"#ff0"},
{"color": "black", "hex":"#000"}
]}
headers = {"content-type": "application/json"}
Output:
200 OK
In some cases you will have to send data (also called a Payload), to be interpreted
by a remote server to perform some action on your behalf. For example, you could
send a JSON Payload to Twitter with a number Tweets to be posted. I have seen
situations where in order to apply for certain jobs, you had to send a Payload with
your name, telephone number, and a link to your online resume.
If you ever run into a similar situation, make sure to test the Payload before actually
sending it to the real URL. You can use sites like JSON Test or Echo JSON for
testing purposes.
64
Real Python for the Web RealPython.com
Youtube
Google, the owner of YouTube, has been very liberal when it comes to providing
access to their data via web APIs, allowing hundreds of thousands of developers to
create their own applications.
Download the GData library for this next example: pip install gdata 2
Code:
65
Real Python for the Web RealPython.com
Test the program out with my Youtube ID, "hermanmu". You should see a listing of
my Youtube playlists.
In the above code, we started by importing the required libraries, which are for the
Youtube Python client library. We then established communication with Youtube,
prompted the user for a user ID, and then made the API call to request the data.
You can see that the data was returned to the variable video_feed , which we
looped through and pulled out certain values. Let's take a closer look.
66
Real Python for the Web RealPython.com
Comment out the loop and just output the video_feed variable. Look familiar?
You should be looking at an XML file. It's difficult to read, though. So, how did I
know which elements to extract? Well, I went and looked at the API
documentation.
First, you can see the URL for connecting with (calling) the API and extracting a
user's information with regard to playlists:
http://gdata.youtube.com/feeds/api/users/username/playlists
Try replacing username with my username, hermanmu, then navigate to this URL
in your browser. You should see the same XML file that you did just a second ago
when you printed just the video_feed variable.
Again, this is such a mess we can't read it. At this point, we could download the
file, like we did in lesson 2.2, to examine it. But first, let's look at the documentation
some more. Perhaps there's a clue there.
Scroll down to the "Retrieving playlist information", and look at the code block. You
can see that there is sample code there for iterating through the XML file:
With enough experience, you will be able to just look at this and know that all you
need to do is append title and text , which is what I did in the original code to
obtain the required information:
for p in video_feed.entry:
print p.title.text
For now, while you're still learning, you have one of two options:
67
Real Python for the Web RealPython.com
2. Download the XML file using the requests library and examine the contents
Always look at the documentation first. You will usually find something to work with,
and again this is how you learn. Then if you get stuck, use the "Google-it-first"
philosophy, then if you still have trouble, go ahead and download the XML.
By the way, one of my readers helped with developing the following code to pull not
only the title of the playlist but the list of videos associated with each playlist as
well.
68
Real Python for the Web RealPython.com
Does this make sense? Notice the nested loops. What's different about this code?
Like Google, Twitter provides a very open API. I use the Twitter API extensively for
pulling in tweets on specific topics, then parsing and extracting them to a CSV file
for analysis. One of the best client libraries to use with the Twitter API is Tweepy:
pip install tweepy . 3
69
Real Python for the Web RealPython.com
Before we look at example, you need to obtain access codes for authentication.
1. Navigate to https://dev.twitter.com/apps
2. Click the button to create a new application
3. Enter dummy data
4. After you register, you will be taken to your application where you will need the
following access codes:
◦ consumer_key
◦ consumer_secret
◦ access_token
◦ access_secret
5. Make sure to create your access token to obtain the access_token and
access_secret.
# ex3.6b.py
import tweepy
consumer_key = "<get_your_own>"
consumer_secret = "<get_your_own>"
access_token = "<get_your_own>"
access_secret = "<get_your_own>"
tweets = api.search(q='web2py')
70
Real Python for the Web RealPython.com
If done correctly, this should output the tweets and the dates and times they were
created. Essentially, the search results were returned to a list, and then you used a
For loop to iterate through that list to extract the desired information.
How did I know I wanted the keys created at and text ? Again, trial and error.
I started with the documentation, then I did some Google searches on my own.
You will become quite adept at knowing the types of reliable sources to use when
trying to obtain information about an API. Try this on your own. See what other
information you can extract.
With the Google Directions API, you can obtain directions between to points for a
number of different modes of transportation. Start by looking at the documentation
here. In essence, the documentation is split in two. The first part describes the type
of information you can obtain from the API, and the second part details how to
obtain said information.
http://maps.googleapis.com/maps/api/directions/
output?parameters
2. You then must specify the output. We'll use JSON since it's easier to work
with.
3. Also, you must specify some parameters. Notice in the documentation how
some parameters are required while others are optional. Let's use these
parameters:
origin=Central+Park
destination=Times+Square
sensor=false
mode=walking
71
Real Python for the Web RealPython.com
4. You could simply append the output as well as the parameters to the end of
the URL - http://maps.googleapis.com/maps/api/directions/
json?origin=Central+Park&destination=Times+Square&sensor=false&mode=walking
- and then call the API directly from your browser:
However, there's a lot more information there than we need. Let's call the API
directly from the Python Shell, and then extract the actual driving directions:
72
Real Python for the Web RealPython.com
Compare the loops to the entire output. You can see that for each loop we're just
moving in one level:
So, if I wanted to print the star_address and end address , I would just need
two for loops:
73
Real Python for the Web RealPython.com
Homework
• Using the Google Direction API, pull driving directions from San Francisco to
Los Angeles in XML. Extract the step-by-step driving directions.
Whenever you start working with a new API, you always, always, ALWAYS want to
start with the documentation. Again, all APIs work a little differently because few
universal standards or practices have been established. Fortunately, the Rotten
Tomatoes API is not only well documented but also easy to read and follow.
In this example, we want to grab a list of all movies currently playing in theaters.
According to the documentation, we need to register for an API key to access the
API. We then can grab the needed data based on a series of endpoints. Endpoints
are the actual connection points for accessing the data. In other words, they are
the specific URLs used for calling an API to GET data. Each endpoint is generally
associated with a different type of data, which is why endpoints are often
associated with groupings of data (e.g., movies playing in the theater, movies
opening on a certain date, top rentals, and so on). Go ahead and click the link for
"In Theaters Movies". Immediately, you'll see the URL (or endpoint):
http://api.rottentomatoes.com/api/public/v1.0/lists/movies/
in_theaters.json?apikey=[your_api_key]
74
Real Python for the Web RealPython.com
Notice how you need an API key to access the API. The majority of web APIs
require users to go through some form of authentication in order to access their
services. There are a number of different means of going through authentication.
It's less important that you understand how each method works than to understand
how to obtain authentication to access the web service. Always refer to the web
service provider's documentation to obtain this information.
In this particular case, we just need to register for a developer's key. To register,
click the "Register" link in the top right corner of the web page. It's fairly
straightforward. Make sure to enter a working email address, as the authentication
key will be emailed to you. When you get to the point in the process where you're
asked the name and URL of the application you are creating, simply enter dummy
data:
Once you register for a key, DO NOT share it with anyone. You do not want
someone else using that key to obtain information and possibly use it in either an
illegal or unethical manner.
Once you have your key, go ahead and test it out. Use the URL from above and
replace "[your_api_key]" with the generated key. Now test it in your browser. You
should see a large JSON file full of data. If not, there may be a problem with your
key. Make sure you copied and pasted the entire key and appended it
correctly to the URL. If you continue having problems, post a message to the
Real Python message forum.
Now comes the fun part: building the program to actually GET the data, parsing the
relevant data, and then dumping it into a database.
75
Real Python for the Web RealPython.com
Code:
import sqlite3
# create a table
c.execute("""CREATE TABLE new_movies
(title TEXT, year INT, rating text,
release text, runtime INT,
critics_review INT,
audience_review INT)""")
We need one more script now to pull the data again and dump it directly to the
database:
76
Real Python for the Web RealPython.com
import json
import requests
import sqlite3
YOUR_OWN_KEY = 'GET_YOUR_OWN_KEY'
url = requests.get("http://api.rottentomatoes.com/api/public/
v1.0/lists/movies/in_theaters.json?apikey=%s" %
(YOUR_OWN_KEY,))
# retrieve data
c.execute("SELECT * FROM new_movies ORDER BY title ASC")
77
Real Python for the Web RealPython.com
We grabbed the data URL with a GET request, converted the data to binary,
decoded the JSON feed, then used a for loop to write the data to the database,
and finally we grabbed the data. Nice, right?
That said, most developers will tell you to follow these two principles:
78
Real Python for the Web RealPython.com
It's absolutely vital to adhere to ethical scraping. You could very well get
yourself banned from a website if you scrape millions of pages using a loop. With
regard to the second principle, there is much debate about whether accepting a
website's terms of use is a binding contract or not. This is not a course on ethics or
law, though. So, the examples covered will adhere to both principles.
Finally, it's also a good idea to check the robots.txt file before scraping or crawling.
Found in the root directory of a web site, robots.txt establishes a set of rules, much
like a protocol, that web crawlers or robots should adhere to.
79
Real Python for the Web RealPython.com
1. The User-Agent is the robot, or crawlier, itself. Nine times out of ten you will
see a wildcard * used as the argument, specifying that robots.txt applies to all
robots.
2. Disallow parameters establish the directories or files - "Disallow: /folder/" or
"Disallow: /file.html" - that robots must avoid.
3. The Crawl-delay parameter is used to indicate the minimum delay (in
seconds) between successive server requests. So, in the HackerNews'
example, after scraping the first page, a robot must wait thirty seconds before
crawling to the next page and scraping it, and so on.
There are a number of great libraries you can use for extracting data from
websites. If you are new to web scraping, start with Beautiful Soup. It's easy to
learn, simple to use, and the documentation is great. That being said, there are
plenty of examples of using Beautiful Soup in the original Real Python course.
Start there. We're going to be looking at a more advanced library called Scrapy. 6
If you are using Windows there are additional steps and dependencies that
you need to install. Please follow the video accompanying this lesson for
details.
80
Real Python for the Web RealPython.com
HackerNews (BaseSpider)
Once Scrapy is installed, open your terminal and navigate to your Chapter 3
directory, and then start a new Scrapy project: scrapy startproject
hackernews 7
hackernews/
scrapy.cfg
hackernews/
__init__.py
items.py
pipelines.py
settings.py
spiders/
__init__.py
In this basic example, we're only going to worry about the items.py file and creating
a spider, which is the actual Python script used for scraping.
First, open up the items.py file and edit it to define the fields that you want
extracted. Let's grab the title and url of each posting:
class HackernewsItem(Item):
# define the fields for your item here like:
title = Field()
url = Field()
81
Real Python for the Web RealPython.com
# spider.py
class MySpider(BaseSpider):
82
Real Python for the Web RealPython.com
Essentially, you used XPath to parse and extract the data using HTML tags:
4. You can see that everything we need, text and url, is located between the <td
class="title"> </td> tag:
<td class="title">
<a href="http://ycombinator.com/hdp.html">The Handshake
Deal Protocol</a>
<span class="comhead"> (ycombinator.com) </span>
</td>
And if you look at the rest of the document, all other postings fall within the
same tag.
83
Real Python for the Web RealPython.com
6. Now, we just need to establish the XPath for the title and url. Take a look at
the HTML again:
7. Both the title and url fall within the <a> </a> tag. So our XPath must begin
with those tags. Then we just need to extract the right attributes, text and
@href respectively.
Scrapy Shell
Scrapy comes with an interactive tool called Scrapy Shell which easily tests XPath
expressions. It's already included with the standard Scrapy installation.
The basic format is scrapy shell <url> . Open your terminal and type scrapy
shell http://news.ycombinator.com . Assuming there are no errors in the
URL, you can now test your XPath expressions.
1. Start by using Firebug to get an idea of what to test. Based on the Firebug
analysis we conducted a few lines up, we know that
//td[@class="title"] is part of the XPath used for extracting the title and
link. If you didn't know that, you could test it out in Scrapy Shell.
3. It's hard to see, but the URL and title are both part of the results. We're on the
right path.
84
Real Python for the Web RealPython.com
hxs.select('//td[@class="title"]/a').extract()
5. Now you can see that just the title and URL are part of the results. Now, just
extract the text and then the href:
hxs.select('//td[@class="title"]/a/text()').extract()
and
hxs.select('//td[@class="title"]/a/@href').extract()
Scrapy Shell is a valuable tool for testing whether your XPath expressions are
targeting the data that you want to scrape.
hxs.select('//span[@class="yclinks"]/a[3]/@href').extract()
hxs.select('//td[@class="subtext"]/a/@href').extract()
and
hxs.select('//td[@class="subtext"]/a/text()').extract()
If you need a quick primer on XPath, check out the W3C tutorial. Scrapy also
has some great documentation. Also, before you start the next section, read
this part of the Scrapy documentation. Make sure you understand the
difference between the BaseSpider and Crawlspider.
85
Real Python for the Web RealPython.com
Wikipedia (BaseSpider)
In this next example, we'll be scraping a listing of new movies from Wikipedia:
http://en.wikipedia.org/wiki/Category:2013_films
First, check the terms of use and the robots.txt file and answer the following
questions:
Start by building a scraper to scrape just the first page. Grab the movie title and
URL. This is a slightly more advanced example than the previous one. Please try it
on your own before looking at the code.
class WikipediaItem(Item):
title = Field()
url = Field()
3. Setup your crawler. You can setup a skeleton crawler using the following
command:
86
Real Python for the Web RealPython.com
class $classname(BaseSpider):
name = "$name"
allowed_domains = ["$domain"]
start_urls = (
'http://www.$domain/',
)
4. Copy and paste the output into gedit, and then finish coding the scraper:
87
Real Python for the Web RealPython.com
# wikibase.py - basespider
class MySpider(BaseSpider):
name = "wiki"
allowed_domains = ["en.wikipedia.org"]
start_urls = [
"http://en.wikipedia.org/wiki/Category:2013_films"
]
hxs.select('//tr[@style="vertical-align: top;"]//li')
is equivalent to
hxs.select('//tr[@style="vertical-align: top;"]/td/ul/li')`
88
Real Python for the Web RealPython.com
cd wikipedia/wikipedia
Take a look at the results. We now need to change the relative URLs to absolute
by appending http://en.wikipedia.org to the front of the URLs.
First, import the urlparse library - from urlparse import urljoin - then
update the for loop:
Delete the JSON file and run the scraper again. You should now have the full URL.
In this next example, we'll be scraping a listing of publicly available datasets from
Socrata: https://opendata.socrata.com/.
89
Real Python for the Web RealPython.com
Start with the BaseSpider. We want the title, URL, and the number of views for
each listing. Do this on your own.
1. items.py:
class SocrataItem(Item):
text = Field()
url = Field()
views = Field()
2. socrata_base.py:
90
Real Python for the Web RealPython.com
# socrata_base.py - basespider
class MySpider(BaseSpider):
name = "socrata"
allowed_domains = ["opendata.socrata.com"]
start_urls = [
"https://opendata.socrata.com"
]
CrawlSpider
91
Real Python for the Web RealPython.com
Moving on, let's now look at how to crawl a website as well as scrape. Basically,
we'll start at the same starting URL, scrape the page, follow the first link in the
pagination links at the bottom of the page. Then we'll start over on that page.
Scrape. Crawl. Scrape. Crawl. Scrape. Etc.
Earlier, when you looked up the difference between the BaseSpider and
CrawlSpider, what did you find? Do you feel comfortable setting up the
CrawlSpider? Give it a try.
rules = (Rule
(SgmlLinkExtractor(allow=("browse\?utf8=%E2%9C%93&page=\d*",
)), callback="parse_items", follow= True),)
5. Final code:
92
Real Python for the Web RealPython.com
# socrata_crawl.py - crawlspider
class MySpider(CrawlSpider):
name = "socrata2"
allowed_domains = ["opendata.socrata.com"]
start_urls = [
"https://opendata.socrata.com"
]
rules = (Rule
(SgmlLinkExtractor(allow=("browse\?utf8=%E2%9C%93&page=\d*",
)), callback="parse_items", follow= True),)
As you can see, the only new part of the code, besides the imports, are the rules,
which define the crawling portion of the spider:
93
Real Python for the Web RealPython.com
rules = (Rule
(SgmlLinkExtractor(allow=("browse\?utf8=%E2%9C%93&page=\d*",
)), callback="parse_items", follow= True),)
Please read over the documentation regarding rules quickly before you read
the explanation. Also, it's important that you have a basic understanding of
regular expressions. Please refer to the original Real Python course for a
high-level overview.
So, the SgmlLinkExtractor is used to specify the links that should br crawled.
The allow parameter is used to define the regular expressions that the URLs
must match in order to be crawled.
• https://opendata.socrata.com/browse?utf8=%E2%9C%93&page=2
• https://opendata.socrata.com/browse?utf8=%E2%9C%93&page=3
• https://opendata.socrata.com/browse?utf8=%E2%9C%93&page=4
What differs between them? The numbers on the end, right? So, we need to
replace the number with an equivalent regular expression, which will recognize any
number. The regular expression \d represents any number, 0 - 9. Then the *
operator is used as a wildcard. Thus, any number will be followed, which will crawl
every page in the pagination list.
We also need to escape the question mark (?) from the URL since question marks
have special meaning in regular expressions. In other words, if we don't escape the
question mark, it will be treated as a regular expression as well, which we don't
want because it is part of the URL. Thus, we are left with this regular expression:
browse\?utf8=%E2%9C%93&page=\d*
Make sense?
All right. Remember how I said that we need to crawl "ethically"? Well, let's put a
10-second delay between each crawl/scrape combo. This is very easy to forget to
do. Once you get yourself banned from a site, though, you'll start to remember.
94
Real Python for the Web RealPython.com
I cannot urge you enough to be careful. Only crawl sites where it is 100%
legal at first. If you start venturing into gray area, do so at your own risk.
These are powerful tools you are learning. Act responsibly. Or getting banned
from a site will be the least of your worries.
Speaking of which, did you check the terms of use and the robots.txt file? If not, do
so now.
To add a delay, open up the settings.py file, and then add the following code:
DOWNLOAD_DELAY = 10
Item Pipeline
Finally, instead of dumping the data a JSON file, let's feed it to a database.
1. Create the database within the "socrata\socrata" directory from your shell:
import sqlite3
conn = sqlite3.connect("project.db")
cursor = conn.cursor()
cursor.execute("""CREATE TABLE data
(text TEXT, url TEXT, views TEXT)
""")
95
Real Python for the Web RealPython.com
import sqlite3
class SocrataPipeline(object):
def __init__(self):
self.conn = sqlite3.connect('project.db')
self.cur = self.conn.cursor()
ITEM_PIPELINES = ['socrata.pipelines.SocrataPipeline']
Look good? Go ahead and delete the data using the SQLite Browser. Save the
database.
This will take a while. In the meantime, read about using Firebug with Scrapy from
the official Scrapy documentation. Still running? Take a break. Stretch.
Once complete, open the database with the SQLite Browser. You should have
about ~20,000 rows of data. Make sure to hold onto this database we'll be using it
later.
96
Real Python for the Web RealPython.com
Homework
• Use your knowledge of Beautiful Soup, which, again, was taught in the original
Real Python course, as well the the requests library, to scrape and parse all
links from the web2py homepage. Use a for loop to output the results to the
screen. Refer back to the main course or the Beautiful Soup documentation
for assistance.
97
Real Python for the Web RealPython.com
import requests
import time
i = 0
base_url = 'http://download.finance.yahoo.com/d/quotes.csv'
Save this file as ex3.8a.py and run it. Then load up the CSV file after the program
ends to see the stock prices. You could change the sleep time to 60 seconds so it
pulls the stock price every minute or 360 to pull it every hour.
98
Real Python for the Web RealPython.com
http://download.finance.yahoo.com/d/
quotes.csv?s=goog&f=sl1d1t1c1ohgv&e=.csv
How would you then pull prices for a number of quotes using a loop?
↩
1. http://docs.python-requests.org/en/latest/↩
↩
2. https://developers.google.com/youtube/1.0/developers_guide_python↩
↩
3. http://tweepy.github.com/↩
↩
4. http://developer.rottentomatoes.com/docs/read/Home↩
99
Real Python for the Web RealPython.com
↩
5. http://pages.ebay.com/help/policies/user-agreement.html↩
↩
6. http://scrapy.org/↩
↩
7. http://doc.scrapy.org/en/latest/topics/shell.html↩
100
Real Python for the Web RealPython.com
4) Fundamentals: Server-Side
Programming
4.1 Introduction
Python is one of the most widely-used programming languages for building and
driving websites. From small scripts used to process HTML forms to entire,
enterprise-level websites controlled by web frameworks, Python serves a breadth
of roles with regards to web development. Python first made its appearance on the
Internet in the form of CGI Scripting. Programs written using CGI are simple yet
slow and should thus only be used for small applications - such as form handling.
Despite its limitations and the rise of faster methods of getting Python to
communicate with web servers, we will be addressing CGI scripts in detail, as they
embody the main principles of web frameworks.
To test our CGI scripts, we must use a web server in order to serve the web pages
to the web browser. Python comes with a number of built-in web servers, two of
which we will be using: SimpleHTTPServer and CGIHTTPServer. 1 These servers
essentially turn any directory on your local system into a mini web server. Both are
ideal for training purposes as well as debugging code. Perhaps the best part about
them, though, is that you can just run them directly from the command line. You
don't even need a script!
101
Real Python for the Web RealPython.com
Hello, World!!!! :)
That's it. Hit CTRL-C on your keyboard back in the terminal to close the server.
If you received a "socket.error: [Errno 13] Permission denied" message then port
8000 is in use, you can add another port to the end of the command: python -m
SimpleHTTPServer 9000 , for example.
This is a quick way to get a web server up with little configuration and without
eating up system resources. I personally use this quick method for testing and
sharing files. If you run the command from a directory without an index.html file in
it, files in that directory will be displayed, and can then be downloaded from another
computer within your network. You can access those files by going to
http://your_ip_address:8000.
• Unix: Type ifconfig en1 in your terminal. The IP address is right after the
"inet" descriptor.
• Windows: Type ipconfig /all in your terminal. (If you're using Windows 7
or 8, the IP address is called "IPv4".)
You can also write a custom script to do the same thing. The advantage of going
this route is that you have the freedom to customize the server. You could set up
GET or POST handling for testing purposes:
102
Real Python for the Web RealPython.com
import sys
import BaseHTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
Run this from the simplehttp directory, and you should see the same output as in
the last example. You can also establish the port from the command line.
Otherwise, it will default to 8000:
103
Real Python for the Web RealPython.com
Once again, hit CTRL-C on your keyboard from the terminal to close the server.
I've personally used Python CGI Programming to understand some of the basics of
creating a dynamic website. Many of the popular high-level frameworks, including
web2py and Django, employ various application shortcuts (often called "magic") to
speed up the development process. Starting a basic application from scratch is
vital for understanding how things work behind the scenes, at a low level, within
those frameworks. This will ultimately make you a better developer.
When a user accesses a CGI script from a browser, data is received by the web
server (called a request), which, in turn, passes it to the CGI script. This script
performs some action and then sends the data back (called a response).
Code:
104
Real Python for the Web RealPython.com
#!/usr/bin/env python
print "Content-Type: text/html"
print
print """
<html>
<body>
<h2>Hello, World!</h2>
</body>
</html>
"""
Note: When working with CGI, do not include the header at the top of the file -
that is, # ex4.3b.py - hello cgi example
Save this file as ex4.3b.py in the "cgihttp/cgi-bin" directory within the Chapter 4
"exercises" folder. If you are running on a Unix machine, you need to make sure
both the directory and file are executable before you run the file. To do this, within
the terminal, navigate to the "cgihttp" directory, then run the following command to
update the user permissions:
If you are on an Unix operating system, you will need to set the permissions
for each script in this chapter.
This setup is for running locally only, using the test servers provided by
Python. If you are working with a web server, please check with your hosting
provider to see how to set up your user permissions. You'll want to set up the
permissions so that everyone can read and (possibly) execute the file, but
only you can write to it.
105
Real Python for the Web RealPython.com
Navigate to your "cgihttp" directory within the terminal and then fire up the server:
$ python -m CGIHTTPServer
Output:
Hello, World!
If you have problems running this file, jump ahead to the end of the lesson
where debugging techniques are discussed.
The first two characters of the first line of the script, #! , are called a "shebang",
which indicates that the file is to be executed by the Python interpreter specified by
the path, /usr/bin/env python . This is applicable only for Unix-based systems,
as Windows handles this process differently. That said, you can run a Python
program without it. Unix users - Create a new file called "shebang.py" and add the
following code. Windows Users - Although you can't partake in this particular
example, please read it over as it will ultimately pertain to you as well.
#!/usr/bin/env python
print "shebang"
Run it as usual. Then run the program again using this command: ./shebang.py .
It should run the same. Now go back and remove the "shebang" and the Python
path. Run the program again using both methods. You should have seen a
./shebang.py: line 2: print: command not found error using the latter,
less verbose method of execution, because the Terminal did not know what to do
with the print statement. Since CGI scripts are meant to run on a web-server,
you must add the "shebang" line so the server will know to execute the file
using the Python interpreter.
Next, you need to set the HTTP header to text/html, which lets the server know that
it should expect HTML to be sent. The blank line that follows is required, signaling
106
Real Python for the Web RealPython.com
the end of the header. You now have the actual HTML. If you view the page
source, you will only see the HTML.
Now for a more complicated example: form handling. One of the more common
uses for CGI still used today is for processing HTML forms.
<html>
<title>CGI Form</title>
<body>
<form method="POST" action="/cgi-bin/ex4.3c.py">
<p>Please enter your first name:</p>
<input type="text" name="name"><input type="submit"
value="Submit">
</form>
</body>
</html>
Save this file as ex4.3c.html in the cgihttp directory within the exercises folder:
107
Real Python for the Web RealPython.com
#!/usr/bin/env python
import cgi
form = cgi.FieldStorage()
Back on the terminal, navigate to the cgihttp directory, then run the following
command:
$ python -m CGIHTTPServer
If you go back to the terminal you can see the messages from the server:
108
Real Python for the Web RealPython.com
Notice the HTTP requests, GET and POST, and the response message 200. We
sent the server a GET request when we navigated to ex4.3c.html. When we
submitted the form, a POST request was sent to the server. Both requests worked,
as we received a response 200 for each. When our programs get more
complicated, looking at the server responses is a good way to debug our programs
and narrow down the problems.
Now, let's take this a step further and process some data that a user inputs via an
HTML form:
<html>
<title>Square Root</title>
<body>
<form method="POST" action="/cgi-bin/ex4.3d.py">
<p>Hello. Please enter a number so we can provide the
square root. Thanks!</p>
<br/>
<input type="text" name="sqrt"><input type="submit"
value="Submit">
</form>
</body>
</html>
109
Real Python for the Web RealPython.com
#!/usr/bin/env python
import cgi, math
form = cgi.FieldStorage()
This works very similarly to the last example. In this example, however, we took the
additional step of processing the data by using the math module to calculate the
square root of the value.
110
Real Python for the Web RealPython.com
<html>
<title>Login</title>
<body>
<form method="POST" action="/cgi-bin/ex4.3e.py">
<p>Please enter your username and password to access the
restricted site</p>
<p>Username: <input type="text" name="user" value=""></p>
<p>Password: <input type="password" name="password"
value=""></p>
<input type="submit" value="Submit">
</form>
</body>
</html>
111
Real Python for the Web RealPython.com
# ex4.3e.py - login
#!/usr/bin/env python
import cgi
form = cgi.FieldStorage()
username = "admin"
password = "admin"
if user == username and passwd == password:
msg = "Welcome {username}. You are now logged
in.".format(username=username)
else:
msg = "Incorrect username or password. Please try again."
Try both the correct username and password, "admin" and "admin", as well as an
invalid username and/or password.
112
Real Python for the Web RealPython.com
user = form['user'].value
passwd = form['password'].value
113
Real Python for the Web RealPython.com
#!/usr/bin/env python
import sqlite3
import cgi
# retrieve data
rows = c.fetchall()
114
Real Python for the Web RealPython.com
<names>
<name>
<first>John</first>
<last>Bell</last>
</name>
<name>
<first>Michael</first>
<last>Sloane</last>
</name>
<name>
<first>Rachel</first>
<last>Peterson</last>
</name>
</names>
In this example, we indicated to the server that it should expect an XML file. You
should recognize the SQL syntax from the Databases chapter. If not, go back and
refresh your memory, as we will be covering a lot more SQL in the following
chapters.
Debugging CGI
You will often need to debug your script in order to correct errors. This can be a
difficult task, depending on the issues.
Possible issues:
Start by checking the Python code by running the file on its own (e.g., python
ex4.3b.py ). There could be a syntax error, for example. Next, enable the cgitb
115
Real Python for the Web RealPython.com
module, which is part of Python's standard library, to display errors. Let's look at a
quick example.
Code:
#!/usr/bin/env python
import cgitb
cgitb.enable()
print "Content-Type: text/html"
print
print 1/0
You can see that there is a division error. Comment out cgitb.enable() , run
again, and see the difference. You can add this module to any of your scripts to
help with debugging.
You never want your end users to see such errors, so make sure to disable
cgitb before deploying to a web server accessible by end users
Unix users: To find the correct path for the "shebang" line, type the following
command in your terminal: which python
116
Real Python for the Web RealPython.com
This exercise is meant to be difficult. Working with Python via CGI is very rare
in the real world. However, this teaches you the lowest level of web
development so that you will understand the basics before adding in web
frameworks. Try your best on this, and if you get stuck and can't finish,
you will be fine as long as you understand the overall concepts.
Application workflow: When a user logs in, they’ll be presented with all of the blog
posts. The user can then add new text-only blog entries from the same screen,
read the entries themselves, or logout. That's it. Before we even think about
coding, though, we need to define the application's workflow from a user’s
perspective. You can setup a matrix that defines all the actions, or requests, a user
can make as well as the subsequent server response in terms of CRUD:
Login Page
HTML is
Navigates to login page GET 200
rendered
117
Real Python for the Web RealPython.com
Main Page
HTTP Response
User Action Server Response
Request Code
Article accepted,
Posts a blog article POST 200
added to database
Redirects to login
Logs out POST 302
page
Before looking at my code, see how much of this you can plan and implement on
your own. Like before, start by mapping the processes out on a piece of paper to
determine the number of scripts and HTML files you'll need. From there, write out
short statements of what each script will do, and then turn those statements into
the comments for your program. You will then be ready to start writing code.
All right. Start by creating a directory called "blog" within your Chapter 4 folder.
Then, in that directory, create a sub-directory called "cgi-bin". Unix users: Don't
forget to set the proper executables on the "cgi-bin" directory.
Database
Since databases are the core of most applications, it's best to start there. It can
also be difficult to make changes to a database management system (i.e., SQLite,
MySQL, SQL Server) once configured, so database design is important. Even
though we will be using SQLite, which is easy to configure and modify (as you now
know), always take database design seriously.
118
Real Python for the Web RealPython.com
Our database will have one table called posts, with two fields - title and post.
Let's create the database and populate the table with some dummy data:
import sqlite3
Save the file in the "blog" directory and run it from the terminal. Double-check to
make sure the data was entered correctly with the SQLite Database Browser.
Now that the database has been implemented, let's create the main listing page.
119
Real Python for the Web RealPython.com
We'll be adding user authentication and security after the main functions have
been put into place. It's a good strategy to break development into pieces like
this, adding features step-by-step, to simplify the process.
Before adding the form, we'll start with simply outputting the data to the screen to
make sure everything prints correctly.
Code:
120
Real Python for the Web RealPython.com
# total posts
c.execute("SELECT COUNT(post) FROM posts")
total = c.fetchone()[0]
print "Total Posts: ", total
# query posts
c.execute("SELECT * FROM posts")
Once again, save to the "blog" directory and then run it from the terminal. You
should see the data we just added to the database.
Next, let's look at how to create the HTML output via CGI:
121
Real Python for the Web RealPython.com
# total posts
c.execute("SELECT COUNT(post) FROM posts")
total = c.fetchone()[0]
# query posts
c.execute("SELECT * FROM posts")
data_table = "<table
border=1><tr><th>Title</th><th>Post</th></tr>"
for p in posts:
data_table += "<tr>"
data_table += "<td>%s</td>" % (p[0])
data_table += "<td>%s</td>" % (p[1])
data_table += "</tr>"
data_table += "</table>"
print """\
<html>
122
Real Python for the Web RealPython.com
<head><title>Blog</title></head>
<body>
<h1><center>Bloggy</center></h1>
<h2>Welcome!</h2>
<p>Total Posts: %s</p>
<p>%s</p>
</body>
</html>
""" % (total, data_table)
Save this file in your "cgi-bin", navigate to your blog directory in your terminal, and
then run the command python -m CGIHTTPServer . Navigate to
http://localhost:8000/cgi-bin/ex4.4_blog3.py in your browser and you should see
the following output:
Next let's add a basic form to allow users the ability to create new posts.
Essentially, we just need to add a basic html form to the previous script:
123
Real Python for the Web RealPython.com
# total posts
c.execute("SELECT COUNT(post) FROM posts")
total = c.fetchone()[0]
# query posts
c.execute("SELECT * FROM posts")
124
Real Python for the Web RealPython.com
data_table = "<table
border=1><tr><th>Title</th><th>Post</th></tr>"
for p in posts:
data_table += "<tr>"
data_table += "<td>%s</td>" % (p[0])
data_table += "<td>%s</td>" % (p[1])
data_table += "</tr>"
data_table += "</table>"
print """\
<html>
<head><title>Blog</title></head>
<body>
<h1><center>Bloggy</center></h1>
<h2>Welcome!</h2>
<p>Total Posts: %s</p>
<p>%s</p>
<div>
<form method="POST" action="ex4.4_blog4.py">
<h2>Add New Post:</h2>
<p>Title: <input type="text" name="title"
value=""></p>
<p>Post: <input type="text" name="post"
value=""></p>
<input type="submit" value="Submit">
</form>
</div>
</body>
</html>
""" % (total, data_table)
Save to the "cgi-bin", load the server from the "blog" directory, navigate to
http://localhost:8000/cgi-bin/ex4.4_blog3.py. Test out the form. Are you able to
add a new post?
Login Page
We will now be adding a login page so that only authenticated users will be able to
view the Main Page.
125
Real Python for the Web RealPython.com
<html>
<title>Login Required</title>
<body>
<form method="POST" action="/cgi-bin/ex4.4_login.py">
<h2>Login Required!</h2>
<p>Please enter your username and password to access
the restricted site</p>
<p>Username: <input type="text" name="user"
value=""></p>
<p>Password: <input type="password" name="password"
value=""></p>
<input type="submit" value="Submit">
</form>
</body>
</html>
Save this as ex4.4_login.html under your "blog" folder. Do not test it yet. Also,
create the following script under "cgihttp/cgi-bin" and save it as
126
Real Python for the Web RealPython.com
# ex4.4_login.py - login
#!/usr/bin/env python
import cgi
Essentially, this script checks if the username and password entered by the user in
login.html are correct. If correct, the page is redirected to the Main Page.
Otherwise, it is redirected back to login.html.
Cookies
Although we have created a login page, the user can still access our Main Page by
navigating his or her browser to http://localhost:8000/cgi-bin/ex4.4_blog4.py. To
address this issue, our main script must be able to check if the user has been
authenticated or not. To allow the script to do this, we will be using cookies.
127
Real Python for the Web RealPython.com
Cookies are pieces of information about the user that are stored in the user's
browser.
Verifying user authentication through cookies alone is not secure. There are
better solutions - namely using sessions. This example nevertheless will show
you how to store cookies to the user's browser. You will learn about sessions
in the next chapter.
# ex4.4_login.py - login
#!/usr/bin/env python
import cgi
import Cookie
Save this in your "cgi-bin" directory, but do not run it just yet. If the username and
password are correct, the script will create a cookie with value = True.
128
Real Python for the Web RealPython.com
129
Real Python for the Web RealPython.com
logged_in = False
# check if the user is authenticated through a Cookie named
'logged_in'
thiscookie = Cookie.SimpleCookie()
if os.environ.has_key('HTTP_COOKIE'):
thiscookie.load(os.environ['HTTP_COOKIE'])
if 'logged_in' in thiscookie:
logged_in = bool(thiscookie['logged_in'].value)
130
Real Python for the Web RealPython.com
# total posts
c.execute("SELECT COUNT(post) FROM posts")
total = c.fetchone()[0]
# query posts
c.execute("SELECT * FROM posts")
data_table = "<table
border=1><tr><th>Title</th><th>Post</th></tr>"
for p in posts:
data_table += "<tr>"
data_table += "<td>%s</td>" % (p[0])
data_table += "<td>%s</td>" % (p[1])
data_table += "</tr>"
data_table += "</table>"
print """
<html>
<head><title>Blog</title></head>
<body>
<h1><center>Bloggy</center></h1>
<h2>Welcome!</h2>
<p>Total Posts: %s</p>
<p>%s</p>
<div>
<form method="POST" action="ex4.4_blog5.py">
<h2>Add New Post:</h2>
<p>Title: <input type="text" name="title"
value=""></p>
<p>Post: <input type="text" name="post"
value=""></p>
<input type="submit" value="Submit">
</form>
131
Real Python for the Web RealPython.com
Once again, save this to the "cgi-bin" but don't run it yet.
Finally, we need a logout script. This script will simply unset the cookie named
'logged_in' and redirect the page to the login page:
# ex4.4_logout.py - login
#!/usr/bin/env python
import Cookie
Unix users: Don't forget to make all files executable that reside in the cgi-bin,
except for the database.
Fire up the server again (from the "blog" directory), then navigate to
http://localhost:8000/cgi-bin/ex4.4_login.py. Test everything:
132
Real Python for the Web RealPython.com
↩
1. http://docs.python.org/2/library/internet.html↩
133
Real Python for the Web RealPython.com
Overview
So far, we have addressed older technologies (e.g. Python, HTML, HTTP, SQL),
used for web development since the advent of the Web. These technologies,
however, underlie the web frameworks currently used today. Learning how each of
these work is key to understanding what makes web frameworks tick, underneath
the hood.
By learning web development from the ground up, you will develop a deeper
understanding of web frameworks, allowing for quicker and more flexible web
development.
1. Front-end: The presentation layer, it's what the end user sees when
interacting with a web application. HTML makes up the structure, CSS
provides a pretty facade, and JavaScript/jQuery create interaction. This layer
is housed by the web browser. The Front-end is also reliant on the application
logic and data source provided by the Middleware and Back- end in order to
function.
2. Middleware: This layer relays information between the Front and Back-ends,
in order to:
134
Real Python for the Web RealPython.com
Model-View-Controller (MVC)
Web frameworks reside just above those three layers, abstracting away much of
the processes that occur in each. There are pros and cons associated with this: It's
great for experienced web developers, for example, who understand the
automation (magic!) behind the scenes.
For example, think if you had to define the aspects that make up a tree, when
referring to a tree, rather than just saying the word tree. You'd have to say, "The
trunk with branches and leaves ... is getting big", instead of, "The tree is getting
big." In other words, web frameworks simplify web development, by handling much
of the superfluous, repetitious tasks.
For a beginner, however, it can be confusing, which reinforces the need for the
Fundamentals Section of this course.
Frameworks also separate the presentation from the application logic and the
underlying data in what’s commonly referred to as the Model-View-Controller
135
Real Python for the Web RealPython.com
We'll be covering this pattern numerous times throughout the remainder of this
course. Let's get to it!
Homework
136
Real Python for the Web RealPython.com
5) Flask: QuickStart
5.1) Overview
Building on what you learned in the first section, this section introduces you to the
Flask web framework.
Flask grew from an elaborate April fool’s joke in 2010 into one of the most popular
Python web frameworks in use today. 1 Small yet powerful, you can build your
application from a single file, and, as it grows, organically develop components to
add functionality and complexity 2. Let’s take a look.
5.2) Installation
1. Within your terminal, navigate to your "realpython" directory, and then run the
following commands to create a virtualenv.
2. Navigate into the "flask" directory (root directory), then activate the virtual env:
Unix:
137
Real Python for the Web RealPython.com
source bin/activate
Windows:
scripts\activate
From this point on, you will no longer be separating virtualenvs by chapter.
Instead, each project will have a separate virtualenv.
138
Real Python for the Web RealPython.com
1. This creates a test server (or development server), listening on port 5000.
Open a web browser and navigate to http://127.0.0.1:5000/. You should see
the "Hello, World!" greeting.
139
Real Python for the Web RealPython.com
app = Flask(__name__)
if __name__ == "__main__":
app.run()
I know that is confusing and probably makes little, if any, sense at this point, but
you will eventually get it. Trust me.
first.py
# first.py
if __name__ == '__main__':
print 'This print statement is ran from the current
module.'
else:
print 'This print statement is ran from a different
module'
second.py
140
Real Python for the Web RealPython.com
#second.py
import first
$ python first.py
This print statement is ran from the current module.
$ python second.py
This print statement is ran from a different module
• first.py runs the code within the current module, while second.py imports the
code from first.py.
Does that make better sense? If not, take the "Google-it-first" approach. Again, this
is a confusing topic that many developers don't even really understand how it
works. That said, modules are powerful tools. Learn the basics and use them,
regardless of whether you truly understand how they work.
@app.route("/")
@app.route("/hello")
def hello_world():
return "Hello, World!"
1. The function should make sense. We're simply returning the string "Hello,
World!" .
2. Those funny looking statements above the function are called decorators.
According to the official Python documentation, a decorator is defined as:
141
Real Python for the Web RealPython.com
def world(fn):
def wrapped():
return fn() + ", World!"
return wrapped
def hello():
return "Hello"
@world
def hello_world():
return "Hello"
print hello()
print hello_world()
Make sense?
142
Real Python for the Web RealPython.com
@app.route("/")
@app.route("/hello")
def hello_world():
return "Hello, World!"
… the decorators define the URL routes. We assigned the / and /hello URLS
to the hello_world() function. Thus, when you pull up either URL in your browser,
the hello_world() function is called. Try adding another route such as
@app.route("/hw") to your script. Fire up the server and navigate to the new
URL: http://127.0.0.1:5000/hw.
Please go over this lesson again so it sinks in. It's imperative that you understand
everything that's going on, as all the Flask apps use this basic structure.
Homework
Requirements:
After a user logs in he or she is presented with all of the blog posts. Users can add
new text-only blog entries from the same screen, read the entries themselves, or
logout. That's it.
Project Structure
143
Real Python for the Web RealPython.com
◦ Windows - scripts\activate
/blog
/static
/templates
blog.py
This is a good project structure for almost any small application. The main HTML
files and templates are held in the "templates" directory, while the styles (CSS,
fonts, images, JavaScripts, etc.) are found in the "static" directory. The blog.py file,
which is the application module (or controller), receives HTTP requests from the
browser and then communicates with the database and templates to generate
responses back to the user:
Model
As you recall, our database has one table called posts with two fields - title and
post. We can use the exact same script to create and populate the database:
144
Real Python for the Web RealPython.com
import sqlite3
Save the file and run it. Then, check the SQLite Browser to ensure the table was
created correctly.
Application Module/Controller
Like the application module in the hello_world app, this script will define the
imports, configurations, and each view.
145
Real Python for the Web RealPython.com
# imports
from flask import Flask, render_template, request, session, \
flash, redirect, url_for, g
import sqlite3
# configuration
DATABASE = 'fblog.db'
app = Flask(__name__)
if __name__ == '__main__':
app.run(debug=True)
Save this file as blog.py in your main project directory. The configuration section is
used for defining application-specific settings.4
Views
After a user logs in, he or she is redirected to the main blog homepage where all
posts are displayed. Users can also add posts from this page. For now, let's get the
page set up, and worry about the functionality later.
146
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<div class="jumbo">
<h2>Welcome to the Flask Blog!</h2>
</div>
{% endblock %}
{% extends "template.html" %}
{% block content %}
<div class="jumbo">
<h2>Welcome to the Flask Blog!</h2>
<h3>Please login to access your blog.</h3>
<p>Temp Login: <a href="/main">Login</a></p>
</div>
{% endblock %}
Now update blog.py by adding two new functions for the views:
@app.route('/')
def login():
return render_template('login.html')
@app.route('/main')
def main():
return render_template('main.html')
Updated code:
147
Real Python for the Web RealPython.com
# imports
from flask import Flask, render_template, request, session, \
flash, redirect, url_for, g
import sqlite3
# configuration
DATABASE = 'fblog.db'
app = Flask(__name__)
@app.route('/')
def login():
return render_template('login.html')
@app.route('/main')
def main():
return render_template('main.html')
if __name__ == '__main__':
app.run(debug=True)
We mapped the URL '/' to the login() function, which in turn sets the route to
login.html in the templates directory. Now, how about the main page? Try to
explain it to yourself.
148
Real Python for the Web RealPython.com
Templates
Templates (or boilerplates) are HTML skeletons that serve as the base for either
your entire website or pieces of your website. They eliminate the need to code the
basic HTML structure more than once. Separating templates from the main
business logic (blog.py) helps with the overall organization. As your app grows, for
example, you could have a designer working on the front-end templates while you
work on the backend application module.
Remember how ugly it was to use print statements to embed Python into HTML
from Chapter 4:
#!/usr/bin/env python
print "Content-Type: text/html"
print
print """
<html>
<body>
<h2>Hello, World!</h2>
</body>
</html>
"""
149
Real Python for the Web RealPython.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome, friends!</title>
</head>
<body>
<div class="container">
{% block content %}
{% endblock %}
</div>
</body>
</html>
Look back at one of the views. Do you see the code {% extends
"template.html" %} ? This tag establishes the first relationship between the
template and views. When Flask renders main.html it must first render
template.html.
You may have also noticed that both the views and template files have identical
block tags: {% block content %} and {% endblock %} . These define where
the child templates, login.html and main.html, fill in on the parent template. When
Flask renders the parent template, template.html, the block tags are filled in with
the code from the child templates:
150
Real Python for the Web RealPython.com
All right! Fire up your server, navigate to http://localhost:5000/, and let's run a test
to make sure everything is working up to this point.
You should see the login page, and then if you click the link, you should be
directed to the main page. If not, kill the server and double-check your code against
mine.
User Login
Now that we have the basic structure set up, let's have some fun and add the
blog's main functionality. Starting with the login page, we need to set up a basic
HTML form for users to login, so that they can access the main blog page.
Add the following username and password variables to the configuration section in
blog.py:
USERNAME = 'admin'
PASSWORD = 'admin'
Also in the configuration section, add the secret_key, which is used for managing
user sessions:
151
Real Python for the Web RealPython.com
Make the value of your secret key really, really hard, if not impossible, to
guess. Use a random key generator to do this. Never, ever use a value you
pick on your own. 6
# configuration
DATABASE = 'fblog.db'
USERNAME = 'admin'
PASSWORD = 'admin'
SECRET_KEY = 'hard to guess'
Update the login() function in the blog.py file to match the following code:
This function compares the username and password entered against those from
the configuration section. If the correct username and password are entered, the
user is redirected to the main page and the session key, logged_in , is set to
True . If the wrong information is entered, an error message is flashed to the user.
152
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<h2>Welcome to the Flask Blog!</h2>
<h3>Please login to access your blog.</h3>
<form action="" method="post">
Username: <input type="text" name="username" value="{{
request.form.username }}">
Password: <input type="password" name="password" value="{{
request.form.password }}">
<p><input type="submit" value="Login"></p>
</form>
{% endblock %}
@app.route('/logout')
def logout():
session.pop('logged_in', None)
flash('You were logged out')
return redirect(url_for('login'))
The logout() function uses the pop() method to reset the session key to the default
value when the user logs out. It then redirects the user back to the login screen and
flashes a message indicating that they were logged out.
Add the following code to the template.html file, just before the content tag ( {%
block content %} ):
153
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<div class="jumbo">
<h2>Welcome to the Flask Blog!</h2>
<p><a href="/logout">Logout</a></p>
</div>
{% endblock %}
Fire up the server. Test everything out. Make sure you can login and logout and
that the appropriate messages are displayed.
Now that users are able to login and logout, we need to protect main.html from
unauthorized access. Currently, it can be accessed without logging in. Go ahead
and see for yourself: Launch the server and navigate in your browser to
http://localhost:5000/main. This is not good.
Functools is a module used for extending the capabilities of functions with other
functions.7 First, setup the new function in blog.py:
154
Real Python for the Web RealPython.com
def login_required(test):
@wraps(test)
def wrap(*args, **kwargs):
if 'logged_in' in session:
return test(*args, **kwargs)
else:
flash('You need to login first.')
return redirect(url_for('login'))
return wrap
This tests to see if logged_in is in the session. If it is, then we call the method,
and if not, the user is redirected back to the login screen with a message stating
that a login is required.
@app.route('/main')
@login_required
def main():
return render_template('main.html')
Updated code:
155
Real Python for the Web RealPython.com
# imports
from flask import Flask, render_template, request, session, \
flash, redirect, url_for
import sqlite3
from functools import wraps
# configuration
DATABASE = 'fblog.db'
USERNAME = 'admin'
PASSWORD = 'admin'
SECRET_KEY = 'hard to guess'
app = Flask(__name__)
def login_required(test):
@wraps(test)
def wrap(*args, **kwargs):
156
Real Python for the Web RealPython.com
if 'logged_in' in session:
return test(*args, **kwargs)
else:
flash('You need to login first.')
return redirect(url_for('login'))
return wrap
@app.route('/main')
@login_required
def main():
return render_template('main.html')
@app.route('/logout')
def logout():
session.pop('logged_in', None)
flash('You were logged out')
return redirect(url_for('login'))
if __name__ == '__main__':
app.run(debug=True)
Test this out. But first, did you notice in the terminal that you can see the client
requests as well as the server responses? After you perform each test check the
server responses.
1. Login successful:
157
Real Python for the Web RealPython.com
The login credentials were sent with a POST request, the server responded
with a 302, redirecting the user to main.html. The GET request to access
main.html was successful, as the server responded with a 200.
2. Logout:
When you logged out, you are actually issuing a GET request that redirects to
login.html. This request was successful.
3. Login failed:
If you enter the wrong login credentials when trying to login you still get a 200
success code as the server responds with an error.
If you try to access main.html without logging in first, you will be redirected
back to login.html.
The server log comes in handy when you need to debug your code. Let's say, for
example, that you forgot to add the redirect to the login() function ( return
redirect(url_for('main')) ). If you glance at your code and can't figure out
what's going on, the server log may provide a hint:
158
Real Python for the Web RealPython.com
You can see that the POST request was successful, but nothing happened after.
This should give you enough of a hint to know what to do. This is a rather simple
case, but you will find that when your codebase grows just how handy the server
log can be with respect to debugging errors.
Show Posts
Now that security is set up, we need to display some information to the user.
What’s the point of the user logging in in the first place? Let's start by displaying
the current posts. Update the main() function within blog.py:
@app.route('/main')
@login_required
def main():
g.db = connect_db()
cur = g.db.execute('select * from posts')
posts = [dict(title=row[0], post=row[1]) for row in
cur.fetchall()]
g.db.close()
return render_template('main.html', posts=posts)
Here we are connecting to the database and then fetching data from the "posts"
table. The data is then passed to a dictionary, which is assigned to the variable
posts . Finally, we pass that variable to the main.html file.
We next need to edit main.html to loop through the dictionary in order to display the
tiles and posts:
159
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<h2>Welcome to the Flask Blog!</h2>
<p><a href="/logout">Logout</a></p>
<br/>
<br/>
<h3>Posts:</h3>
{% for p in posts %}
<strong>Title:</strong> {{ p.title }} <br/>
<strong>Post:</strong> {{ p.post }} <br/>
<br/>
{% endfor %}
{% endblock %}
Add Posts
Finally, users need the ability to add new posts. We can start by adding a new
function to blog.py called add():
160
Real Python for the Web RealPython.com
@app.route('/add', methods=['POST'])
@login_required
def add():
title = request.form['title']
post = request.form['post']
if not title or not post:
flash("All fields are required. Please try again.")
return redirect(url_for('main'))
else:
g.db = connect_db()
g.db.execute('insert into posts (title, post) values
(?, ?)',
[request.form['title'],
request.form['post']])
g.db.commit()
g.db.close()
flash('New entry was successfully posted')
return redirect(url_for('main'))
First, we used an if statement to ensure that all fields are populated with data.
Then, the data is added, as a new row, to the table.
161
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<h2>Welcome to the Flask Blog!</h2>
<p><a href="/logout">Logout</a></p>
<div class="add">
<h3>Add a new post:</h3>
<form action="{{ url_for('add') }}" method="post"
class="add">
<label><strong>Title:</strong></label>
<input name="title" type="text">
<p><label><strong>Post:</strong></label><br/>
<textarea name="post" rows="5" cols="40"></textarea></p>
<input class="button" type="submit" value="Save">
</form>
<br/>
<br/>
<h3>Posts:</h3>
{% for p in posts %}
<strong>Title:</strong> {{ p.title }} <br/>
<strong>Post:</strong> {{ p.post }} <br/>
<br/>
{% endfor %}
{% endblock %}
We issued an HTTP POST request to submit the form to the add() function, which
then redirected us back to main.html with the new post:
Style
All right. Now that the app is working properly, let's make it look a bit nicer. To do
this, we need to edit the HTML, CSS, and JavaScript. I'll show you a more in-depth
162
Real Python for the Web RealPython.com
example in the next chapters. For now, though, let's just create something very
simple:
Save this as styles.css and place it in your "static" directory. Then add a link to the
external stylesheet within the head ( <head> </head> ) of the template.html file:
This tag is fairly straightforward. Essentially, the url_for() function generates a URL
to the styles.css file. In other words, this translates to: "Look in the static folder for
the file styles.css".
Feel free to play around with the CSS more if you'd like. If you do, send me the
CSS, so I can make mine look better. :)
163
Real Python for the Web RealPython.com
Conclusion
Let's recap:
1. First, we used Flask to create a basic website structure to house static pages.
2. Then we added a login form.
3. We added sessions and the login_required decorator to prevent unauthorized
access to the main.html page.
4. Next, we fetched data from SQLite to show all the blog posts, then added the
ability for users to add new posts.
5. Finally, we added some basic CSS styles.
Simple, right? You can now take a look at the accompanying video to see how to
deploy your app on PythonAnywhere.
↩
1. http://lucumr.pocoo.org/2010/4/3/april-1st-post-mortem/↩
↩
2. http://flask.pocoo.org/↩
↩
3. http://docs.python.org/2/glossary.html#term-decorator↩
↩
4. http://flask.pocoo.org/docs/config/↩
↩
5. http://wiki.python.org/moin/Templating↩
↩
6. http://en.wikipedia.org/wiki/Session_key↩
↩
7. http://docs.python.org/2/library/functools.html↩
164
Real Python for the Web RealPython.com
6) Flask: FlaskTaskr
6.1) Overview
In this section, we will develop a task manager called FlaskTaskr. We'll start by
creating a simple skeleton app like in the blog exercise in the last chapter but we'll
be adding plenty of bells and whistles in order to make this a full-featured
application. Let's get to it.
1. Users sign in and out from the landing page, which is managed by sessions.
Only one user is supported.
2. Once signed in, users can add new tasks. Each task consists of a name, due
date, priority, status, and an auto-incremented ID.
3. Users can view all uncompleted tasks from the same screen.
4. Users can also delete tasks and mark tasks as completed. If a user deletes a
task, it will also be deleted from the database.
Before beginning take a moment to review the steps taken to create the blog
application from the previous Chapter. We'll be using the same process in Lesson
6.2, but it will go much faster.
Homework
165
Real Python for the Web RealPython.com
3. Fire up virtualenv:
Unix:
$ source env/bin/activate
Windows:
$ env\scripts\activate
4. Install Flask:
└── app
├── static
└── templates
Configuration
Create a configuration file called config.py and save it in the main directory
("flasktaskr"):
166
Real Python for the Web RealPython.com
# config.py
import os
DATABASE = 'flasktask.db'
USERNAME = 'admin'
PASSWORD = 'admin'
SECRET_KEY = 'my precious'
Remember the configuration section of our application module from the blog app?
Go back and take a look. For medium and large-sized applications, it's good to
create a separate script altogether to house this information so that the application
module is easier to read.
Database
Based on the info above regarding the main functionality of the app, we need one
database table, consisting of these fields - "task_id", "name", "due_date", "priority",
and "status". The value of status will either be a 1 or 0: 1 if the task is open and 0 if
closed.
167
Real Python for the Web RealPython.com
# db_create.py
import sqlite3
from config import DATABASE_PATH
1. Notice how we did not need to specify the "task_id" when entering data into
the table as it's an auto-incremented value, which means that it's auto-
generated with each new row of data. Also, we used a status of 1 to indicate
that each of those tasks are considered "open" tasks.
Save the file as db_create.py under the root directory and run it. Was the table
created? Did it populate with data? How do you check? SQL Database Browser.
168
Real Python for the Web RealPython.com
Application Module/Controller
Create an application module (controller) called views.py and fill it with the
following code:
169
Real Python for the Web RealPython.com
# views.py
app = Flask(__name__)
app.config.from_object('config')
def connect_db():
return sqlite3.connect(app.config['DATABASE'])
def login_required(test):
@wraps(test)
def wrap(*args, **kwargs):
if 'logged_in' in session:
return test(*args, **kwargs)
else:
flash('You need to login first.')
return redirect(url_for('login'))
return wrap
@app.route('/logout/')
def logout():
session.pop('logged_in', None)
flash('You are logged out. Bye. :(')
return redirect (url_for('login'))
170
Real Python for the Web RealPython.com
session['logged_in'] = True
return redirect(url_for('tasks'))
return render_template('login.html', error=error)
Note: we're saving this as views.py, since the controller holds all the views.
Save this file in the "app" directory. You've seen this all before. Right now, we have
one view, login.html, which is mapped to the main URL, '/' . Sessions and the
login_required decorator are set-up. You can see that after you login, you are
redirected to tasks , which still needs to be specified. Please refer to the blog
application from the previous chapter for further explantation on any details of this
code that you do not understand.
Let's go ahead and setup the login template, base template, and stylesheet.
Login template:
{% extends "template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskr.</h1>
<h3>Please login to access your task list.</h3>
<form action="" method="post">
Username: <input type="text" name="username" value="{{
request.form.username }}">
Password: <input type="password" name="password" value="{{
request.form.password }}">
<input type="submit" value="Login">
</form>
<p><em>Use 'admin' for the username and password.</em></p>
{% endblock %}
Base template:
171
Real Python for the Web RealPython.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to FlaskTaskr!!</title>
<link rel="stylesheet" href="{{ url_for('static',
filename='styles.css') }}">
</head>
<body>
<div class="page">
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
<br/>
{% endfor %}
{% if error %}
<div class="error"><strong>Error:</strong> {{ error
}}</div>
{% endif %}
{% block content %}
{% endblock %}
</div>
</body>
</html>
We'll temporarily "borrow" the majority of the stylesheet from the Flask website.
Please copy and paste this.
172
Real Python for the Web RealPython.com
body {
font-family: sans-serif;
background: #eee;
}
h1, h2 {
font-family: 'Georgia', serif;
margin: 0;
}
h2 { font-size: 1.5em; }
.page {
margin: 2em auto;
width: 50em;
border: 5px solid #ccc;
padding: 0.8em;
background: white;
}
.entries {
list-style: none;
margin: 0;
padding: 0;
}
.add-task {
font-size: 0.9em;
border-bottom: 1px solid #ccc;
}
.add-task dl { font-weight: bold; }
173
Real Python for the Web RealPython.com
.metanav {
text-align: right;
font-size: 0.8em;
padding: 0.3em;
margin-bottom: 1em;
background: #fafafa;
}
.flash {
background: #CEE5F5;
padding: 0.5em;
}
.error {
background: #F0D6D6;
padding: 0.5em;
}
.datagrid table {
border-collapse: collapse;
text-align: left;
width: 100%;
}
.datagrid {
background: #fff;
overflow: hidden;
border: 1px solid #000000;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.datagrid table td, .datagrid table th { padding: 3px 10px; }
.datagrid table thead th {
background:-webkit-gradient(linear, left top, left bottom,
color-stop(0.05, #000000), color-stop(1, #000000));
background:-moz-linear-gradient(center top, #000000 5%,
#000000 100%);
174
Real Python for the Web RealPython.com
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#000000',
endColorstr='#000000');
background-color:#000000;
color:#FFFFFF; font-size:
15px; font-weight:
bold;
}
.datagrid table thead th:first-child { border: none; }
.datagrid table tbody td {
color: #000000;
border-left: 1px solid #E1EEF4;
font-size: 12px;
font-weight: normal;
}
.datagrid table tbody .alt td {
background: #E1EEF4;
color: #000000;
}
.datagrid table tbody td:first-child { border-left: none; }
.datagrid table tbody tr:last-child td { border-bottom: none; }
.button {
background-color:#000;
display:inline-block;
color:#ffffff;
font-size:13px;
padding:3px 12px;
margin: 0;
text-decoration:none;
position:relative;
}
175
Real Python for the Web RealPython.com
# run.py
Instead of running the application from the application module. We created this
separate file to handle it. Save it as run.py in the "app" directory. Before running,
though, create a blank __init__.py file, which establishes our app as a Python
Project. Save this in the "app" directory as well.
├── app
│ ├── __init__.py
│ ├── static
│ │ └── styles.css
│ ├── templates
│ │ ├── login.html
│ │ ├── tasks.html
│ │ └── template.html
│ └── views.py
├── config.py
├── db_create.py
├── flasktask.db
└── run.py
$ python run.py
Make sure everything works thus far. You'll only be able to view the login page (but
not login) and the styles, as we have not setup the tasks.html page yet.
176
Real Python for the Web RealPython.com
Tasks
The tasks.html page will be quite a bit different than the main.html page from our
blog as the user will have the ability to delete tasks and mark tasks as complete
rather than just being able to add new rows (tasks) to the table.
@app.route('/tasks/')
@login_required
def tasks():
g.db = connect_db()
cur = g.db.execute('select name, due_date, priority,
task_id from ftasks where status=1')
open_tasks = [dict(name=row[0], due_date=row[1],
priority=row[2], task_id=row[3]) for row in cur.fetchall()]
cur = g.db.execute('select name, due_date, priority,
task_id from ftasks where status=0')
closed_tasks = [dict(name=row[0], due_date=row[1],
priority=row[2], task_id=row[3]) for row in cur.fetchall()]
g.db.close()
return render_template('tasks.html', open_tasks=open_tasks,
closed_tasks=closed_tasks)
We queried the database for open and closed tasks, saving the results to two
variables, and then passing those variables, open_tasks and closed tasks , to
the tasks.html page. These variables will then be used to populate the open and
closed task lists.
177
Real Python for the Web RealPython.com
Next, we need to add the ability to add new tasks, mark tasks as complete, and
delete tasks. Add each of these three functions to the views.py file:
178
Real Python for the Web RealPython.com
# Delete Tasks:
@app.route('/delete/<int:task_id>/',)
@login_required
def delete_entry(task_id):
g.db = connect_db()
cur = g.db.execute('delete from ftasks where
179
Real Python for the Web RealPython.com
task_id='+str(task_id))
g.db.commit()
g.db.close()
flash('The task was deleted.')
return redirect(url_for('tasks'))
The last two functions pass in a variable, task_id from the tasks.html page
(which we will create next). This variable is equal to the task_id field in the
database. A query is then performed and the appropriate action takes place. In this
case, an action means either marking a task as complete or deleting a task. Notice
how we have to convert the task_id variable to a string, since we are using
concatenation to combine the SQL query to the task_id , which is an integer.
Tasks Template:
180
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskR</h1>
<a href="/logout">Logout</a>
<div class="add-task">
<h3>Add a new task:</h3>
<table>
<tr>
<form action="{{ url_for('new_task') }}" method="post">
<td>
<label>Task Name:</label>
<input name="name" type="text">
</td>
<td>
<label>Due Date (mm/dd/yyyy):</label>
<input name="due_date" type="text" width="120px">
</td>
<td>
<label>Priority:</label>
<select name="priority" width="100px">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
<option value="8">8</option>
<option value="9">9</option>
<option value="10">10</option>
</select>
</td>
<td>
<input class="button" type="submit" value="Save">
</td>
</form>
</tr>
181
Real Python for the Web RealPython.com
</table>
</div>
<div class="entries">
<br/>
<br/>
<h2>Open tasks:</h2>
<div class="datagrid">
<table>
<thread>
<tr>
<th width="300px"><strong>Task Name</strong></th>
<th width="100px"><strong>Due Date</strong></th>
<th width="50px"><strong>Priority</strong></th>
<th><strong>Actions</strong></th>
</tr>
</thread>
{% for o in open_tasks %}
<tr>
<td width="300px">{{ o.name }}</td>
<td width="100px">{{ o.due_date }}</td>
<td width="50px">{{ o.priority }}</td>
<td>
<a href="{{ url_for('delete_entry', task_id =
o.task_id) }}">Delete</a> -
<a href="{{ url_for('complete', task_id =
o.task_id) }}">Mark as Complete</a>
</td>
</tr>
{% endfor %}
</table>
</div>
<br/>
<br/>
<div class="entries">
<h2>Closed tasks:</h2>
<div class="datagrid">
<table>
<thread>
<tr>
182
Real Python for the Web RealPython.com
Although a lot is going on in here, the only new things are these statements:
Essentially, we pulled the task_id from the database dynamically from each row
in the database table as the for loop progresses. We then assigned it to a variable,
also named task_id , which is then passed back to either the delete() function -
@app.route('/delete/<int:task_id>/',) - or the complete() function -
@app.route('/complete/<int:task_id>/',) .
183
Real Python for the Web RealPython.com
Finally, go ahead and test out the functionality of the app. Fire up the server:
python run.py . If you get any errors, be sure to double check your code.
184
Real Python for the Web RealPython.com
Homework
That said, no matter how much you use an ORM, you will eventually have to use
SQL for troubleshooting or testing quick, one-off queries as well as advanced
queries. It's also really, really helpful to know SQL, when trying to decide on the
most efficient way to query the database, to know what calls the ORM will be
making to the database. Learn SQL first, in other words. :)
Create a Table
185
Real Python for the Web RealPython.com
Delete flasktask.db, and then create a new file called models.py in the "app"
directory. We're going to recreate the database using SQLAlchemy.
# models.py
class FTasks(db.Model):
__tablename__ = "ftasks"
def __repr__(self):
return '<name %r>' % (self.body)
We also have to transfer the creation of the Flask application object from views.py
to __init__.py.
186
Real Python for the Web RealPython.com
# __init__.py
app = Flask(__name__)
app.config.from_object('config')
db = SQLAlchemy(app)
The above file was created so that our modules (like the views and models
modules) will be able to access the app and db objects.
import sqlite3
app = Flask(__name__)
app.config.from_object('config')
def connect_db():
return sqlite3.connect(app.config['DATABASE'])
187
Real Python for the Web RealPython.com
# config.py
import os
DATABASE = 'flasktaskr.db'
USERNAME = 'admin'
PASSWORD = 'admin'
SECRET_KEY = 'my precious'
# run.py
188
Real Python for the Web RealPython.com
# db_create.py
# insert data
db.session.add(FTasks("Finish this tutorial", date(2013, 3,
13), 10, 1))
db.session.add(FTasks("Finish my book", date(2013, 3, 13), 10,
1))
$ python db_create.py
The flasktask.db should have been recreated. Open up SQLite Browser to ensure
that the table and data above are present in the ftasks table.
189
Real Python for the Web RealPython.com
Note that variable names are used as the column names, any field that has a
primary_key set to True will auto-increment.
4. We then populated it with some data. We used the FTASKS object to specify
the data we wanted:
190
Real Python for the Web RealPython.com
# views.py
def login_required(test):
@wraps(test)
def wrap(*args, **kwargs):
if 'logged_in' in session:
return test(*args, **kwargs)
else:
flash('You need to login first.')
return redirect(url_for('login'))
return wrap
@app.route('/logout/')
def logout():
session.pop('logged_in', None)
flash('You are logged out. Bye. :(')
return redirect (url_for('login'))
191
Real Python for the Web RealPython.com
@app.route('/tasks/')
@login_required
def tasks():
open_tasks =
db.session.query(FTasks).filter_by(status='1').order_by(
FTasks.due_date.asc())
closed_tasks =
db.session.query(FTasks).filter_by(status='0').order_by(
FTasks.due_date.asc())
return render_template('tasks.html',form =
AddTask(request.form),
open_tasks=open_tasks,
closed_tasks=closed_tasks)
db.session.query(FTasks).filter_by(task_id=new_id).update({"status":"0"})
db.session.commit()
192
Real Python for the Web RealPython.com
# Delete Tasks:
@app.route('/delete/<int:task_id>/',)
@login_required
def delete_entry(task_id):
new_id = task_id
db.session.query(FTasks).filter_by(task_id=new_id).delete()
db.session.commit()
flash('The task was deleted. Why not add a new one?')
return redirect(url_for('tasks'))
Since we are now using SQLAlchemy, we've modified the way we do database
queries, INSERT and DELETE. The code is much cleaner. Take a look. Compare it
with the actual SQL code from the beginning of the chapter.
Now let's create a new file called forms.py and add the following code:
193
Real Python for the Web RealPython.com
# forms.py
class AddTask(Form):
task_id = IntegerField('Priority')
name = TextField('Task Name', validators=[Required()])
due_date = DateField('Date Due (mm/dd/yyyy)',
validators=[Required()],
format='%m/%d/%Y')
priority = SelectField('Priority', validators=[Required()],
choices=[('1', '1'),('2',
'2'),('3', '3'),
('4', '4'),('5', '5')])
status = IntegerField('Status')
Save it in "app" directory. As the name suggests, the validators, validate the data
submitted by the user. For example, Required simply means that the field cannot
be blank, while the format validator restricts the input to the MM/DD/YY date
format.
Fire up your server: python run.py . Ensure that you can still view tasks, add
new tasks, mark tasks as complete, and delete tasks.
Let's allow multiple users to access the task manager by setting up a user
registration form.
First we need to create a new table in our database to house user data. To do so,
just add a new class to models.py:
194
Real Python for the Web RealPython.com
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True, nullable=False)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
def __repr__(self):
return '<User %r>' % (self.name)
Run db_create.py again. Open up SQLite Browser. Notice how it ignores the table
already created and just creates the users table:
Configuration
195
Real Python for the Web RealPython.com
USERNAME = 'admin'
PASSWORD = 'admin'
We no longer need this configuration since we will use the information from the
users table in the database.
We also need to update forms.py to cater for registration and logging in:
196
Real Python for the Web RealPython.com
# forms.py
class RegisterForm(Form):
name = TextField('Username', validators=[Required(),
Length(min=6, max=25)])
email = TextField('Email', validators=[Required(),
Length(min=6, max=40)])
password = PasswordField('Password',
validators=[Required(), Length(min=6, max=40)])
confirm = PasswordField('Repeat Password',[Required(),
EqualTo('password', message='Passwords must match')])
class LoginForm(Form):
name = TextField('Username', validators=[Required()])
password = PasswordField('Password',
validators=[Required()])
class AddTask(Form):
task_id = IntegerField('Priority')
name = TextField('Task Name', validators=[Required()])
due_date = DateField('Date Due (mm/dd/yyyy)',
validators=[Required()],format='%m/%d/%Y')
priority = SelectField('Priority',
validators=[Required()],choices=[('1', '1'),('2', '2'),('3',
'3'),('4', '4'),('5', '5')])
status = IntegerField('Status')
197
Real Python for the Web RealPython.com
These allow us to use the RegisterForm and LoginForm classes from forms.py
and the User class from model.py.
@app.route('/register/', methods=['GET','POST'])
def register():
error = None
form = RegisterForm(request.form, csrf_enabled=False)
if form.validate_on_submit():
new_user = User(
form.name.data,
form.email.data,
form.password.data,
)
db.session.add(new_user)
db.session.commit()
flash('Thanks for registering. Please login.')
return redirect(url_for('login'))
return render_template('register.html', form=form,
error=error)
Here, the user information, which will be passed from the register.html
template, is stored inside the variable new_user . That data is then stored in
the database, and after successful registration, the user is redirected to
login.html with a message thanking them for registering.
validate_on_submit() returns either True or False depending on whether
the submitted data passes the form validators associated with each field in the
form.
Templates
Registration:
198
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskr.</h1>
<h3>Please register to access the task list.</h3>
<form method="POST" action="">
<p>{{ form.name.label }}: {{ form.name }} {{
form.email.label }}: {{ form.email }}</p>
<p>{{ form.password.label }}: {{ form.password
}} {{ form.confirm.label }}: {{ form.confirm }}</p>
<p><input type="submit" value="Register"></p>
</form>
<p><em>Already registered?</em> Click <a href="/">here</a>
to login.</p>
{% endblock %}
Let's go ahead and test it out. Load the server, click the link to register, and register
a new user. You should be able to register just fine, but we need to update the coe
so users can login. Everything turn out okay? Double check my code, if not.
Application Module
199
Real Python for the Web RealPython.com
password=request.form['password']).first()
if u is None:
error = 'Invalid username or password.'
else:
session['logged_in'] = True
flash('You are logged in. Go Crazy.')
return redirect(url_for('tasks'))
return render_template("login.html",
form = LoginForm(request.form),
error = error)
This code is not too much different than the old code. When a user submits
their user credentials via a POST request, the database is queried for the
submitted username and password. If the credentials are not found, an error
populates; otherwise, the user is logged in and redirected to tasks.html.
Templates
200
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskr.</h1>
<h3>Please login to access your task list.</h3>
<form method="post" action="">
<p>{{ form.name.label }}: {{ form.name }} {{
form.password.label }}: {{ form.password }} <input
type="submit" value="Submit"></p>
</form>
<p><em>Need an account? </em><a
href="/register">Signup!!</a></p>
{% endblock %}
Test it out. Try logging in with the same user you registered. If done correctly, you
should get logged in and redirected to tasks.html. Check out the server logs:
Can you tell what happened? Can you predict what the server log will look like
when you submit a bad username and/or password? Try it.
First, let's update the database to add two new fields: "posted_date" and "user_id"
to the FTASKS table. The "user_id" field needs to link back to the User table.
Database relationships
201
Real Python for the Web RealPython.com
"user_id" field from the FTASKS table, we can do basic SQL queries to find out
who created a certain task as well as find out all the tasks created by a certain
user:
Let's look at how to alter the tables to create such relationships within model.py.
The "user_id" field in the ftasks table is a foreign key, which binds the values from
this field to the values found in the "id" field of the users table. Foreign keys are
essential for creating relationships between tables in order to correlate information.
Need help with foreign keys? Take a look at the W3C documentation.
For example:
202
Real Python for the Web RealPython.com
In our case, we have a one to many relationship: one user can post many tasks:
We also need to update the imports as well as add another field, "posted_date", to
the FTASKS class:
203
Real Python for the Web RealPython.com
# models.py
class FTasks(db.Model):
__tablename__ = "ftasks"
def __repr__(self):
return '<name %r>' % (self.body)
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String, unique=True, nullable=False)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
tasks = db.relationship('FTasks', backref='poster')
204
Real Python for the Web RealPython.com
def __repr__(self):
return '<User %r>' % (self.name)
The above code will work if we will be using a fresh, empty database. But since our
database already has the ftasks and users tables, SQLAlchemy will not try to
redefine these database tables. We need a migration script that will update the
schema and transfer any existing data:
205
Real Python for the Web RealPython.com
# db_migrate.py
206
Real Python for the Web RealPython.com
Note that this script did not touch the "users" table; it is only the "ftasks" table that
has underlying schema changes. Using SQLite Browser, verify that the
posted_date and user_id columns have been added to the ftasks table.
Forms
We now need to add the posted_date field to the AddTask form. Open forms.py
and add the following line at the end of AddTask class:
Application Module
We also need to update our view for adding new tasks. Within the new_task
function.
207
Real Python for the Web RealPython.com
new_task = FTasks(
form.name.data,
form.due_date.data,
form.priority.data,
'1'
)
to:
new_task = FTasks(
form.name.data,
form.due_date.data,
form.priority.data,
form.posted_date.data,
'1',
'1'
)
Templates
208
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskR</h1>
<br/>
<a href="/logout">Logout</a>
<div class="add-task">
<h3>Add a new task:</h3>
<form method="POST" action="{{ url_for('new_task')
}}">
<p>{{ form.name.label }}: {{ form.name }}<br />{{
form.due_date.label }}:
{{ form.due_date }} {{ form.posted_date.label }}:
{{ form.posted_date }} {{ form.priority.label }}: {{
form.priority }}</p>
<p><input type="submit" value="Submit"></p
</form>
</div>
<div class="entries">
<br/>
<br/>
<h2>Open tasks:</h2>
<div class="datagrid">
<table>
<thread>
<tr>
<th width="200px"><strong>Task Name</strong></th>
<th width="75px"><strong>Due Date</strong></th>
<th width="100px"><strong>Posted Date</strong></th>
<th width="50px"><strong>Priority</strong></th>
<th width="90px"><strong>Posted By</strong></th>
<th><strong>Actions</strong></th>
</tr>
</thread>
{% for o in open_tasks %}
<tr>
<td width="200px">{{ o.name }}</td>
<td width="75px">{{ o.due_date }}</td>
<td width="100px">{{ o.posted_date }}</td>
<td width="50px">{{ o.priority }}</td>
209
Real Python for the Web RealPython.com
210
Real Python for the Web RealPython.com
{% endfor %}
</table>
</div>
{% endblock %}
The changes are fairly straightforward. Take a look at this file along with forms.py
to see how the drop-down list is implemented. And now you are ready to test! Fire
up your server and try adding a few tasks. Register a new user and add some
more tasks. We can see that the first user is always showing up under Posted by.
Well, with that information, we can query for the actual name of the user for each
task posted. First, we need to log the user_id in the session when a user
successfully logs in. So make the update to the login() function in views.py:
211
Real Python for the Web RealPython.com
password=request.form['password']).first()
if u is None:
error = 'Invalid username or password.'
else:
session['logged_in'] = True
session['user_id'] = u.id
flash('You are logged in. Go Crazy.')
return redirect(url_for('tasks'))
return render_template("login.html",
form = LoginForm(request.form),
error = error)
Next, when we post a new task, we need to grab that user id and add it to the
SQLAlchemy ORM query:
212
Real Python for the Web RealPython.com
We're grabbing the current user in session, pulling the user id and adding it to the
query.
Another pop() method needs to be used for when a user logs out:
@app.route('/logout/')
def logout():
session.pop('logged_in', None)
session.pop('user_id', None)
flash('You are logged out. Bye. :(')
return redirect (url_for('login'))
Now open up tasks.html. In each of the two for loops, note these statements:
Go back to your model real quick, and notice that because we used poster as the
backref, we can use it like a regular query object.
213
Real Python for the Web RealPython.com
Fire up your server. Register a new user and then login using that newly created
user. Create new tasks and see how the "Posted By" field gets populated with the
name of the user who created the task.
With that, we're done looking at database relationships and the conversion to
SQLAlchemy. Again, we can now easily switch SQL database engines. The code
now abstracts away much of the repetition from straight SQL so our code is cleaner
and more readable.
Try to register a new user without entering any information. Nothing should
happen. Obviously this will be very confusing for end users. Thus, we need to add
in an error message. Fortunately WTForms provides error messages for any form
that has a validator attached to it.
Go ahead and check out forms.py, there are already some validators in place, it's
pretty straightforward. For example, in the RegisterForm class, the name field
should be between 6 and 25 characters:
214
Real Python for the Web RealPython.com
class RegisterForm(Form):
name = TextField('Username', validators=[Required(),
Length(min=6, max=25)])
email = TextField('Email', validators=[Required(),
Length(min=6, max=40)])
password = PasswordField('Password',
validators=[Required(),
Length(min=6, max=40)])
confirm = PasswordField(
'Repeat Password',
[Required(), EqualTo('password',
message='Passwords must match')])
def flash_errors(form):
for field, errors in form.errors.items():
for error in errors:
flash(u"Error in the %s field - %s" % (
getattr(form, field).label.text,error), 'error')
if form.validate_on_submit():
###Do Something###
else:
flash_errors(form)
Again, try to register a new user without entering any information. You should now
see the error messages. Now, log into the application. Try creating a task without
entering any information. You should see error messages here as well.
215
Real Python for the Web RealPython.com
Test to see what happens when you try to register someone with the same
username and/or password. You should get an IntegrityError. We need to use the
try/except pair to handle the error, as follows:
@app.route('/register/', methods=['GET','POST'])
def register():
error = None
form = RegisterForm(request.form, csrf_enabled=False)
if form.validate_on_submit():
new_user = User(
form.name.data,
form.email.data,
form.password.data,
)
try:
db.session.add(new_user)
db.session.commit()
flash('Thanks for registering. Please login.')
return redirect(url_for('login'))
except IntegrityError:
error = 'Oh no! That username and/or email already
exist. Please try again.'
else:
flash_errors(form)
return render_template('register.html', form=form,
error=error)
Essentially, the try block code attempts to execute. If the program encounters the
error specified in the except block, the code execution is stopped and the code
216
Real Python for the Web RealPython.com
within the except block is ran. If the error does not occur then the program fully
executes and the except block is skipped altogether.
Test again to see what happens when you try to register someone with the same
username and/or email address.
You will never be able to anticipate every error, which is why you need to
implement error handlers to catch common errors to handle them gracefully so that
your application looks professional and to prevent any security vulnerabilities.
The first thing you absolutely must do when preparing to make your app available
to the public (deploying to production) is turn off debug mode. Debug mode simply
provides a handy debugger for when errors occur, which is great during
development, but you never want users to see this. It's also a security vulnerability,
as it is possible to execute code through the debugger. You'll find the parameter in
the run.py file:
app.run(debug=True)
Now go back and comment out - e.g., add comments to the code so the Python
interpreter skips the code altogether - the try/except pairs within the register()
function that you just set up, and then register a user with a duplicate name. You
should see an Internal Server Error, which is also know as a 500 error. Another
common error is the annoying 404 Page Not Found. Again, no matter how much
you try to prevent such errors, they will still occur from time to time. Fortunately,
Flask makes it easy to customize error handlers to handle these more gracefully.
You set these up as functions like any other view:
217
Real Python for the Web RealPython.com
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500
@app.errorhandler(404)
def internal_error(error):
return render_template('404.html'), 404
{% extends "template.html" %}
{% block content %}
<h1>Sorry ...</h1>
<p>There's nothing here!</p>
<p><a href="{{url_for('login')}}">Back</a></p>
{% endblock %}
{% extends "template.html" %}
{% block content %}
<h1>Something's wrong!</h1>
<p>Fortunately we are on the job, and you can just return
to the login page!</p>
<p><a href="{{url_for('login')}}">Back</a></p>
{% endblock %}
Now try to add a duplicate entry. You should be redirected to the 500.html
template. Easy, right? Set them up for other common errors like 403 and 410 as
well. Don't forget to to uncomment the try/except pair from the register() function.
218
Real Python for the Web RealPython.com
Logging Module
Finally, it's vital that you set up a means of capturing all errors so that you can spot
trends, setup handling, and, of course, fix them.
It's very easy to setup logging at the server level or within the Flask application
itself. Many of the third party libraries also log errors.
if not app.debug:
import os
import logging
file_handler =
FileHandler(os.path.join(basedir,'error.log'))
file_handler.setFormatter(Formatter('%(asctime)s
%(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'))
app.logger.setLevel(logging.INFO)
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.info('errors')
Here we set the filename that we want errors to be logged in, error.log and
choose the message format (time, error level, error message) as well as the
logging level. There are five levels shown in increasing orders of severity:
1. DEBUG
2. INFO
3. WARNING
4. ERROR
219
Real Python for the Web RealPython.com
5. CRITICAL
By setting the level to INFO , the majority of errors will be logged. If you set up an
email logger, you would probably want to set it at a higher severity so only the most
important errors are sent to your email - otherwise, you may stop viewing them
altogether.
In __init__.py, try adding the above logging code just before from flasktaskr
import views, models and then set debug=False in run.py. Now try
generating an error - i.e., navigating to a page that doesn't exist, and you'll see an
error.log file in the main project directory.
Alright, in the next lesson, we'll attempt to prevent some errors from happening
altogether as we look at unit testing.
In many cases, defining the unit tests is done before the application is developed.
This helps you fully hash out your application's requirements. It also prevents over-
coding: You develop your application until the unit test passes, adding no more
code than necessary. Keep in mind though that it's difficult, if not impossible, to
establish all your test cases beforehand. You also do not want to limit your
application in anyway. It's common practice to write a few unit tests beforehand just
to help with building out the skeleton and to establish the main functionalities of the
application.
220
Real Python for the Web RealPython.com
Each test is written as a separate function within a larger class. You can break
classes into several test suites. For example, one suite could test the managing of
users and sessions, while another tests user registration, and so forth. Such test
suites are meant to affirm whether the desired outcome varies from the actual
outcome.
import unittest
class TestCase(unittest.TestCase):
if __name__ == '__main__':
unittest.main()
221
Real Python for the Web RealPython.com
# flasktaskr_test.py
import os
import unittest
TEST_DB = 'test.db'
class AddUser(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
222
Real Python for the Web RealPython.com
Output:
----------------------------------------------------------------------
Ran 1 test in 0.546s
OK
Exit code: False
1. The setUp method() was invoked which created a test database (if it doesn't
exist yet) and initialized the database schema from the main database (e.g.,
creates the tables, relationships).
2. The test_user_setup() method was called, inserting data to the "users" table.
3. Lastly, the tearDown() method was invoked which dropped all the tables in the
test database.
Try commenting out the tearDown() method and run the test script once again.
Check the database in the SQLite Browser. Is the data there?
Now, while the tearDown() method is still commented out, run the test script the
second time.
Output:
----------------------------------------------------------------------
Ran 1 test in 0.047s
FAILED (errors=1)
Exit code: True
223
Real Python for the Web RealPython.com
As you can see, we got an error this time because the name and email must be
unique (as defined in our User class in models.py).
Assert
Each test should have an assert() method to either verify an expected result or a
condition, or that an exception is raised.
224
Real Python for the Web RealPython.com
# flasktaskr_test2.py
import os
import unittest
TEST_DB = 'test.db'
class AddUser(unittest.TestCase):
225
Real Python for the Web RealPython.com
if __name__ == "__main__":
unittest.main()
In this example, we're testing the same thing: whether a new user is successfully
added to the database. We then pull all data, extract just the name, and then test
to make sure the name equals the expected result, which is "mherman".
Run this program in the current form, then change the assert statement to assert
t.name != "mherman" so you can see what an assertion error looks like:
======================================================================
FAIL: test_user_setup (__main__.AddUser)
----------------------------------------------------------------------
Traceback (most recent call last):
File "flasktaskr_test.py", line 35, in test_user_setup
assert t.name != "mherman"
AssertionError
----------------------------------------------------------------------
Ran 1 test in 0.641s
FAILED (failures=1)
Exit code: True
You can run multiple tests at once using the Python library nose.
To install:
To run you can either explicitly state the names of the test files from the command
line:
226
Real Python for the Web RealPython.com
Or you can place all test files in a single directory and then call the directory name
from the command line:
$ nosetests flasktaskr_tests
6.11) Styling
Now that we're done with creating the basic app, let's change up the styles a bit.
Thus far, we've been using the CSS file from the main Flask tutorial. Let's make
this app ours. We will be using Twitter Bootstrap to get the basics setup and then
I'll briefly show you how to edit the main CSS file to make style changes.
First off, Twitter bootstrap is a front-end framework that makes your app look pretty
good darn right out of the box.5 You can just use the generic styles; however, it's
best to make some changes so that the layout doesn't look like a cookie-cutter
template. The framework is great. You'll get the essential tools (e.g., CSS, HTML,
and Javascript) needed to build a nice-looking website at your disposal. As long as
you have a basic understanding of HTML and CSS, you can create a design
quickly.
You can either download all the associated files and place them in your project
directory:
└── static
├── css
│ ├── bootstrap-responsive.css
│ ├── bootstrap-responsive.min.css
│ ├── bootstrap.css
│ └── bootstrap.min.css
├── img
│ ├── glyphicons-halflings-white.png
│ └── glyphicons-halflings.png
├── js
│ ├── bootstrap.js
│ └── bootstrap.min.js
└── styles.css
227
Real Python for the Web RealPython.com
Or you can just link directly to the styles in your template.html file:
<link href="http://twitter.github.com/bootstrap/assets/css/
bootstrap.css" rel="stylesheet">
It's perfectly fine to use the latter method during development just to get a sense of
what your basic application will look like. It's also a means of just getting something
up quickly. Keep in mind though that you will need to download the actual files and
add them to your project structure to make any changes.
Go ahead and download the files and use this new code for your base template,
template.html:
228
Real Python for the Web RealPython.com
<!DOCTYPE html>
<html lang="en">
<head>
<title>Welcome to FlaskTaskr!!!</title>
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<meta name="description" content="">
<meta name="keywords" content="">
<meta name="author" content="">
<meta charset="utf-8">
<link href="/static/css/bootstrap.css" rel="stylesheet">
<style> body { padding-top: 65px; } </style>
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar" data-toggle="collapse"
data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/">FlaskTaskr</a>
<div class="nav-collapse">
<ul class="nav">
</ul>
<ul class="nav pull-right">
{% if not session.logged_in %}
<li><a href="/register">Signup</a></li>
{% else %}
<li><a href="/logout">Logout</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
</div>
<div class="new">
229
Real Python for the Web RealPython.com
<div class="content">
{% for message in get_flashed_messages() %}
<div class=flash>{{ message }}</div>
<br/>
{% endfor %}
{% if error %}
<p class=error><strong>Error:</strong> {{ error }}
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
<footer class="footer">
<hr>
<p>© FlaskTaskr</p>
</footer>
</body>
</html>
I won't go into too many details, but essentially we just pulled in the bootstrap
stylesheets, added a navigation bar to the top, and used the bootstrap classes to
style the app. Let's edit some of those styles now. Be sure to check out the
bootstrap documentation for more information.
Before we start, take a look at your app. See the difference. Now, let's make some
changes! We'll simplify this a bit and work with just the main bootstrap file,
bootstrap.css.
First, let's update the color of the h1 , h2 , a tags, the font-size and font-weight of
the a tag, and add the same border beneath the h1 tag as before:
230
Real Python for the Web RealPython.com
h1 {
font-size: 38.5px;
color: #8D1855;
border-bottom: 2px solid #eee;
}
h2 {
font-size: 31.5px;
color: #8D1855;
}
a {
color: #8D1855;
text-decoration: none;
font-size: 16px;
font-weight: bold;
}
Next, let's update the content class. Add the following CSS to bootstrap.css:
.new {
margin: 2em auto;
width: 60em;
border: 2px solid #d4d4d4;
padding: 0.8em;
background: #fafafa;
}
Update the flash and error message styles as well as the footer by adding the
following css to bootstrap.css:
231
Real Python for the Web RealPython.com
.flash, .error {
background: #DEEBF8;
padding: 0.5em;
}
footer {
margin: 2em auto;
width: 60em;
padding: 0.8em;
}
1. login.html:
{% extends "template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskr.</h1>
<h3>Please login to access your task list.</h3>
<form method="post" action="">
{{ form.name.label }}{{ form.name }}
{{ form.password.label }}{{ form.password }}
<p><input type="submit" value="Submit"
class="btn-primary"></p>
</form>
<p><em>Need an account? </em><a
href="/register">Signup!!</a></p>
{% endblock %}
2. register.html:
232
Real Python for the Web RealPython.com
{% extends "template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskR</h1>
<br/>
<h3>Add a new task:</h3>
<form method="post" action="{{
url_for('new_task') }}">
<table>
<tr>
<td>{{ form.name.label }}{{ form.name
}} </td>
<td>{{ form.due_date.label}}{{
form.due_date }}</td>
</tr>
<tr>
<td>{{ form.posted_date.label }}{{
form.posted_date }} </td>
<td>{{ form.priority.label }}{{
form.priority }}<td>
<tr>
</table>
<p><input type="submit" value="Submit"
class="btn-primary"></p>
</form>
<hr>
<br/>
<h2>Open tasks:</h2>
<div class="datagrid">
<table>
<thread>
<tr>
<th width="200px"><strong>Task Name</strong></th>
<th width="80px"><strong>Due Date</strong></th>
<th width="90px"><strong>Posted Date</strong></th>
<th width="50px"><strong>Priority</strong></th>
<th width="90px"><strong>Posted By</strong></th>
<th width="50px"><strong>Delete</strong></th>
<th width="150px"><strong>Complete?</strong></th>
</tr>
233
Real Python for the Web RealPython.com
</thread>
{% for o in open_tasks %}
<tr>
<td width="200px">{{ o.name }}</td>
<td width="80px">{{ o.due_date }}</td>
<td width="90px">{{ o.posted_date }}</td>
<td width="50px">{{ o.priority }}</td>
<td width="90px">{{ o.poster.name }}</td>
<td width="50px"><a href="{{
url_for('delete_entry', task_id = o.task_id)
}}">Delete</a></td>
<td width="150px"><a href="{{ url_for('complete',
task_id = o.task_id) }}">Mark as Complete</a></td>
</tr>
{% endfor %}
</table>
</div>
<br/>
<br/>
<h2>Closed tasks:</h2>
<div class="datagrid">
<table>
<thread>
<tr>
<th width="200px"><strong>Task Name</strong></th>
<th width="80px"><strong>Due Date</strong></th>
<th width="90px"><strong>Posted Date</strong></th>
<th width="50px"><strong>Priority</strong></th>
<th width="90px"><strong>Posted By</strong></th>
<th><strong>Actions</strong></th>
</tr>
</thread>
{% for c in closed_tasks %}
<tr>
<td width="200px">{{ c.name }}</td>
<td width="80px">{{ c.due_date }}</td>
<td width="90px">{{ c.posted_date }}</td>
<td width="50px">{{ c.priority }}</td>
<td width="90px">{{ c.poster.name }}</td>
234
Real Python for the Web RealPython.com
<td>
<a href="{{ url_for('delete_entry', task_id =
c.task_id) }}">Delete</a>
</td>
</tr>
{% endfor %}
</table>
</div>
{% endblock %}
235
Real Python for the Web RealPython.com
.datagrid table {
border-collapse: collapse;
text-align: left; width: 100%;
}
.datagrid {
background: #fff;
overflow: hidden;
border: 1px solid #000000;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.datagrid table td, .datagrid table th {
padding: 3px 10px;
}
.datagrid table thead th {
background:-webkit-gradient( linear, left top, left bottom,
color-stop(0.05, #000000), color-stop(1, #000000) );
background:-moz-linear-gradient( center top, #000000 5%,
#000000 100% );
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#000000',
endColorstr='#000000');
background-color:#000000;
color:#FFFFFF;
font-size: 15px;
font-weight: bold;
}
.datagrid table thead th:first-child {
border: none;
}
.datagrid table tbody td {
color: #000000;
border-left: 1px solid #E1EEF4;
font-size: 12px;
font-weight: normal;
}
.datagrid table tbody .alt td {
background: #E1EEF4;
236
Real Python for the Web RealPython.com
color: #000000;
}
.datagrid table tbody td:first-child {
border-left: none;
}
.datagrid table tbody tr:last-child td {
border-bottom: none;
}
Obviously, we can keep making little changes like this until we have something
unique.
6.12) Blueprints
Flask has a built-in feature called Blueprints that allows us to break our application
into components.6 This is nice feature especially for larger applications because it
significantly increases code maintainability and reusability. It also allows us to
apply custom templates to each components, among other cool things.
237
Real Python for the Web RealPython.com
Before beginning, please note that this is not a complicated lesson but there are a
number of layers to it so it can be confusing. That said, programming in general is
nothing more than stacking various layers of knowledge on top of one another,
which again is why it makes learning much easier to start at a low-level and build
up from there rather than skipping layers. Sound familiar?
With regard to this lesson: Take it slow. Go through it once without changing any
code. Just read and deduce what's happening. Take notes. Draw diagrams. Etc.
Let's add Blueprints to our FlaskTaskr application. We'll break it down to two
components:
1. Users
2. Tasks
◦ this will handle operations for viewing, deleting, and marking tasks as
complete
Are you ready? Let's start by identifying the existing directory structure and files for
our application:
238
Real Python for the Web RealPython.com
├── app
│ ├── __init__.py
│ ├── forms.py
│ ├── models.py
│ ├── static
│ │ ├── css
│ │ │ ├── bootstrap-responsive.css
│ │ │ ├── bootstrap-responsive.min.css
│ │ │ ├── bootstrap.css
│ │ │ └── bootstrap.min.css
│ │ ├── img
│ │ │ ├── glyphicons-halflings-white.png
│ │ │ └── glyphicons-halflings.png
│ │ ├── js
│ │ │ ├── bootstrap.js
│ │ │ └── bootstrap.min.js
│ │ └── styles.css
│ ├── templates
│ │ ├── 404.html
│ │ ├── 500.html
│ │ ├── login.html
│ │ ├── register.html
│ │ ├── tasks.html
│ │ └── template.html
│ └── views.py
├── config.py
├── db_create.py
├── db_migrate.py
├── error.log
├── flasktask.db
├── flasktaskr.db
├── flasktaskr_test.py
├── flasktaskr_test2.py
├── run.py
└── test.db
After breaking our app up using Blueprints, our application should have the
following structure:
239
Real Python for the Web RealPython.com
├── app
│ ├── __init__.py
│ ├── static
│ │ ├── css
│ │ │ ├── bootstrap-responsive.css
│ │ │ ├── bootstrap-responsive.min.css
│ │ │ ├── bootstrap.css
│ │ │ └── bootstrap.min.css
│ │ ├── img
│ │ │ ├── glyphicons-halflings-white.png
│ │ │ └── glyphicons-halflings.png
│ │ ├── js
│ │ │ ├── bootstrap.js
│ │ │ └── bootstrap.min.js
│ │ └── styles.css
│ ├── tasks
│ │ ├── __init__.py
│ │ ├── forms.py
│ │ ├── static
│ │ │ └── styles.css
│ │ ├── templates
│ │ │ └── tasks
│ │ │ ├── tasks.html
│ │ │ └── template.html
│ │ └── views.py
│ ├── templates
│ │ ├── 404.html
│ │ ├── 500.html
│ │ └── template.html
│ ├── users
│ │ ├── __init__.py
│ │ ├── forms.py
│ │ ├── static
│ │ │ └── bootstrap.css
│ │ ├── templates
│ │ │ └── users
│ │ │ ├── login.html
│ │ │ ├── register.html
│ │ │ └── template.html
240
Real Python for the Web RealPython.com
│ │ └── views.py
│ └── views.py
├── config.py
├── db_create.py
├── db_migrate.py
├── error.log
├── flasktask.db
├── flasktaskr.db
├── flasktaskr_test.py
├── flasktaskr_test2.py
├── run.py
└── test.db
Study this new structure. You'll notice that both Users and Tasks have their own
views, forms, templates, and static files. On the other hand, the models.py file
remains at the main application directory. In our case, the users and tasks table are
bounded by a database relationship so it makes more sense to define them on a
single models.py.
2. Create two empty __init__.py files, and place them in the "users" and "tasks"
directories.
By using different css files, we will see later that the tasks page uses the
old layout, while the login and registration pages uses the newer twitter
bootstrap layout.
241
Real Python for the Web RealPython.com
Users Component
1. Views
242
Real Python for the Web RealPython.com
# /app/users/views.py
@mod.route('/logout/')
def logout():
session.pop('logged_in', None)
session.pop('user_id', None)
flash('You are logged out. Bye. :(')
return redirect (url_for('.login'))
password=request.form['password']).first()
if u is None:
error = 'Invalid username or password.'
else:
session['logged_in'] = True
session['user_id'] = u.id
flash('You are logged in. Go Crazy.')
return redirect(url_for('tasks.tasks'))
return render_template("users/login.html",
form = LoginForm(request.form),
243
Real Python for the Web RealPython.com
error = error)
@mod.route('/register/', methods=['GET','POST'])
def register():
error = None
form = RegisterForm(request.form, csrf_enabled=False)
if form.validate_on_submit():
new_user = User(
form.name.data,
form.email.data,
form.password.data,
)
try:
db.session.add(new_user)
db.session.commit()
flash('Thanks for registering. Please login.')
return redirect(url_for('.login'))
except IntegrityError:
error = 'Oh no! That username and/or email
already exist. Please try again.'
else:
flash_errors(form)
return render_template('users/register.html',
form=form, error=error)
What's going on here? First, we defined our Users Blueprint with custom
templates and static folders:
We then use the mod variable as if it's the Flask application itself. You can
see that when we defined the routes for the views, e.g.
@mod.route('/logout/')
244
Real Python for the Web RealPython.com
The rest of the code is the same, except for the parameters we passed to the
url_for and render_template functions.
◦ url_for('.login') is used to construct the url for the login view of the
current blueprint, which is the users blueprint.
For the render_template , we had to specify the path relative to the current
blueprint's template folder: render_template('users/register.html')
points to this template: /app/users/templates/register.html
2. Forms
245
Real Python for the Web RealPython.com
# /app/users/forms.py
class RegisterForm(Form):
name = TextField('Username', validators=[Required(),
Length(min=6, max=25)])
email = TextField('Email', validators=[Required(),
Length(min=6, max=40)])
password = PasswordField('Password',
validators=[Required(),
Length(min=6, max=40)])
confirm = PasswordField(
'Repeat Password',
[Required(), EqualTo('password',
message='Passwords must match')])
class LoginForm(Form):
name = TextField('Username', validators=[Required()])
password = PasswordField('Password',
validators=[Required()])
3. /app/users/templates/template.html:
246
Real Python for the Web RealPython.com
<!DOCTYPE html>
<html lang="en">
<head>
<title>Welcome to FlaskTaskr!!!</title>
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<meta name="description" content="">
<meta name="keywords" content="">
<meta name="author" content="">
<meta charset="utf-8">
<link href="{{ url_for('users.static',
filename='bootstrap.css') }}" rel="stylesheet">
<style> body { padding-top: 65px; } </style>
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="btn btn-navbar"
data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/">FlaskTaskr</a>
<div class="nav-collapse">
<ul class="nav">
</ul>
<ul class="nav pull-right">
{% if not session.logged_in %}
<li><a href="{{
url_for('users.register') }}">Signup</a></li>
{% else %}
<li><a href="{{ url_for('users.logout')
}}">Logout</a></li>
{% endif %}
</ul>
</div>
</div>
247
Real Python for the Web RealPython.com
</div>
</div>
<div class="new">
<div class="content">
{% for message in get_flashed_messages() %}
<div class=flash>{{ message }}</div>
<br/>
{% endfor %}
{% if error %}
<p class=error><strong>Error:</strong> {{ error
}}
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
<footer class="footer">
<hr>
<p>© FlaskTaskr</p>
</footer>
</body>
</html>
There is also nothing special here, except that we made sure that the url_for()
function points to the correct css file.
4. /app/users/templates/login.html:
248
Real Python for the Web RealPython.com
{% extends "users/template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskr.</h1>
<h3>Please login to access your task list.</h3>
<form method="post" action="">
{{ form.name.label }}{{ form.name }}
{{ form.password.label }}{{ form.password }}
<p><input type="submit" value="Submit"
class="btn-primary"></p>
</form>
<p><em>Need an account? </em><a href="{{
url_for('users.register') }}">Signup!!</a></p>
{% endblock %}
Here, we need to make sure that the extends() function receives the correct
template path, similar to the render_template() function. And don't forget the
url_for() function.
5. /app/users/templates/register.html:
249
Real Python for the Web RealPython.com
{% extends "users/template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskr.</h1>
<h3>Please register to access the task list.</h3>
<form method="post" action="">
<table>
<tr>
<td>{{ form.name.label }}{{ form.name
}} </td>
<td>{{ form.email.label }}{{ form.email }}</td>
</tr>
<tr>
<td>{{ form.password.label }}{{ form.password
}} </td>
<td>{{ form.confirm.label }}{{ form.confirm }}</td>
</tr>
</table>
<p><input type="submit" value="Register"
class="btn-primary"></p>
</form>
<p><em>Already registered?</em> Click <a href="{{
url_for('users.login') }}">here</a> to login.</p>
{% endblock %}
Can you identify the modifications in the file, register.html? Also, for the Tasks
Components the process is the same. See if you can mimic what we did for
the User Components before looking at the scripts below.
Tasks Component
1. Views
250
Real Python for the Web RealPython.com
# /app/tasks/views.py
@mod.route('/tasks/')
@login_required
def tasks():
open_tasks =
db.session.query(FTasks).filter_by(status='1').order_by(
FTasks.due_date.asc())
closed_tasks =
db.session.query(FTasks).filter_by(status='0').order_by(
FTasks.due_date.asc())
return render_template('tasks/tasks.html',form =
AddTask(request.form),
open_tasks=open_tasks,
closed_tasks=closed_tasks)
251
Real Python for the Web RealPython.com
session['user_id']
)
db.session.add(new_task)
db.session.commit()
flash('New entry was successfully posted. Thanks.')
else:
flash_errors(form)
return redirect(url_for('.tasks'))
@mod.route('/complete/<int:task_id>/',)
@login_required
def complete(task_id):
new_id = task_id
db.session.query(FTasks).filter_by(task_id=new_id).update({"status":"0"})
db.session.commit()
flash('The task was marked as complete. Nice.')
return redirect(url_for('.tasks'))
@mod.route('/delete/<int:task_id>/',)
@login_required
def delete_entry(task_id):
new_id = task_id
db.session.query(FTasks).filter_by(task_id=new_id).delete()
db.session.commit()
flash('The task was deleted. Why not add a new one?')
return redirect(url_for('.tasks'))
2. Forms
252
Real Python for the Web RealPython.com
# /app/tasks/forms.py
class AddTask(Form):
task_id = IntegerField('Priority')
name = TextField('Task Name', validators=[Required()])
due_date = DateField('Date Due (mm/dd/yyyy)',
validators=[Required()],
format='%m/%d/%Y')
priority = SelectField('Priority',
validators=[Required()],
choices=[('1', '1'),('2',
'2'),('3', '3'),
('4', '4'),('5',
'5')])
status = IntegerField('Status')
posted_date = DateField('Posted Date (mm/dd/yyyy)',
validators=[Required()], format='%m/%d/%Y')
3. /app/tasks/templates/template.html:
253
Real Python for the Web RealPython.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to FlaskTaskr!!</title>
<link rel="stylesheet" href="{{
url_for('tasks.static', filename='styles.css') }}">
</head>
<body>
<div class="page">
{% for message in get_flashed_messages() %}
<div class="flash">{{ message }}</div>
<br/>
{% endfor %}
{% if error %}
<div class="error"><strong>Error:</strong> {{ error
}}</div>
{% endif %}
{% block content %}
{% endblock %}
</div>
</body>
</html>
4. /app/tasks/templates/tasks.html
254
Real Python for the Web RealPython.com
{% extends "tasks/template.html" %}
{% block content %}
<h1>Welcome to FlaskTaskR</h1>
<br/>
<a href="{{ url_for('users.logout') }}">Logout</a>
<div class="add-task">
<h3>Add a new task:</h3>
<form method="POST" action="{{
url_for('tasks.new_task') }}">
<p>{{ form.name.label }}: {{ form.name }}<br
/>{{ form.due_date.label }}:
{{ form.due_date }} {{ form.posted_date.label
}}: {{ form.posted_date }} {{ form.priority.label }}:
{{ form.priority }}</p>
<p><input type="submit" value="Submit"></p>
</form>
</div>
<div class="entries">
<br/>
<br/>
<h2>Open tasks:</h2>
<div class="datagrid">
<table>
<thread>
<tr>
<th width="200px"><strong>Task Name</strong></th>
<th width="75px"><strong>Due Date</strong></th>
<th width="100px"><strong>Posted Date</strong></th>
<th width="50px"><strong>Priority</strong></th>
<th width="90px"><strong>Posted By</strong></th>
<th><strong>Actions</strong></th>
</tr>
</thread>
{% for o in open_tasks %}
<tr>
<td width="200px">{{ o.name }}</td>
<td width="75px">{{ o.due_date }}</td>
<td width="100px">{{ o.posted_date }}</td>
<td width="50px">{{ o.priority }}</td>
255
Real Python for the Web RealPython.com
256
Real Python for the Web RealPython.com
{% endfor %}
</table>
</div>
{% endblock %}
Main App
1. /app/init.py:
257
Real Python for the Web RealPython.com
# __init__.py
app = Flask(__name__)
app.config.from_object('config')
db = SQLAlchemy(app)
if not app.debug:
import os
import logging
file_handler =
FileHandler(os.path.join(basedir,'error.log'))
file_handler.setFormatter(Formatter('%(asctime)s
%(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'))
app.logger.setLevel(logging.INFO)
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.info('errors')
app.register_blueprint(usersMod)
app.register_blueprint(tasksMod)
258
Real Python for the Web RealPython.com
This needs to be done for each component that we have in our application.
Fortunately, we only have two.
2. /app/views.py:
259
Real Python for the Web RealPython.com
# views.py
def flash_errors(form):
for field, errors in form.errors.items():
for error in errors:
flash(u"Error in the %s field - %s" % (
getattr(form, field).label.text,error),
'error')
def login_required(test):
@wraps(test)
def wrap(*args, **kwargs):
if 'logged_in' in session:
return test(*args, **kwargs)
else:
flash('You need to login first.')
return redirect(url_for('users.login'))
return wrap
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500
@app.errorhandler(404)
def internal_error(error):
return render_template('404.html'), 404
260
Real Python for the Web RealPython.com
This code is essentially what's left after we moved most of our views to the
Users and Tasks components. Take note of the last view:
This view redirects the homepage to the tasks view of the Tasks blueprint.
3. /app/templates/404.html:
{% extends "template.html" %}
{% block content %}
<h1>Sorry ...</h1>
<p>There's nothing here!</p>
<p><a href="{{url_for('users.login')}}">Back</a></p>
{% endblock %}
4. /app/templates/500.html:
{% extends "template.html" %}
{% block content %}
<h1>Something's wrong!</h1>
<p>Fortunately we are on the job, and you can just
return to the login page!</p>
<p><a href="{{url_for('users.login')}}">Back</a></p>
{% endblock %}
Now, fire up your server. Everything should be the same as before, except for the
layout of the tasks page. All done! Again, by breaking our application up logically
by functional area using blueprints our code is cleaner and more readable. As your
app grows, it will be much easier to develop the additional functionalities due to the
separation of concerns.
261
Real Python for the Web RealPython.com
3. Once installed, open your command-line and run the following command:
$ heroku login
6. Create a Procfile. Open up a text editor and save the following text in it:
262
Real Python for the Web RealPython.com
Then save the file in your application's root or main directory as Procfile (no
extension). The word "web" indicates to Heroku that the application will be
attached to HTTP once deployed (sent to the production server).
7. Update your run.py file. On your local machine, the application runs on port
5000 by default. On Heroku, the application must run on a random port
number specified by Heroku. We will identify this port number by reading the
environment variable 'PORT' and passing it to app.run :
# run.py
import os
$ git init
$ git add .
$ git commit -m "initial files"
$ heroku create
263
Real Python for the Web RealPython.com
$ heroku ps
$ heroku open
13. If you see errors, open the Heroku log to view all errors and output:
$ heroku logs
That's it. You'll also want to push your local repository to Github. I'll show you how
to do that in the next section.
You should now understand many of the underlying basics of creating an app in
Flask. I want to leave you with a pre-configured Flask boilerplate template that has
many of the bells and whistles we looked at already installed (batteries included) so
that you can get started creating an app right away. I'll also detail a workflow for
you to use throughout the development process to help you stay organized and
ensure that your Flask instance will scale right along with you. In essence, I'll show
you how to get an app up and running on Heroku as fast as possible, and then
describe a simple workflow for you to adhere to as you build your app.
$ mkdir flask
$ cd flask
Unix:
264
Real Python for the Web RealPython.com
Windows:
This downloads the files needed for creating this generic application. Make sure
you are in the "flask" directory before cloning.
Project structure:
265
Real Python for the Web RealPython.com
├── Procfile
├── Procfile.dev
├── README.md
├── app.py
├── config.py
├── error.log
├── forms.py
├── models.py
├── requirements.txt
├── static
│ ├── css
│ │ ├── boostrap-responsive.css
│ │ └── bootstrap.css
│ ├── img
│ │ ├── glyphicons-halflings-white.png
│ │ └── glyphicons-halflings.png
│ └── js
│ ├── libs
│ │ ├── bootstrap.min.js
│ │ ├── jquery.min.js
│ │ └── modernizr-2.0.6.min.js
│ ├── plugins.js
│ └── script.js
└── templates
├── 404.html
├── 500.html
├── index.html
├── login.html
├── register.html
└── template.html
You can also view the dependencies by running the command pip freeze :
266
Real Python for the Web RealPython.com
Flask==0.10.1
Flask-SQLAlchemy==1.0
Flask-WTF==0.8.4
Jinja2==2.7
MarkupSafe==0.18
SQLAlchemy==0.8.2
WTForms==1.0.4
Werkzeug==0.9.3
itsdangerous==0.22
wsgiref==0.1.2
Deploy to Heroku
Now that you have your skeleton app up, it's time to start developing locally. Here's
a great workflow to follow. Before we start, make sure to signup for a Github
account.
Github is a web-based version control system and web hosting service. We will be
using Github for version control, which is used to track changes in code, store a
backup of your project's files, and allow for project collaboration.
It's important to PUSH to Github often during development in case you need
to revert back to earlier versions of a code base, because of some change
you made that caused the application to break. If you need more help, please
refer to my repository for a basic workflow, designed for a single user.
Once you have a Github account, create a new repository. If you need help, follow
along Github's documentation.
267
Real Python for the Web RealPython.com
$ git init
$ git add .
$ git commit -m 'first commit'
$ git remote add origin https://github.com/YOUR_GITHUB_USERNAME/
MY_APP.git
$ git push -u origin master
Now that everything is setup, let's look at the workflow you can follow as you create
and modify your application:
That's it. You now have a skeleton app to work with to build your own applications.
Cheers!
If you prefer PythonAnywhere over Heroku, simply deploy your app their
during the setup phase and then change step #4 in your workflow to deploy to
PythonAnywhere instead of Heroku. Simple. You can also look at the
documentation here.
And with that, we are down with Flask. Let's move on to a a high-level web
framework: web2py.
↩
1. http://pythonhosted.org/Flask-SQLAlchemy/↩
↩
2. http://pythonhosted.org/Flask-WTF/↩
↩
3. http://docs.python.org/2/howto/logging.html↩
268
Real Python for the Web RealPython.com
↩
4. http://docs.python.org/2/library/unittest.html↩
↩
5. http://twitter.github.com/bootstrap/index.html↩
↩
6. http://flask.pocoo.org/docs/blueprints/↩
↩
7. https://devcenter.heroku.com/articles/python↩
269
Real Python for the Web RealPython.com
Overview
As previously mentioned, web frameworks alleviate the overhead incurred from
common, repetitive tasks associated with web development. By using a web
framework, web developers delegate responsibility of low-level tasks to the
framework itself, allowing the developer to focus on the application logic.
Take note of the concept "Don’t Repeat Yourself" (or DRY). Always avoid
reinventing the wheel. This is exactly what web frameworks are great at. The
majority of the above low-level tasks are part of every web application. Since
frameworks automate much of these tasks, you can get up and running
quickly, so you can focus your development time on what really matters:
making your application stand out from the crowd.
Most frameworks also include a development web server, which is a great tool
used not only for rapid development but testing as well.
270
Real Python for the Web RealPython.com
offer some automation, however, to help speed up web development. In the end,
it's up to the developer to decide how much control he or she wants. Beginning
developers should first focus on demystifying much of the magic, (what developers
commonly refer to automation as), to help understand the differences between the
various web frameworks and avoid later confusion.
Popular Frameworks
1. web2py, Django, and Turbogears are all full frameworks, which offer a
number of pre-installed utilities and automate many tasks in the backend.
They all have excellent documentation and community support. The high-level
of automation, though, can make the learning curve for these quite steep.
3. Both Pyramid and Flask, which are still considered micro-frameworks, have
quite a few pre-installed batteries and a number of additional pre-configured
batteries available as well, which are very easy to install. Again,
documentation and community support are excellent, and both are very easy
to use.
Components
Before starting development with a new framework, learn what pre-installed and
available batteries it offers. At the core, most of the components work in a similar
manner; however, there are subtle differences. Take Flask and web2py, for
example; Flask uses an ORM for database communication and a type of template
engine called Jinja2. web2py, on the other hand, uses a DAL for communicating
with a database and has its own brand of templates.
271
Real Python for the Web RealPython.com
Don't just jump right in, in other words, thinking that once you learn to develop in
one, you can use the same techniques to develop in another. Take the time to
learn the differences between frameworks to avoid later confusion.
Don't let other developers dictate what you do or try. Find out for yourself!
272
Real Python for the Web RealPython.com
7) web2py: QuickStart
7.1) Overview
web2py is a high-level, open source web framework designed for rapid
development. With web2py, you can accomplish everything from installation, to
project setup, to actual development, quickly and easily. In no time flat, you’ll be up
and running and ready to build beautiful, dynamic websites.
273
Real Python for the Web RealPython.com
Homework
• Watch this excellent speech by Di Pierro from PyCon US 2012. Don't worry if
all the concepts don't make sense right now. They will soon enough. Pause
the video at times and look up any concepts that you don't understand. Take
notes. Google-it-first.
7.2) Installation
Quick Install
If you want to get started quickly, you can download the binary archive, unzip, and
run either web2py.exe (Windows) or web2py.app (Unix). You must set an
administrator password to access the administrative interface. The Python
Interpreter is included in the archive, as well as many third party libraries and
packages. You will then have a development environment set up, and be ready to
start building your application - all in less than a minute, and without even having to
touch the terminal.2 This is the quick and dirty method of installation, however. If
you intend on using a virtualenv, which is the common practice, you will need to
download the source code instead.
Full Install
1. Navigate to the "realpython" directory from your terminal. Then run the
following command to create the virtualenv:
Unix
$ source bin/activate
274
Real Python for the Web RealPython.com
Windows
$ scripts\activate
3. Create a directory called "start". Download the source code from the web2py
website and place it into this new directory. Unzip the file.
$ python web2py.py
5. After web2py loads, set an admin password, and you're good to go. Once
you've finished your current session with web2py, exit the virtualenv:
$ deactivate
Regardless of how you install web2py (Quick vs Full), a number of libraries are pre-
imported. These provide a functional base to work with so you can start
programming dynamic websites as soon as web2py is installed.
$ cd desktop/realpython/web2py
$ source bin/activate
$ cd start
$ python web2py.py
275
Real Python for the Web RealPython.com
2. Input your admin password. Once logged in, click the button for the
"Administrative Interface" on the right side of the page. Enter your password
again. You're now on the Sites page. This is the main administration page
where you create and modify your applications.
3. To create a new application, type the name of the app, "hello_world", in the
text field below "New simple application". Click create to be taken to the Edit
page. All new applications are just copies of the "welcome" app:
You'll soon find out that web2py has a default for pretty much everything (but
which can be modified). In this case, if you don't make any changes to the
276
Real Python for the Web RealPython.com
views, for example, your app will have the basic styles and layout taken from
the default, "welcome" app.
Think about some of the pros and cons to having a a default for
everything. You obviously can get an application set up quickly. Perhaps
if you aren't adept at a particular part of development, you could just rely
on the defaults. However, if you do go that route you probably won't learn
anything new - and you could have a difficult transition to another
framework that doesn't rely on defaults. Thus, I urge you to practice.
Break things. Learn the default behaviors. Make them better. Make them
your own.
We'll go over the MVC architecture with regard to web2py in more detail in
later chapters. For now, just know that it's a means of splitting the back-end
business logic from the front-end views, and it's used to simplify the
development process by allowing you to develop your application in phases or
chunks.
5. Next we need to modify the default controller, default.py, so click the "edit"
button next to the name of the file.
6. Now we're in the web2py IDE (the Shell). Replace the index() function with
the following code:
def index():
return dict(message="Hello from web2py!")
7. Save the file, then hit the Back button to return to the Edit page.
277
Real Python for the Web RealPython.com
8. Now let's update the view. Edit the default/index.html file, replacing the
existing code with:
<html>
<head>
<title>Hello App!</title>
</head>
<body>
<br/>
<h1>{{=message}}</h1>
</body>
</html>
9. Save the file, return to the Edit page again. Click the "index" link next to the
default.py file which loads the main page. You should see the greeting, "Hello
from web2py!" staring back at you.
The controller returned a dictionary with the key/value pair message="Hello from
web2py" . Then, in the view, we defined how we wanted the greeting to be
displayed by the browser. In other words, the functions in the controller return
dictionaries, which are then converted into views surrounded by {{ }} tags. The
values from the dictionary are used as variables. In the beginning, you won't need
to worry about the views since web2py has so many pre-made views already built
in (again: defaults). So, you can focus solely on back-end development.
When a dictionary is returned, web2py looks to associate the dictionary with a view
that matches the following format: [controller_name]/[function_name].[extension]. If
no extension is specified, it defaults to .html, and if web2py cannot find the view, it
defaults to using the generic.html view:
278
Real Python for the Web RealPython.com
• Controller = run.py
• Function = hello()
• Extension .html
In the hello_world app, since we used the default.py controller with the index()
function, web2py looked to associate this view with default/index.html.
You can also easily render the view in different formats, like JSON or XML, by
simply updating the extension:
• Go to http://localhost:8000/hello_world/default/index.html
• Change the extension to render the data in a different format:
◦ XML: http://localhost:8000/hello_world/default/index.xml
◦ JSON: http://localhost:8000/hello_world/default/index.json
◦ RSS: http://localhost:8000/hello_world/default/index.rss
Try this out. When done, hit CRTL-C from your terminal to stop the server.
279
Real Python for the Web RealPython.com
Simply invite a friend or colleague to join your session, and you have the perfect
collaborative environment for pair programming projects - or for getting help with a
certain script you can't get working. It has a lot of other useful features as well,
such as Drop-box integration and Python Shell access, among others.
1. Go ahead and create an account and log in. Once logged in, click "Web" =>
"Add a new web app", choose your_username.pythonanywhere.com, and
click the button for web2py. Set an admin password and then click "Next" one
last time to set up the web2py project.
280
Real Python for the Web RealPython.com
the right side of the page, give your app a name ("hello_World"), and finally
upload the w2p-file you saved to your computer earlier. Click install.
Homework
We'll be developing this locally. However, feel free to try developing it directly
on PythonAnywhere. Better yet: Do both.
In this example, the controller will define two functions. The first function, index(),
will return a form to index.html, which is then displayed for users to enter the
number of seconds they want converted over to minutes. Meanwhile, the second
function, convert(), takes the number of seconds and converts them to the number
of minutes. Both the variables are then passed to the convert.html view.
281
Real Python for the Web RealPython.com
def index():
form=FORM('# of seconds: ',
INPUT(_name='seconds', requires=IS_NOT_EMPTY()),
INPUT(_type='submit')).process()
if form.accepted:
redirect(URL('convert',args=form.vars.seconds))
return dict(form=form)
def convert():
seconds = request.args(0,cast=int)
return dict(seconds=seconds, minutes=seconds/60,
new_seconds=seconds%60)
<center>
<h1>seconds2minutes</h1>
<h3>Please enter the number of seconds you would like
converted to minutes.</h3>
<p>{{=form}}</p>
</center>
3. Create a new view called default/convert.html, replacing the default code with:
<center>
<h1>seconds2minutes</h1>
<p>{{=seconds}} seconds is {{=minutes}} minutes and
{{=new_seconds}} seconds.</p>
<br/>
<p><a href="/seconds2minutes/default/index">Try
again?</a><p>
</center>
282
Real Python for the Web RealPython.com
validation we have is that the form doesn’t show up blank. Let's alter the code to
add additional validators.
def index():
form=FORM('# of seconds: ',
INPUT(_type='integer', _name='seconds',
requires=IS_INT_IN_RANGE(0,1000000)),
INPUT(_type='submit')).process()
if form.accepted:
redirect(URL('convert',args=form.vars.seconds))
return dict(form=form)
Test it out. You should get an error, unless you enter an integer between 0 and
999,999:
Again, the convert() function takes the seconds and then runs the basic
expressions to convert the seconds to minutes. These variables are then passed to
the dictionary and are used in the convert.html view.
Homework
283
Real Python for the Web RealPython.com
Twitter is a great resource for sourcing data for sentiment analysis. You could use
the Twitter API to pull hundreds of thousands of tweets on topics such as Obama,
abortion, gun control, etc. to get sense of how the nation feels about a particular
topic. Companies use sentiment analysis to gain a deeper understanding about
marketing campaigns, certain product lines, and the company itself.
You'll want to pick a topic that people have strong opinions about.
1. Start by reading the API documentation for the natural language classifier
we'll be using for our app. Are the docs clear? What questions do you have?
Write them down. If you can't answer them by the end of this lesson, try the
"Google-it-first" method, then, if you still have questions, post them to the Real
Python message forum.
2. First, what the heck is cURL? For simplicity, cURL is a utility used for
transferring data across numerous protocols.4 We will be using it to test HTTP
requests.
Traditionally, you would access cURL from the terminal in Unix systems.
Unfortunately for Windows users, command prompt does not come with
284
Real Python for the Web RealPython.com
Unix users, and Windows users with Cygwin installed, test out the API in the
terminal:
So, you can see the natural language, the probability of the sentiment being
positive, negative, or neutral, and then the final sentiment. Did you notice the
last two text statements are more neutral than negative but were classified as
negative? Why do you think that is? How can a computer analyze sarcasm?
285
Real Python for the Web RealPython.com
Steps:
All right. Let's build the app. Before we begin, though, we will be using the requests
library for initiating the POST request. The cURL command is equivalent to the
following code:
import requests
url = 'http://text-processing.com/api/sentiment/'
data = {'text': 'great'}
r = requests.post(url, data=data)
print r.content
286
Real Python for the Web RealPython.com
Now, let's build the app for easily testing sentiment analysis.
Pulse App
1. You know the drill: Activate virtualenv, fire up the server, enter the Admin
Interface, and create a new app called "pulse".
2. Like the last app, the controller will define two functions, index() and pulser().
index(), will return a form to index.html, so users can enter the text for
analysis. pulser(), meanwhile, handles the POST request to the API and
outputs the results of the analysis to pulser.html.
287
Real Python for the Web RealPython.com
import requests
def index():
form=FORM(
TEXTAREA(_name='pulse', requires=IS_NOT_EMPTY()),
INPUT(_type='submit')).process()
if form.accepted:
redirect(URL('pulser',args=form.vars.pulse))
return dict(form=form)
def pulser():
text = request.args(0)
text = text.split('_')
text = ' '.join(text)
url = 'http://text-processing.com/api/sentiment/'
data = {'text': text}
r = requests.post(url, data=data)
return dict(text=text, r=r.content)
default/index.html:
{{extend 'layout.html'}}
<center>
<br/>
<br/>
<h1>check a pulse</h1>
<h4>Just another Sentiment Analysis tool.</h4>
<br/>
<p>{{=form}}</p>
</center>
default/pulser.html:
288
Real Python for the Web RealPython.com
{{extend 'layout.html'}}
<center>
<p>{{=text}}</p>
<br/>
<p>{{=r}}</p>
<br/>
<p><a href="/pulse/default/index">Another Pulse?</a><p>
</center>
5. All right. Test this out. Compare the results to the results using either cURL or
Hurl to make sure all is set up correctly.
Update default.py:
289
Real Python for the Web RealPython.com
import requests
import json
def index():
form=FORM(
TEXTAREA(_name='pulse', requires=IS_NOT_EMPTY()),
INPUT(_type='submit')).process()
if form.accepted:
redirect(URL('pulser',args=form.vars.pulse))
return dict(form=form)
def pulser():
text = request.args(0)
text = text.split('_')
text = ' '.join(text)
url = 'http://text-processing.com/api/sentiment/'
data = {'text': text}
r = requests.post(url, data=data)
binary = r.content
output = json.loads(binary)
label = output["label"]
Update default/pulser.html:
290
Real Python for the Web RealPython.com
{{extend 'layout.html'}}
<center>
<br/>
<br/>
<h1>your pulse</h1>
<h4>{{=text}}</h4>
<p>is</p>
<h4>{{=label}}</h4>
<br/>
<p><a href="/pulse/default/index">Another Pulse?</a><p>
</center>
7. Make it pretty.
Update layout.html
<!DOCTYPE html>
<html>
<head>
<title>check your pulse</title>
<meta charset="utf-8" />
<style type="text/css">
body {font-family: Arial, Helvetica, sans-serif;
font-size:x-large;}
</style>
{{
middle_columns = {0:'span12',1:'span9',2:'span6'}
}}
{{block head}}{{end}}
</head>
<body>
<div class="{{=middle_columns}}">
{{block center}}
{{include}}
{{end}}
</div>
</body>
</html>
291
Real Python for the Web RealPython.com
292
Real Python for the Web RealPython.com
What else could you do with this? Well, you could easily tie a database into the
application to save the inputed text as well as the results. With sentiment analysis,
you want your algorithm to get smarter (machine learning). Right now, the
algorithm is static. Try entering the term "i like milk". It's negative, right?
Why is that?
"i":
293
Real Python for the Web RealPython.com
"like":
"milk":
Once again, activate your virtualenv, start the server, set a password, and enter the
Admin Interface. Finally create a new app called "web2blog". (Please Feel free to
come up with something a bit more creative.)
294
Real Python for the Web RealPython.com
Model
The database has one table, blog_posts, with two fields - title and post. Remember
how we used an ORM to interact with the database with the Flask framework?
Well, web2py uses a similar system called a Database Abstraction Layer (DAL).5
To keep things simple, an ORM is a subset of a DAL. Both are used to map
database functions to Python objects.
db.define_table('blog_posts',
Field('title', notnull=True),
Field('post', 'text', notnull=True))
We'll go over the main differences between an ORM and a DAL in the next
chapter. For now, go ahead and save the file and return to the Edit page. As long
as there are no errors, web2py created an admin interface used to manage the
database, located directly below the "Models" header. Click the button to access
the admin interface. From here you can add data to the tables within the database.
Go ahead and add a few rows of dummy data, then click on the actual table
(db.posts) to view the records you just added.
As soon as the admin interface is accessed, db.py is executed and the tables are
created. Return to the Edit page. A new button should have populated next to the
admin interface button called "sql.log". Click the link to view the actual SQL
statements used to create the table. Scroll to the bottom. You should see the
following:
timestamp: 2013-03-04T19:51:44.464307
CREATE TABLE blog_posts(
id INTEGER PRIMARY KEY AUTOINCREMENT,
title CHAR(512) NOT NULL,
post TEXT NOT NULL
);
success!
295
Real Python for the Web RealPython.com
But what are all those other tables? We'll get to that. One step at a time.
Controller
def index():
form=SQLFORM(db.blog_posts)
if form.process().accepted:
response.flash = "Post accepted - cheers!"
elif form.errors:
response.flash = "Post not accepted - fix the error(s)."
else:
response.flash = "Please fill out the form - thank you!"
posts = db().select(db.blog_posts.ALL)
return dict(posts=posts, form=form)
This adds an HTML form so that users can add posts. It also queries the database,
pulling all rows of data and returning the results as a dictionary. Again, the values
of the dictionary are turned into variables.
View
{{extend 'layout.html'}}
<h1>Add a Post</h1>
{{=form}}
<h1>Current Posts</h1>
<br/>
<table>
<tr><td><h3>Title</h3></td><td><h3>Post</h3></td></tr>
{{for p in posts:}}
<tr><td>{{=(A(p.title))}}</td><td>{{=(A(p.post))}}</td></tr>
{{pass}}
</table>
296
Real Python for the Web RealPython.com
Wrap Up
1. User login/registration
2. Login_required Decorator to protect index.html
3. Session Management
Here's where the real power of web2py comes into play. All of those are auto-
implemented in web2py. That's right. Are you starting to like defaults? Remember
how long this took to implement into the Flask app?
Simply add the login_required decorator to the index() function in the controller,
default.py:
@auth.requires_login()
def index():
form=SQLFORM(db.blog_posts)
if form.process().accepted:
response.flash = "Post accepted - cheers!"
elif form.errors:
response.flash = "Post not accepted - fix the error(s)."
else:
response.flash = "Please fill out the form - thank you!"
posts = db().select(db.blog_posts.ALL)
return dict(posts=posts, form=form)
Try to access the site again. You should now have to register and login. That's all
there is to it! Session Management is already set up as well. I'll explain how this
works in the next chapter.
↩
1. http://www.cdm.depaul.edu/People/Pages/facultyinfo.aspx?fid=343↩
297
Real Python for the Web RealPython.com
↩
2. http://web2py.com/books/default/chapter/29/03#Startup↩
↩
3. https://www.pythonanywhere.com/details/develop_anywhere↩
↩
4. http://en.wikipedia.org/wiki/CURL↩
↩
5. http://web2py.com/books/default/chapter/29/06↩
298
Real Python for the Web RealPython.com
8) web2py: py2manager
8.1) Introduction
In the last chapter we built several small applications to illustrate the power of
web2py. Those applications were meant more for learning. In this chapter we will
develop a much larger application: a task manager, similar to FlaskTaskr, called
py2manager.
This application will be developed from the ground up to not only show you all that
web2py has to offer - but to also dig deeper into modern web development and the
Model View Controller development style.
1. Users must sign in (and register, if necessary) before hitting the landing page,
index.html.
2. Once signed in, users can add new companies, projects, and notes
associated with projects and view other employees' profiles.
3. Each company consists of a company name, email, phone number, and URL.
4. Each project consists of a name, employee name (person who logged the
project), description, start date, due date, and completed field that indicates
whether the project has been completed.
5. Finally, notes reference a project and include a text field for the actual note,
created date, and a created by field.
Up to this point, you have developed a number of different applications using the
Model View Controller (MVC) architecture pattern:
299
Real Python for the Web RealPython.com
Again, a user sends a request to a web server. The server, in turn, passes that
request to the controller. Using the established workflow, the controller then
performs an action, such as querying or modifying the database (model). Once the
data is found or updated, the controller then passes the results back to the views,
which is seen by the user (response).
8.2) Setup
Before we begin, let's setup a virtualenv and a new app:
3. CD into the new directory, and then activate the virtual environment:
Unix:
source bin/activate
Windows:
scripts\activate
300
Real Python for the Web RealPython.com
4. Download the source code from the web2py website and place it into the
"py2manager" directory. Unzip the file, placing all files and folders into the
"py2manager" directory.
Sublime Text
Before moving on, we're going to start using a more advanced text editor called
Sublime Text to help keep our project organized and speed up development. The
internal web2py IDE is great for small applications, but you'll want to use either an
advanced text editor or an IDE for larger applications.
You can download Sublime Text 2 here. Once installed, you need to load your
development project:
You should now have the entire application structure (files and folders) in Sublime.
Take a look around. Open the "Models", "Views", and "Controllers" folders. Simply
double-click to open a particular file to load it in the editor window. Files appear as
tabs on the top of the editor window, allowing you to move between files quickly.
301
Real Python for the Web RealPython.com
Such systems allow you to easily track changes when code is updated, revert (or
rollback) to earlier versions of your codebase in case of an error (like inadvertently
deleting a file or a large chunk of code, for example), and collaborate on the
project. Take the time to learn how to use a version control system. This could
save you much time in the future - when an error occurs, for example, and you
need to rollback your code - and is a required skill for web developers to have.
2. If you've never installed Git before you need to set your global first name, last
name, and email address. Open the terminal in Unix or the Git Bash Shell in
302
Real Python for the Web RealPython.com
Windows (Start > All Programs > Git > Git Bash), then enter the following
commands:
3. Sign up for Github to host your Git repository (which is just a secure location
to store your files), and setup a new repository.
4. Back in your terminal, navigate to your project directory, then run the following
commands:
git init
touch README
git add *
git commit -m "My initial commit message"
git remote add origin [email protected]:Your-Username/
awesomeProject.git
git push origin master
5. This creates the necessary files and pushes them to the remote repository on
Github.
git add *
git commit -m 'Text goes here'
git push origin master
The string text goes here should be replaced each time with a brief
description of the changes made since the last PUSH.
303
Real Python for the Web RealPython.com
If you are collaborating on a project, you'll want to grab the latest master
version before making changes to the codebase to the local copy on your
machine.
That's it. With regard to Git, it's essential that you know how to:
If you want to take it a step further, it's good to learn how to make branches, which
are just copies of the master repository where you can make changes that do not
affect that master branch, and then merging that branch with the main branch. You
can think of them as a sandbox where you can make any change you want without
affecting the main codebase. Then if you do decide to keep those changes, you
can merge them with the master branch.
8.4) Database
As you saw in the previous chapter, web2py uses an API called a Database
Abstraction Layer (DAL) to map Python objects to database objects. Like an ORM,
a DAL hides the complexity of the underlying SQL code. The major difference
between an ORM and a DAL, is that a DAL operates at a lower level.1 In other
words, its syntax is somewhat closer to SQL. If you have experience with SQL, you
may find DAL easier to work with than an ORM. If not, learning the syntax is no
more difficult than an ORM:
ORM:
class User(db.Model):
__tablename__ = 'users'
name = db.Column(db.String, unique=True, nullable=False)
email = db.Column(db.String, unique=True, nullable=False)
password = db.Column(db.String, nullable=False)
304
Real Python for the Web RealPython.com
DAL:
db.define_table('users',
Field('name', 'string', unique=True, notnull=True),
Field('email', 'string', unique=True, notnull=True),
Field('password', 'string', 'password', readable=False,
label='Password'))
The above examples create the exact same "users" table. ORMs generally use
classes to declare tables, while the web2py DAL uses functions. Both are portable
among many different relational database engines (database agnostic). Meaning
you can switch your database engine without having to re-write the code within
your Model. web2py is integrated with a number of popular databases, including
SQLite, PostgreSQL, MySQL, SQL Server, FireBird, Oracle, MongoDB, among
others.
Shell
If you prefer the command line, you can work directly from the web2py Shell. The
following is a quick, unrelated example:
305
Real Python for the Web RealPython.com
>>> db =
DAL('sqlite://storage.sqlite',pool_size=1,check_reserved=['all'])
>>> db.special_users.bulk_insert([{'name':'Alan',
'email':'[email protected]'},
{'name':'John', 'email':'[email protected]'}, {'name':'Tim',
'email':'[email protected]'}])
[2L, 3L, 4L]
>>> db.commit()
>>> db.special_users.drop()
>>> exit()
306
Real Python for the Web RealPython.com
Here we created a new table called "special_users" with the fields "name" and
"email". We then inserted a single row of data then multiple rows. Finally, we
printed the data to the screen before dropping the table and exiting the Shell.
web2py Admin
Now, as I mentioned in the last chapter, everything has a default. These are the
default values for each table field:
307
Real Python for the Web RealPython.com
1. Create a new file to define your database schema, in Sublime within Models,
called db_tasks.py. Then add the following code:
db.define_table('company',
Field('company_name', notnull=True, unique=True),
Field('email'),
Field('phone', notnull=True),
Field('url'),
format = '%(company_name)s')
db.company.email.requires=IS_EMAIL()
db.company.url.requires=IS_EMPTY_OR(IS_URL())
db.define_table('project',
Field('name', notnull=True),
Field('employee_name', db.auth_user,
default=auth.user_id),
Field('company_name', 'reference company',
notnull=True),
Field('description', 'text', notnull=True),
Field('start_date', 'date', notnull=True),
Field('due_date', 'date', notnull=True),
Field('completed', 'boolean', notnull=True),
format = '%(company_name)s')
db.project.employee_name.readable =
db.project.employee_name.writable = False
We defined a two tables tables: "company" and "project". You can see the foreign
key in the project "table", reference client . The "auth_user" table is an auto-
generated table, among others. Also, the "employee_name" field in the "project"
308
Real Python for the Web RealPython.com
table references the logged in user. So when a user posts a new project, their user
information will automatically be added to the database. Save the file.
Navigate to the Edit page and click the "database administration" button to execute
the DAL commands.
Take a look at the sql.log file within the databases directory in Sublime to verify
exactly which tables and fields were created. You can also read the documentation
on all the auto-generated tables in the web2py official documentation.
Notice the format attribute. All references are linked to the Primary Key of the
associated table, which is the auto-generated ID (check the sql.log). By using the
format attribute references will not show up by the id - but by the preferred field.
One less thing you have to worry about. For more information, please check
out the web2py documentation.
309
Real Python for the Web RealPython.com
git add *
git commit -m 'updated database'
git push origin master
Homework
• Download the web2py cheatsheet. Paste it on your wall. Frame it if you want.
Read it too.
URL Routing
web2py provides a simple means of matching URLs with views.3 In other words,
when the controller provides you with the appropriate view, there is an URL
associated with that view, which can be customized.
def index():
return dict(message="Hello!")
310
Real Python for the Web RealPython.com
This is just a simple function used to output the string "Hello!" to the screen. You
can't tell from the above info, but the application name is "hello" and the controller
used for this function is default.py. The function name is "index".
http://www.yoursite.com/hello/default/index.html
http://www.yousite.com/application_name/controller_name/
function_name.html
You can customize the URL routing methods in the routes.example.py file, which is
found in the "web2py" root directory ("realpython/web2py/py2manager"). Just
rename it to routes.py. For example, if you wanted to remove the controller_name
from the url, add the following code to the routes.py file:
routers = dict(
BASE = dict(default_application='py2manager'),
)
Test this out. Restart the server. Navigate to the login page again:
http://localhost:8000/py2manager/user/login
For more information on URL routing, please see the official web2py
documentation.
Let's setup the logic and URL routing in the py2manager app. Add the following
code to default.py:
311
Real Python for the Web RealPython.com
@auth.requires_login()
def index():
project_form = SQLFORM(db.project).process()
projects = db(db.project).select()
users = db(db.auth_user).select()
companies = db(db.company).select()
return locals()
Here we displayed the data found in the "project", "auth_user", and "company"
tables, as well as added a form for adding projects.
Most of the functionality is now in place for a basic application. We just need to
update the views, organize the index.html page, and update the layout and styles.
Before moving on though, PUSH the changes to Github.
Homework
• Template Engine: Template engines are used for embedding Python into
standard HTML. web2py uses a slightly modified Python syntax to make the
code more readable. You can also define control statements such as for and
while loops and if statements.
For example:
312
Real Python for the Web RealPython.com
<html>
<body>
{{numbers = [1, 2, 3]}}
<ul>
{{for n in numbers:}}<li>{{=n}}</li>{{pass}}
</ul>
</body>
</html>
def tester():
return locals()
{{extend 'default.html'}}
<h1>This is the index.html page</h1>
313
Real Python for the Web RealPython.com
<html>
<head>
{{extend 'header.html'}}
</head>
<body>
{{include}}
</body>
{{extend 'footer.html'}}
</html>
1. index.html:
{{extend 'layout.html'}}
<h2>Welcome to py2manager</h2>
<br/>
{{=(project_form)}}
<br/>
<h3> All Open Projects </h3>
<ul>{{for project in projects:}}
<li>
{{=(project.name)}}
</li>
{{pass}}
</ul>
This file has a form at the top to add new projects to the database. It also lists
out all open projects using a for loop. You can view the results here:
http://localhost:8000/py2manager/index.
2. user.html:
314
Real Python for the Web RealPython.com
Open up this file. This view was created automatically to make the
development process easier and quicker. If you go back to the default.py file,
you can see a description of the main functionalities of the user function:
"""
exposes:
http://..../[app]/default/user/login
http://..../[app]/default/user/logout
http://..../[app]/default/user/register
http://..../[app]/default/user/profile
http://..../[app]/default/user/retrieve_password
http://..../[app]/default/user/change_password
use @auth.requires_login()
@auth.requires_membership('group name')
@auth.requires_permission('read','table
name',record_id)
to decorate functions that need access control
"""
3. Layout:
Let's edit the main layout to replace the generic template. Start with models/
menu.py. Update the following code:
response.logo =
A(B('py',SPAN(2),'manager'),XML('™ '),
_class="brand")
response.title = "py2manager"
response.subtitle = T('just another project manager')
315
Real Python for the Web RealPython.com
DEVELOPMENT_MENU = False
git add *
git commit -m 'updated the templates and views'
git push origin master
Now that we've gone over the Model View Controller architecture in detail, let's now
focus on the main functions of the application.
It's easy to add fields to any of the Auth tables. Open up db.py in Sublime and
place the following code after auth = Auth(db) and before
auth.define_tables) :
auth.settings.extra_fields['auth_user']= [
Field('address'),
Field('city'),
Field('zip'),
Field('image', 'upload')]
316
Real Python for the Web RealPython.com
ahead and update the new fields and upload an image. Then click Save Profile.
Nice, right?
317
Real Python for the Web RealPython.com
@auth.requires_login()
def add():
project_form = SQLFORM(db.project).process()
return dict(project_form=project_form)
@auth.requires_login()
def index():
projects = db(db.project).select()
users = db(db.auth_user).select()
companies = db(db.company).select()
return locals()
{{extend 'layout.html'}}
<h2>Add a new project:</h2>
<br/>
{{=project_form.custom.begin}}
<strong>Project
name</strong><br/>{{=project_form.custom.widget.name}}<br/>
<strong>Company
name</strong><br/>{{=project_form.custom.widget.company_name}}<br/>
<strong>Description</strong><br/>{{=project_form.custom.widget.description}}<br/>
<strong>Start
Date</strong><br/>{{=project_form.custom.widget.start_date}}<br/>
<strong>Due
Date<strong><br/>{{=project_form.custom.widget.due_date}}<br/>
{{=project_form.custom.submit}}
{{=project_form.custom.end}}
Push to Github.
318
Real Python for the Web RealPython.com
Default.py:
@auth.requires_login()
def company():
company_form = SQLFORM(db.company).process()
return dict(company_form=company_form)
{{extend 'layout.html'}}
<h2>Add a new company:</h2>
<br/>
{{=company_form.custom.begin}}
<strong>Company
Name</strong><br/>{{=company_form.custom.widget.company_name}}<br/>
<strong>Email</strong><br/>{{=company_form.custom.widget.email}}<br/>
<strong>Phone</strong><br/>{{=company_form.custom.widget.phone}}<br/>
<strong>URL</strong><br/>{{=company_form.custom.widget.url}}<br/>
{{=company_form.custom.submit}}
{{=company_form.custom.end}}
Push to Github.
8.10) Homepage
Now, let's finish organizing the homepage to display all projects. We'll be using the
SQLFORM.grid to display all projects. Essentially, the SQLFORM.grid is a high-
level table that creates complex CRUD controls. It provides pagination, the ability
to browse, search, sort, create, update and delete records from a single table.
319
Real Python for the Web RealPython.com
@auth.requires_login()
def index():
response.flash = T('Welcome!')
grid = SQLFORM.grid(db.project)
return locals()
{{extend 'layout.html'}}
<h2>All projects:</h2>
<br/>
{{=grid}}
db.project.start_date.requires =
IS_DATE(format=T('%m-%d-%Y'),
error_message='Must be MM-DD-YYYY!')
db.project.due_date.requires =
IS_DATE(format=T('%m-%d-%Y'),
error_message='Must be MM-DD-YYYY!')
320
Real Python for the Web RealPython.com
5. Within the views folder, open web2py_ajax.html and make the following
changes:
Change:
To:
What does this do? Take a look at the documentation here. It's all self-
explanatory. Compare the before and after for additional help.
1. default.py:
321
Real Python for the Web RealPython.com
@auth.requires_login()
def company():
company_form = SQLFORM(db.company).process()
grid = SQLFORM.grid(db.company, create=False,
deletable=False, editable=False, maxtextlength=50,
orderby=db.company.company_name)
return locals()
2. company.html:
{{extend 'layout.html'}}
<h2>Add a new company:</h2>
<br/>
{{=company_form.custom.begin}}
<strong>Company
Name</strong><br/>{{=company_form.custom.widget.company_name}}<br/>
<strong>Email</strong><br/>{{=company_form.custom.widget.email}}<br/>
<strong>Phone</strong><br/>{{=company_form.custom.widget.phone}}<br/>
{{=company_form.custom.submit}}
{{=company_form.custom.end}}
<br/>
<br/>
<h2>All companies:</h2>
<br/>
{{=grid}}
1. default.py:
322
Real Python for the Web RealPython.com
@auth.requires_login()
def employee():
employee_form = SQLFORM(db.auth_user).process()
grid = SQLFORM.grid(db.auth_user, create=False,
fields=[db.auth_user.first_name, db.auth_user.last_name,
db.auth_user.email], deletable=False, editable=False,
maxtextlength=50)
return locals()
2. employee.html:
{{extend 'layout.html'}}
<h2>All employees:</h2>
<br/>
{{=grid}}
Test both of the new views out, and then PUSH to Github.
8.12 Notes
Next, let's add the ability to add notes to each project.
323
Real Python for the Web RealPython.com
db.define_table('note',
Field('post_id', 'reference project', writable=False),
Field('post', 'text', notnull=True),
Field('created_on', 'datetime', default=request.now,
writable=False),
Field('created_by', db.auth_user,
default=auth.user_id))
2. Update the index() function and add a note() function in the controller:
@auth.requires_login()
def index():
response.flash = T('Welcome!')
notes = [lambda project:
A('Notes',_href=URL("default","note",args=[project.id]))]
grid = SQLFORM.grid(db.project, create=False,
links=notes, fields=[db.project.name,
db.project.employee_name, db.project.company_name,
db.project.start_date, db.project.due_date,
db.project.completed], deletable=False, maxtextlength=50)
return locals()
@auth.requires_login()
def note():
project = db.project(request.args(0))
db.note.post_id.default = project.id
form = crud.create(db.note) if auth.user else "Login
to Post to the Project"
allnotes = db(db.note.post_id==project.id).select()
return locals()
324
Real Python for the Web RealPython.com
3. Take a look. Add some notes. Now let's add a new view called default/
note.html:
{{extend 'layout.html'}}
<h2>Project Notes</h2>
<br/>
<h4>Current Notes</h4>
{{for n in allnotes:}}
<ul>
<li>{{=db.auth_user[n.created_by].first_name}} on
{{=n.created_on.strftime("%m/%d/%Y")}}
- {{=n.post}}</li>
</ul>
{{pass}}
<h4>Add a note</h4>
{{=form}}</br>
4. Finally, let's update the index() function to add a button for the Notes link:
@auth.requires_login()
def index():
response.flash = T('Welcome!')
notes = [lambda project: A('Notes', _class="btn",
_href=URL("default","note",args=[project.id]))]
grid = SQLFORM.grid(db.project, create=False,
links=notes, fields=[db.project.name,
db.project.employee_name, db.project.company_name,
db.project.start_date, db.project.due_date,
db.project.completed], deletable=False, maxtextlength=50)
return locals()
325
Real Python for the Web RealPython.com
We just added the btn class to the notes variable. How do we know where
this class comes from? Open up the default template, layout.html, and find the
area in the head, <head></head> where the CSS files are included:
With CSS, the last file included ( web2py_bootstrap.css ) will overwrite any
classes in the CSS files before it. So, start with the last included file to see if the
.btn class is there, and if not, work your way up through the files.
Have you seen an error yet? Remove the closing parenthesis from the statement in
the index() function: response.flash = T('Welcome!' . Now navigate to the
homepage. You should see that a ticket number was logged. When you click on
the ticket number, you get the specific details regarding the error.
326
Real Python for the Web RealPython.com
You do not want users seeing errors, so add the following code to the routes.py
file:
routes_onerror = [
('*/*', '/py2manager/static/error.html')
]
Refresh the homepage to see the new error message. Now errors are still logged,
but end users won't see them. Correct the error. PUSH the new code to GitHub.
Homework
1. http://stackoverflow.com/questions/580383/where-is-the-line-between-dal-and-
↩
orm↩
↩
2. http://web2py.com/books/default/chapter/29/06↩
↩
3. http://web2py.com/books/default/chapter/29/04#URL-rewrite↩
327
Real Python for the Web RealPython.com
9.1) Introduction
Remember the data we scraped from Socrata back in Chapter 3? Go back and
quickly review the lesson. Then, locate the project.db file on your local computer
we used to store the data. In short, we're going to build our own RESTful web
service to expose the data that we scraped. Why would we want to do this when
the data is already available?
1. The data could be in high demand but the Socrata website is unreliable. By
scraping the data and providing it via REST, you can ensure the data is
always available to you or your clients.
2. Again, the data could be in high demand but it's poorly organized on the
website. You can cleanse the data after scraping and offer it in a more human
and machine readable format.
3. You want to create a mashup. Perhaps you are scraping other websites
(legally) that also have data sources and you're creating an aggregator
service.
Whatever the reason, let's look at how to quickly setup a REST web service via
web2py by to expose the data we pulled.
Remember =>
328
Real Python for the Web RealPython.com
Let's start with a basic example before using the scraped data from Socrata. We'll
be using the web2py IDE for this chapter for simplicity's sake.
db.define_table('fam',Field('role'),Field('name'))
@request.restful()
def api():
response.view = 'generic.'+request.extension
def GET(*args,**vars):
patterns = 'auto'
parser = db.parse_as_rest(patterns,args,vars)
if parser.status == 200:
return dict(content=parser.response)
else:
raise HTTP(parser.status,parser.error)
def POST(table_name,**vars):
return db[table_name].validate_and_insert(**vars)
def PUT(table_name,record_id,**vars):
return
db(db[table_name]._id==record_id).update(**vars)
def DELETE(table_name,record_id):
return db(db[table_name]._id==record_id).delete()
return dict(GET=GET, POST=POST, PUT=PUT, DELETE=DELETE)
329
Real Python for the Web RealPython.com
These functions expose any field in our database. If you want to limit the
resources exposed, you'll need to define various patterns.
For example:
def GET(*args,**vars):
patterns = [
"/test[fam]",
"/test/{fam.name.startswith}",
"/test/{fam.name}/:field",
]
parser = db.parse_as_rest(patterns,args,vars)
if parser.status == 200:
return dict(content=parser.response)
else:
raise HTTP(parser.status,parser.error)
So:
◦ URI:
http://127.0.0.1:8000/rest/default/api/fam.json
Result:
330
Real Python for the Web RealPython.com
◦ URI:
http://127.0.0.1:8000/rest/default/api/fam/id/1.json
Results:
5. Test out the following requests in the Shell and look at the results after each
requests in the database:
>>> r = requests.delete("http://127.0.0.1:8000/rest/
default/api/fam/2.json")
>>> print r
<Response [200]>
331
Real Python for the Web RealPython.com
6. Now in most cases, you do not want just anybody having access to your
model like this. Besides, limiting the datapoints as described above, you also
want to have user authentication in place.
auth.settings.allow_basic_login = True
@auth.requires_login()
@request.restful()
def api():
response.view = 'generic.'+request.extension
def GET(*args,**vars):
patterns = 'auto'
parser = db.parse_as_rest(patterns,args,vars)
if parser.status == 200:
return dict(content=parser.response)
else:
raise HTTP(parser.status,parser.error)
def POST(table_name,**vars):
return db[table_name].validate_and_insert(**vars)
def PUT(table_name,record_id,**vars):
return
db(db[table_name]._id==record_id).update(**vars)
def DELETE(table_name,record_id):
return db(db[table_name]._id==record_id).delete()
return dict(GET=GET, POST=POST, PUT=PUT,
DELETE=DELETE)
Unauthorized:
332
Real Python for the Web RealPython.com
Homework
db.define_table('socrata',Field('name'),Field('url'),Field('views',
integer))
333
Real Python for the Web RealPython.com
4. Now we need to extract the data from the projects.db data and import into the
new database table you just created. There are a number of different ways to
handle this. 2 We'll export the data from the old database in CSV format and
then import it directly into the new web2py table.
◦ Open projects.db in your SQLite Browser. Then click File => Export =>
Table as CSV file. Save the file in the following directory as socrata.csv:
.../realpython/web2py/socrata/applications/socrata
◦ You need to rename the "text" field since it's technically a restricted
name. Also, the field names "text" and "url" are labeled under the wrong
columns respectively. Open up the CSV file in gedit and make the
following changes to the header:
◦ Also, open the file in Excel. Use the Text-to-Columns feature on the
"views" column to extract out the word "views" from each data point. The
word "views" makes it impossible to filter and sort the data. When we
scrapped the data, we should have only grabbed the view count, not the
word "views". Mistakes are how you learn, though. If you don't know how
to do this in Excel, I've included the socrata.csv file formatted correctly.
◦ To upload the CSV file, return to the Edit page on web2py, click the
button for "database administration", then click the "db.socrata" link.
Scroll to the bottom of the page and click "choose file" select socrata.csv.
Now click import.
334
Real Python for the Web RealPython.com
In the future, when you set up your Scrapy Items Pipeline, you need to
dump the data right to the web2py database. The process is the same as
outlined in Chapter 3. Also, make sure to only grab the view count, not
the word "views".
1. First, When designing your RESTful API, you should follow these best
practices: 3
335
Real Python for the Web RealPython.com
@request.restful()
def api():
response.view = 'generic.'+request.extension
def GET(*args,**vars):
patterns = 'auto'
parser = db.parse_as_rest(patterns,args,vars)
if parser.status == 200:
return dict(content=parser.response)
else:
raise HTTP(parser.status,parser.error)
def POST(table_name,**vars):
return db[table_name].validate_and_insert(**vars)
def PUT(table_name,record_id,**vars):
return
db(db[table_name]._id==record_id).update(**vars)
def DELETE(table_name,record_id):
return db(db[table_name]._id==record_id).delete()
return dict(GET=GET, POST=POST, PUT=PUT, DELETE=DELETE)
GET
1. Navigate to the following URL to see the resources/end points that are
available via GET:
http://127.0.0.1:8000/socrata/default/api/patterns.json
Output:
336
Real Python for the Web RealPython.com
Endpoints:
◦ http://127.0.0.1:8000/socrata/default/api/socrata.json
◦ http://127.0.0.1:8000/socrata/default/api/socrata/id/[id].json
◦ http://127.0.0.1:8000/socrata/default/api/socrata/id/[id]/[field_name].json
◦ http://127.0.0.1:8000/socrata/default/api/socrata/
views/[start_range]/[end_range].json
◦ http://127.0.0.1:8000/socrata/default/api/
socrata/[start_range]/[end_range]/[field_name].json
2. http://127.0.0.1:8000/socrata/default/api/socrata.json
>>> r = requests.get("http://127.0.0.1:8000/socrata/
default/api/socrata.json")
>>> print r
<Response [400]>
>>> print r.content
too many records
3. http://127.0.0.1:8000/socrata/default/api/socrata/id/[id].json
337
Real Python for the Web RealPython.com
>>> r = requests.get("http://127.0.0.1:8000/socrata/
default/api/socrata/id/100.json")
>>> print r
<Response [200]>
>>> print r.content
{"content": [{"name": "Ohio arsons per 1,000 by county",
"views": 1, "url": "/dataset/
Ohio-arsons-per-1-000-by-county/8axx-frtc", "id": 100}]}
4. http://127.0.0.1:8000/socrata/default/api/socrata/id/[id]/[field_name].json
>>> r = requests.get("http://127.0.0.1:8000/socrata/
default/api/socrata/id/100/views.json")
>>> print r
<Response [200]>
>>> print
r.content
{"content": [{"views": 1}]}
>>> r = requests.get("http://127.0.0.1:8000/socrata/
default/api/socrata/id/100/name.json")
>>> print r
<Response [200]>
>>> print
r.content
{"content": [{"name": "Ohio arsons per 1,000 by county"}]}
5. http://127.0.0.1:8000/socrata/default/api/socrata/
views/[start_range]/[end_range].json
338
Real Python for the Web RealPython.com
>>> r = requests.get("http://127.0.0.1:8000/socrata/
default/api/socrata/views/300000/1000000.json")
>>> print
r
<Response [200]>
>>> print
r.content
{"content": [{"name": "The White House - Nominations &
Appointments", "views": 418075, "url": "/Government/
The-White-House-Nominations-Appointments/n5m4-mism", "id":
2}, {"name": "2011 Report to Congress on White House
Staff", "views": 330053, "url": "/Government/
2011-Report-to-Congress-on-White-House-Staff/73t8-rw4g",
"id": 3}]}
6. http://127.0.0.1:8000/socrata/default/api/
socrata/[start_range]/[end_range]/[field_name].json
>>> r = requests.get("http://127.0.0.1:8000/socrata/
default/api/socrata/views/300000/1000000/name.json")
>>> print r
<Response [200]>
>>> print r.content
{"content": [{"name": "The White House - Nominations &
Appointments"}, {"name": "2011 Report to Congress on White
House Staff"}]}
POST
1. http://127.0.0.1:8000/socrata/default/api/socrata.json
339
Real Python for the Web RealPython.com
PUT
1. http://127.0.0.1:8000/socrata/default/api/socrata/[id].json
DELETE
1. http://127.0.0.1:8000/socrata/default/api/socrata/[id].json
>>> r = requests.delete("http://127.0.0.1:8000/socrata/
default/api/socrata/3.json")
>>> print r
<Response [200]>
Patterns
Again, you can customize the patterns to define the specific available endpoints.
For example:
patterns = [
"/socrata/{socrata.name.startswith}",
"/socrata/{socrata.name}/:field",
"/socrata/{socrata.id}/:field",
"/socrata/{socrata.id}"
]
1. http://127.0.0.1:8000/socrata/default/api/socrata/{socrata.name.startswith}
340
Real Python for the Web RealPython.com
>>> r = requests.get("http://127.0.0.1:8000/socrata/
default/api/socrata/government.json")
>>> print r
<Response [200]>
>>> print r.content
{"content": [{"name": "Government Employees with salarium
greater than 100K", "views": 5, "url": "/dataset/
Government-Employees-with-salarium-greater-than-10/
ctwy-exdw", "id": 1168}, {"name": "Government", "views":
878, "url": "/dataset/Government/g5vf-f4hx", "id": 1269},
{"name": "Government Services", "views": 758, "url":
"/Business/Government-Services/st9p-sb8m", "id": 1619},
{"name": "Government data is the property of taxpayers and
should be free to all citizens", "views": 658, "url":
"/Government/
Government-data-is-the-property-of-taxpayers-and-s/
u83v-7srs", "id": 1855}, {"name": "Government Website
Satisfaction", "views": 18, "url": "/dataset/
Government-Website-Satisfaction/j3zd-539b", "id": 10144},
{"name": "government relations", "views": 33, "url":
"/dataset/government-relations/peg6-sm86", "id":
19010}]}
2. http://127.0.0.1:8000/socrata/default/api/id/{socrata.id}
>>> r = requests.get("http://127.0.0.1:8000/socrata/
default/api/id/1168.json")
>>> print r
<Response [200]>
>>> print r.content
{"content": [{"name": "Government Employees with salarium
greater than 100K", "views": 5, "url": "/dataset/
Government-Employees-with-salarium-greater-than-10/
ctwy-exdw", "id": 1168}]}
3. http://127.0.0.1:8000/socrata/default/api/socrata/{socrata.id}
341
Real Python for the Web RealPython.com
>>> r = requests.delete("http://127.0.0.1:8000/socrata/
default/api/socrata/1168.json")
>>> print
r
<Response [200]>
Try adding additional patterns), and test out the API using various POST methods.
Authentication
Finally, make sure to add the login() required decorator to the api() function, so that
users have to be registered to make API calls.
auth.settings.allow_basic_login = True
@auth.requires_login()
Unauthorized:
>>> r = requests.delete("http://127.0.0.1:8000/socrata/
default/api/socrata/1168.json")
>>> print
r
<Response [403]>
1. http://web2py.com/books/default/chapter/29/10#parse_as_rest-
↩
(experimental)↩
2. http://web2py.com/books/default/chapter/29/6#Legacy-databases-and-keyed-
↩
tables↩
↩
3. http://devo.ps/blog/2013/03/22/designing-a-restful-api-that-doesn-t-suck.html↩
342
Real Python for the Web RealPython.com
10.1) Overview
Like web2py, Django is a high-level web framework, which supports elegant,
beautiful design and rapid web development. With a strong community of
supporters and some of the largest and most popular sites using it such as Reddit,
Instagram, Mozilla, Pinterest, Disqus, and Rdio, to name a few 1, it's the most well-
known and used Python web framework. In spite of that, Django has a high
learning curve due to much of the implicit automation that happens in the backend.
It's much more important to understand the basics - e.g., the Python syntax and
language, web client and server fundamentals, etc. - and then move on to lighter-
weight/minimalist frameworks (like Flask or bottle.py) so that when you do start
developing with Django, it will be much easier to obtain a deeper understanding of
the automation and its integrated functionality. Even web2py, which is slightly more
automated, is easier to learn because it was specifically designed as a learning
tool.
343
Real Python for the Web RealPython.com
I know this is a bit confusing, but just remember that the MTV and MVC
architectures work the same: 2
In Django’s interpretation of MVC, the view describes the data that gets presented
to the user; it’s not necessarily just how the data looks, but which data is
presented.
In contrast ... similar frameworks suggest that the controller’s job includes deciding
which data gets presented to the user, whereas the view is strictly how the data
looks, not which data is presented.
In this chapter, you'll see how easy it is to get a project up due to the automation of
common web development tasks and included integrated functions (batteries
included). As long as you are aware of the inherent structure and organization
(scaffolding) that Django uses, you can focus less on monotonous tasks, inherent
in web development, and more on developing the higher-level portions of your
application.
Brief History
Django grew organically from the web developers at the Lawrence Journal-World
newspaper in Lawrence, Kansas (home of the University of Kansas) in 2003. The
344
Real Python for the Web RealPython.com
The developers found that the commonalities shared between most applications
could (and should) be automated. Django came to fruition from this rather simple
realization, changing the state of web development as a whole.
Homework
10.2) Installation
1. Within your terminal, navigate to your "realpython" directory, and then run the
following command to create a virtualenv for your Django projects from this
chapter:
2. Navigate into the "django" directory (the Django root directory), then activate
the virtualenv:
Unix:
345
Real Python for the Web RealPython.com
$ source bin/activate
Windows:
$ scripts\activate
Please make sure to install Django version 1.5, which as of writing, is the
latest stable release.
If you need to check the Django version currently installed on your machine, open
the Python shell and run the following commands:
Again, when you are done working with your Django project simply type
deactivate to exit the virtualenv. Then to reactivate the virtualenv, navigate to
the "django" directory and type one of the following:
Unix:
$ source bin/activate
Windows:
$ scripts\activate
346
Real Python for the Web RealPython.com
Basic Setup
Unix:
Windows:
2. This will create the basic project layout (commonly referred to as the
scaffolding), containing one directories and five files:
├── manage.py
└── sites
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
For now you just need to worry about these files manage.py, settings.py, and
urls.py:
347
Real Python for the Web RealPython.com
◦ settings.py: This is your project settings file for your project, where you
configure your project's resources, such as database connections,
external applications, and template files. There are numerous defaults
setup in this file, which often get changed as you develop your Project.
◦ urls.py: This file contains the URL mappings, connecting URLs to Views.
3. Before we start creating our Hello, World! application, let's make sure
everything is setup correctly by running the development server. Navigate into
the first sites directory and run the following command:
You can specify a different port with the following command (if necessary):
Validating models...
0 errors found
June 03, 2013 - 13:07:43
Django version 1.5, using settings 'sites.settings'
Development server is running at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
348
Real Python for the Web RealPython.com
It's much easier to work with Sublime 2 by adding all the Django files to a Sublime
Project:
You should see both directories and all five files on the left pane:
349
Real Python for the Web RealPython.com
1. Now that the Django project and development environment are setup, let's
create a new app. With virtualenv activated, navigate to your "sites" directory,
and then run the following command:
This will create a new directory called "helloworld", which includes the
following files:
◦ models.py: This file is used to define your data models that are
connected to the database.
◦ tests.py: This houses your test files used for setting up unit and
integration tests (don't worry about this for now).
◦ views.py: This file is your application's controller (as mentioned above),
defining the business logic in order to render a view to the browser.
350
Real Python for the Web RealPython.com
├── helloworld
│ ├── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── manage.py
└── sites
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
2. Next, we need to include the new app in the settings.py file so that Django
knows that it exists. Scroll down to "INSTALLED_APPS" and add the app
name, helloworld.
351
Real Python for the Web RealPython.com
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'helloworld',
# Uncomment the next line to enable the admin:
#'django.contrib.admin',
# Uncomment the next line to enable admin
documentation:
# 'django.contrib.admindocs',
)
def hello_view(request):
return HttpResponse('<html><body>Hello,
World!</body></html>')
4. With the view setup, we now just need to add it to the urls.py file to link a URL
to that specific view. With urls.py open, add this code to the urlpatterns:
352
Real Python for the Web RealPython.com
urlpatterns = patterns('',
url(r'^hello/$', 'helloworld.views.hello_view')
)
We'll go over the exact syntax for generating the views in the next chapter.
5. Let's test it out. Navigate to the first "sites" directory in your terminal, fire up
the server ( python manage.py runserver ), and then open your browser to
http://localhost:8000/hello.
It worked! You should see the "Hello, World!" text in the browser.
urlpatterns = patterns('',
url(r'^hello/$', 'helloworld.views.hello_view'),
url(r'^$', 'helloworld.views.hello_view')
)
Save the file and refresh the page. You should see the same "Hello, World!"
text. So, we simply assigned or mapped two URLs ( / and /hello ) to that
single view.
Homework
Experiment with adding additional text-based views and assigning them to URLs.
353
Real Python for the Web RealPython.com
10.3) Templates
Django templates are really the same as web2py views, which are used for
displaying HTML to the user. You can also embed Python into the templates. Let's
modify the example above to include templates.
1. Navigate to the first "sites" directory and create a new directory called
"templates". Your project structure should now look like this:
├── helloworld
│ ├── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
├── manage.py
├── sites
│ ├── __init__.py
│ ├── settings.py
│ ├── urls.py
│ └── wsgi.py
└── templates
As stated in the file make sure to use strings and absolute paths - i.e.,
"/home/html/realpython/django/sites/templates" or "C:/www/realpython/
django/sites/templates". Also, always use forward slashes regardless of
whether you're working from a Windows or Unix environment.
TEMPLATE_DIRS = (
'/Users/michaelherman/desktop/realpython/django/sites/
templates'
)
354
Real Python for the Web RealPython.com
def hello_view(request):
return HttpResponse('<html><body>Hello,
World!</body></html>')
def better_hello(request)
t = loader.get_template('betterhello.html')
c = Context({'current_time': datetime.now(),})
return HttpResponse(t.render(c))
4. Next, let's setup the template. Create a new HTML file called betterhello.html
in the "template" directory and pass in the key from the dictionary surrounded
by double curly braces {{ }} . Place the following code in the file:
<html>
<head><title>A Better Hello!</title>
<body>
<p>Hello, World! This template was rendered on
{{current_time}}.</p>
</body>
</html>
url(r'^better/$', 'helloworld.views.better_hello')
355
Real Python for the Web RealPython.com
"Hello, World! This template was rendered on June 4, 2013, 11:54 a.m.."
7. Notice the time. Is it correct? The time zone defaults to U.S. Central Time. If
you would like to change it, open the settings.py file, and then change the
COUNTRY/CITY based on the timezones found in Wikipedia.
Change the time zone so that the time is correct based on where you live.
Model
1. Navigate to your "django" directory and activate your virtualenv, then create a
new project called bloggy:
Unix:
Windows:
2. Navigate into the newly created "bloggy" directory and launch the Django
development server to ensure the new project was setup correctly:
356
Real Python for the Web RealPython.com
Again, make sure to use absolute paths and forward slashes - i.e.,
"/home/html/realpython/django/bloggy/database_name.db" or "C:/www/
realpython/django/bloggy/database_name.db".
The results should look like something similar to this, depending upon your
path:
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/Users/michaelherman/desktop/realpython/django/
bloggy/bloggy.db',
This will create the basic tables and ask you to setup a superuser. Use
"admin" for both your username and password:
357
Real Python for the Web RealPython.com
7. Before moving on, let's put this under version control with Git using the
following three commands (feel free to also push to Github):
$ git init
$ git add *
$ git commit -m "Init Commit"
8. Finally, we can define the model using the Django ORM rather than straight
SQL. Add the following code to model.py:
class Post(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100)
content = models.TextField()
358
Real Python for the Web RealPython.com
This class, which inherits some of the basic properties from the standard
Model class, defines the database table as well as each field - created_at,
title, and content.
Much like SQLAlchemy and web2py's DAL, the DJango ORM provides a
database abstraction layer used for interacting with a database using CRUD
(create, read, update, and delete) via Python objects4. Flip back to lesson 8.4
to see SQLAlchemy and web2py's DAL to compare the differences between
the three abstraction layers.
Note that the primary key, a unique id, and the created_at timestamp will
both be automatically generated for us when objects are added to the
database. We just need to add the title and content when creating new
objects (database rows).
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog'
10. Before we sync the database, we can view the actual SQL statements that will
be used by running the following command:
359
Real Python for the Web RealPython.com
BEGIN;
CREATE TABLE "blog_post" (
"id" integer NOT NULL PRIMARY KEY,
"created_at" datetime NOT NULL,
"title" varchar(100) NOT NULL,
"content" text NOT NULL
)
;
Compare the field types here to the field types used in the ORM. You can
view all the available field types on the Django website.
11. We can also check to see if there are any errors in the data model:
Django Shell
Django provides an interactive Python shell for accessing the Django API.
2. Let's add some data to the table we just created via the database API.5 Start
by importing the model Class we created:
3. If you search for objects in the table (somewhat equivalent to select * from
blog_post ) you should find that it's empty:
360
Real Python for the Web RealPython.com
>>> Post.objects.all()
361
Real Python for the Web RealPython.com
Now if you search for all objects, three objects should be returned:
>>> Post.objects.all()
[<Post: Post object>, <Post: Post object>, <Post: Post
object>]
def __unicode__(self):
return self.title
class Post(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100)
content = models.TextField()
def __unicode__(self):
return self.title
This is a bit confusing, as Python Classes are usually returned with __str__ ,
not __unicode__ . We use unicode because Django models use unicode by
default6.
362
Real Python for the Web RealPython.com
Save your models.py file, exit the Shell, re-open the Shell, import the Post
model Class again ( from blog.models import Post ), and now run the
query Post.objects.all() . You should now see -
This should be much easier to read and understand. We know there are three
rows in the table, and we know their titles. Depending on how much
information you want returned, you could add all the fields to the model.py file:
def __unicode__(self):
return str(self.id) + " / " + str(self.created_at) + "
/ " + self.title + " / " + self.content + "\n"
Test this out. What does this return? Make sure to update this so it's just
returning the title:
def __unicode__(self):
return self.title
Open up your SQLite Browser to make sure the data was added correctly:
363
Real Python for the Web RealPython.com
>>> Post.objects.filter(id=1)
>> Post.objects.filter(id__gt=1)
>>> Post.objects.filter(title__contains='data')
If you want more info on querying databases via the Django ORM in the Shell,
take a look at the official documentation. And if you want a challenge, add
364
Real Python for the Web RealPython.com
more data and practice querying using straight SQL and then convert it over to
objects via the Django ORM.
Now that the database table is setup, let's write a quick unit test. It's common
practice to write tests as you develop new models and functions. Make sure to
include at least one test for each function within your model.py files.
class PostTests(TestCase):
def test_str(self):
my_title = Post(title='This is a basic title for a
basic test case')
self.assertEquals(str(my_title), 'This is a basic
title for a basic test case',)
You can run all the tests in the Django project with the following
command - python manage.py test ; or you can run the tests from a
specific app, like in the command above.
365
Real Python for the Web RealPython.com
..
----------------------------------------------------------------------
OK
One thing to to note is that since this test needed to add data to a database to run,
it created a temporary database and then destroyed it after the test ran. This
prevents the test from accessing the real database and possibly damaging the
database by mixing test data with real data.
Django Admin
Depending how familiar you are with the Django ORM and SQL statements in
general, it's probably much easier to access and modify the data model using the
Django Admin.
1. To access the admin, we need to enable the app and setup the URL.
366
Real Python for the Web RealPython.com
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
'django.contrib.admin'
)
◦ Within your "blog" directory add a new file called admin.py that contains
the following code:
admin.site.register(Post)
2. Now let's access the Django Admin. Fire up the server and navigate to
http://localhost:8000/admin within your browser. Enter your login credentials
("admin" and "admin"). You can now see the model Class:
367
Real Python for the Web RealPython.com
3. Add some more posts, change some posts, delete some posts. Go crazy.
As mentioned, Django's views are equivalent to the controllers in most other MVC-
style web frameworks. The views are generally paired with templates to generate
HTML to the browser. Let's look at how to add the basic templates and views to our
blog.
Before we start, create a new directory within the "blog" directory called
"templates", and then add the relative path to the settings.py file:
TEMPLATE_DIRS = (
'/Users/michaelherman/desktop/realpython/django/bloggy/blog/
templates')
Note: The above code will vary depending upon the path and environment
(Windows or Unix) you are using.
index.html
368
Real Python for the Web RealPython.com
def index(request):
latest_posts =
Post.objects.all().order_by('-created_at')
t = loader.get_template('index.html')
c = Context({'latest_posts': latest_posts,})
return HttpResponse(t.render(c))
• template: Create a new file called index.html within the "templates" file and
add the following code:
urlpatterns = patterns('',
url(r'^admin/', include(admin.site.urls)),
url(r'^blog/$', 'blog.views.index'),
)
369
Real Python for the Web RealPython.com
<html>
<head> <title>Bloggy: a blog app</title> </head>
<body>
<h2>Welcome to Bloggy!</h2>
{% if latest_posts %}
<ul>
{% for post in latest_posts %}
<h3><a href="/blog/{{ post.id }}">{{ post.title
}}</a></h3>
<p><em>{{ post.created_at }}</em></p>
<p>{{ post.content }}</p>
<br/>
{% endfor %}
</ul>
{% endif %}
</body>
</html>
370
Real Python for the Web RealPython.com
• If you refresh the page now, you'll see that it's easier to read. Also, each post
title is now a link. Try clicking on the link. You should see a 404 error because
we have not set up the URL routing or the template. Let's do that now.
• Before moving on, notice how we used two kind of braces within the template.
The first, {% %} , is used to signify Python logic such as a IF statements or
For loops, and the second, {{ }} , is used for inserting variables.
post.html
urlpatterns = patterns('',
url(r'^admin/', include(admin.site.urls)),
url(r'^blog/$', 'blog.views.index'),
url(r'^blog/(?P<post_id>\d+)/$', 'blog.views.post'),
)
371
Real Python for the Web RealPython.com
This function accepts two parameters: the request object and the post_id ,
which is the number parsed from the URL by the regular expression we setup.
Meanwhile the get_object_or_404 method queries the database for
objects by a specific type (id) and returns that object if found. If not found, it
returns a 404 error.
372
Real Python for the Web RealPython.com
<html>
<head> <title>Bloggy: {{ single_post.title }}</title>
</head>
<body>
<h2>{{ single_post.title }}</h2>
<ul>
<p><em>{{ single_post.created_at }}</em></p>
<p>{{ single_post.content }}</p>
<br/>
</ul>
<p>Had enough? Return <a href="/blog">home</a>.<br/>
</body>
</html>
• Go back to your blog on the development server and test out the links for each
post. They should all be working now.
Homework
Please read about the Django Models for more information on the Django ORM
syntax.
Styles
Before adding any styles, let's break our templates into parent and child templates,
so that the child templates will inherent the HTML and styles from the parent
template. We've covered this a number of times before so I won't go into great
detail.
1. Create a new template file called base.html. This is the parent file. Add the
following code:
373
Real Python for the Web RealPython.com
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Bloggy: a blog app</title>
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
374
Real Python for the Web RealPython.com
<body>
<div id="messages">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{message.tags}}">
<a class="close" data-dismiss="alert">×</a>
{{message}}
</div>
{% endfor %}
{% endif %}
</div>
<div class="container">
{% block content %}
{% endblock %}
</div> <!-- /container -->
</body>
</html>
375
Real Python for the Web RealPython.com
2. Update index.html:
{% extends 'base.html' %}
{% block content %}
{% if latest_posts %}
<ul>
{% for post in latest_posts %}
<h3><a href="/blog/{{ post.id }}">{{ post.title
}}</a></h3>
<p><em>{{ post.created_at }}</em></p>
<p>{{ post.content }}</p>
<br/>
{% endfor %}
</ul>
{% endif %}
{% endblock %}
3. Update post.html:
{% extends 'base.html' %}
{% block content %}
<h2>{{ single_post.title }}</h2>
<ul>
<p><em>{{ single_post.created_at }}</em></p>
<p>{{ single_post.content }}</p>
<br/>
</ul>
<p>Had enough? Return <a href="/blog">home</a>.<br/>
{% endblock %}
Take a look at the results. Amazing what five minutes can do.
Finally, update Git. In your terminal, navigate to your Bloggy directory and run the
following commands:
376
Real Python for the Web RealPython.com
South
Before moving on to the next chapter, let's look at how to handle database
migrations (database schema changes) in Django.
1. Open up your models.py file and add two new fields to the table:
3. Fire up your development server and login to the admin page, and then try to
add a new row to the database that includes a tag. You should see the error
"table blog_post has no column named tag" because the field we tried to add
didn't get added to the database. That's a problem. Fortunately, there is an
easy solution: South. 7
377
Real Python for the Web RealPython.com
6. Once again add the new fields to the model and run these commands to
update the schema:
7. Run the server, go the Django admin, and you now should be able to add
rows with a tag and/or and image. Boom.
8. Now let's update the the application so that images will be viewable on the
post page.
◦ Update posts.html:
{% extends 'base.html' %}
{% block content %}
<h2>{{ single_post.title }}</h2>
<ul>
<p><em>{{ single_post.created_at }}</em></p>
<p>{{ single_post.content }}</p>
<p>Tag: {{ single_post.tag }}
<br/>
<p>
{% if single_post.image %}
<img src="/media/{{ single_post.image }}">
{% endif %}
</ul>
<p>Had enough? Return <a href="/blog">home</a>.<br/>
{% endblock %}
◦ Update settings.py:
378
Real Python for the Web RealPython.com
MEDIA_ROOT = '/Users/michaelherman/desktop/realpython/
django/bloggy/blog/static/media'
MEDIA_URL = '/media/'
STATIC_ROOT = '/Users/michaelherman/desktop/realpython/
django/bloggy/blog/static'
STATIC_URL = '/static/'
STATICFILES_DIRS = ('/Users/michaelherman/desktop/
realpython/django/bloggy/blog/static/')
◦ Update urls.py:
urlpatterns = patterns('',
url(r'^admin/', include(admin.site.urls)),
url(r'^blog/$', 'blog.views.index'),
(r'^media/(?P<path>.*)$',
'django.views.static.serve',{'document_root': '/Users/
michaelherman/desktop/realpython/django/bloggy/bloggy/
static/media'}),
url(r'^blog/(?P<post_id>\d+)/$', 'blog.views.post'),
Homework
• Now that we can add a tag to our posts, on the post.html page, list out a tag
associated with the post.
• Optional: You're ready to go through the Django tutorial. For beginners, this
tutorial can be pretty confusing. However, since you now have plenty of web
development experience and have already created a basic Django app, you
should have no trouble. Have fun! Learn something.
379
Real Python for the Web RealPython.com
↩
1. http://www.djangosites.org/↩
↩
2. http://stackoverflow.com/questions/6621653/django-vs-model-view-controller↩
↩
3. http://www.djangobook.com/en/2.0/chapter01.html↩
↩
4. https://docs.djangoproject.com/en/1.5/topics/db/models/↩
↩
5. https://docs.djangoproject.com/en/1.5/topics/db/queries/↩
6. https://docs.djangoproject.com/en/1.5/ref/models/
↩
instances/#django.db.models.Model.unicode↩
↩
7. http://south.aeracode.org/↩
380
Real Python for the Web RealPython.com
11.1) Overview
In the past ten chapters you've learned the web fundamentals from the ground up,
hacked your way through a number of different exercises, asked questions, tried
new things, etc. - and now it's time to put it all together and build a practical, real-
world application, using the principles of rapid web development. After you
complete this project, you should be able to take your new skills out into the real
world and develop your own project.
As you have seen, development is broken into logical chunks. If we were starting
from complete scratch we'd begin with prototyping to define the major features and
design a mockup through iterations: Prototype, Review, Refine. After each stage
you generally get more and more detailed, from low to high-fidelity, until you've
hashed out all the features and prepared a mockup complex enough to add a web
framework in order to apply the MVC-style architecture.
If you are beginning with low-fidelity, start prototyping with a pencil and paper.
Avoid the temptation to use prototyping software until the latter stages, as these
can be restricting. Do not stifle your creativity in the early stages.
381
Real Python for the Web RealPython.com
As you define each function and apply a design, put yourself in the end users'
shoes. What can he see? What can she do? Do they really care? For example, if
one of your application's functions is to allow users to view a list of search results
from an airline aggregator in pricing buckets, what does this look like? Are the
results text or graphic-based? Can the user drill down on the ranges to see the
prices at a granular level? And, most importantly, does the end user care? Will this
be a differentiator in your product versus the competition? Perhaps not. But there
should be some function(s) that do separate your product from your competitor's
products. Or perhaps your functionally is the same - you just implement it better?
Finally, rapid web development is one of the most important skills a web developer
can have, especially developers who work at startups. Speed is the main
advantage that startups have over their larger, more established competition. The
key is to understand each layer of the development process, from beginning to
end.
11.3) Prototyping
Prototyping is the process of building a working model of your application, from a
front-end perspective, allowing you to test and refine your application's main
features and functions. Again, it's common practice to begin with a low-fidelity
prototype.
From there you can start building a storyboard, which traces the users'
movements. For example, if the user clicks the action button and s/he is taken to a
new page, create that new page. Again, for each main function, answer these three
questions:
If you're building out a full-featured web application take the time to define in detail
every interaction the user has with the website. Create a story board, and then
382
Real Python for the Web RealPython.com
after plenty of iterations, build a high-fidelity prototype. One of the quickest means
of doing this is via the front-end framework, Bootstrap.
First, let's get a basic Django Project and App setup. With this app, you can
implement other pieces to develop your own web app.
In most cases, development begins with defining the model and constructing
the database first. Since we're creating a rapid prototype, we'll be starting with
the front-end first, adding the most important functions, then moving to the
back-end. This is vital for when you develop your basic minimum viable
prototype. The goal is to create an app quick to validate your product and
business model. Once validated, you can finish developing your product,
adding all components you need to transform your prototype into a project.
Basic Setup
1. Create a new directory in "realpython" called "mvp" within your terminal, and
then navigate into the new directory.
Unix:
$ source myenv/bin/activate
Windows:
$ myenv\scripts\activate
3. Install Django:
383
Real Python for the Web RealPython.com
6. Navigate into the "project_name" directory, and then place your project under
version control:
$ git init
$ git add .
$ git commit -m "initial commit"
├── manage.py
├── main
│ ├── __init__.py
│ ├── models.py
│ ├── tests.py
│ └── views.py
└── project_name
├── __init__.py
├── settings.py
├── urls.py
└── wsgi.py
384
Real Python for the Web RealPython.com
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'main',
and
import os
PROJECT_ROOT = os.path.realpath(os.path.dirname(__file__))
SITE_ROOT = os.path.dirname(PROJECT_ROOT)
The realpath() function is used to access the relative path within your file
system.1
'django.contrib.admin',
385
Real Python for the Web RealPython.com
url(r'^admin/', include(admin.site.urls)),
$ git add .
$ git commit -m "added main app"
Now that the main app is up, let's quickly add a barebones landing page.
def index(request):
return render_to_response('index.html',
context_instance=RequestContext(request))
386
Real Python for the Web RealPython.com
words, when a user navigates to the index.html page (the request), the django
controller renders the index.html template (the response).
3. Finally, we need to create the index.html template. First create a new file
called base.html (parent template) within the templates directory, and add the
following code:
387
Real Python for the Web RealPython.com
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Your MVP</title>
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<meta name="description" content="Setting up an MVP with
Django by RealPython.com">
<meta name="author" content="Michael Herman">
<!-- Le styles -->
<link href="http://www.realpython.com/css/bootstrap.css"
rel="stylesheet">
<style>
body {
padding-top: 20px;
padding-bottom: 40px;
}
/* Custom container */
.container-narrow {
margin: 0 auto;
max-width: 700px;
}
.container-narrow > hr {
margin: 30px 0;
}
388
Real Python for the Web RealPython.com
.jumbotron .btn {
font-size: 21px;
padding: 14px 24px;
}
</style>
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements
-->
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/
html5.js">
</script>
<![endif]-->
<!-- Le fav and touch icons -->
</head>
<body>
<div class="container-narrow">
<div class="masthead">
<ul class="nav nav-pills pull-right">
<li><a href="{% url 'home' %}">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
<h3><span class="fui-settings-16 muted">Your
MVP!</span></h3>
</div>
<hr>
389
Real Python for the Web RealPython.com
{% if messages %}
<div class="alert alert-success">
<div class="messages">
{% for message in messages %}
{{ message }}
{% endfor %}
</div>
</div>
{% endif %}
<div class="container-narrow">
{% block content %}
{% endblock %}
</div>
<hr>
<div class="footer">
<p>Copyright © 2013 <a
href="http://www.yoursite.com">Your MVP</a>. Developed by
<a href="http://www.mherman.org">Your Name Here</a>.
Powered by Django.</P></p>
</div>
</div>
</body>
</head>
390
Real Python for the Web RealPython.com
{% extends 'base.html' %}
{% block content %}
<div class="jumbotron">
<img src="http://tischlerei-weissenbacher.at/img/
responsive_web_design.png">
<h1>Please put some text here.</h1>
<p class="lead">If you want, you can add some text here
as well. Or not.</p>
<a class="btn btn-large btn-success"
href="contact">Contact us today to get
started</a><br/><br/><br/>
<a href="https://twitter.com/RealPython"
class="twitter-follow-button" data-show-count="false"
data-size="large">Follow @RealPython</a>
<script>!function(d,s,id){var
js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if
widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document,
'script', 'twitter-wjs');
</script>
<a href="https://twitter.com/intent/
tweet?screen_name=RealPython"
class="twitter-mention-button" data-size="large">Send us a
Tweet to @RealPython</a>
<script>!function(d,s,id){var
js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if
widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document,
'script', 'twitter-wjs');
</script>
<a href="https://twitter.com/share"
class="twitter-share-button"
data-url="http://www.realpython.com" data-text="Django for
the Non-Programmer #realpython -"
data-size="large">Share!</a>
<script>!function(d,s,id){var
js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if
391
Real Python for the Web RealPython.com
widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document,
'script', 'twitter-wjs');
</script>
</div>
<hr>
<div class="span6">
<h3>(4) Four</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Mauris ut dui ac nisi vestibulum accumsan. Praesent
gravida nulla vitae arcu blandit, non congue elit tempus.
Suspendisse quis vestibulum diam.</p>
<h3>(5) Five</h3>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Mauris ut dui ac nisi vestibulum accumsan. Praesent
392
Real Python for the Web RealPython.com
</div>
{% endblock %}
$ git add .
$ git commit -m "added templates and main view"
393
Real Python for the Web RealPython.com
Looking good so far, but let's add some more pages. Before that, though, let's
take a step back and talk about the design we used: Bootstrap.
Bootstrap
We used the Bootstrap front end framework to quickly add a great looking design.
However, it should be customized, which is not difficult to do. In fact, as long as
you have a basic understanding of CSS and HTML, you shouldn't have a problem.
That said, customizing Bootstrap can be time-consuming. It takes practice to get
fast at it. Try not to get discouraged as you build out your prototype. Work on one
single area at a time, then take a break. Remember: It does not have to be perfect
- it just has to give users, and potential viewers/testers of the prototype, a better
sense of how your application works.
Make as many changes as you want. Learning CSS like this is just a trial and error
process. You just have to make a few changes, then refresh the browser, and then
394
Real Python for the Web RealPython.com
make more changes or update or revert old changes. Again, it takes time to know
what will look good. Eventually, after much practice, you will find that you will be
spending less and less time on each section, as you know how to get a good base
quickly and then you can focus on creating something unique.
Homework
• Optional: If you want to learn more about CSS and Bootstrap quickly, go
through the tutorial found here.
1. First, let's add the Flatpages App, which will allow us to add basic pages with
HTML content.[^1]
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'main',
'django.contrib.flatpages',
395
Real Python for the Web RealPython.com
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(SITE_ROOT,
'test.db'),
}
}
Again, use absolute paths and name the database whatever you want.
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
)
('^pages/', include('django.contrib.flatpages.urls')),
Add a new template folder within the "templates" directory called "flatpages".
Then add a default template by creating a new file, default.html, within that
new directory. Add the following code to the file:
{% extends 'base.html' %}
{% block content %}
{{ flatpage.content }}
{% endblock %}
396
Real Python for the Web RealPython.com
<li><a href="/pages/about">About</a></li>
397
Real Python for the Web RealPython.com
<br/>
<p>You can add some text about yourself here. Then when you
are done, just add a closing HTML paragraph tag.</p>
<ul>
<li>Bullet Point # 1</li>
<li>Bullet Point # 2</li>
<li>Bullet Point # 3</li>
<li>Bullet Point # 4</li>
</ul>
<br/>
398
Real Python for the Web RealPython.com
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'main',
'django.contrib.flatpages',
'contact',
class ContactForm(models.Model):
name = models.CharField(max_length=150)
email = models.EmailField(max_length=250)
topic = models.CharField(max_length=200)
message = models.CharField(max_length=1000)
timestamp = models.DateTimeField(auto_now_add=True,
default=datetime.datetime.now)
def __unicode__(self):
return self.email
class Meta:
ordering = ['-timestamp']
5. Add a view:
399
Real Python for the Web RealPython.com
def contact(request):
form = ContactView(request.POST or None)
if form.is_valid():
our_form = form.save(commit=False)
our_form.save()
messages.add_message(request, messages.INFO, 'Your
message has been sent. Thank you.')
return HttpResponseRedirect('/')
t = loader.get_template('contact.html')
c = RequestContext(request, {'form': form,})
return HttpResponse(t.render(c))
class ContactView(ModelForm):
message = forms.CharField(widget=forms.Textarea)
class Meta:
model = ContactForm
400
Real Python for the Web RealPython.com
class ContactFormAdmin(admin.ModelAdmin):
class Meta:
model = ContactForm
admin.site.register(ContactForm,ContactFormAdmin)
{% extends 'base.html' %}
{% block content %}
<h3><center>Contact Us</center></h3>
{{ form.as_table }}
<br/>
<button type='submit' class='btn'>Submit</button>
</form>
{% endblock %}
Fire up the server and load your app. Click the link for "contact". Test it out, then
make sure that data is being added to the table within the Admin page.
401
Real Python for the Web RealPython.com
Did you notice the Flash message? See if you can recognize the code for it in the
views.py file. You can read more about the messages framework here, which
provides support for Flask-like flash messages.
We will be using Stripe for payment processing. Take a look at this brief tutorial on
implementing Flask and Stripe to get a sense of how Stripe works.
1. Update the relative path to STATICFILES_DIRS in the settings.py file and add
the "static" directory to the "project_name" directory:
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
'main',
'django.contrib.flatpages',
'contact',
'payments',
402
Real Python for the Web RealPython.com
4. Install stripe:
5. Update models.py:
class User(models.Model):
name = models.CharField(max_length=255)
email = models.CharField(max_length=255, unique=True)
password = models.CharField(max_length=60)
last_4_digits = models.CharField(max_length=4,
blank=True, null=True)
stripe_id = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.email
7. Add the following import, from payments import views , and patterns to
urls.py:
8. Add the remaining templates and all of the static files from the accompanying
"files" folder from chapter 11, lesson 11.6.
403
Real Python for the Web RealPython.com
class UserAdmin(admin.ModelAdmin):
class Meta:
model = User
admin.site.register(User,UserAdmin)
404
Real Python for the Web RealPython.com
class PaymentForm(forms.Form):
def addError(self, message):
self._errors[NON_FIELD_ERRORS] =
self.error_class([message])
class SigninForm(PaymentForm):
email = forms.EmailField(required = True)
password = forms.CharField(required = True,
widget=forms.PasswordInput(render_value=False))
class CardForm(PaymentForm):
last_4_digits = forms.CharField(required = True,
min_length = 4, max_length = 4, widget =
forms.HiddenInput())
stripe_token = forms.CharField(required = True, widget
= forms.HiddenInput())
class UserForm(CardForm):
name = forms.CharField(required = True)
email = forms.EmailField(required = True)
password = forms.CharField(required = True,
label=(u'Password'),
widget=forms.PasswordInput(render_value=False))
ver_password = forms.CharField(required = True,
label=(u' Verify Password'),
widget=forms.PasswordInput(render_value=False))
def clean(self):
cleaned_data = self.cleaned_data
password = cleaned_data.get('password')
ver_password = cleaned_data.get('ver_password')
if password != ver_password:
raise forms.ValidationError('Passwords do not
match')
return cleaned_data
405
Real Python for the Web RealPython.com
406
Real Python for the Web RealPython.com
stripe.api_key = settings.STRIPE_SECRET
def soon():
soon = datetime.date.today() +
datetime.timedelta(days=30)
return {'month': soon.month, 'year': soon.year}
def sign_in(request):
user = None
if request.method == 'POST':
form = SigninForm(request.POST)
if form.is_valid():
results =
User.objects.filter(email=form.cleaned_data['email'])
if len(results) == 1:
if
results[0].check_password(form.cleaned_data['password']):
request.session['user'] = results[0].pk
return HttpResponseRedirect('/')
else:
form.addError('Incorrect email address or
password')
else:
form.addError('Incorrect email address or
password')
else:
form = SigninForm()
407
Real Python for the Web RealPython.com
print form.non_field_errors()
return render_to_response(
'sign_in.html',
{
'form': form,
'user': user
},
context_instance=RequestContext(request)
)
def sign_out(request):
del request.session['user']
return HttpResponseRedirect('/')
def register(request):
user = None
if request.method == 'POST':
form = UserForm(request.POST)
if form.is_valid():
user = User(
name = form.cleaned_data['name'],
email = form.cleaned_data['email'],
408
Real Python for the Web RealPython.com
last_4_digits =
form.cleaned_data['last_4_digits'],
stripe_id = customer.id,
password = form.cleaned_data['password']
)
#
user.set_password(form.cleaned_data['password'])
try:
user.save()
except IntegrityError:
form.addError(user.email + ' is already a
member')
else:
request.session['user'] = user.pk
return HttpResponseRedirect('/')
else:
form = UserForm()
return render_to_response(
'register.html',
{
'form': form,
'months': range(1, 12),
'publishable': settings.STRIPE_PUBLISHABLE,
'soon': soon(),
'user': user,
'years': range(2011, 2036),
},
context_instance=RequestContext(request)
)
def edit(request):
uid = request.session.get('user')
if uid is None:
return HttpResponseRedirect('/')
409
Real Python for the Web RealPython.com
user = User.objects.get(pk=uid)
if request.method == 'POST':
form = CardForm(request.POST)
if form.is_valid():
customer =
stripe.Customer.retrieve(user.stripe_id)
customer.card = form.cleaned_data['stripe_token']
customer.save()
user.last_4_digits =
form.cleaned_data['last_4_digits']
user.stripe_id = customer.id
user.save()
return HttpResponseRedirect('/')
else:
form = CardForm()
return render_to_response(
'edit.html',
{
'form': form,
'publishable': settings.STRIPE_PUBLISHABLE,
'soon': soon(),
'months': range(1, 12),
'years': range(2011, 2036)
},
context_instance=RequestContext(request)
)
You can charge users either a one time charge or a recurring charge. This
script is setup for the latter. To change to a one time charge, simply make the
following changes to the file:
410
Real Python for the Web RealPython.com
def index(request):
uid = request.session.get('user')
if uid is None:
return render_to_response('index.html')
else:
return render_to_response('user.html', {'user':
User.objects.get(pk=uid)})
411
Real Python for the Web RealPython.com
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Your MVP</title>
<meta name="viewport" content="width=device-width,
initial-scale=1.0">
<meta name="description" content="Setting up an MVP with
Django by RealPython.com">
<meta name="author" content="Michael Herman">
<!-- Le styles -->
<link href="http://www.realpython.com/css/bootstrap.css"
rel="stylesheet">
<style>
body {
padding-top: 20px;
padding-bottom: 40px;
}
/* Custom container */
.container-narrow {
margin: 0 auto;
max-width: 700px;
}
.container-narrow > hr {
margin: 30px 0;
}
412
Real Python for the Web RealPython.com
line-height: 1;
}
.jumbotron .btn {
font-size: 21px;
padding: 14px 24px;
}
<div class="container-narrow">
<div class="masthead">
<ul class="nav nav-pills pull-right">
<li><a href="{% url 'home' %}">Home</a></li>
<li><a href="/pages/about">About</a></li>
<li><a href="{% url 'contact' %}">Contact</a></li>
{% if user %}
<li><a href="{% url 'sign_out'
%}">Logout</a></li>
413
Real Python for the Web RealPython.com
{% else %}
<li><a href="{% url 'sign_in' %}">Login</a></li>
<li><a href="{% url 'register'
%}">Register</a></li>
{% endif %}
</ul>
<h3><span class="fui-settings-16 muted">Your
MVP!</span></h3>
</div>
<hr>
{% if messages %}
<div class="alert alert-success">
<div class="messages">
{% for message in messages %}
{{ message }}
{% endfor %}
</div>
</div>
{% endif %}
<div class="container-narrow">
{% block content %}
{% endblock %}
</div>
<hr>
<div class="footer">
<p>Copyright © 2013 <a
href="http://www.yoursite.com">Your MVP</a>. Developed by
<a href="http://www.mherman.org">Your Name Here</a>.
Powered by Django.</P></p>
</div>
414
Real Python for the Web RealPython.com
STRIPE_SECRET = 'sk_test_h36oicOrlA7ATkI9JJ6dUGyA'
STRIPE_PUBLISHABLE = 'pk_test_8Xho4FfArFFuQspdH8V1KlHS'
Go ahead and test this out using the test keys. Use the following credit card
number, 4242424242424242, and any 3 digits for the CVC code. Use any
future date for the expiration date. Make sure you have a subscription plan in
place. Run the following script to set one up:
import stripe
stripe.api_key = "sk_test_zl9IWWHkI2JUvGnaLyiSSmxH"
stripe.Plan.create(
amount=2000,
interval='month',
name='Amazing Gold Plan',
currency='usd',
id='gold')
415
Real Python for the Web RealPython.com
Finally, after you process the dummy payment, make sure the user is added
to the database as well as the dashboard on Stripe:
416
Real Python for the Web RealPython.com
417
Real Python for the Web RealPython.com
Refer to the Stripe documentation and API reference docs for more
info.
↩
1. http://docs.python.org/2/library/os.path.html↩
418
Real Python for the Web RealPython.com
Windows 7
Start by downloading Python 2.7.4 from the official Python website. The Windows
version is distributed as a MSI package. Once downloaded, double-click the file to
install. By default this will install Python to C:\Python2.7.
You also need to add Python to your PATH environmental variables, so when you
want to run a Python script, you do not have to type the full path each and every
time, as this is quite tedious.
Since you downloaded Python version 2.7.4, you need to add the add the following
directories to your PATH:
• C:\Python27\
• C:\Python27\Scripts\
• C:\PYTHON27\DLLs\
• C:\PYTHON27\LIB\
[Environment]::SetEnvironmentVariable("Path",
"$env:Path;C:\Python27\;C:\Python27\Scripts\;C:\PYTHON27\
DLLs\;C:\PYTHON27\LIB\;", "User")
That's it. To test to make sure Python was installed correctly open your command
prompt and then type python to load the Shell:
419
Real Python for the Web RealPython.com
Video
Mac OS X
All Mac OS X versions since 10.4 come with Python pre-installed. You can view the
version by opening the terminal and typing python to enter the shell. The output
will look like this:
If you don't currently have version 2.7.4, go ahead and upgrade by downloading
the latest installer.
Linux
If you are using Ubuntu, Linux Mint, or another Debian-based system, enter the
following command in your terminal:
420
Real Python for the Web RealPython.com
Or you can download the tarball directly from the official Python website. Once
downloaded, run the following commands:
Once installed, fire up the terminal and type python to get to the shell:
Please note, as of April 7th, 2013, Python 2.7.4 must be downloaded via
tarball for Linux. If you have problems just use the Package Manager to install
2.7.3.
If you have problems or have a different Linux distribution, you can always use
your package manager or just do a Google search for how to install Python on your
particular Linux distribution.
421
Real Python for the Web RealPython.com
FTP (File Transfer Protocol) is used for file exchange over the Internet. Much like
HTTP and SMTP, which are used for exchanging web pages and email across the
Internet respectively, FTP uses the TCP/IP protocols for transferring data.
In most cases, FTP is used to either upload a file (such as a web page) to a remote
server or download a file from a server. In this lesson, we will be accessing an FTP
server to view the main directory listing, upload a file, and then download a file.
Code:
422
Real Python for the Web RealPython.com
import ftplib
server = ''
username = ''
password = ''
So, we imported the ftplib library, which provides Python all dependencies it needs
to access remote servers, files, and directories. We then defined variables for the
remote server and the login credentials to initialize the FTP connection. You can
leave the username and password empty if you are connecting to an anonymous
FTP server (which usually doesn't require a username or password). Next, we
created a list to receive the directory listing, and used the dir command to
append data to the list. Finally we disconnected from the server and then printed
the directories, line by line, using a for loop.
423
Real Python for the Web RealPython.com
Let's test this out with a public ftp site, using these server and login credentials:
• server = 'ftp.secureftp-test.com'
• username = 'test'
• password = 'test'
Keep everything else the same, and save the file again. Now you can run it.
Next, let's take a look at how to download a file from a FTP server.
Code:
424
Real Python for the Web RealPython.com
import ftplib
import sys
server = 'ftp.secureftp-test.com'
username = 'test'
password = 'test'
# Create a local file with the same name as the remote file
with open(file_name, "wb") as f:
When you run the file, make sure you specify a file name from the remote server
on the command line. You can use any of the files you saw in the above script
when we outputted them to the screen.
For example:
Code:
425
Real Python for the Web RealPython.com
import ftplib
import sys
server = ''
username = ''
password = ''
Unfortunately, the public FTP site we have been using does not allow uploads. You
will have to use your own server to test. Many free hosting services offer FTP
access. You can set one up in less than fifteen minutes. Just search Google for
"free hosting with ftp" to find a free hosting service. One good example is
www.0fees.net
After you setup your FTP Server, update the server name and login credentials in
the above script, save the file, and then run it. Again, specify the filename as one of
the command line arguments. It should be any file found on your local directory.
For example:
426
Real Python for the Web RealPython.com
Check to ensure that the file has been uploaded to the remote directory
It's best to send files in binary mode "rb" as this mode sends the raw bytes of
the file. Thus, the file is transferred in its exact original form.
Homework
• See if you figure out how to navigate to a specific directory, upload a file, and
then run a directory listing of that directory to see the newly uploaded file. Do
this all in one script.
To use SFTP you have to install the pysftp library. There are other custom libraries
for SFTP but this one is good if you want to run SFTP commands with minimal
configuration.
Now, let's try to list the directory contents from a remote server via SFTP.
Code:
427
Real Python for the Web RealPython.com
import pysftp
server = ''
username = ''
password = ''
To test the code, you will have to use your own remote server that supports SFTP.
Unfortunately, most of the free hosting services does not offer SFTP access. But
don't worry, there are free sites that offer free shell accounts which normally
include SFTP access. Just search Google for "free shell accounts". One good
example is www.cjb.net
After you setup your SFTP Server, update the server name and login credentials in
the above script, save the file, and then run it.
If done correctly, all the files and directories of the current directory of your remote
server will be displayed.
Next, let's take a look at how to download a file from an SFTP server.
428
Real Python for the Web RealPython.com
Code:
import pysftp
import sys
server = ''
username = ''
password = ''
When you run it, make sure you specify a file name from the remote server on the
command line. You can use any of the files you saw in the above script when we
outputted them to the screen.
For example:
Code:
429
Real Python for the Web RealPython.com
import pysftp
import sys
server = 'shell.cjb.net'
username = 'tstssh007'
password = 'p@sssh5ll'
When you run, make sure you specify a file name from your local directory on the
command line.
For example:
Check to ensure that the file has been uploaded to the remote directory
430
Real Python for the Web RealPython.com
send mail through Unix mail servers. MIME is an Internet standard used for
building the various parts of emails, such as "From", "To", "Subject", and so forth.
We will be using that library as well.
Code:
431
Real Python for the Web RealPython.com
import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
# email account info from where we'll be sending the email from
smtp_host = 'smtp.mail.com'
smtp_port = '###'
user = 'username'
password = 'password'
# use encryption
server.starttls()
432
Real Python for the Web RealPython.com
Since this code is pretty self-explanatory (follow along with the comments), go
ahead and update the the following variables: smtp_host, smtp_port, user,
password to match your email account's SMTP info and login credentials you wish
to send from.
Example:
# email account info from where we'll be sending the email from
smtp_host = 'smtp.gmail.com'
smtp_port = 587
user = '[email protected]'
password = "it's a secret - sorry"
I suggest using a GMail account and sending and receiving from the same address
at first to test it out, and then try sending from Gmail to a different email account,
on a different email service. Once complete, run the file. As long as you don't get
an error, the email should have sent correctly. Check your email to make sure.
433
Real Python for the Web RealPython.com
import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
# use encryption
server.starttls()
server.quit()
if __name__ == '__main__':
fromaddr = '[email protected]'
toaddr = '[email protected]'
subject = 'test'
434
Real Python for the Web RealPython.com
Meanwhile, IMAP (Internet Message Access Protocol) is the Internet standard for
receiving email on a remote mail server. Python provides the imaplib module as
part of the standard library which is used to define the IMAP client session
implementation, used for accessing email. Essentially, we will be setting up our
own mail server.
Code:
435
Real Python for the Web RealPython.com
import imaplib
# email account info from where we'll be sending the email from
imap_host = 'imap.gmail.com'
imap_port = '993'
user = '[email protected]'
password = "It's a secret - sorry!"
Notice how I used the same GMail account as in the last example. Make sure you
tailor this to your own account settings. Essentially, this code is used to read the
most recent email in the Inbox. This just so happened to be the email I sent myself
in the last example.
436
Real Python for the Web RealPython.com
Also, in the program num[0] specifies the message we wish to view, while
(BODY[TEXT]) displays the information from the email.
Homework
• See if you can figure out how to use a for loop to read the first 10 messages in
your inbox.
437