├── db_repository ├── __init__.py ├── versions │ ├── __init__.py │ ├── 004_migration.py │ ├── 002_migration.py │ ├── 001_migration.py │ └── 003_migration.py ├── README └── manage.py ├── app.db ├── test.db ├── run.py ├── runp.py ├── app ├── templates │ ├── 404.html │ ├── 500.html │ ├── post.html │ ├── index.html │ ├── edit.html │ ├── base.html │ ├── user.html │ └── login.html ├── forms.py ├── __init__.py ├── models.py ├── tests.py └── views.py ├── db_upgrade.py ├── db_downgrade.py ├── db_create.py ├── .gitignore ├── README.md ├── config.py ├── db_migrate.py └── tests.py /db_repository/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /db_repository/versions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karan/Flask-Tutorial/master/app.db -------------------------------------------------------------------------------- /test.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karan/Flask-Tutorial/master/test.db -------------------------------------------------------------------------------- /db_repository/README: -------------------------------------------------------------------------------- 1 | This is a database migration repository. 2 | 3 | More information at 4 | http://code.google.com/p/sqlalchemy-migrate/ 5 | -------------------------------------------------------------------------------- /db_repository/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from migrate.versioning.shell import main 3 | 4 | if __name__ == '__main__': 5 | main() 6 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # this script runs the web server 4 | # file must be executable 5 | 6 | from app import app 7 | 8 | app.run(debug=True) -------------------------------------------------------------------------------- /runp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # this script runs the web server 4 | # file must be executable 5 | 6 | from app import app 7 | 8 | app.run(debug=False) -------------------------------------------------------------------------------- /app/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {%block content %} 4 |

Page Not Found

5 |

Back

6 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

Something went wrong!

5 |

Administrators have been notified. They'll fix it soon.

6 |

Go back

7 | {% endblock %} -------------------------------------------------------------------------------- /app/templates/post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

{{ user.nickname }} says:
{{ post.body }}

-------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 |

Hi, {{user.nickname}}!

6 | {% for post in posts %} 7 |

8 | {{post.author.nickname}} says: {{post.body}} 9 |

10 | {% endfor %} 11 | {% endblock %} -------------------------------------------------------------------------------- /db_upgrade.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from migrate.versioning import api 4 | from config import SQLALCHEMY_DATABASE_URI 5 | from config import SQLALCHEMY_MIGRATE_REPO 6 | api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 7 | print 'Current database version: ' + str(api.db_version( 8 | SQLALCHEMY_DATABASE_URI, 9 | SQLALCHEMY_MIGRATE_REPO) 10 | ) -------------------------------------------------------------------------------- /db_downgrade.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from migrate.versioning import api 4 | from config import SQLALCHEMY_DATABASE_URI 5 | from config import SQLALCHEMY_MIGRATE_REPO 6 | v = api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 7 | api.downgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, v - 1) 8 | print 'Current database version: ' + str(api.db_version( 9 | SQLALCHEMY_DATABASE_URI, 10 | SQLALCHEMY_MIGRATE_REPO) 11 | ) -------------------------------------------------------------------------------- /db_create.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from migrate.versioning import api 4 | from config import SQLALCHEMY_DATABASE_URI 5 | from config import SQLALCHEMY_MIGRATE_REPO 6 | from app import db 7 | import os.path 8 | db.create_all() 9 | if not os.path.exists(SQLALCHEMY_MIGRATE_REPO): 10 | api.create(SQLALCHEMY_MIGRATE_REPO, 'database repository') 11 | api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 12 | else: 13 | api.version_control(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO, 14 | api.version(SQLALCHEMY_MIGRATE_REPO)) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | __pycache__ 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | 30 | # Translations 31 | *.mo 32 | 33 | # Mr Developer 34 | .mr.developer.cfg 35 | .project 36 | .pydevproject 37 | 38 | # Komodo 39 | .komodotools/ 40 | *.komodoproject 41 | 42 | *.cfg 43 | .DS_Store 44 | tmp/* 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Following through http://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-i-hello-world 2 | 3 | These are some of the topics I will cover as we make progress with the app: 4 | 5 | User management, including managing logins, sessions, user roles, profiles and user avatars. 6 | Database management, including migration handling. 7 | Web form support, including field validation. 8 | Pagination of long lists of items. 9 | Full text search. 10 | Email notifications to users. 11 | HTML templates. 12 | Support for multiple languages. 13 | Caching and other performance optimizations. 14 | Debugging techniques for development and production servers. 15 | Installation on a production server. -------------------------------------------------------------------------------- /app/templates/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block content %} 4 |

