From 856f450885aaf81ebfe71c684d60c6dfa3973228 Mon Sep 17 00:00:00 2001 From: rimu <3310831+rimu@users.noreply.github.com> Date: Thu, 30 Nov 2023 06:36:08 +1300 Subject: [PATCH] significant code reorganisation - split communities and posts --- app/__init__.py | 3 + app/community/forms.py | 4 - app/community/routes.py | 194 +---------------- app/community/util.py | 52 ----- app/models.py | 5 +- app/post/__init__.py | 5 + app/post/forms.py | 9 + app/post/routes.py | 201 ++++++++++++++++++ app/post/util.py | 58 +++++ app/static/scss/_typography.scss | 12 ++ app/static/structure.css | 12 ++ app/static/styles.css | 12 ++ app/templates/community/community.html | 2 +- app/templates/domain/domain.html | 2 +- .../{community => post}/_post_full.html | 4 +- .../_post_reply_teaser.html | 0 .../{community => post}/_post_teaser.html | 12 +- .../_post_voting_buttons.html | 4 +- .../{community => post}/_voting_buttons.html | 4 +- .../{community => post}/add_reply.html | 0 .../continue_discussion.html | 8 +- app/templates/{community => post}/post.html | 8 +- app/templates/post/post_options.html | 23 ++ app/templates/user/show_profile.html | 6 +- 24 files changed, 369 insertions(+), 271 deletions(-) create mode 100644 app/post/__init__.py create mode 100644 app/post/forms.py create mode 100644 app/post/routes.py create mode 100644 app/post/util.py rename app/templates/{community => post}/_post_full.html (96%) rename app/templates/{community => post}/_post_reply_teaser.html (100%) rename app/templates/{community => post}/_post_teaser.html (68%) rename app/templates/{community => post}/_post_voting_buttons.html (76%) rename app/templates/{community => post}/_voting_buttons.html (76%) rename app/templates/{community => post}/add_reply.html (100%) rename app/templates/{community => post}/continue_discussion.html (92%) rename app/templates/{community => post}/post.html (94%) create mode 100644 app/templates/post/post_options.html diff --git a/app/__init__.py b/app/__init__.py index 19687551..3c3475f0 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -62,6 +62,9 @@ def create_app(config_class=Config): from app.community import bp as community_bp 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 app.register_blueprint(user_bp) diff --git a/app/community/forms.py b/app/community/forms.py index 51ba62d9..19c2d39e 100644 --- a/app/community/forms.py +++ b/app/community/forms.py @@ -81,7 +81,3 @@ class CreatePost(FlaskForm): 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')) diff --git a/app/community/routes.py b/app/community/routes.py index 2b225a60..1555dcf9 100644 --- a/app/community/routes.py +++ b/app/community/routes.py @@ -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_login import login_user, logout_user, current_user, login_required from flask_babel import _ @@ -9,12 +6,12 @@ from sqlalchemy import or_, desc from app import db, constants from app.activitypub.signature import RsaKeys, HttpSignature -from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost, NewReplyForm -from app.community.util import search_for_community, community_url_exists, actor_to_community, post_replies, \ - get_comment_branch, post_reply_count, ensure_directory_exists, opengraph_parse, url_to_thumbnail_file +from app.community.forms import SearchRemoteCommunity, AddLocalCommunity, CreatePost +from app.community.util import search_for_community, community_url_exists, actor_to_community, \ + 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.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, PostReply, \ - PostReplyVote, PostVote, File +from app.models import User, Community, CommunityMember, CommunityJoinRequest, CommunityBan, Post, \ + File from app.community 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 @@ -295,184 +292,3 @@ def add_post(actor): return render_template('community/add_post.html', title=_('Add post to community'), form=form, community=community, images_disabled=images_disabled) - - -@bp.route('/post/', 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//', 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//', 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//comment/') -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//comment//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) diff --git a/app/community/util.py b/app/community/util.py index 327226b5..eefff461 100644 --- a/app/community/util.py +++ b/app/community/util.py @@ -87,58 +87,6 @@ def actor_to_community(actor) -> 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): parts = directory.split('/') rebuild_directory = '' diff --git a/app/models.py b/app/models.py index 965c5d00..5c58aff7 100644 --- a/app/models.py +++ b/app/models.py @@ -4,7 +4,7 @@ from time import time from typing import List 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 werkzeug.security import generate_password_hash, check_password_hash from flask_babel import _, lazy_gettext as _l @@ -129,6 +129,9 @@ class Community(db.Model): )) ).all() + def is_moderator(self): + return any(moderator.user_id == current_user.id for moderator in self.moderators()) + user_role = db.Table('user_role', db.Column('user_id', db.Integer, db.ForeignKey('user.id')), diff --git a/app/post/__init__.py b/app/post/__init__.py new file mode 100644 index 00000000..272b2cb0 --- /dev/null +++ b/app/post/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('post', __name__) + +from app.post import routes diff --git a/app/post/forms.py b/app/post/forms.py new file mode 100644 index 00000000..35d2e85e --- /dev/null +++ b/app/post/forms.py @@ -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')) \ No newline at end of file diff --git a/app/post/routes.py b/app/post/routes.py new file mode 100644 index 00000000..3bb94415 --- /dev/null +++ b/app/post/routes.py @@ -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/', 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//', 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//', 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//comment/') +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//comment//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//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) + diff --git a/app/post/util.py b/app/post/util.py new file mode 100644 index 00000000..87b3c1d6 --- /dev/null +++ b/app/post/util.py @@ -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() diff --git a/app/static/scss/_typography.scss b/app/static/scss/_typography.scss index 6f0d7c3d..fbf9d08e 100644 --- a/app/static/scss/_typography.scss +++ b/app/static/scss/_typography.scss @@ -166,6 +166,18 @@ content: "\e935"; } +.fe-options::before { + content: "\e99b"; +} + +.fe-edit::before { + content: "\e95a"; +} + +.fe-delete::before { + content: "\ea03"; +} + .fe-report::before { content: "\e967"; } diff --git a/app/static/structure.css b/app/static/structure.css index 9c9f2258..e44bf55b 100644 --- a/app/static/structure.css +++ b/app/static/structure.css @@ -169,6 +169,18 @@ nav, etc which are used site-wide */ content: "\e935"; } +.fe-options::before { + content: "\e99b"; +} + +.fe-edit::before { + content: "\e95a"; +} + +.fe-delete::before { + content: "\ea03"; +} + .fe-report::before { content: "\e967"; } diff --git a/app/static/styles.css b/app/static/styles.css index d50b195b..8c0f2c2c 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -168,6 +168,18 @@ content: "\e935"; } +.fe-options::before { + content: "\e99b"; +} + +.fe-edit::before { + content: "\e95a"; +} + +.fe-delete::before { + content: "\ea03"; +} + .fe-report::before { content: "\e967"; } diff --git a/app/templates/community/community.html b/app/templates/community/community.html index 34410d09..0114a4d6 100644 --- a/app/templates/community/community.html +++ b/app/templates/community/community.html @@ -37,7 +37,7 @@ {% endif %}
{% for post in posts %} - {% include 'community/_post_teaser.html' %} + {% include 'post/_post_teaser.html' %} {% else %}

