Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 249 lines (205 sloc) 8.378 kB
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
1 # -*- coding: utf-8 -*-
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
2 """
3 MiniTwit
4 ~~~~~~~~
5
6 A microblogging application written with Flask and sqlite3.
7
8 :copyright: (c) 2010 by Armin Ronacher.
9 :license: BSD, see LICENSE for more details.
10 """
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
11 from __future__ import with_statement
12 import time
13 import sqlite3
14 from hashlib import md5
15 from datetime import datetime
16 from contextlib import closing
17 from flask import Flask, request, session, url_for, redirect, \
3b36bef Improved documentation, added a contextmanager for request binding
Armin Ronacher authored
18 render_template, abort, g, flash
19 from werkzeug import check_password_hash, generate_password_hash
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
20
21
22 # configuration
23 DATABASE = '/tmp/minitwit.db'
24 PER_PAGE = 30
25 DEBUG = True
26 SECRET_KEY = 'development key'
27
28 # create our little application :)
29 app = Flask(__name__)
30
31
32 def connect_db():
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
33 """Returns a new connection to the database."""
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
34 return sqlite3.connect(DATABASE)
35
36
37 def init_db():
38 """Creates the database tables."""
39 with closing(connect_db()) as db:
40 with app.open_resource('schema.sql') as f:
41 db.cursor().executescript(f.read())
42 db.commit()
43
44
45 def query_db(query, args=(), one=False):
46 """Queries the database and returns a list of dictionaries."""
47 cur = g.db.execute(query, args)
48 rv = [dict((cur.description[idx][0], value)
49 for idx, value in enumerate(row)) for row in cur.fetchall()]
50 return (rv[0] if rv else None) if one else rv
51
52
53 def get_user_id(username):
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
54 """Convenience method to look up the id for a username."""
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
55 rv = g.db.execute('select user_id from user where username = ?',
56 [username]).fetchone()
57 return rv[0] if rv else None
58
59
60 def format_datetime(timestamp):
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
61 """Format a timestamp for display."""
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
62 return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M')
63
64
65 def gravatar_url(email, size=80):
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
66 """Return the gravatar image for the given email address."""
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
67 return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \
4ec7d2a @mitsuhiko Started working on documentation.
authored
68 (md5(email.strip().lower().encode('utf-8')).hexdigest(), size)
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
69
70
fb2d2e4 @mitsuhiko request_init -> before_request and request_shutdown -> after_request
authored
71 @app.before_request
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
72 def before_request():
73 """Make sure we are connected to the database each request and look
74 up the current user so that we know he's there.
75 """
727c701 @mitsuhiko And finished documentation for most parts.
authored
76 g.db = connect_db()
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
77 g.user = None
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
78 if 'user_id' in session:
79 g.user = query_db('select * from user where user_id = ?',
80 [session['user_id']], one=True)
81
82
fb2d2e4 @mitsuhiko request_init -> before_request and request_shutdown -> after_request
authored
83 @app.after_request
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
84 def after_request(response):
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
85 """Closes the database again at the end of the request."""
86 g.db.close()
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
87 return response
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
88
89
90 @app.route('/')
91 def timeline():
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
92 """Shows a users timeline or if no user is logged in it will
93 redirect to the public timeline. This timeline shows the user's
94 messages as well as all the messages of followed users.
95 """
96 if not g.user:
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
97 return redirect(url_for('public_timeline'))
98 return render_template('timeline.html', messages=query_db('''
99 select message.*, user.* from message, user
100 where message.author_id = user.user_id and (
101 user.user_id = ? or
102 user.user_id in (select whom_id from follower
103 where who_id = ?))
104 order by message.pub_date desc limit ?''',
105 [session['user_id'], session['user_id'], PER_PAGE]))
106
107
108 @app.route('/public')
109 def public_timeline():
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
110 """Displays the latest messages of all users."""
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
111 return render_template('timeline.html', messages=query_db('''
112 select message.*, user.* from message, user
113 where message.author_id = user.user_id
114 order by message.pub_date desc limit ?''', [PER_PAGE]))
115
116
117 @app.route('/<username>')
118 def user_timeline(username):
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
119 """Display's a users tweets."""
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
120 profile_user = query_db('select * from user where username = ?',
121 [username], one=True)
122 if profile_user is None:
123 abort(404)
73a47a0 @mitsuhiko Removed unused stuff from minitwit and fixed a bug.
authored
124 followed = False
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
125 if g.user:
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
126 followed = query_db('''select 1 from follower where
127 follower.who_id = ? and follower.whom_id = ?''',
128 [session['user_id'], profile_user['user_id']], one=True) is not None
129 return render_template('timeline.html', messages=query_db('''
130 select message.*, user.* from message, user where
131 user.user_id = message.author_id and user.user_id = ?
132 order by message.pub_date desc limit ?''',
133 [profile_user['user_id'], PER_PAGE]), followed=followed,
134 profile_user=profile_user)
135
136
137 @app.route('/<username>/follow')
138 def follow_user(username):
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
139 """Adds the current user as follower of the given user."""
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
140 if not g.user:
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
141 abort(401)
142 whom_id = get_user_id(username)
143 if whom_id is None:
144 abort(404)
145 g.db.execute('insert into follower (who_id, whom_id) values (?, ?)',
146 [session['user_id'], whom_id])
147 g.db.commit()
148 flash('You are now following "%s"' % username)
149 return redirect(url_for('user_timeline', username=username))
150
151
152 @app.route('/<username>/unfollow')
153 def unfollow_user(username):
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
154 """Removes the current user as follower of the given user."""
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
155 if not g.user:
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
156 abort(401)
157 whom_id = get_user_id(username)
158 if whom_id is None:
159 abort(404)
160 g.db.execute('delete from follower where who_id=? and whom_id=?',
161 [session['user_id'], whom_id])
162 g.db.commit()
163 flash('You are no longer following "%s"' % username)
164 return redirect(url_for('user_timeline', username=username))
165
166
4aa7621 Updated documentation. Starting to look pretty good
Armin Ronacher authored
167 @app.route('/add_message', methods=['POST'])
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
168 def add_message():
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
169 """Registers a new message for the user."""
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
170 if 'user_id' not in session:
171 abort(401)
172 if request.form['text']:
173 g.db.execute('''insert into message (author_id, text, pub_date)
174 values (?, ?, ?)''', (session['user_id'], request.form['text'],
175 int(time.time())))
176 g.db.commit()
177 flash('Your message was recorded')
178 return redirect(url_for('timeline'))
179
180
4aa7621 Updated documentation. Starting to look pretty good
Armin Ronacher authored
181 @app.route('/login', methods=['GET', 'POST'])
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
182 def login():
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
183 """Logs the user in."""
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
184 if g.user:
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
185 return redirect(url_for('timeline'))
186 error = None
187 if request.method == 'POST':
188 user = query_db('''select * from user where
189 username = ?''', [request.form['username']], one=True)
190 if user is None:
191 error = 'Invalid username'
192 elif not check_password_hash(user['pw_hash'],
193 request.form['password']):
194 error = 'Invalid password'
195 else:
196 flash('You were logged in')
197 session['user_id'] = user['user_id']
198 return redirect(url_for('timeline'))
199 return render_template('login.html', error=error)
200
201
4aa7621 Updated documentation. Starting to look pretty good
Armin Ronacher authored
202 @app.route('/register', methods=['GET', 'POST'])
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
203 def register():
2f5a4f8 @mitsuhiko Doc updates and typo fixes
authored
204 """Registers the user."""
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
205 if g.user:
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
206 return redirect(url_for('timeline'))
207 error = None
208 if request.method == 'POST':
209 if not request.form['username']:
210 error = 'You have to enter a username'
211 elif not request.form['email'] or \
212 '@' not in request.form['email']:
213 error = 'You have to enter a valid email address'
214 elif not request.form['password']:
215 error = 'You have to enter a password'
216 elif request.form['password'] != request.form['password2']:
f2dc38c @mitsuhiko Added tests for minitwit. Testing with Flask is awesome
authored
217 error = 'The two passwords do not match'
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
218 elif get_user_id(request.form['username']) is not None:
219 error = 'The username is already taken'
220 else:
221 g.db.execute('''insert into user (
222 username, email, pw_hash) values (?, ?, ?)''',
223 [request.form['username'], request.form['email'],
224 generate_password_hash(request.form['password'])])
225 g.db.commit()
226 flash('You were successfully registered and can login now')
227 return redirect(url_for('login'))
228 return render_template('register.html', error=error)
229
230
231 @app.route('/logout')
232 def logout():
03168a5 @mitsuhiko Removed the useless apishowcase example
authored
233 """Logs the user out"""
33850c0 @mitsuhiko Initial checkin of stuff that exists so far.
authored
234 flash('You were logged out')
235 session.pop('user_id', None)
236 return redirect(url_for('public_timeline'))
237
238
239 # add some filters to jinja and set the secret key and debug mode
240 # from the configuration.
241 app.jinja_env.filters['datetimeformat'] = format_datetime
242 app.jinja_env.filters['gravatar'] = gravatar_url
243 app.secret_key = SECRET_KEY
244 app.debug = DEBUG
245
246
247 if __name__ == '__main__':
248 app.run()
Something went wrong with that request. Please try again.