Repository: miguelgrinberg/flasky-first-edition
Branch: master
Commit: 4052642d1cf0
Files: 78
Total size: 109.8 KB
Directory structure:
gitextract_uuti03g0/
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── __init__.py
│ ├── api_1_0/
│ │ ├── __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
│ ├── 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
│ ├── error_page.html
│ ├── followers.html
│ ├── index.html
│ ├── mail/
│ │ ├── new_user.html
│ │ └── new_user.txt
│ ├── moderate.html
│ ├── post.html
│ └── user.html
├── config.py
├── manage.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
│ ├── heroku.txt
│ └── prod.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
================================================
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: README.md
================================================
Flasky
======
**NOTE: This repository is unmaintained. Refer to my newer projects if you are interested in learning Flask.**
This repository contains the archived source code examples for my O'Reilly book [Flask Web Development](http://www.flaskbook.com), first edition. For the code examples for the current edition of the book, go to [https://github.com/miguelgrinberg/flasky](https://github.com/miguelgrinberg/flasky).
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.
================================================
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.session_protection = 'strong'
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 not app.debug and not app.testing and not app.config['SSL_DISABLE']:
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_1_0 import api as api_1_0_blueprint
app.register_blueprint(api_1_0_blueprint, url_prefix='/api/v1.0')
return app
================================================
FILE: app/api_1_0/__init__.py
================================================
from flask import Blueprint
api = Blueprint('api', __name__)
from . import authentication, posts, users, comments, errors
================================================
FILE: app/api_1_0/authentication.py
================================================
from flask import g, jsonify
from flask_httpauth import HTTPBasicAuth
from ..models import User, AnonymousUser
from . import api
from .errors import unauthorized, forbidden
auth = HTTPBasicAuth()
@auth.verify_password
def verify_password(email_or_token, password):
if email_or_token == '':
g.current_user = AnonymousUser()
return True
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).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('/token')
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_1_0/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, 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, _external=True)
next = None
if pagination.has_next:
next = url_for('api.get_comments', page=page+1, _external=True)
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, 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,
_external=True)
next = None
if pagination.has_next:
next = url_for('api.get_post_comments', id=id, page=page+1,
_external=True)
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,
_external=True)}
================================================
FILE: app/api_1_0/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_1_0/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_1_0/posts.py
================================================
from flask import jsonify, request, g, abort, 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, 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, _external=True)
next = None
if pagination.has_next:
next = url_for('api.get_posts', page=page+1, _external=True)
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_ARTICLES)
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, _external=True)}
@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE_ARTICLES)
def edit_post(id):
post = Post.query.get_or_404(id)
if g.current_user != post.author and \
not g.current_user.can(Permission.ADMINISTER):
return forbidden('Insufficient permissions')
post.body = request.json.get('body', post.body)
db.session.add(post)
return jsonify(post.to_json())
================================================
FILE: app/api_1_0/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, 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,
_external=True)
next = None
if pagination.has_next:
next = url_for('api.get_user_posts', id=id, page=page+1,
_external=True)
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, 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,
_external=True)
next = None
if pagination.has_next:
next = url_for('api.get_user_followed_posts', id=id, page=page+1,
_external=True)
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 Required, Length, Email, Regexp, EqualTo
from wtforms import ValidationError
from ..models import User
class LoginForm(FlaskForm):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
password = PasswordField('Password', validators=[Required()])
remember_me = BooleanField('Keep me logged in')
submit = SubmitField('Log In')
class RegistrationForm(FlaskForm):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
username = StringField('Username', validators=[
Required(), 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=[
Required(), EqualTo('password2', message='Passwords must match.')])
password2 = PasswordField('Confirm password', validators=[Required()])
submit = SubmitField('Register')
def validate_email(self, field):
if User.query.filter_by(email=field.data).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=[Required()])
password = PasswordField('New password', validators=[
Required(), EqualTo('password2', message='Passwords must match')])
password2 = PasswordField('Confirm new password', validators=[Required()])
submit = SubmitField('Update Password')
class PasswordResetRequestForm(FlaskForm):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
submit = SubmitField('Reset Password')
class PasswordResetForm(FlaskForm):
email = StringField('Email', validators=[Required(), Length(1, 64),
Email()])
password = PasswordField('New Password', validators=[
Required(), EqualTo('password2', message='Passwords must match')])
password2 = PasswordField('Confirm password', validators=[Required()])
submit = SubmitField('Reset Password')
def validate_email(self, field):
if User.query.filter_by(email=field.data).first() is None:
raise ValidationError('Unknown email address.')
class ChangeEmailForm(FlaskForm):
email = StringField('New Email', validators=[Required(), Length(1, 64),
Email()])
password = PasswordField('Password', validators=[Required()])
submit = SubmitField('Update Email Address')
def validate_email(self, field):
if User.query.filter_by(email=field.data).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.endpoint[:5] != '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).first()
if user is not None and user.verify_password(form.password.data):
login_user(user, form.remember_me.data)
return redirect(request.args.get('next') or url_for('main.index'))
flash('Invalid username 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,
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):
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)
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).first()
if user:
token = user.generate_reset_token()
send_email(user.email, 'Reset Your Password',
'auth/email/reset_password',
user=user, token=token,
next=request.args.get('next'))
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():
user = User.query.filter_by(email=form.email.data).first()
if user is None:
return redirect(url_for('main.index'))
if user.reset_password(token, form.password.data):
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
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):
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.ADMINISTER)(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/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 Required, 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=[Required()])
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=[Required(), Length(1, 64),
Email()])
username = StringField('Username', validators=[
Required(), 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=[Required()])
submit = SubmitField('Submit')
class CommentForm(FlaskForm):
body = StringField('Enter your comment', validators=[Required()])
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_ARTICLES) and \
form.validate_on_submit():
post = Post(body=form.body.data,
author=current_user._get_current_object())
db.session.add(post)
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, 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, 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)
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)
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)
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, 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.ADMINISTER):
abort(403)
form = PostForm()
if form.validate_on_submit():
post.body = form.body.data
db.session.add(post)
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)
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)
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, 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, 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_COMMENTS)
def moderate():
page = request.args.get('page', 1, type=int)
pagination = Comment.query.order_by(Comment.timestamp.desc()).paginate(
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_COMMENTS)
def moderate_enable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = False
db.session.add(comment)
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_COMMENTS)
def moderate_disable(id):
comment = Comment.query.get_or_404(id)
comment.disabled = True
db.session.add(comment)
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 = 0x01
COMMENT = 0x02
WRITE_ARTICLES = 0x04
MODERATE_COMMENTS = 0x08
ADMINISTER = 0x80
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')
@staticmethod
def insert_roles():
roles = {
'User': (Permission.FOLLOW |
Permission.COMMENT |
Permission.WRITE_ARTICLES, True),
'Moderator': (Permission.FOLLOW |
Permission.COMMENT |
Permission.WRITE_ARTICLES |
Permission.MODERATE_COMMENTS, False),
'Administrator': (0xff, False)
}
for r in roles:
role = Role.query.filter_by(name=r).first()
if role is None:
role = Role(name=r)
role.permissions = roles[r][0]
role.default = roles[r][1]
db.session.add(role)
db.session.commit()
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 generate_fake(count=100):
from sqlalchemy.exc import IntegrityError
from random import seed
import forgery_py
seed()
for i in range(count):
u = User(email=forgery_py.internet.email_address(),
username=forgery_py.internet.user_name(True),
password=forgery_py.lorem_ipsum.word(),
confirmed=True,
name=forgery_py.name.full_name(),
location=forgery_py.address.city(),
about_me=forgery_py.lorem_ipsum.sentence(),
member_since=forgery_py.date.date(True))
db.session.add(u)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
@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(permissions=0xff).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 = hashlib.md5(
self.email.encode('utf-8')).hexdigest()
self.followed.append(Follow(followed=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})
def confirm(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
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})
def reset_password(self, token, new_password):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
except:
return False
if data.get('reset') != self.id:
return False
self.password = new_password
db.session.add(self)
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})
def change_email(self, token):
s = Serializer(current_app.config['SECRET_KEY'])
try:
data = s.loads(token)
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 = hashlib.md5(
self.email.encode('utf-8')).hexdigest()
db.session.add(self)
return True
def can(self, permissions):
return self.role is not None and \
(self.role.permissions & permissions) == permissions
def is_administrator(self):
return self.can(Permission.ADMINISTER)
def ping(self):
self.last_seen = datetime.utcnow()
db.session.add(self)
def gravatar(self, size=100, default='identicon', rating='g'):
if request.is_secure:
url = 'https://secure.gravatar.com/avatar'
else:
url = 'http://www.gravatar.com/avatar'
hash = self.avatar_hash or hashlib.md5(
self.email.encode('utf-8')).hexdigest()
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):
return self.followed.filter_by(
followed_id=user.id).first() is not None
def is_followed_by(self, user):
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, _external=True),
'username': self.username,
'member_since': self.member_since,
'last_seen': self.last_seen,
'posts': url_for('api.get_user_posts', id=self.id, _external=True),
'followed_posts': url_for('api.get_user_followed_posts',
id=self.id, _external=True),
'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('ascii')
@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 generate_fake(count=100):
from random import seed, randint
import forgery_py
seed()
user_count = User.query.count()
for i in range(count):
u = User.query.offset(randint(0, user_count - 1)).first()
p = Post(body=forgery_py.lorem_ipsum.sentences(randint(1, 5)),
timestamp=forgery_py.date.date(True),
author=u)
db.session.add(p)
db.session.commit()
@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, _external=True),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author': url_for('api.get_user', id=self.author_id,
_external=True),
'comments': url_for('api.get_post_comments', id=self.id,
_external=True),
'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, _external=True),
'post': url_for('api.get_post', id=self.post_id, _external=True),
'body': self.body,
'body_html': self.body_html,
'timestamp': self.timestamp,
'author': url_for('api.get_user', id=self.author_id,
_external=True),
}
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_COMMENTS) %}
<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/error_page.html
================================================
{% extends "base.html" %}
{% block title %}Flasky - {{ code }}: {{ name }}{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>{{ code }}: {{ name }}</h1>
<p>{{ description }}</p>
</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_ARTICLES) %}
{{ 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') }}">Followers</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: 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'
SSL_DISABLE = False
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_RECORD_QUERIES = True
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
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')
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:///' + os.path.join(basedir, 'data-test.sqlite')
WTF_CSRF_ENABLED = False
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
@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_DISABLE = bool(os.environ.get('SSL_DISABLE'))
@classmethod
def init_app(cls, app):
ProductionConfig.init_app(app)
# handle proxy server headers
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.WARNING)
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.WARNING)
app.logger.addHandler(syslog_handler)
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'heroku': HerokuConfig,
'unix': UnixConfig,
'default': DevelopmentConfig
}
================================================
FILE: manage.py
================================================
#!/usr/bin/env python
import os
COV = None
if os.environ.get('FLASK_COVERAGE'):
import coverage
COV = coverage.coverage(branch=True, include='app/*')
COV.start()
if os.path.exists('.env'):
print('Importing environment from .env...')
for line in open('.env'):
var = line.strip().split('=')
if len(var) == 2:
os.environ[var[0]] = var[1]
from app import create_app, db
from app.models import User, Follow, Role, Permission, Post, Comment
from flask_script import Manager, Shell
from flask_migrate import Migrate, MigrateCommand
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)
def make_shell_context():
return dict(app=app, db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post, Comment=Comment)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)
@manager.command
def test(coverage=False):
"""Run the unit tests."""
if coverage and not os.environ.get('FLASK_COVERAGE'):
import sys
os.environ['FLASK_COVERAGE'] = '1'
os.execvp(sys.executable, [sys.executable] + sys.argv)
import unittest
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()
@manager.command
def profile(length=25, profile_dir=None):
"""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()
@manager.command
def deploy():
"""Run deployment tasks."""
from flask_migrate import upgrade
from app.models import Role, User
# migrate database to latest revision
upgrade()
# create user roles
Role.insert_roles()
# create self-follows for all users
User.add_self_follows()
if __name__ == '__main__':
manager.run()
================================================
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
================================================
Flask==0.12
Flask-Bootstrap==3.0.3.1
Flask-HTTPAuth==2.7.0
Flask-Login==0.3.1
Flask-Mail==0.9.0
Flask-Migrate==2.0.3
Flask-Moment==0.2.1
Flask-PageDown==0.1.4
Flask-SQLAlchemy==2.1
Flask-Script==2.0.5
Flask-WTF==0.14.2
Jinja2==2.9.5
Mako==1.0.6
Markdown==2.3.1
MarkupSafe==0.23
SQLAlchemy==1.1.5
WTForms==2.1
Werkzeug==0.11.15
alembic==0.8.10
bleach==1.4.0
blinker==1.3
click==6.7
html5lib==1.0b3
itsdangerous==0.24
python-editor==1.0.3
six==1.4.1
================================================
FILE: requirements/dev.txt
================================================
-r common.txt
ForgeryPy==0.1
Pygments==1.6
colorama==0.2.7
coverage==3.7.1
httpie==0.7.2
requests==2.1.0
selenium==2.45.0
================================================
FILE: requirements/heroku.txt
================================================
-r prod.txt
Flask-SSLify==0.1.4
gunicorn==18.0
psycopg2==2.5.1
================================================
FILE: requirements/prod.txt
================================================
-r common.txt
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/test_api.py
================================================
import unittest
import json
import re
from base64 import b64encode
from flask import url_for
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.assertTrue(response.status_code == 404)
json_response = json.loads(response.data.decode('utf-8'))
self.assertTrue(json_response['error'] == 'not found')
def test_no_auth(self):
response = self.client.get(url_for('api.get_posts'),
content_type='application/json')
self.assertTrue(response.status_code == 200)
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(
url_for('api.get_posts'),
headers=self.get_api_headers('john@example.com', 'dog'))
self.assertTrue(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(
url_for('api.get_posts'),
headers=self.get_api_headers('bad-token', ''))
self.assertTrue(response.status_code == 401)
# get a token
response = self.client.get(
url_for('api.get_token'),
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertIsNotNone(json_response.get('token'))
token = json_response['token']
# issue a request with the token
response = self.client.get(
url_for('api.get_posts'),
headers=self.get_api_headers(token, ''))
self.assertTrue(response.status_code == 200)
def test_anonymous(self):
response = self.client.get(
url_for('api.get_posts'),
headers=self.get_api_headers('', ''))
self.assertTrue(response.status_code == 200)
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(
url_for('api.get_posts'),
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertTrue(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(
url_for('api.new_post'),
headers=self.get_api_headers('john@example.com', 'cat'),
data=json.dumps({'body': ''}))
self.assertTrue(response.status_code == 400)
# write a post
response = self.client.post(
url_for('api.new_post'),
headers=self.get_api_headers('john@example.com', 'cat'),
data=json.dumps({'body': 'body of the *blog* post'}))
self.assertTrue(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.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertTrue(json_response['url'] == url)
self.assertTrue(json_response['body'] == 'body of the *blog* post')
self.assertTrue(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(
url_for('api.get_user_posts', id=u.id),
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertIsNotNone(json_response.get('posts'))
self.assertTrue(json_response.get('count', 0) == 1)
self.assertTrue(json_response['posts'][0] == json_post)
# get the post from the user as a follower
response = self.client.get(
url_for('api.get_user_followed_posts', id=u.id),
headers=self.get_api_headers('john@example.com', 'cat'))
self.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertIsNotNone(json_response.get('posts'))
self.assertTrue(json_response.get('count', 0) == 1)
self.assertTrue(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.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertTrue(json_response['url'] == url)
self.assertTrue(json_response['body'] == 'updated body')
self.assertTrue(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(
url_for('api.get_user', id=u1.id),
headers=self.get_api_headers('susan@example.com', 'dog'))
self.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertTrue(json_response['username'] == 'john')
response = self.client.get(
url_for('api.get_user', id=u2.id),
headers=self.get_api_headers('susan@example.com', 'dog'))
self.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertTrue(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(
url_for('api.new_post_comment', id=post.id),
headers=self.get_api_headers('susan@example.com', 'dog'),
data=json.dumps({'body': 'Good [post](http://example.com)!'}))
self.assertTrue(response.status_code == 201)
json_response = json.loads(response.data.decode('utf-8'))
url = response.headers.get('Location')
self.assertIsNotNone(url)
self.assertTrue(json_response['body'] ==
'Good [post](http://example.com)!')
self.assertTrue(
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.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertTrue(json_response['url'] == url)
self.assertTrue(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(
url_for('api.get_post_comments', id=post.id),
headers=self.get_api_headers('susan@example.com', 'dog'))
self.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertIsNotNone(json_response.get('comments'))
self.assertTrue(json_response.get('count', 0) == 2)
# get all the comments
response = self.client.get(
url_for('api.get_comments', id=post.id),
headers=self.get_api_headers('susan@example.com', 'dog'))
self.assertTrue(response.status_code == 200)
json_response = json.loads(response.data.decode('utf-8'))
self.assertIsNotNone(json_response.get('comments'))
self.assertTrue(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 flask import url_for
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(url_for('main.index'))
self.assertTrue(b'Stranger' in response.data)
def test_register_and_login(self):
# register a new account
response = self.client.post(url_for('auth.register'), data={
'email': 'john@example.com',
'username': 'john',
'password': 'cat',
'password2': 'cat'
})
self.assertTrue(response.status_code == 302)
# login with the new account
response = self.client.post(url_for('auth.login'), data={
'email': 'john@example.com',
'password': 'cat'
}, follow_redirects=True)
self.assertTrue(re.search(b'Hello,\s+john!', response.data))
self.assertTrue(
b'You have not confirmed your account yet' in response.data)
# send a confirmation token
user = User.query.filter_by(email='john@example.com').first()
token = user.generate_confirmation_token()
response = self.client.get(url_for('auth.confirm', token=token),
follow_redirects=True)
self.assertTrue(
b'You have confirmed your account' in response.data)
# log out
response = self.client.get(url_for('auth.logout'), follow_redirects=True)
self.assertTrue(b'You have been logged out' in response.data)
================================================
FILE: tests/test_selenium.py
================================================
import re
import threading
import time
import unittest
from selenium import webdriver
from app import create_app, db
from app.models import Role, User, Post
class SeleniumTestCase(unittest.TestCase):
client = None
@classmethod
def setUpClass(cls):
# start Firefox
try:
cls.client = webdriver.Firefox()
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()
User.generate_fake(10)
Post.generate_fake(10)
# add an administrator user
admin_role = Role.query.filter_by(permissions=0xff).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
threading.Thread(target=cls.app.run).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.close()
# 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.assertTrue('<h1>Login</h1>' in 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.assertTrue('<h1>john</h1>' in 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(u.reset_password(token, 'dog'))
self.assertTrue(u.verify_password('dog'))
def test_invalid_reset_token(self):
u1 = User(password='cat')
u2 = User(password='dog')
db.session.add(u1)
db.session.add(u2)
db.session.commit()
token = u1.generate_reset_token()
self.assertFalse(u2.reset_password(token, 'horse'))
self.assertTrue(u2.verify_password('dog'))
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_roles_and_permissions(self):
u = User(email='john@example.com', password='cat')
self.assertTrue(u.can(Permission.WRITE_ARTICLES))
self.assertFalse(u.can(Permission.MODERATE_COMMENTS))
def test_anonymous_user(self):
u = AnonymousUser()
self.assertFalse(u.can(Permission.FOLLOW))
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')
with self.app.test_request_context('/', base_url='https://example.com'):
gravatar_ssl = u.gravatar()
self.assertTrue('http://www.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)
self.assertTrue('https://secure.gravatar.com/avatar/' +
'd4c74594d841139328695756648b6bd6' in gravatar_ssl)
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()
json_user = u.to_json()
expected_keys = ['url', 'username', 'member_since', 'last_seen',
'posts', 'followed_posts', 'post_count']
self.assertEqual(sorted(json_user.keys()), sorted(expected_keys))
self.assertTrue('api/v1.0/users/' in json_user['url'])
gitextract_uuti03g0/
├── .gitignore
├── LICENSE
├── README.md
├── app/
│ ├── __init__.py
│ ├── api_1_0/
│ │ ├── __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
│ ├── 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
│ ├── error_page.html
│ ├── followers.html
│ ├── index.html
│ ├── mail/
│ │ ├── new_user.html
│ │ └── new_user.txt
│ ├── moderate.html
│ ├── post.html
│ └── user.html
├── config.py
├── manage.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
│ ├── heroku.txt
│ └── prod.txt
└── tests/
├── __init__.py
├── test_api.py
├── test_basics.py
├── test_client.py
├── test_selenium.py
└── test_user_model.py
SYMBOL INDEX (208 symbols across 35 files)
FILE: app/__init__.py
function create_app (line 21) | def create_app(config_name):
FILE: app/api_1_0/authentication.py
function verify_password (line 11) | def verify_password(email_or_token, password):
function auth_error (line 28) | def auth_error():
function before_request (line 34) | def before_request():
function get_token (line 41) | def get_token():
FILE: app/api_1_0/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 61) | def new_post_comment(id):
FILE: app/api_1_0/decorators.py
function permission_required (line 6) | def permission_required(permission):
FILE: app/api_1_0/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_1_0/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_1_0/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 37) | 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 28) | def validate_email(self, field):
method validate_username (line 32) | def validate_username(self, field):
class ChangePasswordForm (line 37) | class ChangePasswordForm(FlaskForm):
class PasswordResetRequestForm (line 45) | class PasswordResetRequestForm(FlaskForm):
class PasswordResetForm (line 51) | class PasswordResetForm(FlaskForm):
method validate_email (line 59) | def validate_email(self, field):
class ChangeEmailForm (line 64) | class ChangeEmailForm(FlaskForm):
method validate_email (line 70) | 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 44) | def logout():
function register (line 51) | def register():
function confirm (line 69) | def confirm(token):
function resend_confirmation (line 81) | def resend_confirmation():
function change_password (line 91) | def change_password():
function password_reset_request (line 105) | def password_reset_request():
function password_reset (line 124) | def password_reset(token):
function change_email_request (line 142) | def change_email_request():
function change_email (line 161) | 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/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 36) | def __init__(self, user, *args, **kwargs):
method validate_email (line 42) | def validate_email(self, field):
method validate_username (line 47) | def validate_username(self, field):
class PostForm (line 53) | class PostForm(FlaskForm):
class CommentForm (line 58) | 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 92) | def edit_profile_admin(id):
function post (line 117) | def post(id):
function edit (line 141) | def edit(id):
function follow (line 159) | def follow(username):
function unfollow (line 175) | def unfollow(username):
function followers (line 189) | def followers(username):
function followed_by (line 206) | def followed_by(username):
function show_all (line 224) | def show_all():
function show_followed (line 232) | def show_followed():
function moderate (line 241) | def moderate():
function moderate_enable (line 254) | def moderate_enable(id):
function moderate_disable (line 265) | def moderate_disable(id):
FILE: app/models.py
class Permission (line 13) | class Permission:
class Role (line 21) | class Role(db.Model):
method insert_roles (line 30) | def insert_roles():
method __repr__ (line 50) | def __repr__(self):
class Follow (line 54) | class Follow(db.Model):
class User (line 63) | class User(UserMixin, db.Model):
method generate_fake (line 91) | def generate_fake(count=100):
method add_self_follows (line 113) | def add_self_follows():
method __init__ (line 120) | def __init__(self, **kwargs):
method password (line 133) | def password(self):
method password (line 137) | def password(self, password):
method verify_password (line 140) | def verify_password(self, password):
method generate_confirmation_token (line 143) | def generate_confirmation_token(self, expiration=3600):
method confirm (line 147) | def confirm(self, token):
method generate_reset_token (line 159) | def generate_reset_token(self, expiration=3600):
method reset_password (line 163) | def reset_password(self, token, new_password):
method generate_email_change_token (line 175) | def generate_email_change_token(self, new_email, expiration=3600):
method change_email (line 179) | def change_email(self, token):
method can (line 198) | def can(self, permissions):
method is_administrator (line 202) | def is_administrator(self):
method ping (line 205) | def ping(self):
method gravatar (line 209) | def gravatar(self, size=100, default='identicon', rating='g'):
method follow (line 219) | def follow(self, user):
method unfollow (line 224) | def unfollow(self, user):
method is_following (line 229) | def is_following(self, user):
method is_followed_by (line 233) | def is_followed_by(self, user):
method followed_posts (line 238) | def followed_posts(self):
method to_json (line 242) | def to_json(self):
method generate_auth_token (line 255) | def generate_auth_token(self, expiration):
method verify_auth_token (line 261) | def verify_auth_token(token):
method __repr__ (line 269) | def __repr__(self):
class AnonymousUser (line 273) | class AnonymousUser(AnonymousUserMixin):
method can (line 274) | def can(self, permissions):
method is_administrator (line 277) | def is_administrator(self):
function load_user (line 284) | def load_user(user_id):
class Post (line 288) | class Post(db.Model):
method generate_fake (line 298) | def generate_fake(count=100):
method on_changed_body (line 313) | def on_changed_body(target, value, oldvalue, initiator):
method to_json (line 321) | def to_json(self):
method from_json (line 336) | def from_json(json_post):
class Comment (line 346) | class Comment(db.Model):
method on_changed_body (line 357) | def on_changed_body(target, value, oldvalue, initiator):
method to_json (line 364) | def to_json(self):
method from_json (line 377) | 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 47) | def init_app(cls, app):
class HerokuConfig (line 70) | class HerokuConfig(ProductionConfig):
method init_app (line 74) | def init_app(cls, app):
class UnixConfig (line 89) | class UnixConfig(ProductionConfig):
method init_app (line 91) | def init_app(cls, app):
FILE: manage.py
function make_shell_context (line 26) | def make_shell_context():
function test (line 34) | def test(coverage=False):
function profile (line 56) | def profile(length=25, profile_dir=None):
function deploy (line 65) | 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 10) | class APITestCase(unittest.TestCase):
method setUp (line 11) | def setUp(self):
method tearDown (line 19) | def tearDown(self):
method get_api_headers (line 24) | def get_api_headers(self, username, password):
method test_404 (line 32) | def test_404(self):
method test_no_auth (line 40) | def test_no_auth(self):
method test_bad_auth (line 45) | def test_bad_auth(self):
method test_token_auth (line 60) | def test_token_auth(self):
method test_anonymous (line 90) | def test_anonymous(self):
method test_unconfirmed_account (line 96) | def test_unconfirmed_account(self):
method test_posts (line 111) | def test_posts(self):
method test_users (line 179) | def test_users(self):
method test_comments (line 204) | 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 7) | class FlaskClientTestCase(unittest.TestCase):
method setUp (line 8) | def setUp(self):
method tearDown (line 16) | def tearDown(self):
method test_home_page (line 21) | 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 54) | def tearDownClass(cls):
method setUp (line 67) | def setUp(self):
method tearDown (line 71) | def tearDown(self):
method test_admin_home_page (line 74) | 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 82) | def test_valid_email_change_token(self):
method test_invalid_email_change_token (line 90) | def test_invalid_email_change_token(self):
method test_duplicate_email_change_token (line 100) | def test_duplicate_email_change_token(self):
method test_roles_and_permissions (line 110) | def test_roles_and_permissions(self):
method test_anonymous_user (line 115) | def test_anonymous_user(self):
method test_timestamps (line 119) | def test_timestamps(self):
method test_ping (line 128) | def test_ping(self):
method test_gravatar (line 137) | def test_gravatar(self):
method test_follows (line 154) | def test_follows(self):
method test_to_json (line 191) | def test_to_json(self):
Condensed preview — 78 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (121K chars).
[
{
"path": ".gitignore",
"chars": 372,
"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": "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": "README.md",
"chars": 663,
"preview": "Flasky\n======\n\n**NOTE: This repository is unmaintained. Refer to my newer projects if you are interested in learning Fla"
},
{
"path": "app/__init__.py",
"chars": 1255,
"preview": "from flask import Flask\nfrom flask_bootstrap import Bootstrap\nfrom flask_mail import Mail\nfrom flask_moment import Momen"
},
{
"path": "app/api_1_0/__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_1_0/authentication.py",
"chars": 1250,
"preview": "from flask import g, jsonify\nfrom flask_httpauth import HTTPBasicAuth\nfrom ..models import User, AnonymousUser\nfrom . im"
},
{
"path": "app/api_1_0/comments.py",
"chars": 2335,
"preview": "from flask import jsonify, request, g, url_for, current_app\nfrom .. import db\nfrom ..models import Post, Permission, Com"
},
{
"path": "app/api_1_0/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_1_0/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_1_0/posts.py",
"chars": 1737,
"preview": "from flask import jsonify, request, g, abort, url_for, current_app\nfrom .. import db\nfrom ..models import Post, Permissi"
},
{
"path": "app/api_1_0/users.py",
"chars": 1860,
"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": 3170,
"preview": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, PasswordField, BooleanField, SubmitField\nfrom wtforms.v"
},
{
"path": "app/auth/views.py",
"chars": 6083,
"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": 499,
"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/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": 2397,
"preview": "from flask_wtf import FlaskForm\nfrom wtforms import StringField, TextAreaField, BooleanField, SelectField,\\\n SubmitFi"
},
{
"path": "app/main/views.py",
"chars": 9876,
"preview": "from flask import render_template, redirect, url_for, abort, flash, request,\\\n current_app, make_response\nfrom flask_"
},
{
"path": "app/models.py",
"chars": 13764,
"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": 2902,
"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/error_page.html",
"chars": 229,
"preview": "{% extends \"base.html\" %}\n\n{% block title %}Flasky - {{ code }}: {{ name }}{% endblock %}\n\n{% block page_content %}\n<div"
},
{
"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": 1061,
"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": "config.py",
"chars": 3338,
"preview": "import os\nbasedir = os.path.abspath(os.path.dirname(__file__))\n\n\nclass Config:\n SECRET_KEY = os.environ.get('SECRET_K"
},
{
"path": "manage.py",
"chars": 2379,
"preview": "#!/usr/bin/env python\nimport os\nCOV = None\nif os.environ.get('FLASK_COVERAGE'):\n import coverage\n COV = coverage.c"
},
{
"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": 448,
"preview": "Flask==0.12\nFlask-Bootstrap==3.0.3.1\nFlask-HTTPAuth==2.7.0\nFlask-Login==0.3.1\nFlask-Mail==0.9.0\nFlask-Migrate==2.0.3\nFla"
},
{
"path": "requirements/dev.txt",
"chars": 122,
"preview": "-r common.txt\nForgeryPy==0.1\nPygments==1.6\ncolorama==0.2.7\ncoverage==3.7.1\nhttpie==0.7.2\nrequests==2.1.0\nselenium==2.45."
},
{
"path": "requirements/heroku.txt",
"chars": 63,
"preview": "-r prod.txt\nFlask-SSLify==0.1.4\ngunicorn==18.0\npsycopg2==2.5.1\n"
},
{
"path": "requirements/prod.txt",
"chars": 14,
"preview": "-r common.txt\n"
},
{
"path": "tests/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "tests/test_api.py",
"chars": 10692,
"preview": "import unittest\nimport json\nimport re\nfrom base64 import b64encode\nfrom flask import url_for\nfrom app import create_app,"
},
{
"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": 1937,
"preview": "import re\nimport unittest\nfrom flask import url_for\nfrom app import create_app, db\nfrom app.models import User, Role\n\ncl"
},
{
"path": "tests/test_selenium.py",
"chars": 2977,
"preview": "import re\nimport threading\nimport time\nimport unittest\nfrom selenium import webdriver\nfrom app import create_app, db\nfro"
},
{
"path": "tests/test_user_model.py",
"chars": 7301,
"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-first-edition GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 78 files (109.8 KB), approximately 28.1k tokens, and a symbol index with 208 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.