Browse Source

Chapter 13: Blog post comments (13a)

master
T. Meissner 6 years ago
parent
commit
889522d9e0
12 changed files with 174 additions and 12 deletions
  1. +5
    -0
      app/main/forms.py
  2. +22
    -4
      app/main/views.py
  3. +24
    -0
      app/models.py
  4. +32
    -0
      app/static/styles.css
  5. +22
    -0
      app/templates/_comments.html
  6. +5
    -5
      app/templates/_macros.html
  7. +3
    -0
      app/templates/_posts.html
  8. +13
    -0
      app/templates/post.html
  9. +4
    -1
      app/templates/user.html
  10. +1
    -0
      config.py
  11. +2
    -2
      flasky.py
  12. +41
    -0
      migrations/versions/643e9b785c28_.py

+ 5
- 0
app/main/forms.py View File

@ -54,3 +54,8 @@ class EditProfileAdminForm(FlaskForm):
class PostForm(FlaskForm): class PostForm(FlaskForm):
body = PageDownField("What's on your mind?", validators=[DataRequired()]) body = PageDownField("What's on your mind?", validators=[DataRequired()])
submit = SubmitField('Submit') submit = SubmitField('Submit')
class CommentForm(FlaskForm):
body = StringField('', validators=[DataRequired()])
submit = SubmitField('Submit')

+ 22
- 4
app/main/views.py View File

@ -2,9 +2,9 @@ from flask import render_template, redirect, url_for, flash, request, \
current_app, abort, make_response current_app, abort, make_response
from flask_login import login_required, current_user from flask_login import login_required, current_user
from . import main from . import main
from .forms import EditProfileForm, EditProfileAdminForm, PostForm
from .forms import EditProfileForm, EditProfileAdminForm, PostForm, CommentForm
from .. import db 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 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) 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): def post(id):
post = Post.query.get_or_404(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/<int:id>', methods=['GET', 'POST']) @main.route('/edit/<int:id>', methods=['GET', 'POST'])


+ 24
- 0
app/models.py View File

@ -105,6 +105,7 @@ class User(UserMixin, db.Model):
backref=db.backref('followed', lazy='joined'), backref=db.backref('followed', lazy='joined'),
lazy='dynamic', lazy='dynamic',
cascade='all, delete-orphan') cascade='all, delete-orphan')
comments = db.relationship('Comment', backref='author', lazy='dynamic')
def __init__(self, **kwargs): def __init__(self, **kwargs):
super(User, self).__init__(**kwargs) super(User, self).__init__(**kwargs)
@ -251,6 +252,7 @@ class Post(db.Model):
body_html = db.Column(db.Text) body_html = db.Column(db.Text)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
author_id = db.Column(db.Integer, db.ForeignKey('users.id')) author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
comments = db.relationship('Comment', backref='post', lazy='dynamic')
@staticmethod @staticmethod
def on_changed_body(target, value, oldvalue, initiator): 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) 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): class AnonymousUser(AnonymousUserMixin):
def can(self, perm): def can(self, perm):
return False return False


+ 32
- 0
app/static/styles.css View File

