mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
significant code reorganisation - split communities and posts
This commit is contained in:
parent
2fb1abbc08
commit
856f450885
24 changed files with 369 additions and 271 deletions
|
@ -62,6 +62,9 @@ def create_app(config_class=Config):
|
||||||
from app.community import bp as community_bp
|
from app.community import bp as community_bp
|
||||||
app.register_blueprint(community_bp, url_prefix='/community')
|
app.register_blueprint(community_bp, url_prefix='/community')
|
||||||
|
|
||||||
|
from app.post import bp as post_bp
|
||||||
|
app.register_blueprint(post_bp)
|
||||||
|
|
||||||
from app.user import bp as user_bp
|
from app.user import bp as user_bp
|
||||||
app.register_blueprint(user_bp)
|
app.register_blueprint(user_bp)
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,3 @@ class CreatePost(FlaskForm):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class NewReplyForm(FlaskForm):
|
|
||||||
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 3}, validators={DataRequired(), Length(min=3, max=5000)})
|
|
||||||
submit = SubmitField(_l('Comment'))
|
|
||||||
|
|
|
@ -1,6 +1,3 @@
|
||||||
from datetime import date, datetime, timedelta
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
||||||
from flask_login import login_user, logout_user, current_user, login_required
|
from flask_login import login_user, logout_user, current_user, login_required
|
||||||
from flask_babel import _
|
from flask_babel import _
|
||||||
|
@ -9,12 +6,12 @@ from sqlalchemy import or_, desc
|
||||||
|
|
||||||
from app import db, constants
|
from app import db, constants
|
||||||
from app.activitypub.signature import RsaKeys, HttpSignature
|
from app.activitypub.signature import RsaKeys, HttpSignature
|
||||||
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost, NewReplyForm
|
from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost
|
||||||
from app.community.util import search_for_community, community_url_exists, actor_to_community, post_replies, \
|
from app.community.util import search_for_community, community_url_exists, actor_to_community, \
|
||||||
get_comment_branch, post_reply_count, ensure_directory_exists, opengraph_parse, url_to_thumbnail_file
|
ensure_directory_exists, opengraph_parse, url_to_thumbnail_file
|
||||||
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
||||||
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, PostReply, \
|
from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \
|
||||||
PostReplyVote, PostVote, File
|
File
|
||||||
from app.community import bp
|
from app.community import bp
|
||||||
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish
|
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish
|
||||||
|
@ -295,184 +292,3 @@ def add_post(actor):
|
||||||
|
|
||||||
return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community,
|
return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community,
|
||||||
images_disabled=images_disabled)
|
images_disabled=images_disabled)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/post/<int:post_id>', methods=['GET', 'POST'])
|
|
||||||
def show_post(post_id: int):
|
|
||||||
post = Post.query.get_or_404(post_id)
|
|
||||||
mods = post.community.moderators()
|
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
|
||||||
form = NewReplyForm()
|
|
||||||
if current_user.is_authenticated and current_user.verified and form.validate_on_submit():
|
|
||||||
reply = PostReply(user_id=current_user.id, post_id=post.id, community_id=post.community.id, body=form.body.data,
|
|
||||||
body_html=markdown_to_html(form.body.data), body_html_safe=True,
|
|
||||||
from_bot=current_user.bot, up_votes=1, nsfw=post.nsfw, nsfl=post.nsfl)
|
|
||||||
db.session.add(reply)
|
|
||||||
db.session.commit()
|
|
||||||
reply_vote = PostReplyVote(user_id=current_user.id, author_id=current_user.id, post_reply_id=reply.id,
|
|
||||||
effect=1.0)
|
|
||||||
db.session.add(reply_vote)
|
|
||||||
db.session.commit()
|
|
||||||
form.body.data = ''
|
|
||||||
flash('Your comment has been added.')
|
|
||||||
# todo: flush cache
|
|
||||||
# todo: federation
|
|
||||||
return redirect(url_for('community.show_post',
|
|
||||||
post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
|
|
||||||
else:
|
|
||||||
replies = post_replies(post.id, 'top')
|
|
||||||
|
|
||||||
og_image = post.image.source_url if post.image_id else None
|
|
||||||
description = shorten_string(markdown_to_text(post.body), 150) if post.body else None
|
|
||||||
|
|
||||||
return render_template('community/post.html', title=post.title, post=post, is_moderator=is_moderator,
|
|
||||||
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
|
||||||
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
|
|
||||||
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/post/<int:post_id>/<vote_direction>', methods=['GET', 'POST'])
|
|
||||||
@login_required
|
|
||||||
@validation_required
|
|
||||||
def post_vote(post_id: int, vote_direction):
|
|
||||||
upvoted_class = downvoted_class = ''
|
|
||||||
post = Post.query.get_or_404(post_id)
|
|
||||||
existing_vote = PostVote.query.filter_by(user_id=current_user.id, post_id=post.id).first()
|
|
||||||
if existing_vote:
|
|
||||||
post.author.reputation -= existing_vote.effect
|
|
||||||
if existing_vote.effect > 0: # previous vote was up
|
|
||||||
if vote_direction == 'upvote': # new vote is also up, so remove it
|
|
||||||
db.session.delete(existing_vote)
|
|
||||||
post.up_votes -= 1
|
|
||||||
post.score -= 1
|
|
||||||
else: # new vote is down while previous vote was up, so reverse their previous vote
|
|
||||||
existing_vote.effect = -1
|
|
||||||
post.up_votes -= 1
|
|
||||||
post.down_votes += 1
|
|
||||||
post.score -= 2
|
|
||||||
downvoted_class = 'voted_down'
|
|
||||||
else: # previous vote was down
|
|
||||||
if vote_direction == 'upvote': # new vote is upvote
|
|
||||||
existing_vote.effect = 1
|
|
||||||
post.up_votes += 1
|
|
||||||
post.down_votes -= 1
|
|
||||||
post.score += 1
|
|
||||||
upvoted_class = 'voted_up'
|
|
||||||
else: # reverse a previous downvote
|
|
||||||
db.session.delete(existing_vote)
|
|
||||||
post.down_votes -= 1
|
|
||||||
post.score += 2
|
|
||||||
else:
|
|
||||||
if vote_direction == 'upvote':
|
|
||||||
effect = 1
|
|
||||||
post.up_votes += 1
|
|
||||||
post.score += 1
|
|
||||||
upvoted_class = 'voted_up'
|
|
||||||
else:
|
|
||||||
effect = -1
|
|
||||||
post.down_votes += 1
|
|
||||||
post.score -= 1
|
|
||||||
downvoted_class = 'voted_down'
|
|
||||||
vote = PostVote(user_id=current_user.id, post_id=post.id, author_id=post.author.id,
|
|
||||||
effect=effect)
|
|
||||||
post.author.reputation += effect
|
|
||||||
db.session.add(vote)
|
|
||||||
db.session.commit()
|
|
||||||
return render_template('community/_post_voting_buttons.html', post=post,
|
|
||||||
upvoted_class=upvoted_class,
|
|
||||||
downvoted_class=downvoted_class)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/comment/<int:comment_id>/<vote_direction>', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
@validation_required
|
|
||||||
def comment_vote(comment_id, vote_direction):
|
|
||||||
upvoted_class = downvoted_class = ''
|
|
||||||
comment = PostReply.query.get_or_404(comment_id)
|
|
||||||
existing_vote = PostReplyVote.query.filter_by(user_id=current_user.id, post_reply_id=comment.id).first()
|
|
||||||
if existing_vote:
|
|
||||||
if existing_vote.effect > 0: # previous vote was up
|
|
||||||
if vote_direction == 'upvote': # new vote is also up, so remove it
|
|
||||||
db.session.delete(existing_vote)
|
|
||||||
comment.up_votes -= 1
|
|
||||||
comment.score -= 1
|
|
||||||
else: # new vote is down while previous vote was up, so reverse their previous vote
|
|
||||||
existing_vote.effect = -1
|
|
||||||
comment.up_votes -= 1
|
|
||||||
comment.down_votes += 1
|
|
||||||
comment.score -= 2
|
|
||||||
downvoted_class = 'voted_down'
|
|
||||||
else: # previous vote was down
|
|
||||||
if vote_direction == 'upvote': # new vote is upvote
|
|
||||||
existing_vote.effect = 1
|
|
||||||
comment.up_votes += 1
|
|
||||||
comment.down_votes -= 1
|
|
||||||
comment.score += 1
|
|
||||||
upvoted_class = 'voted_up'
|
|
||||||
else: # reverse a previous downvote
|
|
||||||
db.session.delete(existing_vote)
|
|
||||||
comment.down_votes -= 1
|
|
||||||
comment.score += 2
|
|
||||||
else:
|
|
||||||
if vote_direction == 'upvote':
|
|
||||||
effect = 1
|
|
||||||
comment.up_votes += 1
|
|
||||||
comment.score += 1
|
|
||||||
upvoted_class = 'voted_up'
|
|
||||||
else:
|
|
||||||
effect = -1
|
|
||||||
comment.down_votes += 1
|
|
||||||
comment.score -= 1
|
|
||||||
downvoted_class = 'voted_down'
|
|
||||||
vote = PostReplyVote(user_id=current_user.id, post_reply_id=comment_id, author_id=comment.author.id, effect=effect)
|
|
||||||
comment.author.reputation += effect
|
|
||||||
db.session.add(vote)
|
|
||||||
db.session.commit()
|
|
||||||
return render_template('community/_voting_buttons.html', comment=comment,
|
|
||||||
upvoted_class=upvoted_class,
|
|
||||||
downvoted_class=downvoted_class)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/post/<int:post_id>/comment/<int:comment_id>')
|
|
||||||
def continue_discussion(post_id, comment_id):
|
|
||||||
post = Post.query.get_or_404(post_id)
|
|
||||||
comment = PostReply.query.get_or_404(comment_id)
|
|
||||||
mods = post.community.moderators()
|
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
|
||||||
replies = get_comment_branch(post.id, comment.id, 'top')
|
|
||||||
|
|
||||||
return render_template('community/continue_discussion.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
|
||||||
is_moderator=is_moderator, comment=comment, replies=replies)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/reply', methods=['GET', 'POST'])
|
|
||||||
@login_required
|
|
||||||
def add_reply(post_id: int, comment_id: int):
|
|
||||||
post = Post.query.get_or_404(post_id)
|
|
||||||
comment = PostReply.query.get_or_404(comment_id)
|
|
||||||
mods = post.community.moderators()
|
|
||||||
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
|
||||||
form = NewReplyForm()
|
|
||||||
if form.validate_on_submit():
|
|
||||||
reply = PostReply(user_id=current_user.id, post_id=post.id, parent_id=comment.id, depth=comment.depth + 1,
|
|
||||||
community_id=post.community.id, body=form.body.data,
|
|
||||||
body_html=markdown_to_html(form.body.data), body_html_safe=True,
|
|
||||||
from_bot=current_user.bot, up_votes=1, nsfw=post.nsfw, nsfl=post.nsfl)
|
|
||||||
db.session.add(reply)
|
|
||||||
db.session.commit()
|
|
||||||
reply_vote = PostReplyVote(user_id=current_user.id, author_id=current_user.id, post_reply_id=reply.id,
|
|
||||||
effect=1.0)
|
|
||||||
db.session.add(reply_vote)
|
|
||||||
post.reply_count = post_reply_count(post.id)
|
|
||||||
db.session.commit()
|
|
||||||
form.body.data = ''
|
|
||||||
flash('Your comment has been added.')
|
|
||||||
# todo: flush cache
|
|
||||||
# todo: federation
|
|
||||||
if reply.depth <= constants.THREAD_CUTOFF_DEPTH:
|
|
||||||
return redirect(url_for('community.show_post', post_id=post_id, _anchor=f'comment_{reply.parent_id}'))
|
|
||||||
else:
|
|
||||||
return redirect(url_for('community.continue_discussion', post_id=post_id, comment_id=reply.parent_id))
|
|
||||||
else:
|
|
||||||
return render_template('community/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
|
||||||
is_moderator=is_moderator, form=form, comment=comment)
|
|
||||||
|
|
|
@ -87,58 +87,6 @@ def actor_to_community(actor) -> Community:
|
||||||
return community
|
return community
|
||||||
|
|
||||||
|
|
||||||
# replies to a post, in a tree, sorted by a variety of methods
|
|
||||||
def post_replies(post_id: int, sort_by: str, show_first: int = 0) -> List[PostReply]:
|
|
||||||
comments = PostReply.query.filter_by(post_id=post_id)
|
|
||||||
if sort_by == 'hot':
|
|
||||||
comments = comments.order_by(desc(PostReply.ranking))
|
|
||||||
elif sort_by == 'top':
|
|
||||||
comments = comments.order_by(desc(PostReply.score))
|
|
||||||
elif sort_by == 'new':
|
|
||||||
comments = comments.order_by(desc(PostReply.posted_at))
|
|
||||||
|
|
||||||
comments_dict = {comment.id: {'comment': comment, 'replies': []} for comment in comments.all()}
|
|
||||||
|
|
||||||
for comment in comments:
|
|
||||||
if comment.parent_id is not None:
|
|
||||||
parent_comment = comments_dict.get(comment.parent_id)
|
|
||||||
if parent_comment:
|
|
||||||
parent_comment['replies'].append(comments_dict[comment.id])
|
|
||||||
|
|
||||||
return [comment for comment in comments_dict.values() if comment['comment'].parent_id is None]
|
|
||||||
|
|
||||||
|
|
||||||
def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[PostReply]:
|
|
||||||
# Fetch the specified parent comment and its replies
|
|
||||||
parent_comment = PostReply.query.get(comment_id)
|
|
||||||
if parent_comment is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
comments = PostReply.query.filter(PostReply.post_id == post_id)
|
|
||||||
if sort_by == 'hot':
|
|
||||||
comments = comments.order_by(desc(PostReply.ranking))
|
|
||||||
elif sort_by == 'top':
|
|
||||||
comments = comments.order_by(desc(PostReply.score))
|
|
||||||
elif sort_by == 'new':
|
|
||||||
comments = comments.order_by(desc(PostReply.posted_at))
|
|
||||||
|
|
||||||
comments_dict = {comment.id: {'comment': comment, 'replies': []} for comment in comments.all()}
|
|
||||||
|
|
||||||
for comment in comments:
|
|
||||||
if comment.parent_id is not None:
|
|
||||||
parent_comment = comments_dict.get(comment.parent_id)
|
|
||||||
if parent_comment:
|
|
||||||
parent_comment['replies'].append(comments_dict[comment.id])
|
|
||||||
|
|
||||||
return [comment for comment in comments_dict.values() if comment['comment'].id == comment_id]
|
|
||||||
|
|
||||||
|
|
||||||
# The number of replies a post has
|
|
||||||
def post_reply_count(post_id) -> int:
|
|
||||||
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id'),
|
|
||||||
{'post_id': post_id}).scalar()
|
|
||||||
|
|
||||||
|
|
||||||
def ensure_directory_exists(directory):
|
def ensure_directory_exists(directory):
|
||||||
parts = directory.split('/')
|
parts = directory.split('/')
|
||||||
rebuild_directory = ''
|
rebuild_directory = ''
|
||||||
|
|
|
@ -4,7 +4,7 @@ from time import time
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from flask import current_app, escape, url_for
|
from flask import current_app, escape, url_for
|
||||||
from flask_login import UserMixin
|
from flask_login import UserMixin, current_user
|
||||||
from sqlalchemy import or_, text
|
from sqlalchemy import or_, text
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from flask_babel import _, lazy_gettext as _l
|
from flask_babel import _, lazy_gettext as _l
|
||||||
|
@ -129,6 +129,9 @@ class Community(db.Model):
|
||||||
))
|
))
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
|
def is_moderator(self):
|
||||||
|
return any(moderator.user_id == current_user.id for moderator in self.moderators())
|
||||||
|
|
||||||
|
|
||||||
user_role = db.Table('user_role',
|
user_role = db.Table('user_role',
|
||||||
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
|
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
|
||||||
|
|
5
app/post/__init__.py
Normal file
5
app/post/__init__.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from flask import Blueprint
|
||||||
|
|
||||||
|
bp = Blueprint('post', __name__)
|
||||||
|
|
||||||
|
from app.post import routes
|
9
app/post/forms.py
Normal file
9
app/post/forms.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import TextAreaField, SubmitField
|
||||||
|
from wtforms.validators import DataRequired, Length
|
||||||
|
from flask_babel import _, lazy_gettext as _l
|
||||||
|
|
||||||
|
|
||||||
|
class NewReplyForm(FlaskForm):
|
||||||
|
body = TextAreaField(_l('Body'), render_kw={'placeholder': 'What are your thoughts?', 'rows': 3}, validators={DataRequired(), Length(min=3, max=5000)})
|
||||||
|
submit = SubmitField(_l('Comment'))
|
201
app/post/routes.py
Normal file
201
app/post/routes.py
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
from flask import redirect, url_for, flash, request, make_response, session, Markup, current_app, abort
|
||||||
|
from flask_login import login_user, logout_user, current_user, login_required
|
||||||
|
from flask_babel import _
|
||||||
|
from sqlalchemy import or_, desc
|
||||||
|
|
||||||
|
from app import db, constants
|
||||||
|
from app.post.forms import NewReplyForm
|
||||||
|
from app.post.util import post_replies, get_comment_branch, post_reply_count
|
||||||
|
from app.constants import SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER, POST_TYPE_LINK, POST_TYPE_ARTICLE, POST_TYPE_IMAGE
|
||||||
|
from app.models import Post, PostReply, \
|
||||||
|
PostReplyVote, PostVote
|
||||||
|
from app.post import bp
|
||||||
|
from app.utils import get_setting, render_template, allowlist_html, markdown_to_html, validation_required, \
|
||||||
|
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/post/<int:post_id>', methods=['GET', 'POST'])
|
||||||
|
def show_post(post_id: int):
|
||||||
|
post = Post.query.get_or_404(post_id)
|
||||||
|
mods = post.community.moderators()
|
||||||
|
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||||
|
form = NewReplyForm()
|
||||||
|
if current_user.is_authenticated and current_user.verified and form.validate_on_submit():
|
||||||
|
reply = PostReply(user_id=current_user.id, post_id=post.id, community_id=post.community.id, body=form.body.data,
|
||||||
|
body_html=markdown_to_html(form.body.data), body_html_safe=True,
|
||||||
|
from_bot=current_user.bot, up_votes=1, nsfw=post.nsfw, nsfl=post.nsfl)
|
||||||
|
db.session.add(reply)
|
||||||
|
db.session.commit()
|
||||||
|
reply_vote = PostReplyVote(user_id=current_user.id, author_id=current_user.id, post_reply_id=reply.id,
|
||||||
|
effect=1.0)
|
||||||
|
db.session.add(reply_vote)
|
||||||
|
db.session.commit()
|
||||||
|
form.body.data = ''
|
||||||
|
flash('Your comment has been added.')
|
||||||
|
# todo: flush cache
|
||||||
|
# todo: federation
|
||||||
|
return redirect(url_for('post.show_post',
|
||||||
|
post_id=post_id)) # redirect to current page to avoid refresh resubmitting the form
|
||||||
|
else:
|
||||||
|
replies = post_replies(post.id, 'top')
|
||||||
|
|
||||||
|
og_image = post.image.source_url if post.image_id else None
|
||||||
|
description = shorten_string(markdown_to_text(post.body), 150) if post.body else None
|
||||||
|
|
||||||
|
return render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator,
|
||||||
|
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH,
|
||||||
|
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
|
||||||
|
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/post/<int:post_id>/<vote_direction>', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@validation_required
|
||||||
|
def post_vote(post_id: int, vote_direction):
|
||||||
|
upvoted_class = downvoted_class = ''
|
||||||
|
post = Post.query.get_or_404(post_id)
|
||||||
|
existing_vote = PostVote.query.filter_by(user_id=current_user.id, post_id=post.id).first()
|
||||||
|
if existing_vote:
|
||||||
|
post.author.reputation -= existing_vote.effect
|
||||||
|
if existing_vote.effect > 0: # previous vote was up
|
||||||
|
if vote_direction == 'upvote': # new vote is also up, so remove it
|
||||||
|
db.session.delete(existing_vote)
|
||||||
|
post.up_votes -= 1
|
||||||
|
post.score -= 1
|
||||||
|
else: # new vote is down while previous vote was up, so reverse their previous vote
|
||||||
|
existing_vote.effect = -1
|
||||||
|
post.up_votes -= 1
|
||||||
|
post.down_votes += 1
|
||||||
|
post.score -= 2
|
||||||
|
downvoted_class = 'voted_down'
|
||||||
|
else: # previous vote was down
|
||||||
|
if vote_direction == 'upvote': # new vote is upvote
|
||||||
|
existing_vote.effect = 1
|
||||||
|
post.up_votes += 1
|
||||||
|
post.down_votes -= 1
|
||||||
|
post.score += 1
|
||||||
|
upvoted_class = 'voted_up'
|
||||||
|
else: # reverse a previous downvote
|
||||||
|
db.session.delete(existing_vote)
|
||||||
|
post.down_votes -= 1
|
||||||
|
post.score += 2
|
||||||
|
else:
|
||||||
|
if vote_direction == 'upvote':
|
||||||
|
effect = 1
|
||||||
|
post.up_votes += 1
|
||||||
|
post.score += 1
|
||||||
|
upvoted_class = 'voted_up'
|
||||||
|
else:
|
||||||
|
effect = -1
|
||||||
|
post.down_votes += 1
|
||||||
|
post.score -= 1
|
||||||
|
downvoted_class = 'voted_down'
|
||||||
|
vote = PostVote(user_id=current_user.id, post_id=post.id, author_id=post.author.id,
|
||||||
|
effect=effect)
|
||||||
|
post.author.reputation += effect
|
||||||
|
db.session.add(vote)
|
||||||
|
db.session.commit()
|
||||||
|
return render_template('post/_post_voting_buttons.html', post=post,
|
||||||
|
upvoted_class=upvoted_class,
|
||||||
|
downvoted_class=downvoted_class)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/comment/<int:comment_id>/<vote_direction>', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
@validation_required
|
||||||
|
def comment_vote(comment_id, vote_direction):
|
||||||
|
upvoted_class = downvoted_class = ''
|
||||||
|
comment = PostReply.query.get_or_404(comment_id)
|
||||||
|
existing_vote = PostReplyVote.query.filter_by(user_id=current_user.id, post_reply_id=comment.id).first()
|
||||||
|
if existing_vote:
|
||||||
|
if existing_vote.effect > 0: # previous vote was up
|
||||||
|
if vote_direction == 'upvote': # new vote is also up, so remove it
|
||||||
|
db.session.delete(existing_vote)
|
||||||
|
comment.up_votes -= 1
|
||||||
|
comment.score -= 1
|
||||||
|
else: # new vote is down while previous vote was up, so reverse their previous vote
|
||||||
|
existing_vote.effect = -1
|
||||||
|
comment.up_votes -= 1
|
||||||
|
comment.down_votes += 1
|
||||||
|
comment.score -= 2
|
||||||
|
downvoted_class = 'voted_down'
|
||||||
|
else: # previous vote was down
|
||||||
|
if vote_direction == 'upvote': # new vote is upvote
|
||||||
|
existing_vote.effect = 1
|
||||||
|
comment.up_votes += 1
|
||||||
|
comment.down_votes -= 1
|
||||||
|
comment.score += 1
|
||||||
|
upvoted_class = 'voted_up'
|
||||||
|
else: # reverse a previous downvote
|
||||||
|
db.session.delete(existing_vote)
|
||||||
|
comment.down_votes -= 1
|
||||||
|
comment.score += 2
|
||||||
|
else:
|
||||||
|
if vote_direction == 'upvote':
|
||||||
|
effect = 1
|
||||||
|
comment.up_votes += 1
|
||||||
|
comment.score += 1
|
||||||
|
upvoted_class = 'voted_up'
|
||||||
|
else:
|
||||||
|
effect = -1
|
||||||
|
comment.down_votes += 1
|
||||||
|
comment.score -= 1
|
||||||
|
downvoted_class = 'voted_down'
|
||||||
|
vote = PostReplyVote(user_id=current_user.id, post_reply_id=comment_id, author_id=comment.author.id, effect=effect)
|
||||||
|
comment.author.reputation += effect
|
||||||
|
db.session.add(vote)
|
||||||
|
db.session.commit()
|
||||||
|
return render_template('post/_voting_buttons.html', comment=comment,
|
||||||
|
upvoted_class=upvoted_class,
|
||||||
|
downvoted_class=downvoted_class)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/post/<int:post_id>/comment/<int:comment_id>')
|
||||||
|
def continue_discussion(post_id, comment_id):
|
||||||
|
post = Post.query.get_or_404(post_id)
|
||||||
|
comment = PostReply.query.get_or_404(comment_id)
|
||||||
|
mods = post.community.moderators()
|
||||||
|
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||||
|
replies = get_comment_branch(post.id, comment.id, 'top')
|
||||||
|
|
||||||
|
return render_template('post/continue_discussion.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
||||||
|
is_moderator=is_moderator, comment=comment, replies=replies)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/post/<int:post_id>/comment/<int:comment_id>/reply', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
def add_reply(post_id: int, comment_id: int):
|
||||||
|
post = Post.query.get_or_404(post_id)
|
||||||
|
comment = PostReply.query.get_or_404(comment_id)
|
||||||
|
mods = post.community.moderators()
|
||||||
|
is_moderator = current_user.is_authenticated and any(mod.user_id == current_user.id for mod in mods)
|
||||||
|
form = NewReplyForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
reply = PostReply(user_id=current_user.id, post_id=post.id, parent_id=comment.id, depth=comment.depth + 1,
|
||||||
|
community_id=post.community.id, body=form.body.data,
|
||||||
|
body_html=markdown_to_html(form.body.data), body_html_safe=True,
|
||||||
|
from_bot=current_user.bot, up_votes=1, nsfw=post.nsfw, nsfl=post.nsfl)
|
||||||
|
db.session.add(reply)
|
||||||
|
db.session.commit()
|
||||||
|
reply_vote = PostReplyVote(user_id=current_user.id, author_id=current_user.id, post_reply_id=reply.id,
|
||||||
|
effect=1.0)
|
||||||
|
db.session.add(reply_vote)
|
||||||
|
post.reply_count = post_reply_count(post.id)
|
||||||
|
db.session.commit()
|
||||||
|
form.body.data = ''
|
||||||
|
flash('Your comment has been added.')
|
||||||
|
# todo: flush cache
|
||||||
|
# todo: federation
|
||||||
|
if reply.depth <= constants.THREAD_CUTOFF_DEPTH:
|
||||||
|
return redirect(url_for('post.show_post', post_id=post_id, _anchor=f'comment_{reply.parent_id}'))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('post.continue_discussion', post_id=post_id, comment_id=reply.parent_id))
|
||||||
|
else:
|
||||||
|
return render_template('post/add_reply.html', title=_('Discussing %(title)s', title=post.title), post=post,
|
||||||
|
is_moderator=is_moderator, form=form, comment=comment)
|
||||||
|
|
||||||
|
@bp.route('/post/<int:post_id>/options', methods=['GET', 'POST'])
|
||||||
|
def post_options(post_id: int):
|
||||||
|
post = Post.query.get_or_404(post_id)
|
||||||
|
return render_template('post/post_options.html', post=post)
|
||||||
|
|
58
app/post/util.py
Normal file
58
app/post/util.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from sqlalchemy import desc, text
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.models import PostReply
|
||||||
|
|
||||||
|
|
||||||
|
# replies to a post, in a tree, sorted by a variety of methods
|
||||||
|
def post_replies(post_id: int, sort_by: str, show_first: int = 0) -> List[PostReply]:
|
||||||
|
comments = PostReply.query.filter_by(post_id=post_id)
|
||||||
|
if sort_by == 'hot':
|
||||||
|
comments = comments.order_by(desc(PostReply.ranking))
|
||||||
|
elif sort_by == 'top':
|
||||||
|
comments = comments.order_by(desc(PostReply.score))
|
||||||
|
elif sort_by == 'new':
|
||||||
|
comments = comments.order_by(desc(PostReply.posted_at))
|
||||||
|
|
||||||
|
comments_dict = {comment.id: {'comment': comment, 'replies': []} for comment in comments.all()}
|
||||||
|
|
||||||
|
for comment in comments:
|
||||||
|
if comment.parent_id is not None:
|
||||||
|
parent_comment = comments_dict.get(comment.parent_id)
|
||||||
|
if parent_comment:
|
||||||
|
parent_comment['replies'].append(comments_dict[comment.id])
|
||||||
|
|
||||||
|
return [comment for comment in comments_dict.values() if comment['comment'].parent_id is None]
|
||||||
|
|
||||||
|
|
||||||
|
def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[PostReply]:
|
||||||
|
# Fetch the specified parent comment and its replies
|
||||||
|
parent_comment = PostReply.query.get(comment_id)
|
||||||
|
if parent_comment is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
comments = PostReply.query.filter(PostReply.post_id == post_id)
|
||||||
|
if sort_by == 'hot':
|
||||||
|
comments = comments.order_by(desc(PostReply.ranking))
|
||||||
|
elif sort_by == 'top':
|
||||||
|
comments = comments.order_by(desc(PostReply.score))
|
||||||
|
elif sort_by == 'new':
|
||||||
|
comments = comments.order_by(desc(PostReply.posted_at))
|
||||||
|
|
||||||
|
comments_dict = {comment.id: {'comment': comment, 'replies': []} for comment in comments.all()}
|
||||||
|
|
||||||
|
for comment in comments:
|
||||||
|
if comment.parent_id is not None:
|
||||||
|
parent_comment = comments_dict.get(comment.parent_id)
|
||||||
|
if parent_comment:
|
||||||
|
parent_comment['replies'].append(comments_dict[comment.id])
|
||||||
|
|
||||||
|
return [comment for comment in comments_dict.values() if comment['comment'].id == comment_id]
|
||||||
|
|
||||||
|
|
||||||
|
# The number of replies a post has
|
||||||
|
def post_reply_count(post_id) -> int:
|
||||||
|
return db.session.execute(text('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id'),
|
||||||
|
{'post_id': post_id}).scalar()
|
|
@ -166,6 +166,18 @@
|
||||||
content: "\e935";
|
content: "\e935";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-options::before {
|
||||||
|
content: "\e99b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.fe-edit::before {
|
||||||
|
content: "\e95a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.fe-delete::before {
|
||||||
|
content: "\ea03";
|
||||||
|
}
|
||||||
|
|
||||||
.fe-report::before {
|
.fe-report::before {
|
||||||
content: "\e967";
|
content: "\e967";
|
||||||
}
|
}
|
||||||
|
|
|
@ -169,6 +169,18 @@ nav, etc which are used site-wide */
|
||||||
content: "\e935";
|
content: "\e935";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-options::before {
|
||||||
|
content: "\e99b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.fe-edit::before {
|
||||||
|
content: "\e95a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.fe-delete::before {
|
||||||
|
content: "\ea03";
|
||||||
|
}
|
||||||
|
|
||||||
.fe-report::before {
|
.fe-report::before {
|
||||||
content: "\e967";
|
content: "\e967";
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,6 +168,18 @@
|
||||||
content: "\e935";
|
content: "\e935";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fe-options::before {
|
||||||
|
content: "\e99b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.fe-edit::before {
|
||||||
|
content: "\e95a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.fe-delete::before {
|
||||||
|
content: "\ea03";
|
||||||
|
}
|
||||||
|
|
||||||
.fe-report::before {
|
.fe-report::before {
|
||||||
content: "\e967";
|
content: "\e967";
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="post_list">
|
<div class="post_list">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include 'community/_post_teaser.html' %}
|
{% include 'post/_post_teaser.html' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ _('No posts in this community yet.') }}</p>
|
<p>{{ _('No posts in this community yet.') }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<h1 class="mt-2">{{ domain.name }}</h1>
|
<h1 class="mt-2">{{ domain.name }}</h1>
|
||||||
<div class="post_list">
|
<div class="post_list">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include 'community/_post_teaser.html' %}
|
{% include 'post/_post_teaser.html' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>{{ _('No posts in this domain yet.') }}</p>
|
<p>{{ _('No posts in this domain yet.') }}</p>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="voting_buttons">
|
<div class="voting_buttons">
|
||||||
{% include "community/_post_voting_buttons.html" %}
|
{% include "post/_post_voting_buttons.html" %}
|
||||||
</div>
|
</div>
|
||||||
<h1 class="mt-2">{{ post.title }}</h1>
|
<h1 class="mt-2">{{ post.title }}</h1>
|
||||||
{% if post.url %}
|
{% if post.url %}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
</ol>
|
</ol>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="voting_buttons">
|
<div class="voting_buttons">
|
||||||
{% include "community/_post_voting_buttons.html" %}
|
{% include "post/_post_voting_buttons.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% if post.type == POST_TYPE_LINK and post.image_id and not (post.url and 'youtube.com' in post.url) %}
|
{% if post.type == POST_TYPE_LINK and post.image_id and not (post.url and 'youtube.com' in post.url) %}
|
||||||
<div class="url_thumbnail">
|
<div class="url_thumbnail">
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="row main_row">
|
<div class="row main_row">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3>
|
<h3>
|
||||||
<a href="{{ url_for('community.show_post', post_id=post.id) }}">{{ post.title }}</a>
|
<a href="{{ url_for('post.show_post', post_id=post.id) }}">{{ post.title }}</a>
|
||||||
{% if post.type == POST_TYPE_IMAGE %}<span class="fe fe-image"> </span>{% endif %}
|
{% if post.type == POST_TYPE_IMAGE %}<span class="fe fe-image"> </span>{% endif %}
|
||||||
{% if post.type == POST_TYPE_LINK and post.domain_id %}
|
{% if post.type == POST_TYPE_LINK and post.domain_id %}
|
||||||
{% if post.url and 'youtube.com' in post.url %}
|
{% if post.url and 'youtube.com' in post.url %}
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
<span class="small">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</span>
|
<span class="small">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</span>
|
||||||
{% if post.image_id %}
|
{% if post.image_id %}
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<a href="{{ url_for('community.show_post', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text }}"
|
<a href="{{ url_for('post.show_post', post_id=post.id) }}"><img src="{{ post.image.thumbnail_url() }}" alt="{{ post.image.alt_text }}"
|
||||||
height="50" /></a>
|
height="50" /></a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -28,15 +28,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row utilities_row">
|
<div class="row utilities_row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<a href="{{ url_for('community.show_post', post_id=post.id, _anchor='replies') }}"><span class="fe fe-reply"></span></a>
|
<a href="{{ url_for('post.show_post', post_id=post.id, _anchor='replies') }}"><span class="fe fe-reply"></span></a>
|
||||||
<a href="{{ url_for('community.show_post', post_id=post.id, _anchor='replies') }}">{{ post.reply_count }}</a>
|
<a href="{{ url_for('post.show_post', post_id=post.id, _anchor='replies') }}">{{ post.reply_count }}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">{{ post.score }}</div>
|
<div class="col-2"><a href="{{ url_for('post.post_options', post_id=post.id) }}"><span class="fe fe-options" title="Options"> </span></a></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-md-2">
|
<div class="col col-md-2">
|
||||||
<div class="voting_buttons pt-2">
|
<div class="voting_buttons pt-2">
|
||||||
{% include "community/_post_voting_buttons.html" %}
|
{% include "post/_post_voting_buttons.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,11 +1,11 @@
|
||||||
{% if current_user.is_authenticated and current_user.verified %}
|
{% if current_user.is_authenticated and current_user.verified %}
|
||||||
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}"
|
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}"
|
||||||
hx-post="/community/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||||
<span class="fe fe-arrow-up"></span>
|
<span class="fe fe-arrow-up"></span>
|
||||||
{{ post.up_votes }}
|
{{ post.up_votes }}
|
||||||
</div>
|
</div>
|
||||||
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}"
|
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}"
|
||||||
hx-post="/community/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||||
<span class="fe fe-arrow-down"></span>
|
<span class="fe fe-arrow-down"></span>
|
||||||
{{ post.down_votes }}
|
{{ post.down_votes }}
|
||||||
</div>
|
</div>
|
|
@ -1,11 +1,11 @@
|
||||||
{% if current_user.is_authenticated and current_user.verified %}
|
{% if current_user.is_authenticated and current_user.verified %}
|
||||||
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}"
|
<div class="upvote_button digits_{{ digits(comment.up_votes) }} {{ upvoted_class }}"
|
||||||
hx-post="/community/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||||
<span class="fe fe-arrow-up"></span>
|
<span class="fe fe-arrow-up"></span>
|
||||||
{{ comment.up_votes }}
|
{{ comment.up_votes }}
|
||||||
</div>
|
</div>
|
||||||
<div class="downvote_button digits_{{ digits(comment.down_votes) }} {{ downvoted_class }}"
|
<div class="downvote_button digits_{{ digits(comment.down_votes) }} {{ downvoted_class }}"
|
||||||
hx-post="/community/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons">
|
||||||
<span class="fe fe-arrow-down"></span>
|
<span class="fe fe-arrow-down"></span>
|
||||||
{{ comment.down_votes }}
|
{{ comment.down_votes }}
|
||||||
</div>
|
</div>
|
|
@ -4,8 +4,8 @@
|
||||||
{% block app_content %}
|
{% block app_content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-8 position-relative main_pane">
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
{% include 'community/_post_full.html' %}
|
{% include 'post/_post_full.html' %}
|
||||||
<p><a href="{{ url_for('community.show_post', post_id=post.id, _anchor='replies') }}">Back to main discussion</a></p>
|
<p><a href="{{ url_for('post.show_post', post_id=post.id, _anchor='replies') }}">Back to main discussion</a></p>
|
||||||
<div class="row post_replies">
|
<div class="row post_replies">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{% macro render_comment(comment) %}
|
{% macro render_comment(comment) %}
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
<div class="limit_height">
|
<div class="limit_height">
|
||||||
<div class="voting_buttons">
|
<div class="voting_buttons">
|
||||||
{% with comment=comment['comment'] %}
|
{% with comment=comment['comment'] %}
|
||||||
{% include "community/_voting_buttons.html" %}
|
{% include "post/_voting_buttons.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="hide_button"><a href='#'>[-] hide</a></div>
|
<div class="hide_button"><a href='#'>[-] hide</a></div>
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="comment_actions hidable">
|
<div class="comment_actions hidable">
|
||||||
<a href="{{ url_for('community.add_reply', post_id=post.id, comment_id=comment['comment'].id) }}" rel="nofollow"><span class="fe fe-reply"></span> reply</a>
|
<a href="{{ url_for('post.add_reply', post_id=post.id, comment_id=comment['comment'].id) }}" rel="nofollow"><span class="fe fe-reply"></span> reply</a>
|
||||||
</div>
|
</div>
|
||||||
{% if comment['replies'] %}
|
{% if comment['replies'] %}
|
||||||
<div class="replies hidable">
|
<div class="replies hidable">
|
|
@ -7,7 +7,7 @@
|
||||||
</script>
|
</script>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-8 position-relative main_pane">
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
{% include 'community/_post_full.html' %}
|
{% include 'post/_post_full.html' %}
|
||||||
{% if post.comments_enabled %}
|
{% if post.comments_enabled %}
|
||||||
{% if current_user.is_authenticated %}
|
{% if current_user.is_authenticated %}
|
||||||
{% if current_user.verified %}
|
{% if current_user.verified %}
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
<div class="limit_height">
|
<div class="limit_height">
|
||||||
<div class="voting_buttons">
|
<div class="voting_buttons">
|
||||||
{% with comment=comment['comment'] %}
|
{% with comment=comment['comment'] %}
|
||||||
{% include "community/_voting_buttons.html" %}
|
{% include "post/_voting_buttons.html" %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="hide_button">
|
<div class="hide_button">
|
||||||
|
@ -72,7 +72,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% if current_user.is_authenticated and current_user.verified %}
|
{% if current_user.is_authenticated and current_user.verified %}
|
||||||
<div class="comment_actions hidable">
|
<div class="comment_actions hidable">
|
||||||
<a href="{{ url_for('community.add_reply', post_id=post.id, comment_id=comment['comment'].id) }}" rel="nofollow"><span class="fe fe-reply"></span> reply</a>
|
<a href="{{ url_for('post.add_reply', post_id=post.id, comment_id=comment['comment'].id) }}" rel="nofollow"><span class="fe fe-reply"></span> reply</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if comment['replies'] %}
|
{% if comment['replies'] %}
|
||||||
|
@ -84,7 +84,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="continue_thread hidable">
|
<div class="continue_thread hidable">
|
||||||
<a href="{{ url_for('community.continue_discussion', post_id=post.id, comment_id=comment['comment'].id, _anchor='replies') }}">
|
<a href="{{ url_for('post.continue_discussion', post_id=post.id, comment_id=comment['comment'].id, _anchor='replies') }}">
|
||||||
Continue thread</a>
|
Continue thread</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
23
app/templates/post/post_options.html
Normal file
23
app/templates/post/post_options.html
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% from 'bootstrap/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-login mx-auto">
|
||||||
|
<div class="card mt-5">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
<div class="card-title">{{ _('Options for "%(post_title)s"', post_title=post.title) }}</div>
|
||||||
|
<ul>
|
||||||
|
{% if post.user_id == current_user.id or post.community.is_moderator() %}
|
||||||
|
<li><a href="#" class="no-underline"><span class="fe fe-edit"></span> Edit</a></li>
|
||||||
|
<li><a href="#" class="no-underline confirm_first"><span class="fe fe-delete"></span> Delete</a></li>
|
||||||
|
{% endif %}
|
||||||
|
{% if post.user_id != current_user.id %}
|
||||||
|
<li><a href="#" class="no-underline"><span class="fe fe-report"></span> Report</a></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -42,7 +42,7 @@
|
||||||
<h2 class="mt-4">Posts</h2>
|
<h2 class="mt-4">Posts</h2>
|
||||||
<div class="post_list">
|
<div class="post_list">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include 'community/_post_teaser.html' %}
|
{% include 'post/_post_teaser.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
<h2 class="mt-4">Comments</h2>
|
<h2 class="mt-4">Comments</h2>
|
||||||
<div class="post_list">
|
<div class="post_list">
|
||||||
{% for post_reply in post_replies %}
|
{% for post_reply in post_replies %}
|
||||||
{% include 'community/_post_reply_teaser.html' %}
|
{% include 'post/_post_reply_teaser.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -143,7 +143,7 @@
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for post in upvoted %}
|
{% for post in upvoted %}
|
||||||
<li><a href="{{ url_for('community.show_post', post_id=post.id) }}">{{ post.title }}</a></li>
|
<li><a href="{{ url_for('post.show_post', post_id=post.id) }}">{{ post.title }}</a></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue