permissions may be the right approach# app/models.py
#...
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(64), unique=True)
default = db.Column(db.Boolean, default=False, index=True)
permissions = db.Column(db.Integer)
users = db.relationship('User', backref='role', lazy='dynamic')
def __repr__(self):
return '<Role %r>' % self.name
#...
True for only one role and False for all the others| Task name | Bit value | Description |
|---|---|---|
| Follow users | 0b00000001 (0x01) | Follow other users |
| Comment on posts made by others | 0b00000010 (0x02) | Comment on articles written by others |
| Write articles | 0b00000100 (0x04) | Write original articles |
| Moderate comments made by others | 0b00001000 (0x08) | Suppress offensive comments made by others |
| Administration access | 0b10000000 (0x80) | Administrative access to the site |
# app/models.py
#...
class Permission:
FOLLOW = 0x01
COMMENT = 0x02
WRITE_ARTICLES = 0x04
MODERATE_COMMENTS = 0x08
ADMINISTER = 0x80
#...
| User role | Permissions | Description |
|---|---|---|
| Anonymous | 0b00000000 (0x00) | User who is not logged in. Read-only access to the application. |
| User | 0b00000111 (0x07) | Basic permissions to write articles and comments and to follow other users. This is the default for new users. |
| Moderator | 0b00001111 (0x0f) | Adds permission to suppress comments deemed offensive or inappropriate. |
| Administrator | 0b11111111 (0xff) | Full access, which includes permission to change the roles of other users. |
insert_roles() to the Role class to create roles in the database# app/models.py
#...
class Role(db.Model):
#...
@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()
roles dictionary inside the insert_roles() and rerun the function¶(venv) $ python manage.py db upgrade
(venv) $ python manage.py shell
>>> Role.insert_roles()
>>> Role.query.all()
[<Role 'Administrator'>, <Role 'User'>, <Role 'Moderator'>]

FLASKY_ADMIN configuration variable.administrator or default roles depending on the email address.# app/models.py
#...
class User(UserMixin, db.Model):
#...
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()
else:
self.role = Role.query.filter_by(default=True).first()
#...
# app/models.py
# ...
class User(UserMixin, db.Model):
# ...
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)
current_user.can() and current_user.is_administrator() without having to check whether the user is logged in first.AnonymousUserMixin class and is registered to current_user when the user is not logged in.AnonymousUser class that implements the can() and is_administrator() methods.# app/models.py
#...
from flask_login import UserMixin, AnonymousUserMixin
#...
class AnonymousUser(AnonymousUserMixin):
def can(self, permissions):
return False
def is_administrator(self):
return False
login_manager.anonymous_user = AnonymousUser
Implement two customized decorators to check user permissions
Note: If you are unfamiliar with decorator, please refer to http://tw.pyladies.com/~maomao/6_effective_python.slides.html#/1.
# 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)
<!-- app/templates/403.html -->
{% extends "base.html" %}
{% block title %}Flasky - Forbidden{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Forbidden</h1>
</div>
{% endblock %}
# app/main/views.py
from flask import render_template
from flask_login import login_required
from . import main
from ..decorators import admin_required, permission_required
from ..models import Permission
@main.route('/')
def index():
return render_template('index.html')
@main.route('/admin')
@login_required
@admin_required
def for_admins_only():
return "For administrators!"
@main.route('/moderator')
@login_required
@permission_required(Permission.MODERATE_COMMENTS)
def for_moderators_only():
return "For comment moderators!"
user, moderator and administrator in the database¶role to your old data¶(venv) $ python manage.py shell
>>> users = User.query.all()
>>> for u in users:
... u.role = Role.query.filter_by(default=True).first()
... db.session.add(u)
>>> db.session.commit()

(venv) $ python manage.py shell
>>> u = User(username="newuser", email="newuser@pyladies.com", password="123456", confirmed=True)
>>> db.session.add(u)
>>> db.session.commit()

moderator role in the database¶>>> u = User(username="alicia", email="alicia@pyladies.com", password="123456", confirmed=True, role=Role.query.filter_by(name="Moderator").first())
>>> db.session.add(u)
>>> db.session.commit()
administrator's email as an environment variable.¶Mac
(venv) $ export FLASKY_ADMIN="admin@pyladies.com"
Microsoft
(venv) $ set FLASKY_ADMIN="admin@pyladies.com"
administrator role in the database¶(venv) $ python manage.py shell
>>> u = User(username="admin", email="admin@pyladies.com", password="123456", confirmed=True)
>>> db.session.add(u)
>>> db.session.commit()

(venv) $ python manage.py runserver



Permission globally available to all templates.¶render_template() call, add the Permission class to the template context by a context processor.# app/main/__init__.py
#...
from ..models import Permission
#...
@main.app_context_processor
def inject_permissions():
return dict(Permission=Permission)
# tests/test_user_model.py
#...
from app.models import User, Role, Permission, AnonymousUser
class UserModelTestCase(unittest.TestCase):
#...
def test_roles_and_permissions(self):
Role.insert_roles()
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))

