Repository: miguelgrinberg/flasky
Branch: master
Commit: 3beedd640b91
Files: 84
Total size: 112.7 KB
Directory structure:
gitextract_ml7yrowa/
├── .gitignore
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── app/
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── authentication.py
│ │ ├── comments.py
│ │ ├── decorators.py
│ │ ├── errors.py
│ │ ├── posts.py
│ │ └── users.py
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── forms.py
│ │ └── views.py
│ ├── decorators.py
│ ├── email.py
│ ├── exceptions.py
│ ├── fake.py
│ ├── main/
│ │ ├── __init__.py
│ │ ├── errors.py
│ │ ├── forms.py
│ │ └── views.py
│ ├── models.py
│ ├── static/
│ │ └── styles.css
│ └── templates/
│ ├── 403.html
│ ├── 404.html
│ ├── 500.html
│ ├── _comments.html
│ ├── _macros.html
│ ├── _posts.html
│ ├── auth/
│ │ ├── change_email.html
│ │ ├── change_password.html
│ │ ├── email/
│ │ │ ├── change_email.html
│ │ │ ├── change_email.txt
│ │ │ ├── confirm.html
│ │ │ ├── confirm.txt
│ │ │ ├── reset_password.html
│ │ │ └── reset_password.txt
│ │ ├── login.html
│ │ ├── register.html
│ │ ├── reset_password.html
│ │ └── unconfirmed.html
│ ├── base.html
│ ├── edit_post.html
│ ├── edit_profile.html
│ ├── followers.html
│ ├── index.html
│ ├── mail/
│ │ ├── new_user.html
│ │ └── new_user.txt
│ ├── moderate.html
│ ├── post.html
│ └── user.html
├── boot.sh
├── config.py
├── docker-compose.yml
├── flasky.py
├── migrations/
│ ├── README
│ ├── alembic.ini
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
│ ├── 190163627111_account_confirmation.py
│ ├── 198b0eebcf9_caching_of_avatar_hashes.py
│ ├── 1b966e7f4b9e_post_model.py
│ ├── 2356a38169ea_followers.py
│ ├── 288cd3dc5a8_rich_text_posts.py
│ ├── 38c4e85512a9_initial_migration.py
│ ├── 456a945560f6_login_support.py
│ ├── 51f5ccfba190_comments.py
│ ├── 56ed7d33de8d_user_roles.py
│ └── d66f086b258_user_information.py
├── requirements/
│ ├── common.txt
│ ├── dev.txt
│ ├── docker.txt
│ ├── heroku.txt
│ └── prod.txt
├── requirements.txt
└── tests/
├── __init__.py
├── test_api.py
├── test_basics.py
├── test_client.py
├── test_selenium.py
└── test_user_model.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
__pycache__
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# SQLite databases
*.sqlite
# Virtual environment
venv
# Environment files
.env
.env-mysql
================================================
FILE: Dockerfile
================================================
FROM python:3.6-alpine
ENV FLASK_APP flasky.py
ENV FLASK_CONFIG production
RUN adduser -D flasky
USER flasky
WORKDIR /home/flasky
COPY requirements requirements
RUN python -m venv venv
RUN venv/bin/pip install -r requirements/docker.txt
COPY app app
COPY migrations migrations
COPY flasky.py config.py boot.sh ./
# run-time configuration
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2013 Miguel Grinberg
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
================================================
FILE: Procfile
================================================
web: gunicorn flasky:app
================================================
FILE: README.md
================================================
Flasky
======
This repository contains the source code examples for the second edition of my O'Reilly book [Flask Web Development](http://www.flaskbook.com).
The commits and tags in this repository were carefully created to match the sequence in which concepts are presented in the book. Please read the section titled "How to Work with the Example Code" in the book's preface for instructions.
For Readers of the First Edition of the Book
--------------------------------------------
The code examples for the first edition of the book were moved to a different repository: [https://github.com/miguelgrinberg/flasky-first-edition](https://github.com/miguelgrinberg/flasky-first-edition).
================================================
FILE: app/__init__.py
================================================
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_pagedown import PageDown
from config import config
bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()
pagedown = PageDown()
login_manager = LoginManager()
login_manager.login_view = 'auth.login'
def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
login_manager.init_app(app)
pagedown.init_app(app)
if app.config['SSL_REDIRECT']:
from flask_sslify import SSLify
sslify = SSLify(app)
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
from .auth import auth as auth_blueprint
app.register_blueprint(auth_blueprint, url_prefix='/auth')
from .api import api as api_blueprint
app.register_blueprint(api_blueprint, url_prefix='/api/v1')
return app
================================================
FILE: app/api/__init__.py
================================================
from flask import Blueprint
api = Blueprint('api', __name__)
from . import authentication, posts, users, comments, errors
================================================
FILE: app/api/authentication.py
================================================
from flask import g, jsonify
from flask_httpauth import HTTPBasicAuth
from ..models import User
from . import api
from .errors import unauthorized, forbidden
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(email_or_token, password):
if email_or_token == '':
return False
if password == '':
g.current_user = User.verify_auth_token(email_or_token)
g.token_used = True
return g.current_user is not None
user = User.query.filter_by(email=email_or_token.lower()).first()
if not user:
return False
g.current_user = user
g.token_used = False
return user.verify_password(password)
@auth.error_handler
def auth_error():
return unauthorized('Invalid credentials')
@api.before_request
@auth.login_required
def before_request():
if not g.current_user.is_anonymous and \
not g.current_user.confirmed:
return forbidden('Unconfirmed account')
@api.route('/tokens/', methods=['POST'])
def get_token():
if g.current_user.is_anonymous or g.token_used:
return unauthorized('Invalid credentials')
return jsonify({'token': g.current_user.generate_auth_token(
expiration=3600), 'expiration': 3600})
================================================
FILE: app/api/comments.py
================================================
from flask import jsonify, request, g, url_for, current_app
from .. import db
from ..models import Post, Permission, Comment
from . import api
from .decorators import permission_required
@api.route('/comments/')
def get_comments():
page = request.args.get('page', 1, type=int)
pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
page=page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
error_out=False)
comments = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_comments', page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_comments', page=page+1)
return jsonify({
'comments': [comment.to_json() for comment in comments],
'prev': prev,
'next': next,
'count': pagination.total
})
@api.route('/comments/<int:id>')
def get_comment(id):
comment = Comment.query.get_or_404(id)
return jsonify(comment.to_json())
@api.route('/posts/<int:id>/comments/')
def get_post_comments(id):
post = Post.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
page=page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
error_out=False)
comments = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_post_comments', id=id, page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_post_comments', id=id, page=page+1)
return jsonify({
'comments': [comment.to_json() for comment in comments],
'prev': prev,
'next': next,
'count': pagination.total
})
@api.route('/posts/<int:id>/comments/', methods=['POST'])
@permission_required(Permission.COMMENT)
def new_post_comment(id):
post = Post.query.get_or_404(id)
comment = Comment.from_json(request.json)
comment.author = g.current_user
comment.post = post
db.session.add(comment)
db.session.commit()
return jsonify(comment.to_json()), 201, \
{'Location': url_for('api.get_comment', id=comment.id)}
================================================
FILE: app/api/decorators.py
================================================
from functools import wraps
from flask import g
from .errors import forbidden
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not g.current_user.can(permission):
return forbidden('Insufficient permissions')
return f(*args, **kwargs)
return decorated_function
return decorator
================================================
FILE: app/api/errors.py
================================================
from flask import jsonify
from app.exceptions import ValidationError
from . import api
def bad_request(message):
response = jsonify({'error': 'bad request', 'message': message})
response.status_code = 400
return response
def unauthorized(message):
response = jsonify({'error': 'unauthorized', 'message': message})
response.status_code = 401
return response
def forbidden(message):
response = jsonify({'error': 'forbidden', 'message': message})
response.status_code = 403
return response
@api.errorhandler(ValidationError)
def validation_error(e):
return bad_request(e.args[0])
================================================
FILE: app/api/posts.py
================================================
from flask import jsonify, request, g, url_for, current_app
from .. import db
from ..models import Post, Permission
from . import api
from .decorators import permission_required
from .errors import forbidden
@api.route('/posts/')
def get_posts():
page = request.args.get('page', 1, type=int)
pagination = Post.query.paginate(
page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_posts', page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_posts', page=page+1)
return jsonify({
'posts': [post.to_json() for post in posts],
'prev': prev,
'next': next,
'count': pagination.total
})
@api.route('/posts/<int:id>')
def get_post(id):
post = Post.query.get_or_404(id)
return jsonify(post.to_json())
@api.route('/posts/', methods=['POST'])
@permission_required(Permission.WRITE)
def new_post():
post = Post.from_json(request.json)
post.author = g.current_user
db.session.add(post)
db.session.commit()
return jsonify(post.to_json()), 201, \
{'Location': url_for('api.get_post', id=post.id)}
@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE)
def edit_post(id):
post = Post.query.get_or_404(id)
if g.current_user != post.author and \
not g.current_user.can(Permission.ADMIN):
return forbidden('Insufficient permissions')
post.body = request.json.get('body', post.body)
db.session.add(post)
db.session.commit()
return jsonify(post.to_json())
================================================
FILE: app/api/users.py
================================================
from flask import jsonify, request, current_app, url_for
from . import api
from ..models import User, Post
@api.route('/users/<int:id>')
def get_user(id):
user = User.query.get_or_404(id)
return jsonify(user.to_json())
@api.route('/users/<int:id>/posts/')
def get_user_posts(id):
user = User.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
pagination = user.posts.order_by(Post.timestamp.desc()).paginate(
page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_user_posts', id=id, page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_user_posts', id=id, page=page+1)
return jsonify({
'posts': [post.to_json() for post in posts],
'prev': prev,
'next': next,
'count': pagination.total
})
@api.route('/users/<int:id>/timeline/')
def get_user_followed_posts(id):
user = User.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
pagination = user.followed_posts.order_by(Post.timestamp.desc()).paginate(
page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_user_followed_posts', id=id, page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_user_followed_posts', id=id, page=page+1)
return jsonify({
'posts': [post.to_json() for post in posts],
'prev': prev,
'next': next,
'count': pagination.total
})
================================================
FILE: app/auth/__init__.py
================================================
from flask import Blueprint
auth = Blueprint('auth', __name__)
from . import views
================================================
FILE: app/auth/forms.py
================================================
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
Email()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log In')
class RegistrationForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
Email()])
username = StringField('Username', validators=[
DataRequired(), Length(1, 64),
Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'Usernames must have only letters, numbers, dots or '
'underscores')])
password = PasswordField('Password', validators=[
DataRequired(), EqualTo('password2', message='Passwords must match.')])
password2 = PasswordField('Confirm password', validators=[DataRequired()])
submit = SubmitField('Register')
def validate_email(self, field):
if User.query.filter_by(email=field.data.lower()).first():
raise ValidationError('Email already registered.')
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
class ChangePasswordForm(FlaskForm):
old_password = PasswordField('Old password', validators=[DataRequired()])
password = PasswordField('New password', validators=[
DataRequired(), EqualTo('password2', message='Passwords must match.')])
password2 = PasswordField('Confirm new password',
validators=[DataRequired()])
submit = SubmitField('Update Password')
class PasswordResetRequestForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
Email()])
submit = SubmitField('Reset Password')
class PasswordResetForm(FlaskForm):
password = PasswordField('New Password', validators=[
DataRequired(), EqualTo('password2', message='Passwords must match')])
password2 = PasswordField('Confirm password', validators=[DataRequired()])
submit = SubmitField('Reset Password')
class ChangeEmailForm(FlaskForm):
email = StringField('New Email', validators=[DataRequired(), Length(1, 64),
Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Update Email Address')
def validate_email(self, field):
if User.query.filter_by(email=field.data.lower()).first():
raise ValidationError('Email already registered.')
================================================
FILE: app/auth/views.py
================================================
from flask import render_template, redirect, request, url_for, flash
from flask_login import login_user, logout_user, login_required, \
current_user
from . import auth
from .. import db
from ..models import User
from ..email import send_email
from .forms import LoginForm, RegistrationForm, ChangePasswordForm,\
PasswordResetRequestForm, PasswordResetForm, ChangeEmailForm
@auth.before_app_request
def before_request():
if current_user.is_authenticated:
current_user.ping()
if not current_user.confirmed \
and request.endpoint \
and request.blueprint != 'auth' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))
@auth.route('/unconfirmed')
def unconfirmed():
if current_user.is_anonymous or current_user.confirmed:
return redirect(url_for('main.index'))
return render_template('auth/unconfirmed.html')
@auth.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
next = request.args.get('next')
if next is None or not next.startswith('/'):
next = url_for('main.index')
return redirect(next)
flash('Invalid email or password.')
return render_template('auth/login.html', form=form)
@auth.route('/logout')
@login_required
def logout():
logout_user()
flash('You have been logged out.')
return redirect(url_for('main.index'))
@auth.route('/register', methods=['GET', 'POST'])
def register():
form = RegistrationForm()
if form.validate_on_submit():
user = User(email=form.email.data.lower(),
username=form.username.data,
password=form.password.data)
db.session.add(user)
db.session.commit()
token = user.generate_confirmation_token()
send_email(user.email, 'Confirm Your Account',
'auth/email/confirm', user=user, token=token)
flash('A confirmation email has been sent to you by email.')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', form=form)
@auth.route('/confirm/<token>')
@login_required
def confirm(token):
if current_user.confirmed:
return redirect(url_for('main.index'))
if current_user.confirm(token):
db.session.commit()
flash('You have confirmed your account. Thanks!')
else:
flash('The confirmation link is invalid or has expired.')
return redirect(url_for('main.index'))
@auth.route('/confirm')
@login_required
def resend_confirmation():
token = current_user.generate_confirmation_token()
send_email(current_user.email, 'Confirm Your Account',
'auth/email/confirm', user=current_user, token=token)
flash('A new confirmation email has been sent to you by email.')
return redirect(url_for('main.index'))
@auth.route('/change-password', methods=['GET', 'POST'])
@login_required
def change_password():
form = ChangePasswordForm()
if form.validate_on_submit():
if current_user.verify_password(form.old_password.data):
current_user.password = form.password.data
db.session.add(current_user)
db.session.commit()
flash('Your password has been updated.')
return redirect(url_for('main.index'))
else:
flash('Invalid password.')
return render_template("auth/change_password.html", form=form)
@auth.route('/reset', methods=['GET', 'POST'])
def password_reset_request():
if not current_user.is_anonymous:
return redirect(url_for('main.index'))
form = PasswordResetRequestForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data.lower()).first()
if user:
token = user.generate_reset_token()
send_email(user.email, 'Reset Your Password',
'auth/email/reset_password',
user=user, token=token)
flash('An email with instructions to reset your password has been '
'sent to you.')
return redirect(url_for('auth.login'))
return render_template('auth/reset_password.html', form=form)
@auth.route('/reset/<token>', methods=['GET', 'POST'])
def password_reset(token):
if not current_user.is_anonymous:
return redirect(url_for('main.index'))
form = PasswordResetForm()
if form.validate_on_submit():
if User.reset_password(token, form.password.data):
db.session.commit()
flash('Your password has been updated.')
return redirect(url_for('auth.login'))
else:
return redirect(url_for('main.index'))
return render_template('auth/reset_password.html', form=form)
@auth.route('/change_email', methods=['GET', 'POST'])
@login_required
def change_email_request():
form = ChangeEmailForm()
if form.validate_on_submit():
if current_user.verify_password(form.password.data):
new_email = form.email.data.lower()
token = current_user.generate_email_change_token(new_email)
send_email(new_email, 'Confirm your email address',
'auth/email/change_email',
user=current_user, token=token)
flash('An email with instructions to confirm your new email '
'address has been sent to you.')
return redirect(url_for('main.index'))
else:
flash('Invalid email or password.')
return render_template("auth/change_email.html", form=form)
@auth.route('/change_email/<token>')
@login_required
def change_email(token):
if current_user.change_email(token):
db.session.commit()
flash('Your email address has been updated.')
else:
flash('Invalid request.')
return redirect(url_for('main.index'))
================================================
FILE: app/decorators.py
================================================
from functools import wraps
from flask import abort
from flask_login import current_user
from .models import Permission
def permission_required(permission):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.can(permission):
abort(403)
return f(*args, **kwargs)
return decorated_function
return decorator
def admin_required(f):
return permission_required(Permission.ADMIN)(f)
================================================
FILE: app/email.py
================================================
from threading import Thread
from flask import current_app, render_template
from flask_mail import Message
from . import mail
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
def send_email(to, subject, template, **kwargs):
app = current_app._get_current_object()
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + ' ' + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
================================================
FILE: app/exceptions.py
================================================
class ValidationError(ValueError):
pass
================================================
FILE: app/fake.py
================================================
from random import randint
from sqlalchemy.exc import IntegrityError
from faker import Faker
from . import db
from .models import User, Post
def users(count=100):
fake = Faker()
i = 0
while i < count:
u = User(email=fake.email(),
username=fake.user_name(),
password='password',
confirmed=True,
name=fake.name(),
location=fake.city(),
about_me=fake.text(),
member_since=fake.past_date())
db.session.add(u)
try:
db.session.commit()
i += 1
except IntegrityError:
db.session.rollback()
def posts(count=100):
fake = Faker()
user_count = User.query.count()
for i in range(count):
u = User.query.offset(randint(0, user_count - 1)).first()
p = Post(body=fake.text(),
timestamp=fake.past_date(),
author=u)
db.session.add(p)
db.session.commit()
================================================
FILE: app/main/__init__.py
================================================
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors
from ..models import Permission
@main.app_context_processor
def inject_permissions():
return dict(Permission=Permission)
================================================
FILE: app/main/errors.py
================================================
from flask import render_template, request, jsonify
from . import main
@main.app_errorhandler(403)
def forbidden(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'forbidden'})
response.status_code = 403
return response
return render_template('403.html'), 403
@main.app_errorhandler(404)
def page_not_found(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'not found'})
response.status_code = 404
return response
return render_template('404.html'), 404
@main.app_errorhandler(500)
def internal_server_error(e):
if request.accept_mimetypes.accept_json and \
not request.accept_mimetypes.accept_html:
response = jsonify({'error': 'internal server error'})
response.status_code = 500
return response
return render_template('500.html'), 500
================================================
FILE: app/main/forms.py
================================================
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, BooleanField, SelectField,\
SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp
from wtforms import ValidationError
from flask_pagedown.fields import PageDownField
from ..models import Role, User
class NameForm(FlaskForm):
name = StringField('What is your name?', validators=[DataRequired()])
submit = SubmitField('Submit')
class EditProfileForm(FlaskForm):
name = StringField('Real name', validators=[Length(0, 64)])
location = StringField('Location', validators=[Length(0, 64)])
about_me = TextAreaField('About me')
submit = SubmitField('Submit')
class EditProfileAdminForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
Email()])
username = StringField('Username', validators=[
DataRequired(), Length(1, 64),
Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'Usernames must have only letters, numbers, dots or '
'underscores')])
confirmed = BooleanField('Confirmed')
role = SelectField('Role', coerce=int)
name = StringField('Real name', validators=[Length(0, 64)])
location = StringField('Location', validators=[Length(0, 64)])
about_me = TextAreaField('About me')
submit = SubmitField('Submit')
def __init__(self, user, *args, **kwargs):
super(EditProfileAdminForm, self).__init__(*args, **kwargs)
self.role.choices = [(role.id, role.name)
for role in Role.query.order_by(Role.name).all()]
self.user = user
def validate_email(self, field):
if field.data != self.user.email and \
User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered.')
def validate_username(self, field):
if field.data != self.user.username and \
User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
class PostForm(FlaskForm):
body = PageDownField("What's on your mind?", validators=[DataRequired()])
submit = SubmitField('Submit')
class CommentForm(FlaskForm):
body = StringField('Enter your comment', validators=[DataRequired()])
submit = SubmitField('Submit')
================================================
FILE: app/main/views.py
================================================
from flask import render_template, redirect, url_for, abort, flash, request,\
current_app, make_response
from flask_login import login_required, current_user
from flask_sqlalchemy import get_debug_queries
from . import main
from .forms import EditProfileForm, EditProfileAdminForm, PostForm,\
CommentForm
from .. import db
from ..models import Permission, Role, User, Post, Comment
from ..decorators import admin_required, permission_required
@main.after_app_request
def after_request(response):
for query in get_debug_queries():
if query.duration >= current_app.config['FLASKY_SLOW_DB_QUERY_TIME']:
current_app.logger.warning(
'Slow query: %s\nParameters: %s\nDuration: %fs\nContext: %s\n'
% (query.statement, query.parameters, query.duration,
query.context))
return response
@main.route('/shutdown')
def server_shutdown():
if not current_app.testing:
abort(404)
shutdown = request.environ.get('werkzeug.server.shutdown')
if not shutdown:
abort(500)
shutdown()
return 'Shutting down...'
@main.route('/', methods=['GET', 'POST'])
def index():
form = PostForm()
if current_user.can(Permission.WRITE) and form.validate_on_submit():
post = Post(body=form.body.data,
author=current_user._get_current_object())
db.session.add(post)
db.session.commit()
return redirect(url_for('.index'))
page = request.args.get('page', 1, type=int)
show_followed = False
if current_user.is_authenticated:
show_followed = bool(request.cookies.get('show_followed', ''))
if show_followed:
query = current_user.followed_posts
else:
query = Post.query
pagination = query.order_by(Post.timestamp.desc()).paginate(
page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items
return render_template('index.html', form=form, posts=posts,
show_followed=show_followed, pagination=pagination)
@main.route('/user/<username>')
def user(username):
user = User.query.filter_by(username=username).first_or_404()
page = request.args.get('page', 1, type=int)
pagination = user.posts.order_by(Post.timestamp.desc()).paginate(
page=page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items
return render_template('user.html', user=user, posts=posts,
pagination=pagination)
@main.route('/edit-profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
current_user.name = form.name.data
current_user.location = form.location.data
current_user.about_me = form.about_me.data
db.session.add(current_user._get_current_object())
db.session.commit()
flash('Your profile has been updated.')
return redirect(url_for('.user', username=current_user.username))
form.name.data = current_user.name
form.location.data = current_user.location
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', form=form)
@main.route('/edit-profile/<int:id>', methods=['GET', 'POST'])
@login_required
@admin_required
def edit_profile_admin(id):
user = User.query.get_or_404(id)
form = EditProfileAdminForm(user=user)
if form.validate_on_submit():
user.email = form.email.data
user.username = form.username.data
user.confirmed = form.confirmed.data
user.role = Role.query.get(form.role.data)
user.name = form.name.data
user.location = form.location.data
user.about_me = form.about_me.data
db.session.add(user)
db.session.commit()
flash('The profile has been updated.')
return redirect(url_for('.user', username=user.username))
form.email.data = user.email
form.username.data = user.username
form.confirmed.data = user.confirmed
form.role.data = user.role_id
form.name.data = user.name
form.location.data = user.location
form.about_me.data = user.about_me
return render_template('edit_profile.html', form=form, user=user)
@main.route('/post/<int:id>', methods=['GET', 'POST'])
def post(id):
post = Post.query.get_or_404(id)
form = CommentForm()
if form.validate_on_submit():
comment = Comment(body=form.body.data,
post=post,
author=current_user._get_current_object())
db.session.add(comment)
db.session.commit()
flash('Your comment has been published.')
return redirect(url_for('.post', id=post.id, page=-1))
page = request.args.get('page', 1, type=int)
if page == -1:
page = (post.comments.count() - 1) // \
current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1
pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
page=page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
error_out=False)
comments = pagination.items
return render_template('post.html', posts=[post], form=form,
comments=comments, pagination=pagination)
@main.route('/edit/<int:id>', methods=['GET', 'POST'])
@login_required
def edit(id):
post = Post.query.get_or_404(id)
if current_user != post.author and \
not current_user.can(Permission.ADMIN):
abort(403)
form = PostForm()
if form.validate_on_submit():
post.body = form.body.data
db.session.add(post)
db.session.commit()
flash('The post has been updated.')
return redirect(url_for('.post', id=post.id))
form.body.data = post.body
return render_template('edit_post.html', form=form)
@main.route('/follow/<username>')
@login_required
@permission_required(Permission.FOLLOW)
def follow(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('Invalid user.')
return redirect(url_for('.index'))
if current_user.is_following(user):
flash('You are already following this user.')
return redirect(url_for('.user', username=username))
current_user.follow(user)
db.session.commit()
flash('You are now following %s.' % username)
return redirect(url_for('.user', username=username))
@main.route('/unfollow/<username>')
@login_required
@permission_required(Permission.FOLLOW)
def unfollow(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('Invalid user.')
return redirect(url_for('.index'))
if not current_user.is_following(user):
flash('You are not following this user.')
return redirect(url_for('.user', username=username))
current_user.unfollow(user)
db.session.commit()
flash('You are not following %s anymore.' % username)
return redirect(url_for('.user', username=username))
@main.route('/followers/<username>')
def followers(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('Invalid user.')
return redirect(url_for('.index'))
page = request.args.get('page', 1, type=int)
pagination = user.followers.paginate(
page=page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
error_out=False)
follows = [{'user': item.follower, 'timestamp': item.timestamp}
for item in pagination.items]
return render_template('followers.html', user=user, title="Followers of",
endpoint='.followers', pagination=pagination,
follows=follows)
@main.route('/followed_by/<username>')
def followed_by(username):
user = User.query.filter_by(username=username).first()
if user is None:
flash('Invalid user.')
return redirect(url_for('.index'))
page = request.args.get('page', 1, type=int)
pagination = user.followed.paginate(
page=page, per_page=current_app.config['FLASKY_FOLLOWERS_PER_PAGE'],
error_out=False)
follows = [{'user': item.followed, 'timestamp': item.timestamp}
for item in pagination.items]
return render_template('followers.html', user=user, title="Followed by",
endpoint='.followed_by', pagination=pagination,
follows=follows)
@main.route('/all')
@login_required
def show_all():
resp = make_response(redirect(url_for('.index')))
resp.set_cookie('show_followed', '', max_age=30*24*60*60)
return resp
@main.route('/followed')
@login_required
def show_followed():
resp = make_response(redirect(url_for('.index')))
resp.set_cookie('show_followed', '1', max_age=30*24*60*60)
return resp
@main.route('/moderate')
@login_required
@permission_required(Permission.MODERATE)
def moderate():
page = request.args.get('page', 1, type=int)
pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
page=page, per_page=current_app.config['FLASKY_COMMENTS_PER_PAGE'],
error_out=False)
comments = pagination.items
return render_template('moderate.html', comments=comments,
pagination=pagination, page=page)
@main.route('/moderate/enable/<int:id>')
@login_required
@permission_required(Permission.MODERATE)
def moderate_enable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = False
db.session.add(comment)
db.session.commit()
return redirect(url_for('.moderate',
page=request.args.get('page', 1, type=int)))
@main.route('/moderate/disable/<int:id>')
@login_required
@permission_required(Permission.MODERATE)
def moderate_disable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = True
db.session.add(comment)
db.session.commit()
return redirect(url_for('.moderate',
page=request.args.get('page', 1, type=int)))
================================================
FILE: app/models.py
================================================
from datetime import datetime
import hashlib
from werkzeug.security import generate_password_hash, check_password_hash
from itsdangerous import TimedJSONWebSignatureSerializer as Serializer
from markdown import markdown
import bleach
from flask import current_app, request, url_for
from flask_login import UserMixin, AnonymousUserMixin
from app.exceptions import ValidationError
from . import db, login_manager
class Permission:
FOLLOW = 1
COMMENT = 2
WRITE = 4
MODERATE = 8
ADMIN = 16
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.Integer)
users = db.relationship('User', backref='role', lazy='dynamic')
def __init__(self, **kwargs):
super(Role, self).__init__(**kwargs)
if self.permissions is None:
self.permissions = 0
@staticmethod
def insert_roles():
roles = {
'User': [Permission.FOLLOW, Permission.COMMENT, Permission.WRITE],
'Moderator': [Permission.FOLLOW, Permission.COMMENT,
Permission.WRITE, Permission.MODERATE],
'Administrator': [Permission.FOLLOW, Permission.COMMENT,
Permission.WRITE, Permission.MODERATE,
Permission.ADMIN],
}
default_role = 'User'
for r in roles:
role = Role.query.filter_by(name=r).first()
if role is None:
role = Role(name=r)
role.reset_permissions()
for perm in roles[r]:
role.add_permission(perm)
role.default = (role.name == default_role)
db.session.add(role)
db.session.commit()
def add_permission(self, perm):
if not self.has_permission(perm):
self.permissions += perm
def remove_permission(self, perm):
if self.has_permission(perm):
self.permissions -= perm
def reset_permissions(self):
self.permissions = 0
def has_permission(self, perm):
return self.permissions & perm == perm
def __repr__(self):
return '<Role %r>' % self.name
class Follow(db.Model):
__tablename__ = 'follows'
follower_id = db.Column(db.Integer, db.ForeignKey('users.id'),
primary_key=True)
followed_id = db.Column(db.Integer, db.ForeignKey('users.id'),
primary_key=True)
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True)
username = db.Column(db.String(64), unique=True, index=True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
password_hash = db.Column(db.String(128))
confirmed = db.Column(db.Boolean, default=False)
name = db.Column(db.String(64))
location = db.Column(db.String(64))
about_me = db.Column(db.Text())
member_since = db.Column(db.DateTime(), default=datetime.utcnow)
last_seen = db.Column(db.DateTime(), default=datetime.utcnow)
avatar_hash = db.Column(db.String(32))
posts = db.relationship('Post', backref='author', lazy='dynamic')
followed = db.relationship('Follow',
foreign_keys=[Follow.follower_id],
backref=db.backref('follower', lazy='joined'),
lazy='dynamic',
cascade='all, delete-orphan')
followers = db.relationship('Follow',
foreign_keys=[Follow.followed_id],
backref=db.backref('followed', lazy='joined'),
lazy='dynamic',
cascade='all, delete-orphan')
comments = db.relationship('Comment', backref='author', lazy='dynamic')
@staticmethod
def add_self_follows():
for user in User.query.all():
if not user.is_following(user):
user.follow(user)
db.session.add(user)
db.session.commit()
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
if self.role is None:
if self.email == current_app.config['FLASKY_ADMIN']:
self.role = Role.query.filter_by(name='Administrator').first()
if self.role is None:
self.role = Role.query.filter_by(default=True).first()
if self.email is not None and self.avatar_hash is None:
self.avatar_hash = self.gravatar_hash()
self.follow(self)
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
def generate_confirmation_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'confirm': self.id}).decode('utf-8')
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
if data.get('confirm') != self.id:
return False
self.confirmed = True
db.session.add(self)
return True
def generate_reset_token(self, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps({'reset': self.id}).decode('utf-8')
@staticmethod
def reset_password(token, new_password):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
user = User.query.get(data.get('reset'))
if user is None:
return False
user.password = new_password
db.session.add(user)
return True
def generate_email_change_token(self, new_email, expiration=3600):
s = Serializer(current_app.config['SECRET_KEY'], expiration)
return s.dumps(
{'change_email': self.id, 'new_email': new_email}).decode('utf-8')
def change_email(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token.encode('utf-8'))
except:
return False
if data.get('change_email') != self.id:
return False
new_email = data.get('new_email')
if new_email is None:
return False
if self.query.filter_by(email=new_email).first() is not None:
return False
self.email = new_email
self.avatar_hash = self.gravatar_hash()
db.session.add(self)
return True
def can(self, perm):
return self.role is not None and self.role.has_permission(perm)
def is_administrator(self):
return self.can(Permission.ADMIN)
def ping(self):
self.last_seen = datetime.utcnow()
db.session.add(self)
def gravatar_hash(self):
return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
def gravatar(self, size=100, default='identicon', rating='g'):
url = 'https://secure.gravatar.com/avatar'
hash = self.avatar_hash or self.gravatar_hash()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
url=url, hash=hash, size=size, default=default, rating=rating)
def follow(self, user):
if not self.is_following(user):
f = Follow(follower=self, followed=user)
db.session.add(f)
def unfollow(self, user):
f = self.followed.filter_by(followed_id=user.id).first()
if f:
db.session.delete(f)
def is_following(self, user):
if user.id is None:
return False
return self.followed.filter_by(
followed_id=user.id).first() is not None
def is_followed_by(self, user):
if user.id is None:
return False
return self.followers.filter_by(
follower_id=user.id).first() is not None
@property
def followed_posts(self):
return Post.query.join(Follow, Follow.followed_id == Post.author_id)\
.filter(Follow.follower_id == self.id)
def to_json(self):
json_user = {
'url': url_for('api.get_user', id=self.id),
'username': self.username,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts_url': url_for('api.get_user_posts', id=self.id),
'followed_posts_url': url_for('api.get_user_followed_posts',
id=self.id),
'post_count': self.posts.count()
}
return json_user
def generate_auth_token(self, expiration):
s = Serializer(current_app.config['SECRET_KEY'],
expires_in=expiration)
return s.dumps({'id': self.id}).decode('utf-8')
@staticmethod
def verify_auth_token(token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return None
return User.query.get(data['id'])
def __repr__(self):
return '<User %r>' % self.username
class AnonymousUser(AnonymousUserMixin):
def can(self, permissions):
return False
def is_administrator(self):
return False
login_manager.anonymous_user = AnonymousUser
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
body_html = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
comments = db.relationship('Comment', backref='post', lazy='dynamic')
@staticmethod
def on_changed_body(target, value, oldvalue, initiator):
allowed_tags = ['a', 'abbr', 'acronym', 'b', 'blockquote', 'code',
'em', 'i', 'li', 'ol', 'pre', 'strong', 'ul',
'h1', 'h2', 'h3', 'p']
target.body_html = bleach.linkify(bleach.clean(
markdown(value, output_format='html'),
tags=allowed_tags, strip=True))
def to_json(self):
json_post = {
'url': url_for('api.get_post', id=self.id),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author_url': url_for('api.get_user', id=self.author_id),
'comments_url': url_for('api.get_post_comments', id=self.id),
'comment_count': self.comments.count()
}
return json_post
@staticmethod
def from_json(json_post):
body = json_post.get('body')
if body is None or body == '':
raise ValidationError('post does not have a body')
return Post(body=body)
db.event.listen(Post.body, 'set', Post.on_changed_body)
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
body_html = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
disabled = db.Column(db.Boolean)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
@staticmethod
def on_changed_body(target, value, oldvalue, initiator):
allowed_tags = ['a', 'abbr', 'acronym', 'b', 'code', 'em', 'i',
'strong']
target.body_html = bleach.linkify(bleach.clean(
markdown(value, output_format='html'),
tags=allowed_tags, strip=True))
def to_json(self):
json_comment = {
'url': url_for('api.get_comment', id=self.id),
'post_url': url_for('api.get_post', id=self.post_id),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author_url': url_for('api.get_user', id=self.author_id),
}
return json_comment
@staticmethod
def from_json(json_comment):
body = json_comment.get('body')
if body is None or body == '':
raise ValidationError('comment does not have a body')
return Comment(body=body)
db.event.listen(Comment.body, 'set', Comment.on_changed_body)
================================================
FILE: app/static/styles.css
================================================
.profile-thumbnail {
position: absolute;
}
.profile-header {
min-height: 260px;
margin-left: 280px;
}
div.post-tabs {
margin-top: 16px;
}
ul.posts {
list-style-type: none;
padding: 0px;
margin: 16px 0px 0px 0px;
border-top: 1px solid #e0e0e0;
}
div.post-tabs ul.posts {
margin: 0px;
border-top: none;
}
ul.posts li.post {
padding: 8px;
border-bottom: 1px solid #e0e0e0;
}
ul.posts li.post:hover {
background-color: #f0f0f0;
}
div.post-date {
float: right;
}
div.post-author {
font-weight: bold;
}
div.post-thumbnail {
position: absolute;
}
div.post-content {
margin-left: 48px;
min-height: 48px;
}
div.post-footer {
text-align: right;
}
ul.comments {
list-style-type: none;
padding: 0px;
margin: 16px 0px 0px 0px;
}
ul.comments li.comment {
margin-left: 32px;
padding: 8px;
border-bottom: 1px solid #e0e0e0;
}
ul.comments li.comment:nth-child(1) {
border-top: 1px solid #e0e0e0;
}
ul.comments li.comment:hover {
background-color: #f0f0f0;
}
div.comment-date {
float: right;
}
div.comment-author {
font-weight: bold;
}
div.comment-thumbnail {
position: absolute;
}
div.comment-content {
margin-left: 48px;
min-height: 48px;
}
div.comment-form {
margin: 16px 0px 16px 32px;
}
div.pagination {
width: 100%;
text-align: right;
padding: 0px;
margin: 0px;
}
div.flask-pagedown-preview {
margin: 10px 0px 10px 0px;
border: 1px solid #e0e0e0;
padding: 4px;
}
div.flask-pagedown-preview h1 {
font-size: 140%;
}
div.flask-pagedown-preview h2 {
font-size: 130%;
}
div.flask-pagedown-preview h3 {
font-size: 120%;
}
.post-body h1 {
font-size: 140%;
}
.post-body h2 {
font-size: 130%;
}
.post-body h3 {
font-size: 120%;
}
.table.followers tr {
border-bottom: 1px solid #e0e0e0;
}
================================================
FILE: app/templates/403.html
================================================
{% extends "base.html" %}
{% block title %}Flasky - Forbidden{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Forbidden</h1>
</div>
{% endblock %}
================================================
FILE: app/templates/404.html
================================================
{% extends "base.html" %}
{% block title %}Flasky - Page Not Found{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Not Found</h1>
</div>
{% endblock %}
================================================
FILE: app/templates/500.html
================================================
{% extends "base.html" %}
{% block title %}Flasky - Internal Server Error{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Internal Server Error</h1>
</div>
{% endblock %}
================================================
FILE: app/templates/_comments.html
================================================
<ul class="comments">
{% for comment in comments %}
<li class="comment">
<div class="comment-thumbnail">
<a href="{{ url_for('.user', username=comment.author.username) }}">
<img class="img-rounded profile-thumbnail" src="{{ comment.author.gravatar(size=40) }}">
</a>
</div>
<div class="comment-content">
<div class="comment-date">{{ moment(comment.timestamp).fromNow() }}</div>
<div class="comment-author"><a href="{{ url_for('.user', username=comment.author.username) }}">{{ comment.author.username }}</a></div>
<div class="comment-body">
{% if comment.disabled %}
<p><i>This comment has been disabled by a moderator.</i></p>
{% endif %}
{% if moderate or not comment.disabled %}
{% if comment.body_html %}
{{ comment.body_html | safe }}
{% else %}
{{ comment.body }}
{% endif %}
{% endif %}
</div>
{% if moderate %}
<br>
{% if comment.disabled %}
<a class="btn btn-default btn-xs" href="{{ url_for('.moderate_enable', id=comment.id, page=page) }}">Enable</a>
{% else %}
<a class="btn btn-danger btn-xs" href="{{ url_for('.moderate_disable', id=comment.id, page=page) }}">Disable</a>
{% endif %}
{% endif %}
</div>
</li>
{% endfor %}
</ul>
================================================
FILE: app/templates/_macros.html
================================================
{% macro pagination_widget(pagination, endpoint, fragment='') %}
<ul class="pagination">
<li{% if not pagination.has_prev %} class="disabled"{% endif %}>
<a href="{% if pagination.has_prev %}{{ url_for(endpoint, page=pagination.prev_num, **kwargs) }}{{ fragment }}{% else %}#{% endif %}">
«
</a>
</li>
{% for p in pagination.iter_pages() %}
{% if p %}
{% if p == pagination.page %}
<li class="active">
<a href="{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}">{{ p }}</a>
</li>
{% else %}
<li>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}">{{ p }}</a>
</li>
{% endif %}
{% else %}
<li class="disabled"><a href="#">…</a></li>
{% endif %}
{% endfor %}
<li{% if not pagination.has_next %} class="disabled"{% endif %}>
<a href="{% if pagination.has_next %}{{ url_for(endpoint, page=pagination.next_num, **kwargs) }}{{ fragment }}{% else %}#{% endif %}">
»
</a>
</li>
</ul>
{% endmacro %}
================================================
FILE: app/templates/_posts.html
================================================
<ul class="posts">
{% for post in posts %}
<li class="post">
<div class="post-thumbnail">
<a href="{{ url_for('.user', username=post.author.username) }}">
<img class="img-rounded profile-thumbnail" src="{{ post.author.gravatar(size=40) }}">
</a>
</div>
<div class="post-content">
<div class="post-date">{{ moment(post.timestamp).fromNow() }}</div>
<div class="post-author"><a href="{{ url_for('.user', username=post.author.username) }}">{{ post.author.username }}</a></div>
<div class="post-body">
{% if post.body_html %}
{{ post.body_html | safe }}
{% else %}
{{ post.body }}
{% endif %}
</div>
<div class="post-footer">
{% if current_user == post.author %}
<a href="{{ url_for('.edit', id=post.id) }}">
<span class="label label-primary">Edit</span>
</a>
{% elif current_user.is_administrator() %}
<a href="{{ url_for('.edit', id=post.id) }}">
<span class="label label-danger">Edit [Admin]</span>
</a>
{% endif %}
<a href="{{ url_for('.post', id=post.id) }}">
<span class="label label-default">Permalink</span>
</a>
<a href="{{ url_for('.post', id=post.id) }}#comments">
<span class="label label-primary">{{ post.comments.count() }} Comments</span>
</a>
</div>
</div>
</li>
{% endfor %}
</ul>
================================================
FILE: app/templates/auth/change_email.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Change Email Address{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change Your Email Address</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
================================================
FILE: app/templates/auth/change_password.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Change Password{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Change Your Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
================================================
FILE: app/templates/auth/email/change_email.html
================================================
<p>Dear {{ user.username }},</p>
<p>To confirm your new email address <a href="{{ url_for('auth.change_email', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.change_email', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
================================================
FILE: app/templates/auth/email/change_email.txt
================================================
Dear {{ user.username }},
To confirm your new email address click on the following link:
{{ url_for('auth.change_email', token=token, _external=True) }}
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.
================================================
FILE: app/templates/auth/email/confirm.html
================================================
<p>Dear {{ user.username }},</p>
<p>Welcome to <b>Flasky</b>!</p>
<p>To confirm your account please <a href="{{ url_for('auth.confirm', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.confirm', token=token, _external=True) }}</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
================================================
FILE: app/templates/auth/email/confirm.txt
================================================
Dear {{ user.username }},
Welcome to Flasky!
To confirm your account please click on the following link:
{{ url_for('auth.confirm', token=token, _external=True) }}
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.
================================================
FILE: app/templates/auth/email/reset_password.html
================================================
<p>Dear {{ user.username }},</p>
<p>To reset your password <a href="{{ url_for('auth.password_reset', token=token, _external=True) }}">click here</a>.</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('auth.password_reset', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Flasky Team</p>
<p><small>Note: replies to this email address are not monitored.</small></p>
================================================
FILE: app/templates/auth/email/reset_password.txt
================================================
Dear {{ user.username }},
To reset your password click on the following link:
{{ url_for('auth.password_reset', token=token, _external=True) }}
If you have not requested a password reset simply ignore this message.
Sincerely,
The Flasky Team
Note: replies to this email address are not monitored.
================================================
FILE: app/templates/auth/login.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Login{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Login</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
<br>
<p>Forgot your password? <a href="{{ url_for('auth.password_reset_request') }}">Click here to reset it</a>.</p>
<p>New user? <a href="{{ url_for('auth.register') }}">Click here to register</a>.</p>
</div>
{% endblock %}
================================================
FILE: app/templates/auth/register.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Register{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Register</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
================================================
FILE: app/templates/auth/reset_password.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Password Reset{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Reset Your Password</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
================================================
FILE: app/templates/auth/unconfirmed.html
================================================
{% extends "base.html" %}
{% block title %}Flasky - Confirm your account{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>
Hello, {{ current_user.username }}!
</h1>
<h3>You have not confirmed your account yet.</h3>
<p>
Before you can access this site you need to confirm your account.
Check your inbox, you should have received an email with a confirmation link.
</p>
<p>
Need another confirmation email?
<a href="{{ url_for('auth.resend_confirmation') }}">Click here</a>
</p>
</div>
{% endblock %}
================================================
FILE: app/templates/base.html
================================================
{% extends "bootstrap/base.html" %}
{% block title %}Flasky{% endblock %}
{% block head %}
{{ super() }}
<link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}" type="image/x-icon">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
{% endblock %}
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{ url_for('main.index') }}">Flasky</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a href="{{ url_for('main.index') }}">Home</a></li>
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('main.user', username=current_user.username) }}">Profile</a></li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
{% if current_user.can(Permission.MODERATE) %}
<li><a href="{{ url_for('main.moderate') }}">Moderate Comments</a></li>
{% endif %}
{% if current_user.is_authenticated %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<img src="{{ current_user.gravatar(size=18) }}">
Account <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li><a href="{{ url_for('auth.change_password') }}">Change Password</a></li>
<li><a href="{{ url_for('auth.change_email_request') }}">Change Email</a></li>
<li><a href="{{ url_for('auth.logout') }}">Log Out</a></li>
</ul>
</li>
{% else %}
<li><a href="{{ url_for('auth.login') }}">Log In</a></li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endblock %}
{% block content %}
<div class="container">
{% for message in get_flashed_messages() %}
<div class="alert alert-warning">
<button type="button" class="close" data-dismiss="alert">×</button>
{{ message }}
</div>
{% endfor %}
{% block page_content %}{% endblock %}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
================================================
FILE: app/templates/edit_post.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Edit Post{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Edit Post</h1>
</div>
<div>
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}
================================================
FILE: app/templates/edit_profile.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Edit Profile{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Edit Your Profile</h1>
</div>
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
{% endblock %}
================================================
FILE: app/templates/followers.html
================================================
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - {{ title }} {{ user.username }}{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>{{ title }} {{ user.username }}</h1>
</div>
<table class="table table-hover followers">
<thead><tr><th>User</th><th>Since</th></tr></thead>
{% for follow in follows %}
{% if follow.user != user %}
<tr>
<td>
<a href="{{ url_for('.user', username = follow.user.username) }}">
<img class="img-rounded" src="{{ follow.user.gravatar(size=32) }}">
{{ follow.user.username }}
</a>
</td>
<td>{{ moment(follow.timestamp).format('L') }}</td>
</tr>
{% endif %}
{% endfor %}
</table>
<div class="pagination">
{{ macros.pagination_widget(pagination, endpoint, username = user.username) }}
</div>
{% endblock %}
================================================
FILE: app/templates/index.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
{% block title %}Flasky{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Hello, {% if current_user.is_authenticated %}{{ current_user.username }}{% else %}Stranger{% endif %}!</h1>
</div>
<div>
{% if current_user.can(Permission.WRITE) %}
{{ wtf.quick_form(form) }}
{% endif %}
</div>
<div class="post-tabs">
<ul class="nav nav-tabs">
<li{% if not show_followed %} class="active"{% endif %}><a href="{{ url_for('.show_all') }}">All</a></li>
{% if current_user.is_authenticated %}
<li{% if show_followed %} class="active"{% endif %}><a href="{{ url_for('.show_followed') }}">Followed</a></li>
{% endif %}
</ul>
{% include '_posts.html' %}
</div>
{% if pagination %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.index') }}
</div>
{% endif %}
{% endblock %}
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}
================================================
FILE: app/templates/mail/new_user.html
================================================
User <b>{{ user.username }}</b> has joined.
================================================
FILE: app/templates/mail/new_user.txt
================================================
User {{ user.username }} has joined.
================================================
FILE: app/templates/moderate.html
================================================
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - Comment Moderation{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Comment Moderation</h1>
</div>
{% set moderate = True %}
{% include '_comments.html' %}
{% if pagination %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.moderate') }}
</div>
{% endif %}
{% endblock %}
================================================
FILE: app/templates/post.html
================================================
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - Post{% endblock %}
{% block page_content %}
{% include '_posts.html' %}
<h4 id="comments">Comments</h4>
{% if current_user.can(Permission.COMMENT) %}
<div class="comment-form">
{{ wtf.quick_form(form) }}
</div>
{% endif %}
{% include '_comments.html' %}
{% if pagination %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.post', fragment='#comments', id=posts[0].id) }}
</div>
{% endif %}
{% endblock %}
================================================
FILE: app/templates/user.html
================================================
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - {{ user.username }}{% endblock %}
{% block page_content %}
<div class="page-header">
<img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=256) }}">
<div class="profile-header">
<h1>{{ user.username }}</h1>
{% if user.name or user.location %}
<p>
{% if user.name %}{{ user.name }}<br>{% endif %}
{% if user.location %}
from <a href="http://maps.google.com/?q={{ user.location }}">{{ user.location }}</a><br>
{% endif %}
</p>
{% endif %}
{% if current_user.is_administrator() %}
<p><a href="mailto:{{ user.email }}">{{ user.email }}</a></p>
{% endif %}
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
<p>Member since {{ moment(user.member_since).format('L') }}. Last seen {{ moment(user.last_seen).fromNow() }}.</p>
<p>{{ user.posts.count() }} blog posts. {{ user.comments.count() }} comments.</p>
<p>
{% if current_user.can(Permission.FOLLOW) and user != current_user %}
{% if not current_user.is_following(user) %}
<a href="{{ url_for('.follow', username=user.username) }}" class="btn btn-primary">Follow</a>
{% else %}
<a href="{{ url_for('.unfollow', username=user.username) }}" class="btn btn-default">Unfollow</a>
{% endif %}
{% endif %}
<a href="{{ url_for('.followers', username=user.username) }}">Followers: <span class="badge">{{ user.followers.count() - 1 }}</span></a>
<a href="{{ url_for('.followed_by', username=user.username) }}">Following: <span class="badge">{{ user.followed.count() - 1 }}</span></a>
{% if current_user.is_authenticated and user != current_user and user.is_following(current_user) %}
| <span class="label label-default">Follows you</span>
{% endif %}
</p>
<p>
{% if user == current_user %}
<a class="btn btn-default" href="{{ url_for('.edit_profile') }}">Edit Profile</a>
{% endif %}
{% if current_user.is_administrator() %}
<a class="btn btn-danger" href="{{ url_for('.edit_profile_admin', id=user.id) }}">Edit Profile [Admin]</a>
{% endif %}
</p>
</div>
</div>
<h3>Posts by {{ user.username }}</h3>
{% include '_posts.html' %}
{% if pagination %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.user', username=user.username) }}
</div>
{% endif %}
{% endblock %}
================================================
FILE: boot.sh
================================================
#!/bin/sh
source venv/bin/activate
while true; do
flask deploy
if [[ "$?" == "0" ]]; then
break
fi
echo Deploy command failed, retrying in 5 secs...
sleep 5
done
exec gunicorn -b :5000 --access-logfile - --error-logfile - flasky:app
================================================
FILE: config.py
================================================
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in \
['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>'
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
SSL_REDIRECT = False
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_RECORD_QUERIES = True
FLASKY_POSTS_PER_PAGE = 20
FLASKY_FOLLOWERS_PER_PAGE = 50
FLASKY_COMMENTS_PER_PAGE = 30
FLASKY_SLOW_DB_QUERY_TIME = 0.5
@staticmethod
def init_app(app):
pass
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite://'
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
SERVER_NAME = os.environ['SERVER_NAME'] # configure the domain name in use
@classmethod
def init_app(cls, app):
Config.init_app(app)
# email errors to the administrators
import logging
from logging.handlers import SMTPHandler
credentials = None
secure = None
if getattr(cls, 'MAIL_USERNAME', None) is not None:
credentials = (cls.MAIL_USERNAME, cls.MAIL_PASSWORD)
if getattr(cls, 'MAIL_USE_TLS', None):
secure = ()
mail_handler = SMTPHandler(
mailhost=(cls.MAIL_SERVER, cls.MAIL_PORT),
fromaddr=cls.FLASKY_MAIL_SENDER,
toaddrs=[cls.FLASKY_ADMIN],
subject=cls.FLASKY_MAIL_SUBJECT_PREFIX + ' Application Error',
credentials=credentials,
secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
class HerokuConfig(ProductionConfig):
SSL_REDIRECT = True if os.environ.get('DYNO') else False
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)
# handle reverse proxy server headers
try:
from werkzeug.middleware.proxy_fix import ProxyFix
except ImportError:
from werkzeug.contrib.fixers import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app)
# log to stderr
import logging
from logging import StreamHandler
file_handler = StreamHandler()
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
class DockerConfig(ProductionConfig):
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)
# log to stderr
import logging
from logging import StreamHandler
file_handler = StreamHandler()
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
class UnixConfig(ProductionConfig):
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)
# log to syslog
import logging
from logging.handlers import SysLogHandler
syslog_handler = SysLogHandler()
syslog_handler.setLevel(logging.INFO)
app.logger.addHandler(syslog_handler)
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'heroku': HerokuConfig,
'docker': DockerConfig,
'unix': UnixConfig,
'default': DevelopmentConfig
}
================================================
FILE: docker-compose.yml
================================================
version: '3'
services:
flasky:
build: .
ports:
- "8000:5000"
env_file: .env
restart: always
links:
- mysql:dbserver
mysql:
image: "mysql/mysql-server:5.7"
env_file: .env-mysql
restart: always
================================================
FILE: flasky.py
================================================
import os
from dotenv import load_dotenv
dotenv_path = os.path.join(os.path.dirname(__file__), '.env')
if os.path.exists(dotenv_path):
load_dotenv(dotenv_path)
COV = None
if os.environ.get('FLASK_COVERAGE'):
import coverage
COV = coverage.coverage(branch=True, include='app/*')
COV.start()
import sys
import click
from flask_migrate import Migrate, upgrade
from app import create_app, db
from app.models import User, Follow, Role, Permission, Post, Comment
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post, Comment=Comment)
@app.cli.command()
@click.option('--coverage/--no-coverage', default=False,
help='Run tests under code coverage.')
@click.argument('test_names', nargs=-1)
def test(coverage, test_names):
"""Run the unit tests."""
if coverage and not os.environ.get('FLASK_COVERAGE'):
import subprocess
os.environ['FLASK_COVERAGE'] = '1'
sys.exit(subprocess.call(sys.argv))
import unittest
if test_names:
tests = unittest.TestLoader().loadTestsFromNames(test_names)
else:
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)
if COV:
COV.stop()
COV.save()
print('Coverage Summary:')
COV.report()
basedir = os.path.abspath(os.path.dirname(__file__))
covdir = os.path.join(basedir, 'tmp/coverage')
COV.html_report(directory=covdir)
print('HTML version: file://%s/index.html' % covdir)
COV.erase()
@app.cli.command()
@click.option('--length', default=25,
help='Number of functions to include in the profiler report.')
@click.option('--profile-dir', default=None,
help='Directory where profiler data files are saved.')
def profile(length, profile_dir):
"""Start the application under the code profiler."""
from werkzeug.contrib.profiler import ProfilerMiddleware
app.wsgi_app = ProfilerMiddleware(app.wsgi_app, restrictions=[length],
profile_dir=profile_dir)
app.run()
@app.cli.command()
def deploy():
"""Run deployment tasks."""
# migrate database to latest revision
upgrade()
# create or update user roles
Role.insert_roles()
# ensure all users are following themselves
User.add_self_follows()
================================================
FILE: migrations/README
================================================
Generic single-database configuration.
================================================
FILE: migrations/alembic.ini
================================================
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
================================================
FILE: migrations/env.py
================================================
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url', current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
engine = engine_from_config(
config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(
connection=connection,
target_metadata=target_metadata
)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
================================================
FILE: migrations/script.py.mako
================================================
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
================================================
FILE: migrations/versions/190163627111_account_confirmation.py
================================================
"""account confirmation
Revision ID: 190163627111
Revises: 456a945560f6
Create Date: 2013-12-29 02:58:45.577428
"""
# revision identifiers, used by Alembic.
revision = '190163627111'
down_revision = '456a945560f6'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('confirmed', sa.Boolean(), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'confirmed')
### end Alembic commands ###
================================================
FILE: migrations/versions/198b0eebcf9_caching_of_avatar_hashes.py
================================================
"""caching of avatar hashes
Revision ID: 198b0eebcf9
Revises: d66f086b258
Create Date: 2014-02-04 09:10:02.245503
"""
# revision identifiers, used by Alembic.
revision = '198b0eebcf9'
down_revision = 'd66f086b258'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('avatar_hash', sa.String(length=32), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'avatar_hash')
### end Alembic commands ###
================================================
FILE: migrations/versions/1b966e7f4b9e_post_model.py
================================================
"""post model
Revision ID: 1b966e7f4b9e
Revises: 198b0eebcf9
Create Date: 2013-12-31 00:00:14.700591
"""
# revision identifiers, used by Alembic.
revision = '1b966e7f4b9e'
down_revision = '198b0eebcf9'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('posts',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('timestamp', sa.DateTime(), nullable=True),
sa.Column('author_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_posts_timestamp', 'posts', ['timestamp'], unique=False)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_posts_timestamp', 'posts')
op.drop_table('posts')
### end Alembic commands ###
================================================
FILE: migrations/versions/2356a38169ea_followers.py
================================================
"""followers
Revision ID: 2356a38169ea
Revises: 288cd3dc5a8
Create Date: 2013-12-31 16:10:34.500006
"""
# revision identifiers, used by Alembic.
revision = '2356a38169ea'
down_revision = '288cd3dc5a8'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('follows',
sa.Column('follower_id', sa.Integer(), nullable=False),
sa.Column('followed_id', sa.Integer(), nullable=False),
sa.Column('timestamp', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['followed_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['follower_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('follower_id', 'followed_id')
)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_table('follows')
### end Alembic commands ###
================================================
FILE: migrations/versions/288cd3dc5a8_rich_text_posts.py
================================================
"""rich text posts
Revision ID: 288cd3dc5a8
Revises: 1b966e7f4b9e
Create Date: 2013-12-31 03:25:13.286503
"""
# revision identifiers, used by Alembic.
revision = '288cd3dc5a8'
down_revision = '1b966e7f4b9e'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('posts', sa.Column('body_html', sa.Text(), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('posts', 'body_html')
### end Alembic commands ###
================================================
FILE: migrations/versions/38c4e85512a9_initial_migration.py
================================================
"""initial migration
Revision ID: 38c4e85512a9
Revises: None
Create Date: 2013-12-27 01:23:59.392801
"""
# revision identifiers, used by Alembic.
revision = '38c4e85512a9'
down_revision = None
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=64), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=64), nullable=True),
sa.Column('role_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_users_username', 'users', ['username'], unique=True)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_users_username', 'users')
op.drop_table('users')
op.drop_table('roles')
### end Alembic commands ###
================================================
FILE: migrations/versions/456a945560f6_login_support.py
================================================
"""login support
Revision ID: 456a945560f6
Revises: 38c4e85512a9
Create Date: 2013-12-29 00:18:35.795259
"""
# revision identifiers, used by Alembic.
revision = '456a945560f6'
down_revision = '38c4e85512a9'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('email', sa.String(length=64), nullable=True))
op.add_column('users', sa.Column('password_hash', sa.String(length=128), nullable=True))
op.create_index('ix_users_email', 'users', ['email'], unique=True)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_users_email', 'users')
op.drop_column('users', 'password_hash')
op.drop_column('users', 'email')
### end Alembic commands ###
================================================
FILE: migrations/versions/51f5ccfba190_comments.py
================================================
"""comments
Revision ID: 51f5ccfba190
Revises: 2356a38169ea
Create Date: 2014-01-01 12:08:43.287523
"""
# revision identifiers, used by Alembic.
revision = '51f5ccfba190'
down_revision = '2356a38169ea'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.create_table('comments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('body_html', sa.Text(), nullable=True),
sa.Column('timestamp', sa.DateTime(), nullable=True),
sa.Column('disabled', sa.Boolean(), nullable=True),
sa.Column('author_id', sa.Integer(), nullable=True),
sa.Column('post_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['author_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_comments_timestamp', 'comments', ['timestamp'], unique=False)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_comments_timestamp', 'comments')
op.drop_table('comments')
### end Alembic commands ###
================================================
FILE: migrations/versions/56ed7d33de8d_user_roles.py
================================================
"""user roles
Revision ID: 56ed7d33de8d
Revises: 190163627111
Create Date: 2013-12-29 22:19:54.212604
"""
# revision identifiers, used by Alembic.
revision = '56ed7d33de8d'
down_revision = '190163627111'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('roles', sa.Column('default', sa.Boolean(), nullable=True))
op.add_column('roles', sa.Column('permissions', sa.Integer(), nullable=True))
op.create_index('ix_roles_default', 'roles', ['default'], unique=False)
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_roles_default', 'roles')
op.drop_column('roles', 'permissions')
op.drop_column('roles', 'default')
### end Alembic commands ###
================================================
FILE: migrations/versions/d66f086b258_user_information.py
================================================
"""user information
Revision ID: d66f086b258
Revises: 56ed7d33de8d
Create Date: 2013-12-29 23:50:49.566954
"""
# revision identifiers, used by Alembic.
revision = 'd66f086b258'
down_revision = '56ed7d33de8d'
from alembic import op
import sqlalchemy as sa
def upgrade():
### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('about_me', sa.Text(), nullable=True))
op.add_column('users', sa.Column('last_seen', sa.DateTime(), nullable=True))
op.add_column('users', sa.Column('location', sa.String(length=64), nullable=True))
op.add_column('users', sa.Column('member_since', sa.DateTime(), nullable=True))
op.add_column('users', sa.Column('name', sa.String(length=64), nullable=True))
### end Alembic commands ###
def downgrade():
### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'name')
op.drop_column('users', 'member_since')
op.drop_column('users', 'location')
op.drop_column('users', 'last_seen')
op.drop_column('users', 'about_me')
### end Alembic commands ###
================================================
FILE: requirements/common.txt
================================================
alembic==0.9.3
bleach==2.0.0
blinker==1.4
click==6.7
dominate==2.3.1
Flask==0.12.2
Flask-Bootstrap==3.3.7.1
Flask-HTTPAuth==3.2.3
Flask-Login==0.4.0
Flask-Mail==0.9.1
Flask-Migrate==2.0.4
Flask-Moment==0.5.1
Flask-PageDown==0.2.2
Flask-SQLAlchemy==2.2
Flask-WTF==0.14.2
html5lib==0.999999999
itsdangerous==0.24
Jinja2==2.9.6
Mako==1.0.7
Markdown==2.6.8
MarkupSafe==1.1.1
python-dateutil==2.6.1
python-dotenv==0.6.5
python-editor==1.0.3
six==1.10.0
SQLAlchemy==1.1.11
visitor==0.1.3
webencodings==0.5.1
Werkzeug==0.12.2
WTForms==2.1
================================================
FILE: requirements/dev.txt
================================================
-r common.txt
certifi==2017.7.27.1
chardet==3.0.4
coverage==4.4.1
faker==0.7.18
httpie==0.9.9
idna==2.5
Pygments==2.2.0
requests==2.18.2
selenium==3.141.0
urllib3==1.22
================================================
FILE: requirements/docker.txt
================================================
-r common.txt
gunicorn==19.7.1
pymysql==0.7.11
================================================
FILE: requirements/heroku.txt
================================================
-r prod.txt
Flask-SSLify==0.1.5
gunicorn==19.7.1
psycopg2==2.7.3
================================================
FILE: requirements/prod.txt
================================================
-r common.txt
================================================
FILE: requirements.txt
================================================
# this requirements file is used by Heroku
# requirements for other configurations are located in the requirements subdirectory
-r requirements/heroku.txt
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/test_api.py
================================================
import unittest
import json
import re
from base64 import b64encode
from app import create_app, db
from app.models import User, Role, Post, Comment
class APITestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
Role.insert_roles()
self.client = self.app.test_client()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def get_api_headers(self, username, password):
return {
'Authorization': 'Basic ' + b64encode(
(username + ':' + password).encode('utf-8')).decode('utf-8'),
'Accept': 'application/json',
'Content-Type': 'application/json'
}
def test_404(self):
response = self.client.get(
'/wrong/url',
headers=self.get_api_headers('email', 'password'))
self.assertEqual(response.status_code, 404)
json_response = json.loads(response.get_data(as_text=True))
self.assertEqual(json_response['error'], 'not found')
def test_no_auth(self):
response = self.client.get('/api/v1/posts/',
content_type='application/json')
self.assertEqual(response.status_code, 401)
def test_bad_auth(self):
# add a user
r = Role.query.filter_by(name='User').first()
self.assertIsNotNone(r)
u = User(email='john@example.com', password='cat', confirmed=True,
role=r)
db.session.add(u)
db.session.commit()
# authenticate with bad password
response = self.client.get(
'/api/v1/posts/',
headers=self.get_api_headers('john@example.com', 'dog'))
self.assertEqual(response.status_code, 401)
def test_token_auth(self):
# add a user
r = Role.query.filter_by(name='User').first()
self.assertIsNotNone(r)
u = User(email='john@example.com', password='cat', confirmed=True,
role=r)
db.session.add(u)
db.session.commit()
# issue a request with a bad token
response = self.client.get(
'/api/v1/posts/',
headers=self.get_api_headers('bad-token', ''))
self.assertEqual(response.status_code, 401)
# get a token
response = self.client.post(
'/api/v1/tokens/',
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertIsNotNone(json_response.get('token'))
token = json_response['token']
# issue a request with the token
response = self.client.get(
'/api/v1/posts/',
headers=self.get_api_headers(token, ''))
self.assertEqual(response.status_code, 200)
def test_anonymous(self):
response = self.client.get(
'/api/v1/posts/',
headers=self.get_api_headers('', ''))
self.assertEqual(response.status_code, 401)
def test_unconfirmed_account(self):
# add an unconfirmed user
r = Role.query.filter_by(name='User').first()
self.assertIsNotNone(r)
u = User(email='john@example.com', password='cat', confirmed=False,
role=r)
db.session.add(u)
db.session.commit()
# get list of posts with the unconfirmed account
response = self.client.get(
'/api/v1/posts/',
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertEqual(response.status_code, 403)
def test_posts(self):
# add a user
r = Role.query.filter_by(name='User').first()
self.assertIsNotNone(r)
u = User(email='john@example.com', password='cat', confirmed=True,
role=r)
db.session.add(u)
db.session.commit()
# write an empty post
response = self.client.post(
'/api/v1/posts/',
headers=self.get_api_headers('john@example.com', 'cat'),
data=json.dumps({'body': ''}))
self.assertEqual(response.status_code, 400)
# write a post
response = self.client.post(
'/api/v1/posts/',
headers=self.get_api_headers('john@example.com', 'cat'),
data=json.dumps({'body': 'body of the *blog* post'}))
self.assertEqual(response.status_code, 201)
url = response.headers.get('Location')
self.assertIsNotNone(url)
# get the new post
response = self.client.get(
url,
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertEqual('http://localhost' + json_response['url'], url)
self.assertEqual(json_response['body'], 'body of the *blog* post')
self.assertEqual(json_response['body_html'],
'<p>body of the <em>blog</em> post</p>')
json_post = json_response
# get the post from the user
response = self.client.get(
'/api/v1/users/{}/posts/'.format(u.id),
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertIsNotNone(json_response.get('posts'))
self.assertEqual(json_response.get('count', 0), 1)
self.assertEqual(json_response['posts'][0], json_post)
# get the post from the user as a follower
response = self.client.get(
'/api/v1/users/{}/timeline/'.format(u.id),
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertIsNotNone(json_response.get('posts'))
self.assertEqual(json_response.get('count', 0), 1)
self.assertEqual(json_response['posts'][0], json_post)
# edit post
response = self.client.put(
url,
headers=self.get_api_headers('john@example.com', 'cat'),
data=json.dumps({'body': 'updated body'}))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertEqual('http://localhost' + json_response['url'], url)
self.assertEqual(json_response['body'], 'updated body')
self.assertEqual(json_response['body_html'], '<p>updated body</p>')
def test_users(self):
# add two users
r = Role.query.filter_by(name='User').first()
self.assertIsNotNone(r)
u1 = User(email='john@example.com', username='john',
password='cat', confirmed=True, role=r)
u2 = User(email='susan@example.com', username='susan',
password='dog', confirmed=True, role=r)
db.session.add_all([u1, u2])
db.session.commit()
# get users
response = self.client.get(
'/api/v1/users/{}'.format(u1.id),
headers=self.get_api_headers('susan@example.com', 'dog'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertEqual(json_response['username'], 'john')
response = self.client.get(
'/api/v1/users/{}'.format(u2.id),
headers=self.get_api_headers('susan@example.com', 'dog'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertEqual(json_response['username'], 'susan')
def test_comments(self):
# add two users
r = Role.query.filter_by(name='User').first()
self.assertIsNotNone(r)
u1 = User(email='john@example.com', username='john',
password='cat', confirmed=True, role=r)
u2 = User(email='susan@example.com', username='susan',
password='dog', confirmed=True, role=r)
db.session.add_all([u1, u2])
db.session.commit()
# add a post
post = Post(body='body of the post', author=u1)
db.session.add(post)
db.session.commit()
# write a comment
response = self.client.post(
'/api/v1/posts/{}/comments/'.format(post.id),
headers=self.get_api_headers('susan@example.com', 'dog'),
data=json.dumps({'body': 'Good [post](http://example.com)!'}))
self.assertEqual(response.status_code, 201)
json_response = json.loads(response.get_data(as_text=True))
url = response.headers.get('Location')
self.assertIsNotNone(url)
self.assertEqual(json_response['body'],
'Good [post](http://example.com)!')
self.assertEqual(
re.sub('<.*?>', '', json_response['body_html']), 'Good post!')
# get the new comment
response = self.client.get(
url,
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertEqual('http://localhost' + json_response['url'], url)
self.assertEqual(json_response['body'],
'Good [post](http://example.com)!')
# add another comment
comment = Comment(body='Thank you!', author=u1, post=post)
db.session.add(comment)
db.session.commit()
# get the two comments from the post
response = self.client.get(
'/api/v1/posts/{}/comments/'.format(post.id),
headers=self.get_api_headers('susan@example.com', 'dog'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertIsNotNone(json_response.get('comments'))
self.assertEqual(json_response.get('count', 0), 2)
# get all the comments
response = self.client.get(
'/api/v1/posts/{}/comments/'.format(post.id),
headers=self.get_api_headers('susan@example.com', 'dog'))
self.assertEqual(response.status_code, 200)
json_response = json.loads(response.get_data(as_text=True))
self.assertIsNotNone(json_response.get('comments'))
self.assertEqual(json_response.get('count', 0), 2)
================================================
FILE: tests/test_basics.py
================================================
import unittest
from flask import current_app
from app import create_app, db
class BasicsTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_app_exists(self):
self.assertFalse(current_app is None)
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
================================================
FILE: tests/test_client.py
================================================
import re
import unittest
from app import create_app, db
from app.models import User, Role
class FlaskClientTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
Role.insert_roles()
self.client = self.app.test_client(use_cookies=True)
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_home_page(self):
response = self.client.get('/')
self.assertEqual(response.status_code, 200)
self.assertTrue('Stranger' in response.get_data(as_text=True))
def test_register_and_login(self):
# register a new account
response = self.client.post('/auth/register', data={
'email': 'john@example.com',
'username': 'john',
'password': 'cat',
'password2': 'cat'
})
self.assertEqual(response.status_code, 302)
# login with the new account
response = self.client.post('/auth/login', data={
'email': 'john@example.com',
'password': 'cat'
}, follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertTrue(re.search('Hello,\s+john!',
response.get_data(as_text=True)))
self.assertTrue(
'You have not confirmed your account yet' in response.get_data(
as_text=True))
# send a confirmation token
user = User.query.filter_by(email='john@example.com').first()
token = user.generate_confirmation_token()
response = self.client.get('/auth/confirm/{}'.format(token),
follow_redirects=True)
user.confirm(token)
self.assertEqual(response.status_code, 200)
self.assertTrue(
'You have confirmed your account' in response.get_data(
as_text=True))
# log out
response = self.client.get('/auth/logout', follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertTrue('You have been logged out' in response.get_data(
as_text=True))
================================================
FILE: tests/test_selenium.py
================================================
import re
import threading
import time
import unittest
from selenium import webdriver
from app import create_app, db, fake
from app.models import Role, User, Post
class SeleniumTestCase(unittest.TestCase):
client = None
@classmethod
def setUpClass(cls):
# start Chrome
options = webdriver.ChromeOptions()
options.add_argument('headless')
try:
cls.client = webdriver.Chrome(chrome_options=options)
except:
pass
# skip these tests if the browser could not be started
if cls.client:
# create the application
cls.app = create_app('testing')
cls.app_context = cls.app.app_context()
cls.app_context.push()
# suppress logging to keep unittest output clean
import logging
logger = logging.getLogger('werkzeug')
logger.setLevel("ERROR")
# create the database and populate with some fake data
db.create_all()
Role.insert_roles()
fake.users(10)
fake.posts(10)
# add an administrator user
admin_role = Role.query.filter_by(name='Administrator').first()
admin = User(email='john@example.com',
username='john', password='cat',
role=admin_role, confirmed=True)
db.session.add(admin)
db.session.commit()
# start the Flask server in a thread
cls.server_thread = threading.Thread(target=cls.app.run,
kwargs={'debug': False})
cls.server_thread.start()
# give the server a second to ensure it is up
time.sleep(1)
@classmethod
def tearDownClass(cls):
if cls.client:
# stop the flask server and the browser
cls.client.get('http://localhost:5000/shutdown')
cls.client.quit()
cls.server_thread.join()
# destroy database
db.drop_all()
db.session.remove()
# remove application context
cls.app_context.pop()
def setUp(self):
if not self.client:
self.skipTest('Web browser not available')
def tearDown(self):
pass
def test_admin_home_page(self):
# navigate to home page
self.client.get('http://localhost:5000/')
self.assertTrue(re.search('Hello,\s+Stranger!',
self.client.page_source))
# navigate to login page
self.client.find_element_by_link_text('Log In').click()
self.assertIn('<h1>Login</h1>', self.client.page_source)
# login
self.client.find_element_by_name('email').\
send_keys('john@example.com')
self.client.find_element_by_name('password').send_keys('cat')
self.client.find_element_by_name('submit').click()
self.assertTrue(re.search('Hello,\s+john!', self.client.page_source))
# navigate to the user's profile page
self.client.find_element_by_link_text('Profile').click()
self.assertIn('<h1>john</h1>', self.client.page_source)
================================================
FILE: tests/test_user_model.py
================================================
import unittest
import time
from datetime import datetime
from app import create_app, db
from app.models import User, AnonymousUser, Role, Permission, Follow
class UserModelTestCase(unittest.TestCase):
def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()
Role.insert_roles()
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
def test_password_setter(self):
u = User(password='cat')
self.assertTrue(u.password_hash is not None)
def test_no_password_getter(self):
u = User(password='cat')
with self.assertRaises(AttributeError):
u.password
def test_password_verification(self):
u = User(password='cat')
self.assertTrue(u.verify_password('cat'))
self.assertFalse(u.verify_password('dog'))
def test_password_salts_are_random(self):
u = User(password='cat')
u2 = User(password='cat')
self.assertTrue(u.password_hash != u2.password_hash)
def test_valid_confirmation_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_confirmation_token()
self.assertTrue(u.confirm(token))
def test_invalid_confirmation_token(self):
u1 = User(password='cat')
u2 = User(password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token = u1.generate_confirmation_token()
self.assertFalse(u2.confirm(token))
def test_expired_confirmation_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_confirmation_token(1)
time.sleep(2)
self.assertFalse(u.confirm(token))
def test_valid_reset_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_reset_token()
self.assertTrue(User.reset_password(token, 'dog'))
self.assertTrue(u.verify_password('dog'))
def test_invalid_reset_token(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_reset_token()
self.assertFalse(User.reset_password(token + 'a', 'horse'))
self.assertTrue(u.verify_password('cat'))
def test_valid_email_change_token(self):
u = User(email='john@example.com', password='cat')
db.session.add(u)
db.session.commit()
token = u.generate_email_change_token('susan@example.org')
self.assertTrue(u.change_email(token))
self.assertTrue(u.email == 'susan@example.org')
def test_invalid_email_change_token(self):
u1 = User(email='john@example.com', password='cat')
u2 = User(email='susan@example.org', password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token = u1.generate_email_change_token('david@example.net')
self.assertFalse(u2.change_email(token))
self.assertTrue(u2.email == 'susan@example.org')
def test_duplicate_email_change_token(self):
u1 = User(email='john@example.com', password='cat')
u2 = User(email='susan@example.org', password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token = u2.generate_email_change_token('john@example.com')
self.assertFalse(u2.change_email(token))
self.assertTrue(u2.email == 'susan@example.org')
def test_user_role(self):
u = User(email='john@example.com', password='cat')
self.assertTrue(u.can(Permission.FOLLOW))
self.assertTrue(u.can(Permission.COMMENT))
self.assertTrue(u.can(Permission.WRITE))
self.assertFalse(u.can(Permission.MODERATE))
self.assertFalse(u.can(Permission.ADMIN))
def test_moderator_role(self):
r = Role.query.filter_by(name='Moderator').first()
u = User(email='john@example.com', password='cat', role=r)
self.assertTrue(u.can(Permission.FOLLOW))
self.assertTrue(u.can(Permission.COMMENT))
self.assertTrue(u.can(Permission.WRITE))
self.assertTrue(u.can(Permission.MODERATE))
self.assertFalse(u.can(Permission.ADMIN))
def test_administrator_role(self):
r = Role.query.filter_by(name='Administrator').first()
u = User(email='john@example.com', password='cat', role=r)
self.assertTrue(u.can(Permission.FOLLOW))
self.assertTrue(u.can(Permission.COMMENT))
self.assertTrue(u.can(Permission.WRITE))
self.assertTrue(u.can(Permission.MODERATE))
self.assertTrue(u.can(Permission.ADMIN))
def test_anonymous_user(self):
u = AnonymousUser()
self.assertFalse(u.can(Permission.FOLLOW))
self.assertFalse(u.can(Permission.COMMENT))
self.assertFalse(u.can(Permission.WRITE))
self.assertFalse(u.can(Permission.MODERATE))
self.assertFalse(u.can(Permission.ADMIN))
def test_timestamps(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
self.assertTrue(
(datetime.utcnow() - u.member_since).total_seconds() < 3)
self.assertTrue(
(datetime.utcnow() - u.last_seen).total_seconds() < 3)
def test_ping(self):
u = User(password='cat')
db.session.add(u)
db.session.commit()
time.sleep(2)
last_seen_before = u.last_seen
u.ping()
self.assertTrue(u.last_seen > last_seen_before)
def test_gravatar(self):
u = User(email='john@example.com', password='cat')
with self.app.test_request_context('/'):
gravatar = u.gravatar()
gravatar_256 = u.gravatar(size=256)
gravatar_pg = u.gravatar(rating='pg')
gravatar_retro = u.gravatar(default='retro')
self.assertTrue('https://secure.gravatar.com/avatar/' +
'd4c74594d841139328695756648b6bd6'in gravatar)
self.assertTrue('s=256' in gravatar_256)
self.assertTrue('r=pg' in gravatar_pg)
self.assertTrue('d=retro' in gravatar_retro)
def test_follows(self):
u1 = User(email='john@example.com', password='cat')
u2 = User(email='susan@example.org', password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
self.assertFalse(u1.is_following(u2))
self.assertFalse(u1.is_followed_by(u2))
timestamp_before = datetime.utcnow()
u1.follow(u2)
db.session.add(u1)
db.session.commit()
timestamp_after = datetime.utcnow()
self.assertTrue(u1.is_following(u2))
self.assertFalse(u1.is_followed_by(u2))
self.assertTrue(u2.is_followed_by(u1))
self.assertTrue(u1.followed.count() == 2)
self.assertTrue(u2.followers.count() == 2)
f = u1.followed.all()[-1]
self.assertTrue(f.followed == u2)
self.assertTrue(timestamp_before <= f.timestamp <= timestamp_after)
f = u2.followers.all()[-1]
self.assertTrue(f.follower == u1)
u1.unfollow(u2)
db.session.add(u1)
db.session.commit()
self.assertTrue(u1.followed.count() == 1)
self.assertTrue(u2.followers.count() == 1)
self.assertTrue(Follow.query.count() == 2)
u2.follow(u1)
db.session.add(u1)
db.session.add(u2)
db.session.commit()
db.session.delete(u2)
db.session.commit()
self.assertTrue(Follow.query.count() == 1)
def test_to_json(self):
u = User(email='john@example.com', password='cat')
db.session.add(u)
db.session.commit()
with self.app.test_request_context('/'):
json_user = u.to_json()
expected_keys = ['url', 'username', 'member_since', 'last_seen',
'posts_url', 'followed_posts_url', 'post_count']
self.assertEqual(sorted(json_user.keys()), sorted(expected_keys))
self.assertEqual('/api/v1/users/' + str(u.id), json_user['url'])
gitextract_ml7yrowa/
├── .gitignore
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── app/
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── authentication.py
│ │ ├── comments.py
│ │ ├── decorators.py
│ │ ├── errors.py
│ │ ├── posts.py
│ │ └── users.py
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── forms.py
│ │ └── views.py
│ ├── decorators.py
│ ├── email.py
│ ├── exceptions.py
│ ├── fake.py
│ ├── main/
│ │ ├── __init__.py
│ │ ├── errors.py
│ │ ├── forms.py
│ │ └── views.py
│ ├── models.py
│ ├── static/
│ │ └── styles.css
│ └── templates/
│ ├── 403.html
│ ├── 404.html
│ ├── 500.html
│ ├── _comments.html
│ ├── _macros.html
│ ├── _posts.html
│ ├── auth/
│ │ ├── change_email.html
│ │ ├── change_password.html
│ │ ├── email/
│ │ │ ├── change_email.html
│ │ │ ├── change_email.txt
│ │ │ ├── confirm.html
│ │ │ ├── confirm.txt
│ │ │ ├── reset_password.html
│ │ │ └── reset_password.txt
│ │ ├── login.html
│ │ ├── register.html
│ │ ├── reset_password.html
│ │ └── unconfirmed.html
│ ├── base.html
│ ├── edit_post.html
│ ├── edit_profile.html
│ ├── followers.html
│ ├── index.html
│ ├── mail/
│ │ ├── new_user.html
│ │ └── new_user.txt
│ ├── moderate.html
│ ├── post.html
│ └── user.html
├── boot.sh
├── config.py
├── docker-compose.yml
├── flasky.py
├── migrations/
│ ├── README
│ ├── alembic.ini
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
│ ├── 190163627111_account_confirmation.py
│ ├── 198b0eebcf9_caching_of_avatar_hashes.py
│ ├── 1b966e7f4b9e_post_model.py
│ ├── 2356a38169ea_followers.py
│ ├── 288cd3dc5a8_rich_text_posts.py
│ ├── 38c4e85512a9_initial_migration.py
│ ├── 456a945560f6_login_support.py
│ ├── 51f5ccfba190_comments.py
│ ├── 56ed7d33de8d_user_roles.py
│ └── d66f086b258_user_information.py
├── requirements/
│ ├── common.txt
│ ├── dev.txt
│ ├── docker.txt
│ ├── heroku.txt
│ └── prod.txt
├── requirements.txt
└── tests/
├── __init__.py
├── test_api.py
├── test_basics.py
├── test_client.py
├── test_selenium.py
└── test_user_model.py
SYMBOL INDEX (217 symbols across 36 files)
FILE: app/__init__.py
function create_app (line 20) | def create_app(config_name):
FILE: app/api/authentication.py
function verify_password (line 11) | def verify_password(email_or_token, password):
function auth_error (line 27) | def auth_error():
function before_request (line 33) | def before_request():
function get_token (line 40) | def get_token():
FILE: app/api/comments.py
function get_comments (line 9) | def get_comments():
function get_comment (line 30) | def get_comment(id):
function get_post_comments (line 36) | def get_post_comments(id):
function new_post_comment (line 59) | def new_post_comment(id):
FILE: app/api/decorators.py
function permission_required (line 6) | def permission_required(permission):
FILE: app/api/errors.py
function bad_request (line 6) | def bad_request(message):
function unauthorized (line 12) | def unauthorized(message):
function forbidden (line 18) | def forbidden(message):
function validation_error (line 25) | def validation_error(e):
FILE: app/api/posts.py
function get_posts (line 10) | def get_posts():
function get_post (line 31) | def get_post(id):
function new_post (line 38) | def new_post():
function edit_post (line 49) | def edit_post(id):
FILE: app/api/users.py
function get_user (line 7) | def get_user(id):
function get_user_posts (line 13) | def get_user_posts(id):
function get_user_followed_posts (line 35) | def get_user_followed_posts(id):
FILE: app/auth/forms.py
class LoginForm (line 8) | class LoginForm(FlaskForm):
class RegistrationForm (line 16) | class RegistrationForm(FlaskForm):
method validate_email (line 29) | def validate_email(self, field):
method validate_username (line 33) | def validate_username(self, field):
class ChangePasswordForm (line 38) | class ChangePasswordForm(FlaskForm):
class PasswordResetRequestForm (line 47) | class PasswordResetRequestForm(FlaskForm):
class PasswordResetForm (line 53) | class PasswordResetForm(FlaskForm):
class ChangeEmailForm (line 60) | class ChangeEmailForm(FlaskForm):
method validate_email (line 66) | def validate_email(self, field):
FILE: app/auth/views.py
function before_request (line 13) | def before_request():
function unconfirmed (line 24) | def unconfirmed():
function login (line 31) | def login():
function logout (line 47) | def logout():
function register (line 54) | def register():
function confirm (line 72) | def confirm(token):
function resend_confirmation (line 85) | def resend_confirmation():
function change_password (line 95) | def change_password():
function password_reset_request (line 110) | def password_reset_request():
function password_reset (line 128) | def password_reset(token):
function change_email_request (line 144) | def change_email_request():
function change_email (line 163) | def change_email(token):
FILE: app/decorators.py
function permission_required (line 7) | def permission_required(permission):
function admin_required (line 18) | def admin_required(f):
FILE: app/email.py
function send_async_email (line 7) | def send_async_email(app, msg):
function send_email (line 12) | def send_email(to, subject, template, **kwargs):
FILE: app/exceptions.py
class ValidationError (line 1) | class ValidationError(ValueError):
FILE: app/fake.py
function users (line 8) | def users(count=100):
function posts (line 28) | def posts(count=100):
FILE: app/main/__init__.py
function inject_permissions (line 10) | def inject_permissions():
FILE: app/main/errors.py
function forbidden (line 6) | def forbidden(e):
function page_not_found (line 16) | def page_not_found(e):
function internal_server_error (line 26) | def internal_server_error(e):
FILE: app/main/forms.py
class NameForm (line 10) | class NameForm(FlaskForm):
class EditProfileForm (line 15) | class EditProfileForm(FlaskForm):
class EditProfileAdminForm (line 22) | class EditProfileAdminForm(FlaskForm):
method __init__ (line 37) | def __init__(self, user, *args, **kwargs):
method validate_email (line 43) | def validate_email(self, field):
method validate_username (line 48) | def validate_username(self, field):
class PostForm (line 54) | class PostForm(FlaskForm):
class CommentForm (line 59) | class CommentForm(FlaskForm):
FILE: app/main/views.py
function after_request (line 14) | def after_request(response):
function server_shutdown (line 25) | def server_shutdown():
function index (line 36) | def index():
function user (line 61) | def user(username):
function edit_profile (line 74) | def edit_profile():
function edit_profile_admin (line 93) | def edit_profile_admin(id):
function post (line 119) | def post(id):
function edit (line 144) | def edit(id):
function follow (line 163) | def follow(username):
function unfollow (line 180) | def unfollow(username):
function followers (line 195) | def followers(username):
function followed_by (line 212) | def followed_by(username):
function show_all (line 230) | def show_all():
function show_followed (line 238) | def show_followed():
function moderate (line 247) | def moderate():
function moderate_enable (line 260) | def moderate_enable(id):
function moderate_disable (line 272) | def moderate_disable(id):
FILE: app/models.py
class Permission (line 13) | class Permission:
class Role (line 21) | class Role(db.Model):
method __init__ (line 29) | def __init__(self, **kwargs):
method insert_roles (line 35) | def insert_roles():
method add_permission (line 56) | def add_permission(self, perm):
method remove_permission (line 60) | def remove_permission(self, perm):
method reset_permissions (line 64) | def reset_permissions(self):
method has_permission (line 67) | def has_permission(self, perm):
method __repr__ (line 70) | def __repr__(self):
class Follow (line 74) | class Follow(db.Model):
class User (line 83) | class User(UserMixin, db.Model):
method add_self_follows (line 111) | def add_self_follows():
method __init__ (line 118) | def __init__(self, **kwargs):
method password (line 130) | def password(self):
method password (line 134) | def password(self, password):
method verify_password (line 137) | def verify_password(self, password):
method generate_confirmation_token (line 140) | def generate_confirmation_token(self, expiration=3600):
method confirm (line 144) | def confirm(self, token):
method generate_reset_token (line 156) | def generate_reset_token(self, expiration=3600):
method reset_password (line 161) | def reset_password(token, new_password):
method generate_email_change_token (line 174) | def generate_email_change_token(self, new_email, expiration=3600):
method change_email (line 179) | def change_email(self, token):
method can (line 197) | def can(self, perm):
method is_administrator (line 200) | def is_administrator(self):
method ping (line 203) | def ping(self):
method gravatar_hash (line 207) | def gravatar_hash(self):
method gravatar (line 210) | def gravatar(self, size=100, default='identicon', rating='g'):
method follow (line 216) | def follow(self, user):
method unfollow (line 221) | def unfollow(self, user):
method is_following (line 226) | def is_following(self, user):
method is_followed_by (line 232) | def is_followed_by(self, user):
method followed_posts (line 239) | def followed_posts(self):
method to_json (line 243) | def to_json(self):
method generate_auth_token (line 256) | def generate_auth_token(self, expiration):
method verify_auth_token (line 262) | def verify_auth_token(token):
method __repr__ (line 270) | def __repr__(self):
class AnonymousUser (line 274) | class AnonymousUser(AnonymousUserMixin):
method can (line 275) | def can(self, permissions):
method is_administrator (line 278) | def is_administrator(self):
function load_user (line 285) | def load_user(user_id):
class Post (line 289) | class Post(db.Model):
method on_changed_body (line 299) | def on_changed_body(target, value, oldvalue, initiator):
method to_json (line 307) | def to_json(self):
method from_json (line 320) | def from_json(json_post):
class Comment (line 330) | class Comment(db.Model):
method on_changed_body (line 341) | def on_changed_body(target, value, oldvalue, initiator):
method to_json (line 348) | def to_json(self):
method from_json (line 360) | def from_json(json_comment):
FILE: config.py
class Config (line 5) | class Config:
method init_app (line 25) | def init_app(app):
class DevelopmentConfig (line 29) | class DevelopmentConfig(Config):
class TestingConfig (line 35) | class TestingConfig(Config):
class ProductionConfig (line 42) | class ProductionConfig(Config):
method init_app (line 48) | def init_app(cls, app):
class HerokuConfig (line 71) | class HerokuConfig(ProductionConfig):
method init_app (line 75) | def init_app(cls, app):
class DockerConfig (line 93) | class DockerConfig(ProductionConfig):
method init_app (line 95) | def init_app(cls, app):
class UnixConfig (line 106) | class UnixConfig(ProductionConfig):
method init_app (line 108) | def init_app(cls, app):
FILE: flasky.py
function make_shell_context (line 25) | def make_shell_context():
function test (line 34) | def test(coverage, test_names):
function profile (line 64) | def profile(length, profile_dir):
function deploy (line 73) | def deploy():
FILE: migrations/env.py
function run_migrations_offline (line 27) | def run_migrations_offline():
function run_migrations_online (line 45) | def run_migrations_online():
FILE: migrations/versions/190163627111_account_confirmation.py
function upgrade (line 17) | def upgrade():
function downgrade (line 23) | def downgrade():
FILE: migrations/versions/198b0eebcf9_caching_of_avatar_hashes.py
function upgrade (line 17) | def upgrade():
function downgrade (line 23) | def downgrade():
FILE: migrations/versions/1b966e7f4b9e_post_model.py
function upgrade (line 17) | def upgrade():
function downgrade (line 31) | def downgrade():
FILE: migrations/versions/2356a38169ea_followers.py
function upgrade (line 17) | def upgrade():
function downgrade (line 30) | def downgrade():
FILE: migrations/versions/288cd3dc5a8_rich_text_posts.py
function upgrade (line 17) | def upgrade():
function downgrade (line 23) | def downgrade():
FILE: migrations/versions/38c4e85512a9_initial_migration.py
function upgrade (line 17) | def upgrade():
function downgrade (line 36) | def downgrade():
FILE: migrations/versions/456a945560f6_login_support.py
function upgrade (line 17) | def upgrade():
function downgrade (line 25) | def downgrade():
FILE: migrations/versions/51f5ccfba190_comments.py
function upgrade (line 17) | def upgrade():
function downgrade (line 35) | def downgrade():
FILE: migrations/versions/56ed7d33de8d_user_roles.py
function upgrade (line 17) | def upgrade():
function downgrade (line 25) | def downgrade():
FILE: migrations/versions/d66f086b258_user_information.py
function upgrade (line 17) | def upgrade():
function downgrade (line 27) | def downgrade():
FILE: tests/test_api.py
class APITestCase (line 9) | class APITestCase(unittest.TestCase):
method setUp (line 10) | def setUp(self):
method tearDown (line 18) | def tearDown(self):
method get_api_headers (line 23) | def get_api_headers(self, username, password):
method test_404 (line 31) | def test_404(self):
method test_no_auth (line 39) | def test_no_auth(self):
method test_bad_auth (line 44) | def test_bad_auth(self):
method test_token_auth (line 59) | def test_token_auth(self):
method test_anonymous (line 89) | def test_anonymous(self):
method test_unconfirmed_account (line 95) | def test_unconfirmed_account(self):
method test_posts (line 110) | def test_posts(self):
method test_users (line 178) | def test_users(self):
method test_comments (line 203) | def test_comments(self):
FILE: tests/test_basics.py
class BasicsTestCase (line 6) | class BasicsTestCase(unittest.TestCase):
method setUp (line 7) | def setUp(self):
method tearDown (line 13) | def tearDown(self):
method test_app_exists (line 18) | def test_app_exists(self):
method test_app_is_testing (line 21) | def test_app_is_testing(self):
FILE: tests/test_client.py
class FlaskClientTestCase (line 6) | class FlaskClientTestCase(unittest.TestCase):
method setUp (line 7) | def setUp(self):
method tearDown (line 15) | def tearDown(self):
method test_home_page (line 20) | def test_home_page(self):
method test_register_and_login (line 25) | def test_register_and_login(self):
FILE: tests/test_selenium.py
class SeleniumTestCase (line 10) | class SeleniumTestCase(unittest.TestCase):
method setUpClass (line 14) | def setUpClass(cls):
method tearDownClass (line 58) | def tearDownClass(cls):
method setUp (line 72) | def setUp(self):
method tearDown (line 76) | def tearDown(self):
method test_admin_home_page (line 79) | def test_admin_home_page(self):
FILE: tests/test_user_model.py
class UserModelTestCase (line 8) | class UserModelTestCase(unittest.TestCase):
method setUp (line 9) | def setUp(self):
method tearDown (line 16) | def tearDown(self):
method test_password_setter (line 21) | def test_password_setter(self):
method test_no_password_getter (line 25) | def test_no_password_getter(self):
method test_password_verification (line 30) | def test_password_verification(self):
method test_password_salts_are_random (line 35) | def test_password_salts_are_random(self):
method test_valid_confirmation_token (line 40) | def test_valid_confirmation_token(self):
method test_invalid_confirmation_token (line 47) | def test_invalid_confirmation_token(self):
method test_expired_confirmation_token (line 56) | def test_expired_confirmation_token(self):
method test_valid_reset_token (line 64) | def test_valid_reset_token(self):
method test_invalid_reset_token (line 72) | def test_invalid_reset_token(self):
method test_valid_email_change_token (line 80) | def test_valid_email_change_token(self):
method test_invalid_email_change_token (line 88) | def test_invalid_email_change_token(self):
method test_duplicate_email_change_token (line 98) | def test_duplicate_email_change_token(self):
method test_user_role (line 108) | def test_user_role(self):
method test_moderator_role (line 116) | def test_moderator_role(self):
method test_administrator_role (line 125) | def test_administrator_role(self):
method test_anonymous_user (line 134) | def test_anonymous_user(self):
method test_timestamps (line 142) | def test_timestamps(self):
method test_ping (line 151) | def test_ping(self):
method test_gravatar (line 160) | def test_gravatar(self):
method test_follows (line 173) | def test_follows(self):
method test_to_json (line 210) | def test_to_json(self):
Condensed preview — 84 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (124K chars).
[
{
"path": ".gitignore",
"chars": 409,
"preview": "*.py[cod]\n\n# C extensions\n*.so\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed."
},
{
"path": "Dockerfile",
"chars": 381,
"preview": "FROM python:3.6-alpine\n\nENV FLASK_APP flasky.py\nENV FLASK_CONFIG production\n\nRUN adduser -D flasky\nUSER flasky\n\nWORKDIR "
},
{
"path": "LICENSE",
"chars": 1083,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2013 Miguel Grinberg\n\nPermission is hereby granted, free of charge, to any person o"
},
{
"path": "Procfile",
"chars": 25,
"preview": "web: gunicorn flasky:app\n"
},
{
"path": "README.md",
"chars": 693,
"preview": "Flasky\n======\n\nThis repository contains the source code examples for the second edition of my O'Reilly book [Flask Web D"
},
{
"path": "app/__init__.py",
"chars": 1156,
"preview": "from flask import Flask\nfrom flask_bootstrap import Bootstrap\nfrom flask_mail import Mail\nfrom flask_moment import Momen"
},
{
"path": "app/api/__init__.py",
"chars": 124,
"preview": "from flask import Blueprint\n\napi = Blueprint('api', __name__)\n\nfrom . import authentication, posts, users, comments, err"
},
{
"path": "app/api/authentication.py",
"chars": 1223,
"preview": "from flask import g, jsonify\nfrom flask_httpauth import HTTPBasicAuth\nfrom ..models import User\nfrom . import api\nfrom ."
},
{
"path": "app/api/comments.py",
"chars": 2190,
"preview": "from flask import jsonify, request, g, url_for, current_app\nfrom .. import db\nfrom ..models import Post, Permission, Com"
},
{
"path": "app/api/decorators.py",
"chars": 411,
"preview": "from functools import wraps\nfrom flask import g\nfrom .errors import forbidden\n\n\ndef permission_required(permission):\n "
},
{
"path": "app/api/errors.py",
"chars": 625,
"preview": "from flask import jsonify\nfrom app.exceptions import ValidationError\nfrom . import api\n\n\ndef bad_request(message):\n r"
},
{
"path": "app/api/posts.py",
"chars": 1688,
"preview": "from flask import jsonify, request, g, url_for, current_app\nfrom .. import db\nfrom ..models import Post, Permission\nfrom"
},
{
"path": "app/api/users.py",
"chars": 1714,
"preview": "from flask import jsonify, request, current_app, url_for\nfrom . import api\nfrom ..models import User, Post\n\n\n@api.route("
},
{
"path": "app/auth/__init__.py",
"chars": 85,
"preview": "from flask import Blueprint\n\nauth = Blueprint('auth', __name__)\n\nfrom . import views\n"
},
{
"path": "app/auth/forms.py",
"chars": 2941,
"preview": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, PasswordField, BooleanField, SubmitField\nfrom wtforms.v"
},
{
"path": "app/auth/views.py",
"chars": 6132,
"preview": "from flask import render_template, redirect, request, url_for, flash\nfrom flask_login import login_user, logout_user, lo"
},
{
"path": "app/decorators.py",
"chars": 494,
"preview": "from functools import wraps\nfrom flask import abort\nfrom flask_login import current_user\nfrom .models import Permission\n"
},
{
"path": "app/email.py",
"chars": 669,
"preview": "from threading import Thread\nfrom flask import current_app, render_template\nfrom flask_mail import Message\nfrom . import"
},
{
"path": "app/exceptions.py",
"chars": 44,
"preview": "class ValidationError(ValueError):\n pass\n"
},
{
"path": "app/fake.py",
"chars": 1012,
"preview": "from random import randint\nfrom sqlalchemy.exc import IntegrityError\nfrom faker import Faker\nfrom . import db\nfrom .mode"
},
{
"path": "app/main/__init__.py",
"chars": 220,
"preview": "from flask import Blueprint\n\nmain = Blueprint('main', __name__)\n\nfrom . import views, errors\nfrom ..models import Permis"
},
{
"path": "app/main/errors.py",
"chars": 1018,
"preview": "from flask import render_template, request, jsonify\nfrom . import main\n\n\n@main.app_errorhandler(403)\ndef forbidden(e):\n "
},
{
"path": "app/main/forms.py",
"chars": 2375,
"preview": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, TextAreaField, BooleanField, SelectField,\\\n SubmitFi"
},
{
"path": "app/main/views.py",
"chars": 10109,
"preview": "from flask import render_template, redirect, url_for, abort, flash, request,\\\n current_app, make_response\nfrom flask_"
},
{
"path": "app/models.py",
"chars": 12892,
"preview": "from datetime import datetime\nimport hashlib\nfrom werkzeug.security import generate_password_hash, check_password_hash\nf"
},
{
"path": "app/static/styles.css",
"chars": 1855,
"preview": ".profile-thumbnail {\n position: absolute;\n}\n.profile-header {\n min-height: 260px;\n margin-left: 280px;\n}\ndiv.po"
},
{
"path": "app/templates/403.html",
"chars": 174,
"preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Forbidden{% endblock %}\n\n{% block page_content %}\n<div class=\"page-"
},
{
"path": "app/templates/404.html",
"chars": 179,
"preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Page Not Found{% endblock %}\n\n{% block page_content %}\n<div class=\""
},
{
"path": "app/templates/500.html",
"chars": 198,
"preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Internal Server Error{% endblock %}\n\n{% block page_content %}\n<div "
},
{
"path": "app/templates/_comments.html",
"chars": 1585,
"preview": "<ul class=\"comments\">\n {% for comment in comments %}\n <li class=\"comment\">\n <div class=\"comment-thumbnail\">"
},
{
"path": "app/templates/_macros.html",
"chars": 1163,
"preview": "{% macro pagination_widget(pagination, endpoint, fragment='') %}\n<ul class=\"pagination\">\n <li{% if not pagination.has"
},
{
"path": "app/templates/_posts.html",
"chars": 1698,
"preview": "<ul class=\"posts\">\n {% for post in posts %}\n <li class=\"post\">\n <div class=\"post-thumbnail\">\n <a"
},
{
"path": "app/templates/auth/change_email.html",
"chars": 302,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Change Email Address{% end"
},
{
"path": "app/templates/auth/change_password.html",
"chars": 292,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Change Password{% endblock"
},
{
"path": "app/templates/auth/email/change_email.html",
"chars": 439,
"preview": "<p>Dear {{ user.username }},</p>\n<p>To confirm your new email address <a href=\"{{ url_for('auth.change_email', token=tok"
},
{
"path": "app/templates/auth/email/change_email.txt",
"chars": 240,
"preview": "Dear {{ user.username }},\n\nTo confirm your new email address click on the following link:\n\n{{ url_for('auth.change_email"
},
{
"path": "app/templates/auth/email/confirm.html",
"chars": 459,
"preview": "<p>Dear {{ user.username }},</p>\n<p>Welcome to <b>Flasky</b>!</p>\n<p>To confirm your account please <a href=\"{{ url_for("
},
{
"path": "app/templates/auth/email/confirm.txt",
"chars": 252,
"preview": "Dear {{ user.username }},\n\nWelcome to Flasky!\n\nTo confirm your account please click on the following link:\n\n{{ url_for('"
},
{
"path": "app/templates/auth/email/reset_password.html",
"chars": 510,
"preview": "<p>Dear {{ user.username }},</p>\n<p>To reset your password <a href=\"{{ url_for('auth.password_reset', token=token, _exte"
},
{
"path": "app/templates/auth/email/reset_password.txt",
"chars": 303,
"preview": "Dear {{ user.username }},\n\nTo reset your password click on the following link:\n\n{{ url_for('auth.password_reset', token="
},
{
"path": "app/templates/auth/login.html",
"chars": 483,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Login{% endblock %}\n\n{% bl"
},
{
"path": "app/templates/auth/register.html",
"chars": 274,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Register{% endblock %}\n\n{%"
},
{
"path": "app/templates/auth/reset_password.html",
"chars": 290,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Password Reset{% endblock "
},
{
"path": "app/templates/auth/unconfirmed.html",
"chars": 589,
"preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - Confirm your account{% endblock %}\n\n{% block page_content %}\n<div c"
},
{
"path": "app/templates/base.html",
"chars": 2893,
"preview": "{% extends \"bootstrap/base.html\" %}\n\n{% block title %}Flasky{% endblock %}\n\n{% block head %}\n{{ super() }}\n<link rel=\"sh"
},
{
"path": "app/templates/edit_post.html",
"chars": 343,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Edit Post{% endblock %}\n\n{"
},
{
"path": "app/templates/edit_profile.html",
"chars": 287,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n\n{% block title %}Flasky - Edit Profile{% endblock %}"
},
{
"path": "app/templates/followers.html",
"chars": 907,
"preview": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - {{ title }} {{ user.username "
},
{
"path": "app/templates/index.html",
"chars": 1051,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n{% import \"_macros.html\" as macros %}\n\n{% block title"
},
{
"path": "app/templates/mail/new_user.html",
"chars": 44,
"preview": "User <b>{{ user.username }}</b> has joined.\n"
},
{
"path": "app/templates/mail/new_user.txt",
"chars": 37,
"preview": "User {{ user.username }} has joined.\n"
},
{
"path": "app/templates/moderate.html",
"chars": 411,
"preview": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - Comment Moderation{% endblock"
},
{
"path": "app/templates/post.html",
"chars": 564,
"preview": "{% extends \"base.html\" %}\n{% import \"bootstrap/wtf.html\" as wtf %}\n{% import \"_macros.html\" as macros %}\n\n{% block title"
},
{
"path": "app/templates/user.html",
"chars": 2662,
"preview": "{% extends \"base.html\" %}\n{% import \"_macros.html\" as macros %}\n\n{% block title %}Flasky - {{ user.username }}{% endbloc"
},
{
"path": "boot.sh",
"chars": 263,
"preview": "#!/bin/sh\nsource venv/bin/activate\n\nwhile true; do\n flask deploy\n if [[ \"$?\" == \"0\" ]]; then\n break\n fi\n"
},
{
"path": "config.py",
"chars": 3963,
"preview": "import os\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n\nclass Config:\n SECRET_KEY = os.environ.get('SECRET_K"
},
{
"path": "docker-compose.yml",
"chars": 240,
"preview": "version: '3'\nservices:\n flasky:\n build: .\n ports:\n - \"8000:5000\"\n env_file: .env\n restart: always\n "
},
{
"path": "flasky.py",
"chars": 2534,
"preview": "import os\nfrom dotenv import load_dotenv\n\ndotenv_path = os.path.join(os.path.dirname(__file__), '.env')\nif os.path.exist"
},
{
"path": "migrations/README",
"chars": 38,
"preview": "Generic single-database configuration."
},
{
"path": "migrations/alembic.ini",
"chars": 770,
"preview": "# A generic, single database configuration.\n\n[alembic]\n# template used to generate migration files\n# file_template = %%("
},
{
"path": "migrations/env.py",
"chars": 2158,
"preview": "from __future__ import with_statement\nfrom alembic import context\nfrom sqlalchemy import engine_from_config, pool\nfrom l"
},
{
"path": "migrations/script.py.mako",
"chars": 412,
"preview": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision}\nCreate Date: ${create_date}\n\n\"\"\"\n\n# revision identi"
},
{
"path": "migrations/versions/190163627111_account_confirmation.py",
"chars": 616,
"preview": "\"\"\"account confirmation\n\nRevision ID: 190163627111\nRevises: 456a945560f6\nCreate Date: 2013-12-29 02:58:45.577428\n\n\"\"\"\n\n#"
},
{
"path": "migrations/versions/198b0eebcf9_caching_of_avatar_hashes.py",
"chars": 628,
"preview": "\"\"\"caching of avatar hashes\n\nRevision ID: 198b0eebcf9\nRevises: d66f086b258\nCreate Date: 2014-02-04 09:10:02.245503\n\n\"\"\"\n"
},
{
"path": "migrations/versions/1b966e7f4b9e_post_model.py",
"chars": 983,
"preview": "\"\"\"post model\n\nRevision ID: 1b966e7f4b9e\nRevises: 198b0eebcf9\nCreate Date: 2013-12-31 00:00:14.700591\n\n\"\"\"\n\n# revision i"
},
{
"path": "migrations/versions/2356a38169ea_followers.py",
"chars": 908,
"preview": "\"\"\"followers\n\nRevision ID: 2356a38169ea\nRevises: 288cd3dc5a8\nCreate Date: 2013-12-31 16:10:34.500006\n\n\"\"\"\n\n# revision id"
},
{
"path": "migrations/versions/288cd3dc5a8_rich_text_posts.py",
"chars": 606,
"preview": "\"\"\"rich text posts\n\nRevision ID: 288cd3dc5a8\nRevises: 1b966e7f4b9e\nCreate Date: 2013-12-31 03:25:13.286503\n\n\"\"\"\n\n# revis"
},
{
"path": "migrations/versions/38c4e85512a9_initial_migration.py",
"chars": 1163,
"preview": "\"\"\"initial migration\n\nRevision ID: 38c4e85512a9\nRevises: None\nCreate Date: 2013-12-27 01:23:59.392801\n\n\"\"\"\n\n# revision i"
},
{
"path": "migrations/versions/456a945560f6_login_support.py",
"chars": 863,
"preview": "\"\"\"login support\n\nRevision ID: 456a945560f6\nRevises: 38c4e85512a9\nCreate Date: 2013-12-29 00:18:35.795259\n\n\"\"\"\n\n# revisi"
},
{
"path": "migrations/versions/51f5ccfba190_comments.py",
"chars": 1224,
"preview": "\"\"\"comments\n\nRevision ID: 51f5ccfba190\nRevises: 2356a38169ea\nCreate Date: 2014-01-01 12:08:43.287523\n\n\"\"\"\n\n# revision id"
},
{
"path": "migrations/versions/56ed7d33de8d_user_roles.py",
"chars": 850,
"preview": "\"\"\"user roles\n\nRevision ID: 56ed7d33de8d\nRevises: 190163627111\nCreate Date: 2013-12-29 22:19:54.212604\n\n\"\"\"\n\n# revision "
},
{
"path": "migrations/versions/d66f086b258_user_information.py",
"chars": 1101,
"preview": "\"\"\"user information\n\nRevision ID: d66f086b258\nRevises: 56ed7d33de8d\nCreate Date: 2013-12-29 23:50:49.566954\n\n\"\"\"\n\n# revi"
},
{
"path": "requirements/common.txt",
"chars": 532,
"preview": "alembic==0.9.3\nbleach==2.0.0\nblinker==1.4\nclick==6.7\ndominate==2.3.1\nFlask==0.12.2\nFlask-Bootstrap==3.3.7.1\nFlask-HTTPAu"
},
{
"path": "requirements/dev.txt",
"chars": 169,
"preview": "-r common.txt\ncertifi==2017.7.27.1\nchardet==3.0.4\ncoverage==4.4.1\nfaker==0.7.18\nhttpie==0.9.9\nidna==2.5\nPygments==2.2.0\n"
},
{
"path": "requirements/docker.txt",
"chars": 47,
"preview": "-r common.txt\ngunicorn==19.7.1\npymysql==0.7.11\n"
},
{
"path": "requirements/heroku.txt",
"chars": 65,
"preview": "-r prod.txt\nFlask-SSLify==0.1.5\ngunicorn==19.7.1\npsycopg2==2.7.3\n"
},
{
"path": "requirements/prod.txt",
"chars": 14,
"preview": "-r common.txt\n"
},
{
"path": "requirements.txt",
"chars": 155,
"preview": "# this requirements file is used by Heroku\n# requirements for other configurations are located in the requirements subdi"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/test_api.py",
"chars": 10644,
"preview": "import unittest\nimport json\nimport re\nfrom base64 import b64encode\nfrom app import create_app, db\nfrom app.models import"
},
{
"path": "tests/test_basics.py",
"chars": 563,
"preview": "import unittest\nfrom flask import current_app\nfrom app import create_app, db\n\n\nclass BasicsTestCase(unittest.TestCase):\n"
},
{
"path": "tests/test_client.py",
"chars": 2266,
"preview": "import re\nimport unittest\nfrom app import create_app, db\nfrom app.models import User, Role\n\nclass FlaskClientTestCase(un"
},
{
"path": "tests/test_selenium.py",
"chars": 3228,
"preview": "import re\nimport threading\nimport time\nimport unittest\nfrom selenium import webdriver\nfrom app import create_app, db, fa"
},
{
"path": "tests/test_user_model.py",
"chars": 8222,
"preview": "import unittest\nimport time\nfrom datetime import datetime\nfrom app import create_app, db\nfrom app.models import User, An"
}
]
About this extraction
This page contains the full source code of the miguelgrinberg/flasky GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 84 files (112.7 KB), approximately 28.8k tokens, and a symbol index with 217 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.