@ -41,6 +41,38 @@ div.post-content {
div.post-footer { div.post-footer {
text-align: right; 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 { div.pagination {
width: 100%; width: 100%;
text-align: center; text-align: center;


+ 22
- 0
app/templates/_comments.html View File

@ -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="comemnt-body">
{% if comment.body_html %}
{{ comment.body_html | safe }}
{% else %}
{{ comment.body }}
{% endif %}
</div>
</div>
</li>
{% endfor %}
</ul>

+ 5
- 5
app/templates/_macros.html View File

@ -1,7 +1,7 @@
{% macro pagination_widget(pagination, endpoint) %}
{% macro pagination_widget(pagination, endpoint, fragment='') %}
<ul class="pagination"> <ul class="pagination">
<li{% if not pagination.has_prev %} class="disabled"{% endif %}> <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; &laquo;
</a> </a>
</li> </li>
@ -9,11 +9,11 @@
{% if p %} {% if p %}
{% if p == pagination.page %} {% if p == pagination.page %}
<li class="active"> <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> </li>
{% else %} {% else %}
<li> <li>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}">{{ p }}</a>
<a href="{{ url_for(endpoint, page = p, **kwargs) }}{{ fragment }}">{{ p }}</a>
</li> </li>
{% endif %} {% endif %}
{% else %} {% else %}
@ -21,7 +21,7 @@
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<li{% if not pagination.has_next %} class="disabled"{% endif %}> <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; &raquo;
</a> </a>
</li> </li>


+ 3
- 0
app/templates/_posts.html View File

@ -29,6 +29,9 @@
<a href="{{ url_for('.post', id=post.id) }}"> <a href="{{ url_for('.post', id=post.id) }}">
<span class="label label-default">Permalink</span> <span class="label label-default">Permalink</span>
</a> </a>
<a href="{{ url_for('.post', id=post.id) }}#comments">
<span class="label label-primary">{{ post.comments.count() }} Comments</span>
</a>
</div> </div>
</div> </div>
</li> </li>


+ 13
- 0
app/templates/post.html View File

@ -1,8 +1,21 @@
{% extends "base.html" %} {% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% import "_macros.html" as macros %} {% import "_macros.html" as macros %}
{% block title %}Flasky{% endblock %} {% block title %}Flasky{% endblock %}
{% block page_content %} {% block page_content %}
{% include '_posts.html' %} {% 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 %} {% endblock %}

+ 4
- 1
app/templates/user.html View File

@ -27,7 +27,10 @@
<p> <p>
Member since {{ moment(user.member_since).format('L') }}. Member since {{ moment(user.member_since).format('L') }}.
Last seen {{ moment(user.last_seen).fromNow() }}. Last seen {{ moment(user.last_seen).fromNow() }}.
<p>{{ user.posts.count() }} blog post{% if user.posts.count() > 1 %}s{% endif %}.</p>
<p>
{{ user.posts.count() }} blog post{% if user.posts.count() != 1 %}s{% endif %}.
{{ user.comments.count() }} comment{% if user.comments.count() != 1 %}s{% endif %}.
</p>
</p> </p>
<p> <p>
{% if current_user.can(Permission.FOLLOW) and user != current_user %} {% if current_user.can(Permission.FOLLOW) and user != current_user %}


+ 1
- 0
config.py View File

@ -16,6 +16,7 @@ class Config:
SQLALCHEMY_TRACK_MODIFICATIONS = False SQLALCHEMY_TRACK_MODIFICATIONS = False
FLASKY_POSTS_PER_PAGE = 20 FLASKY_POSTS_PER_PAGE = 20
FLASKY_FOLLOWERS_PER_PAGE = 50 FLASKY_FOLLOWERS_PER_PAGE = 50
FLASKY_COMMENTS_PER_PAGE = 30
@staticmethod @staticmethod
def init_app(app): def init_app(app):


+ 2
- 2
flasky.py View File

@ -1,7 +1,7 @@
import os import os
from flask_migrate import Migrate from flask_migrate import Migrate
from app import create_app, db 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') app = create_app(os.getenv('FLASK_CONFIG') or 'default')
@ -11,7 +11,7 @@ migrate = Migrate(app, db)
@app.shell_context_processor @app.shell_context_processor
def make_shell_context(): def make_shell_context():
return dict(db=db, User=User, Follow=Follow, Role=Role, return dict(db=db, User=User, Follow=Follow, Role=Role,
Permission=Permission, Post=Post)
Permission=Permission, Post=Post, Comment=Comment)
@app.cli.command() @app.cli.command()


+ 41
- 0
migrations/versions/643e9b785c28_.py View File

@ -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 ###

Loading…
Cancel
Save