Edit Your Profile

5 |
6 | {{ form.hidden_tag() }} 7 | 8 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
Your nickname: 11 | {{ form.nickname(size=24) }} 12 | {% for error in form.errors.nickname %} 13 |
[{{ error }}] 14 | {% endfor %} 15 |
About yourself: {{ form.about_me(cols=32, rows=4) }}
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /db_repository/versions/004_migration.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | 5 | from migrate.changeset import schema 6 | pre_meta = MetaData() 7 | post_meta = MetaData() 8 | followers = Table('followers', post_meta, 9 | Column('follower_id', Integer), 10 | Column('followed_id', Integer), 11 | ) 12 | 13 | 14 | def upgrade(migrate_engine): 15 | # Upgrade operations go here. Don't create your own engine; bind 16 | # migrate_engine to your metadata 17 | pre_meta.bind = migrate_engine 18 | post_meta.bind = migrate_engine 19 | post_meta.tables['followers'].create() 20 | 21 | 22 | def downgrade(migrate_engine): 23 | # Operations to reverse the above upgrade go here. 24 | pre_meta.bind = migrate_engine 25 | post_meta.bind = migrate_engine 26 | post_meta.tables['followers'].drop() 27 | -------------------------------------------------------------------------------- /app/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% if title %} 4 | {{title}} - microblog 5 | {% else %} 6 | microblog 7 | {% endif %} 8 | 9 | 10 |
Microblog: 11 | Home 12 | {% if g.user.is_authenticated() %} 13 | | {{ g.user.nickname }} 14 | | Logout 15 | {% endif %} 16 |
17 |
18 | {% with messages = get_flashed_messages() %} 19 | {% if messages %} 20 | 25 | {% endif %} 26 | {% endwith %} 27 | {% block content %}{% endblock %} 28 | 29 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | basedir = os.path.abspath(os.path.dirname(__file__)) 3 | 4 | CSRF_ENABLED = True 5 | SECRET_KEY = 'you-will-never-guess' 6 | 7 | OPENID_PROVIDERS = [ 8 | { 'name': 'Google', 'url': 'https://www.google.com/accounts/o8/id' }, 9 | { 'name': 'Yahoo', 'url': 'https://me.yahoo.com' }, 10 | { 'name': 'AOL', 'url': 'http://openid.aol.com/' }, 11 | { 'name': 'Flickr', 'url': 'http://www.flickr.com/' }, 12 | { 'name': 'MyOpenID', 'url': 'https://www.myopenid.com' }] 13 | 14 | SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db') 15 | SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository') 16 | 17 | # mail server settings 18 | MAIL_SERVER = 'localhost' 19 | MAIL_PORT = 25 20 | MAIL_USERNAME = None 21 | MAIL_PASSWORD = None 22 | 23 | # admin list 24 | ADMINS = ['karan@goel.im'] -------------------------------------------------------------------------------- /db_repository/versions/002_migration.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | 5 | from migrate.changeset import schema 6 | pre_meta = MetaData() 7 | post_meta = MetaData() 8 | post = Table('post', post_meta, 9 | Column('id', Integer, primary_key=True, nullable=False), 10 | Column('body', String(length=140)), 11 | Column('timestamp', DateTime), 12 | Column('user_id', Integer), 13 | ) 14 | 15 | 16 | def upgrade(migrate_engine): 17 | # Upgrade operations go here. Don't create your own engine; bind 18 | # migrate_engine to your metadata 19 | pre_meta.bind = migrate_engine 20 | post_meta.bind = migrate_engine 21 | post_meta.tables['post'].create() 22 | 23 | 24 | def downgrade(migrate_engine): 25 | # Operations to reverse the above upgrade go here. 26 | pre_meta.bind = migrate_engine 27 | post_meta.bind = migrate_engine 28 | post_meta.tables['post'].drop() 29 | -------------------------------------------------------------------------------- /db_repository/versions/001_migration.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | 5 | from migrate.changeset import schema 6 | pre_meta = MetaData() 7 | post_meta = MetaData() 8 | user = Table('user', post_meta, 9 | Column('id', Integer, primary_key=True, nullable=False), 10 | Column('nickname', String(length=64)), 11 | Column('email', String(length=120)), 12 | Column('role', SmallInteger, default=ColumnDefault(0)), 13 | ) 14 | 15 | 16 | def upgrade(migrate_engine): 17 | # Upgrade operations go here. Don't create your own engine; bind 18 | # migrate_engine to your metadata 19 | pre_meta.bind = migrate_engine 20 | post_meta.bind = migrate_engine 21 | post_meta.tables['user'].create() 22 | 23 | 24 | def downgrade(migrate_engine): 25 | # Operations to reverse the above upgrade go here. 26 | pre_meta.bind = migrate_engine 27 | post_meta.bind = migrate_engine 28 | post_meta.tables['user'].drop() 29 | -------------------------------------------------------------------------------- /app/templates/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | 6 | 7 | 21 | 22 |
8 |

