-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
429 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,266 @@ | ||
import datetime | ||
import operator | ||
|
||
from flask import abort | ||
from flask import flash | ||
from flask import Flask | ||
from flask import redirect | ||
from flask import render_template | ||
from flask import request | ||
from flask import session | ||
from flask import url_for | ||
from functools import wraps | ||
from hashlib import md5 | ||
from walrus import * | ||
|
||
# Configure our app's settings. | ||
DEBUG = True | ||
SECRET_KEY = 'hin6bab8ge25*r=x&+5$0kn=-#log$pt^#@vrqjld!^2ci@g*b' | ||
|
||
# Create a flask application - this `app` object will be used to handle | ||
# inbound requests, routing them to the proper 'view' functions, etc. | ||
app = Flask(__name__) | ||
app.config.from_object(__name__) | ||
|
||
# Create a walrus database instance - our models will use this database to | ||
# persist information. | ||
database = Database() | ||
|
||
# Model definitions - the standard "pattern" is to define a base model class | ||
# that specifies which database to use. Then, any subclasses will automatically | ||
# use the correct storage. | ||
class BaseModel(Model): | ||
database = database | ||
namespace = 'twitter' | ||
|
||
# Model classes specify fields declaratively, like django. | ||
class User(BaseModel): | ||
username = TextField(primary_key=True) | ||
password = TextField(index=True) | ||
email = TextField() | ||
|
||
followers = ZSetField() | ||
following = ZSetField() | ||
|
||
def get_followers(self): | ||
# Because all users are added to the `followers` sorted-set with the | ||
# same score, when retrieved they will be sorted by key (username). | ||
return [User.load(username) for username in self.followers] | ||
|
||
def get_following(self): | ||
# Because all users are added to the `following` sorted-set with the | ||
# same score, when retrieved they will be sorted by key (username). | ||
return [User.load(username) for username in self.following] | ||
|
||
def is_following(self, user): | ||
# We can use Pythonic operators when working with Walrus containers. | ||
return user.username in self.following | ||
|
||
def gravatar_url(self, size=80): | ||
return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ | ||
(md5(self.email.strip().lower().encode('utf-8')).hexdigest(), size) | ||
|
||
|
||
# Simple model with a one-to-many relationship: one user has 0..n messages. | ||
# A user is associated with a message via the `username` field. | ||
class Message(BaseModel): | ||
username = TextField(index=True) | ||
content = TextField() | ||
timestamp = DateTimeField(default=datetime.datetime.now) | ||
|
||
def get_user(self): | ||
return User.load(self.username) | ||
|
||
|
||
# Flask provides a `session` object, which allows us to store information | ||
# across requests (stored by default in a secure cookie). This function allows | ||
# us to mark a user as being logged-in by setting some values in the session: | ||
def auth_user(user): | ||
session['logged_in'] = True | ||
session['username'] = user.username | ||
flash('You are logged in as %s' % (user.username)) | ||
|
||
# Get the currently logged-in user, or return `None`. | ||
def get_current_user(): | ||
if session.get('logged_in'): | ||
try: | ||
return User.load(session['username']) | ||
except KeyError: | ||
session.pop('logged_in') | ||
|
||
# View decorator which indicates that the requesting user must be authenticated | ||
# before they can access the wrapped view. The decorator checks the session to | ||
# see if they're logged in, and if not redirects them to the login view. | ||
def login_required(f): | ||
@wraps(f) | ||
def inner(*args, **kwargs): | ||
if not session.get('logged_in'): | ||
return redirect(url_for('login')) | ||
return f(*args, **kwargs) | ||
return inner | ||
|
||
# Retrieve an object by primary key. If the object does not exist, return a | ||
# 404 not found. | ||
def get_object_or_404(model, pk): | ||
try: | ||
return model.load(pk) | ||
except ValueError: | ||
abort(404) | ||
|
||
# Custom template filter: Flask allows you to define these functions and then | ||
# they are accessible in the template. This one returns a boolean whether the | ||
# given user is following another user. | ||
@app.template_filter('is_following') | ||
def is_following(from_user, to_user): | ||
return from_user.is_following(to_user) | ||
|
||
# Views: these are the actual mappings of url to view function. | ||
@app.route('/') | ||
def homepage(): | ||
# Depending on whether the requesting user is logged in or not, show them | ||
# either the public timeline or their own private timeline. | ||
if session.get('logged_in'): | ||
return private_timeline() | ||
else: | ||
return public_timeline() | ||
|
||
@app.route('/private/') | ||
def private_timeline(): | ||
# The private timeline is a bit interesting as it shows how to create a | ||
# query dynamically. We are taking all the users the current user follows | ||
# and basically performing a big set union on message objects. Matching | ||
# messages are then sorted newest to oldest. | ||
user = get_current_user() | ||
if user.following: | ||
query = reduce(operator.or_, [ | ||
Message.username == username | ||
for username, _ in user.following | ||
]) | ||
messages = Message.query(query, order_by=Message.timestamp.desc()) | ||
else: | ||
messages = [] | ||
return render_template('private_messages.html', message_list=messages) | ||
|
||
@app.route('/public/') | ||
def public_timeline(): | ||
# Display all messages, newest to oldest. | ||
messages = Message.query(order_by=Message.timestamp.desc()) | ||
return render_template('public_messages.html', message_list=messages) | ||
|
||
@app.route('/join/', methods=['GET', 'POST']) | ||
def join(): | ||
if request.method == 'POST' and request.form['username']: | ||
username = request.form['username'] | ||
try: | ||
User.load(username) | ||
except KeyError: | ||
user = User.create( | ||
username=username, | ||
password=md5(request.form['password']).hexdigest(), | ||
email=request.form['email']) | ||
auth_user(user) | ||
return redirect(url_for('homepage')) | ||
else: | ||
flash('That username is already taken') | ||
|
||
return render_template('join.html') | ||
|
||
@app.route('/login/', methods=['GET', 'POST']) | ||
def login(): | ||
if request.method == 'POST' and request.form['username']: | ||
try: | ||
user = User.get( | ||
(User.username == request.form['username']) & | ||
(User.password == md5(request.form['password']).hexdigest())) | ||
except ValueError: | ||
flash('The password entered is incorrect') | ||
else: | ||
auth_user(user) | ||
return redirect(url_for('homepage')) | ||
|
||
return render_template('login.html') | ||
|
||
@app.route('/logout/') | ||
def logout(): | ||
session.pop('logged_in', None) | ||
flash('You were logged out') | ||
return redirect(url_for('homepage')) | ||
|
||
@app.route('/following/') | ||
@login_required | ||
def following(): | ||
# Get the list of user objects the current user is following. | ||
user = get_current_user() | ||
return render_template('user_following', user_list=user.get_following()) | ||
|
||
@app.route('/followers/') | ||
@login_required | ||
def followers(): | ||
# Get the list of user objects the current user is followed by. | ||
user = get_current_user() | ||
return render_template('user_following', user_list=user.get_followers()) | ||
|
||
@app.route('/users/') | ||
def user_list(): | ||
# Display all users ordered by their username. | ||
users = User.query(order_by=User.username) | ||
return render_template('user_list.html', user_list=users) | ||
|
||
@app.route('/users/<username>/') | ||
def user_detail(username): | ||
# Using the "get_object_or_404" shortcut here to get a user with a valid | ||
# username or short-circuit and display a 404 if no user exists in the db. | ||
user = get_object_or_404(User, username) | ||
|
||
# Get all the users messages ordered newest-first. | ||
messages = Message.query( | ||
Message.username == user.username, | ||
order_by=Message.timestamp.desc()) | ||
return render_template( | ||
'user_detail.html', | ||
message_list=messages, | ||
user=user) | ||
|
||
@app.route('/users/<username>/follow/', methods=['POST']) | ||
@login_required | ||
def user_follow(username): | ||
current_user = get_current_user() | ||
user = get_object_or_404(User, username) | ||
current_user.following.add(user.username, 0) | ||
user.followers.add(current_user.username, 0) | ||
|
||
flash('You are following %s' % user.username) | ||
return redirect(url_for('user_detail', username=user.username)) | ||
|
||
@app.route('/users/<username>/unfollow/', methods=['POST']) | ||
@login_required | ||
def user_unfollow(username): | ||
current_user = get_current_user() | ||
user = get_object_or_404(User, username) | ||
current_user.following.remove(user.username) | ||
user.followers.remove(current_user.username) | ||
|
||
flash('You are no longer following %s' % user.username) | ||
return redirect(url_for('user_detail', username=user.username)) | ||
|
||
@app.route('/create/', methods=['GET', 'POST']) | ||
@login_required | ||
def create(): | ||
# Create a new message. | ||
user = get_current_user() | ||
if request.method == 'POST' and request.form['content']: | ||
message = Message.create( | ||
username=user.username, | ||
content=request.form['content']) | ||
flash('Your message has been created') | ||
return redirect(url_for('user_detail', username=user.username)) | ||
|
||
return render_template('create.html') | ||
|
||
@app.context_processor | ||
def _inject_user(): | ||
return {'current_user': get_current_user()} | ||
|
||
# allow running from the command line | ||
if __name__ == '__main__': | ||
app.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
flask | ||
redis | ||
walrus |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#!/usr/bin/env python | ||
|
||
import sys | ||
sys.path.insert(0, '../..') | ||
|
||
from app import app | ||
app.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
body { font-family: sans-serif; background: #eee; } | ||
a, h1, h2 { color: #377BA8; } | ||
h1, h2 { font-family: 'Georgia', serif; margin: 0; } | ||
h1 { border-bottom: 2px solid #eee; } | ||
h2 { font-size: 1.2em; } | ||
|
||
.page { margin: 2em auto; width: 35em; border: 5px solid #ccc; | ||
padding: 0.8em; background: white; } | ||
.page ul { list-style-type: none; } | ||
.page li { clear: both; } | ||
.metanav { text-align: right; font-size: 0.8em; padding: 0.3em; | ||
margin-bottom: 1em; background: #fafafa; } | ||
.flash { background: #CEE5F5; padding: 0.5em; | ||
border: 1px solid #AACBE2; } | ||
.avatar { display: block; float: left; margin: 0 10px 0 0; } | ||
.message-content { min-height: 80px; } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{% extends "layout.html" %} | ||
{% block body %} | ||
<h2>Create</h2> | ||
<form action="{{ url_for('create') }}" method=post> | ||
<dl> | ||
<dt>Message:</dt> | ||
<dd><textarea name="content"></textarea></dd> | ||
<dd><input type="submit" value="Create" /></dd> | ||
</dl> | ||
</form> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{% extends "layout.html" %} | ||
{% block body %} | ||
<h2>Home</h2> | ||
<p>Welcome to the site!</p> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{% set msg_user = message.get_user() %} | ||
<a class="avatar" href="{{ url_for('user_detail', username=message.username) }}"><img src="{{ msg_user.gravatar_url() }}" /></a> | ||
<p class="message-content">{{ message.content|urlize }}</p> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
{% extends "layout.html" %} | ||
{% block body %} | ||
<h2>Join</h2> | ||
<form action="{{ url_for('join') }}" method="post"> | ||
<dl> | ||
<dt>Username:</dt> | ||
<dd><input type="text" name="username"></dd> | ||
<dt>Password:</dt> | ||
<dd><input type="password" name="password"></dd> | ||
<dt>Email:</dt> | ||
<dd><input type="text" name="email"> | ||
<p><small>(used for gravatar)</small></p> | ||
</dd> | ||
<dd><input type="submit" value="Join"> | ||
</dl> | ||
</form> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
<!doctype html> | ||
<title>Twalrus</title> | ||
<link rel=stylesheet type=text/css href="{{ url_for('static', filename='style.css') }}"> | ||
<div class=page> | ||
<h1><a href="{{ url_for('homepage') }}">Twalrus</a></h1> | ||
<div class=metanav> | ||
{% if not session.logged_in %} | ||
<a href="{{ url_for('login') }}">log in</a> | ||
<a href="{{ url_for('join') }}">join</a> | ||
{% else %} | ||
<a href="{{ url_for('public_timeline') }}">public timeline</a> | ||
<a href="{{ url_for('create') }}">create</a> | ||
<a href="{{ url_for('logout') }}">log out</a> | ||
{% endif %} | ||
</div> | ||
{% for message in get_flashed_messages() %} | ||
<div class=flash>{{ message }}</div> | ||
{% endfor %} | ||
{% block body %}{% endblock %} | ||
</div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
{% extends "layout.html" %} | ||
{% block body %} | ||
<h2>Login</h2> | ||
{% if error %}<p class=error><strong>Error:</strong> {{ error }}{% endif %} | ||
<form action="{{ url_for('login') }}" method=post> | ||
<dl> | ||
<dt>Username: | ||
<dd><input type=text name=username> | ||
<dt>Password: | ||
<dd><input type=password name=password> | ||
<dd><input type=submit value=Login> | ||
</dl> | ||
</form> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{% extends "layout.html" %} | ||
{% block body %} | ||
<h2>Private Timeline</h2> | ||
<ul> | ||
{% for message in message_list %} | ||
<li>{% include "includes/message.html" %}</li> | ||
{% endfor %} | ||
</ul> | ||
{% endblock %} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{% extends "layout.html" %} | ||
{% block body %} | ||
<h2>Public Timeline</h2> | ||
<ul> | ||
{% for message in message_list %} | ||
<li>{% include "includes/message.html" %}</li> | ||
{% endfor %} | ||
</ul> | ||
{% endblock %} |
Oops, something went wrong.