significant code reorganisation - split communities and posts

This commit is contained in:
rimu 2023-11-30 06:36:08 +13:00
parent 2fb1abbc08
commit 856f450885
24 changed files with 369 additions and 271 deletions

View file

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

View file

@ -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'))

View file

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

View file

@ -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 = ''

View file

@ -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
View 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
View 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
View 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
View 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()

View file

@ -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";
} }

View file

@ -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";
} }

View file

@ -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";
} }

View file

@ -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 %}

View file

@ -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 %}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 %}

View 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 %}

View file

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