├── tests ├── __init__.py ├── test_pending_email_model.py ├── test_comment_model.py ├── test_talk_model.py ├── test_user_model.py └── test_api.py ├── app ├── auth │ ├── __init__.py │ ├── forms.py │ └── routes.py ├── talks │ ├── __init__.py │ ├── forms.py │ └── routes.py ├── templates │ ├── talks │ │ ├── _talks.html │ │ ├── edit_talk.html │ │ ├── profile.html │ │ ├── _comments.html │ │ ├── moderate.html │ │ ├── index.html │ │ ├── _talk_header.html │ │ ├── user.html │ │ ├── talk.html │ │ └── _comment.html │ ├── auth │ │ └── login.html │ ├── email │ │ ├── notify.txt │ │ └── notify.html │ ├── _api_client.html │ └── base.html ├── api_1_0 │ ├── __init__.py │ ├── errors.py │ └── comments.py ├── static │ └── styles.css ├── __init__.py ├── emails.py └── models.py ├── requirements.txt ├── .gitignore ├── LICENSE ├── manage.py ├── config.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/auth/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | auth = Blueprint('auth', __name__) 4 | 5 | from . import routes 6 | -------------------------------------------------------------------------------- /app/talks/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | talks = Blueprint('talks', __name__) 4 | 5 | from . import routes 6 | 7 | -------------------------------------------------------------------------------- /app/templates/talks/_talks.html: -------------------------------------------------------------------------------- 1 |
Dear {{ name }},
2 |New comments have been posted to talk {{ talk.title }}. 3 |
Sincerely,
4 |The talks admin
5 | {% if unsubscribe %} 6 |PS: This email was sent to {{ email }}. To stop receiving notifications for this talk click here.
7 | {% endif %} 8 | 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | bleach==1.4 2 | blinker==1.3 3 | click==6.6 4 | coverage==3.7.1 5 | Flask==0.11 6 | Flask-Bootstrap==3.1.1.1 7 | Flask-Login==0.3.2 8 | Flask-Mail==0.9.0 9 | Flask-Moment==0.3.0 10 | Flask-PageDown==0.1.4 11 | Flask-Script==0.6.7 12 | Flask-SQLAlchemy==2.1 13 | Flask-WTF==0.9.4 14 | html5lib==0.999 15 | itsdangerous==0.24 16 | Jinja2==2.8 17 | Markdown==2.4 18 | MarkupSafe==0.23 19 | nose==1.3.1 20 | six==1.5.2 21 | SQLAlchemy==1.0.13 22 | Werkzeug==0.11.10 23 | WTForms==1.0.5 24 | -------------------------------------------------------------------------------- /app/auth/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import Form 2 | from wtforms import StringField, PasswordField, BooleanField, SubmitField 3 | from wtforms.validators import Required, Length, Email 4 | 5 | 6 | class LoginForm(Form): 7 | email = StringField('Email', validators=[Required(), Length(1, 64), 8 | Email()]) 9 | password = PasswordField('Password', validators=[Required()]) 10 | remember_me = BooleanField('Keep me logged in') 11 | submit = SubmitField('Log In') 12 | -------------------------------------------------------------------------------- /app/templates/talks/moderate.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_content %} 4 |11 | Presented by {{ talk.author.username }} at 12 | {% if talk.venue_url %} 13 | {{ talk.venue }} 14 | {% else %} 15 | {{ talk.venue }} 16 | {% endif %} 17 | on {{ moment(talk.date, local=True).format('LL') }}. 18 |
19 |2 | {% if comment.author %} 3 | {% if index %} 4 | #{{ index }} 5 | {% endif %} 6 | {{ comment.author.username }} 7 | {% else %} 8 | {% if talk.author == current_user or current_user.is_admin %} 9 | {% if not comment.approved %} 10 |
14 | 17 | 20 | {% endif %} 21 | {% endif %} 22 | {% if index %} 23 | #{{ index }} 24 | {% endif %} 25 | {{ comment.author_name }} 26 | {% if current_user.is_authenticated and (talk.author == current_user or current_user.is_admin) %} 27 | ({{ comment.author_email }}) 28 | {% endif %} 29 | {% endif %} 30 | commented {{ moment(comment.timestamp).fromNow() }}: 31 | 32 |text bold ' 23 | 'and italic
') 24 | 25 | def test_notification_list(self): 26 | db.create_all() 27 | u1 = User(email='john@example.com', username='john', password='cat') 28 | u2 = User(email='susan@example.com', username='susan', password='cat') 29 | t = Talk(title='t', description='d', author=u1) 30 | c1 = Comment(talk=t, body='c1', author_name='n1', 31 | author_email='e@e.com', approved=True) 32 | c2 = Comment(talk=t, body='c2', author_name='n2', 33 | author_email='e2@e2.com', approved=True, notify=False) 34 | c3 = Comment(talk=t, body='c3', author=u2, approved=True) 35 | c4 = Comment(talk=t, body='c4', author_name='n4', 36 | author_email='e4@e4.com', approved=False) 37 | c5 = Comment(talk=t, body='c5', author=u2, approved=True) 38 | c6 = Comment(talk=t, body='c6', author_name='n6', 39 | author_email='e6@e6.com', approved=True, notify=False) 40 | db.session.add_all([u1, u2, t, c1, c2, c3, c4, c5]) 41 | db.session.commit() 42 | email_list = c4.notification_list() 43 | self.assertTrue(('e@e.com', 'n1') in email_list) 44 | self.assertFalse(('e2@e2.com', 'n2') in email_list) # notify=False 45 | self.assertTrue(('susan@example.com', 'susan') in email_list) 46 | self.assertFalse(('e4@e4.com', 'n4') in email_list) # comment author 47 | self.assertFalse(('e6@e6.com', 'n6') in email_list) 48 | email_list = c5.notification_list() 49 | self.assertFalse(('john@example.com', 'john') in email_list) 50 | self.assertTrue(('e4@e4.com', 'n4') in email_list) # comment author 51 | -------------------------------------------------------------------------------- /tests/test_talk_model.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from app import create_app, db 3 | from app.models import User, Talk, Comment 4 | 5 | 6 | class CommentModelTestCase(unittest.TestCase): 7 | def setUp(self): 8 | self.app = create_app('testing') 9 | self.app_context = self.app.app_context() 10 | self.app_context.push() 11 | #db.create_all() 12 | 13 | def tearDown(self): 14 | db.session.remove() 15 | db.drop_all() 16 | self.app_context.pop() 17 | 18 | def test_approved(self): 19 | db.create_all() 20 | u = User(email='john@example.com', username='john', password='cat') 21 | t = Talk(title='t', description='d', author=u) 22 | c1 = Comment(talk=t, body='c1', author_name='n', 23 | author_email='e@e.com', approved=True) 24 | c2 = Comment(talk=t, body='c2', author_name='n', 25 | author_email='e@e.com', approved=False) 26 | db.session.add_all([u, t, c1, c2]) 27 | db.session.commit() 28 | approved = t.approved_comments().all() 29 | self.assertTrue(len(approved) == 1) 30 | self.assertTrue(approved[0] == c1) 31 | 32 | def test_unsubscribe(self): 33 | db.create_all() 34 | u = User(email='john@example.com', username='john', password='cat') 35 | t = Talk(title='t', description='d', author=u) 36 | c1 = Comment(talk=t, body='c1', author_name='n', 37 | author_email='e@e.com', approved=True, notify=True) 38 | c2 = Comment(talk=t, body='c2', author_name='n', 39 | author_email='e2@e2.com', approved=False, notify=True) 40 | c3 = Comment(talk=t, body='c3', author_name='n', 41 | author_email='e@e.com', approved=False, notify=True) 42 | db.session.add_all([u, t, c1, c2, c3]) 43 | db.session.commit() 44 | token = t.get_unsubscribe_token(u'e@e.com') 45 | Talk.unsubscribe_user(token) 46 | comments = t.comments.all() 47 | for comment in comments: 48 | if comment.author_email == 'e@e.com': 49 | self.assertTrue(comment.notify == False) 50 | else: 51 | self.assertTrue(comment.notify == True) 52 | 53 | def test_bad_unsubscribe_token(self): 54 | talk, email = Talk.unsubscribe_user('an invalid token') 55 | self.assertIsNone(talk) 56 | self.assertIsNone(email) 57 | u = User(email='john@example.com', username='john', password='cat') 58 | t = Talk(title='t', description='d', author=u) 59 | token = t.get_unsubscribe_token('e@e.com') 60 | talk, email = Talk.unsubscribe_user(token) 61 | self.assertIsNone(talk) 62 | self.assertIsNone(email) 63 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_bootstrap import Bootstrap 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_login import LoginManager 5 | from flask_moment import Moment 6 | from flask_pagedown import PageDown 7 | from flask_mail import Mail 8 | from config import config 9 | 10 | bootstrap = Bootstrap() 11 | db = SQLAlchemy() 12 | moment = Moment() 13 | pagedown = PageDown() 14 | mail = Mail() 15 | 16 | login_manager = LoginManager() 17 | login_manager.login_view = 'auth.login' 18 | 19 | 20 | def create_app(config_name): 21 | app = Flask(__name__) 22 | app.config.from_object(config[config_name]) 23 | 24 | if not app.config['DEBUG'] and not app.config['TESTING']: 25 | # configure logging for production 26 | 27 | # email errors to the administrators 28 | if app.config.get('MAIL_ERROR_RECIPIENT') is not None: 29 | import logging 30 | from logging.handlers import SMTPHandler 31 | credentials = None 32 | secure = None 33 | if app.config.get('MAIL_USERNAME') is not None: 34 | credentials = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD']) 35 | if app.config['MAIL_USE_TLS'] is not None: 36 | secure = () 37 | mail_handler = SMTPHandler( 38 | mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']), 39 | fromaddr=app.config['MAIL_DEFAULT_SENDER'], 40 | toaddrs=[app.config['MAIL_ERROR_RECIPIENT']], 41 | subject='[Talks] Application Error', 42 | credentials=credentials, 43 | secure=secure) 44 | mail_handler.setLevel(logging.ERROR) 45 | app.logger.addHandler(mail_handler) 46 | 47 | # send standard logs to syslog 48 | import logging 49 | from logging.handlers import SysLogHandler 50 | syslog_handler = SysLogHandler() 51 | syslog_handler.setLevel(logging.WARNING) 52 | app.logger.addHandler(syslog_handler) 53 | 54 | bootstrap.init_app(app) 55 | db.init_app(app) 56 | moment.init_app(app) 57 | pagedown.init_app(app) 58 | mail.init_app(app) 59 | login_manager.init_app(app) 60 | 61 | from .talks import talks as talks_blueprint 62 | app.register_blueprint(talks_blueprint) 63 | 64 | from .auth import auth as auth_blueprint 65 | app.register_blueprint(auth_blueprint, url_prefix='/auth') 66 | 67 | from .api_1_0 import api as api_blueprint 68 | app.register_blueprint(api_blueprint, url_prefix='/api/1.0') 69 | 70 | from app.emails import start_email_thread 71 | @app.before_first_request 72 | def before_first_request(): 73 | start_email_thread() 74 | 75 | return app 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flask By Example 2 | ================ 3 | 4 | Code for my PyCon 2014 tutorial "Flask By Example". 5 | 6 | Pre-requisites 7 | -------------- 8 | 9 | - Some previous Python coding experience 10 | - Basic knowledge of HTML and CSS 11 | - A bit of JavaScript will definitely not hurt 12 | 13 | Requirements 14 | ------------ 15 | 16 | - Python 2.7 or 3.3+ on any supported OS (even Windows!) 17 | - virtualenv (or pyvenv if you are using Python 3.4) 18 | - git 19 | - Network connection (only to install the application) 20 | 21 | Setup 22 | ----- 23 | 24 | The tutorial does not have a hands-on portion. For that reason it is not required that you come with your laptop with everything installed; you can come unplugged and just listen and learn if you like. 25 | 26 | However, you will want to work with this application once you learn all about it in class, so I recommend that you try to install it ahead of time and have a chance to talk to me if you run into problems. 27 | 28 | Please make sure your computer meets all the requirements listed above before you begin. Below are step-by-step installation instructions: 29 | 30 | **Step 1**: Clone the git repository 31 | 32 | $ git clone https://github.com/miguelgrinberg/flask-pycon2014.git 33 | $ cd flask-pycon2014 34 | 35 | **Step 2**: Create a virtual environment. 36 | 37 | For Linux, OSX or any other platform that uses *bash* as command prompt (including Cygwin on Windows): 38 | 39 | $ virtualenv venv 40 | $ source venv/bin/activate 41 | (venv) $ pip install -r requirements.txt 42 | 43 | For Windows users working on the standard command prompt: 44 | 45 | > virtualenv venv 46 | > venv\scripts\activate 47 | (venv) > pip install -r requirements.txt 48 | 49 | **Step 3**: Create an administrator user 50 | 51 | (venv) $ python manage.py adduser --admin