Full Code of miguelgrinberg/flasky for AI

master 3beedd640b91 cached
84 files
112.7 KB
28.8k tokens
217 symbols
1 requests
Download .txt
Repository: miguelgrinberg/flasky
Branch: master
Commit: 3beedd640b91
Files: 84
Total size: 112.7 KB

Directory structure:
gitextract_ml7yrowa/

├── .gitignore
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── app/
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── authentication.py
│   │   ├── comments.py
│   │   ├── decorators.py
│   │   ├── errors.py
│   │   ├── posts.py
│   │   └── users.py
│   ├── auth/
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   └── views.py
│   ├── decorators.py
│   ├── email.py
│   ├── exceptions.py
│   ├── fake.py
│   ├── main/
│   │   ├── __init__.py
│   │   ├── errors.py
│   │   ├── forms.py
│   │   └── views.py
│   ├── models.py
│   ├── static/
│   │   └── styles.css
│   └── templates/
│       ├── 403.html
│       ├── 404.html
│       ├── 500.html
│       ├── _comments.html
│       ├── _macros.html
│       ├── _posts.html
│       ├── auth/
│       │   ├── change_email.html
│       │   ├── change_password.html
│       │   ├── email/
│       │   │   ├── change_email.html
│       │   │   ├── change_email.txt
│       │   │   ├── confirm.html
│       │   │   ├── confirm.txt
│       │   │   ├── reset_password.html
│       │   │   └── reset_password.txt
│       │   ├── login.html
│       │   ├── register.html
│       │   ├── reset_password.html
│       │   └── unconfirmed.html
│       ├── base.html
│       ├── edit_post.html
│       ├── edit_profile.html
│       ├── followers.html
│       ├── index.html
│       ├── mail/
│       │   ├── new_user.html
│       │   └── new_user.txt
│       ├── moderate.html
│       ├── post.html
│       └── user.html
├── boot.sh
├── config.py
├── docker-compose.yml
├── flasky.py
├── migrations/
│   ├── README
│   ├── alembic.ini
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
│       ├── 190163627111_account_confirmation.py
│       ├── 198b0eebcf9_caching_of_avatar_hashes.py
│       ├── 1b966e7f4b9e_post_model.py
│       ├── 2356a38169ea_followers.py
│       ├── 288cd3dc5a8_rich_text_posts.py
│       ├── 38c4e85512a9_initial_migration.py
│       ├── 456a945560f6_login_support.py
│       ├── 51f5ccfba190_comments.py
│       ├── 56ed7d33de8d_user_roles.py
│       └── d66f086b258_user_information.py
├── requirements/
│   ├── common.txt
│   ├── dev.txt
│   ├── docker.txt
│   ├── heroku.txt
│   └── prod.txt
├── requirements.txt
└── tests/
    ├── __init__.py
    ├── test_api.py
    ├── test_basics.py
    ├── test_client.py
    ├── test_selenium.py
    └── test_user_model.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.py[cod]

# C extensions
*.so

# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
__pycache__

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox
nosetests.xml

# Translations
*.mo

# Mr Developer
.mr.developer.cfg
.project
.pydevproject

# SQLite databases
*.sqlite

# Virtual environment
venv

# Environment files
.env
.env-mysql


================================================
FILE: Dockerfile
================================================
FROM python:3.6-alpine

ENV FLASK_APP flasky.py
ENV FLASK_CONFIG production

RUN adduser -D flasky
USER flasky

WORKDIR /home/flasky

COPY requirements requirements
RUN python -m venv venv
RUN venv/bin/pip install -r requirements/docker.txt

COPY app app
COPY migrations migrations
COPY flasky.py config.py boot.sh ./

# run-time configuration
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2013 Miguel Grinberg

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.



================================================
FILE: Procfile
================================================
web: gunicorn flasky:app


================================================
FILE: README.md
================================================
Flasky
======

This repository contains the source code examples for the second edition of my O'Reilly book [Flask Web Development](http://www.flaskbook.com).

The commits and tags in this repository were carefully created to match the sequence in which concepts are presented in the book. Please read the section titled "How to Work with the Example Code" in the book's preface for instructions.

For Readers of the First Edition of the Book
--------------------------------------------

The code examples for the first edition of the book were moved to a different repository: [https://github.com/miguelgrinberg/flasky-first-edition](https://github.com/miguelgrinberg/flasky-first-edition).


================================================
FILE: app/__init__.py
================================================
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_pagedown import PageDown
from config import config

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()
pagedown = PageDown()

login_manager = LoginManager()
login_manager.login_view = 'auth.login'


def create_app(config_name):
    app = Flask(__name__)
    app.config.from_object(config[config_name])
    config[config_name].init_app(app)

    bootstrap.init_app(app)
    mail.init_app(app)
    moment.init_app(app)
    db.init_app(app)
    login_manager.init_app(app)
    pagedown.init_app(app)

    if app.config['SSL_REDIRECT']:
        from flask_sslify import SSLify
        sslify = SSLify(app)

    from .main import main as main_blueprint
    app.register_blueprint(main_blueprint)

    from .auth import auth as auth_blueprint
    app.register_blueprint(auth_blueprint, url_prefix='/auth')

    from .api import api as api_blueprint
    app.register_blueprint(api_blueprint, url_prefix='/api/v1')

    return app


================================================
FILE: app/api/__init__.py
================================================
from flask import Blueprint

api = Blueprint('api', __name__)

from . import authentication, posts, users, comments, errors


================================================
FILE: app/api/authentication.py
================================================
from flask import g, jsonify
from flask_httpauth import HTTPBasicAuth
from ..models import User
from . import api
from .errors import unauthorized, forbidden

auth = HTTPBasicAuth()


@auth.verify_password
def verify_password(email_or_token, password):
    if email_or_token == '':
        return False
    if password == '':
        g.current_user = User.verify_auth_token(email_or_token)
        g.token_used = True
        return g.current_user is not None
    user = User.query.filter_by(email=email_or_token.lower()).first()
    if not user:
        return False
    g.current_user = user
    g.token_used = False
    return user.verify_password(password)


@auth.error_handler
def auth_error():
    return unauthorized('Invalid credentials')


@api.before_request
@auth.login_required
def before_request():
    if not g.current_user.is_anonymous and \
            not g.current_user.confirmed:
        return forbidden('Unconfirmed account')


@api.route('/tokens/', methods=['POST'])
def get_token():
    if g.current_user.is_anonymous or g.token_used:
        return unauthorized('Invalid credentials')
    return jsonify({'token': g.current_user.generate_auth_token(
        expiration=3600), 'expiration': 3600})


================================================
FILE: app/api/comments.py
================================================
from flask import jsonify, request, g, url_for, current_app
from .. import db
from ..models import Post, Permission, Comment
from . import api
from .decorators import permission_required


@api.route('/comments/')
def get_comments():
    page = request.args.get('page', 1, type=int)
    pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
        page=page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
        error_out=False)
    comments = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_comments', page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_comments', page=page+1)
    return jsonify({
        'comments': [comment.to_json() for comment in comments],
        'prev': prev,
        'next': next,
        'count': pagination.total
    })


@api.route('/comments/<int:id>')
def get_comment(id):
    comment = Comment.query.get_or_404(id)
    return jsonify(comment.to_json())


@api.route('/posts/<int:id>/comments/')
def get_post_comments(id):
    post = Post.query.get_or_404(id)
    page = request.args.get('page', 1, type=int)
    pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
        page=page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
        error_out=False)
    comments = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_post_comments', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_post_comments', id=id, page=page+1)
    return jsonify({
        'comments': [comment.to_json() for comment in comments],
        'prev': prev,
        'next': next,
        'count': pagination.total
    })


@api.route('/posts/<int:id>/comments/', methods=['POST'])
@permission_required(Permission.COMMENT)
def new_post_comment(id):
    post = Post.query.get_or_404(id)
    comment = Comment.from_json(request.json)
    comment.author = g.current_user
    comment.post = post
    db.session.add(comment)
    db.session.commit()
    return jsonify(comment.to_json()), 201, \
        {'Location': url_for('api.get_comment', id=comment.id)}


================================================
FILE: app/api/decorators.py
================================================
from functools import wraps
from flask import g
from .errors import forbidden


def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not g.current_user.can(permission):
                return forbidden('Insufficient permissions')
            return f(*args, **kwargs)
        return decorated_function
    return decorator


================================================
FILE: app/api/errors.py
================================================
from flask import jsonify
from app.exceptions import ValidationError
from . import api


def bad_request(message):
    response = jsonify({'error': 'bad request', 'message': message})
    response.status_code = 400
    return response


def unauthorized(message):
    response = jsonify({'error': 'unauthorized', 'message': message})
    response.status_code = 401
    return response


def forbidden(message):
    response = jsonify({'error': 'forbidden', 'message': message})
    response.status_code = 403
    return response


@api.errorhandler(ValidationError)
def validation_error(e):
    return bad_request(e.args[0])


================================================
FILE: app/api/posts.py
================================================
from flask import jsonify, request, g, url_for, current_app
from .. import db
from ..models import Post, Permission
from . import api
from .decorators import permission_required
from .errors import forbidden


@api.route('/posts/')
def get_posts():
    page = request.args.get('page', 1, type=int)
    pagination = Post.query.paginate(
        page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
        error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_posts', page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_posts', page=page+1)
    return jsonify({
        'posts': [post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': pagination.total
    })


@api.route('/posts/<int:id>')
def get_post(id):
    post = Post.query.get_or_404(id)
    return jsonify(post.to_json())


@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE)
def new_post():
    post = Post.from_json(request.json)
    post.author = g.current_user
    db.session.add(post)
    db.session.commit()
    return jsonify(post.to_json()), 201, \
        {'Location': url_for('api.get_post', id=post.id)}


@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE)
def edit_post(id):
    post = Post.query.get_or_404(id)
    if g.current_user != post.author and \
            not g.current_user.can(Permission.ADMIN):
        return forbidden('Insufficient permissions')
    post.body = request.json.get('body', post.body)
    db.session.add(post)
    db.session.commit()
    return jsonify(post.to_json())


================================================
FILE: app/api/users.py
================================================
from flask import jsonify, request, current_app, url_for
from . import api
from ..models import User, Post


@api.route('/users/<int:id>')
def get_user(id):
    user = User.query.get_or_404(id)
    return jsonify(user.to_json())


@api.route('/users/<int:id>/posts/')
def get_user_posts(id):
    user = User.query.get_or_404(id)
    page = request.args.get('page', 1, type=int)
    pagination = user.posts.order_by(Post.timestamp.desc()).paginate(
        page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
        error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_user_posts', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_user_posts', id=id, page=page+1)
    return jsonify({
        'posts': [post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': pagination.total
    })


@api.route('/users/<int:id>/timeline/')
def get_user_followed_posts(id):
    user = User.query.get_or_404(id)
    page = request.args.get('page', 1, type=int)
    pagination = user.followed_posts.order_by(Post.timestamp.desc()).paginate(
        page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
        error_out=False)
    posts = pagination.items
    prev = None
    if pagination.has_prev:
        prev = url_for('api.get_user_followed_posts', id=id, page=page-1)
    next = None
    if pagination.has_next:
        next = url_for('api.get_user_followed_posts', id=id, page=page+1)
    return jsonify({
        'posts': [post.to_json() for post in posts],
        'prev': prev,
        'next': next,
        'count': pagination.total
    })


================================================
FILE: app/auth/__init__.py
================================================
from flask import Blueprint

auth = Blueprint('auth', __name__)

from . import views


================================================
FILE: app/auth/forms.py
================================================
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User


class LoginForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Length(1, 64),
                                             Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    remember_me = BooleanField('Keep me logged in')
    submit = SubmitField('Log In')


class RegistrationForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Length(1, 64),
                                             Email()])
    username = StringField('Username', validators=[
        DataRequired(), Length(1, 64),
        Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
               'Usernames must have only letters, numbers, dots or '
               'underscores')])
    password = PasswordField('Password', validators=[
        DataRequired(), EqualTo('password2', message='Passwords must match.')])
    password2 = PasswordField('Confirm password', validators=[DataRequired()])
    submit = SubmitField('Register')

    def validate_email(self, field):
        if User.query.filter_by(email=field.data.lower()).first():
            raise ValidationError('Email already registered.')

    def validate_username(self, field):
        if User.query.filter_by(username=field.data).first():
            raise ValidationError('Username already in use.')


class ChangePasswordForm(FlaskForm):
    old_password = PasswordField('Old password', validators=[DataRequired()])
    password = PasswordField('New password', validators=[
        DataRequired(), EqualTo('password2', message='Passwords must match.')])
    password2 = PasswordField('Confirm new password',
                              validators=[DataRequired()])
    submit = SubmitField('Update Password')


class PasswordResetRequestForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Length(1, 64),
                                             Email()])
    submit = SubmitField('Reset Password')


class PasswordResetForm(FlaskForm):
    password = PasswordField('New Password', validators=[
        DataRequired(), EqualTo('password2', message='Passwords must match')])
    password2 = PasswordField('Confirm password', validators=[DataRequired()])
    submit = SubmitField('Reset Password')


class ChangeEmailForm(FlaskForm):
    email = StringField('New Email', validators=[DataRequired(), Length(1, 64),
                                                 Email()])
    password = PasswordField('Password', validators=[DataRequired()])
    submit = SubmitField('Update Email Address')

    def validate_email(self, field):
        if User.query.filter_by(email=field.data.lower()).first():
            raise ValidationError('Email already registered.')


================================================
FILE: app/auth/views.py
================================================
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required, \
    current_user
from . import auth
from .. import db
from ..models import User
from ..email import send_email
from .forms import LoginForm, RegistrationForm, ChangePasswordForm,\
    PasswordResetRequestForm, PasswordResetForm, ChangeEmailForm


@auth.before_app_request
def before_request():
    if current_user.is_authenticated:
        current_user.ping()
        if not current_user.confirmed \
                and request.endpoint \
                and request.blueprint != 'auth' \
                and request.endpoint != 'static':
            return redirect(url_for('auth.unconfirmed'))


@auth.route('/unconfirmed')
def unconfirmed():
    if current_user.is_anonymous or current_user.confirmed:
        return redirect(url_for('main.index'))
    return render_template('auth/unconfirmed.html')


@auth.route('/login', methods=['GET', 'POST'])
def login():
    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data.lower()).first()
        if user is not None and user.verify_password(form.password.data):
            login_user(user, form.remember_me.data)
            next = request.args.get('next')
            if next is None or not next.startswith('/'):
                next = url_for('main.index')
            return redirect(next)
        flash('Invalid email or password.')
    return render_template('auth/login.html', form=form)


@auth.route('/logout')
@login_required
def logout():
    logout_user()
    flash('You have been logged out.')
    return redirect(url_for('main.index'))


@auth.route('/register', methods=['GET', 'POST'])
def register():
    form = RegistrationForm()
    if form.validate_on_submit():
        user = User(email=form.email.data.lower(),
                    username=form.username.data,
                    password=form.password.data)
        db.session.add(user)
        db.session.commit()
        token = user.generate_confirmation_token()
        send_email(user.email, 'Confirm Your Account',
                   'auth/email/confirm', user=user, token=token)
        flash('A confirmation email has been sent to you by email.')
        return redirect(url_for('auth.login'))
    return render_template('auth/register.html', form=form)


@auth.route('/confirm/<token>')
@login_required
def confirm(token):
    if current_user.confirmed:
        return redirect(url_for('main.index'))
    if current_user.confirm(token):
        db.session.commit()
        flash('You have confirmed your account. Thanks!')
    else:
        flash('The confirmation link is invalid or has expired.')
    return redirect(url_for('main.index'))