$ git clone https://github.com/win911/flask_class.git$ git checkout 9a$ python manage.py db upgrade# app/models.py
from datetime import datetime
#...
class User(UserMixin, db.Model):
#...
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)
#...
db.string: a string with a maximum length (optional in some databases, e.g. PostgreSQL)db.Text: some longer unicode text, does not need a maximum lengthdatetime.utcnow is missing the () at the end¶default argument to db.Column() can take a function as a default valuelast_seen field is initialized to the current time upon creation.# app/models.py
from datetime import datetime
#...
class User(UserMixin, db.Model):
#...
def ping(self):
self.last_seen = datetime.utcnow()
db.session.add(self)
ping() method must be called each time a request from the user is received.before_app_request handler in the auth blueprint runs before every request.last_seen field.# app/auth/views.py
#...
@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'))
#...
(venv) $ python manage.py db upgrade
# app/main/views.py
from flask import render_template, abort
#...
#...
from ..models import Permission, User
#...
@main.route('/user/<username>')
def user_profile(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
return render_template('user.html', user=user)
<!-- app/templates/user.html -->
{% extends "base.html" %}
{% block title %}Flasky - {{ user.username }}{% endblock %}
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>{{ user.username }}</h1>
{% if user.name or user.location %}
<p>
{% if user.name %}{{ user.name }}{% endif %}
{% if user.location %}
From <a href="http://maps.google.com/?q={{ user.location }}">
{{ user.location }}
</a>
{% 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>
</div>
{% endblock %}
base.html.is_authenticated) is necessary because the navigation bar is also rendered for non-authenticated users.<!-- app/templates/base.html -->
...
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
<div class="container">
...
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
...
{% if current_user.is_authenticated %}
<li><a href="{{ url_for('main.user_profile', username=current_user.username) }}">Profile</a></li>
{% endif %}
</ul>
...

location and about_me information¶(venv) $ python manage.py shell
>>> u = User(username='newuser2', email='newuser2@pyladies.com', password='123456', confirmed=True, name='New User 2', location='Taipei', about_me='member of pyladies')
>>> db.session.add(u)
>>> db.session.commit()


$ git clone https://github.com/win911/flask_class.git$ git checkout 10a$ python manage.py db upgrade# app/main/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import Required, Length
# ...
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')
# app/main/views.py
from flask import render_template, abort, flash, redirect, url_for
from flask_login import login_required, current_user
#...
from .forms import EditProfileForm
from .. import db
#...
@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_profile', 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)
<!-- 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 %}
<!-- app/templates/user.html -->
...
{% block page_content %}
...
<p>
Member since {{ moment(user.member_since).format('L') }}.
Last seen {{ moment(user.last_seen).fromNow() }}.
</p>
<p>
{% if user == current_user %}
<a class="btn btn-default" href="{{ url_for('.edit_profile') }}">
Edit Profile
</a>
{% endif %}
</p>
</div>
{% endblock %}


