From 889522d9e073e30dd828c2c1050002927482d19c Mon Sep 17 00:00:00 2001
From: tmeissner
Date: Sun, 25 Nov 2018 00:35:31 +0100
Subject: [PATCH] Chapter 13: Blog post comments (13a)
---
app/main/forms.py | 5 ++++
app/main/views.py | 26 +++++++++++++++---
app/models.py | 24 ++++++++++++++++
app/static/styles.css | 32 ++++++++++++++++++++++
app/templates/_comments.html | 22 +++++++++++++++
app/templates/_macros.html | 10 +++----
app/templates/_posts.html | 3 ++
app/templates/post.html | 13 +++++++++
app/templates/user.html | 5 +++-
config.py | 1 +
flasky.py | 4 +--
migrations/versions/643e9b785c28_.py | 41 ++++++++++++++++++++++++++++
12 files changed, 174 insertions(+), 12 deletions(-)
create mode 100644 app/templates/_comments.html
create mode 100644 migrations/versions/643e9b785c28_.py
diff --git a/app/main/forms.py b/app/main/forms.py
index 454fc75..c7e71db 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -54,3 +54,8 @@ class EditProfileAdminForm(FlaskForm):
class PostForm(FlaskForm):
body = PageDownField("What's on your mind?", validators=[DataRequired()])
submit = SubmitField('Submit')
+
+
+class CommentForm(FlaskForm):
+ body = StringField('', validators=[DataRequired()])
+ submit = SubmitField('Submit')
diff --git a/app/main/views.py b/app/main/views.py
index 003dbbc..c14c365 100644
--- a/app/main/views.py
+++ b/app/main/views.py
@@ -2,9 +2,9 @@ from flask import render_template, redirect, url_for, flash, request, \
current_app, abort, 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 User, Role, Permission, Post
+from ..models import User, Role, Permission, Post, Comment
from ..decorators import admin_required, permission_required
@@ -91,10 +91,28 @@ def edit_profile_admin(id):
return render_template('edit_profile.html', form=form, user=user)
-@main.route('/post/')
+@main.route('/post/', 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)
+ 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/', methods=['GET', 'POST'])
diff --git a/app/models.py b/app/models.py
index 3cd46bb..d4002fe 100644
--- a/app/models.py
+++ b/app/models.py
@@ -105,6 +105,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')
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
@@ -251,6 +252,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):
@@ -265,6 +267,28 @@ class Post(db.Model):
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']
+ md = markdown(value, output_format='html')
+ clean_md = bleach.clean(md, tags=allowed_tags, strip=True)
+ target.body_html = bleach.linkify(clean_md)
+
+
+db.event.listen(Comment.body, 'set', Comment.on_changed_body)
+
+
class AnonymousUser(AnonymousUserMixin):
def can(self, perm):
return False
diff --git a/app/static/styles.css b/app/static/styles.css
index 4d648a5..c96681b 100644
--- a/app/static/styles.css
+++ b/app/static/styles.css
@@ -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: center;
diff --git a/app/templates/_comments.html b/app/templates/_comments.html
new file mode 100644
index 0000000..73ee65d
--- /dev/null
+++ b/app/templates/_comments.html
@@ -0,0 +1,22 @@
+
diff --git a/app/templates/_macros.html b/app/templates/_macros.html
index b5d55a3..a4789c8 100644
--- a/app/templates/_macros.html
+++ b/app/templates/_macros.html
@@ -1,7 +1,7 @@
-{% macro pagination_widget(pagination, endpoint) %}
+{% macro pagination_widget(pagination, endpoint, fragment='') %}
{% if current_user.can(Permission.FOLLOW) and user != current_user %}
diff --git a/config.py b/config.py
index 7094673..af99e58 100644
--- a/config.py
+++ b/config.py
@@ -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):
diff --git a/flasky.py b/flasky.py
index a379daf..ba0afa9 100644
--- a/flasky.py
+++ b/flasky.py
@@ -1,7 +1,7 @@
import os
from flask_migrate import Migrate
from app import create_app, db
-from app.models import User, Role, Permission, Post, Follow
+from app.models import User, Role, Permission, Post, Follow, Comment
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
@@ -11,7 +11,7 @@ migrate = Migrate(app, db)
@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()
diff --git a/migrations/versions/643e9b785c28_.py b/migrations/versions/643e9b785c28_.py
new file mode 100644
index 0000000..985b3f1
--- /dev/null
+++ b/migrations/versions/643e9b785c28_.py
@@ -0,0 +1,41 @@
+"""empty message
+
+Revision ID: 643e9b785c28
+Revises: d1ab608c102a
+Create Date: 2018-11-24 23:31:37.923248
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '643e9b785c28'
+down_revision = 'd1ab608c102a'
+branch_labels = None
+depends_on = None
+
+
+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(op.f('ix_comments_timestamp'), 'comments', ['timestamp'], unique=False)
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f('ix_comments_timestamp'), table_name='comments')
+ op.drop_table('comments')
+ # ### end Alembic commands ###