User: {{user.nickname}}

9 | {% if user.about_me %}

{{user.about_me}}

{% endif %} 10 | {% if user.last_seen %}

Last seen on: {{user.last_seen}}

{% endif %} 11 |

{{ user.followers.count() }} followers | 12 | {% if user.id == g.user.id %} 13 | Edit 14 | {% elif not g.user.if_following(user) %} 15 | Follow 16 | {% else %} 17 | Unfollow 18 | {% endif %} 19 |

20 |
23 |
24 | {% for post in posts %} 25 | {% include 'post.html' %} 26 | {% endfor %} 27 | {% endblock %} -------------------------------------------------------------------------------- /db_migrate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import imp 4 | from migrate.versioning import api 5 | from app import db 6 | from config import SQLALCHEMY_DATABASE_URI 7 | from config import SQLALCHEMY_MIGRATE_REPO 8 | migration = SQLALCHEMY_MIGRATE_REPO + '/versions/%03d_migration.py' % ( 9 | api.db_version(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) + 1 10 | ) 11 | tmp_module = imp.new_module('old_model') 12 | old_model = api.create_model(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 13 | exec old_model in tmp_module.__dict__ 14 | script = api.make_update_script_for_model(SQLALCHEMY_DATABASE_URI, 15 | SQLALCHEMY_MIGRATE_REPO, 16 | tmp_module.meta, db.metadata) 17 | open(migration, "wt").write(script) 18 | api.upgrade(SQLALCHEMY_DATABASE_URI, SQLALCHEMY_MIGRATE_REPO) 19 | print 'New migration saved as ' + migration 20 | print 'Current database version: ' + str(api.db_version( 21 | SQLALCHEMY_DATABASE_URI, 22 | SQLALCHEMY_MIGRATE_REPO) 23 | ) -------------------------------------------------------------------------------- /app/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "base.html" %} 3 | 4 | {% block content %} 5 | 18 |

Sign In

19 |
20 | {{form.hidden_tag()}} 21 |