{{ _('No posts in this community yet.') }}

{% endfor %} diff --git a/app/templates/domain/domain.html b/app/templates/domain/domain.html index e50a2f74..f8aa8ae4 100644 --- a/app/templates/domain/domain.html +++ b/app/templates/domain/domain.html @@ -14,7 +14,7 @@

{{ domain.name }}

{% for post in posts %} - {% include 'community/_post_teaser.html' %} + {% include 'post/_post_teaser.html' %} {% else %}

{{ _('No posts in this domain yet.') }}

{% endfor %} diff --git a/app/templates/community/_post_full.html b/app/templates/post/_post_full.html similarity index 96% rename from app/templates/community/_post_full.html rename to app/templates/post/_post_full.html index 3a64f428..08e131eb 100644 --- a/app/templates/community/_post_full.html +++ b/app/templates/post/_post_full.html @@ -10,7 +10,7 @@
- {% include "community/_post_voting_buttons.html" %} + {% include "post/_post_voting_buttons.html" %}

{{ post.title }}

{% if post.url %} @@ -37,7 +37,7 @@
- {% include "community/_post_voting_buttons.html" %} + {% include "post/_post_voting_buttons.html" %}
{% if post.type == POST_TYPE_LINK and post.image_id and not (post.url and 'youtube.com' in post.url) %}
diff --git a/app/templates/community/_post_reply_teaser.html b/app/templates/post/_post_reply_teaser.html similarity index 100% rename from app/templates/community/_post_reply_teaser.html rename to app/templates/post/_post_reply_teaser.html diff --git a/app/templates/community/_post_teaser.html b/app/templates/post/_post_teaser.html similarity index 68% rename from app/templates/community/_post_teaser.html rename to app/templates/post/_post_teaser.html index 23ec18b9..952077ea 100644 --- a/app/templates/community/_post_teaser.html +++ b/app/templates/post/_post_teaser.html @@ -4,7 +4,7 @@