$ git clone https://github.com/win911/flask_class.git$ git checkout 10b# app/main/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField, BooleanField, SelectField
from wtforms.validators import Required, Length, Email, Regexp
from wtforms import ValidationError
from ..models import Role, User
#...
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')
<select> HTML form control, which implements a dropdown listchoices attributecoerce=int argument is added to the SelectField constructor¶# app/main/forms.py
#...
class EditProfileAdminForm(FlaskForm):
#...
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.')
email and username fields requires some careful handling¶id, so Flask-SQLALchemy's get_or_404() convenience function is used.id is invalid, the request will return a code 404 error.role field, the role_id is used because the choices attribute uses the numeric identifiers.# app/main/views.py
#...
from .forms import EditProfileForm, EditProfileAdminForm
from ..models import Permission, User, Role
#...
@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_profile', 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)
<!-- app/templates/user.html -->
...
{% block page_content %}
...
<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>
{% endblock %}

$ git clone https://github.com/win911/flask_class.git$ git checkout 10c
Edit User link for admin¶


/edit_profile/choose-user¶# app/main/views.py
# ...
@main.route('/edit-profile/choose-user', methods=['GET', 'POST'])
@login_required
@admin_required
def choose_user():
users = User.query.all()
return render_template('choose_user.html', users=users)
choose_user.html¶<!-- app/templates/choose_user.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block title %}Flasky - Choose User{% endblock %}
{% block page_content %}
<div class="page-header">
<h1>Choose which user to edit</h1>
</div>
<ul class="posts">
{% for user in users %}
<li>
<a href="{{ url_for('.user_profile', username=user.username) }}">{{ user.username }}</a>
{% endfor %}
</ul>
{% endblock %}
Edit User¶<!-- app/templates/base.html -->
...
{% block navbar %}
<div class="navbar navbar-inverse" role="navigation">
...
<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_profile', username=current_user.username) }}">Profile</a></li>
{% endif %}
{% if current_user.is_administrator() %}
<li><a href="{{ url_for('main.choose_user') }}">Edit User</a></li>
{% endif %}
</ul>
...
edit-profile.html to customize heading¶<!-- app/templates/edit_profile.html -->
...
{% block page_content %}
<div class="page-header">
{% if user == current_user %}
<h1>Edit Your Profile</h1>
{% else %}
<h1>Edit {{ user.username }}'s Profile</h1>
{% endif %}
</div>
...
# app/main/views.py
# ...
@main.route('/edit-profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
# ...
return render_template('edit_profile.html', form=form, user=current_user)
@main.route('/edit-profile/<int:id>', methods=['GET', 'POST'])
@login_required
@admin_required
def edit_profile_admin(id):
# ...
return render_template('edit_profile.html', form=form, user=user)
$ git clone https://github.com/win911/flask_class.git$ git checkout 10d(venv) $ python
>>> import hashlib
>>> hashlib.md5('john@example.com'.encode('utf-8')).hexdigest()
'd4c74594d841139328695756648b6bd6'

http:// www.gravatar.com/avatar/ or https://secure.gravatar.com/avatar/.john@example.com: http://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6| Argument name | Description |
|---|---|
| s | Image size, in pixels. |
| r | Image rating. Options are "g", "pg", "r", and "x". |
| d | The default image generator for users who have no avatars registered with the Gravatar service. Options are "404"to return a 404 error,a URL that points to a default image, or one of the following image generators: "mm", "identicon", "monsterid", "wavatar", "retro", or "blank". |
| fd | Force the use of default avatars. |
User model¶# app/models.py
#...
import hashlib
from flask import current_app, request
#...
class User(UserMixin, db.Model):
#...
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_value = hashlib.md5(self.email.encode('utf-8')).hexdigest()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
url=url, hash=hash_value, size=size, default=default, rating=rating)
(venv) $ python manage.py shell
>>> u = User(email='john@example.com')
>>> u.gravatar()
'http://www.gravatar.com/avatar/d4c74594d84113932869575bd6?s=100&d=identicon&r=g'
>>> u.gravatar(size=256)
'http://www.gravatar.com/avatar/d4c74594d84113932869575bd6?s=256&d=identicon&r=g'