@auth.route('/confirm')
@login_required
def resend_confirmation():
    token = current_user.generate_confirmation_token()
    send_email(current_user.email, 'Confirm Your Account',
               'auth/email/confirm', user=current_user, token=token)
    flash('A new confirmation email has been sent to you by email.')
    return redirect(url_for('main.index'))


@auth.route('/change-password', methods=['GET', 'POST'])
@login_required
def change_password():
    form = ChangePasswordForm()
    if form.validate_on_submit():
        if current_user.verify_password(form.old_password.data):
            current_user.password = form.password.data
            db.session.add(current_user)
            db.session.commit()
            flash('Your password has been updated.')
            return redirect(url_for('main.index'))
        else:
            flash('Invalid password.')
    return render_template("auth/change_password.html", form=form)


@auth.route('/reset', methods=['GET', 'POST'])
def password_reset_request():
    if not current_user.is_anonymous:
        return redirect(url_for('main.index'))
    form = PasswordResetRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data.lower()).first()
        if user:
            token = user.generate_reset_token()
            send_email(user.email, 'Reset Your Password',
                       'auth/email/reset_password',
                       user=user, token=token)
        flash('An email with instructions to reset your password has been '
              'sent to you.')
        return redirect(url_for('auth.login'))
    return render_template('auth/reset_password.html', form=form)


@auth.route('/reset/<token>', methods=['GET', 'POST'])
def password_reset(token):
    if not current_user.is_anonymous:
        return redirect(url_for('main.index'))
    form = PasswordResetForm()
    if form.validate_on_submit():
        if User.reset_password(token, form.password.data):
            db.session.commit()
            flash('Your password has been updated.')
            return redirect(url_for('auth.login'))
        else:
            return redirect(url_for('main.index'))
    return render_template('auth/reset_password.html', form=form)


@auth.route('/change_email', methods=['GET', 'POST'])
@login_required
def change_email_request():
    form = ChangeEmailForm()
    if form.validate_on_submit():
        if current_user.verify_password(form.password.data):
            new_email = form.email.data.lower()
            token = current_user.generate_email_change_token(new_email)
            send_email(new_email, 'Confirm your email address',
                       'auth/email/change_email',
                       user=current_user, token=token)
            flash('An email with instructions to confirm your new email '
                  'address has been sent to you.')
            return redirect(url_for('main.index'))
        else:
            flash('Invalid email or password.')
    return render_template("auth/change_email.html", form=form)


@auth.route('/change_email/<token>')
@login_required
def change_email(token):
    if current_user.change_email(token):
        db.session.commit()
        flash('Your email address has been updated.')
    else:
        flash('Invalid request.')
    return redirect(url_for('main.index'))


================================================
FILE: app/decorators.py
================================================
from functools import wraps
from flask import abort
from flask_login import current_user
from .models import Permission


def permission_required(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.can(permission):
                abort(403)
            return f(*args, **kwargs)
        return decorated_function
    return decorator


def admin_required(f):
    return permission_required(Permission.ADMIN)(f)


================================================
FILE: app/email.py
================================================
from threading import Thread
from flask import current_app, render_template
from flask_mail import Message
from . import mail


def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)


def send_email(to, subject, template, **kwargs):
    app = current_app._get_current_object()
    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject,
                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
    msg.body = render_template(template + '.txt', **kwargs)
    msg.html = render_template(template + '.html', **kwargs)
    thr = Thread(target=send_async_email, args=[app, msg])
    thr.start()
    return thr


================================================
FILE: app/exceptions.py
================================================
class ValidationError(ValueError):
    pass


================================================
FILE: app/fake.py
================================================
from random import randint
from sqlalchemy.exc import IntegrityError
from faker import Faker
from . import db
from .models import User, Post


def users(count=100):
    fake = Faker()
    i = 0
    while i < count:
        u = User(email=fake.email(),
                 username=fake.user_name(),
                 password='password',
                 confirmed=True,
                 name=fake.name(),
                 location=fake.city(),
                 about_me=fake.text(),
                 member_since=fake.past_date())
        db.session.add(u)
        try:
            db.session.commit()
            i += 1
        except IntegrityError:
            db.session.rollback()


def posts(count=100):
    fake = Faker()
    user_count = User.query.count()
    for i in range(count):
        u = User.query.offset(randint(0, user_count - 1)).first()
        p = Post(body=fake.text(),
                 timestamp=fake.past_date(),
                 author=u)
        db.session.add(p)
    db.session.commit()


================================================
FILE: app/main/__init__.py
================================================
from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors
from ..models import Permission


@main.app_context_processor
def inject_permissions():
    return dict(Permission=Permission)


================================================
FILE: app/main/errors.py
================================================
from flask import render_template, request, jsonify
from . import main


@main.app_errorhandler(403)
def forbidden(e):
    if request.accept_mimetypes.accept_json and \
            not request.accept_mimetypes.accept_html:
        response = jsonify({'error': 'forbidden'})
        response.status_code = 403
        return response
    return render_template('403.html'), 403


@main.app_errorhandler(404)
def page_not_found(e):
    if request.accept_mimetypes.accept_json and \
            not request.accept_mimetypes.accept_html:
        response = jsonify({'error': 'not found'})
        response.status_code = 404
        return response
    return render_template('404.html'), 404


@main.app_errorhandler(500)
def internal_server_error(e):
    if request.accept_mimetypes.accept_json and \
            not request.accept_mimetypes.accept_html:
        response = jsonify({'error': 'internal server error'})
        response.status_code = 500
        return response
    return render_template('500.html'), 500


================================================
FILE: app/main/forms.py
================================================
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, BooleanField, SelectField,\
    SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp
from wtforms import ValidationError
from flask_pagedown.fields import PageDownField
from ..models import Role, User


class NameForm(FlaskForm):
    name = StringField('What is your name?', validators=[DataRequired()])
    submit = SubmitField('Submit')


class EditProfileForm(FlaskForm):
    name = StringField('Real name', validators=[Length(0, 64)])
    location = StringField('Location', validators=[Length(0, 64)])
    about_me = TextAreaField('About me')
    submit = SubmitField('Submit')


class EditProfileAdminForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Length(1, 64),
                                             Email()])
    username = StringField('Username', validators=[
        DataRequired(), Length(1, 64),
        Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
               'Usernames must have only letters, numbers, dots or '
               'underscores')])
    confirmed = BooleanField('Confirmed')
    role = SelectField('Role', coerce=int)
    name = StringField('Real name', validators=[Length(0, 64)])
    location = StringField('Location', validators=[Length(0, 64)])
    about_me = TextAreaField('About me')
    submit = SubmitField('Submit')

    def __init__(self, user, *args, **kwargs):
        super(EditProfileAdminForm, self).__init__(*args, **kwargs)
        self.role.choices = [(role.id, role.name)
                             for role in Role.query.order_by(Role.name).all()]
        self.user = user

    def validate_email(self, field):
        if field.data != self.user.email and \
                User.query.filter_by(email=field.data).first():
            raise ValidationError('Email already registered.')

    def validate_username(self, field):
        if field.data != self.user.username and \
                User.query.filter_by(username=field.data).first():
            raise ValidationError('Username already in use.')


class PostForm(FlaskForm):
    body = PageDownField("What's on your mind?", validators=[DataRequired()])
    submit = SubmitField('Submit')


class CommentForm(FlaskForm):
    body = StringField('Enter your comment', validators=[DataRequired()])
    submit = SubmitField('Submit')


================================================
FILE: app/main/views.py
================================================
from flask import render_template, redirect, url_for, abort, flash, request,\
    current_app, make_response
from flask_login import login_required, current_user
from flask_sqlalchemy import get_debug_queries
from . import main
from .forms import EditProfileForm, EditProfileAdminForm, PostForm,\
    CommentForm
from .. import db
from ..models import Permission, Role, User, Post, Comment
from ..decorators import admin_required, permission_required


@main.after_app_request
def after_request(response):
    for query in get_debug_queries():
        if query.duration >= current_app.config['FLASKY_SLOW_DB_QUERY_TIME']:
            current_app.logger.warning(
                'Slow query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n'
                % (query.statement, query.parameters, query.duration,
                   query.context))
    return response


@main.route('/shutdown')
def server_shutdown():
    if not current_app.testing:
        abort(404)
    shutdown = request.environ.get('werkzeug.server.shutdown')
    if not shutdown:
        abort(500)
    shutdown()
    return 'Shutting down...'


@main.route('/', methods=['GET', 'POST'])
def index():
    form = PostForm()
    if current_user.can(Permission.WRITE) and form.validate_on_submit():
        post = Post(body=form.body.data,
                    author=current_user._get_current_object())
        db.session.add(post)
        db.session.commit()
        return redirect(url_for('.index'))
    page = request.args.get('page', 1, type=int)
    show_followed = False
    if current_user.is_authenticated:
        show_followed = bool(request.cookies.get('show_followed', ''))
    if show_followed:
        query = current_user.followed_posts
    else:
        query = Post.query
    pagination = query.order_by(Post.timestamp.desc()).paginate(
        page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
        error_out=False)
    posts = pagination.items
    return render_template('index.html', form=form, posts=posts,
                           show_followed=show_followed, pagination=pagination)


@main.route('/user/<username>')
def user(username):
    user = User.query.filter_by(username=username).first_or_404()
    page = request.args.get('page', 1, type=int)
    pagination = user.posts.order_by(Post.timestamp.desc()).paginate(
        page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
        error_out=False)
    posts = pagination.items
    return render_template('user.html', user=user, posts=posts,
                           pagination=pagination)


@main.route('/edit-profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
    form = EditProfileForm()
    if form.validate_on_submit():
        current_user.name = form.name.data
        current_user.location = form.location.data
        current_user.about_me = form.about_me.data
        db.session.add(current_user._get_current_object())
        db.session.commit()
        flash('Your profile has been updated.')
        return redirect(url_for('.user', username=current_user.username))
    form.name.data = current_user.name
    form.location.data = current_user.location
    form.about_me.data = current_user.about_me
    return render_template('edit_profile.html', form=form)


@main.route('/edit-profile/<int:id>', methods=['GET', 'POST'])
@login_required
@admin_required
def edit_profile_admin(id):
    user = User.query.get_or_404(id)
    form = EditProfileAdminForm(user=user)
    if form.validate_on_submit():
        user.email = form.email.data
        user.username = form.username.data
        user.confirmed = form.confirmed.data
        user.role = Role.query.get(form.role.data)
        user.name = form.name.data
        user.location = form.location.data
        user.about_me = form.about_me.data
        db.session.add(user)
        db.session.commit()
        flash('The profile has been updated.')
        return redirect(url_for('.user', username=user.username))
    form.email.data = user.email
    form.username.data = user.username
    form.confirmed.data = user.confirmed
    form.role.data = user.role_id
    form.name.data = user.name
    form.location.data = user.location
    form.about_me.data = user.about_me
    return render_template('edit_profile.html', form=form, user=user)


@main.route('/post/<int:id>', methods=['GET', 'POST'])
def post(id):
    post = Post.query.get_or_404(id)
    form = CommentForm()
    if form.validate_on_submit():
        comment = Comment(body=form.body.data,
                          post=post,
                          author=current_user._get_current_object())
        db.session.add(comment)
        db.session.commit()
        flash('Your comment has been published.')
        return redirect(url_for('.post', id=post.id, page=-1))
    page = request.args.get('page', 1, type=int)
    if page == -1:
        page = (post.comments.count() - 1) // \
            current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1
    pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
        page=page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
        error_out=False)
    comments = pagination.items
    return render_template('post.html', posts=[post], form=form,
                           comments=comments, pagination=pagination)


@main.route('/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit(id):
    post = Post.query.get_or_404(id)
    if current_user != post.author and \
            not current_user.can(Permission.ADMIN):
        abort(403)
    form = PostForm()
    if form.validate_on_submit():
        post.body = form.body.data
        db.session.add(post)
        db.session.commit()
        flash('The post has been updated.')
        return redirect(url_for('.post', id=post.id))
    form.body.data = post.body
    return render_template('edit_post.html', form=form)


@main.route('/follow/<username>')
@login_required
@permission_required(Permission.FOLLOW)
def follow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('Invalid user.')
        return redirect(url_for('.index'))
    if current_user.is_following(user):
        flash('You are already following this user.')
        return redirect(url_for('.user', username=username))
    current_user.follow(user)
    db.session.commit()
    flash('You are now following %s.' % username)
    return redirect(url_for('.user', username=username))


@main.route('/unfollow/<username>')
@login_required
@permission_required(Permission.FOLLOW)
def unfollow(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('Invalid user.')
        return redirect(url_for('.index'))
    if not current_user.is_following(user):
        flash('You are not following this user.')
        return redirect(url_for('.user', username=username))
    current_user.unfollow(user)
    db.session.commit()
    flash('You are not following %s anymore.' % username)
    return redirect(url_for('.user', username=username))


@main.route('/followers/<username>')
def followers(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('Invalid user.')
        return redirect(url_for('.index'))
    page = request.args.get('page', 1, type=int)
    pagination = user.followers.paginate(
        page=page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
        error_out=False)
    follows = [{'user': item.follower, 'timestamp': item.timestamp}
               for item in pagination.items]
    return render_template('followers.html', user=user, title="Followers of",
                           endpoint='.followers', pagination=pagination,
                           follows=follows)


@main.route('/followed_by/<username>')
def followed_by(username):
    user = User.query.filter_by(username=username).first()
    if user is None:
        flash('Invalid user.')
        return redirect(url_for('.index'))
    page = request.args.get('page', 1, type=int)
    pagination = user.followed.paginate(
        page=page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
        error_out=False)
    follows = [{'user': item.followed, 'timestamp': item.timestamp}
               for item in pagination.items]
    return render_template('followers.html', user=user, title="Followed by",
                           endpoint='.followed_by', pagination=pagination,
                           follows=follows)


@main.route('/all')
@login_required
def show_all():
    resp = make_response(redirect(url_for('.index')))
    resp.set_cookie('show_followed', '', max_age=30*24*60*60)
    return resp


@main.route('/followed')
@login_required
def show_followed():
    resp = make_response(redirect(url_for('.index')))
    resp.set_cookie('show_followed', '1', max_age=30*24*60*60)
    return resp


@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE)
def moderate():
    page = request.args.get('page', 1, type=int)
    pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
        page=page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
        error_out=False)
    comments = pagination.items
    return render_template('moderate.html', comments=comments,
                           pagination=pagination, page=page)


@main.route('/moderate/enable/<int:id>')
@login_required
@permission_required(Permission.MODERATE)
def moderate_enable(id):
    comment = Comment.query.get_or_404(id)
    comment.disabled = False
    db.session.add(comment)
    db.session.commit()
    return redirect(url_for('.moderate',
                            page=request.args.get('page', 1, type=int)))


@main.route('/moderate/disable/<int:id>')
@login_required
@permission_required(Permission.MODERATE)
def moderate_disable(id):
    comment = Comment.query.get_or_404(id)
    comment.disabled = True
    db.session.add(comment)
    db.session.commit()
    return redirect(url_for('.moderate',
                            page=request.args.get('page', 1, type=int)))


================================================
FILE: app/models.py
================================================
from datetime import datetime
import hashlib
from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from markdown import markdown
import bleach
from flask import current_app, request, url_for
from flask_login import UserMixin, AnonymousUserMixin
from app.exceptions import ValidationError
from . import db, login_manager


class Permission:
    FOLLOW = 1
    COMMENT = 2
    WRITE = 4
    MODERATE = 8
    ADMIN = 16


class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), unique=True)
    default = db.Column(db.Boolean, default=False, index=True)
    permissions = db.Column(db.Integer)
    users = db.relationship('User', backref='role', lazy='dynamic')

    def __init__(self, **kwargs):
        super(Role, self).__init__(**kwargs)
        if self.permissions is None:
            self.permissions = 0

    @staticmethod
    def insert_roles():
        roles = {
            'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],
            'Moderator': [Permission.FOLLOW, Permission.COMMENT,
                          Permission.WRITE, Permission.MODERATE],
            'Administrator': [Permission.FOLLOW, Permission.COMMENT,
                              Permission.WRITE, Permission.MODERATE,
                              Permission.ADMIN],
        }
        default_role = 'User'
        for r in roles:
            role = Role.query.filter_by(name=r).first()
            if role is None:
                role = Role(name=r)
            role.reset_permissions()
            for perm in roles[r]:
                role.add_permission(perm)
            role.default = (role.name == default_role)
            db.session.add(role)
        db.session.commit()

    def add_permission(self, perm):
        if not self.has_permission(perm):
            self.permissions += perm

    def remove_permission(self, perm):
        if self.has_permission(perm):
            self.permissions -= perm

    def reset_permissions(self):
        self.permissions = 0

    def has_permission(self, perm):
        return self.permissions & perm == perm

    def __repr__(self):
        return '<Role %r>' % self.name


