Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump flask from 0.12.2 to 1.0 in /requirements #449

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4603119
Chapter 11: Blog post pagination (11d)
miguelgrinberg Jul 18, 2017
381c71e
Chapter 11: Rich text blog posts with Flask-PageDown (11e)
miguelgrinberg Jul 18, 2017
98ff259
Chapter 11: Rich text server side handling with Markdown and Bleach (…
miguelgrinberg Jul 18, 2017
9bb9b51
Chapter 11: Permanent links to posts (11g)
miguelgrinberg Jul 18, 2017
3e5f075
Chapter 11: Blog post editor (11h)
miguelgrinberg Jul 18, 2017
89e41df
Chapter 12: Database representaton of followers (12a)
miguelgrinberg Jul 18, 2017
7ae2857
Chapter 12: Followers in the application (12b)
miguelgrinberg Jul 18, 2017
a677913
Chapter 12: Followed posts with a join (12c)
miguelgrinberg Jul 18, 2017
bc7b932
Chapter 12: Show followed blog posts in home page (12d)
miguelgrinberg Jul 18, 2017
ed585f9
Chapter 12: Self-followers (12e)
miguelgrinberg Jul 18, 2017
85ed2ec
Chapter 13: Blog post comments (13a)
miguelgrinberg Jul 18, 2017
226f5f7
Chapter 13: Comment moderation (13b)
miguelgrinberg Jul 18, 2017
ef36924
Chapter 14: API (14a)
miguelgrinberg Jul 18, 2017
7d2e6b6
Chapter 15: Coverage metrics (15a)
miguelgrinberg Jul 18, 2017
97904e0
Chapter 15: Unit tests with the Flask test client (15b)
miguelgrinberg Jul 18, 2017
ec4af47
Chapter 15: API testing with the Flask test client (15c)
miguelgrinberg Jul 18, 2017
50bd047
Chapter 15: Unit tests with Selenium (15d)
miguelgrinberg Jul 18, 2017
abf6d4d
Chapter 16: Logging of slow database queries (16a)
miguelgrinberg Jul 18, 2017
d018373
Chapter 16: Source code profiling (16b)
miguelgrinberg Jul 18, 2017
7e7e564
Chapter 17: Deploy command (17a)
miguelgrinberg Jul 18, 2017
c4bb787
Chapter 17: Email notification of application errors (17b)
miguelgrinberg Jul 18, 2017
9a99ab8
Chapter 17: Heroku support with Waitress (17c-waitress)
miguelgrinberg Jul 18, 2017
3b12e26
Chapter 17: Heroku support with Gunicorn (17c)
miguelgrinberg Aug 31, 2017
2249355
Chapter 17: Docker support (17d)
miguelgrinberg Aug 5, 2017
521382b
Chapter 17: MySQL support for Docker (17e)
miguelgrinberg Aug 12, 2017
63d88c4
Chapter 17: Docker Compose support (17f)
miguelgrinberg Aug 25, 2017
f346e38
Chapter 17: Traditional hosting (17g)
miguelgrinberg Jul 18, 2017
574e624
Chapter 14: API (14a)
dependabot[bot] Apr 29, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ nosetests.xml

# Virtual environment
venv

# Environment files
.env
.env-mysql
21 changes: 21 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
FROM python:3.6-alpine

ENV FLASK_APP flasky.py
ENV FLASK_CONFIG production

RUN adduser -D flasky
USER flasky

WORKDIR /home/flasky

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

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

# run-time configuration
EXPOSE 5000
ENTRYPOINT ["./boot.sh"]
1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: gunicorn flasky:app
10 changes: 10 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_pagedown import PageDown
from config import config

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

login_manager = LoginManager()
login_manager.login_view = 'auth.login'
Expand All @@ -25,11 +27,19 @@ def create_app(config_name):
moment.init_app(app)
db.init_app(app)
login_manager.init_app(app)
pagedown.init_app(app)

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

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

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

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

return app
5 changes: 5 additions & 0 deletions app/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flask import Blueprint

api = Blueprint('api', __name__)

from . import authentication, posts, users, comments, errors
44 changes: 44 additions & 0 deletions app/api/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from flask import g, jsonify
from flask_httpauth import HTTPBasicAuth
from ..models import User
from . import api
from .errors import unauthorized, forbidden

auth = HTTPBasicAuth()


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


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


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


@api.route('/tokens/', methods=['POST'])
def get_token():
if g.current_user.is_anonymous or g.token_used:
return unauthorized('Invalid credentials')
return jsonify({'token': g.current_user.generate_auth_token(
expiration=3600), 'expiration': 3600})
67 changes: 67 additions & 0 deletions app/api/comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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)
next = None
if pagination.has_next:
next = url_for('api.get_comments', page=page+1)
return jsonify({
'comments': [comment.to_json() for comment in comments],
'prev': prev,
'next': next,
'count': pagination.total
})


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


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


@api.route('/posts/<int:id>/comments/', methods=['POST'])
@permission_required(Permission.COMMENT)
def new_post_comment(id):
post = Post.query.get_or_404(id)
comment = Comment.from_json(request.json)
comment.author = g.current_user
comment.post = post
db.session.add(comment)
db.session.commit()
return jsonify(comment.to_json()), 201, \
{'Location': url_for('api.get_comment', id=comment.id)}
14 changes: 14 additions & 0 deletions app/api/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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
26 changes: 26 additions & 0 deletions app/api/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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])
57 changes: 57 additions & 0 deletions app/api/posts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from flask import jsonify, request, g, url_for, current_app
from .. import db
from ..models import Post, Permission
from . import api
from .decorators import permission_required
from .errors import forbidden


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


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


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


@api.route('/posts/<int:id>', methods=['PUT'])
@permission_required(Permission.WRITE)
def edit_post(id):
post = Post.query.get_or_404(id)
if g.current_user != post.author and \
not g.current_user.can(Permission.ADMIN):
return forbidden('Insufficient permissions')
post.body = request.json.get('body', post.body)
db.session.add(post)
db.session.commit()
return jsonify(post.to_json())
53 changes: 53 additions & 0 deletions app/api/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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)
next = None
if pagination.has_next:
next = url_for('api.get_user_posts', id=id, page=page+1)
return jsonify({
'posts': [post.to_json() for post in posts],
'prev': prev,
'next': next,
'count': pagination.total
})


@api.route('/users/<int:id>/timeline/')
def get_user_followed_posts(id):
user = User.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
pagination = user.followed_posts.order_by(Post.timestamp.desc()).paginate(
page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'],
error_out=False)
posts = pagination.items
prev = None
if pagination.has_prev:
prev = url_for('api.get_user_followed_posts', id=id, page=page-1)
next = None
if pagination.has_next:
next = url_for('api.get_user_followed_posts', id=id, page=page+1)
return jsonify({
'posts': [post.to_json() for post in posts],
'prev': prev,
'next': next,
'count': pagination.total
})
2 changes: 2 additions & 0 deletions app/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class ValidationError(ValueError):
pass
17 changes: 16 additions & 1 deletion app/main/errors.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
from flask import render_template
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
8 changes: 7 additions & 1 deletion app/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
SubmitField
from wtforms.validators import DataRequired, Length, Email, Regexp
from wtforms import ValidationError
from flask_pagedown.fields import PageDownField
from ..models import Role, User


Expand Down Expand Up @@ -51,5 +52,10 @@ def validate_username(self, field):


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


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