<!-- app/tempaltes/user.html -->
...
{% 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>
...
</div>
</div>
{% endblock %}

<!-- app/templates/base.html -->
...
{% block head %}
{{ super() }}
...
<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="navbar-collapse collapse">
...
<ul class="nav navbar-nav navbar-right">
{% 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>
...
app/static/styles.css¶/* app/static/styles.css */
.profile-thumbnail {
position: absolute;
}
.profile-header {
min-height: 260px;
margin-left: 280px;
}

# app/models.py
#...
class User(UserMixin, db.Model):
#...
avatar_hash = db.Column(db.String(32))
def __init__(self, **kwargs):
#...
if self.email is not None and self.avatar_hash is None:
self.avatar_hash = self.gravatar_hash()
def change_email(self, token):
#...
self.email = new_email
self.avatar_hash = self.gravatar_hash()
db.session.add(self)
return True
#...
# app/models.py
#...
class User(UserMixin, db.Model):
#...
def gravatar_hash(self):
return hashlib.md5(self.email.lower().encode('utf-8')).hexdigest()
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_value = self.avatar_hash or self.gravatar_hash()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
url=url, hash=hash_value, size=size, default=default, rating=rating)
(venv) $ python manage.py db upgrade



$ git clone https://github.com/win911/flask_class.git$ git checkout 10e$ python manage.py db upgradePost model to represent blog posts¶Post model has a one-to-many relationship from the User model.body field is defined with type db.Text, which has no limitation on the length.# app/models.py
#...
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
class User(UserMixin, db.Model):
#...
posts = db.relationship('Post', backref='author', lazy='dynamic')
(venv) $ python manage.py db upgrade
# app/main/forms.py
#...
class PostForm(FlaskForm):
body = TextAreaField("What's on your mind?", validators=[Required()])
submit = SubmitField('Submit')
index() view function handles the blog post form and passes the list of old blog posts to the template.current_user variable from Flask-Login is a wrapper that contains the acutal user object inside._get_current_object().# app/main/views.py
from .forms import EditProfileForm, EditProfileAdminForm, PostForm
from ..models import Permission, Role, User, Post
#...
@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'))
posts = Post.query.order_by(Post.timestamp.desc()).all()
return render_template('index.html', form=form, posts=posts)
index.html template.User.can() is used to skip the blog post form for users who do not have the WRITE_ARTICLES permission.styles.css file in the app/static folder.<!-- app/tempaltes/index.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% 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>
...
<!-- app/tempaltes/index.html -->
...
<ul class="posts">
{% for post in posts %}
<li class="post">
<div class="profile-thumbnail">
<a href="{{ url_for('.user_profile', 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_profile', username=post.author.username) }}">{{ post.author.username }}</a></div>
<div class="post-body">{{ post.body }}</div>
</div>
</li>
{% endfor %}
</ul>
{% endblock %}
/* app/static/styles.css */
...
ul.posts {
list-style-type: none;
padding: 0px;
margin: 16px 0px 0px 0px;
border-top: 1px solid #e0e0e0;
}
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;
}