22 | Please enter your OpenID, or select one of the providers below:
23 | {{form.openid(size=80)}} 24 | {% for error in form.errors.openid %} 25 | [{{error}}] 26 | {% endfor %}
27 | |{% for pr in providers %} 28 | {{pr.name}} | 29 | {% endfor %} 30 |

31 |

{{form.remember_me}} Remember Me

32 |

33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /app/forms.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from flask.ext.wtf import Form 4 | from wtforms import TextField, BooleanField, TextAreaField 5 | from wtforms.validators import Required, Length 6 | 7 | from app.models import User 8 | 9 | class LoginForm(Form): 10 | openid = TextField('openid', validators=[Required()]) 11 | remember_me = BooleanField('remember_me', default=False) 12 | 13 | class EditForm(Form): 14 | nickname = TextField('nickname', validators=[Required()]) 15 | about_me = TextAreaField('about_me', validators=[Length(min=0, max=140)]) 16 | 17 | def __init__(self, original_nickname, *args, **kwargs): 18 | Form.__init__(self, *args, **kwargs) 19 | self.original_nickname = original_nickname 20 | 21 | def validate(self): 22 | if not Form.validate(self): 23 | return False 24 | if self.nickname.data == self.original_nickname: 25 | return True 26 | user = User.query.filter_by(nickname=self.nickname.data).first() 27 | if user != None: 28 | self.nickname.errors.append('This nickname is already in use. Please choose another one.') 29 | return False 30 | return True -------------------------------------------------------------------------------- /db_repository/versions/003_migration.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import * 2 | from migrate import * 3 | 4 | 5 | from migrate.changeset import schema 6 | pre_meta = MetaData() 7 | post_meta = MetaData() 8 | user = Table('user', post_meta, 9 | Column('id', Integer, primary_key=True, nullable=False), 10 | Column('nickname', String(length=64)), 11 | Column('email', String(length=120)), 12 | Column('role', SmallInteger, default=ColumnDefault(0)), 13 | Column('about_me', String(length=140)), 14 | Column('last_seen', DateTime), 15 | ) 16 | 17 | 18 | def upgrade(migrate_engine): 19 | # Upgrade operations go here. Don't create your own engine; bind 20 | # migrate_engine to your metadata 21 | pre_meta.bind = migrate_engine 22 | post_meta.bind = migrate_engine 23 | post_meta.tables['user'].columns['about_me'].create() 24 | post_meta.tables['user'].columns['last_seen'].create() 25 | 26 | 27 | def downgrade(migrate_engine): 28 | # Operations to reverse the above upgrade go here. 29 | pre_meta.bind = migrate_engine 30 | post_meta.bind = migrate_engine 31 | post_meta.tables['user'].columns['about_me'].drop() 32 | post_meta.tables['user'].columns['last_seen'].drop() 33 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask 3 | from flask.ext.sqlalchemy import SQLAlchemy 4 | from flask.ext.login import LoginManager 5 | from flask.ext.openid import OpenID 6 | from config import basedir, ADMINS, MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD 7 | 8 | app = Flask(__name__) 9 | app.config.from_object('config') 10 | db = SQLAlchemy(app) 11 | lm = LoginManager() 12 | lm.init_app(app) 13 | lm.login_view = 'login' 14 | oid = OpenID(app, os.path.join(basedir, 'tmp')) 15 | 16 | from app import views, models 17 | 18 | if not app.debug: 19 | import logging 20 | from logging.handlers import SMTPHandler 21 | credentials = None 22 | if MAIL_USERNAME or MAIL_PASSWORD: 23 | credentials = (MAIL_USERNAME, MAIL_PASSWORD) 24 | mail_handler = SMTPHandler((MAIL_SERVER, MAIL_PORT), 'no-reply@' + MAIL_SERVER, ADMINS, 25 | 'microblog failure', credentials) 26 | mail_handler.setLevel(logging.ERROR) 27 | app.logger.addHandler(mail_handler) 28 | 29 | if not app.debug: 30 | import logging 31 | from logging.handlers import RotatingFileHandler 32 | file_handler = RotatingFileHandler('tmp/log.log', 'a', 1 * 1024 * 1024, 10) 33 | file_handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)s]')) 34 | app.logger.setLevel(logging.INFO) 35 | file_handler.setLevel(logging.INFO) 36 | app.logger.addHandler(file_handler) 37 | app.logger.info('microblog startup') -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import unittest 5 | 6 | from config import basedir 7 | from app import app, db 8 | from app.models import User 9 | 10 | class TestCase(unittest.TestCase): 11 | def setUp(self): 12 | app.config['TESTING'] = True 13 | app.config['CSRF_ENABLED'] = False 14 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'test.db') 15 | self.app = app.test_client() 16 | db.create_all() 17 | 18 | def tearDown(self): 19 | db.session.remove() 20 | db.drop_all() 21 | 22 | def test_avatar(self): 23 | u = User(nickname='john', email='john@example.com') 24 | avatar = u.avatar(128) 25 | extencted = 'http://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6' 26 | assert avatar[0:len(expected)] == expected 27 | 28 | def test_make_unique_nickname(self): 29 | u = User(nickname='john', email='john@example.com') 30 | db.session.add(u) 31 | db.session.commit() 32 | nickname = User.make_unique_nickname('john') 33 | assert nickname != 'john' 34 | u = User(nickname=nickname, email='susan@example.com') 35 | db.session.add(u) 36 | db.session.commit() 37 | nickname2 = User.make_unique_nickname('john') 38 | assert nickname2 != 'john' 39 | assert nickname2 != nickname 40 | 41 | if __name__ == '__main__': 42 | unittest.main() -------------------------------------------------------------------------------- /app/models.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from hashlib import md5 4 | 5 | from app import db 6 | 7 | ROLE_USER = 0 8 | ROLE_ADMIN = 1 9 | 10 | followers = db.Table('followers', 11 | db.Column('follower_id', db.Integer, db.ForeignKey('user.id')), 12 | db.Column('followed_id', db.Integer, db.ForeignKey('user.id')) 13 | ) 14 | 15 | class User(db.Model): 16 | id = db.Column(db.Integer, primary_key = True) 17 | nickname = db.Column(db.String(64), unique = True) 18 | email = db.Column(db.String(120), index = True, unique = True) 19 | role = db.Column(db.SmallInteger, default = ROLE_USER) 20 | posts = db.relationship('Post', backref = 'author', lazy = 'dynamic') 21 | about_me = db.Column(db.String(140)) 22 | last_seen = db.Column(db.DateTime) 23 | followed = db.relationship('User', 24 | secondary = followers, 25 | primaryjoin = (followers.c.follower_id == id), 26 | secondaryjoin = (followers.c.followed_id == id), 27 | backref = db.backref('followers', lazy = 'dynamic'), 28 | lazy = 'dynamic') 29 | 30 | @staticmethod 31 | def make_unique_nickname(nickname): 32 | if User.query.filter_by(nickname = nickname).first() == None: 33 | return nickname 34 | version = 2 35 | while True: 36 | new_nickname = nickname + str(version) 37 | if User.query.filter_by(nickname = new_nickname).first() == None: 38 | break 39 | version += 1 40 | return new_nickname 41 | 42 | def avatar(self, size): 43 | avatar = 'http://www.gravatar.com/avatar/' + md5(self.email).hexdigest() + '?d=mm&s=' + str(size) 44 | return avatar 45 | 46 | def is_authenticated(self): 47 | return True 48 | 49 | def is_active(self): 50 | return True 51 | 52 | def is_anonymous(self): 53 | return False 54 | 55 | def get_id(self): 56 | return unicode(self.id) 57 | 58 | def follow(self, user): 59 | if not self.is_following(user): 60 | self.followed.append(user) 61 | return self 62 | 63 | def unfollow(self, user): 64 | if self.is_following(user): 65 | self.followed.remove(user) 66 | return self 67 | 68 | def is_following(self, user): 69 | return self.followed.filter(followers.c.followed_id == user.id).count() > 0 70 | 71 | def followed_posts(self): 72 | return Post.query.join( 73 | followers, (followers.c.followed_id == Post.user_id) 74 | ).filter( 75 | followers.c.follower_id == self.id 76 | ).order_by(Post.timestamp.desc()) 77 | 78 | def __repr__(self): 79 | return '' % (self.nickname) 80 | 81 | class Post(db.Model): 82 | id = db.Column(db.Integer, primary_key=True) 83 | body = db.Column(db.String(140)) 84 | timestamp = db.Column(db.DateTime) 85 | user_id = db.Column(db.Integer, db.ForeignKey('user.id')) 86 | 87 | def __repr__(self): 88 | return '' % (self.body) -------------------------------------------------------------------------------- /app/tests.py: -------------------------------------------------------------------------------- 1 | #!flask/bin/env python 2 | import os 3 | import unittest 4 | from datetime import datetime, timedelta 5 | 6 | from config import basedir 7 | from app import app, db 8 | from app.models import User, Post 9 | 10 | class TestCase(unittest.TestCase): 11 | def setUp(self): 12 | app.config['TESTING'] = True 13 | app.config['CSRF_ENABLED'] = False 14 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'test.db') 15 | db.create_all() 16 | 17 | def tearDown(self): 18 | db.session.remove() 19 | db.drop_all() 20 | 21 | def test_avatar(self): 22 | # create a user 23 | u = User(nickname = 'john', email = 'john@example.com') 24 | avatar = u.avatar(128) 25 | expected = 'http://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6' 26 | assert avatar[0:len(expected)] == expected 27 | 28 | def test_make_unique_nickname(self): 29 | # create a user and write it to the database 30 | u = User(nickname = 'john', email = 'john@example.com') 31 | db.session.add(u) 32 | db.session.commit() 33 | nickname = User.make_unique_nickname('john') 34 | assert nickname != 'john' 35 | # make another user with the new nickname 36 | u = User(nickname = nickname, email = 'susan@example.com') 37 | db.session.add(u) 38 | db.session.commit() 39 | nickname2 = User.make_unique_nickname('john') 40 | assert nickname2 != 'john' 41 | assert nickname2 != nickname 42 | 43 | def test_follow(self): 44 | u1 = User(nickname = 'john', email = 'john@example.com') 45 | u2 = User(nickname = 'susan', email = 'susan@example.com') 46 | db.session.add(u1) 47 | db.session.add(u2) 48 | db.session.commit() 49 | assert u1.unfollow(u2) == None 50 | u = u1.follow(u2) 51 | db.session.add(u) 52 | db.session.commit() 53 | assert u1.follow(u2) == None 54 | assert u1.is_following(u2) 55 | assert u1.followed.count() == 1 56 | assert u1.followed.first().nickname == 'susan' 57 | assert u2.followers.count() == 1 58 | assert u2.followers.first().nickname == 'john' 59 | u = u1.unfollow(u2) 60 | assert u != None 61 | db.session.add(u) 62 | db.session.commit() 63 | assert u1.is_following(u2) == False 64 | assert u1.followed.count() == 0 65 | assert u2.followers.count() == 0 66 | 67 | def test_follow_posts(self): 68 | # make four users 69 | u1 = User(nickname = 'john', email = 'john@example.com') 70 | u2 = User(nickname = 'susan', email = 'susan@example.com') 71 | u3 = User(nickname = 'mary', email = 'mary@example.com') 72 | u4 = User(nickname = 'david', email = 'david@example.com') 73 | db.session.add(u1) 74 | db.session.add(u2) 75 | db.session.add(u3) 76 | db.session.add(u4) 77 | # make four posts 78 | utcnow = datetime.utcnow() 79 | p1 = Post(body = "post from john", author = u1, timestamp = utcnow + timedelta(seconds = 1)) 80 | p2 = Post(body = "post from susan", author = u2, timestamp = utcnow + timedelta(seconds = 2)) 81 | p3 = Post(body = "post from mary", author = u3, timestamp = utcnow + timedelta(seconds = 3)) 82 | p4 = Post(body = "post from david", author = u4, timestamp = utcnow + timedelta(seconds = 4)) 83 | db.session.add(p1) 84 | db.session.add(p2) 85 | db.session.add(p3) 86 | db.session.add(p4) 87 | db.session.commit() 88 | # setup the followers 89 | u1.follow(u1) # john follows himself 90 | u1.follow(u2) # john follows susan 91 | u1.follow(u4) # john follows david 92 | u2.follow(u2) # susan follows herself 93 | u2.follow(u3) # susan follows mary 94 | u3.follow(u3) # mary follows herself 95 | u3.follow(u4) # mary follows david 96 | u4.follow(u4) # david follows himself 97 | db.session.add(u1) 98 | db.session.add(u2) 99 | db.session.add(u3) 100 | db.session.add(u4) 101 | db.session.commit() 102 | # check the followed posts of each user 103 | f1 = u1.followed_posts().all() 104 | f2 = u2.followed_posts().all() 105 | f3 = u3.followed_posts().all() 106 | f4 = u4.followed_posts().all() 107 | assert len(f1) == 3 108 | assert len(f2) == 2 109 | assert len(f3) == 2 110 | assert len(f4) == 1 111 | assert f1 == [p4, p2, p1] 112 | assert f2 == [p3, p2] 113 | assert f3 == [p4, p3] 114 | assert f4 == [p4] 115 | 116 | if __name__ == '__main__': 117 | unittest.main() -------------------------------------------------------------------------------- /app/views.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from flask import render_template, flash, redirect, session, url_for, request, g 4 | from flask.ext.login import login_user, logout_user, current_user, login_required 5 | from app import app, db, lm, oid 6 | from forms import LoginForm, EditForm 7 | from models import User, ROLE_USER, ROLE_ADMIN 8 | 9 | @lm.user_loader 10 | def load_user(id): 11 | return User.query.get(int(id)) 12 | 13 | @app.before_request 14 | def before_request(): 15 | g.user = current_user 16 | if g.user.is_authenticated(): 17 | g.user.last_seen = datetime.utcnow() 18 | db.session.add(g.user) 19 | db.session.commit() 20 | 21 | @app.route('/') 22 | @app.route('/index') 23 | @login_required 24 | def index(): 25 | user = g.user 26 | posts = [ 27 | { 28 | 'author': { 'nickname': 'John' }, 29 | 'body': 'Beautiful day in Portland!' 30 | }, 31 | { 32 | 'author': { 'nickname': 'Susan' }, 33 | 'body': 'The Avengers movie was so cool!' 34 | } 35 | ] 36 | return render_template('index.html', 37 | title = 'Home', 38 | user = user, 39 | posts = posts) 40 | 41 | @app.route('/login', methods=['GET', 'POST']) 42 | @oid.loginhandler 43 | def login(): 44 | if g.user is not None and g.user.is_authenticated(): 45 | return redirect(url_for('index')) 46 | form = LoginForm() 47 | if form.validate_on_submit(): 48 | session['remember_me'] = form.remember_me.data 49 | return oid.try_login(form.openid.data, ask_for = ['nickname', 'email']) 50 | return render_template('login.html', 51 | title = 'Sign In', 52 | form = form, 53 | providers = app.config['OPENID_PROVIDERS']) 54 | 55 | @app.route('/edit', methods = ['GET', 'POST']) 56 | @login_required 57 | def edit(): 58 | form = EditForm(g.user.nickname) 59 | if form.validate_on_submit(): 60 | g.user.nickname = form.nickname.data 61 | g.user.about_me = form.about_me.data 62 | db.session.add(g.user) 63 | db.session.commit() 64 | flash('Your changes have been saved.') 65 | return redirect(url_for('edit')) 66 | else: 67 | form.nickname.data = g.user.nickname 68 | form.about_me.data = g.user.about_me 69 | return render_template('edit.html', 70 | form = form) 71 | 72 | @oid.after_login 73 | def after_login(resp): 74 | if resp.email is None or resp.email == "": 75 | flash('Invalid login. Please try again.') 76 | return redirect(url_for('login')) 77 | user = User.query.filter_by(email = resp.email).first() 78 | if user is None: 79 | nickname = resp.nickname 80 | if nickname is None or nickname == "": 81 | nickname = resp.email.split('@')[0] 82 | nickname = User.make_unique_nickname(nickname) 83 | user = User(nickname = nickname, email = resp.email, role = ROLE_USER) 84 | db.session.add(user) 85 | db.session.commit() 86 | db.session.add(user.follow(user)) 87 | db.session.commit() 88 | remember_me = False 89 | if 'remember_me' in session: 90 | remember_me = session['remember_me'] 91 | session.pop('remember_me', None) 92 | login_user(user, remember = remember_me) 93 | return redirect(request.args.get('next') or url_for('index')) 94 | 95 | @app.route('/logout') 96 | def logout(): 97 | logout_user() 98 | return redirect(url_for('index')) 99 | 100 | @app.route('/user/') 101 | @login_required 102 | def user(nickname): 103 | user = User.query.filter_by(nickname=nickname).first() 104 | if user == None: 105 | flash('User ' + nickname + ' not found.') 106 | return redirect(url_for('index')) 107 | posts = [ 108 | {'author': user, 'body': 'Test post #1'}, 109 | {'author': user, 'body': 'Test post #2'} 110 | ] 111 | return render_template('user.html', 112 | user=user, 113 | posts=posts) 114 | 115 | @app.errorhandler(404) 116 | def internal_error(error): 117 | return render_template('404.html'), 404 118 | 119 | @app.errorhandler(500) 120 | def internal_error(error): 121 | db.session.rollback() 122 | return render_template('500.html'), 500 123 | 124 | 125 | @app.route('/follow/') 126 | def follow(nickname): 127 | user = User.query.filter_by(nickname = nickname).first() 128 | if user == None: 129 | flash('User ' + nickname + ' not found.') 130 | return redirect(url_for('index')) 131 | if user == g.user: 132 | flash('You can\'t follow yourself!') 133 | return redirect(url_for('user', nickname = nickname)) 134 | u = g.user.follow(user) 135 | if u is None: 136 | flash('Cannot follow ' + nickname + '.') 137 | return redirect(url_for('user', nickname = nickname)) 138 | db.session.add(u) 139 | db.session.commit() 140 | flash('You are now following ' + nickname + '!') 141 | return redirect(url_for('user', nickname = nickname)) 142 | 143 | @app.route('/unfollow/') 144 | def unfollow(nickname): 145 | user = User.query.filter_by(nickname = nickname).first() 146 | if user == None: 147 | flash('User ' + nickname + ' not found.') 148 | return redirect(url_for('index')) 149 | if user == g.user: 150 | flash('You can\'t unfollow yourself!') 151 | return redirect(url_for('user', nickname = nickname)) 152 | u = g.user.unfollow(user) 153 | if u is None: 154 | flash('Cannot unfollow ' + nickname + '.') 155 | return redirect(url_for('user', nickname = nickname)) 156 | db.session.add(u) 157 | db.session.commit() 158 | flash('You have stopped following ' + nickname + '.') 159 | return redirect(url_for('user', nickname = nickname)) --------------------------------------------------------------------------------