[
  {
    "path": ".gitignore",
    "content": "*.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.cfg\nlib\nlib64\n__pycache__\n\n# Installer logs\npip-log.txt\n\n# Unit test / coverage reports\n.coverage\n.tox\nnosetests.xml\n\n# Translations\n*.mo\n\n# Mr Developer\n.mr.developer.cfg\n.project\n.pydevproject\n\n# SQLite databases\n*.sqlite\n\n# Virtual environment\nvenv\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2013 Miguel Grinberg\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n"
  },
  {
    "path": "README.md",
    "content": "Flasky\n======\n\n**NOTE: This repository is unmaintained. Refer to my newer projects if you are interested in learning Flask.**\n\nThis repository contains the archived source code examples for my O'Reilly book [Flask Web Development](http://www.flaskbook.com), first edition. For the code examples for the current edition of the book, go to [https://github.com/miguelgrinberg/flasky](https://github.com/miguelgrinberg/flasky).\n\nThe 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.\n\n"
  },
  {
    "path": "app/__init__.py",
    "content": "from flask import Flask\nfrom flask_bootstrap import Bootstrap\nfrom flask_mail import Mail\nfrom flask_moment import Moment\nfrom flask_sqlalchemy import SQLAlchemy\nfrom flask_login import LoginManager\nfrom flask_pagedown import PageDown\nfrom config import config\n\nbootstrap = Bootstrap()\nmail = Mail()\nmoment = Moment()\ndb = SQLAlchemy()\npagedown = PageDown()\n\nlogin_manager = LoginManager()\nlogin_manager.session_protection = 'strong'\nlogin_manager.login_view = 'auth.login'\n\n\ndef create_app(config_name):\n    app = Flask(__name__)\n    app.config.from_object(config[config_name])\n    config[config_name].init_app(app)\n\n    bootstrap.init_app(app)\n    mail.init_app(app)\n    moment.init_app(app)\n    db.init_app(app)\n    login_manager.init_app(app)\n    pagedown.init_app(app)\n\n    if not app.debug and not app.testing and not app.config['SSL_DISABLE']:\n        from flask_sslify import SSLify\n        sslify = SSLify(app)\n\n    from .main import main as main_blueprint\n    app.register_blueprint(main_blueprint)\n\n    from .auth import auth as auth_blueprint\n    app.register_blueprint(auth_blueprint, url_prefix='/auth')\n\n    from .api_1_0 import api as api_1_0_blueprint\n    app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')\n\n    return app\n"
  },
  {
    "path": "app/api_1_0/__init__.py",
    "content": "from flask import Blueprint\n\napi = Blueprint('api', __name__)\n\nfrom . import authentication, posts, users, comments, errors\n"
  },
  {
    "path": "app/api_1_0/authentication.py",
    "content": "from flask import g, jsonify\nfrom flask_httpauth import HTTPBasicAuth\nfrom ..models import User, AnonymousUser\nfrom . import api\nfrom .errors import unauthorized, forbidden\n\nauth = HTTPBasicAuth()\n\n\n@auth.verify_password\ndef verify_password(email_or_token, password):\n    if email_or_token == '':\n        g.current_user = AnonymousUser()\n        return True\n    if password == '':\n        g.current_user = User.verify_auth_token(email_or_token)\n        g.token_used = True\n        return g.current_user is not None\n    user = User.query.filter_by(email=email_or_token).first()\n    if not user:\n        return False\n    g.current_user = user\n    g.token_used = False\n    return user.verify_password(password)\n\n\n@auth.error_handler\ndef auth_error():\n    return unauthorized('Invalid credentials')\n\n\n@api.before_request\n@auth.login_required\ndef before_request():\n    if not g.current_user.is_anonymous and \\\n            not g.current_user.confirmed:\n        return forbidden('Unconfirmed account')\n\n\n@api.route('/token')\ndef get_token():\n    if g.current_user.is_anonymous or g.token_used:\n        return unauthorized('Invalid credentials')\n    return jsonify({'token': g.current_user.generate_auth_token(\n        expiration=3600), 'expiration': 3600})\n"
  },
  {
    "path": "app/api_1_0/comments.py",
    "content": "from flask import jsonify, request, g, url_for, current_app\nfrom .. import db\nfrom ..models import Post, Permission, Comment\nfrom . import api\nfrom .decorators import permission_required\n\n\n@api.route('/comments/')\ndef get_comments():\n    page = request.args.get('page', 1, type=int)\n    pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(\n        page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],\n        error_out=False)\n    comments = pagination.items\n    prev = None\n    if pagination.has_prev:\n        prev = url_for('api.get_comments', page=page-1, _external=True)\n    next = None\n    if pagination.has_next:\n        next = url_for('api.get_comments', page=page+1, _external=True)\n    return jsonify({\n        'comments': [comment.to_json() for comment in comments],\n        'prev': prev,\n        'next': next,\n        'count': pagination.total\n    })\n\n\n@api.route('/comments/<int:id>')\ndef get_comment(id):\n    comment = Comment.query.get_or_404(id)\n    return jsonify(comment.to_json())\n\n\n@api.route('/posts/<int:id>/comments/')\ndef get_post_comments(id):\n    post = Post.query.get_or_404(id)\n    page = request.args.get('page', 1, type=int)\n    pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(\n        page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],\n        error_out=False)\n    comments = pagination.items\n    prev = None\n    if pagination.has_prev:\n        prev = url_for('api.get_post_comments', id=id, page=page-1,\n                       _external=True)\n    next = None\n    if pagination.has_next:\n        next = url_for('api.get_post_comments', id=id, page=page+1,\n                       _external=True)\n    return jsonify({\n        'comments': [comment.to_json() for comment in comments],\n        'prev': prev,\n        'next': next,\n        'count': pagination.total\n    })\n\n\n@api.route('/posts/<int:id>/comments/', methods=['POST'])\n@permission_required(Permission.COMMENT)\ndef new_post_comment(id):\n    post = Post.query.get_or_404(id)\n    comment = Comment.from_json(request.json)\n    comment.author = g.current_user\n    comment.post = post\n    db.session.add(comment)\n    db.session.commit()\n    return jsonify(comment.to_json()), 201, \\\n        {'Location': url_for('api.get_comment', id=comment.id,\n                             _external=True)}\n"
  },
  {
    "path": "app/api_1_0/decorators.py",
    "content": "from functools import wraps\nfrom flask import g\nfrom .errors import forbidden\n\n\ndef permission_required(permission):\n    def decorator(f):\n        @wraps(f)\n        def decorated_function(*args, **kwargs):\n            if not g.current_user.can(permission):\n                return forbidden('Insufficient permissions')\n            return f(*args, **kwargs)\n        return decorated_function\n    return decorator\n"
  },
  {
    "path": "app/api_1_0/errors.py",
    "content": "from flask import jsonify\nfrom app.exceptions import ValidationError\nfrom . import api\n\n\ndef bad_request(message):\n    response = jsonify({'error': 'bad request', 'message': message})\n    response.status_code = 400\n    return response\n\n\ndef unauthorized(message):\n    response = jsonify({'error': 'unauthorized', 'message': message})\n    response.status_code = 401\n    return response\n\n\ndef forbidden(message):\n    response = jsonify({'error': 'forbidden', 'message': message})\n    response.status_code = 403\n    return response\n\n\n@api.errorhandler(ValidationError)\ndef validation_error(e):\n    return bad_request(e.args[0])\n"
  },
  {
    "path": "app/api_1_0/posts.py",
    "content": "from flask import jsonify, request, g, abort, url_for, current_app\nfrom .. import db\nfrom ..models import Post, Permission\nfrom . import api\nfrom .decorators import permission_required\nfrom .errors import forbidden\n\n\n@api.route('/posts/')\ndef get_posts():\n    page = request.args.get('page', 1, type=int)\n    pagination = Post.query.paginate(\n        page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\n        error_out=False)\n    posts = pagination.items\n    prev = None\n    if pagination.has_prev:\n        prev = url_for('api.get_posts', page=page-1, _external=True)\n    next = None\n    if pagination.has_next:\n        next = url_for('api.get_posts', page=page+1, _external=True)\n    return jsonify({\n        'posts': [post.to_json() for post in posts],\n        'prev': prev,\n        'next': next,\n        'count': pagination.total\n    })\n\n\n@api.route('/posts/<int:id>')\ndef get_post(id):\n    post = Post.query.get_or_404(id)\n    return jsonify(post.to_json())\n\n\n@api.route('/posts/', methods=['POST'])\n@permission_required(Permission.WRITE_ARTICLES)\ndef new_post():\n    post = Post.from_json(request.json)\n    post.author = g.current_user\n    db.session.add(post)\n    db.session.commit()\n    return jsonify(post.to_json()), 201, \\\n        {'Location': url_for('api.get_post', id=post.id, _external=True)}\n\n\n@api.route('/posts/<int:id>', methods=['PUT'])\n@permission_required(Permission.WRITE_ARTICLES)\ndef edit_post(id):\n    post = Post.query.get_or_404(id)\n    if g.current_user != post.author and \\\n            not g.current_user.can(Permission.ADMINISTER):\n        return forbidden('Insufficient permissions')\n    post.body = request.json.get('body', post.body)\n    db.session.add(post)\n    return jsonify(post.to_json())\n"
  },
  {
    "path": "app/api_1_0/users.py",
    "content": "from flask import jsonify, request, current_app, url_for\nfrom . import api\nfrom ..models import User, Post\n\n\n@api.route('/users/<int:id>')\ndef get_user(id):\n    user = User.query.get_or_404(id)\n    return jsonify(user.to_json())\n\n\n@api.route('/users/<int:id>/posts/')\ndef get_user_posts(id):\n    user = User.query.get_or_404(id)\n    page = request.args.get('page', 1, type=int)\n    pagination = user.posts.order_by(Post.timestamp.desc()).paginate(\n        page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\n        error_out=False)\n    posts = pagination.items\n    prev = None\n    if pagination.has_prev:\n        prev = url_for('api.get_user_posts', id=id, page=page-1,\n                       _external=True)\n    next = None\n    if pagination.has_next:\n        next = url_for('api.get_user_posts', id=id, page=page+1,\n                       _external=True)\n    return jsonify({\n        'posts': [post.to_json() for post in posts],\n        'prev': prev,\n        'next': next,\n        'count': pagination.total\n    })\n\n\n@api.route('/users/<int:id>/timeline/')\ndef get_user_followed_posts(id):\n    user = User.query.get_or_404(id)\n    page = request.args.get('page', 1, type=int)\n    pagination = user.followed_posts.order_by(Post.timestamp.desc()).paginate(\n        page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\n        error_out=False)\n    posts = pagination.items\n    prev = None\n    if pagination.has_prev:\n        prev = url_for('api.get_user_followed_posts', id=id, page=page-1,\n                       _external=True)\n    next = None\n    if pagination.has_next:\n        next = url_for('api.get_user_followed_posts', id=id, page=page+1,\n                       _external=True)\n    return jsonify({\n        'posts': [post.to_json() for post in posts],\n        'prev': prev,\n        'next': next,\n        'count': pagination.total\n    })\n"
  },
  {
    "path": "app/auth/__init__.py",
    "content": "from flask import Blueprint\n\nauth = Blueprint('auth', __name__)\n\nfrom . import views\n"
  },
  {
    "path": "app/auth/forms.py",
    "content": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, PasswordField, BooleanField, SubmitField\nfrom wtforms.validators import Required, Length, Email, Regexp, EqualTo\nfrom wtforms import ValidationError\nfrom ..models import User\n\n\nclass LoginForm(FlaskForm):\n    email = StringField('Email', validators=[Required(), Length(1, 64),\n                                             Email()])\n    password = PasswordField('Password', validators=[Required()])\n    remember_me = BooleanField('Keep me logged in')\n    submit = SubmitField('Log In')\n\n\nclass RegistrationForm(FlaskForm):\n    email = StringField('Email', validators=[Required(), Length(1, 64),\n                                           Email()])\n    username = StringField('Username', validators=[\n        Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,\n                                          'Usernames must have only letters, '\n                                          'numbers, dots or underscores')])\n    password = PasswordField('Password', validators=[\n        Required(), EqualTo('password2', message='Passwords must match.')])\n    password2 = PasswordField('Confirm password', validators=[Required()])\n    submit = SubmitField('Register')\n\n    def validate_email(self, field):\n        if User.query.filter_by(email=field.data).first():\n            raise ValidationError('Email already registered.')\n\n    def validate_username(self, field):\n        if User.query.filter_by(username=field.data).first():\n            raise ValidationError('Username already in use.')\n\n\nclass ChangePasswordForm(FlaskForm):\n    old_password = PasswordField('Old password', validators=[Required()])\n    password = PasswordField('New password', validators=[\n        Required(), EqualTo('password2', message='Passwords must match')])\n    password2 = PasswordField('Confirm new password', validators=[Required()])\n    submit = SubmitField('Update Password')\n\n\nclass PasswordResetRequestForm(FlaskForm):\n    email = StringField('Email', validators=[Required(), Length(1, 64),\n                                             Email()])\n    submit = SubmitField('Reset Password')\n\n\nclass PasswordResetForm(FlaskForm):\n    email = StringField('Email', validators=[Required(), Length(1, 64),\n                                             Email()])\n    password = PasswordField('New Password', validators=[\n        Required(), EqualTo('password2', message='Passwords must match')])\n    password2 = PasswordField('Confirm password', validators=[Required()])\n    submit = SubmitField('Reset Password')\n\n    def validate_email(self, field):\n        if User.query.filter_by(email=field.data).first() is None:\n            raise ValidationError('Unknown email address.')\n\n\nclass ChangeEmailForm(FlaskForm):\n    email = StringField('New Email', validators=[Required(), Length(1, 64),\n                                                 Email()])\n    password = PasswordField('Password', validators=[Required()])\n    submit = SubmitField('Update Email Address')\n\n    def validate_email(self, field):\n        if User.query.filter_by(email=field.data).first():\n            raise ValidationError('Email already registered.')\n"
  },
  {
    "path": "app/auth/views.py",
    "content": "from flask import render_template, redirect, request, url_for, flash\nfrom flask_login import login_user, logout_user, login_required, \\\n    current_user\nfrom . import auth\nfrom .. import db\nfrom ..models import User\nfrom ..email import send_email\nfrom .forms import LoginForm, RegistrationForm, ChangePasswordForm,\\\n    PasswordResetRequestForm, PasswordResetForm, ChangeEmailForm\n\n\n@auth.before_app_request\ndef before_request():\n    if current_user.is_authenticated:\n        current_user.ping()\n        if not current_user.confirmed \\\n                and request.endpoint \\\n                and request.endpoint[:5] != 'auth.' \\\n                and request.endpoint != 'static':\n            return redirect(url_for('auth.unconfirmed'))\n\n\n@auth.route('/unconfirmed')\ndef unconfirmed():\n    if current_user.is_anonymous or current_user.confirmed:\n        return redirect(url_for('main.index'))\n    return render_template('auth/unconfirmed.html')\n\n\n@auth.route('/login', methods=['GET', 'POST'])\ndef login():\n    form = LoginForm()\n    if form.validate_on_submit():\n        user = User.query.filter_by(email=form.email.data).first()\n        if user is not None and user.verify_password(form.password.data):\n            login_user(user, form.remember_me.data)\n            return redirect(request.args.get('next') or url_for('main.index'))\n        flash('Invalid username or password.')\n    return render_template('auth/login.html', form=form)\n\n\n@auth.route('/logout')\n@login_required\ndef logout():\n    logout_user()\n    flash('You have been logged out.')\n    return redirect(url_for('main.index'))\n\n\n@auth.route('/register', methods=['GET', 'POST'])\ndef register():\n    form = RegistrationForm()\n    if form.validate_on_submit():\n        user = User(email=form.email.data,\n                    username=form.username.data,\n                    password=form.password.data)\n        db.session.add(user)\n        db.session.commit()\n        token = user.generate_confirmation_token()\n        send_email(user.email, 'Confirm Your Account',\n                   'auth/email/confirm', user=user, token=token)\n        flash('A confirmation email has been sent to you by email.')\n        return redirect(url_for('auth.login'))\n    return render_template('auth/register.html', form=form)\n\n\n@auth.route('/confirm/<token>')\n@login_required\ndef confirm(token):\n    if current_user.confirmed:\n        return redirect(url_for('main.index'))\n    if current_user.confirm(token):\n        flash('You have confirmed your account. Thanks!')\n    else:\n        flash('The confirmation link is invalid or has expired.')\n    return redirect(url_for('main.index'))\n\n\n@auth.route('/confirm')\n@login_required\ndef resend_confirmation():\n    token = current_user.generate_confirmation_token()\n    send_email(current_user.email, 'Confirm Your Account',\n               'auth/email/confirm', user=current_user, token=token)\n    flash('A new confirmation email has been sent to you by email.')\n    return redirect(url_for('main.index'))\n\n\n@auth.route('/change-password', methods=['GET', 'POST'])\n@login_required\ndef change_password():\n    form = ChangePasswordForm()\n    if form.validate_on_submit():\n        if current_user.verify_password(form.old_password.data):\n            current_user.password = form.password.data\n            db.session.add(current_user)\n            flash('Your password has been updated.')\n            return redirect(url_for('main.index'))\n        else:\n            flash('Invalid password.')\n    return render_template(\"auth/change_password.html\", form=form)\n\n\n@auth.route('/reset', methods=['GET', 'POST'])\ndef password_reset_request():\n    if not current_user.is_anonymous:\n        return redirect(url_for('main.index'))\n    form = PasswordResetRequestForm()\n    if form.validate_on_submit():\n        user = User.query.filter_by(email=form.email.data).first()\n        if user:\n            token = user.generate_reset_token()\n            send_email(user.email, 'Reset Your Password',\n                       'auth/email/reset_password',\n                       user=user, token=token,\n                       next=request.args.get('next'))\n        flash('An email with instructions to reset your password has been '\n              'sent to you.')\n        return redirect(url_for('auth.login'))\n    return render_template('auth/reset_password.html', form=form)\n\n\n@auth.route('/reset/<token>', methods=['GET', 'POST'])\ndef password_reset(token):\n    if not current_user.is_anonymous:\n        return redirect(url_for('main.index'))\n    form = PasswordResetForm()\n    if form.validate_on_submit():\n        user = User.query.filter_by(email=form.email.data).first()\n        if user is None:\n            return redirect(url_for('main.index'))\n        if user.reset_password(token, form.password.data):\n            flash('Your password has been updated.')\n            return redirect(url_for('auth.login'))\n        else:\n            return redirect(url_for('main.index'))\n    return render_template('auth/reset_password.html', form=form)\n\n\n@auth.route('/change-email', methods=['GET', 'POST'])\n@login_required\ndef change_email_request():\n    form = ChangeEmailForm()\n    if form.validate_on_submit():\n        if current_user.verify_password(form.password.data):\n            new_email = form.email.data\n            token = current_user.generate_email_change_token(new_email)\n            send_email(new_email, 'Confirm your email address',\n                       'auth/email/change_email',\n                       user=current_user, token=token)\n            flash('An email with instructions to confirm your new email '\n                  'address has been sent to you.')\n            return redirect(url_for('main.index'))\n        else:\n            flash('Invalid email or password.')\n    return render_template(\"auth/change_email.html\", form=form)\n\n\n@auth.route('/change-email/<token>')\n@login_required\ndef change_email(token):\n    if current_user.change_email(token):\n        flash('Your email address has been updated.')\n    else:\n        flash('Invalid request.')\n    return redirect(url_for('main.index'))\n"
  },
  {
    "path": "app/decorators.py",
    "content": "from functools import wraps\nfrom flask import abort\nfrom flask_login import current_user\nfrom .models import Permission\n\n\ndef permission_required(permission):\n    def decorator(f):\n        @wraps(f)\n        def decorated_function(*args, **kwargs):\n            if not current_user.can(permission):\n                abort(403)\n            return f(*args, **kwargs)\n        return decorated_function\n    return decorator\n\n\ndef admin_required(f):\n    return permission_required(Permission.ADMINISTER)(f)\n"
  },
  {
    "path": "app/email.py",
    "content": "from threading import Thread\nfrom flask import current_app, render_template\nfrom flask_mail import Message\nfrom . import mail\n\n\ndef send_async_email(app, msg):\n    with app.app_context():\n        mail.send(msg)\n\n\ndef send_email(to, subject, template, **kwargs):\n    app = current_app._get_current_object()\n    msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject,\n                  sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])\n    msg.body = render_template(template + '.txt', **kwargs)\n    msg.html = render_template(template + '.html', **kwargs)\n    thr = Thread(target=send_async_email, args=[app, msg])\n    thr.start()\n    return thr\n"
  },
  {
    "path": "app/exceptions.py",
    "content": "class ValidationError(ValueError):\n    pass\n"
  },
  {
    "path": "app/main/__init__.py",
    "content": "from flask import Blueprint\n\nmain = Blueprint('main', __name__)\n\nfrom . import views, errors\nfrom ..models import Permission\n\n\n@main.app_context_processor\ndef inject_permissions():\n    return dict(Permission=Permission)\n"
  },
  {
    "path": "app/main/errors.py",
    "content": "from flask import render_template, request, jsonify\nfrom . import main\n\n\n@main.app_errorhandler(403)\ndef forbidden(e):\n    if request.accept_mimetypes.accept_json and \\\n            not request.accept_mimetypes.accept_html:\n        response = jsonify({'error': 'forbidden'})\n        response.status_code = 403\n        return response\n    return render_template('403.html'), 403\n\n\n@main.app_errorhandler(404)\ndef page_not_found(e):\n    if request.accept_mimetypes.accept_json and \\\n            not request.accept_mimetypes.accept_html:\n        response = jsonify({'error': 'not found'})\n        response.status_code = 404\n        return response\n    return render_template('404.html'), 404\n\n\n@main.app_errorhandler(500)\ndef internal_server_error(e):\n    if request.accept_mimetypes.accept_json and \\\n            not request.accept_mimetypes.accept_html:\n        response = jsonify({'error': 'internal server error'})\n        response.status_code = 500\n        return response\n    return render_template('500.html'), 500\n"
  },
  {
    "path": "app/main/forms.py",
    "content": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, TextAreaField, BooleanField, SelectField,\\\n    SubmitField\nfrom wtforms.validators import Required, Length, Email, Regexp\nfrom wtforms import ValidationError\nfrom flask_pagedown.fields import PageDownField\nfrom ..models import Role, User\n\n\nclass NameForm(FlaskForm):\n    name = StringField('What is your name?', validators=[Required()])\n    submit = SubmitField('Submit')\n\n\nclass EditProfileForm(FlaskForm):\n    name = StringField('Real name', validators=[Length(0, 64)])\n    location = StringField('Location', validators=[Length(0, 64)])\n    about_me = TextAreaField('About me')\n    submit = SubmitField('Submit')\n\n\nclass EditProfileAdminForm(FlaskForm):\n    email = StringField('Email', validators=[Required(), Length(1, 64),\n                                             Email()])\n    username = StringField('Username', validators=[\n        Required(), Length(1, 64), Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,\n                                          'Usernames must have only letters, '\n                                          'numbers, dots or underscores')])\n    confirmed = BooleanField('Confirmed')\n    role = SelectField('Role', coerce=int)\n    name = StringField('Real name', validators=[Length(0, 64)])\n    location = StringField('Location', validators=[Length(0, 64)])\n    about_me = TextAreaField('About me')\n    submit = SubmitField('Submit')\n\n    def __init__(self, user, *args, **kwargs):\n        super(EditProfileAdminForm, self).__init__(*args, **kwargs)\n        self.role.choices = [(role.id, role.name)\n                             for role in Role.query.order_by(Role.name).all()]\n        self.user = user\n\n    def validate_email(self, field):\n        if field.data != self.user.email and \\\n                User.query.filter_by(email=field.data).first():\n            raise ValidationError('Email already registered.')\n\n    def validate_username(self, field):\n        if field.data != self.user.username and \\\n                User.query.filter_by(username=field.data).first():\n            raise ValidationError('Username already in use.')\n\n\nclass PostForm(FlaskForm):\n    body = PageDownField(\"What's on your mind?\", validators=[Required()])\n    submit = SubmitField('Submit')\n\n\nclass CommentForm(FlaskForm):\n    body = StringField('Enter your comment', validators=[Required()])\n    submit = SubmitField('Submit')\n"
  },
  {
    "path": "app/main/views.py",
    "content": "from flask import render_template, redirect, url_for, abort, flash, request,\\\n    current_app, make_response\nfrom flask_login import login_required, current_user\nfrom flask_sqlalchemy import get_debug_queries\nfrom . import main\nfrom .forms import EditProfileForm, EditProfileAdminForm, PostForm,\\\n    CommentForm\nfrom .. import db\nfrom ..models import Permission, Role, User, Post, Comment\nfrom ..decorators import admin_required, permission_required\n\n\n@main.after_app_request\ndef after_request(response):\n    for query in get_debug_queries():\n        if query.duration >= current_app.config['FLASKY_SLOW_DB_QUERY_TIME']:\n            current_app.logger.warning(\n                'Slow query: %s\\nParameters: %s\\nDuration: %fs\\nContext: %s\\n'\n                % (query.statement, query.parameters, query.duration,\n                   query.context))\n    return response\n\n\n@main.route('/shutdown')\ndef server_shutdown():\n    if not current_app.testing:\n        abort(404)\n    shutdown = request.environ.get('werkzeug.server.shutdown')\n    if not shutdown:\n        abort(500)\n    shutdown()\n    return 'Shutting down...'\n\n\n@main.route('/', methods=['GET', 'POST'])\ndef index():\n    form = PostForm()\n    if current_user.can(Permission.WRITE_ARTICLES) and \\\n            form.validate_on_submit():\n        post = Post(body=form.body.data,\n                    author=current_user._get_current_object())\n        db.session.add(post)\n        return redirect(url_for('.index'))\n    page = request.args.get('page', 1, type=int)\n    show_followed = False\n    if current_user.is_authenticated:\n        show_followed = bool(request.cookies.get('show_followed', ''))\n    if show_followed:\n        query = current_user.followed_posts\n    else:\n        query = Post.query\n    pagination = query.order_by(Post.timestamp.desc()).paginate(\n        page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\n        error_out=False)\n    posts = pagination.items\n    return render_template('index.html', form=form, posts=posts,\n                           show_followed=show_followed, pagination=pagination)\n\n\n@main.route('/user/<username>')\ndef user(username):\n    user = User.query.filter_by(username=username).first_or_404()\n    page = request.args.get('page', 1, type=int)\n    pagination = user.posts.order_by(Post.timestamp.desc()).paginate(\n        page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],\n        error_out=False)\n    posts = pagination.items\n    return render_template('user.html', user=user, posts=posts,\n                           pagination=pagination)\n\n\n@main.route('/edit-profile', methods=['GET', 'POST'])\n@login_required\ndef edit_profile():\n    form = EditProfileForm()\n    if form.validate_on_submit():\n        current_user.name = form.name.data\n        current_user.location = form.location.data\n        current_user.about_me = form.about_me.data\n        db.session.add(current_user)\n        flash('Your profile has been updated.')\n        return redirect(url_for('.user', username=current_user.username))\n    form.name.data = current_user.name\n    form.location.data = current_user.location\n    form.about_me.data = current_user.about_me\n    return render_template('edit_profile.html', form=form)\n\n\n@main.route('/edit-profile/<int:id>', methods=['GET', 'POST'])\n@login_required\n@admin_required\ndef edit_profile_admin(id):\n    user = User.query.get_or_404(id)\n    form = EditProfileAdminForm(user=user)\n    if form.validate_on_submit():\n        user.email = form.email.data\n        user.username = form.username.data\n        user.confirmed = form.confirmed.data\n        user.role = Role.query.get(form.role.data)\n        user.name = form.name.data\n        user.location = form.location.data\n        user.about_me = form.about_me.data\n        db.session.add(user)\n        flash('The profile has been updated.')\n        return redirect(url_for('.user', username=user.username))\n    form.email.data = user.email\n    form.username.data = user.username\n    form.confirmed.data = user.confirmed\n    form.role.data = user.role_id\n    form.name.data = user.name\n    form.location.data = user.location\n    form.about_me.data = user.about_me\n    return render_template('edit_profile.html', form=form, user=user)\n\n\n@main.route('/post/<int:id>', methods=['GET', 'POST'])\ndef post(id):\n    post = Post.query.get_or_404(id)\n    form = CommentForm()\n    if form.validate_on_submit():\n        comment = Comment(body=form.body.data,\n                          post=post,\n                          author=current_user._get_current_object())\n        db.session.add(comment)\n        flash('Your comment has been published.')\n        return redirect(url_for('.post', id=post.id, page=-1))\n    page = request.args.get('page', 1, type=int)\n    if page == -1:\n        page = (post.comments.count() - 1) // \\\n            current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1\n    pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(\n        page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],\n        error_out=False)\n    comments = pagination.items\n    return render_template('post.html', posts=[post], form=form,\n                           comments=comments, pagination=pagination)\n\n\n@main.route('/edit/<int:id>', methods=['GET', 'POST'])\n@login_required\ndef edit(id):\n    post = Post.query.get_or_404(id)\n    if current_user != post.author and \\\n            not current_user.can(Permission.ADMINISTER):\n        abort(403)\n    form = PostForm()\n    if form.validate_on_submit():\n        post.body = form.body.data\n        db.session.add(post)\n        flash('The post has been updated.')\n        return redirect(url_for('.post', id=post.id))\n    form.body.data = post.body\n    return render_template('edit_post.html', form=form)\n\n\n@main.route('/follow/<username>')\n@login_required\n@permission_required(Permission.FOLLOW)\ndef follow(username):\n    user = User.query.filter_by(username=username).first()\n    if user is None:\n        flash('Invalid user.')\n        return redirect(url_for('.index'))\n    if current_user.is_following(user):\n        flash('You are already following this user.')\n        return redirect(url_for('.user', username=username))\n    current_user.follow(user)\n    flash('You are now following %s.' % username)\n    return redirect(url_for('.user', username=username))\n\n\n@main.route('/unfollow/<username>')\n@login_required\n@permission_required(Permission.FOLLOW)\ndef unfollow(username):\n    user = User.query.filter_by(username=username).first()\n    if user is None:\n        flash('Invalid user.')\n        return redirect(url_for('.index'))\n    if not current_user.is_following(user):\n        flash('You are not following this user.')\n        return redirect(url_for('.user', username=username))\n    current_user.unfollow(user)\n    flash('You are not following %s anymore.' % username)\n    return redirect(url_for('.user', username=username))\n\n\n@main.route('/followers/<username>')\ndef followers(username):\n    user = User.query.filter_by(username=username).first()\n    if user is None:\n        flash('Invalid user.')\n        return redirect(url_for('.index'))\n    page = request.args.get('page', 1, type=int)\n    pagination = user.followers.paginate(\n        page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],\n        error_out=False)\n    follows = [{'user': item.follower, 'timestamp': item.timestamp}\n               for item in pagination.items]\n    return render_template('followers.html', user=user, title=\"Followers of\",\n                           endpoint='.followers', pagination=pagination,\n                           follows=follows)\n\n\n@main.route('/followed-by/<username>')\ndef followed_by(username):\n    user = User.query.filter_by(username=username).first()\n    if user is None:\n        flash('Invalid user.')\n        return redirect(url_for('.index'))\n    page = request.args.get('page', 1, type=int)\n    pagination = user.followed.paginate(\n        page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],\n        error_out=False)\n    follows = [{'user': item.followed, 'timestamp': item.timestamp}\n               for item in pagination.items]\n    return render_template('followers.html', user=user, title=\"Followed by\",\n                           endpoint='.followed_by', pagination=pagination,\n                           follows=follows)\n\n\n@main.route('/all')\n@login_required\ndef show_all():\n    resp = make_response(redirect(url_for('.index')))\n    resp.set_cookie('show_followed', '', max_age=30*24*60*60)\n    return resp\n\n\n@main.route('/followed')\n@login_required\ndef show_followed():\n    resp = make_response(redirect(url_for('.index')))\n    resp.set_cookie('show_followed', '1', max_age=30*24*60*60)\n    return resp\n\n\n@main.route('/moderate')\n@login_required\n@permission_required(Permission.MODERATE_COMMENTS)\ndef moderate():\n    page = request.args.get('page', 1, type=int)\n    pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(\n        page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],\n        error_out=False)\n    comments = pagination.items\n    return render_template('moderate.html', comments=comments,\n                           pagination=pagination, page=page)\n\n\n@main.route('/moderate/enable/<int:id>')\n@login_required\n@permission_required(Permission.MODERATE_COMMENTS)\ndef moderate_enable(id):\n    comment = Comment.query.get_or_404(id)\n    comment.disabled = False\n    db.session.add(comment)\n    return redirect(url_for('.moderate',\n                            page=request.args.get('page', 1, type=int)))\n\n\n@main.route('/moderate/disable/<int:id>')\n@login_required\n@permission_required(Permission.MODERATE_COMMENTS)\ndef moderate_disable(id):\n    comment = Comment.query.get_or_404(id)\n    comment.disabled = True\n    db.session.add(comment)\n    return redirect(url_for('.moderate',\n                            page=request.args.get('page', 1, type=int)))\n"
  },
  {
    "path": "app/models.py",
    "content": "from datetime import datetime\nimport hashlib\nfrom werkzeug.security import generate_password_hash, check_password_hash\nfrom itsdangerous import TimedJSONWebSignatureSerializer as Serializer\nfrom markdown import markdown\nimport bleach\nfrom flask import current_app, request, url_for\nfrom flask_login import UserMixin, AnonymousUserMixin\nfrom app.exceptions import ValidationError\nfrom . import db, login_manager\n\n\nclass Permission:\n    FOLLOW = 0x01\n    COMMENT = 0x02\n    WRITE_ARTICLES = 0x04\n    MODERATE_COMMENTS = 0x08\n    ADMINISTER = 0x80\n\n\nclass Role(db.Model):\n    __tablename__ = 'roles'\n    id = db.Column(db.Integer, primary_key=True)\n    name = db.Column(db.String(64), unique=True)\n    default = db.Column(db.Boolean, default=False, index=True)\n    permissions = db.Column(db.Integer)\n    users = db.relationship('User', backref='role', lazy='dynamic')\n\n    @staticmethod\n    def insert_roles():\n        roles = {\n            'User': (Permission.FOLLOW |\n                     Permission.COMMENT |\n                     Permission.WRITE_ARTICLES, True),\n            'Moderator': (Permission.FOLLOW |\n                          Permission.COMMENT |\n                          Permission.WRITE_ARTICLES |\n                          Permission.MODERATE_COMMENTS, False),\n            'Administrator': (0xff, False)\n        }\n        for r in roles:\n            role = Role.query.filter_by(name=r).first()\n            if role is None:\n                role = Role(name=r)\n            role.permissions = roles[r][0]\n            role.default = roles[r][1]\n            db.session.add(role)\n        db.session.commit()\n\n    def __repr__(self):\n        return '<Role %r>' % self.name\n\n\nclass Follow(db.Model):\n    __tablename__ = 'follows'\n    follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),\n                            primary_key=True)\n    followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),\n                            primary_key=True)\n    timestamp = db.Column(db.DateTime, default=datetime.utcnow)\n\n\nclass User(UserMixin, db.Model):\n    __tablename__ = 'users'\n    id = db.Column(db.Integer, primary_key=True)\n    email = db.Column(db.String(64), unique=True, index=True)\n    username = db.Column(db.String(64), unique=True, index=True)\n    role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))\n    password_hash = db.Column(db.String(128))\n    confirmed = db.Column(db.Boolean, default=False)\n    name = db.Column(db.String(64))\n    location = db.Column(db.String(64))\n    about_me = db.Column(db.Text())\n    member_since = db.Column(db.DateTime(), default=datetime.utcnow)\n    last_seen = db.Column(db.DateTime(), default=datetime.utcnow)\n    avatar_hash = db.Column(db.String(32))\n    posts = db.relationship('Post', backref='author', lazy='dynamic')\n    followed = db.relationship('Follow',\n                               foreign_keys=[Follow.follower_id],\n                               backref=db.backref('follower', lazy='joined'),\n                               lazy='dynamic',\n                               cascade='all, delete-orphan')\n    followers = db.relationship('Follow',\n                                foreign_keys=[Follow.followed_id],\n                                backref=db.backref('followed', lazy='joined'),\n                                lazy='dynamic',\n                                cascade='all, delete-orphan')\n    comments = db.relationship('Comment', backref='author', lazy='dynamic')\n\n    @staticmethod\n    def generate_fake(count=100):\n        from sqlalchemy.exc import IntegrityError\n        from random import seed\n        import forgery_py\n\n        seed()\n        for i in range(count):\n            u = User(email=forgery_py.internet.email_address(),\n                     username=forgery_py.internet.user_name(True),\n                     password=forgery_py.lorem_ipsum.word(),\n                     confirmed=True,\n                     name=forgery_py.name.full_name(),\n                     location=forgery_py.address.city(),\n                     about_me=forgery_py.lorem_ipsum.sentence(),\n                     member_since=forgery_py.date.date(True))\n            db.session.add(u)\n            try:\n                db.session.commit()\n            except IntegrityError:\n                db.session.rollback()\n\n    @staticmethod\n    def add_self_follows():\n        for user in User.query.all():\n            if not user.is_following(user):\n                user.follow(user)\n                db.session.add(user)\n                db.session.commit()\n\n    def __init__(self, **kwargs):\n        super(User, self).__init__(**kwargs)\n        if self.role is None:\n            if self.email == current_app.config['FLASKY_ADMIN']:\n                self.role = Role.query.filter_by(permissions=0xff).first()\n            if self.role is None:\n                self.role = Role.query.filter_by(default=True).first()\n        if self.email is not None and self.avatar_hash is None:\n            self.avatar_hash = hashlib.md5(\n                self.email.encode('utf-8')).hexdigest()\n        self.followed.append(Follow(followed=self))\n\n    @property\n    def password(self):\n        raise AttributeError('password is not a readable attribute')\n\n    @password.setter\n    def password(self, password):\n        self.password_hash = generate_password_hash(password)\n\n    def verify_password(self, password):\n        return check_password_hash(self.password_hash, password)\n\n    def generate_confirmation_token(self, expiration=3600):\n        s = Serializer(current_app.config['SECRET_KEY'], expiration)\n        return s.dumps({'confirm': self.id})\n\n    def confirm(self, token):\n        s = Serializer(current_app.config['SECRET_KEY'])\n        try:\n            data = s.loads(token)\n        except:\n            return False\n        if data.get('confirm') != self.id:\n            return False\n        self.confirmed = True\n        db.session.add(self)\n        return True\n\n    def generate_reset_token(self, expiration=3600):\n        s = Serializer(current_app.config['SECRET_KEY'], expiration)\n        return s.dumps({'reset': self.id})\n\n    def reset_password(self, token, new_password):\n        s = Serializer(current_app.config['SECRET_KEY'])\n        try:\n            data = s.loads(token)\n        except:\n            return False\n        if data.get('reset') != self.id:\n            return False\n        self.password = new_password\n        db.session.add(self)\n        return True\n\n    def generate_email_change_token(self, new_email, expiration=3600):\n        s = Serializer(current_app.config['SECRET_KEY'], expiration)\n        return s.dumps({'change_email': self.id, 'new_email': new_email})\n\n    def change_email(self, token):\n        s = Serializer(current_app.config['SECRET_KEY'])\n        try:\n            data = s.loads(token)\n        except:\n            return False\n        if data.get('change_email') != self.id:\n            return False\n        new_email = data.get('new_email')\n        if new_email is None:\n            return False\n        if self.query.filter_by(email=new_email).first() is not None:\n            return False\n        self.email = new_email\n        self.avatar_hash = hashlib.md5(\n            self.email.encode('utf-8')).hexdigest()\n        db.session.add(self)\n        return True\n\n    def can(self, permissions):\n        return self.role is not None and \\\n            (self.role.permissions & permissions) == permissions\n\n    def is_administrator(self):\n        return self.can(Permission.ADMINISTER)\n\n    def ping(self):\n        self.last_seen = datetime.utcnow()\n        db.session.add(self)\n\n    def gravatar(self, size=100, default='identicon', rating='g'):\n        if request.is_secure:\n            url = 'https://secure.gravatar.com/avatar'\n        else:\n            url = 'http://www.gravatar.com/avatar'\n        hash = self.avatar_hash or hashlib.md5(\n            self.email.encode('utf-8')).hexdigest()\n        return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(\n            url=url, hash=hash, size=size, default=default, rating=rating)\n\n    def follow(self, user):\n        if not self.is_following(user):\n            f = Follow(follower=self, followed=user)\n            db.session.add(f)\n\n    def unfollow(self, user):\n        f = self.followed.filter_by(followed_id=user.id).first()\n        if f:\n            db.session.delete(f)\n\n    def is_following(self, user):\n        return self.followed.filter_by(\n            followed_id=user.id).first() is not None\n\n    def is_followed_by(self, user):\n        return self.followers.filter_by(\n            follower_id=user.id).first() is not None\n\n    @property\n    def followed_posts(self):\n        return Post.query.join(Follow, Follow.followed_id == Post.author_id)\\\n            .filter(Follow.follower_id == self.id)\n\n    def to_json(self):\n        json_user = {\n            'url': url_for('api.get_user', id=self.id, _external=True),\n            'username': self.username,\n            'member_since': self.member_since,\n            'last_seen': self.last_seen,\n            'posts': url_for('api.get_user_posts', id=self.id, _external=True),\n            'followed_posts': url_for('api.get_user_followed_posts',\n                                      id=self.id, _external=True),\n            'post_count': self.posts.count()\n        }\n        return json_user\n\n    def generate_auth_token(self, expiration):\n        s = Serializer(current_app.config['SECRET_KEY'],\n                       expires_in=expiration)\n        return s.dumps({'id': self.id}).decode('ascii')\n\n    @staticmethod\n    def verify_auth_token(token):\n        s = Serializer(current_app.config['SECRET_KEY'])\n        try:\n            data = s.loads(token)\n        except:\n            return None\n        return User.query.get(data['id'])\n\n    def __repr__(self):\n        return '<User %r>' % self.username\n\n\nclass AnonymousUser(AnonymousUserMixin):\n    def can(self, permissions):\n        return False\n\n    def is_administrator(self):\n        return False\n\nlogin_manager.anonymous_user = AnonymousUser\n\n\n@login_manager.user_loader\ndef load_user(user_id):\n    return User.query.get(int(user_id))\n\n\nclass Post(db.Model):\n    __tablename__ = 'posts'\n    id = db.Column(db.Integer, primary_key=True)\n    body = db.Column(db.Text)\n    body_html = db.Column(db.Text)\n    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)\n    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))\n    comments = db.relationship('Comment', backref='post', lazy='dynamic')\n\n    @staticmethod\n    def generate_fake(count=100):\n        from random import seed, randint\n        import forgery_py\n\n        seed()\n        user_count = User.query.count()\n        for i in range(count):\n            u = User.query.offset(randint(0, user_count - 1)).first()\n            p = Post(body=forgery_py.lorem_ipsum.sentences(randint(1, 5)),\n                     timestamp=forgery_py.date.date(True),\n                     author=u)\n            db.session.add(p)\n            db.session.commit()\n\n    @staticmethod\n    def on_changed_body(target, value, oldvalue, initiator):\n        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',\n                        'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',\n                        'h1', 'h2', 'h3', 'p']\n        target.body_html = bleach.linkify(bleach.clean(\n            markdown(value, output_format='html'),\n            tags=allowed_tags, strip=True))\n\n    def to_json(self):\n        json_post = {\n            'url': url_for('api.get_post', id=self.id, _external=True),\n            'body': self.body,\n            'body_html': self.body_html,\n            'timestamp': self.timestamp,\n            'author': url_for('api.get_user', id=self.author_id,\n                              _external=True),\n            'comments': url_for('api.get_post_comments', id=self.id,\n                                _external=True),\n            'comment_count': self.comments.count()\n        }\n        return json_post\n\n    @staticmethod\n    def from_json(json_post):\n        body = json_post.get('body')\n        if body is None or body == '':\n            raise ValidationError('post does not have a body')\n        return Post(body=body)\n\n\ndb.event.listen(Post.body, 'set', Post.on_changed_body)\n\n\nclass Comment(db.Model):\n    __tablename__ = 'comments'\n    id = db.Column(db.Integer, primary_key=True)\n    body = db.Column(db.Text)\n    body_html = db.Column(db.Text)\n    timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)\n    disabled = db.Column(db.Boolean)\n    author_id = db.Column(db.Integer, db.ForeignKey('users.id'))\n    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))\n\n    @staticmethod\n    def on_changed_body(target, value, oldvalue, initiator):\n        allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i',\n                        'strong']\n        target.body_html = bleach.linkify(bleach.clean(\n            markdown(value, output_format='html'),\n            tags=allowed_tags, strip=True))\n\n    def to_json(self):\n        json_comment = {\n            'url': url_for('api.get_comment', id=self.id, _external=True),\n            'post': url_for('api.get_post', id=self.post_id, _external=True),\n            'body': self.body,\n            'body_html': self.body_html,\n            'timestamp': self.timestamp,\n            'author': url_for('api.get_user', id=self.author_id,\n                              _external=True),\n        }\n        return json_comment\n\n    @staticmethod\n    def from_json(json_comment):\n        body = json_comment.get('body')\n        if body is None or body == '':\n            raise ValidationError('comment does not have a body')\n        return Comment(body=body)\n\n\ndb.event.listen(Comment.body, 'set', Comment.on_changed_body)\n"
  },
  {
    "path": "app/static/styles.css",
    "content": ".profile-thumbnail {\n    position: absolute;\n}\n.profile-header {\n    min-height: 260px;\n    margin-left: 280px;\n}\ndiv.post-tabs {\n    margin-top: 16px;\n}\nul.posts {\n    list-style-type: none;\n    padding: 0px;\n    margin: 16px 0px 0px 0px;\n    border-top: 1px solid #e0e0e0;\n}\ndiv.post-tabs ul.posts {\n    margin: 0px;\n    border-top: none;\n}\nul.posts li.post {\n    padding: 8px;\n    border-bottom: 1px solid #e0e0e0;\n}\nul.posts li.post:hover {\n    background-color: #f0f0f0;\n}\ndiv.post-date {\n    float: right;\n}\ndiv.post-author {\n    font-weight: bold;\n}\ndiv.post-thumbnail {\n    position: absolute;\n}\ndiv.post-content {\n    margin-left: 48px;\n    min-height: 48px;\n}\ndiv.post-footer {\n    text-align: right;\n}\nul.comments {\n    list-style-type: none;\n    padding: 0px;\n    margin: 16px 0px 0px 0px;\n}\nul.comments li.comment {\n    margin-left: 32px;\n    padding: 8px;\n    border-bottom: 1px solid #e0e0e0;\n}\nul.comments li.comment:nth-child(1) {\n    border-top: 1px solid #e0e0e0;\n}\nul.comments li.comment:hover {\n    background-color: #f0f0f0;\n}\ndiv.comment-date {\n    float: right;\n}\ndiv.comment-author {\n    font-weight: bold;\n}\ndiv.comment-thumbnail {\n    position: absolute;\n}\ndiv.comment-content {\n    margin-left: 48px;\n    min-height: 48px;\n}\ndiv.comment-form {\n    margin: 16px 0px 16px 32px;\n}\ndiv.pagination {\n    width: 100%;\n    text-align: right;\n    padding: 0px;\n    margin: 0px;\n}\ndiv.flask-pagedown-preview {\n    margin: 10px 0px 10px 0px;\n    border: 1px solid #e0e0e0;\n    padding: 4px;\n}\ndiv.flask-pagedown-preview h1 {\n    font-size: 140%;\n}\ndiv.flask-pagedown-preview h2 {\n    font-size: 130%;\n}\ndiv.flask-pagedown-preview h3 {\n    font-size: 120%;\n}\n.post-body h1 {\n    font-size: 140%;\n}\n.post-body h2 {\n    font-size: 130%;\n}\n.post-body h3 {\n    font-size: 120%;\n}\n.table.followers tr {\n    border-bottom: 1px solid #e0e0e0;\n}\n"
  },
  {
    "path": "app/templates/403.html",
    "content": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Forbidden{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Forbidden</h1>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/404.html",
    "content": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Page Not Found{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Not Found</h1>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/500.html",
    "content": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Internal Server Error{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Internal Server Error</h1>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/_comments.html",
    "content": "<ul class=\"comments\">\n    {% for comment in comments %}\n    <li class=\"comment\">\n        <div class=\"comment-thumbnail\">\n            <a href=\"{{ url_for('.user', username=comment.author.username) }}\">\n                <img class=\"img-rounded profile-thumbnail\" src=\"{{ comment.author.gravatar(size=40) }}\">\n            </a>\n        </div>\n        <div class=\"comment-content\">\n            <div class=\"comment-date\">{{ moment(comment.timestamp).fromNow() }}</div>\n            <div class=\"comment-author\"><a href=\"{{ url_for('.user', username=comment.author.username) }}\">{{ comment.author.username }}</a></div>\n            <div class=\"comment-body\">\n                {% if comment.disabled %}\n                <p><i>This comment has been disabled by a moderator.</i></p>\n                {% endif %}\n                {% if moderate or not comment.disabled %}\n                    {% if comment.body_html %}\n                        {{ comment.body_html | safe }}\n                    {% else %}\n                        {{ comment.body }}\n                    {% endif %}\n                {% endif %}\n            </div>\n            {% if moderate %}\n                <br>\n                {% if comment.disabled %}\n                <a class=\"btn btn-default btn-xs\" href=\"{{ url_for('.moderate_enable', id=comment.id, page=page) }}\">Enable</a>\n                {% else %}\n                <a class=\"btn btn-danger btn-xs\" href=\"{{ url_for('.moderate_disable', id=comment.id, page=page) }}\">Disable</a>\n                {% endif %}\n            {% endif %}\n        </div>\n    </li>\n    {% endfor %}\n</ul>\n"
  },
  {
    "path": "app/templates/_macros.html",
    "content": "{% macro pagination_widget(pagination, endpoint, fragment='') %}\n<ul class=\"pagination\">\n    <li{% if not pagination.has_prev %} class=\"disabled\"{% endif %}>\n        <a href=\"{% if pagination.has_prev %}{{ url_for(endpoint, page=pagination.prev_num, **kwargs) }}{{ fragment }}{% else %}#{% endif %}\">\n            &laquo;\n        </a>\n    </li>\n    {% for p in pagination.iter_pages() %}\n        {% if p %}\n            {% if p == pagination.page %}\n            <li class=\"active\">\n                <a href=\"{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}\">{{ p }}</a>\n            </li>\n            {% else %}\n            <li>\n                <a href=\"{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}\">{{ p }}</a>\n            </li>\n            {% endif %}\n        {% else %}\n        <li class=\"disabled\"><a href=\"#\">&hellip;</a></li>\n        {% endif %}\n    {% endfor %}\n    <li{% if not pagination.has_next %} class=\"disabled\"{% endif %}>\n        <a href=\"{% if pagination.has_next %}{{ url_for(endpoint, page=pagination.next_num, **kwargs) }}{{ fragment }}{% else %}#{% endif %}\">\n            &raquo;\n        </a>\n    </li>\n</ul>\n{% endmacro %}\n"
  },
  {
    "path": "app/templates/_posts.html",
    "content": "<ul class=\"posts\">\n    {% for post in posts %}\n    <li class=\"post\">\n        <div class=\"post-thumbnail\">\n            <a href=\"{{ url_for('.user', username=post.author.username) }}\">\n                <img class=\"img-rounded profile-thumbnail\" src=\"{{ post.author.gravatar(size=40) }}\">\n            </a>\n        </div>\n        <div class=\"post-content\">\n            <div class=\"post-date\">{{ moment(post.timestamp).fromNow() }}</div>\n            <div class=\"post-author\"><a href=\"{{ url_for('.user', username=post.author.username) }}\">{{ post.author.username }}</a></div>\n            <div class=\"post-body\">\n                {% if post.body_html %}\n                    {{ post.body_html | safe }}\n                {% else %}\n                    {{ post.body }}\n                {% endif %}\n            </div>\n            <div class=\"post-footer\">\n                {% if current_user == post.author %}\n                <a href=\"{{ url_for('.edit', id=post.id) }}\">\n                    <span class=\"label label-primary\">Edit</span>\n                </a>\n                {% elif current_user.is_administrator() %}\n                <a href=\"{{ url_for('.edit', id=post.id) }}\">\n                    <span class=\"label label-danger\">Edit [Admin]</span>\n                </a>\n                {% endif %}\n                <a href=\"{{ url_for('.post', id=post.id) }}\">\n                    <span class=\"label label-default\">Permalink</span>\n                </a>\n                <a href=\"{{ url_for('.post', id=post.id) }}#comments\">\n                    <span class=\"label label-primary\">{{ post.comments.count() }} Comments</span>\n                </a>\n            </div>\n        </div>\n    </li>\n    {% endfor %}\n</ul>\n"
  },
  {
    "path": "app/templates/auth/change_email.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Change Email Address{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Change Your Email Address</h1>\n</div>\n<div class=\"col-md-4\">\n    {{ wtf.quick_form(form) }}\n</div>\n{% endblock %}"
  },
  {
    "path": "app/templates/auth/change_password.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Change Password{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Change Your Password</h1>\n</div>\n<div class=\"col-md-4\">\n    {{ wtf.quick_form(form) }}\n</div>\n{% endblock %}"
  },
  {
    "path": "app/templates/auth/email/change_email.html",
    "content": "<p>Dear {{ user.username }},</p>\n<p>To confirm your new email address <a href=\"{{ url_for('auth.change_email', token=token, _external=True) }}\">click here</a>.</p>\n<p>Alternatively, you can paste the following link in your browser's address bar:</p>\n<p>{{ url_for('auth.change_email', token=token, _external=True) }}</p>\n<p>Sincerely,</p>\n<p>The Flasky Team</p>\n<p><small>Note: replies to this email address are not monitored.</small></p>\n"
  },
  {
    "path": "app/templates/auth/email/change_email.txt",
    "content": "Dear {{ user.username }},\n\nTo confirm your new email address click on the following link:\n\n{{ url_for('auth.change_email', token=token, _external=True) }}\n\nSincerely,\n\nThe Flasky Team\n\nNote: replies to this email address are not monitored.\n"
  },
  {
    "path": "app/templates/auth/email/confirm.html",
    "content": "<p>Dear {{ user.username }},</p>\n<p>Welcome to <b>Flasky</b>!</p>\n<p>To confirm your account please <a href=\"{{ url_for('auth.confirm', token=token, _external=True) }}\">click here</a>.</p>\n<p>Alternatively, you can paste the following link in your browser's address bar:</p>\n<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>\n<p>Sincerely,</p>\n<p>The Flasky Team</p>\n<p><small>Note: replies to this email address are not monitored.</small></p>\n"
  },
  {
    "path": "app/templates/auth/email/confirm.txt",
    "content": "Dear {{ user.username }},\n\nWelcome to Flasky!\n\nTo confirm your account please click on the following link:\n\n{{ url_for('auth.confirm', token=token, _external=True) }}\n\nSincerely,\n\nThe Flasky Team\n\nNote: replies to this email address are not monitored.\n"
  },
  {
    "path": "app/templates/auth/email/reset_password.html",
    "content": "<p>Dear {{ user.username }},</p>\n<p>To reset your password <a href=\"{{ url_for('auth.password_reset', token=token, _external=True) }}\">click here</a>.</p>\n<p>Alternatively, you can paste the following link in your browser's address bar:</p>\n<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>\n<p>If you have not requested a password reset simply ignore this message.</p>\n<p>Sincerely,</p>\n<p>The Flasky Team</p>\n<p><small>Note: replies to this email address are not monitored.</small></p>\n"
  },
  {
    "path": "app/templates/auth/email/reset_password.txt",
    "content": "Dear {{ user.username }},\n\nTo reset your password click on the following link:\n\n{{ url_for('auth.password_reset', token=token, _external=True) }}\n\nIf you have not requested a password reset simply ignore this message.\n\nSincerely,\n\nThe Flasky Team\n\nNote: replies to this email address are not monitored.\n"
  },
  {
    "path": "app/templates/auth/login.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Login{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Login</h1>\n</div>\n<div class=\"col-md-4\">\n    {{ wtf.quick_form(form) }}\n    <br>\n    <p>Forgot your password? <a href=\"{{ url_for('auth.password_reset_request') }}\">Click here to reset it</a>.</p>\n    <p>New user? <a href=\"{{ url_for('auth.register') }}\">Click here to register</a>.</p>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/auth/register.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Register{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Register</h1>\n</div>\n<div class=\"col-md-4\">\n    {{ wtf.quick_form(form) }}\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/auth/reset_password.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Password Reset{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Reset Your Password</h1>\n</div>\n<div class=\"col-md-4\">\n    {{ wtf.quick_form(form) }}\n</div>\n{% endblock %}"
  },
  {
    "path": "app/templates/auth/unconfirmed.html",
    "content": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Confirm your account{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>\n        Hello, {{ current_user.username }}!\n    </h1>\n    <h3>You have not confirmed your account yet.</h3>\n    <p>\n        Before you can access this site you need to confirm your account.\n        Check your inbox, you should have received an email with a confirmation link.\n    </p>\n    <p>\n        Need another confirmation email?\n        <a href=\"{{ url_for('auth.resend_confirmation') }}\">Click here</a>\n    </p>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/base.html",
    "content": "{% extends \"bootstrap/base.html\" %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block head %}\n{{ super() }}\n<link rel=\"shortcut icon\" href=\"{{ url_for('static', filename='favicon.ico') }}\" type=\"image/x-icon\">\n<link rel=\"icon\" href=\"{{ url_for('static', filename='favicon.ico') }}\" type=\"image/x-icon\">\n<link rel=\"stylesheet\" type=\"text/css\" href=\"{{ url_for('static', filename='styles.css') }}\">\n{% endblock %}\n\n{% block navbar %}\n<div class=\"navbar navbar-inverse\" role=\"navigation\">\n    <div class=\"container\">\n        <div class=\"navbar-header\">\n            <button type=\"button\" class=\"navbar-toggle\" data-toggle=\"collapse\" data-target=\".navbar-collapse\">\n                <span class=\"sr-only\">Toggle navigation</span>\n                <span class=\"icon-bar\"></span>\n                <span class=\"icon-bar\"></span>\n                <span class=\"icon-bar\"></span>\n            </button>\n            <a class=\"navbar-brand\" href=\"{{ url_for('main.index') }}\">Flasky</a>\n        </div>\n        <div class=\"navbar-collapse collapse\">\n            <ul class=\"nav navbar-nav\">\n                <li><a href=\"{{ url_for('main.index') }}\">Home</a></li>\n                {% if current_user.is_authenticated %}\n                <li><a href=\"{{ url_for('main.user', username=current_user.username) }}\">Profile</a></li>\n                {% endif %}\n            </ul>\n            <ul class=\"nav navbar-nav navbar-right\">\n                {% if current_user.can(Permission.MODERATE_COMMENTS) %}\n                <li><a href=\"{{ url_for('main.moderate') }}\">Moderate Comments</a></li>\n                {% endif %}\n                {% if current_user.is_authenticated %}\n                <li class=\"dropdown\">\n                    <a href=\"#\" class=\"dropdown-toggle\" data-toggle=\"dropdown\">\n                        <img src=\"{{ current_user.gravatar(size=18) }}\">\n                        Account <b class=\"caret\"></b>\n                    </a>\n                    <ul class=\"dropdown-menu\">\n                        <li><a href=\"{{ url_for('auth.change_password') }}\">Change Password</a></li>\n                        <li><a href=\"{{ url_for('auth.change_email_request') }}\">Change Email</a></li>\n                        <li><a href=\"{{ url_for('auth.logout') }}\">Log Out</a></li>\n                    </ul>\n                </li>\n                {% else %}\n                <li><a href=\"{{ url_for('auth.login') }}\">Log In</a></li>\n                {% endif %}\n            </ul>\n        </div>\n    </div>\n</div>\n{% endblock %}\n\n{% block content %}\n<div class=\"container\">\n    {% for message in get_flashed_messages() %}\n    <div class=\"alert alert-warning\">\n        <button type=\"button\" class=\"close\" data-dismiss=\"alert\">&times;</button>\n        {{ message }}\n    </div>\n    {% endfor %}\n\n    {% block page_content %}{% endblock %}\n</div>\n{% endblock %}\n\n{% block scripts %}\n{{ super() }}\n{{ moment.include_moment() }}\n{% endblock %}\n"
  },
  {
    "path": "app/templates/edit_post.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Edit Post{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Edit Post</h1>\n</div>\n<div>\n    {{ wtf.quick_form(form) }}\n</div>\n{% endblock %}\n\n{% block scripts %}\n{{ super() }}\n{{ pagedown.include_pagedown() }}\n{% endblock %}\n"
  },
  {
    "path": "app/templates/edit_profile.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Edit Profile{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Edit Your Profile</h1>\n</div>\n<div class=\"col-md-4\">\n    {{ wtf.quick_form(form) }}\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/error_page.html",
    "content": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - {{ code }}: {{ name }}{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>{{ code }}: {{ name }}</h1>\n    <p>{{ description }}</p>\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/followers.html",
    "content": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - {{ title }} {{ user.username }}{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>{{ title }} {{ user.username }}</h1>\n</div>\n<table class=\"table table-hover followers\">\n    <thead><tr><th>User</th><th>Since</th></tr></thead>\n    {% for follow in follows %}\n    {% if follow.user != user %}\n    <tr>\n        <td>\n            <a href=\"{{ url_for('.user', username = follow.user.username) }}\">\n                <img class=\"img-rounded\" src=\"{{ follow.user.gravatar(size=32) }}\">\n                {{ follow.user.username }}\n            </a>\n        </td>\n        <td>{{ moment(follow.timestamp).format('L') }}</td>\n    </tr>\n    {% endif %}\n    {% endfor %}\n</table>\n<div class=\"pagination\">\n    {{ macros.pagination_widget(pagination, endpoint, username = user.username) }}\n</div>\n{% endblock %}\n"
  },
  {
    "path": "app/templates/index.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Hello, {% if current_user.is_authenticated %}{{ current_user.username }}{% else %}Stranger{% endif %}!</h1>\n</div>\n<div>\n    {% if current_user.can(Permission.WRITE_ARTICLES) %}\n    {{ wtf.quick_form(form) }}\n    {% endif %}\n</div>\n<div class=\"post-tabs\">\n    <ul class=\"nav nav-tabs\">\n        <li{% if not show_followed %} class=\"active\"{% endif %}><a href=\"{{ url_for('.show_all') }}\">All</a></li>\n        {% if current_user.is_authenticated %}\n        <li{% if show_followed %} class=\"active\"{% endif %}><a href=\"{{ url_for('.show_followed') }}\">Followers</a></li>\n        {% endif %}\n    </ul>\n    {% include '_posts.html' %}\n</div>\n{% if pagination %}\n<div class=\"pagination\">\n    {{ macros.pagination_widget(pagination, '.index') }}\n</div>\n{% endif %}\n{% endblock %}\n\n{% block scripts %}\n{{ super() }}\n{{ pagedown.include_pagedown() }}\n{% endblock %}\n"
  },
  {
    "path": "app/templates/mail/new_user.html",
    "content": "User <b>{{ user.username }}</b> has joined.\n"
  },
  {
    "path": "app/templates/mail/new_user.txt",
    "content": "User {{ user.username }} has joined.\n"
  },
  {
    "path": "app/templates/moderate.html",
    "content": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - Comment Moderation{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <h1>Comment Moderation</h1>\n</div>\n{% set moderate = True %}\n{% include '_comments.html' %}\n{% if pagination %}\n<div class=\"pagination\">\n    {{ macros.pagination_widget(pagination, '.moderate') }}\n</div>\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "app/templates/post.html",
    "content": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - Post{% endblock %}\n\n{% block page_content %}\n{% include '_posts.html' %}\n<h4 id=\"comments\">Comments</h4>\n{% if current_user.can(Permission.COMMENT) %}\n<div class=\"comment-form\">\n    {{ wtf.quick_form(form) }}\n</div>\n{% endif %}\n{% include '_comments.html' %}\n{% if pagination %}\n<div class=\"pagination\">\n    {{ macros.pagination_widget(pagination, '.post', fragment='#comments', id=posts[0].id) }}\n</div>\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "app/templates/user.html",
    "content": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - {{ user.username }}{% endblock %}\n\n{% block page_content %}\n<div class=\"page-header\">\n    <img class=\"img-rounded profile-thumbnail\" src=\"{{ user.gravatar(size=256) }}\">\n    <div class=\"profile-header\">\n        <h1>{{ user.username }}</h1>\n        {% if user.name or user.location %}\n        <p>\n            {% if user.name %}{{ user.name }}<br>{% endif %}\n            {% if user.location %}\n                From <a href=\"http://maps.google.com/?q={{ user.location }}\">{{ user.location }}</a><br>\n            {% endif %}\n        </p>\n        {% endif %}\n        {% if current_user.is_administrator() %}\n        <p><a href=\"mailto:{{ user.email }}\">{{ user.email }}</a></p>\n        {% endif %}\n        {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}\n        <p>Member since {{ moment(user.member_since).format('L') }}. Last seen {{ moment(user.last_seen).fromNow() }}.</p>\n        <p>{{ user.posts.count() }} blog posts. {{ user.comments.count() }} comments.</p>\n        <p>\n            {% if current_user.can(Permission.FOLLOW) and user != current_user %}\n                {% if not current_user.is_following(user) %}\n                <a href=\"{{ url_for('.follow', username=user.username) }}\" class=\"btn btn-primary\">Follow</a>\n                {% else %}\n                <a href=\"{{ url_for('.unfollow', username=user.username) }}\" class=\"btn btn-default\">Unfollow</a>\n                {% endif %}\n            {% endif %}\n            <a href=\"{{ url_for('.followers', username=user.username) }}\">Followers: <span class=\"badge\">{{ user.followers.count() - 1 }}</span></a>\n            <a href=\"{{ url_for('.followed_by', username=user.username) }}\">Following: <span class=\"badge\">{{ user.followed.count() - 1 }}</span></a>\n            {% if current_user.is_authenticated and user != current_user and user.is_following(current_user) %}\n            | <span class=\"label label-default\">Follows you</span>\n            {% endif %}\n        </p>\n        <p>\n            {% if user == current_user %}\n            <a class=\"btn btn-default\" href=\"{{ url_for('.edit_profile') }}\">Edit Profile</a>\n            {% endif %}\n            {% if current_user.is_administrator() %}\n            <a class=\"btn btn-danger\" href=\"{{ url_for('.edit_profile_admin', id=user.id) }}\">Edit Profile [Admin]</a>\n            {% endif %}\n        </p>\n    </div>\n</div>\n<h3>Posts by {{ user.username }}</h3>\n{% include '_posts.html' %}\n{% if pagination %}\n<div class=\"pagination\">\n    {{ macros.pagination_widget(pagination, '.user', username=user.username) }}\n</div>\n{% endif %}\n{% endblock %}\n"
  },
  {
    "path": "config.py",
    "content": "import os\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n\nclass Config:\n    SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'\n    SSL_DISABLE = False\n    SQLALCHEMY_COMMIT_ON_TEARDOWN = True\n    SQLALCHEMY_TRACK_MODIFICATIONS = False\n    SQLALCHEMY_RECORD_QUERIES = True\n    MAIL_SERVER = 'smtp.googlemail.com'\n    MAIL_PORT = 587\n    MAIL_USE_TLS = True\n    MAIL_USERNAME = os.environ.get('MAIL_USERNAME')\n    MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')\n    FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'\n    FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>'\n    FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')\n    FLASKY_POSTS_PER_PAGE = 20\n    FLASKY_FOLLOWERS_PER_PAGE = 50\n    FLASKY_COMMENTS_PER_PAGE = 30\n    FLASKY_SLOW_DB_QUERY_TIME=0.5\n\n    @staticmethod\n    def init_app(app):\n        pass\n\n\nclass DevelopmentConfig(Config):\n    DEBUG = True\n    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \\\n        'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')\n\n\nclass TestingConfig(Config):\n    TESTING = True\n    SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \\\n        'sqlite:///' + os.path.join(basedir, 'data-test.sqlite')\n    WTF_CSRF_ENABLED = False\n\n\nclass ProductionConfig(Config):\n    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \\\n        'sqlite:///' + os.path.join(basedir, 'data.sqlite')\n\n    @classmethod\n    def init_app(cls, app):\n        Config.init_app(app)\n\n        # email errors to the administrators\n        import logging\n        from logging.handlers import SMTPHandler\n        credentials = None\n        secure = None\n        if getattr(cls, 'MAIL_USERNAME', None) is not None:\n            credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)\n            if getattr(cls, 'MAIL_USE_TLS', None):\n                secure = ()\n        mail_handler = SMTPHandler(\n            mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),\n            fromaddr=cls.FLASKY_MAIL_SENDER,\n            toaddrs=[cls.FLASKY_ADMIN],\n            subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error',\n            credentials=credentials,\n            secure=secure)\n        mail_handler.setLevel(logging.ERROR)\n        app.logger.addHandler(mail_handler)\n\n\nclass HerokuConfig(ProductionConfig):\n    SSL_DISABLE = bool(os.environ.get('SSL_DISABLE'))\n\n    @classmethod\n    def init_app(cls, app):\n        ProductionConfig.init_app(app)\n\n        # handle proxy server headers\n        from werkzeug.contrib.fixers import ProxyFix\n        app.wsgi_app = ProxyFix(app.wsgi_app)\n\n        # log to stderr\n        import logging\n        from logging import StreamHandler\n        file_handler = StreamHandler()\n        file_handler.setLevel(logging.WARNING)\n        app.logger.addHandler(file_handler)\n\n\nclass UnixConfig(ProductionConfig):\n    @classmethod\n    def init_app(cls, app):\n        ProductionConfig.init_app(app)\n\n        # log to syslog\n        import logging\n        from logging.handlers import SysLogHandler\n        syslog_handler = SysLogHandler()\n        syslog_handler.setLevel(logging.WARNING)\n        app.logger.addHandler(syslog_handler)\n\n\nconfig = {\n    'development': DevelopmentConfig,\n    'testing': TestingConfig,\n    'production': ProductionConfig,\n    'heroku': HerokuConfig,\n    'unix': UnixConfig,\n\n    'default': DevelopmentConfig\n}\n"
  },
  {
    "path": "manage.py",
    "content": "#!/usr/bin/env python\nimport os\nCOV = None\nif os.environ.get('FLASK_COVERAGE'):\n    import coverage\n    COV = coverage.coverage(branch=True, include='app/*')\n    COV.start()\n\nif os.path.exists('.env'):\n    print('Importing environment from .env...')\n    for line in open('.env'):\n        var = line.strip().split('=')\n        if len(var) == 2:\n            os.environ[var[0]] = var[1]\n\nfrom app import create_app, db\nfrom app.models import User, Follow, Role, Permission, Post, Comment\nfrom flask_script import Manager, Shell\nfrom flask_migrate import Migrate, MigrateCommand\n\napp = create_app(os.getenv('FLASK_CONFIG') or 'default')\nmanager = Manager(app)\nmigrate = Migrate(app, db)\n\n\ndef make_shell_context():\n    return dict(app=app, db=db, User=User, Follow=Follow, Role=Role,\n                Permission=Permission, Post=Post, Comment=Comment)\nmanager.add_command(\"shell\", Shell(make_context=make_shell_context))\nmanager.add_command('db', MigrateCommand)\n\n\n@manager.command\ndef test(coverage=False):\n    \"\"\"Run the unit tests.\"\"\"\n    if coverage and not os.environ.get('FLASK_COVERAGE'):\n        import sys\n        os.environ['FLASK_COVERAGE'] = '1'\n        os.execvp(sys.executable, [sys.executable] + sys.argv)\n    import unittest\n    tests = unittest.TestLoader().discover('tests')\n    unittest.TextTestRunner(verbosity=2).run(tests)\n    if COV:\n        COV.stop()\n        COV.save()\n        print('Coverage Summary:')\n        COV.report()\n        basedir = os.path.abspath(os.path.dirname(__file__))\n        covdir = os.path.join(basedir, 'tmp/coverage')\n        COV.html_report(directory=covdir)\n        print('HTML version: file://%s/index.html' % covdir)\n        COV.erase()\n\n\n@manager.command\ndef profile(length=25, profile_dir=None):\n    \"\"\"Start the application under the code profiler.\"\"\"\n    from werkzeug.contrib.profiler import ProfilerMiddleware\n    app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length],\n                                      profile_dir=profile_dir)\n    app.run()\n\n\n@manager.command\ndef deploy():\n    \"\"\"Run deployment tasks.\"\"\"\n    from flask_migrate import upgrade\n    from app.models import Role, User\n\n    # migrate database to latest revision\n    upgrade()\n\n    # create user roles\n    Role.insert_roles()\n\n    # create self-follows for all users\n    User.add_self_follows()\n\n\nif __name__ == '__main__':\n    manager.run()\n"
  },
  {
    "path": "migrations/README",
    "content": "Generic single-database configuration."
  },
  {
    "path": "migrations/alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "migrations/env.py",
    "content": "from __future__ import with_statement\nfrom alembic import context\nfrom sqlalchemy import engine_from_config, pool\nfrom logging.config import fileConfig\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\nfrom flask import current_app\nconfig.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))\ntarget_metadata = current_app.extensions['migrate'].db.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(url=url)\n\n    with context.begin_transaction():\n        context.run_migrations()\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n    engine = engine_from_config(\n                config.get_section(config.config_ini_section),\n                prefix='sqlalchemy.',\n                poolclass=pool.NullPool)\n\n    connection = engine.connect()\n    context.configure(\n                connection=connection,\n                target_metadata=target_metadata\n                )\n\n    try:\n        with context.begin_transaction():\n            context.run_migrations()\n    finally:\n        connection.close()\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n\n"
  },
  {
    "path": "migrations/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision}\nCreate Date: ${create_date}\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\n\nfrom alembic import op\nimport sqlalchemy as sa\n${imports if imports else \"\"}\n\ndef upgrade():\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "migrations/versions/190163627111_account_confirmation.py",
    "content": "\"\"\"account confirmation\n\nRevision ID: 190163627111\nRevises: 456a945560f6\nCreate Date: 2013-12-29 02:58:45.577428\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '190163627111'\ndown_revision = '456a945560f6'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('users', sa.Column('confirmed', sa.Boolean(), nullable=True))\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('users', 'confirmed')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/198b0eebcf9_caching_of_avatar_hashes.py",
    "content": "\"\"\"caching of avatar hashes\n\nRevision ID: 198b0eebcf9\nRevises: d66f086b258\nCreate Date: 2014-02-04 09:10:02.245503\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '198b0eebcf9'\ndown_revision = 'd66f086b258'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('users', sa.Column('avatar_hash', sa.String(length=32), nullable=True))\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('users', 'avatar_hash')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/1b966e7f4b9e_post_model.py",
    "content": "\"\"\"post model\n\nRevision ID: 1b966e7f4b9e\nRevises: 198b0eebcf9\nCreate Date: 2013-12-31 00:00:14.700591\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '1b966e7f4b9e'\ndown_revision = '198b0eebcf9'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('posts',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('body', sa.Text(), nullable=True),\n    sa.Column('timestamp', sa.DateTime(), nullable=True),\n    sa.Column('author_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_index('ix_posts_timestamp', 'posts', ['timestamp'], unique=False)\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_index('ix_posts_timestamp', 'posts')\n    op.drop_table('posts')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/2356a38169ea_followers.py",
    "content": "\"\"\"followers\n\nRevision ID: 2356a38169ea\nRevises: 288cd3dc5a8\nCreate Date: 2013-12-31 16:10:34.500006\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '2356a38169ea'\ndown_revision = '288cd3dc5a8'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('follows',\n    sa.Column('follower_id', sa.Integer(), nullable=False),\n    sa.Column('followed_id', sa.Integer(), nullable=False),\n    sa.Column('timestamp', sa.DateTime(), nullable=True),\n    sa.ForeignKeyConstraint(['followed_id'], ['users.id'], ),\n    sa.ForeignKeyConstraint(['follower_id'], ['users.id'], ),\n    sa.PrimaryKeyConstraint('follower_id', 'followed_id')\n    )\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_table('follows')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/288cd3dc5a8_rich_text_posts.py",
    "content": "\"\"\"rich text posts\n\nRevision ID: 288cd3dc5a8\nRevises: 1b966e7f4b9e\nCreate Date: 2013-12-31 03:25:13.286503\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '288cd3dc5a8'\ndown_revision = '1b966e7f4b9e'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('posts', sa.Column('body_html', sa.Text(), nullable=True))\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('posts', 'body_html')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/38c4e85512a9_initial_migration.py",
    "content": "\"\"\"initial migration\n\nRevision ID: 38c4e85512a9\nRevises: None\nCreate Date: 2013-12-27 01:23:59.392801\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '38c4e85512a9'\ndown_revision = None\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('roles',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('name', sa.String(length=64), nullable=True),\n    sa.PrimaryKeyConstraint('id'),\n    sa.UniqueConstraint('name')\n    )\n    op.create_table('users',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('username', sa.String(length=64), nullable=True),\n    sa.Column('role_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_index('ix_users_username', 'users', ['username'], unique=True)\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_index('ix_users_username', 'users')\n    op.drop_table('users')\n    op.drop_table('roles')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/456a945560f6_login_support.py",
    "content": "\"\"\"login support\n\nRevision ID: 456a945560f6\nRevises: 38c4e85512a9\nCreate Date: 2013-12-29 00:18:35.795259\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '456a945560f6'\ndown_revision = '38c4e85512a9'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('users', sa.Column('email', sa.String(length=64), nullable=True))\n    op.add_column('users', sa.Column('password_hash', sa.String(length=128), nullable=True))\n    op.create_index('ix_users_email', 'users', ['email'], unique=True)\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_index('ix_users_email', 'users')\n    op.drop_column('users', 'password_hash')\n    op.drop_column('users', 'email')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/51f5ccfba190_comments.py",
    "content": "\"\"\"comments\n\nRevision ID: 51f5ccfba190\nRevises: 2356a38169ea\nCreate Date: 2014-01-01 12:08:43.287523\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '51f5ccfba190'\ndown_revision = '2356a38169ea'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.create_table('comments',\n    sa.Column('id', sa.Integer(), nullable=False),\n    sa.Column('body', sa.Text(), nullable=True),\n    sa.Column('body_html', sa.Text(), nullable=True),\n    sa.Column('timestamp', sa.DateTime(), nullable=True),\n    sa.Column('disabled', sa.Boolean(), nullable=True),\n    sa.Column('author_id', sa.Integer(), nullable=True),\n    sa.Column('post_id', sa.Integer(), nullable=True),\n    sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),\n    sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ),\n    sa.PrimaryKeyConstraint('id')\n    )\n    op.create_index('ix_comments_timestamp', 'comments', ['timestamp'], unique=False)\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_index('ix_comments_timestamp', 'comments')\n    op.drop_table('comments')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/56ed7d33de8d_user_roles.py",
    "content": "\"\"\"user roles\n\nRevision ID: 56ed7d33de8d\nRevises: 190163627111\nCreate Date: 2013-12-29 22:19:54.212604\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = '56ed7d33de8d'\ndown_revision = '190163627111'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('roles', sa.Column('default', sa.Boolean(), nullable=True))\n    op.add_column('roles', sa.Column('permissions', sa.Integer(), nullable=True))\n    op.create_index('ix_roles_default', 'roles', ['default'], unique=False)\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_index('ix_roles_default', 'roles')\n    op.drop_column('roles', 'permissions')\n    op.drop_column('roles', 'default')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "migrations/versions/d66f086b258_user_information.py",
    "content": "\"\"\"user information\n\nRevision ID: d66f086b258\nRevises: 56ed7d33de8d\nCreate Date: 2013-12-29 23:50:49.566954\n\n\"\"\"\n\n# revision identifiers, used by Alembic.\nrevision = 'd66f086b258'\ndown_revision = '56ed7d33de8d'\n\nfrom alembic import op\nimport sqlalchemy as sa\n\n\ndef upgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.add_column('users', sa.Column('about_me', sa.Text(), nullable=True))\n    op.add_column('users', sa.Column('last_seen', sa.DateTime(), nullable=True))\n    op.add_column('users', sa.Column('location', sa.String(length=64), nullable=True))\n    op.add_column('users', sa.Column('member_since', sa.DateTime(), nullable=True))\n    op.add_column('users', sa.Column('name', sa.String(length=64), nullable=True))\n    ### end Alembic commands ###\n\n\ndef downgrade():\n    ### commands auto generated by Alembic - please adjust! ###\n    op.drop_column('users', 'name')\n    op.drop_column('users', 'member_since')\n    op.drop_column('users', 'location')\n    op.drop_column('users', 'last_seen')\n    op.drop_column('users', 'about_me')\n    ### end Alembic commands ###\n"
  },
  {
    "path": "requirements/common.txt",
    "content": "Flask==0.12\nFlask-Bootstrap==3.0.3.1\nFlask-HTTPAuth==2.7.0\nFlask-Login==0.3.1\nFlask-Mail==0.9.0\nFlask-Migrate==2.0.3\nFlask-Moment==0.2.1\nFlask-PageDown==0.1.4\nFlask-SQLAlchemy==2.1\nFlask-Script==2.0.5\nFlask-WTF==0.14.2\nJinja2==2.9.5\nMako==1.0.6\nMarkdown==2.3.1\nMarkupSafe==0.23\nSQLAlchemy==1.1.5\nWTForms==2.1\nWerkzeug==0.11.15\nalembic==0.8.10\nbleach==1.4.0\nblinker==1.3\nclick==6.7\nhtml5lib==1.0b3\nitsdangerous==0.24\npython-editor==1.0.3\nsix==1.4.1\n"
  },
  {
    "path": "requirements/dev.txt",
    "content": "-r common.txt\nForgeryPy==0.1\nPygments==1.6\ncolorama==0.2.7\ncoverage==3.7.1\nhttpie==0.7.2\nrequests==2.1.0\nselenium==2.45.0\n"
  },
  {
    "path": "requirements/heroku.txt",
    "content": "-r prod.txt\nFlask-SSLify==0.1.4\ngunicorn==18.0\npsycopg2==2.5.1\n"
  },
  {
    "path": "requirements/prod.txt",
    "content": "-r common.txt\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/test_api.py",
    "content": "import unittest\nimport json\nimport re\nfrom base64 import b64encode\nfrom flask import url_for\nfrom app import create_app, db\nfrom app.models import User, Role, Post, Comment\n\n\nclass APITestCase(unittest.TestCase):\n    def setUp(self):\n        self.app = create_app('testing')\n        self.app_context = self.app.app_context()\n        self.app_context.push()\n        db.create_all()\n        Role.insert_roles()\n        self.client = self.app.test_client()\n\n    def tearDown(self):\n        db.session.remove()\n        db.drop_all()\n        self.app_context.pop()\n\n    def get_api_headers(self, username, password):\n        return {\n            'Authorization': 'Basic ' + b64encode(\n                (username + ':' + password).encode('utf-8')).decode('utf-8'),\n            'Accept': 'application/json',\n            'Content-Type': 'application/json'\n        }\n\n    def test_404(self):\n        response = self.client.get(\n            '/wrong/url',\n            headers=self.get_api_headers('email', 'password'))\n        self.assertTrue(response.status_code == 404)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertTrue(json_response['error'] == 'not found')\n\n    def test_no_auth(self):\n        response = self.client.get(url_for('api.get_posts'),\n                                   content_type='application/json')\n        self.assertTrue(response.status_code == 200)\n\n    def test_bad_auth(self):\n        # add a user\n        r = Role.query.filter_by(name='User').first()\n        self.assertIsNotNone(r)\n        u = User(email='john@example.com', password='cat', confirmed=True,\n                 role=r)\n        db.session.add(u)\n        db.session.commit()\n\n        # authenticate with bad password\n        response = self.client.get(\n            url_for('api.get_posts'),\n            headers=self.get_api_headers('john@example.com', 'dog'))\n        self.assertTrue(response.status_code == 401)\n\n    def test_token_auth(self):\n        # add a user\n        r = Role.query.filter_by(name='User').first()\n        self.assertIsNotNone(r)\n        u = User(email='john@example.com', password='cat', confirmed=True,\n                 role=r)\n        db.session.add(u)\n        db.session.commit()\n\n        # issue a request with a bad token\n        response = self.client.get(\n            url_for('api.get_posts'),\n            headers=self.get_api_headers('bad-token', ''))\n        self.assertTrue(response.status_code == 401)\n\n        # get a token\n        response = self.client.get(\n            url_for('api.get_token'),\n            headers=self.get_api_headers('john@example.com', 'cat'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertIsNotNone(json_response.get('token'))\n        token = json_response['token']\n\n        # issue a request with the token\n        response = self.client.get(\n            url_for('api.get_posts'),\n            headers=self.get_api_headers(token, ''))\n        self.assertTrue(response.status_code == 200)\n\n    def test_anonymous(self):\n        response = self.client.get(\n            url_for('api.get_posts'),\n            headers=self.get_api_headers('', ''))\n        self.assertTrue(response.status_code == 200)\n\n    def test_unconfirmed_account(self):\n        # add an unconfirmed user\n        r = Role.query.filter_by(name='User').first()\n        self.assertIsNotNone(r)\n        u = User(email='john@example.com', password='cat', confirmed=False,\n                 role=r)\n        db.session.add(u)\n        db.session.commit()\n\n        # get list of posts with the unconfirmed account\n        response = self.client.get(\n            url_for('api.get_posts'),\n            headers=self.get_api_headers('john@example.com', 'cat'))\n        self.assertTrue(response.status_code == 403)\n\n    def test_posts(self):\n        # add a user\n        r = Role.query.filter_by(name='User').first()\n        self.assertIsNotNone(r)\n        u = User(email='john@example.com', password='cat', confirmed=True,\n                 role=r)\n        db.session.add(u)\n        db.session.commit()\n\n        # write an empty post\n        response = self.client.post(\n            url_for('api.new_post'),\n            headers=self.get_api_headers('john@example.com', 'cat'),\n            data=json.dumps({'body': ''}))\n        self.assertTrue(response.status_code == 400)\n\n        # write a post\n        response = self.client.post(\n            url_for('api.new_post'),\n            headers=self.get_api_headers('john@example.com', 'cat'),\n            data=json.dumps({'body': 'body of the *blog* post'}))\n        self.assertTrue(response.status_code == 201)\n        url = response.headers.get('Location')\n        self.assertIsNotNone(url)\n\n        # get the new post\n        response = self.client.get(\n            url,\n            headers=self.get_api_headers('john@example.com', 'cat'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertTrue(json_response['url'] == url)\n        self.assertTrue(json_response['body'] == 'body of the *blog* post')\n        self.assertTrue(json_response['body_html'] ==\n                        '<p>body of the <em>blog</em> post</p>')\n        json_post = json_response\n\n        # get the post from the user\n        response = self.client.get(\n            url_for('api.get_user_posts', id=u.id),\n            headers=self.get_api_headers('john@example.com', 'cat'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertIsNotNone(json_response.get('posts'))\n        self.assertTrue(json_response.get('count', 0) == 1)\n        self.assertTrue(json_response['posts'][0] == json_post)\n\n        # get the post from the user as a follower\n        response = self.client.get(\n            url_for('api.get_user_followed_posts', id=u.id),\n            headers=self.get_api_headers('john@example.com', 'cat'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertIsNotNone(json_response.get('posts'))\n        self.assertTrue(json_response.get('count', 0) == 1)\n        self.assertTrue(json_response['posts'][0] == json_post)\n\n        # edit post\n        response = self.client.put(\n            url,\n            headers=self.get_api_headers('john@example.com', 'cat'),\n            data=json.dumps({'body': 'updated body'}))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertTrue(json_response['url'] == url)\n        self.assertTrue(json_response['body'] == 'updated body')\n        self.assertTrue(json_response['body_html'] == '<p>updated body</p>')\n\n    def test_users(self):\n        # add two users\n        r = Role.query.filter_by(name='User').first()\n        self.assertIsNotNone(r)\n        u1 = User(email='john@example.com', username='john',\n                  password='cat', confirmed=True, role=r)\n        u2 = User(email='susan@example.com', username='susan',\n                  password='dog', confirmed=True, role=r)\n        db.session.add_all([u1, u2])\n        db.session.commit()\n\n        # get users\n        response = self.client.get(\n            url_for('api.get_user', id=u1.id),\n            headers=self.get_api_headers('susan@example.com', 'dog'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertTrue(json_response['username'] == 'john')\n        response = self.client.get(\n            url_for('api.get_user', id=u2.id),\n            headers=self.get_api_headers('susan@example.com', 'dog'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertTrue(json_response['username'] == 'susan')\n\n    def test_comments(self):\n        # add two users\n        r = Role.query.filter_by(name='User').first()\n        self.assertIsNotNone(r)\n        u1 = User(email='john@example.com', username='john',\n                  password='cat', confirmed=True, role=r)\n        u2 = User(email='susan@example.com', username='susan',\n                  password='dog', confirmed=True, role=r)\n        db.session.add_all([u1, u2])\n        db.session.commit()\n\n        # add a post\n        post = Post(body='body of the post', author=u1)\n        db.session.add(post)\n        db.session.commit()\n\n        # write a comment\n        response = self.client.post(\n            url_for('api.new_post_comment', id=post.id),\n            headers=self.get_api_headers('susan@example.com', 'dog'),\n            data=json.dumps({'body': 'Good [post](http://example.com)!'}))\n        self.assertTrue(response.status_code == 201)\n        json_response = json.loads(response.data.decode('utf-8'))\n        url = response.headers.get('Location')\n        self.assertIsNotNone(url)\n        self.assertTrue(json_response['body'] ==\n                        'Good [post](http://example.com)!')\n        self.assertTrue(\n            re.sub('<.*?>', '', json_response['body_html']) == 'Good post!')\n\n        # get the new comment\n        response = self.client.get(\n            url,\n            headers=self.get_api_headers('john@example.com', 'cat'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertTrue(json_response['url'] == url)\n        self.assertTrue(json_response['body'] ==\n                        'Good [post](http://example.com)!')\n\n        # add another comment\n        comment = Comment(body='Thank you!', author=u1, post=post)\n        db.session.add(comment)\n        db.session.commit()\n\n        # get the two comments from the post\n        response = self.client.get(\n            url_for('api.get_post_comments', id=post.id),\n            headers=self.get_api_headers('susan@example.com', 'dog'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertIsNotNone(json_response.get('comments'))\n        self.assertTrue(json_response.get('count', 0) == 2)\n\n        # get all the comments\n        response = self.client.get(\n            url_for('api.get_comments', id=post.id),\n            headers=self.get_api_headers('susan@example.com', 'dog'))\n        self.assertTrue(response.status_code == 200)\n        json_response = json.loads(response.data.decode('utf-8'))\n        self.assertIsNotNone(json_response.get('comments'))\n        self.assertTrue(json_response.get('count', 0) == 2)\n"
  },
  {
    "path": "tests/test_basics.py",
    "content": "import unittest\nfrom flask import current_app\nfrom app import create_app, db\n\n\nclass BasicsTestCase(unittest.TestCase):\n    def setUp(self):\n        self.app = create_app('testing')\n        self.app_context = self.app.app_context()\n        self.app_context.push()\n        db.create_all()\n\n    def tearDown(self):\n        db.session.remove()\n        db.drop_all()\n        self.app_context.pop()\n\n    def test_app_exists(self):\n        self.assertFalse(current_app is None)\n\n    def test_app_is_testing(self):\n        self.assertTrue(current_app.config['TESTING'])\n"
  },
  {
    "path": "tests/test_client.py",
    "content": "import re\nimport unittest\nfrom flask import url_for\nfrom app import create_app, db\nfrom app.models import User, Role\n\nclass FlaskClientTestCase(unittest.TestCase):\n    def setUp(self):\n        self.app = create_app('testing')\n        self.app_context = self.app.app_context()\n        self.app_context.push()\n        db.create_all()\n        Role.insert_roles()\n        self.client = self.app.test_client(use_cookies=True)\n\n    def tearDown(self):\n        db.session.remove()\n        db.drop_all()\n        self.app_context.pop()\n\n    def test_home_page(self):\n        response = self.client.get(url_for('main.index'))\n        self.assertTrue(b'Stranger' in response.data)\n\n    def test_register_and_login(self):\n        # register a new account\n        response = self.client.post(url_for('auth.register'), data={\n            'email': 'john@example.com',\n            'username': 'john',\n            'password': 'cat',\n            'password2': 'cat'\n        })\n        self.assertTrue(response.status_code == 302)\n\n        # login with the new account\n        response = self.client.post(url_for('auth.login'), data={\n            'email': 'john@example.com',\n            'password': 'cat'\n        }, follow_redirects=True)\n        self.assertTrue(re.search(b'Hello,\\s+john!', response.data))\n        self.assertTrue(\n            b'You have not confirmed your account yet' in response.data)\n\n        # send a confirmation token\n        user = User.query.filter_by(email='john@example.com').first()\n        token = user.generate_confirmation_token()\n        response = self.client.get(url_for('auth.confirm', token=token),\n                                   follow_redirects=True)\n        self.assertTrue(\n            b'You have confirmed your account' in response.data)\n\n        # log out\n        response = self.client.get(url_for('auth.logout'), follow_redirects=True)\n        self.assertTrue(b'You have been logged out' in response.data)\n"
  },
  {
    "path": "tests/test_selenium.py",
    "content": "import re\nimport threading\nimport time\nimport unittest\nfrom selenium import webdriver\nfrom app import create_app, db\nfrom app.models import Role, User, Post\n\n\nclass SeleniumTestCase(unittest.TestCase):\n    client = None\n    \n    @classmethod\n    def setUpClass(cls):\n        # start Firefox\n        try:\n            cls.client = webdriver.Firefox()\n        except:\n            pass\n\n        # skip these tests if the browser could not be started\n        if cls.client:\n            # create the application\n            cls.app = create_app('testing')\n            cls.app_context = cls.app.app_context()\n            cls.app_context.push()\n\n            # suppress logging to keep unittest output clean\n            import logging\n            logger = logging.getLogger('werkzeug')\n            logger.setLevel(\"ERROR\")\n\n            # create the database and populate with some fake data\n            db.create_all()\n            Role.insert_roles()\n            User.generate_fake(10)\n            Post.generate_fake(10)\n\n            # add an administrator user\n            admin_role = Role.query.filter_by(permissions=0xff).first()\n            admin = User(email='john@example.com',\n                         username='john', password='cat',\n                         role=admin_role, confirmed=True)\n            db.session.add(admin)\n            db.session.commit()\n\n            # start the Flask server in a thread\n            threading.Thread(target=cls.app.run).start()\n\n            # give the server a second to ensure it is up\n            time.sleep(1) \n\n    @classmethod\n    def tearDownClass(cls):\n        if cls.client:\n            # stop the flask server and the browser\n            cls.client.get('http://localhost:5000/shutdown')\n            cls.client.close()\n\n            # destroy database\n            db.drop_all()\n            db.session.remove()\n\n            # remove application context\n            cls.app_context.pop()\n\n    def setUp(self):\n        if not self.client:\n            self.skipTest('Web browser not available')\n\n    def tearDown(self):\n        pass\n    \n    def test_admin_home_page(self):\n        # navigate to home page\n        self.client.get('http://localhost:5000/')\n        self.assertTrue(re.search('Hello,\\s+Stranger!',\n                                  self.client.page_source))\n\n        # navigate to login page\n        self.client.find_element_by_link_text('Log In').click()\n        self.assertTrue('<h1>Login</h1>' in self.client.page_source)\n\n        # login\n        self.client.find_element_by_name('email').\\\n            send_keys('john@example.com')\n        self.client.find_element_by_name('password').send_keys('cat')\n        self.client.find_element_by_name('submit').click()\n        self.assertTrue(re.search('Hello,\\s+john!', self.client.page_source))\n\n        # navigate to the user's profile page\n        self.client.find_element_by_link_text('Profile').click()\n        self.assertTrue('<h1>john</h1>' in self.client.page_source)\n"
  },
  {
    "path": "tests/test_user_model.py",
    "content": "import unittest\nimport time\nfrom datetime import datetime\nfrom app import create_app, db\nfrom app.models import User, AnonymousUser, Role, Permission, Follow\n\n\nclass UserModelTestCase(unittest.TestCase):\n    def setUp(self):\n        self.app = create_app('testing')\n        self.app_context = self.app.app_context()\n        self.app_context.push()\n        db.create_all()\n        Role.insert_roles()\n\n    def tearDown(self):\n        db.session.remove()\n        db.drop_all()\n        self.app_context.pop()\n\n    def test_password_setter(self):\n        u = User(password='cat')\n        self.assertTrue(u.password_hash is not None)\n\n    def test_no_password_getter(self):\n        u = User(password='cat')\n        with self.assertRaises(AttributeError):\n            u.password\n\n    def test_password_verification(self):\n        u = User(password='cat')\n        self.assertTrue(u.verify_password('cat'))\n        self.assertFalse(u.verify_password('dog'))\n\n    def test_password_salts_are_random(self):\n        u = User(password='cat')\n        u2 = User(password='cat')\n        self.assertTrue(u.password_hash != u2.password_hash)\n\n    def test_valid_confirmation_token(self):\n        u = User(password='cat')\n        db.session.add(u)\n        db.session.commit()\n        token = u.generate_confirmation_token()\n        self.assertTrue(u.confirm(token))\n\n    def test_invalid_confirmation_token(self):\n        u1 = User(password='cat')\n        u2 = User(password='dog')\n        db.session.add(u1)\n        db.session.add(u2)\n        db.session.commit()\n        token = u1.generate_confirmation_token()\n        self.assertFalse(u2.confirm(token))\n\n    def test_expired_confirmation_token(self):\n        u = User(password='cat')\n        db.session.add(u)\n        db.session.commit()\n        token = u.generate_confirmation_token(1)\n        time.sleep(2)\n        self.assertFalse(u.confirm(token))\n\n    def test_valid_reset_token(self):\n        u = User(password='cat')\n        db.session.add(u)\n        db.session.commit()\n        token = u.generate_reset_token()\n        self.assertTrue(u.reset_password(token, 'dog'))\n        self.assertTrue(u.verify_password('dog'))\n\n    def test_invalid_reset_token(self):\n        u1 = User(password='cat')\n        u2 = User(password='dog')\n        db.session.add(u1)\n        db.session.add(u2)\n        db.session.commit()\n        token = u1.generate_reset_token()\n        self.assertFalse(u2.reset_password(token, 'horse'))\n        self.assertTrue(u2.verify_password('dog'))\n\n    def test_valid_email_change_token(self):\n        u = User(email='john@example.com', password='cat')\n        db.session.add(u)\n        db.session.commit()\n        token = u.generate_email_change_token('susan@example.org')\n        self.assertTrue(u.change_email(token))\n        self.assertTrue(u.email == 'susan@example.org')\n\n    def test_invalid_email_change_token(self):\n        u1 = User(email='john@example.com', password='cat')\n        u2 = User(email='susan@example.org', password='dog')\n        db.session.add(u1)\n        db.session.add(u2)\n        db.session.commit()\n        token = u1.generate_email_change_token('david@example.net')\n        self.assertFalse(u2.change_email(token))\n        self.assertTrue(u2.email == 'susan@example.org')\n\n    def test_duplicate_email_change_token(self):\n        u1 = User(email='john@example.com', password='cat')\n        u2 = User(email='susan@example.org', password='dog')\n        db.session.add(u1)\n        db.session.add(u2)\n        db.session.commit()\n        token = u2.generate_email_change_token('john@example.com')\n        self.assertFalse(u2.change_email(token))\n        self.assertTrue(u2.email == 'susan@example.org')\n\n    def test_roles_and_permissions(self):\n        u = User(email='john@example.com', password='cat')\n        self.assertTrue(u.can(Permission.WRITE_ARTICLES))\n        self.assertFalse(u.can(Permission.MODERATE_COMMENTS))\n\n    def test_anonymous_user(self):\n        u = AnonymousUser()\n        self.assertFalse(u.can(Permission.FOLLOW))\n\n    def test_timestamps(self):\n        u = User(password='cat')\n        db.session.add(u)\n        db.session.commit()\n        self.assertTrue(\n            (datetime.utcnow() - u.member_since).total_seconds() < 3)\n        self.assertTrue(\n            (datetime.utcnow() - u.last_seen).total_seconds() < 3)\n\n    def test_ping(self):\n        u = User(password='cat')\n        db.session.add(u)\n        db.session.commit()\n        time.sleep(2)\n        last_seen_before = u.last_seen\n        u.ping()\n        self.assertTrue(u.last_seen > last_seen_before)\n\n    def test_gravatar(self):\n        u = User(email='john@example.com', password='cat')\n        with self.app.test_request_context('/'):\n            gravatar = u.gravatar()\n            gravatar_256 = u.gravatar(size=256)\n            gravatar_pg = u.gravatar(rating='pg')\n            gravatar_retro = u.gravatar(default='retro')\n        with self.app.test_request_context('/', base_url='https://example.com'):\n            gravatar_ssl = u.gravatar()\n        self.assertTrue('http://www.gravatar.com/avatar/' +\n                        'd4c74594d841139328695756648b6bd6'in gravatar)\n        self.assertTrue('s=256' in gravatar_256)\n        self.assertTrue('r=pg' in gravatar_pg)\n        self.assertTrue('d=retro' in gravatar_retro)\n        self.assertTrue('https://secure.gravatar.com/avatar/' +\n                        'd4c74594d841139328695756648b6bd6' in gravatar_ssl)\n\n    def test_follows(self):\n        u1 = User(email='john@example.com', password='cat')\n        u2 = User(email='susan@example.org', password='dog')\n        db.session.add(u1)\n        db.session.add(u2)\n        db.session.commit()\n        self.assertFalse(u1.is_following(u2))\n        self.assertFalse(u1.is_followed_by(u2))\n        timestamp_before = datetime.utcnow()\n        u1.follow(u2)\n        db.session.add(u1)\n        db.session.commit()\n        timestamp_after = datetime.utcnow()\n        self.assertTrue(u1.is_following(u2))\n        self.assertFalse(u1.is_followed_by(u2))\n        self.assertTrue(u2.is_followed_by(u1))\n        self.assertTrue(u1.followed.count() == 2)\n        self.assertTrue(u2.followers.count() == 2)\n        f = u1.followed.all()[-1]\n        self.assertTrue(f.followed == u2)\n        self.assertTrue(timestamp_before <= f.timestamp <= timestamp_after)\n        f = u2.followers.all()[-1]\n        self.assertTrue(f.follower == u1)\n        u1.unfollow(u2)\n        db.session.add(u1)\n        db.session.commit()\n        self.assertTrue(u1.followed.count() == 1)\n        self.assertTrue(u2.followers.count() == 1)\n        self.assertTrue(Follow.query.count() == 2)\n        u2.follow(u1)\n        db.session.add(u1)\n        db.session.add(u2)\n        db.session.commit()\n        db.session.delete(u2)\n        db.session.commit()\n        self.assertTrue(Follow.query.count() == 1)\n\n    def test_to_json(self):\n        u = User(email='john@example.com', password='cat')\n        db.session.add(u)\n        db.session.commit()\n        json_user = u.to_json()\n        expected_keys = ['url', 'username', 'member_since', 'last_seen',\n                         'posts', 'followed_posts', 'post_count']\n        self.assertEqual(sorted(json_user.keys()), sorted(expected_keys))\n        self.assertTrue('api/v1.0/users/' in json_user['url'])\n"
  }
]