- {{ post.title }} + {{ post.title }} {% if post.type == POST_TYPE_IMAGE %} {% endif %} {% if post.type == POST_TYPE_LINK and post.domain_id %} {% if post.url and 'youtube.com' in post.url %} @@ -19,7 +19,7 @@ {{ render_username(post.author) }} ยท {{ moment(post.posted_at).fromNow() }} {% if post.image_id %}
- {{ post.image.alt_text }}{{ post.image.alt_text }}
{% endif %} @@ -28,15 +28,15 @@

-
{{ post.score }}
+
- {% include "community/_post_voting_buttons.html" %} + {% include "post/_post_voting_buttons.html" %}
diff --git a/app/templates/community/_post_voting_buttons.html b/app/templates/post/_post_voting_buttons.html similarity index 76% rename from app/templates/community/_post_voting_buttons.html rename to app/templates/post/_post_voting_buttons.html index ad966bcc..6e5cfa36 100644 --- a/app/templates/community/_post_voting_buttons.html +++ b/app/templates/post/_post_voting_buttons.html @@ -1,11 +1,11 @@ {% if current_user.is_authenticated and current_user.verified %}
+ hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons"> {{ post.up_votes }}
+ hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons"> {{ post.down_votes }}
diff --git a/app/templates/community/_voting_buttons.html b/app/templates/post/_voting_buttons.html similarity index 76% rename from app/templates/community/_voting_buttons.html rename to app/templates/post/_voting_buttons.html index 1b2f300c..fd8f0b90 100644 --- a/app/templates/community/_voting_buttons.html +++ b/app/templates/post/_voting_buttons.html @@ -1,11 +1,11 @@ {% if current_user.is_authenticated and current_user.verified %}
+ hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons"> {{ comment.up_votes }}
+ hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons"> {{ comment.down_votes }}
diff --git a/app/templates/community/add_reply.html b/app/templates/post/add_reply.html similarity index 100% rename from app/templates/community/add_reply.html rename to app/templates/post/add_reply.html diff --git a/app/templates/community/continue_discussion.html b/app/templates/post/continue_discussion.html similarity index 92% rename from app/templates/community/continue_discussion.html rename to app/templates/post/continue_discussion.html index ae12efd4..2ffb9f81 100644 --- a/app/templates/community/continue_discussion.html +++ b/app/templates/post/continue_discussion.html @@ -4,8 +4,8 @@ {% block app_content %}
- {% include 'community/_post_full.html' %} -

Back to main discussion

+ {% include 'post/_post_full.html' %} +

Back to main discussion

{% macro render_comment(comment) %} @@ -13,7 +13,7 @@
{% with comment=comment['comment'] %} - {% include "community/_voting_buttons.html" %} + {% include "post/_voting_buttons.html" %} {% endwith %}
@@ -36,7 +36,7 @@
{% if comment['replies'] %}
diff --git a/app/templates/community/post.html b/app/templates/post/post.html similarity index 94% rename from app/templates/community/post.html rename to app/templates/post/post.html index 6e12cd46..b613cd6e 100644 --- a/app/templates/community/post.html +++ b/app/templates/post/post.html @@ -7,7 +7,7 @@
- {% include 'community/_post_full.html' %} + {% include 'post/_post_full.html' %} {% if post.comments_enabled %} {% if current_user.is_authenticated %} {% if current_user.verified %} @@ -39,7 +39,7 @@
{% with comment=comment['comment'] %} - {% include "community/_voting_buttons.html" %} + {% include "post/_voting_buttons.html" %} {% endwith %}
@@ -72,7 +72,7 @@
{% if current_user.is_authenticated and current_user.verified %} {% endif %} {% if comment['replies'] %} @@ -84,7 +84,7 @@
{% else %} {% endif %} diff --git a/app/templates/post/post_options.html b/app/templates/post/post_options.html new file mode 100644 index 00000000..2f636075 --- /dev/null +++ b/app/templates/post/post_options.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% from 'bootstrap/form.html' import render_form %} + +{% block app_content %} +
+ +
+{% endblock %} \ No newline at end of file diff --git a/app/templates/user/show_profile.html b/app/templates/user/show_profile.html index fa17ea16..09f5578c 100644 --- a/app/templates/user/show_profile.html +++ b/app/templates/user/show_profile.html @@ -42,7 +42,7 @@

Posts

{% for post in posts %} - {% include 'community/_post_teaser.html' %} + {% include 'post/_post_teaser.html' %} {% endfor %}
{% endif %} @@ -51,7 +51,7 @@

Comments

{% for post_reply in post_replies %} - {% include 'community/_post_reply_teaser.html' %} + {% include 'post/_post_reply_teaser.html' %} {% endfor %}
{% endif %} @@ -143,7 +143,7 @@