|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +""" |
| 3 | + MiniTwit |
| 4 | + ~~~~~~~~ |
| 5 | +
|
| 6 | + A microblogging application written with Flask and sqlite3. |
| 7 | +
|
| 8 | + :copyright: © 2010 by the Pallets team. |
| 9 | + :license: BSD, see LICENSE for more details. |
| 10 | +""" |
| 11 | + |
| 12 | +import time |
| 13 | +from sqlite3 import dbapi2 as sqlite3 |
| 14 | +from hashlib import md5 |
| 15 | +from datetime import datetime |
| 16 | +from flask import Flask, request, session, url_for, redirect, \ |
| 17 | + render_template, abort, g, flash, _app_ctx_stack |
| 18 | +from werkzeug import check_password_hash, generate_password_hash |
| 19 | + |
| 20 | + |
| 21 | +# configuration |
| 22 | +DATABASE = '/tmp/minitwit.db' |
| 23 | +PER_PAGE = 30 |
| 24 | +DEBUG = True |
| 25 | +SECRET_KEY = b'_5#y2L"F4Q8z\n\xec]/' |
| 26 | + |
| 27 | +# create our little application :) |
| 28 | +app = Flask('minitwit') |
| 29 | +app.config.from_object(__name__) |
| 30 | +app.config.from_envvar('MINITWIT_SETTINGS', silent=True) |
| 31 | + |
| 32 | + |
| 33 | +def get_db(): |
| 34 | + """Opens a new database connection if there is none yet for the |
| 35 | + current application context. |
| 36 | + """ |
| 37 | + top = _app_ctx_stack.top |
| 38 | + if not hasattr(top, 'sqlite_db'): |
| 39 | + top.sqlite_db = sqlite3.connect(app.config['DATABASE']) |
| 40 | + top.sqlite_db.row_factory = sqlite3.Row |
| 41 | + return top.sqlite_db |
| 42 | + |
| 43 | + |
| 44 | +@app.teardown_appcontext |
| 45 | +def close_database(exception): |
| 46 | + """Closes the database again at the end of the request.""" |
| 47 | + top = _app_ctx_stack.top |
| 48 | + if hasattr(top, 'sqlite_db'): |
| 49 | + top.sqlite_db.close() |
| 50 | + |
| 51 | + |
| 52 | +def init_db(): |
| 53 | + """Initializes the database.""" |
| 54 | + db = get_db() |
| 55 | + with app.open_resource('schema.sql', mode='r') as f: |
| 56 | + db.cursor().executescript(f.read()) |
| 57 | + db.commit() |
| 58 | + |
| 59 | + |
| 60 | +@app.cli.command('initdb') |
| 61 | +def initdb_command(): |
| 62 | + """Creates the database tables.""" |
| 63 | + init_db() |
| 64 | + print('Initialized the database.') |
| 65 | + |
| 66 | + |
| 67 | +def query_db(query, args=(), one=False): |
| 68 | + """Queries the database and returns a list of dictionaries.""" |
| 69 | + cur = get_db().execute(query, args) |
| 70 | + rv = cur.fetchall() |
| 71 | + return (rv[0] if rv else None) if one else rv |
| 72 | + |
| 73 | + |
| 74 | +def get_user_id(username): |
| 75 | + """Convenience method to look up the id for a username.""" |
| 76 | + rv = query_db('select user_id from user where username = ?', |
| 77 | + [username], one=True) |
| 78 | + return rv[0] if rv else None |
| 79 | + |
| 80 | + |
| 81 | +def format_datetime(timestamp): |
| 82 | + """Format a timestamp for display.""" |
| 83 | + return datetime.utcfromtimestamp(timestamp).strftime('%Y-%m-%d @ %H:%M') |
| 84 | + |
| 85 | + |
| 86 | +def gravatar_url(email, size=80): |
| 87 | + """Return the gravatar image for the given email address.""" |
| 88 | + return 'https://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ |
| 89 | + (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) |
| 90 | + |
| 91 | + |
| 92 | +@app.before_request |
| 93 | +def before_request(): |
| 94 | + g.user = None |
| 95 | + if 'user_id' in session: |
| 96 | + g.user = query_db('select * from user where user_id = ?', |
| 97 | + [session['user_id']], one=True) |
| 98 | + |
| 99 | + |
| 100 | +@app.route('/') |
| 101 | +def timeline(): |
| 102 | + """Shows a users timeline or if no user is logged in it will |
| 103 | + redirect to the public timeline. This timeline shows the user's |
| 104 | + messages as well as all the messages of followed users. |
| 105 | + """ |
| 106 | + if not g.user: |
| 107 | + return redirect(url_for('public_timeline')) |
| 108 | + return render_template('timeline.html', messages=query_db(''' |
| 109 | + select message.*, user.* from message, user |
| 110 | + where message.author_id = user.user_id and ( |
| 111 | + user.user_id = ? or |
| 112 | + user.user_id in (select whom_id from follower |
| 113 | + where who_id = ?)) |
| 114 | + order by message.pub_date desc limit ?''', |
| 115 | + [session['user_id'], session['user_id'], PER_PAGE])) |
| 116 | + |
| 117 | + |
| 118 | +@app.route('/public') |
| 119 | +def public_timeline(): |
| 120 | + """Displays the latest messages of all users.""" |
| 121 | + return render_template('timeline.html', messages=query_db(''' |
| 122 | + select message.*, user.* from message, user |
| 123 | + where message.author_id = user.user_id |
| 124 | + order by message.pub_date desc limit ?''', [PER_PAGE])) |
| 125 | + |
| 126 | + |
| 127 | +@app.route('/<username>') |
| 128 | +def user_timeline(username): |
| 129 | + """Display's a users tweets.""" |
| 130 | + profile_user = query_db('select * from user where username = ?', |
| 131 | + [username], one=True) |
| 132 | + if profile_user is None: |
| 133 | + abort(404) |
| 134 | + followed = False |
| 135 | + if g.user: |
| 136 | + followed = query_db('''select 1 from follower where |
| 137 | + follower.who_id = ? and follower.whom_id = ?''', |
| 138 | + [session['user_id'], profile_user['user_id']], |
| 139 | + one=True) is not None |
| 140 | + return render_template('timeline.html', messages=query_db(''' |
| 141 | + select message.*, user.* from message, user where |
| 142 | + user.user_id = message.author_id and user.user_id = ? |
| 143 | + order by message.pub_date desc limit ?''', |
| 144 | + [profile_user['user_id'], PER_PAGE]), followed=followed, |
| 145 | + profile_user=profile_user) |
| 146 | + |
| 147 | + |
| 148 | +@app.route('/<username>/follow') |
| 149 | +def follow_user(username): |
| 150 | + """Adds the current user as follower of the given user.""" |
| 151 | + if not g.user: |
| 152 | + abort(401) |
| 153 | + whom_id = get_user_id(username) |
| 154 | + if whom_id is None: |
| 155 | + abort(404) |
| 156 | + db = get_db() |
| 157 | + db.execute('insert into follower (who_id, whom_id) values (?, ?)', |
| 158 | + [session['user_id'], whom_id]) |
| 159 | + db.commit() |
| 160 | + flash('You are now following "%s"' % username) |
| 161 | + return redirect(url_for('user_timeline', username=username)) |
| 162 | + |
| 163 | + |
| 164 | +@app.route('/<username>/unfollow') |
| 165 | +def unfollow_user(username): |
| 166 | + """Removes the current user as follower of the given user.""" |
| 167 | + if not g.user: |
| 168 | + abort(401) |
| 169 | + whom_id = get_user_id(username) |
| 170 | + if whom_id is None: |
| 171 | + abort(404) |
| 172 | + db = get_db() |
| 173 | + db.execute('delete from follower where who_id=? and whom_id=?', |
| 174 | + [session['user_id'], whom_id]) |
| 175 | + db.commit() |
| 176 | + flash('You are no longer following "%s"' % username) |
| 177 | + return redirect(url_for('user_timeline', username=username)) |
| 178 | + |
| 179 | + |
| 180 | +@app.route('/add_message', methods=['POST']) |
| 181 | +def add_message(): |
| 182 | + """Registers a new message for the user.""" |
| 183 | + if 'user_id' not in session: |
| 184 | + abort(401) |
| 185 | + if request.form['text']: |
| 186 | + db = get_db() |
| 187 | + db.execute('''insert into message (author_id, text, pub_date) |
| 188 | + values (?, ?, ?)''', (session['user_id'], request.form['text'], |
| 189 | + int(time.time()))) |
| 190 | + db.commit() |
| 191 | + flash('Your message was recorded') |
| 192 | + return redirect(url_for('timeline')) |
| 193 | + |
| 194 | + |
| 195 | +@app.route('/login', methods=['GET', 'POST']) |
| 196 | +def login(): |
| 197 | + """Logs the user in.""" |
| 198 | + if g.user: |
| 199 | + return redirect(url_for('timeline')) |
| 200 | + error = None |
| 201 | + if request.method == 'POST': |
| 202 | + user = query_db('''select * from user where |
| 203 | + username = ?''', [request.form['username']], one=True) |
| 204 | + if user is None: |
| 205 | + error = 'Invalid username' |
| 206 | + elif not check_password_hash(user['pw_hash'], |
| 207 | + request.form['password']): |
| 208 | + error = 'Invalid password' |
| 209 | + else: |
| 210 | + flash('You were logged in') |
| 211 | + session['user_id'] = user['user_id'] |
| 212 | + return redirect(url_for('timeline')) |
| 213 | + return render_template('login.html', error=error) |
| 214 | + |
| 215 | + |
| 216 | +@app.route('/register', methods=['GET', 'POST']) |
| 217 | +def register(): |
| 218 | + """Registers the user.""" |
| 219 | + if g.user: |
| 220 | + return redirect(url_for('timeline')) |
| 221 | + error = None |
| 222 | + if request.method == 'POST': |
| 223 | + if not request.form['username']: |
| 224 | + error = 'You have to enter a username' |
| 225 | + elif not request.form['email'] or \ |
| 226 | + '@' not in request.form['email']: |
| 227 | + error = 'You have to enter a valid email address' |
| 228 | + elif not request.form['password']: |
| 229 | + error = 'You have to enter a password' |
| 230 | + elif request.form['password'] != request.form['password2']: |
| 231 | + error = 'The two passwords do not match' |
| 232 | + elif get_user_id(request.form['username']) is not None: |
| 233 | + error = 'The username is already taken' |
| 234 | + else: |
| 235 | + db = get_db() |
| 236 | + db.execute('''insert into user ( |
| 237 | + username, email, pw_hash) values (?, ?, ?)''', |
| 238 | + [request.form['username'], request.form['email'], |
| 239 | + generate_password_hash(request.form['password'])]) |
| 240 | + db.commit() |
| 241 | + flash('You were successfully registered and can login now') |
| 242 | + return redirect(url_for('login')) |
| 243 | + return render_template('register.html', error=error) |
| 244 | + |
| 245 | + |
| 246 | +@app.route('/logout') |
| 247 | +def logout(): |
| 248 | + """Logs the user out.""" |
| 249 | + flash('You were logged out') |
| 250 | + session.pop('user_id', None) |
| 251 | + return redirect(url_for('public_timeline')) |
| 252 | + |
| 253 | + |
| 254 | +# add some filters to jinja |
| 255 | +app.jinja_env.filters['datetimeformat'] = format_datetime |
| 256 | +app.jinja_env.filters['gravatar'] = gravatar_url |
0 commit comments