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 Mar 4, 2020
1 parent 156770a commit a6f626f
Show file tree
Hide file tree
Showing 12 changed files with 169 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 @@ -54,3 +54,8 @@ def validate_username(self, field):
class PostForm(FlaskForm):
body = PageDownField("What's on your mind?", validators=[DataRequired()])
submit = SubmitField('Submit')


class CommentForm(FlaskForm):
body = StringField('Enter your comment', validators=[DataRequired()])
submit = SubmitField('Submit')
27 changes: 23 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_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 @@ -91,10 +92,28 @@ 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())

This comment has been minimized.

Copy link
@ezebunandu

ezebunandu Oct 15, 2020

Should the arguments passed to the Comment class here not be the same as those we defined in the class constructor (i.e. post_id, and author_id)?

This comment has been minimized.

Copy link
@miguelgrinberg

miguelgrinberg Oct 15, 2020

Author Owner

You can do that if you like, but with SQLAlchemy you can also initialize an object through its relationships.

db.session.add(comment)
db.session.commit()
flash('Your comment has been published.')
return redirect(url_for('.post', id=post.id, page=-1))
page = request.args.get('page', 1, type=int)
if page == -1:
page = (post.comments.count() - 1) // \
current_app.config['FLASKY_COMMENTS_PER_PAGE'] + 1
pagination = post.comments.order_by(Comment.timestamp.asc()).paginate(
page, 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 @@ -104,6 +104,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 add_self_follows():
Expand Down Expand Up @@ -264,6 +265,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 on_changed_body(target, value, oldvalue, initiator):
Expand All @@ -275,3 +277,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)

This comment has been minimized.

Copy link
@ezebunandu

ezebunandu Oct 15, 2020

I am getting an AttributeError here, with the following message: Neither 'ColumnClause' object nor 'Comparator' object has an attribute 'dispatch' I thought maybe it was related to how we constructed the Comment in the post route, but I have tried changing those but error still persists.

This comment has been minimized.

Copy link
@miguelgrinberg

miguelgrinberg Oct 15, 2020

Author Owner

This is usually caused by a typo. One that a lot of people make is to define a column with db.column instead of db.Column. The C in column should be uppercase. Compare your code against mine if you need help in finding the typo.

This comment has been minimized.

Copy link
@ezebunandu

ezebunandu Oct 15, 2020

This absolutely was the case!

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 @@ -16,6 +16,7 @@ class Config:
SQLALCHEMY_TRACK_MODIFICATIONS = False
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 flasky.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import click
from flask_migrate import Migrate
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

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
migrate = Migrate(app, db)
Expand All @@ -11,7 +11,7 @@
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post)
Permission=Permission, Post=Post, Comment=Comment)


@app.cli.command()
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 ###

1 comment on commit a6f626f

@ogahozy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is helpful. I will ise it to update my post

Please sign in to comment.