├── 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 | {{ user.nickname }} says: {{ post.body }} |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
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 |
17 |
18 | {% with messages = get_flashed_messages() %}
19 | {% if messages %}
20 |
21 | {% for message in messages %}
22 | - {{ message }}
23 | {% endfor %}
24 |
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 |
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 | |
21 |
22 |
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 |
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))
--------------------------------------------------------------------------------