class Follow(db.Model):
    __tablename__ = 'follows'
    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),
                            primary_key=True)
    followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),
                            primary_key=True)
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)


class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(64), unique=True, index=True)
    username = db.Column(db.String(64), unique=True, index=True)
    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
    password_hash = db.Column(db.String(128))
    confirmed = db.Column(db.Boolean, default=False)
    name = db.Column(db.String(64))
    location = db.Column(db.String(64))
    about_me = db.Column(db.Text())
    member_since = db.Column(db.DateTime(), default=datetime.utcnow)
    last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
    avatar_hash = db.Column(db.String(32))
    posts = db.relationship('Post', backref='author', lazy='dynamic')
    followed = db.relationship('Follow',
                               foreign_keys=[Follow.follower_id],
                               backref=db.backref('follower', lazy='joined'),
                               lazy='dynamic',
                               cascade='all, delete-orphan')
    followers = db.relationship('Follow',
                                foreign_keys=[Follow.followed_id],
                                backref=db.backref('followed', lazy='joined'),
                                lazy='dynamic',
                                cascade='all, delete-orphan')
    comments = db.relationship('Comment', backref='author', lazy='dynamic')

    @staticmethod
    def add_self_follows():
        for user in User.query.all():
            if not user.is_following(user):
                user.follow(user)
                db.session.add(user)
                db.session.commit()

    def __init__(self, **kwargs):
        super(User, self).__init__(**kwargs)
        if self.role is None:
            if self.email == current_app.config['FLASKY_ADMIN']:
                self.role = Role.query.filter_by(name='Administrator').first()
            if self.role is None:
                self.role = Role.query.filter_by(default=True).first()
        if self.email is not None and self.avatar_hash is None:
            self.avatar_hash = self.gravatar_hash()
        self.follow(self)

    @property
    def password(self):
        raise AttributeError('password is not a readable attribute')

    @password.setter
    def password(self, password):
        self.password_hash = generate_password_hash(password)

    def verify_password(self, password):
        return check_password_hash(self.password_hash, password)

    def generate_confirmation_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'confirm': self.id}).decode('utf-8')

    def confirm(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token.encode('utf-8'))
        except:
            return False
        if data.get('confirm') != self.id:
            return False
        self.confirmed = True
        db.session.add(self)
        return True

    def generate_reset_token(self, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps({'reset': self.id}).decode('utf-8')

    @staticmethod
    def reset_password(token, new_password):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token.encode('utf-8'))
        except:
            return False
        user = User.query.get(data.get('reset'))
        if user is None:
            return False
        user.password = new_password
        db.session.add(user)
        return True

    def generate_email_change_token(self, new_email, expiration=3600):
        s = Serializer(current_app.config['SECRET_KEY'], expiration)
        return s.dumps(
            {'change_email': self.id, 'new_email': new_email}).decode('utf-8')

    def change_email(self, token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token.encode('utf-8'))
        except:
            return False
        if data.get('change_email') != self.id:
            return False
        new_email = data.get('new_email')
        if new_email is None:
            return False
        if self.query.filter_by(email=new_email).first() is not None:
            return False
        self.email = new_email
        self.avatar_hash = self.gravatar_hash()
        db.session.add(self)
        return True

    def can(self, perm):
        return self.role is not None and self.role.has_permission(perm)

    def is_administrator(self):
        return self.can(Permission.ADMIN)

    def ping(self):
        self.last_seen = datetime.utcnow()
        db.session.add(self)

    def gravatar_hash(self):
        return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()

    def gravatar(self, size=100, default='identicon', rating='g'):
        url = 'https://secure.gravatar.com/avatar'
        hash = self.avatar_hash or self.gravatar_hash()
        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
            url=url, hash=hash, size=size, default=default, rating=rating)

    def follow(self, user):
        if not self.is_following(user):
            f = Follow(follower=self, followed=user)
            db.session.add(f)

    def unfollow(self, user):
        f = self.followed.filter_by(followed_id=user.id).first()
        if f:
            db.session.delete(f)

    def is_following(self, user):
        if user.id is None:
            return False
        return self.followed.filter_by(
            followed_id=user.id).first() is not None

    def is_followed_by(self, user):
        if user.id is None:
            return False
        return self.followers.filter_by(
            follower_id=user.id).first() is not None

    @property
    def followed_posts(self):
        return Post.query.join(Follow, Follow.followed_id == Post.author_id)\
            .filter(Follow.follower_id == self.id)

    def to_json(self):
        json_user = {
            'url': url_for('api.get_user', id=self.id),
            'username': self.username,
            'member_since': self.member_since,
            'last_seen': self.last_seen,
            'posts_url': url_for('api.get_user_posts', id=self.id),
            'followed_posts_url': url_for('api.get_user_followed_posts',
                                          id=self.id),
            'post_count': self.posts.count()
        }
        return json_user

    def generate_auth_token(self, expiration):
        s = Serializer(current_app.config['SECRET_KEY'],
                       expires_in=expiration)
        return s.dumps({'id': self.id}).decode('utf-8')

    @staticmethod
    def verify_auth_token(token):
        s = Serializer(current_app.config['SECRET_KEY'])
        try:
            data = s.loads(token)
        except:
            return None
        return User.query.get(data['id'])

    def __repr__(self):
        return '<User %r>' % self.username


class AnonymousUser(AnonymousUserMixin):
    def can(self, permissions):
        return False

    def is_administrator(self):
        return False

login_manager.anonymous_user = AnonymousUser


@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))


class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    body_html = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    comments = db.relationship('Comment', backref='post', lazy='dynamic')

    @staticmethod
    def on_changed_body(target, value, oldvalue, initiator):
        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
                        'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
                        'h1', 'h2', 'h3', 'p']
        target.body_html = bleach.linkify(bleach.clean(
            markdown(value, output_format='html'),
            tags=allowed_tags, strip=True))

    def to_json(self):
        json_post = {
            'url': url_for('api.get_post', id=self.id),
            'body': self.body,
            'body_html': self.body_html,
            'timestamp': self.timestamp,
            'author_url': url_for('api.get_user', id=self.author_id),
            'comments_url': url_for('api.get_post_comments', id=self.id),
            'comment_count': self.comments.count()
        }
        return json_post

    @staticmethod
    def from_json(json_post):
        body = json_post.get('body')
        if body is None or body == '':
            raise ValidationError('post does not have a body')
        return Post(body=body)


db.event.listen(Post.body, 'set', Post.on_changed_body)


class Comment(db.Model):
    __tablename__ = 'comments'
    id = db.Column(db.Integer, primary_key=True)
    body = db.Column(db.Text)
    body_html = db.Column(db.Text)
    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
    disabled = db.Column(db.Boolean)
    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))

    @staticmethod
    def on_changed_body(target, value, oldvalue, initiator):
        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i',
                        'strong']
        target.body_html = bleach.linkify(bleach.clean(
            markdown(value, output_format='html'),
            tags=allowed_tags, strip=True))

    def to_json(self):
        json_comment = {
            'url': url_for('api.get_comment', id=self.id),
            'post_url': url_for('api.get_post', id=self.post_id),
            'body': self.body,
            'body_html': self.body_html,
            'timestamp': self.timestamp,
            'author_url': url_for('api.get_user', id=self.author_id),
        }
        return json_comment

    @staticmethod
    def from_json(json_comment):
        body = json_comment.get('body')
        if body is None or body == '':
            raise ValidationError('comment does not have a body')
        return Comment(body=body)


db.event.listen(Comment.body, 'set', Comment.on_changed_body)


================================================
FILE: app/static/styles.css
================================================
.profile-thumbnail {
    position: absolute;
}
.profile-header {
    min-height: 260px;
    margin-left: 280px;
}
div.post-tabs {
    margin-top: 16px;
}
ul.posts {
    list-style-type: none;
    padding: 0px;
    margin: 16px 0px 0px 0px;
    border-top: 1px solid #e0e0e0;
}
div.post-tabs ul.posts {
    margin: 0px;
    border-top: none;
}
ul.posts li.post {
    padding: 8px;
    border-bottom: 1px solid #e0e0e0;
}
ul.posts li.post:hover {
    background-color: #f0f0f0;
}
div.post-date {
    float: right;
}
div.post-author {
    font-weight: bold;
}
div.post-thumbnail {
    position: absolute;
}
div.post-content {
    margin-left: 48px;
    min-height: 48px;
}
div.post-footer {
    text-align: right;
}
ul.comments {
    list-style-type: none;
    padding: 0px;
    margin: 16px 0px 0px 0px;
}
ul.comments li.comment {
    margin-left: 32px;
    padding: 8px;
    border-bottom: 1px solid #e0e0e0;
}
ul.comments li.comment:nth-child(1) {
    border-top: 1px solid #e0e0e0;
}
ul.comments li.comment:hover {
    background-color: #f0f0f0;
}
div.comment-date {
    float: right;
}
div.comment-author {
    font-weight: bold;
}
div.comment-thumbnail {
    position: absolute;
}
div.comment-content {
    margin-left: 48px;
    min-height: 48px;
}
div.comment-form {
    margin: 16px 0px 16px 32px;
}
div.pagination {
    width: 100%;
    text-align: right;
    padding: 0px;
    margin: 0px;
}
div.flask-pagedown-preview {
    margin: 10px 0px 10px 0px;
    border: 1px solid #e0e0e0;
    padding: 4px;
}
div.flask-pagedown-preview h1 {
    font-size: 140%;
}
div.flask-pagedown-preview h2 {
    font-size: 130%;
}
div.flask-pagedown-preview h3 {
    font-size: 120%;
}
.post-body h1 {
    font-size: 140%;
}
.post-body h2 {
    font-size: 130%;
}
.post-body h3 {
    font-size: 120%;
}
.table.followers tr {
    border-bottom: 1px solid #e0e0e0;
}


================================================
FILE: app/templates/403.html
================================================
{% extends "base.html" %}