$ git clone https://github.com/win911/flask_class.git$ git checkout 11a$ python manage.py db upgradeUser.posts relationship.# app/main/views.py
#...
@main.route('/user/<username>')
def user_profile(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
posts = user.posts.order_by(Post.timestamp.desc()).all()
return render_template('user.html', user=user, posts=posts)
user.html renders a list of blog posts like the one in index.html._posts.html and use Jinja2's include() to includes the list from an external file._post.html template name is not a requirment.<!-- app/tempaltes/_post.html -->
<ul class="posts">
{% for post in posts %}
<li class="post">
<div class="post-thumbnail">
<a href="{{ url_for('.user_profile', 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_profile', username=post.author.username) }}">{{ post.author.username }}</a></div>
<div class="post-body">{{ post.body }}</div>
</div>
</li>
{% endfor %}
</ul>
index.html¶_post.html with include().<!-- app/tempaltes/index.html -->
...
{% include '_posts.html' %}
{% endblock %}
user.html¶<!-- app/tempaltes/user.html -->
...
<h3>Posts by {{ user.username }}</h3>
{% include '_posts.html' %}
{% endblock %}

Paginate the data and render it in chunks.ForgeryPy package to generate fake data automatically.(venv) $ pip install forgerypy
requirements.txt file can be replaced with a requirements folder.dev.txt file list the dependencies that are necessary for development.prod.txt file list the dependencies that are needed in production.common.txt file list the common dependencies in both stages.dev.txt and prod.txt use the -r prefix to include the common.txt.alembic==0.9.5
blinker==1.4
click==6.7
dominate==2.3.1
Flask==0.12.2
Flask-Bootstrap==3.3.7.1
Flask-JsonSchema==0.1.1
Flask-Login==0.4.0
Flask-Mail==0.9.1
Flask-Migrate==2.1.1
Flask-Moment==0.5.1
Flask-Script==2.0.5
Flask-SQLAlchemy==2.2
Flask-WTF==0.14.2
itsdangerous==0.24
Jinja2==2.9.6
jsonschema==2.6.0
Mako==1.0.7
MarkupSafe==1.0
python-dateutil==2.6.1
python-editor==1.0.3
six==1.11.0
SQLAlchemy==1.1.14
visitor==0.1.3
Werkzeug==0.12.2
WTForms==2.1
-r common.txt
-r common.txt
ForgeryPy==0.1
User and Post models that can generate fake data.IntegrityError exception.# app/models.py
class User(UserMixin, db.Model):
#...
@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()
offset() query filter.offset() filter skips the number of results we generate from the randint() function.first().# app/models.py
class Post(db.Model):
#...
@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, 3)),
timestamp=forgery_py.date.date(True),
author=u)
db.session.add(p)
db.session.commit()
Post to the shell context¶# manage.py
#...
from app.models import User, Role, Post
#...
def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role, Post=Post)
#...
(venv) $ python manage.py shell
>>> User.generate_fake(100)
>>> Post.generate_fake(100)


