Skip to content

Commit

Permalink
Chapter 13: Blog post comments (13a)
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelgrinberg committed Oct 1, 2015
1 parent b4b18ad commit aa10857
Show file tree
Hide file tree
Showing 12 changed files with 168 additions and 12 deletions.
5 changes: 5 additions & 0 deletions app/main/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,8 @@ def validate_username(self, field):
class PostForm(Form):
body = PageDownField("What's on your mind?", validators=[Required()])
submit = SubmitField('Submit')


class CommentForm(Form):
body = StringField('Enter your comment', validators=[Required()])
submit = SubmitField('Submit')
26 changes: 22 additions & 4 deletions app/main/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
current_app, make_response
from flask.ext.login import login_required, current_user
from . import main
from .forms import EditProfileForm, EditProfileAdminForm, PostForm
from .forms import EditProfileForm, EditProfileAdminForm, PostForm,\
CommentForm
from .. import db
from ..models import Permission, Role, User, Post
from ..models import Permission, Role, User, Post, Comment
from ..decorators import admin_required, permission_required


Expand Down Expand Up @@ -89,10 +90,27 @@ def edit_profile_admin(id):
return render_template('edit_profile.html', form=form, user=user)


@main.route('/post/<int:id>')
@main.route('/post/<int:id>', methods=['GET', 'POST'])
def post(id):
post = Post.query.get_or_404(id)
return render_template('post.html', posts=[post])
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'])
Expand Down
23 changes: 23 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class User(UserMixin, db.Model):
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):
Expand Down Expand Up @@ -263,6 +264,7 @@ class Post(db.Model):
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):
Expand All @@ -289,3 +291,24 @@ def on_changed_body(target, value, oldvalue, initiator):
tags=allowed_tags, strip=True))

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))

db.event.listen(Comment.body, 'set', Comment.on_changed_body)
32 changes: 32 additions & 0 deletions app/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,38 @@ div.post-content {
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;
Expand Down
22 changes: 22 additions & 0 deletions app/templates/_comments.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<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.body_html %}
{{ comment.body_html | safe }}
{% else %}
{{ comment.body }}
{% endif %}
</div>
</div>
</li>
{% endfor %}
</ul>
10 changes: 5 additions & 5 deletions app/templates/_macros.html
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
{% macro pagination_widget(pagination, endpoint) %}
{% 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) }}{% else %}#{% endif %}">
<a href="{% if pagination.has_prev %}{{ url_for(endpoint, page=pagination.prev_num, **kwargs) }}{{ fragment }}{% else %}#{% endif %}">
&laquo;
</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>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}">{{ p }}</a>
</li>
{% else %}
<li>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}">{{ p }}</a>
</li>
{% endif %}
{% else %}
<li class="disabled"><a href="#">&hellip;</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 href="{% if pagination.has_next %}{{ url_for(endpoint, page=pagination.next_num, **kwargs) }}{{ fragment }}{% else %}#{% endif %}">
&raquo;
</a>
</li>
Expand Down
3 changes: 3 additions & 0 deletions app/templates/_posts.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
<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>
Expand Down
13 changes: 13 additions & 0 deletions app/templates/post.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
{% 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 %}
2 changes: 1 addition & 1 deletion app/templates/user.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ <h1>{{ user.username }}</h1>
{% 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.</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) %}
Expand Down
1 change: 1 addition & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Config:
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')
FLASKY_POSTS_PER_PAGE = 20
FLASKY_FOLLOWERS_PER_PAGE = 50
FLASKY_COMMENTS_PER_PAGE = 30

@staticmethod
def init_app(app):
Expand Down
4 changes: 2 additions & 2 deletions manage.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python
import os
from app import create_app, db
from app.models import User, Follow, Role, Permission, Post
from app.models import User, Follow, Role, Permission, Post, Comment
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand

Expand All @@ -12,7 +12,7 @@

def make_shell_context():
return dict(app=app, db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post)
Permission=Permission, Post=Post, Comment=Comment)
manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

Expand Down
39 changes: 39 additions & 0 deletions migrations/versions/51f5ccfba190_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""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 ###

0 comments on commit aa10857

Please sign in to comment.