{% block title %}Flasky - Forbidden{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Forbidden</h1>
</div>
{% endblock %}


================================================
FILE: app/templates/404.html
================================================
{% extends "base.html" %}

{% block title %}Flasky - Page Not Found{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Not Found</h1>
</div>
{% endblock %}


================================================
FILE: app/templates/500.html
================================================
{% extends "base.html" %}

{% block title %}Flasky - Internal Server Error{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Internal Server Error</h1>
</div>
{% endblock %}


================================================
FILE: app/templates/_comments.html
================================================
<ul class="comments">
    {% for comment in comments %}
    <li class="comment">
        <div class="comment-thumbnail">
            <a href="{{ url_for('.user', username=comment.author.username) }}">
                <img class="img-rounded profile-thumbnail" src="{{ comment.author.gravatar(size=40) }}">
            </a>
        </div>
        <div class="comment-content">
            <div class="comment-date">{{ moment(comment.timestamp).fromNow() }}</div>
            <div class="comment-author"><a href="{{ url_for('.user', username=comment.author.username) }}">{{ comment.author.username }}</a></div>
            <div class="comment-body">
                {% if comment.disabled %}
                <p><i>This comment has been disabled by a moderator.</i></p>
                {% endif %}
                {% if moderate or not comment.disabled %}
                    {% if comment.body_html %}
                        {{ comment.body_html | safe }}
                    {% else %}
                        {{ comment.body }}
                    {% endif %}
                {% endif %}
            </div>
            {% if moderate %}
                <br>
                {% if comment.disabled %}
                <a class="btn btn-default btn-xs" href="{{ url_for('.moderate_enable', id=comment.id, page=page) }}">Enable</a>
                {% else %}
                <a class="btn btn-danger btn-xs" href="{{ url_for('.moderate_disable', id=comment.id, page=page) }}">Disable</a>
                {% endif %}
            {% endif %}
        </div>
    </li>
    {% endfor %}
</ul>


================================================
FILE: app/templates/_macros.html
================================================
{% macro pagination_widget(pagination, endpoint, fragment='') %}
<ul class="pagination">
    <li{% if not pagination.has_prev %} class="disabled"{% endif %}>
        <a href="{% if pagination.has_prev %}{{ url_for(endpoint, page=pagination.prev_num, **kwargs) }}{{ fragment }}{% else %}#{% endif %}">
            &laquo;
        </a>
    </li>
    {% for p in pagination.iter_pages() %}
        {% if p %}
            {% if p == pagination.page %}
            <li class="active">
                <a href="{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}">{{ p }}</a>
            </li>
            {% else %}
            <li>
                <a href="{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}">{{ p }}</a>
            </li>
            {% endif %}
        {% else %}
        <li class="disabled"><a href="#">&hellip;</a></li>
        {% endif %}
    {% endfor %}
    <li{% if not pagination.has_next %} class="disabled"{% endif %}>
        <a href="{% if pagination.has_next %}{{ url_for(endpoint, page=pagination.next_num, **kwargs) }}{{ fragment }}{% else %}#{% endif %}">
            &raquo;
        </a>
    </li>
</ul>
{% endmacro %}


================================================
FILE: app/templates/_posts.html
================================================
<ul class="posts">
    {% for post in posts %}
    <li class="post">
        <div class="post-thumbnail">
            <a href="{{ url_for('.user', username=post.author.username) }}">
                <img class="img-rounded profile-thumbnail" src="{{ post.author.gravatar(size=40) }}">
            </a>
        </div>
        <div class="post-content">
            <div class="post-date">{{ moment(post.timestamp).fromNow() }}</div>
            <div class="post-author"><a href="{{ url_for('.user', username=post.author.username) }}">{{ post.author.username }}</a></div>
            <div class="post-body">
                {% if post.body_html %}
                    {{ post.body_html | safe }}
                {% else %}
                    {{ post.body }}
                {% endif %}
            </div>
            <div class="post-footer">
                {% if current_user == post.author %}
                <a href="{{ url_for('.edit', id=post.id) }}">
                    <span class="label label-primary">Edit</span>
                </a>
                {% elif current_user.is_administrator() %}
                <a href="{{ url_for('.edit', id=post.id) }}">
                    <span class="label label-danger">Edit [Admin]</span>
                </a>
                {% endif %}
                <a href="{{ url_for('.post', id=post.id) }}">
                    <span class="label label-default">Permalink</span>
                </a>
                <a href="{{ url_for('.post', id=post.id) }}#comments">
                    <span class="label label-primary">{{ post.comments.count() }} Comments</span>
                </a>
            </div>
        </div>
    </li>
    {% endfor %}
</ul>


================================================
FILE: app/templates/auth/change_email.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Change Email Address{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Change Your Email Address</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

================================================
FILE: app/templates/auth/change_password.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Change Password{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Change Your Password</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

================================================
FILE: app/templates/auth/email/change_email.html
================================================
<p>Dear {{ user.username }},</p>
<p>To confirm your new email address <a href="{{ url_for('auth.change_email', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.change_email', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>


================================================
FILE: app/templates/auth/email/change_email.txt
================================================
Dear {{ user.username }},

To confirm your new email address click on the following link:

{{ url_for('auth.change_email', token=token, _external=True) }}

Sincerely,

The Flasky Team

Note: replies to this email address are not monitored.


================================================
FILE: app/templates/auth/email/confirm.html
================================================
<p>Dear {{ user.username }},</p>
<p>Welcome to <b>Flasky</b>!</p>
<p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>


================================================
FILE: app/templates/auth/email/confirm.txt
================================================
Dear {{ user.username }},

Welcome to Flasky!

To confirm your account please click on the following link:

{{ url_for('auth.confirm', token=token, _external=True) }}

Sincerely,

The Flasky Team

Note: replies to this email address are not monitored.


================================================
FILE: app/templates/auth/email/reset_password.html
================================================
<p>Dear {{ user.username }},</p>
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>


================================================
FILE: app/templates/auth/email/reset_password.txt
================================================
Dear {{ user.username }},

To reset your password click on the following link:

{{ url_for('auth.password_reset', token=token, _external=True) }}

If you have not requested a password reset simply ignore this message.

Sincerely,

The Flasky Team

Note: replies to this email address are not monitored.


================================================
FILE: app/templates/auth/login.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Login{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Login</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
    <br>
    <p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
    <p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
</div>
{% endblock %}


================================================
FILE: app/templates/auth/register.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Register{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Register</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}


================================================
FILE: app/templates/auth/reset_password.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Password Reset{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Reset Your Password</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

================================================
FILE: app/templates/auth/unconfirmed.html
================================================
{% extends "base.html" %}

{% block title %}Flasky - Confirm your account{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>
        Hello, {{ current_user.username }}!
    </h1>
    <h3>You have not confirmed your account yet.</h3>
    <p>
        Before you can access this site you need to confirm your account.
        Check your inbox, you should have received an email with a confirmation link.
    </p>
    <p>
        Need another confirmation email?
        <a href="{{ url_for('auth.resend_confirmation') }}">Click here</a>
    </p>
</div>
{% endblock %}


================================================
FILE: app/templates/base.html
================================================
{% extends "bootstrap/base.html" %}

{% block title %}Flasky{% endblock %}

{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
{% endblock %}

{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="sr-only">Toggle navigation</span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="{{ url_for('main.index') }}">Flasky</a>
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li><a href="{{ url_for('main.index') }}">Home</a></li>
                {% if current_user.is_authenticated %}
                <li><a href="{{ url_for('main.user', username=current_user.username) }}">Profile</a></li>
                {% endif %}
            </ul>
            <ul class="nav navbar-nav navbar-right">
                {% if current_user.can(Permission.MODERATE) %}
                <li><a href="{{ url_for('main.moderate') }}">Moderate Comments</a></li>
                {% endif %}
                {% if current_user.is_authenticated %}
                <li class="dropdown">
                    <a href="#" class="dropdown-toggle" data-toggle="dropdown">
                        <img src="{{ current_user.gravatar(size=18) }}">
                        Account <b class="caret"></b>
                    </a>
                    <ul class="dropdown-menu">
                        <li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li>
                        <li><a href="{{ url_for('auth.change_email_request') }}">Change Email</a></li>
                        <li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
                    </ul>
                </li>
                {% else %}
                <li><a href="{{ url_for('auth.login') }}">Log In</a></li>
                {% endif %}
            </ul>
        </div>
    </div>
</div>
{% endblock %}

{% block content %}
<div class="container">
    {% for message in get_flashed_messages() %}
    <div class="alert alert-warning">
        <button type="button" class="close" data-dismiss="alert">&times;</button>
        {{ message }}
    </div>
    {% endfor %}

    {% block page_content %}{% endblock %}
</div>
{% endblock %}

{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}


================================================
FILE: app/templates/edit_post.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Edit Post{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Edit Post</h1>
</div>
<div>
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}

{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}


================================================
FILE: app/templates/edit_profile.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}

{% block title %}Flasky - Edit Profile{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Edit Your Profile</h1>
</div>
<div class="col-md-4">
    {{ wtf.quick_form(form) }}
</div>
{% endblock %}


================================================
FILE: app/templates/followers.html
================================================
{% extends "base.html" %}
{% import "_macros.html" as macros %}

{% block title %}Flasky - {{ title }} {{ user.username }}{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>{{ title }} {{ user.username }}</h1>
</div>
<table class="table table-hover followers">
    <thead><tr><th>User</th><th>Since</th></tr></thead>
    {% for follow in follows %}
    {% if follow.user != user %}
    <tr>
        <td>
            <a href="{{ url_for('.user', username = follow.user.username) }}">
                <img class="img-rounded" src="{{ follow.user.gravatar(size=32) }}">
                {{ follow.user.username }}
            </a>
        </td>
        <td>{{ moment(follow.timestamp).format('L') }}</td>
    </tr>
    {% endif %}
    {% endfor %}
</table>
<div class="pagination">
    {{ macros.pagination_widget(pagination, endpoint, username = user.username) }}
</div>
{% endblock %}


================================================
FILE: app/templates/index.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}

{% block title %}Flasky{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Hello, {% if current_user.is_authenticated %}{{ current_user.username }}{% else %}Stranger{% endif %}!</h1>
</div>
<div>
    {% if current_user.can(Permission.WRITE) %}
    {{ wtf.quick_form(form) }}
    {% endif %}
</div>
<div class="post-tabs">
    <ul class="nav nav-tabs">
        <li{% if not show_followed %} class="active"{% endif %}><a href="{{ url_for('.show_all') }}">All</a></li>
        {% if current_user.is_authenticated %}
        <li{% if show_followed %} class="active"{% endif %}><a href="{{ url_for('.show_followed') }}">Followed</a></li>
        {% endif %}
    </ul>
    {% include '_posts.html' %}
</div>
{% if pagination %}
<div class="pagination">
    {{ macros.pagination_widget(pagination, '.index') }}
</div>
{% endif %}
{% endblock %}

{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}


================================================
FILE: app/templates/mail/new_user.html
================================================
User <b>{{ user.username }}</b> has joined.


================================================
FILE: app/templates/mail/new_user.txt
================================================
User {{ user.username }} has joined.


================================================
FILE: app/templates/moderate.html
================================================
{% extends "base.html" %}
{% import "_macros.html" as macros %}

{% block title %}Flasky - Comment Moderation{% endblock %}

{% block page_content %}
<div class="page-header">
    <h1>Comment Moderation</h1>
</div>
{% set moderate = True %}
{% include '_comments.html' %}
{% if pagination %}
<div class="pagination">
    {{ macros.pagination_widget(pagination, '.moderate') }}
</div>
{% endif %}
{% endblock %}


================================================
FILE: app/templates/post.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}

{% block title %}Flasky - Post{% endblock %}

{% block page_content %}
{% include '_posts.html' %}
<h4 id="comments">Comments</h4>
{% if current_user.can(Permission.COMMENT) %}
<div class="comment-form">
    {{ wtf.quick_form(form) }}
</div>
{% endif %}
{% include '_comments.html' %}
{% if pagination %}
<div class="pagination">
    {{ macros.pagination_widget(pagination, '.post', fragment='#comments', id=posts[0].id) }}
</div>
{% endif %}
{% endblock %}


================================================
FILE: app/templates/user.html
================================================
{% extends "base.html" %}
{% import "_macros.html" as macros %}

{% block title %}Flasky - {{ user.username }}{% endblock %}

{% block page_content %}
<div class="page-header">
    <img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=256) }}">
    <div class="profile-header">
        <h1>{{ user.username }}</h1>
        {% if user.name or user.location %}
        <p>
            {% if user.name %}{{ user.name }}<br>{% endif %}
            {% if user.location %}
                from <a href="http://maps.google.com/?q={{ user.location }}">{{ user.location }}</a><br>
            {% endif %}
        </p>
        {% endif %}
        {% if current_user.is_administrator() %}
        <p><a href="mailto:{{ user.email }}">{{ user.email }}</a></p>
        {% endif %}
        {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
        <p>Member since {{ moment(user.member_since).format('L') }}. Last seen {{ moment(user.last_seen).fromNow() }}.</p>
        <p>{{ user.posts.count() }} blog posts. {{ user.comments.count() }} comments.</p>
        <p>
            {% if current_user.can(Permission.FOLLOW) and user != current_user %}
                {% if not current_user.is_following(user) %}
                <a href="{{ url_for('.follow', username=user.username) }}" class="btn btn-primary">Follow</a>
                {% else %}
                <a href="{{ url_for('.unfollow', username=user.username) }}" class="btn btn-default">Unfollow</a>
                {% endif %}
            {% endif %}
            <a href="{{ url_for('.followers', username=user.username) }}">Followers: <span class="badge">{{ user.followers.count() - 1 }}</span></a>
            <a href="{{ url_for('.followed_by', username=user.username) }}">Following: <span class="badge">{{ user.followed.count() - 1 }}</span></a>
            {% if current_user.is_authenticated and user != current_user and user.is_following(current_user) %}
            | <span class="label label-default">Follows you</span>
            {% endif %}
        </p>
        <p>
            {% if user == current_user %}
            <a class="btn btn-default" href="{{ url_for('.edit_profile') }}">Edit Profile</a>
            {% endif %}
            {% if current_user.is_administrator() %}
            <a class="btn btn-danger" href="{{ url_for('.edit_profile_admin', id=user.id) }}">Edit Profile [Admin]</a>
            {% endif %}
        </p>
    </div>
</div>
<h3>Posts by {{ user.username }}</h3>
{% include '_posts.html' %}
{% if pagination %}
<div class="pagination">
    {{ macros.pagination_widget(pagination, '.user', username=user.username) }}
</div>
{% endif %}
{% endblock %}


================================================
FILE: boot.sh
================================================
#!/bin/sh
source venv/bin/activate

while true; do
    flask deploy
    if [[ "$?" == "0" ]]; then
        break
    fi
    echo Deploy command failed, retrying in 5 secs...
    sleep 5
done

exec gunicorn -b :5000 --access-logfile - --error-logfile - flasky:app


================================================
FILE: config.py
================================================
import os
basedir = os.path.abspath(os.path.dirname(__file__))


class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
    MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
    MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
    MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
        ['true', 'on', '1']
    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
    FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
    FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>'
    FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
    SSL_REDIRECT = False
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_RECORD_QUERIES = True
    FLASKY_POSTS_PER_PAGE = 20
    FLASKY_FOLLOWERS_PER_PAGE = 50
    FLASKY_COMMENTS_PER_PAGE = 30
    FLASKY_SLOW_DB_QUERY_TIME = 0.5

    @staticmethod
    def init_app(app):
        pass


class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')


class TestingConfig(Config):
    TESTING = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
        'sqlite://'
    WTF_CSRF_ENABLED = False


class ProductionConfig(Config):
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'data.sqlite')
    SERVER_NAME = os.environ['SERVER_NAME']  # configure the domain name in use

    @classmethod
    def init_app(cls, app):
        Config.init_app(app)

        # email errors to the administrators
        import logging
        from logging.handlers import SMTPHandler
        credentials = None
        secure = None
        if getattr(cls, 'MAIL_USERNAME', None) is not None:
            credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)
            if getattr(cls, 'MAIL_USE_TLS', None):
                secure = ()
        mail_handler = SMTPHandler(
            mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),
            fromaddr=cls.FLASKY_MAIL_SENDER,
            toaddrs=[cls.FLASKY_ADMIN],
            subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error',
            credentials=credentials,
            secure=secure)
        mail_handler.setLevel(logging.ERROR)
        app.logger.addHandler(mail_handler)


class HerokuConfig(ProductionConfig):
    SSL_REDIRECT = True if os.environ.get('DYNO') else False

    @classmethod
    def init_app(cls, app):
        ProductionConfig.init_app(app)

        # handle reverse proxy server headers
        try:
            from werkzeug.middleware.proxy_fix import ProxyFix
        except ImportError:
            from werkzeug.contrib.fixers import ProxyFix
        app.wsgi_app = ProxyFix(app.wsgi_app)

        # log to stderr
        import logging
        from logging import StreamHandler
        file_handler = StreamHandler()
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)


class DockerConfig(ProductionConfig):
    @classmethod
    def init_app(cls, app):
        ProductionConfig.init_app(app)

        # log to stderr
        import logging
        from logging import StreamHandler
        file_handler = StreamHandler()
        file_handler.setLevel(logging.INFO)
        app.logger.addHandler(file_handler)


class UnixConfig(ProductionConfig):
    @classmethod
    def init_app(cls, app):
        ProductionConfig.init_app(app)

        # log to syslog
        import logging
        from logging.handlers import SysLogHandler
        syslog_handler = SysLogHandler()
        syslog_handler.setLevel(logging.INFO)
        app.logger.addHandler(syslog_handler)


config = {
    'development': DevelopmentConfig,
    'testing': TestingConfig,
    'production': ProductionConfig,
    'heroku': HerokuConfig,
    'docker': DockerConfig,
    'unix': UnixConfig,

    'default': DevelopmentConfig
}


================================================
FILE: docker-compose.yml
================================================
version: '3'
services:
  flasky:
    build: .
    ports:
      - "8000:5000"
    env_file: .env
    restart: always
    links:
      - mysql:dbserver
  mysql:
    image: "mysql/mysql-server:5.7"
    env_file: .env-mysql
    restart: always


================================================
FILE: flasky.py
================================================
import os
from dotenv import load_dotenv

dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(dotenv_path):
    load_dotenv(dotenv_path)

COV = None
if os.environ.get('FLASK_COVERAGE'):
    import coverage
    COV = coverage.coverage(branch=True, include='app/*')
    COV.start()

import sys
import click
from flask_migrate import Migrate, upgrade
from app import create_app, db
from app.models import User, Follow, Role, Permission, Post, Comment

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)


@app.shell_context_processor
def make_shell_context():
    return dict(db=db, User=User, Follow=Follow, Role=Role,
                Permission=Permission, Post=Post, Comment=Comment)


@app.cli.command()
@click.option('--coverage/--no-coverage', default=False,
              help='Run tests under code coverage.')
@click.argument('test_names', nargs=-1)
def test(coverage, test_names):
    """Run the unit tests."""
    if coverage and not os.environ.get('FLASK_COVERAGE'):
        import subprocess
        os.environ['FLASK_COVERAGE'] = '1'
        sys.exit(subprocess.call(sys.argv))

    import unittest
    if test_names:
        tests = unittest.TestLoader().loadTestsFromNames(test_names)
    else:
        tests = unittest.TestLoader().discover('tests')
    unittest.TextTestRunner(verbosity=2).run(tests)
    if COV:
        COV.stop()
        COV.save()
        print('Coverage Summary:')
        COV.report()
        basedir = os.path.abspath(os.path.dirname(__file__))
        covdir = os.path.join(basedir, 'tmp/coverage')
        COV.html_report(directory=covdir)
        print('HTML version: file://%s/index.html' % covdir)
        COV.erase()


@app.cli.command()
@click.option('--length', default=25,
              help='Number of functions to include in the profiler report.')
@click.option('--profile-dir', default=None,
              help='Directory where profiler data files are saved.')
def profile(length, profile_dir):
    """Start the application under the code profiler."""
    from werkzeug.contrib.profiler import ProfilerMiddleware
    app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length],
                                      profile_dir=profile_dir)
    app.run()


@app.cli.command()
def deploy():
    """Run deployment tasks."""
    # migrate database to latest revision
    upgrade()

    # create or update user roles
    Role.insert_roles()

    # ensure all users are following themselves
    User.add_self_follows()


================================================
FILE: migrations/README
================================================
Generic single-database configuration.

================================================
FILE: migrations/alembic.ini
================================================
# A generic, single database configuration.

[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s

# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false


# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console
qualname =

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[logger_alembic]
level = INFO
handlers =
qualname = alembic

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S


================================================
FILE: migrations/env.py
================================================
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.

def run_migrations_offline():
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    url = config.get_main_option("sqlalchemy.url")
    context.configure(url=url)

    with context.begin_transaction():
        context.run_migrations()

def run_migrations_online():
    """Run migrations in 'online' mode.

    In this scenario we need to create an Engine
    and associate a connection with the context.

    """
    engine = engine_from_config(
                config.get_section(config.config_ini_section),
                prefix='sqlalchemy.',
                poolclass=pool.NullPool)

    connection = engine.connect()
    context.configure(
                connection=connection,
                target_metadata=target_metadata
                )

    try:
        with context.begin_transaction():
            context.run_migrations()
    finally:
        connection.close()

if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()



================================================
FILE: migrations/script.py.mako
================================================
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}

"""

# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

def upgrade():
    ${upgrades if upgrades else "pass"}


def downgrade():
    ${downgrades if downgrades else "pass"}


================================================
FILE: migrations/versions/190163627111_account_confirmation.py
================================================
"""account confirmation

Revision ID: 190163627111
Revises: 456a945560f6
Create Date: 2013-12-29 02:58:45.577428

"""

# revision identifiers, used by Alembic.
revision = '190163627111'
down_revision = '456a945560f6'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('confirmed', sa.Boolean(), nullable=True))
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('users', 'confirmed')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/198b0eebcf9_caching_of_avatar_hashes.py
================================================
"""caching of avatar hashes

Revision ID: 198b0eebcf9
Revises: d66f086b258
Create Date: 2014-02-04 09:10:02.245503

"""

# revision identifiers, used by Alembic.
revision = '198b0eebcf9'
down_revision = 'd66f086b258'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('avatar_hash', sa.String(length=32), nullable=True))
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('users', 'avatar_hash')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/1b966e7f4b9e_post_model.py
================================================
"""post model

Revision ID: 1b966e7f4b9e
Revises: 198b0eebcf9
Create Date: 2013-12-31 00:00:14.700591

"""

# revision identifiers, used by Alembic.
revision = '1b966e7f4b9e'
down_revision = '198b0eebcf9'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table('posts',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('body', sa.Text(), nullable=True),
    sa.Column('timestamp', sa.DateTime(), nullable=True),
    sa.Column('author_id', sa.Integer(), nullable=True),
    sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index('ix_posts_timestamp', 'posts', ['timestamp'], unique=False)
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_index('ix_posts_timestamp', 'posts')
    op.drop_table('posts')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/2356a38169ea_followers.py
================================================
"""followers

Revision ID: 2356a38169ea
Revises: 288cd3dc5a8
Create Date: 2013-12-31 16:10:34.500006

"""

# revision identifiers, used by Alembic.
revision = '2356a38169ea'
down_revision = '288cd3dc5a8'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table('follows',
    sa.Column('follower_id', sa.Integer(), nullable=False),
    sa.Column('followed_id', sa.Integer(), nullable=False),
    sa.Column('timestamp', sa.DateTime(), nullable=True),
    sa.ForeignKeyConstraint(['followed_id'], ['users.id'], ),
    sa.ForeignKeyConstraint(['follower_id'], ['users.id'], ),
    sa.PrimaryKeyConstraint('follower_id', 'followed_id')
    )
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('follows')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/288cd3dc5a8_rich_text_posts.py
================================================
"""rich text posts

Revision ID: 288cd3dc5a8
Revises: 1b966e7f4b9e
Create Date: 2013-12-31 03:25:13.286503

"""

# revision identifiers, used by Alembic.
revision = '288cd3dc5a8'
down_revision = '1b966e7f4b9e'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.add_column('posts', sa.Column('body_html', sa.Text(), nullable=True))
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('posts', 'body_html')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/38c4e85512a9_initial_migration.py
================================================
"""initial migration

Revision ID: 38c4e85512a9
Revises: None
Create Date: 2013-12-27 01:23:59.392801

"""

# revision identifiers, used by Alembic.
revision = '38c4e85512a9'
down_revision = None

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table('roles',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=64), nullable=True),
    sa.PrimaryKeyConstraint('id'),
    sa.UniqueConstraint('name')
    )
    op.create_table('users',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('username', sa.String(length=64), nullable=True),
    sa.Column('role_id', sa.Integer(), nullable=True),
    sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index('ix_users_username', 'users', ['username'], unique=True)
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_index('ix_users_username', 'users')
    op.drop_table('users')
    op.drop_table('roles')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/456a945560f6_login_support.py
================================================
"""login support

Revision ID: 456a945560f6
Revises: 38c4e85512a9
Create Date: 2013-12-29 00:18:35.795259

"""

# revision identifiers, used by Alembic.
revision = '456a945560f6'
down_revision = '38c4e85512a9'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('email', sa.String(length=64), nullable=True))
    op.add_column('users', sa.Column('password_hash', sa.String(length=128), nullable=True))
    op.create_index('ix_users_email', 'users', ['email'], unique=True)
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_index('ix_users_email', 'users')
    op.drop_column('users', 'password_hash')
    op.drop_column('users', 'email')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/51f5ccfba190_comments.py
================================================
"""comments

Revision ID: 51f5ccfba190
Revises: 2356a38169ea
Create Date: 2014-01-01 12:08:43.287523

"""

# revision identifiers, used by Alembic.
revision = '51f5ccfba190'
down_revision = '2356a38169ea'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.create_table('comments',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('body', sa.Text(), nullable=True),
    sa.Column('body_html', sa.Text(), nullable=True),
    sa.Column('timestamp', sa.DateTime(), nullable=True),
    sa.Column('disabled', sa.Boolean(), nullable=True),
    sa.Column('author_id', sa.Integer(), nullable=True),
    sa.Column('post_id', sa.Integer(), nullable=True),
    sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
    sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_index('ix_comments_timestamp', 'comments', ['timestamp'], unique=False)
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_index('ix_comments_timestamp', 'comments')
    op.drop_table('comments')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/56ed7d33de8d_user_roles.py
================================================
"""user roles

Revision ID: 56ed7d33de8d
Revises: 190163627111
Create Date: 2013-12-29 22:19:54.212604

"""

# revision identifiers, used by Alembic.
revision = '56ed7d33de8d'
down_revision = '190163627111'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.add_column('roles', sa.Column('default', sa.Boolean(), nullable=True))
    op.add_column('roles', sa.Column('permissions', sa.Integer(), nullable=True))
    op.create_index('ix_roles_default', 'roles', ['default'], unique=False)
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_index('ix_roles_default', 'roles')
    op.drop_column('roles', 'permissions')
    op.drop_column('roles', 'default')
    ### end Alembic commands ###


================================================
FILE: migrations/versions/d66f086b258_user_information.py
================================================
"""user information

Revision ID: d66f086b258
Revises: 56ed7d33de8d
Create Date: 2013-12-29 23:50:49.566954

"""

# revision identifiers, used by Alembic.
revision = 'd66f086b258'
down_revision = '56ed7d33de8d'

from alembic import op
import sqlalchemy as sa


def upgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.add_column('users', sa.Column('about_me', sa.Text(), nullable=True))
    op.add_column('users', sa.Column('last_seen', sa.DateTime(), nullable=True))
    op.add_column('users', sa.Column('location', sa.String(length=64), nullable=True))
    op.add_column('users', sa.Column('member_since', sa.DateTime(), nullable=True))
    op.add_column('users', sa.Column('name', sa.String(length=64), nullable=True))
    ### end Alembic commands ###


def downgrade():
    ### commands auto generated by Alembic - please adjust! ###
    op.drop_column('users', 'name')
    op.drop_column('users', 'member_since')
    op.drop_column('users', 'location')
    op.drop_column('users', 'last_seen')
    op.drop_column('users', 'about_me')
    ### end Alembic commands ###


================================================
FILE: requirements/common.txt
================================================
alembic==0.9.3
bleach==2.0.0
blinker==1.4
click==6.7
dominate==2.3.1
Flask==0.12.2
Flask-Bootstrap==3.3.7.1
Flask-HTTPAuth==3.2.3
Flask-Login==0.4.0
Flask-Mail==0.9.1
Flask-Migrate==2.0.4
Flask-Moment==0.5.1
Flask-PageDown==0.2.2
Flask-SQLAlchemy==2.2
Flask-WTF==0.14.2
html5lib==0.999999999
itsdangerous==0.24
Jinja2==2.9.6
Mako==1.0.7
Markdown==2.6.8
MarkupSafe==1.1.1
python-dateutil==2.6.1
python-dotenv==0.6.5
python-editor==1.0.3
six==1.10.0
SQLAlchemy==1.1.11
visitor==0.1.3
webencodings==0.5.1
Werkzeug==0.12.2
WTForms==2.1


================================================
FILE: requirements/dev.txt
================================================
-r common.txt
certifi==2017.7.27.1
chardet==3.0.4
coverage==4.4.1
faker==0.7.18
httpie==0.9.9
idna==2.5
Pygments==2.2.0
requests==2.18.2
selenium==3.141.0
urllib3==1.22


================================================
FILE: requirements/docker.txt
================================================
-r common.txt
gunicorn==19.7.1
pymysql==0.7.11


================================================
FILE: requirements/heroku.txt
================================================
-r prod.txt
Flask-SSLify==0.1.5
gunicorn==19.7.1
psycopg2==2.7.3


================================================
FILE: requirements/prod.txt
================================================
-r common.txt


================================================
FILE: requirements.txt
================================================
# this requirements file is used by Heroku
# requirements for other configurations are located in the requirements subdirectory
-r requirements/heroku.txt


================================================
FILE: tests/__init__.py
================================================


================================================
FILE: tests/test_api.py
================================================
import unittest
import json
import re
from base64 import b64encode
from app import create_app, db
from app.models import User, Role, Post, Comment


class APITestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
        Role.insert_roles()
        self.client = self.app.test_client()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def get_api_headers(self, username, password):
        return {
            'Authorization': 'Basic ' + b64encode(
                (username + ':' + password).encode('utf-8')).decode('utf-8'),
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }

    def test_404(self):
        response = self.client.get(
            '/wrong/url',
            headers=self.get_api_headers('email', 'password'))
        self.assertEqual(response.status_code, 404)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertEqual(json_response['error'], 'not found')

    def test_no_auth(self):
        response = self.client.get('/api/v1/posts/',
                                   content_type='application/json')
        self.assertEqual(response.status_code, 401)

    def test_bad_auth(self):
        # add a user
        r = Role.query.filter_by(name='User').first()
        self.assertIsNotNone(r)
        u = User(email='john@example.com', password='cat', confirmed=True,
                 role=r)
        db.session.add(u)
        db.session.commit()

        # authenticate with bad password
        response = self.client.get(
            '/api/v1/posts/',
            headers=self.get_api_headers('john@example.com', 'dog'))
        self.assertEqual(response.status_code, 401)

    def test_token_auth(self):
        # add a user
        r = Role.query.filter_by(name='User').first()
        self.assertIsNotNone(r)
        u = User(email='john@example.com', password='cat', confirmed=True,
                 role=r)
        db.session.add(u)
        db.session.commit()

        # issue a request with a bad token
        response = self.client.get(
            '/api/v1/posts/',
            headers=self.get_api_headers('bad-token', ''))
        self.assertEqual(response.status_code, 401)

        # get a token
        response = self.client.post(
            '/api/v1/tokens/',
            headers=self.get_api_headers('john@example.com', 'cat'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertIsNotNone(json_response.get('token'))
        token = json_response['token']

        # issue a request with the token
        response = self.client.get(
            '/api/v1/posts/',
            headers=self.get_api_headers(token, ''))
        self.assertEqual(response.status_code, 200)

    def test_anonymous(self):
        response = self.client.get(
            '/api/v1/posts/',
            headers=self.get_api_headers('', ''))
        self.assertEqual(response.status_code, 401)

    def test_unconfirmed_account(self):
        # add an unconfirmed user
        r = Role.query.filter_by(name='User').first()
        self.assertIsNotNone(r)
        u = User(email='john@example.com', password='cat', confirmed=False,
                 role=r)
        db.session.add(u)
        db.session.commit()

        # get list of posts with the unconfirmed account
        response = self.client.get(
            '/api/v1/posts/',
            headers=self.get_api_headers('john@example.com', 'cat'))
        self.assertEqual(response.status_code, 403)

    def test_posts(self):
        # add a user
        r = Role.query.filter_by(name='User').first()
        self.assertIsNotNone(r)
        u = User(email='john@example.com', password='cat', confirmed=True,
                 role=r)
        db.session.add(u)
        db.session.commit()

        # write an empty post
        response = self.client.post(
            '/api/v1/posts/',
            headers=self.get_api_headers('john@example.com', 'cat'),
            data=json.dumps({'body': ''}))
        self.assertEqual(response.status_code, 400)

        # write a post
        response = self.client.post(
            '/api/v1/posts/',
            headers=self.get_api_headers('john@example.com', 'cat'),
            data=json.dumps({'body': 'body of the *blog* post'}))
        self.assertEqual(response.status_code, 201)
        url = response.headers.get('Location')
        self.assertIsNotNone(url)

        # get the new post
        response = self.client.get(
            url,
            headers=self.get_api_headers('john@example.com', 'cat'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertEqual('http://localhost' + json_response['url'], url)
        self.assertEqual(json_response['body'], 'body of the *blog* post')
        self.assertEqual(json_response['body_html'],
                        '<p>body of the <em>blog</em> post</p>')
        json_post = json_response

        # get the post from the user
        response = self.client.get(
            '/api/v1/users/{}/posts/'.format(u.id),
            headers=self.get_api_headers('john@example.com', 'cat'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertIsNotNone(json_response.get('posts'))
        self.assertEqual(json_response.get('count', 0), 1)
        self.assertEqual(json_response['posts'][0], json_post)

        # get the post from the user as a follower
        response = self.client.get(
            '/api/v1/users/{}/timeline/'.format(u.id),
            headers=self.get_api_headers('john@example.com', 'cat'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertIsNotNone(json_response.get('posts'))
        self.assertEqual(json_response.get('count', 0), 1)
        self.assertEqual(json_response['posts'][0], json_post)

        # edit post
        response = self.client.put(
            url,
            headers=self.get_api_headers('john@example.com', 'cat'),
            data=json.dumps({'body': 'updated body'}))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertEqual('http://localhost' + json_response['url'], url)
        self.assertEqual(json_response['body'], 'updated body')
        self.assertEqual(json_response['body_html'], '<p>updated body</p>')

    def test_users(self):
        # add two users
        r = Role.query.filter_by(name='User').first()
        self.assertIsNotNone(r)
        u1 = User(email='john@example.com', username='john',
                  password='cat', confirmed=True, role=r)
        u2 = User(email='susan@example.com', username='susan',
                  password='dog', confirmed=True, role=r)
        db.session.add_all([u1, u2])
        db.session.commit()

        # get users
        response = self.client.get(
            '/api/v1/users/{}'.format(u1.id),
            headers=self.get_api_headers('susan@example.com', 'dog'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertEqual(json_response['username'], 'john')
        response = self.client.get(
            '/api/v1/users/{}'.format(u2.id),
            headers=self.get_api_headers('susan@example.com', 'dog'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertEqual(json_response['username'], 'susan')

    def test_comments(self):
        # add two users
        r = Role.query.filter_by(name='User').first()
        self.assertIsNotNone(r)
        u1 = User(email='john@example.com', username='john',
                  password='cat', confirmed=True, role=r)
        u2 = User(email='susan@example.com', username='susan',
                  password='dog', confirmed=True, role=r)
        db.session.add_all([u1, u2])
        db.session.commit()

        # add a post
        post = Post(body='body of the post', author=u1)
        db.session.add(post)
        db.session.commit()

        # write a comment
        response = self.client.post(
            '/api/v1/posts/{}/comments/'.format(post.id),
            headers=self.get_api_headers('susan@example.com', 'dog'),
            data=json.dumps({'body': 'Good [post](http://example.com)!'}))
        self.assertEqual(response.status_code, 201)
        json_response = json.loads(response.get_data(as_text=True))
        url = response.headers.get('Location')
        self.assertIsNotNone(url)
        self.assertEqual(json_response['body'],
                        'Good [post](http://example.com)!')
        self.assertEqual(
            re.sub('<.*?>', '', json_response['body_html']), 'Good post!')

        # get the new comment
        response = self.client.get(
            url,
            headers=self.get_api_headers('john@example.com', 'cat'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertEqual('http://localhost' + json_response['url'], url)
        self.assertEqual(json_response['body'],
                        'Good [post](http://example.com)!')

        # add another comment
        comment = Comment(body='Thank you!', author=u1, post=post)
        db.session.add(comment)
        db.session.commit()

        # get the two comments from the post
        response = self.client.get(
            '/api/v1/posts/{}/comments/'.format(post.id),
            headers=self.get_api_headers('susan@example.com', 'dog'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertIsNotNone(json_response.get('comments'))
        self.assertEqual(json_response.get('count', 0), 2)

        # get all the comments
        response = self.client.get(
            '/api/v1/posts/{}/comments/'.format(post.id),
            headers=self.get_api_headers('susan@example.com', 'dog'))
        self.assertEqual(response.status_code, 200)
        json_response = json.loads(response.get_data(as_text=True))
        self.assertIsNotNone(json_response.get('comments'))
        self.assertEqual(json_response.get('count', 0), 2)


================================================
FILE: tests/test_basics.py
================================================
import unittest
from flask import current_app
from app import create_app, db


class BasicsTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_app_exists(self):
        self.assertFalse(current_app is None)

    def test_app_is_testing(self):
        self.assertTrue(current_app.config['TESTING'])


================================================
FILE: tests/test_client.py
================================================
import re
import unittest
from app import create_app, db
from app.models import User, Role

class FlaskClientTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
        Role.insert_roles()
        self.client = self.app.test_client(use_cookies=True)

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_home_page(self):
        response = self.client.get('/')
        self.assertEqual(response.status_code, 200)
        self.assertTrue('Stranger' in response.get_data(as_text=True))

    def test_register_and_login(self):
        # register a new account
        response = self.client.post('/auth/register', data={
            'email': 'john@example.com',
            'username': 'john',
            'password': 'cat',
            'password2': 'cat'
        })
        self.assertEqual(response.status_code, 302)

        # login with the new account
        response = self.client.post('/auth/login', data={
            'email': 'john@example.com',
            'password': 'cat'
        }, follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(re.search('Hello,\s+john!',
                                  response.get_data(as_text=True)))
        self.assertTrue(
            'You have not confirmed your account yet' in response.get_data(
                as_text=True))

        # send a confirmation token
        user = User.query.filter_by(email='john@example.com').first()
        token = user.generate_confirmation_token()
        response = self.client.get('/auth/confirm/{}'.format(token),
                                   follow_redirects=True)
        user.confirm(token)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(
            'You have confirmed your account' in response.get_data(
                as_text=True))

        # log out
        response = self.client.get('/auth/logout', follow_redirects=True)
        self.assertEqual(response.status_code, 200)
        self.assertTrue('You have been logged out' in response.get_data(
            as_text=True))


================================================
FILE: tests/test_selenium.py
================================================
import re
import threading
import time
import unittest
from selenium import webdriver
from app import create_app, db, fake
from app.models import Role, User, Post


class SeleniumTestCase(unittest.TestCase):
    client = None
    
    @classmethod
    def setUpClass(cls):
        # start Chrome
        options = webdriver.ChromeOptions()
        options.add_argument('headless')
        try:
            cls.client = webdriver.Chrome(chrome_options=options)
        except:
            pass

        # skip these tests if the browser could not be started
        if cls.client:
            # create the application
            cls.app = create_app('testing')
            cls.app_context = cls.app.app_context()
            cls.app_context.push()

            # suppress logging to keep unittest output clean
            import logging
            logger = logging.getLogger('werkzeug')
            logger.setLevel("ERROR")

            # create the database and populate with some fake data
            db.create_all()
            Role.insert_roles()
            fake.users(10)
            fake.posts(10)

            # add an administrator user
            admin_role = Role.query.filter_by(name='Administrator').first()
            admin = User(email='john@example.com',
                         username='john', password='cat',
                         role=admin_role, confirmed=True)
            db.session.add(admin)
            db.session.commit()

            # start the Flask server in a thread
            cls.server_thread = threading.Thread(target=cls.app.run,
                                                 kwargs={'debug': False})
            cls.server_thread.start()

            # give the server a second to ensure it is up
            time.sleep(1) 

    @classmethod
    def tearDownClass(cls):
        if cls.client:
            # stop the flask server and the browser
            cls.client.get('http://localhost:5000/shutdown')
            cls.client.quit()
            cls.server_thread.join()

            # destroy database
            db.drop_all()
            db.session.remove()

            # remove application context
            cls.app_context.pop()

    def setUp(self):
        if not self.client:
            self.skipTest('Web browser not available')

    def tearDown(self):
        pass
    
    def test_admin_home_page(self):
        # navigate to home page
        self.client.get('http://localhost:5000/')
        self.assertTrue(re.search('Hello,\s+Stranger!',
                                  self.client.page_source))

        # navigate to login page
        self.client.find_element_by_link_text('Log In').click()
        self.assertIn('<h1>Login</h1>', self.client.page_source)

        # login
        self.client.find_element_by_name('email').\
            send_keys('john@example.com')
        self.client.find_element_by_name('password').send_keys('cat')
        self.client.find_element_by_name('submit').click()
        self.assertTrue(re.search('Hello,\s+john!', self.client.page_source))

        # navigate to the user's profile page
        self.client.find_element_by_link_text('Profile').click()
        self.assertIn('<h1>john</h1>', self.client.page_source)


================================================
FILE: tests/test_user_model.py
================================================
import unittest
import time
from datetime import datetime
from app import create_app, db
from app.models import User, AnonymousUser, Role, Permission, Follow


class UserModelTestCase(unittest.TestCase):
    def setUp(self):
        self.app = create_app('testing')
        self.app_context = self.app.app_context()
        self.app_context.push()
        db.create_all()
        Role.insert_roles()

    def tearDown(self):
        db.session.remove()
        db.drop_all()
        self.app_context.pop()

    def test_password_setter(self):
        u = User(password='cat')
        self.assertTrue(u.password_hash is not None)

    def test_no_password_getter(self):
        u = User(password='cat')
        with self.assertRaises(AttributeError):
            u.password

    def test_password_verification(self):
        u = User(password='cat')
        self.assertTrue(u.verify_password('cat'))
        self.assertFalse(u.verify_password('dog'))

    def test_password_salts_are_random(self):
        u = User(password='cat')
        u2 = User(password='cat')
        self.assertTrue(u.password_hash != u2.password_hash)

    def test_valid_confirmation_token(self):
        u = User(password='cat')
        db.session.add(u)
        db.session.commit()
        token = u.generate_confirmation_token()
        self.assertTrue(u.confirm(token))

    def test_invalid_confirmation_token(self):
        u1 = User(password='cat')
        u2 = User(password='dog')
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        token = u1.generate_confirmation_token()
        self.assertFalse(u2.confirm(token))

    def test_expired_confirmation_token(self):
        u = User(password='cat')
        db.session.add(u)
        db.session.commit()
        token = u.generate_confirmation_token(1)
        time.sleep(2)
        self.assertFalse(u.confirm(token))

    def test_valid_reset_token(self):
        u = User(password='cat')
        db.session.add(u)
        db.session.commit()
        token = u.generate_reset_token()
        self.assertTrue(User.reset_password(token, 'dog'))
        self.assertTrue(u.verify_password('dog'))

    def test_invalid_reset_token(self):
        u = User(password='cat')
        db.session.add(u)
        db.session.commit()
        token = u.generate_reset_token()
        self.assertFalse(User.reset_password(token + 'a', 'horse'))
        self.assertTrue(u.verify_password('cat'))

    def test_valid_email_change_token(self):
        u = User(email='john@example.com', password='cat')
        db.session.add(u)
        db.session.commit()
        token = u.generate_email_change_token('susan@example.org')
        self.assertTrue(u.change_email(token))
        self.assertTrue(u.email == 'susan@example.org')

    def test_invalid_email_change_token(self):
        u1 = User(email='john@example.com', password='cat')
        u2 = User(email='susan@example.org', password='dog')
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        token = u1.generate_email_change_token('david@example.net')
        self.assertFalse(u2.change_email(token))
        self.assertTrue(u2.email == 'susan@example.org')

    def test_duplicate_email_change_token(self):
        u1 = User(email='john@example.com', password='cat')
        u2 = User(email='susan@example.org', password='dog')
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        token = u2.generate_email_change_token('john@example.com')
        self.assertFalse(u2.change_email(token))
        self.assertTrue(u2.email == 'susan@example.org')

    def test_user_role(self):
        u = User(email='john@example.com', password='cat')
        self.assertTrue(u.can(Permission.FOLLOW))
        self.assertTrue(u.can(Permission.COMMENT))
        self.assertTrue(u.can(Permission.WRITE))
        self.assertFalse(u.can(Permission.MODERATE))
        self.assertFalse(u.can(Permission.ADMIN))

    def test_moderator_role(self):
        r = Role.query.filter_by(name='Moderator').first()
        u = User(email='john@example.com', password='cat', role=r)
        self.assertTrue(u.can(Permission.FOLLOW))
        self.assertTrue(u.can(Permission.COMMENT))
        self.assertTrue(u.can(Permission.WRITE))
        self.assertTrue(u.can(Permission.MODERATE))
        self.assertFalse(u.can(Permission.ADMIN))

    def test_administrator_role(self):
        r = Role.query.filter_by(name='Administrator').first()
        u = User(email='john@example.com', password='cat', role=r)
        self.assertTrue(u.can(Permission.FOLLOW))
        self.assertTrue(u.can(Permission.COMMENT))
        self.assertTrue(u.can(Permission.WRITE))
        self.assertTrue(u.can(Permission.MODERATE))
        self.assertTrue(u.can(Permission.ADMIN))

    def test_anonymous_user(self):
        u = AnonymousUser()
        self.assertFalse(u.can(Permission.FOLLOW))
        self.assertFalse(u.can(Permission.COMMENT))
        self.assertFalse(u.can(Permission.WRITE))
        self.assertFalse(u.can(Permission.MODERATE))
        self.assertFalse(u.can(Permission.ADMIN))

    def test_timestamps(self):
        u = User(password='cat')
        db.session.add(u)
        db.session.commit()
        self.assertTrue(
            (datetime.utcnow() - u.member_since).total_seconds() < 3)
        self.assertTrue(
            (datetime.utcnow() - u.last_seen).total_seconds() < 3)

    def test_ping(self):
        u = User(password='cat')
        db.session.add(u)
        db.session.commit()
        time.sleep(2)
        last_seen_before = u.last_seen
        u.ping()
        self.assertTrue(u.last_seen > last_seen_before)

    def test_gravatar(self):
        u = User(email='john@example.com', password='cat')
        with self.app.test_request_context('/'):
            gravatar = u.gravatar()
            gravatar_256 = u.gravatar(size=256)
            gravatar_pg = u.gravatar(rating='pg')
            gravatar_retro = u.gravatar(default='retro')
        self.assertTrue('https://secure.gravatar.com/avatar/' +
                        'd4c74594d841139328695756648b6bd6'in gravatar)
        self.assertTrue('s=256' in gravatar_256)
        self.assertTrue('r=pg' in gravatar_pg)
        self.assertTrue('d=retro' in gravatar_retro)

    def test_follows(self):
        u1 = User(email='john@example.com', password='cat')
        u2 = User(email='susan@example.org', password='dog')
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        self.assertFalse(u1.is_following(u2))
        self.assertFalse(u1.is_followed_by(u2))
        timestamp_before = datetime.utcnow()
        u1.follow(u2)
        db.session.add(u1)
        db.session.commit()
        timestamp_after = datetime.utcnow()
        self.assertTrue(u1.is_following(u2))
        self.assertFalse(u1.is_followed_by(u2))
        self.assertTrue(u2.is_followed_by(u1))
        self.assertTrue(u1.followed.count() == 2)
        self.assertTrue(u2.followers.count() == 2)
        f = u1.followed.all()[-1]
        self.assertTrue(f.followed == u2)
        self.assertTrue(timestamp_before <= f.timestamp <= timestamp_after)
        f = u2.followers.all()[-1]
        self.assertTrue(f.follower == u1)
        u1.unfollow(u2)
        db.session.add(u1)
        db.session.commit()
        self.assertTrue(u1.followed.count() == 1)
        self.assertTrue(u2.followers.count() == 1)
        self.assertTrue(Follow.query.count() == 2)
        u2.follow(u1)
        db.session.add(u1)
        db.session.add(u2)
        db.session.commit()
        db.session.delete(u2)
        db.session.commit()
        self.assertTrue(Follow.query.count() == 1)

    def test_to_json(self):
        u = User(email='john@example.com', password='cat')
        db.session.add(u)
        db.session.commit()
        with self.app.test_request_context('/'):
            json_user = u.to_json()
        expected_keys = ['url', 'username', 'member_since', 'last_seen',
                         'posts_url', 'followed_posts_url', 'post_count']
        self.assertEqual(sorted(json_user.keys()), sorted(expected_keys))
        self.assertEqual('/api/v1/users/' + str(u.id), json_user['url'])
Download .txt
gitextract_ml7yrowa/

├── .gitignore
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── app/
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   ├── authentication.py
│   │   ├── comments.py
│   │   ├── decorators.py
│   │   ├── errors.py
│   │   ├── posts.py
│   │   └── users.py
│   ├── auth/
│   │   ├── __init__.py
│   │   ├── forms.py
│   │   └── views.py
│   ├── decorators.py
│   ├── email.py
│   ├── exceptions.py
│   ├── fake.py
│   ├── main/
│   │   ├── __init__.py
│   │   ├── errors.py
│   │   ├── forms.py
│   │   └── views.py
│   ├── models.py
│   ├── static/
│   │   └── styles.css
│   └── templates/
│       ├── 403.html
│       ├── 404.html
│       ├── 500.html
│       ├── _comments.html
│       ├── _macros.html
│       ├── _posts.html
│       ├── auth/
│       │   ├── change_email.html
│       │   ├── change_password.html
│       │   ├── email/
│       │   │   ├── change_email.html
│       │   │   ├── change_email.txt
│       │   │   ├── confirm.html
│       │   │   ├── confirm.txt
│       │   │   ├── reset_password.html
│       │   │   └── reset_password.txt
│       │   ├── login.html
│       │   ├── register.html
│       │   ├── reset_password.html
│       │   └── unconfirmed.html
│       ├── base.html
│       ├── edit_post.html
│       ├── edit_profile.html
│       ├── followers.html
│       ├── index.html
│       ├── mail/
│       │   ├── new_user.html
│       │   └── new_user.txt
│       ├── moderate.html
│       ├── post.html
│       └── user.html
├── boot.sh
├── config.py
├── docker-compose.yml
├── flasky.py
├── migrations/
│   ├── README
│   ├── alembic.ini
│   ├── env.py
│   ├── script.py.mako
│   └── versions/
│       ├── 190163627111_account_confirmation.py
│       ├── 198b0eebcf9_caching_of_avatar_hashes.py
│       ├── 1b966e7f4b9e_post_model.py
│       ├── 2356a38169ea_followers.py
│       ├── 288cd3dc5a8_rich_text_posts.py
│       ├── 38c4e85512a9_initial_migration.py
│       ├── 456a945560f6_login_support.py
│       ├── 51f5ccfba190_comments.py
│       ├── 56ed7d33de8d_user_roles.py
│       └── d66f086b258_user_information.py
├── requirements/
│   ├── common.txt
│   ├── dev.txt
│   ├── docker.txt
│   ├── heroku.txt
│   └── prod.txt
├── requirements.txt
└── tests/
    ├── __init__.py
    ├── test_api.py
    ├── test_basics.py
    ├── test_client.py
    ├── test_selenium.py
    └── test_user_model.py
Download .txt
SYMBOL INDEX (217 symbols across 36 files)

FILE: app/__init__.py
  function create_app (line 20) | def create_app(config_name):

FILE: app/api/authentication.py
  function verify_password (line 11) | def verify_password(email_or_token, password):
  function auth_error (line 27) | def auth_error():
  function before_request (line 33) | def before_request():
  function get_token (line 40) | def get_token():

FILE: app/api/comments.py
  function get_comments (line 9) | def get_comments():
  function get_comment (line 30) | def get_comment(id):
  function get_post_comments (line 36) | def get_post_comments(id):
  function new_post_comment (line 59) | def new_post_comment(id):

FILE: app/api/decorators.py
  function permission_required (line 6) | def permission_required(permission):

FILE: app/api/errors.py
  function bad_request (line 6) | def bad_request(message):
  function unauthorized (line 12) | def unauthorized(message):
  function forbidden (line 18) | def forbidden(message):
  function validation_error (line 25) | def validation_error(e):

FILE: app/api/posts.py
  function get_posts (line 10) | def get_posts():
  function get_post (line 31) | def get_post(id):
  function new_post (line 38) | def new_post():
  function edit_post (line 49) | def edit_post(id):

FILE: app/api/users.py
  function get_user (line 7) | def get_user(id):
  function get_user_posts (line 13) | def get_user_posts(id):
  function get_user_followed_posts (line 35) | def get_user_followed_posts(id):

FILE: app/auth/forms.py
  class LoginForm (line 8) | class LoginForm(FlaskForm):
  class RegistrationForm (line 16) | class RegistrationForm(FlaskForm):
    method validate_email (line 29) | def validate_email(self, field):
    method validate_username (line 33) | def validate_username(self, field):
  class ChangePasswordForm (line 38) | class ChangePasswordForm(FlaskForm):
  class PasswordResetRequestForm (line 47) | class PasswordResetRequestForm(FlaskForm):
  class PasswordResetForm (line 53) | class PasswordResetForm(FlaskForm):
  class ChangeEmailForm (line 60) | class ChangeEmailForm(FlaskForm):
    method validate_email (line 66) | def validate_email(self, field):

FILE: app/auth/views.py
  function before_request (line 13) | def before_request():
  function unconfirmed (line 24) | def unconfirmed():
  function login (line 31) | def login():
  function logout (line 47) | def logout():
  function register (line 54) | def register():
  function confirm (line 72) | def confirm(token):
  function resend_confirmation (line 85) | def resend_confirmation():
  function change_password (line 95) | def change_password():
  function password_reset_request (line 110) | def password_reset_request():
  function password_reset (line 128) | def password_reset(token):
  function change_email_request (line 144) | def change_email_request():
  function change_email (line 163) | def change_email(token):

FILE: app/decorators.py
  function permission_required (line 7) | def permission_required(permission):
  function admin_required (line 18) | def admin_required(f):

FILE: app/email.py
  function send_async_email (line 7) | def send_async_email(app, msg):
  function send_email (line 12) | def send_email(to, subject, template, **kwargs):

FILE: app/exceptions.py
  class ValidationError (line 1) | class ValidationError(ValueError):

FILE: app/fake.py
  function users (line 8) | def users(count=100):
  function posts (line 28) | def posts(count=100):

FILE: app/main/__init__.py
  function inject_permissions (line 10) | def inject_permissions():

FILE: app/main/errors.py
  function forbidden (line 6) | def forbidden(e):
  function page_not_found (line 16) | def page_not_found(e):
  function internal_server_error (line 26) | def internal_server_error(e):

FILE: app/main/forms.py
  class NameForm (line 10) | class NameForm(FlaskForm):
  class EditProfileForm (line 15) | class EditProfileForm(FlaskForm):
  class EditProfileAdminForm (line 22) | class EditProfileAdminForm(FlaskForm):
    method __init__ (line 37) | def __init__(self, user, *args, **kwargs):
    method validate_email (line 43) | def validate_email(self, field):
    method validate_username (line 48) | def validate_username(self, field):
  class PostForm (line 54) | class PostForm(FlaskForm):
  class CommentForm (line 59) | class CommentForm(FlaskForm):

FILE: app/main/views.py
  function after_request (line 14) | def after_request(response):
  function server_shutdown (line 25) | def server_shutdown():
  function index (line 36) | def index():
  function user (line 61) | def user(username):
  function edit_profile (line 74) | def edit_profile():
  function edit_profile_admin (line 93) | def edit_profile_admin(id):
  function post (line 119) | def post(id):
  function edit (line 144) | def edit(id):
  function follow (line 163) | def follow(username):
  function unfollow (line 180) | def unfollow(username):
  function followers (line 195) | def followers(username):
  function followed_by (line 212) | def followed_by(username):
  function show_all (line 230) | def show_all():
  function show_followed (line 238) | def show_followed():
  function moderate (line 247) | def moderate():
  function moderate_enable (line 260) | def moderate_enable(id):
  function moderate_disable (line 272) | def moderate_disable(id):

FILE: app/models.py
  class Permission (line 13) | class Permission:
  class Role (line 21) | class Role(db.Model):
    method __init__ (line 29) | def __init__(self, **kwargs):
    method insert_roles (line 35) | def insert_roles():
    method add_permission (line 56) | def add_permission(self, perm):
    method remove_permission (line 60) | def remove_permission(self, perm):
    method reset_permissions (line 64) | def reset_permissions(self):
    method has_permission (line 67) | def has_permission(self, perm):
    method __repr__ (line 70) | def __repr__(self):
  class Follow (line 74) | class Follow(db.Model):
  class User (line 83) | class User(UserMixin, db.Model):
    method add_self_follows (line 111) | def add_self_follows():
    method __init__ (line 118) | def __init__(self, **kwargs):
    method password (line 130) | def password(self):
    method password (line 134) | def password(self, password):
    method verify_password (line 137) | def verify_password(self, password):
    method generate_confirmation_token (line 140) | def generate_confirmation_token(self, expiration=3600):
    method confirm (line 144) | def confirm(self, token):
    method generate_reset_token (line 156) | def generate_reset_token(self, expiration=3600):
    method reset_password (line 161) | def reset_password(token, new_password):
    method generate_email_change_token (line 174) | def generate_email_change_token(self, new_email, expiration=3600):
    method change_email (line 179) | def change_email(self, token):
    method can (line 197) | def can(self, perm):
    method is_administrator (line 200) | def is_administrator(self):
    method ping (line 203) | def ping(self):
    method gravatar_hash (line 207) | def gravatar_hash(self):
    method gravatar (line 210) | def gravatar(self, size=100, default='identicon', rating='g'):
    method follow (line 216) | def follow(self, user):
    method unfollow (line 221) | def unfollow(self, user):
    method is_following (line 226) | def is_following(self, user):
    method is_followed_by (line 232) | def is_followed_by(self, user):
    method followed_posts (line 239) | def followed_posts(self):
    method to_json (line 243) | def to_json(self):
    method generate_auth_token (line 256) | def generate_auth_token(self, expiration):
    method verify_auth_token (line 262) | def verify_auth_token(token):
    method __repr__ (line 270) | def __repr__(self):
  class AnonymousUser (line 274) | class AnonymousUser(AnonymousUserMixin):
    method can (line 275) | def can(self, permissions):
    method is_administrator (line 278) | def is_administrator(self):
  function load_user (line 285) | def load_user(user_id):
  class Post (line 289) | class Post(db.Model):
    method on_changed_body (line 299) | def on_changed_body(target, value, oldvalue, initiator):
    method to_json (line 307) | def to_json(self):
    method from_json (line 320) | def from_json(json_post):
  class Comment (line 330) | class Comment(db.Model):
    method on_changed_body (line 341) | def on_changed_body(target, value, oldvalue, initiator):
    method to_json (line 348) | def to_json(self):
    method from_json (line 360) | def from_json(json_comment):

FILE: config.py
  class Config (line 5) | class Config:
    method init_app (line 25) | def init_app(app):
  class DevelopmentConfig (line 29) | class DevelopmentConfig(Config):
  class TestingConfig (line 35) | class TestingConfig(Config):
  class ProductionConfig (line 42) | class ProductionConfig(Config):
    method init_app (line 48) | def init_app(cls, app):
  class HerokuConfig (line 71) | class HerokuConfig(ProductionConfig):
    method init_app (line 75) | def init_app(cls, app):
  class DockerConfig (line 93) | class DockerConfig(ProductionConfig):
    method init_app (line 95) | def init_app(cls, app):
  class UnixConfig (line 106) | class UnixConfig(ProductionConfig):
    method init_app (line 108) | def init_app(cls, app):

FILE: flasky.py
  function make_shell_context (line 25) | def make_shell_context():
  function test (line 34) | def test(coverage, test_names):
  function profile (line 64) | def profile(length, profile_dir):
  function deploy (line 73) | def deploy():

FILE: migrations/env.py
  function run_migrations_offline (line 27) | def run_migrations_offline():
  function run_migrations_online (line 45) | def run_migrations_online():

FILE: migrations/versions/190163627111_account_confirmation.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 23) | def downgrade():

FILE: migrations/versions/198b0eebcf9_caching_of_avatar_hashes.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 23) | def downgrade():

FILE: migrations/versions/1b966e7f4b9e_post_model.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 31) | def downgrade():

FILE: migrations/versions/2356a38169ea_followers.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 30) | def downgrade():

FILE: migrations/versions/288cd3dc5a8_rich_text_posts.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 23) | def downgrade():

FILE: migrations/versions/38c4e85512a9_initial_migration.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 36) | def downgrade():

FILE: migrations/versions/456a945560f6_login_support.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 25) | def downgrade():

FILE: migrations/versions/51f5ccfba190_comments.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 35) | def downgrade():

FILE: migrations/versions/56ed7d33de8d_user_roles.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 25) | def downgrade():

FILE: migrations/versions/d66f086b258_user_information.py
  function upgrade (line 17) | def upgrade():
  function downgrade (line 27) | def downgrade():

FILE: tests/test_api.py
  class APITestCase (line 9) | class APITestCase(unittest.TestCase):
    method setUp (line 10) | def setUp(self):
    method tearDown (line 18) | def tearDown(self):
    method get_api_headers (line 23) | def get_api_headers(self, username, password):
    method test_404 (line 31) | def test_404(self):
    method test_no_auth (line 39) | def test_no_auth(self):
    method test_bad_auth (line 44) | def test_bad_auth(self):
    method test_token_auth (line 59) | def test_token_auth(self):
    method test_anonymous (line 89) | def test_anonymous(self):
    method test_unconfirmed_account (line 95) | def test_unconfirmed_account(self):
    method test_posts (line 110) | def test_posts(self):
    method test_users (line 178) | def test_users(self):
    method test_comments (line 203) | def test_comments(self):

FILE: tests/test_basics.py
  class BasicsTestCase (line 6) | class BasicsTestCase(unittest.TestCase):
    method setUp (line 7) | def setUp(self):
    method tearDown (line 13) | def tearDown(self):
    method test_app_exists (line 18) | def test_app_exists(self):
    method test_app_is_testing (line 21) | def test_app_is_testing(self):

FILE: tests/test_client.py
  class FlaskClientTestCase (line 6) | class FlaskClientTestCase(unittest.TestCase):
    method setUp (line 7) | def setUp(self):
    method tearDown (line 15) | def tearDown(self):
    method test_home_page (line 20) | def test_home_page(self):
    method test_register_and_login (line 25) | def test_register_and_login(self):

FILE: tests/test_selenium.py
  class SeleniumTestCase (line 10) | class SeleniumTestCase(unittest.TestCase):
    method setUpClass (line 14) | def setUpClass(cls):
    method tearDownClass (line 58) | def tearDownClass(cls):
    method setUp (line 72) | def setUp(self):
    method tearDown (line 76) | def tearDown(self):
    method test_admin_home_page (line 79) | def test_admin_home_page(self):

FILE: tests/test_user_model.py
  class UserModelTestCase (line 8) | class UserModelTestCase(unittest.TestCase):
    method setUp (line 9) | def setUp(self):
    method tearDown (line 16) | def tearDown(self):
    method test_password_setter (line 21) | def test_password_setter(self):
    method test_no_password_getter (line 25) | def test_no_password_getter(self):
    method test_password_verification (line 30) | def test_password_verification(self):
    method test_password_salts_are_random (line 35) | def test_password_salts_are_random(self):
    method test_valid_confirmation_token (line 40) | def test_valid_confirmation_token(self):
    method test_invalid_confirmation_token (line 47) | def test_invalid_confirmation_token(self):
    method test_expired_confirmation_token (line 56) | def test_expired_confirmation_token(self):
    method test_valid_reset_token (line 64) | def test_valid_reset_token(self):
    method test_invalid_reset_token (line 72) | def test_invalid_reset_token(self):
    method test_valid_email_change_token (line 80) | def test_valid_email_change_token(self):
    method test_invalid_email_change_token (line 88) | def test_invalid_email_change_token(self):
    method test_duplicate_email_change_token (line 98) | def test_duplicate_email_change_token(self):
    method test_user_role (line 108) | def test_user_role(self):
    method test_moderator_role (line 116) | def test_moderator_role(self):
    method test_administrator_role (line 125) | def test_administrator_role(self):
    method test_anonymous_user (line 134) | def test_anonymous_user(self):
    method test_timestamps (line 142) | def test_timestamps(self):
    method test_ping (line 151) | def test_ping(self):
    method test_gravatar (line 160) | def test_gravatar(self):
    method test_follows (line 173) | def test_follows(self):
    method test_to_json (line 210) | def test_to_json(self):
Condensed preview — 84 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (124K chars).
[
  {
    "path": ".gitignore",
    "chars": 409,
    "preview": "*.py[cod]\n\n# C extensions\n*.so\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed."
  },
  {
    "path": "Dockerfile",
    "chars": 381,
    "preview": "FROM python:3.6-alpine\n\nENV FLASK_APP flasky.py\nENV FLASK_CONFIG production\n\nRUN adduser -D flasky\nUSER flasky\n\nWORKDIR "
  },
  {
    "path": "LICENSE",
    "chars": 1083,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2013 Miguel Grinberg\n\nPermission is hereby granted, free of charge, to any person o"
  },
  {
    "path": "Procfile",
    "chars": 25,
    "preview": "web: gunicorn flasky:app\n"
  },
  {
    "path": "README.md",
    "chars": 693,
    "preview": "Flasky\n======\n\nThis repository contains the source code examples for the second edition of my O'Reilly book [Flask Web D"
  },
  {
    "path": "app/__init__.py",
    "chars": 1156,
    "preview": "from flask import Flask\nfrom flask_bootstrap import Bootstrap\nfrom flask_mail import Mail\nfrom flask_moment import Momen"
  },
  {
    "path": "app/api/__init__.py",
    "chars": 124,
    "preview": "from flask import Blueprint\n\napi = Blueprint('api', __name__)\n\nfrom . import authentication, posts, users, comments, err"
  },
  {
    "path": "app/api/authentication.py",
    "chars": 1223,
    "preview": "from flask import g, jsonify\nfrom flask_httpauth import HTTPBasicAuth\nfrom ..models import User\nfrom . import api\nfrom ."
  },
  {
    "path": "app/api/comments.py",
    "chars": 2190,
    "preview": "from flask import jsonify, request, g, url_for, current_app\nfrom .. import db\nfrom ..models import Post, Permission, Com"
  },
  {
    "path": "app/api/decorators.py",
    "chars": 411,
    "preview": "from functools import wraps\nfrom flask import g\nfrom .errors import forbidden\n\n\ndef permission_required(permission):\n   "
  },
  {
    "path": "app/api/errors.py",
    "chars": 625,
    "preview": "from flask import jsonify\nfrom app.exceptions import ValidationError\nfrom . import api\n\n\ndef bad_request(message):\n    r"
  },
  {
    "path": "app/api/posts.py",
    "chars": 1688,
    "preview": "from flask import jsonify, request, g, url_for, current_app\nfrom .. import db\nfrom ..models import Post, Permission\nfrom"
  },
  {
    "path": "app/api/users.py",
    "chars": 1714,
    "preview": "from flask import jsonify, request, current_app, url_for\nfrom . import api\nfrom ..models import User, Post\n\n\n@api.route("
  },
  {
    "path": "app/auth/__init__.py",
    "chars": 85,
    "preview": "from flask import Blueprint\n\nauth = Blueprint('auth', __name__)\n\nfrom . import views\n"
  },
  {
    "path": "app/auth/forms.py",
    "chars": 2941,
    "preview": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, PasswordField, BooleanField, SubmitField\nfrom wtforms.v"
  },
  {
    "path": "app/auth/views.py",
    "chars": 6132,
    "preview": "from flask import render_template, redirect, request, url_for, flash\nfrom flask_login import login_user, logout_user, lo"
  },
  {
    "path": "app/decorators.py",
    "chars": 494,
    "preview": "from functools import wraps\nfrom flask import abort\nfrom flask_login import current_user\nfrom .models import Permission\n"
  },
  {
    "path": "app/email.py",
    "chars": 669,
    "preview": "from threading import Thread\nfrom flask import current_app, render_template\nfrom flask_mail import Message\nfrom . import"
  },
  {
    "path": "app/exceptions.py",
    "chars": 44,
    "preview": "class ValidationError(ValueError):\n    pass\n"
  },
  {
    "path": "app/fake.py",
    "chars": 1012,
    "preview": "from random import randint\nfrom sqlalchemy.exc import IntegrityError\nfrom faker import Faker\nfrom . import db\nfrom .mode"
  },
  {
    "path": "app/main/__init__.py",
    "chars": 220,
    "preview": "from flask import Blueprint\n\nmain = Blueprint('main', __name__)\n\nfrom . import views, errors\nfrom ..models import Permis"
  },
  {
    "path": "app/main/errors.py",
    "chars": 1018,
    "preview": "from flask import render_template, request, jsonify\nfrom . import main\n\n\n@main.app_errorhandler(403)\ndef forbidden(e):\n "
  },
  {
    "path": "app/main/forms.py",
    "chars": 2375,
    "preview": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, TextAreaField, BooleanField, SelectField,\\\n    SubmitFi"
  },
  {
    "path": "app/main/views.py",
    "chars": 10109,
    "preview": "from flask import render_template, redirect, url_for, abort, flash, request,\\\n    current_app, make_response\nfrom flask_"
  },
  {
    "path": "app/models.py",
    "chars": 12892,
    "preview": "from datetime import datetime\nimport hashlib\nfrom werkzeug.security import generate_password_hash, check_password_hash\nf"
  },
  {
    "path": "app/static/styles.css",
    "chars": 1855,
    "preview": ".profile-thumbnail {\n    position: absolute;\n}\n.profile-header {\n    min-height: 260px;\n    margin-left: 280px;\n}\ndiv.po"
  },
  {
    "path": "app/templates/403.html",
    "chars": 174,
    "preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Forbidden{% endblock %}\n\n{% block page_content %}\n<div class=\"page-"
  },
  {
    "path": "app/templates/404.html",
    "chars": 179,
    "preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Page Not Found{% endblock %}\n\n{% block page_content %}\n<div class=\""
  },
  {
    "path": "app/templates/500.html",
    "chars": 198,
    "preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Internal Server Error{% endblock %}\n\n{% block page_content %}\n<div "
  },
  {
    "path": "app/templates/_comments.html",
    "chars": 1585,
    "preview": "<ul class=\"comments\">\n    {% for comment in comments %}\n    <li class=\"comment\">\n        <div class=\"comment-thumbnail\">"
  },
  {
    "path": "app/templates/_macros.html",
    "chars": 1163,
    "preview": "{% macro pagination_widget(pagination, endpoint, fragment='') %}\n<ul class=\"pagination\">\n    <li{% if not pagination.has"
  },
  {
    "path": "app/templates/_posts.html",
    "chars": 1698,
    "preview": "<ul class=\"posts\">\n    {% for post in posts %}\n    <li class=\"post\">\n        <div class=\"post-thumbnail\">\n            <a"
  },
  {
    "path": "app/templates/auth/change_email.html",
    "chars": 302,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Change Email Address{% end"
  },
  {
    "path": "app/templates/auth/change_password.html",
    "chars": 292,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Change Password{% endblock"
  },
  {
    "path": "app/templates/auth/email/change_email.html",
    "chars": 439,
    "preview": "<p>Dear {{ user.username }},</p>\n<p>To confirm your new email address <a href=\"{{ url_for('auth.change_email', token=tok"
  },
  {
    "path": "app/templates/auth/email/change_email.txt",
    "chars": 240,
    "preview": "Dear {{ user.username }},\n\nTo confirm your new email address click on the following link:\n\n{{ url_for('auth.change_email"
  },
  {
    "path": "app/templates/auth/email/confirm.html",
    "chars": 459,
    "preview": "<p>Dear {{ user.username }},</p>\n<p>Welcome to <b>Flasky</b>!</p>\n<p>To confirm your account please <a href=\"{{ url_for("
  },
  {
    "path": "app/templates/auth/email/confirm.txt",
    "chars": 252,
    "preview": "Dear {{ user.username }},\n\nWelcome to Flasky!\n\nTo confirm your account please click on the following link:\n\n{{ url_for('"
  },
  {
    "path": "app/templates/auth/email/reset_password.html",
    "chars": 510,
    "preview": "<p>Dear {{ user.username }},</p>\n<p>To reset your password <a href=\"{{ url_for('auth.password_reset', token=token, _exte"
  },
  {
    "path": "app/templates/auth/email/reset_password.txt",
    "chars": 303,
    "preview": "Dear {{ user.username }},\n\nTo reset your password click on the following link:\n\n{{ url_for('auth.password_reset', token="
  },
  {
    "path": "app/templates/auth/login.html",
    "chars": 483,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Login{% endblock %}\n\n{% bl"
  },
  {
    "path": "app/templates/auth/register.html",
    "chars": 274,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Register{% endblock %}\n\n{%"
  },
  {
    "path": "app/templates/auth/reset_password.html",
    "chars": 290,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Password Reset{% endblock "
  },
  {
    "path": "app/templates/auth/unconfirmed.html",
    "chars": 589,
    "preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Confirm your account{% endblock %}\n\n{% block page_content %}\n<div c"
  },
  {
    "path": "app/templates/base.html",
    "chars": 2893,
    "preview": "{% extends \"bootstrap/base.html\" %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block head %}\n{{ super() }}\n<link rel=\"sh"
  },
  {
    "path": "app/templates/edit_post.html",
    "chars": 343,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Edit Post{% endblock %}\n\n{"
  },
  {
    "path": "app/templates/edit_profile.html",
    "chars": 287,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Edit Profile{% endblock %}"
  },
  {
    "path": "app/templates/followers.html",
    "chars": 907,
    "preview": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - {{ title }} {{ user.username "
  },
  {
    "path": "app/templates/index.html",
    "chars": 1051,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n{% import \"_macros.html\" as macros %}\n\n{% block title"
  },
  {
    "path": "app/templates/mail/new_user.html",
    "chars": 44,
    "preview": "User <b>{{ user.username }}</b> has joined.\n"
  },
  {
    "path": "app/templates/mail/new_user.txt",
    "chars": 37,
    "preview": "User {{ user.username }} has joined.\n"
  },
  {
    "path": "app/templates/moderate.html",
    "chars": 411,
    "preview": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - Comment Moderation{% endblock"
  },
  {
    "path": "app/templates/post.html",
    "chars": 564,
    "preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n{% import \"_macros.html\" as macros %}\n\n{% block title"
  },
  {
    "path": "app/templates/user.html",
    "chars": 2662,
    "preview": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - {{ user.username }}{% endbloc"
  },
  {
    "path": "boot.sh",
    "chars": 263,
    "preview": "#!/bin/sh\nsource venv/bin/activate\n\nwhile true; do\n    flask deploy\n    if [[ \"$?\" == \"0\" ]]; then\n        break\n    fi\n"
  },
  {
    "path": "config.py",
    "chars": 3963,
    "preview": "import os\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n\nclass Config:\n    SECRET_KEY = os.environ.get('SECRET_K"
  },
  {
    "path": "docker-compose.yml",
    "chars": 240,
    "preview": "version: '3'\nservices:\n  flasky:\n    build: .\n    ports:\n      - \"8000:5000\"\n    env_file: .env\n    restart: always\n    "
  },
  {
    "path": "flasky.py",
    "chars": 2534,
    "preview": "import os\nfrom dotenv import load_dotenv\n\ndotenv_path = os.path.join(os.path.dirname(__file__), '.env')\nif os.path.exist"
  },
  {
    "path": "migrations/README",
    "chars": 38,
    "preview": "Generic single-database configuration."
  },
  {
    "path": "migrations/alembic.ini",
    "chars": 770,
    "preview": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%("
  },
  {
    "path": "migrations/env.py",
    "chars": 2158,
    "preview": "from __future__ import with_statement\nfrom alembic import context\nfrom sqlalchemy import engine_from_config, pool\nfrom l"
  },
  {
    "path": "migrations/script.py.mako",
    "chars": 412,
    "preview": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision}\nCreate Date: ${create_date}\n\n\"\"\"\n\n# revision identi"
  },
  {
    "path": "migrations/versions/190163627111_account_confirmation.py",
    "chars": 616,
    "preview": "\"\"\"account confirmation\n\nRevision ID: 190163627111\nRevises: 456a945560f6\nCreate Date: 2013-12-29 02:58:45.577428\n\n\"\"\"\n\n#"
  },
  {
    "path": "migrations/versions/198b0eebcf9_caching_of_avatar_hashes.py",
    "chars": 628,
    "preview": "\"\"\"caching of avatar hashes\n\nRevision ID: 198b0eebcf9\nRevises: d66f086b258\nCreate Date: 2014-02-04 09:10:02.245503\n\n\"\"\"\n"
  },
  {
    "path": "migrations/versions/1b966e7f4b9e_post_model.py",
    "chars": 983,
    "preview": "\"\"\"post model\n\nRevision ID: 1b966e7f4b9e\nRevises: 198b0eebcf9\nCreate Date: 2013-12-31 00:00:14.700591\n\n\"\"\"\n\n# revision i"
  },
  {
    "path": "migrations/versions/2356a38169ea_followers.py",
    "chars": 908,
    "preview": "\"\"\"followers\n\nRevision ID: 2356a38169ea\nRevises: 288cd3dc5a8\nCreate Date: 2013-12-31 16:10:34.500006\n\n\"\"\"\n\n# revision id"
  },
  {
    "path": "migrations/versions/288cd3dc5a8_rich_text_posts.py",
    "chars": 606,
    "preview": "\"\"\"rich text posts\n\nRevision ID: 288cd3dc5a8\nRevises: 1b966e7f4b9e\nCreate Date: 2013-12-31 03:25:13.286503\n\n\"\"\"\n\n# revis"
  },
  {
    "path": "migrations/versions/38c4e85512a9_initial_migration.py",
    "chars": 1163,
    "preview": "\"\"\"initial migration\n\nRevision ID: 38c4e85512a9\nRevises: None\nCreate Date: 2013-12-27 01:23:59.392801\n\n\"\"\"\n\n# revision i"
  },
  {
    "path": "migrations/versions/456a945560f6_login_support.py",
    "chars": 863,
    "preview": "\"\"\"login support\n\nRevision ID: 456a945560f6\nRevises: 38c4e85512a9\nCreate Date: 2013-12-29 00:18:35.795259\n\n\"\"\"\n\n# revisi"
  },
  {
    "path": "migrations/versions/51f5ccfba190_comments.py",
    "chars": 1224,
    "preview": "\"\"\"comments\n\nRevision ID: 51f5ccfba190\nRevises: 2356a38169ea\nCreate Date: 2014-01-01 12:08:43.287523\n\n\"\"\"\n\n# revision id"
  },
  {
    "path": "migrations/versions/56ed7d33de8d_user_roles.py",
    "chars": 850,
    "preview": "\"\"\"user roles\n\nRevision ID: 56ed7d33de8d\nRevises: 190163627111\nCreate Date: 2013-12-29 22:19:54.212604\n\n\"\"\"\n\n# revision "
  },
  {
    "path": "migrations/versions/d66f086b258_user_information.py",
    "chars": 1101,
    "preview": "\"\"\"user information\n\nRevision ID: d66f086b258\nRevises: 56ed7d33de8d\nCreate Date: 2013-12-29 23:50:49.566954\n\n\"\"\"\n\n# revi"
  },
  {
    "path": "requirements/common.txt",
    "chars": 532,
    "preview": "alembic==0.9.3\nbleach==2.0.0\nblinker==1.4\nclick==6.7\ndominate==2.3.1\nFlask==0.12.2\nFlask-Bootstrap==3.3.7.1\nFlask-HTTPAu"
  },
  {
    "path": "requirements/dev.txt",
    "chars": 169,
    "preview": "-r common.txt\ncertifi==2017.7.27.1\nchardet==3.0.4\ncoverage==4.4.1\nfaker==0.7.18\nhttpie==0.9.9\nidna==2.5\nPygments==2.2.0\n"
  },
  {
    "path": "requirements/docker.txt",
    "chars": 47,
    "preview": "-r common.txt\ngunicorn==19.7.1\npymysql==0.7.11\n"
  },
  {
    "path": "requirements/heroku.txt",
    "chars": 65,
    "preview": "-r prod.txt\nFlask-SSLify==0.1.5\ngunicorn==19.7.1\npsycopg2==2.7.3\n"
  },
  {
    "path": "requirements/prod.txt",
    "chars": 14,
    "preview": "-r common.txt\n"
  },
  {
    "path": "requirements.txt",
    "chars": 155,
    "preview": "# this requirements file is used by Heroku\n# requirements for other configurations are located in the requirements subdi"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/test_api.py",
    "chars": 10644,
    "preview": "import unittest\nimport json\nimport re\nfrom base64 import b64encode\nfrom app import create_app, db\nfrom app.models import"
  },
  {
    "path": "tests/test_basics.py",
    "chars": 563,
    "preview": "import unittest\nfrom flask import current_app\nfrom app import create_app, db\n\n\nclass BasicsTestCase(unittest.TestCase):\n"
  },
  {
    "path": "tests/test_client.py",
    "chars": 2266,
    "preview": "import re\nimport unittest\nfrom app import create_app, db\nfrom app.models import User, Role\n\nclass FlaskClientTestCase(un"
  },
  {
    "path": "tests/test_selenium.py",
    "chars": 3228,
    "preview": "import re\nimport threading\nimport time\nimport unittest\nfrom selenium import webdriver\nfrom app import create_app, db, fa"
  },
  {
    "path": "tests/test_user_model.py",
    "chars": 8222,
    "preview": "import unittest\nimport time\nfrom datetime import datetime\nfrom app import create_app, db\nfrom app.models import User, An"
  }
]

About this extraction

This page contains the full source code of the miguelgrinberg/flasky GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 84 files (112.7 KB), approximately 28.8k tokens, and a symbol index with 217 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!