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 @@ +
    + {% for comment in comments %} +
  • +
    + + + +
    +
    +
    {{ moment(comment.timestamp).fromNow() }}
    + +
    + {% if comment.body_html %} + {{ comment.body_html | safe }} + {% else %} + {{ comment.body }} + {% endif %} +
    +
    +
  • + {% endfor %} +
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='') %}
    - + « @@ -9,11 +9,11 @@ {% if p %} {% if p == pagination.page %}
  • - {{ p }} + {{ p }}
  • {% else %}
  • - {{ p }} + {{ p }}
  • {% endif %} {% else %} @@ -21,7 +21,7 @@ {% endif %} {% endfor %} - + » diff --git a/app/templates/_posts.html b/app/templates/_posts.html index fc54bd8..8120c4f 100644 --- a/app/templates/_posts.html +++ b/app/templates/_posts.html @@ -29,6 +29,9 @@ Permalink + + {{ post.comments.count() }} Comments + diff --git a/app/templates/post.html b/app/templates/post.html index e4cb256..e020603 100644 --- a/app/templates/post.html +++ b/app/templates/post.html @@ -1,8 +1,21 @@ {% extends "base.html" %} +{% import "bootstrap/wtf.html" as wtf %} {% import "_macros.html" as macros %} {% block title %}Flasky{% endblock %} {% block page_content %} {% include '_posts.html' %} +

    Comments

    +{% if current_user.can(Permission.COMMENT) %} +
    + {{ wtf.quick_form(form) }} +
    +{% endif %} +{% include '_comments.html' %} +{% if pagination %} + + {% endif %} {% endblock %} diff --git a/app/templates/user.html b/app/templates/user.html index f692686..d519bfe 100644 --- a/app/templates/user.html +++ b/app/templates/user.html @@ -27,7 +27,10 @@

    Member since {{ moment(user.member_since).format('L') }}. Last seen {{ moment(user.last_seen).fromNow() }}. -

    {{ user.posts.count() }} blog post{% if user.posts.count() > 1 %}s{% endif %}.

    +

    + {{ user.posts.count() }} blog post{% if user.posts.count() != 1 %}s{% endif %}. + {{ user.comments.count() }} comment{% if user.comments.count() != 1 %}s{% endif %}. +

    {% 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 ###