
Blog Exercise in SQLite
SQLite¶
Note that I decided to use SQLite for this example. It will affect our
code in just a few minor ways. Here's how to create and initialize the
SQLite database, which I saved in a file called blog.db
, which is also
in the directory you will copy.
$ sqlite3 blog.db
SQLite version 3.7.17 2013-05-20 00:56:22
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .read logins.sql
happy|happypass
bashful|bashfulpass
grumpy|grumpypass
sleepy|sleepypass
sneezy|sneezypass
dopey|dopeypass
doc|docpass
sleazy|sleazypass
gropey|gropeypass
dumpy|dumpypass
2020-03-24 12:34:56|dopey|First!
2020-03-24 13:45:00|grumpy|Give it a rest, Dopey.
sqlite>
The .read
command is equivalent to the source
command that we learned
for the mysql
client, though possibly more intuitive.
Another useful command is .schema
which shows you the schemata (schemas
or definitions) of your tables:
sqlite> .schema
CREATE TABLE blog_user(
user varchar(30) primary key,
pass varchar(30)
);
CREATE TABLE blog_entry(
entered timestamp,
user varchar(30),
entry text,
foreign key (user) references blog_user(user));
sqlite>
Using the SQLite client, you can run queries just as usual:
sqlite3 blog.db
sqlite> select * from blog_user;
happy|happypass
bashful|bashfulpass
grumpy|grumpypass
sleepy|sleepypass
sneezy|sneezypass
dopey|dopeypass
doc|docpass
sleazy|sleazypass
gropey|gropeypass
dumpy|dumpypass
sqlite>
The result is formatted a little differently than we are used to, but otherwise this is straightforward.
Running the Blog Exercise Demo¶
Before we attack the blogging application, we'll download it and I'll demo. You can also run this on your own. Here are the steps:
- Go to your
cs304
folder - Activate your venv
- Copy the app (
cp
orcurl
) - start the app and open the SSH tunnel
- In the browser, visit the main page
- Note the listing of current blog entries
- Post something as someone. The blog doesn't check passwords, so anything will work
- Login as someone
- Post once or twice as that person; notice that you don't have to give your name each time.
- You can even try logging in as different people on different browsers
Video of the Blog Exercise¶
Here's a video of how the completed blog
Blogging Exercise¶
Before we attack the blogging application, we'll download it and I'll demo. Here's how to download and run the starter version.
cd ~/cs304
source venv/bin/activate
cp -r ~cs304/pub/downloads/blog-ex blog-ex
cd blog-ex
python start.py
Starting Code¶
Unlike the cookie example in the reading, where I did all the cases in
one Flask route, in the blog-ex
example, I decided to parcel out the
various cases to different routes. I actually like this code better,
though it is more spread out. Let's consider the code in pieces:
API to SQLite
In the blog exercise, I used SQLite. Here's part of the code to import the API and to retrieve the blog entries:
import cs304dbi_sqlite3 as dbi
def recent_entries(conn,limit=10):
'''Returns the most recent 'limit' entries from the blog'''
curs = dbi.dict_cursor(conn)
curs.execute('''SELECT entered as time,user,entry
FROM blog_entry
ORDER BY entered DESC
LIMIT ?''',[limit])
return curs.fetchall()
Note the different import
statements and the ?
instead of the %s
for the placeholder. Otherwise, pretty much the same.
Initializing SQLite
We saw earlier how to set up our connection to the SQLite database,
but here it is again. This code is at the end of the start.py
file:
if __name__ == '__main__':
dbi.cache_cnf()
dbi.use('blog')
conn=dbi.connect()
entries=recent_entries(conn)
for e in entries:
print(e)
import sys,os
if len(sys.argv) > 1:
# arg, if any, is the desired port number
port = int(sys.argv[1])
assert(port>1024)
else:
port = os.getuid()
app.debug = True
app.run('0.0.0.0',port)
Notice the use
statement. The argument here means that database
connections will be made to the file blog.db
, which is in this
directory.
Main Route
The main route retrieves the latest entries and displays the page with the forms and buttons in it:
@app.route('/')
def index():
## TODO: get bloguser value from cookie
bloguser = ''
conn = dbi.connect()
entries = recent_entries(conn)
return render_template('blog.html',
allCookies=request.cookies,
bloguser=bloguser,
rows=entries)
Note the TODO comment, indicating something you should implement.
Main Template
The blog.html
template is pretty straightforward, but notice the
urls for routes to process logins, logout and posting of blog entries:
{% extends "base.html" %}
{% block page_content %}
{% if bloguser %}
<form method="post" action="{{url_for('logout')}}">
<button>logout {{bloguser}}</button>
</form>
{% else %}
<form method="post" action="{{url_for('login')}}">
<p><label>User: <input type=text name="loginname"></label>
<p><label>Pass: <input type=password name="loginpass"></label>
<p><input type="submit" value="login">
</form>
{% endif %}
<form method=post action="{{url_for('post_comment')}}">
{% if bloguser %}
<p>Name: {{bloguser}}</p>
{% else %}
<p><label>Name: <input type=text name="blogname"></label></p>
{% endif %}
<p><label>Comment: <br>
<textarea name="comment" rows="5" cols="30"></textarea></label></p>
<p><input type="submit" value="post"></p>
</form>
<dl>
{% for row in rows %}
<dt>{{row.user}} at {{row.time}}</dt><dd>{{row.entry}}</dd>
{% endfor %}
</dl>
{% endblock %}
Base Template
The main template inherits some important stuff from a base.html
,
such as displaying the cookies, reporting a message, and a little CSS
styling.
<!doctype html>
<html>
<head>
<title>Commenting</title>
<style>
form { border: 1px solid black; }
#msg { font-weight: bold; color: red; }
</style>
</head>
<body>
<h1>Blog App</h1>
<p>From base: These are all the cookies the application can see:</p>
<ul>
{% for key in allCookies %}
<li>{{key}} : {{allCookies[key]}}</li>
{% endfor %}
</ul>
{% if msg %}
<p id="msg">{{msg}}</p>
{% endif %}
{% block page_content %}
{% endblock %}
</body>
</html>
Posting Comments
The starter version, start.py
, has working code to post a blog comment. Let's see how that works:
@app.route('/post_comment/', methods=['POST'])
def post_comment():
## TODO: get bloguser value from cookie
bloguser = ''
if ('comment' in request.form):
if 'blogname' in request.form:
user=request.form['blogname']
else:
user=bloguser
entry = request.form['comment']
conn = dbi.connect()
curs = dbi.cursor(conn) # a tuple cursor
curs.execute('''INSERT INTO blog_entry(entered,user,entry) VALUES
(CURRENT_TIMESTAMP,?,?)''',
[user,entry])
conn.commit() # don't forget to commit!
return redirect(url_for('index'))
else:
return render_template('go_home.html',
msg='comment form missing comment input')
This code works because it either pulls the username out of the form
(starter version) or uses the cookie value (which you will
implement). Give those two values, it inserts the database entry,
commits it, and redirects to the main route. It uses the
CURRENT_TIMESTAMP
value which is evaluate to the current date and
time in UTC (Greenwich Mean Time).
Login
Let's look at the starter code for logins. There's work you need to add to this, but the code itself is mostly working. The basic login strategy is to take the username and password from the form, and count the number of entries in the table where both match. If the count is 0, the username and password were not correct. If the count is 1, they were. The count can't be more than 1 because the username is a key and therefore must be unique.
@app.route('/login/', methods=['POST'])
def login():
if ('loginname' in request.form and
'loginpass' in request.form):
conn = dbi.connect()
curs = dbi.cursor(conn)
curs.execute('''SELECT count(*)
FROM blog_user
WHERE user=? AND pass=?''',
(request.form['loginname'],
request.form['loginpass']))
row = curs.fetchone()
# remember, we are using a tuple cursor this time
if row[0] == 1:
bloguser=request.form.get('loginname')
## TODO: make a response object and set the cookie
resp = render_template('go_home.html',
allCookies=request.cookies,
msg='logged in as '+bloguser)
return resp
else:
errmsg = 'incorrect login'
# we are not using flash, otherwise we would definitely do this
# flash(errmsg)
# instead, we use a message
return render_template('go_home.html',
allCookies=request.cookies,
msg=errmsg)
Note the TODO lines where your code needs to remember the username using a cookie.
Logout
For completeness, we'll look at the logout code:
@app.route('/logout/', methods=['POST'])
def logout():
## TODO: make a response, saying they are logged out,
## expire the cookie and return the response object
return render_template('go_home.html',
allCookies=request.cookies,
msg='logged out')
It doesn't do much yet. You'll implement code to remove the cookie.
Goals/Approach¶
- "posted by" (commenter) field should come from cookie, rather than form
- login action should set the cookie
- various routes should read the cookie
Long Exercise
Create a login session for the comment app.
Here's how to approach the problem:
- Re-read the
start.py
code to make sure you understand it. The code was described above. - Notice the places where it says TODO and suggests what you need to do.
- Copy the
start.py
file to your own file:cp start.py mycode.py
- Modify the
mycode.py
file application to set a cookie when someone logs in. - Modify the application to read the cookie and use it when adding blog entries, so they are no longer anonymous, yet the user doesn't have to keep authenticating.
- Test, test, test
- sigh in amazed satisfaction that it really works
- Check solutions below.
- If you finish before the end of class, you can:
- Bask in the glow of your success
- Implement a "logout" button
- Use the JavaScript console to hack the application: login as, say, "dopey," and then change the username to "grumpy" and continue posting.
Solutions
The solutions are in the done.py
.
I strongly suggest using the Unix diff
command
to compare the start.py
with done.py
- In Flask: done.py