request.args.type=int ensures that if it is not an integer, the default is used.all() with Flask-SQLAlchemy's paginate().paginate() method takes the page number as the first and only requried argument.per_page argument can be given, the default is 20.error_out=True will issue a 404 error code.# app/main/views.py
from flask import render_template, redirect, url_for, abort, flash, request, current_app
#...
@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)
pagination = Post.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, pagination=pagination)
# app/main/views.py
#...
@main.route('/user/<username>')
def user_profile(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(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)
FLASKY_POSTS_PER_PAGE to config¶# config.py
#...
class Config(object):
#...
FLASKY_POSTS_PER_PAGE = 20
#...
paginate() is an object of class Pagination.| Attribute | Description |
|---|---|
| items | The records in the current page |
| query | The source query that was paginated |
| page | The current page number |
| prev_num | The previous page number |
| next_num | The next page number |
| has_next | True if there is a next page |
| has_prev | True if there is a previous page |
| pages | The total number of pages for the query |
| per_page | The number of items per page |
| total | The total number of items returned by the query |
iter_pages(left_edge=2, left_current=2, right_current=5, right_edge=2)¶left_edge pages on the left side, left_current pages to the left of the current page, right_current pages to the right of the current page, and right_edge pages on the right side. None, 48, 49, 50, 51, 52, 53, 54, 55, None, 99, 100. None value in the sequence indicates a gap in the sequence of pages.prev()¶next()¶iter_pages() iterator.url_for()....<!-- app/tempaltes/_macros.html -->
{% macro pagination_widget(pagination, endpoint) %}
<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) }}{% 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) }}">{{ p }}</a>
</li>
{% else %}
<li>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ 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) }}{% else %}#{% endif %}">
»
</a>
</li>
</ul>
{% endmacro %}
index.html and user.html¶pagination_widget macro below the _post.html template.<!-- app/tempaltes/index.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
...
{% include '_posts.html' %}
{% if pagination %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.index') }}
</div>
{% endif %}
{% endblock %}
<!-- app/templates/user.html -->
{% extends "base.html" %}
{% import "_macros.html" as macros %}
...
<h3>Posts by {{ user.username }}</h3>
{% include '_posts.html' %}
{% if pagination %}
<div class="pagination">
{{ macros.pagination_widget(pagination, '.user_profile', username=user.username) }}
</div>
{% endif %}
{% endblock %}


$ git clone https://github.com/win911/flask_class.git$ git checkout 11b(venv) $ pip install flask-pagedown markdown bleach
PageDownField class that has the same interface as the TextAreaField from WTForms.# app/__init__.py
#...
from flask_pagedown import PageDown
#...
pagedown = PageDown()
#...
def create_app(config_name):
#...
pagedown.init_app(app)
#...
body field of the PostForm to a PageDownField.# app/main/forms.py
from flask_pagedown.fields import PageDownField
#...
class PostForm(FlaskForm):
body = PageDownField("What's on your mind?", validators=[Required()])
submit = SubmitField('Submit')
PageDown libraries.<!-- app/tempaltes/index.html -->
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %}
{% block scripts %}
{{ super() }}
{{ pagedown.include_pagedown() }}
{% endblock %}
{% block title %}Flasky{% endblock %}
...

POST request.Post model.Post so the post can be edited later.Post model¶# app/models.py
from markdown import markdown
import bleach
#...
class Post(db.Model):
#...
body_html = db.Column(db.Text)
#...
@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))
db.event.listen(Post.body, 'set', Post.on_changed_body)
post.body with post.body_html¶| safe to tell Jinja 2 not to escapte the HTML elements.<!-- app/tempaltes/_posts.html -->
...
<div class="post-body">
{% if post.body_html %}
{{ post.body_html | safe }}
{% else %}
{{ post.body }}
{% endif %}
</div>
...
(venv) $ python manage.py db upgrade

$ git clone https://github.com/win911/flask_class.git$ git checkout 11c$ python manage.py db upgradeid field assigned when the post is inserted in the database.post.html template receives a list with just the post to render._posts.html template here as well.# app/main/views.py
#...
@main.route('/post/<int:id>')
def post(id):
post = Post.query.get_or_404(id)
return render_template('post.html', posts=[post])
<!-- app/tempaltes/post.html -->
{% extends "base.html" %}
{% import "_macros.html" as macros %}
{% block title %}Flasky - Post{% endblock %}
{% block page_content %}
{% include '_posts.html' %}
{% endblock %}

_post.html¶<!-- app/tempaltes/_posts.html -->
<ul class="posts">
...
<div class="post-content">
...
<div class="post-footer">
<a href="{{ url_for('.post', id=post.id) }}">
<span class="label label-default">Permalink</span>
</a>
</div>
</div>
</li>
{% endfor %}
</ul>


$ git clone https://github.com/win911/flask_class.git$ git checkout 11d<!-- app/tempaltes/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 %}
# app/main/views.py
#...
@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)
db.session.commit()
flash('The post has been updated.')
return redirect(url_for('.post', id=post.id))
form.body.data = post.body
return render_template('edit_post.html', form=form)
<!-- app/tempaltes/_posts.html -->
<ul class="posts">
...
<div class="post-content">
...
<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>
</div>
</div>
</li>
{% endfor %}
</ul>




$ git clone https://github.com/win911/flask_class.git$ git checkout 11e