vote for posts

This commit is contained in:
rimu 2023-10-23 17:22:21 +13:00
parent b05531fda3
commit 8737d3cbad
8 changed files with 227 additions and 130 deletions

View file

@ -53,7 +53,7 @@ class CreatePost(FlaskForm):
self.link_url.errors.append(_('URL is required.')) self.link_url.errors.append(_('URL is required.'))
return False return False
domain = domain_from_url(self.link_url.data) domain = domain_from_url(self.link_url.data)
if domain.banned: if domain and domain.banned:
self.link_url.errors.append(_(f"Links to %s are not allowed.".format(domain.name))) self.link_url.errors.append(_(f"Links to %s are not allowed.".format(domain.name)))
return False return False
elif self.type.data == 'image': elif self.type.data == 'image':

View file

@ -9,10 +9,10 @@ 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, NewReplyForm
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, post_replies, \
get_comment_branch 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.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, PostReply, \
PostReplyVote PostReplyVote, PostVote
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
@ -252,6 +252,58 @@ def show_post(post_id: int):
canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH) canonical=post.ap_id, form=form, replies=replies, THREAD_CUTOFF_DEPTH=constants.THREAD_CUTOFF_DEPTH)
@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']) @bp.route('/comment/<int:comment_id>/<vote_direction>', methods=['POST'])
@login_required @login_required
@validation_required @validation_required
@ -293,7 +345,8 @@ def comment_vote(comment_id, vote_direction):
comment.down_votes += 1 comment.down_votes += 1
comment.score -= 1 comment.score -= 1
downvoted_class = 'voted_down' downvoted_class = 'voted_down'
vote = PostReplyVote(user_id=current_user.id, post_reply_id=comment_id, author_id=comment.user_id, effect=effect) 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.add(vote)
db.session.commit() db.session.commit()
return render_template('community/_voting_buttons.html', comment=comment, return render_template('community/_voting_buttons.html', comment=comment,
@ -331,6 +384,7 @@ def add_reply(post_id: int, comment_id: int):
reply_vote = PostReplyVote(user_id=current_user.id, author_id=current_user.id, post_reply_id=reply.id, reply_vote = PostReplyVote(user_id=current_user.id, author_id=current_user.id, post_reply_id=reply.id,
effect=1.0) effect=1.0)
db.session.add(reply_vote) db.session.add(reply_vote)
post.reply_count = post_reply_count(post.id)
db.session.commit() db.session.commit()
form.body.data = '' form.body.data = ''
flash('Your comment has been added.') flash('Your comment has been added.')

View file

@ -126,3 +126,9 @@ def get_comment_branch(post_id: int, comment_id: int, sort_by: str) -> List[Post
parent_comment['replies'].append(comments_dict[comment.id]) parent_comment['replies'].append(comments_dict[comment.id])
return [comment for comment in comments_dict.values() if comment['comment'].id == 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('SELECT COUNT(id) as c FROM "post_reply" WHERE post_id = :post_id',
{'post_id': post_id}).scalar()

View file

@ -378,6 +378,44 @@ fieldset legend {
padding-right: 5px; padding-right: 5px;
} }
.voting_buttons {
float: right;
display: block;
width: 60px;
padding: 5px;
}
.voting_buttons div {
border: solid 1px #0071CE;
}
.voting_buttons .upvote_button, .voting_buttons .downvote_button {
padding-left: 3px;
border-radius: 3px;
cursor: pointer;
}
.voting_buttons .upvote_button.digits_4, .voting_buttons .downvote_button.digits_4 {
width: 68px;
}
.voting_buttons .upvote_button.digits_5, .voting_buttons .downvote_button.digits_5 {
width: 76px;
}
.voting_buttons .upvote_button.digits_6, .voting_buttons .downvote_button.digits_6 {
width: 84px;
}
.voting_buttons .upvote_button.voted_up, .voting_buttons .downvote_button.voted_up {
color: green;
font-weight: bold;
}
.voting_buttons .upvote_button.voted_down, .voting_buttons .downvote_button.voted_down {
color: darkred;
font-weight: bold;
}
.voting_buttons .downvote_button {
margin-top: 5px;
}
.voting_buttons a {
text-decoration: none;
}
.comment { .comment {
clear: both; clear: both;
margin-bottom: 20px; margin-bottom: 20px;
@ -413,43 +451,6 @@ fieldset legend {
.comment .hide_button a { .comment .hide_button a {
text-decoration: none; text-decoration: none;
} }
.comment .voting_buttons {
float: right;
display: block;
width: 60px;
padding: 5px;
}
.comment .voting_buttons div {
border: solid 1px #0071CE;
}
.comment .voting_buttons .upvote_button, .comment .voting_buttons .downvote_button {
padding-left: 3px;
border-radius: 3px;
cursor: pointer;
}
.comment .voting_buttons .upvote_button.digits_4, .comment .voting_buttons .downvote_button.digits_4 {
width: 68px;
}
.comment .voting_buttons .upvote_button.digits_5, .comment .voting_buttons .downvote_button.digits_5 {
width: 76px;
}
.comment .voting_buttons .upvote_button.digits_6, .comment .voting_buttons .downvote_button.digits_6 {
width: 84px;
}
.comment .voting_buttons .upvote_button.voted_up, .comment .voting_buttons .downvote_button.voted_up {
color: green;
font-weight: bold;
}
.comment .voting_buttons .upvote_button.voted_down, .comment .voting_buttons .downvote_button.voted_down {
color: darkred;
font-weight: bold;
}
.comment .voting_buttons .downvote_button {
margin-top: 5px;
}
.comment .voting_buttons a {
text-decoration: none;
}
.comment .comment_actions { .comment .comment_actions {
margin-top: -10px; margin-top: -10px;
} }

View file

@ -148,6 +148,52 @@ nav, etc which are used site-wide */
padding-right: 5px; padding-right: 5px;
} }
.voting_buttons {
float: right;
display: block;
width: 60px;
padding: 5px;
div {
border: solid 1px $primary-colour;
}
.upvote_button, .downvote_button {
padding-left: 3px;
border-radius: 3px;
cursor: pointer;
&.digits_4 {
width: 68px;
}
&.digits_5 {
width: 76px;
}
&.digits_6 {
width: 84px;
}
&.voted_up {
color: green;
font-weight: bold;
}
&.voted_down {
color: darkred;
font-weight: bold;
}
}
.downvote_button {
margin-top: 5px;
}
a {
text-decoration: none;
}
}
.comment { .comment {
clear: both; clear: both;
margin-bottom: 20px; margin-bottom: 20px;
@ -192,52 +238,6 @@ nav, etc which are used site-wide */
} }
} }
.voting_buttons {
float: right;
display: block;
width: 60px;
padding: 5px;
div {
border: solid 1px $primary-colour;
}
.upvote_button, .downvote_button {
padding-left: 3px;
border-radius: 3px;
cursor: pointer;
&.digits_4 {
width: 68px;
}
&.digits_5 {
width: 76px;
}
&.digits_6 {
width: 84px;
}
&.voted_up {
color: green;
font-weight: bold;
}
&.voted_down {
color: darkred;
font-weight: bold;
}
}
.downvote_button {
margin-top: 5px;
}
a {
text-decoration: none;
}
}
.comment_actions { .comment_actions {
margin-top: -10px; margin-top: -10px;
a { a {

View file

@ -9,6 +9,9 @@
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li> <li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
</ol> </ol>
</nav> </nav>
<div class="voting_buttons">
{% include "community/_post_voting_buttons.html" %}
</div>
<h1 class="mt-2">{{ post.title }}</h1> <h1 class="mt-2">{{ post.title }}</h1>
{% if post.url %} {% if post.url %}
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}</a> <p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}</a>
@ -29,6 +32,7 @@
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}
<div class="col">
<nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation"> <nav aria-label="breadcrumb" id="breadcrumb_nav" title="Navigation">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li> <li class="breadcrumb-item"><a href="/">{{ _('Home') }}</a></li>
@ -37,6 +41,9 @@
<li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li> <li class="breadcrumb-item active">{{ post.title|shorten(15) }}</li>
</ol> </ol>
</nav> </nav>
<div class="voting_buttons">
{% include "community/_post_voting_buttons.html" %}
</div>
<h1 class="mt-2">{{ post.title }}</h1> <h1 class="mt-2">{{ post.title }}</h1>
{% if post.url %} {% if post.url %}
<p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }} <p><small><a href="{{ post.url }}" rel="nofollow ugc">{{ post.url|shorten_url }}
@ -49,6 +56,7 @@
<p class="small">submitted {{ moment(post.posted_at).fromNow() }} by <p class="small">submitted {{ moment(post.posted_at).fromNow() }} by
{{ render_username(post.author) }} {{ render_username(post.author) }}
</p> </p>
</div>
{% endif %} {% endif %}
</div> </div>

View file

@ -1,4 +1,6 @@
<div class="post_teaser"> <div class="post_teaser">
<div class="row">
<div class="col col-md-10">
<div class="row meta_row small"> <div class="row meta_row small">
<div class="col">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</div> <div class="col">{{ render_username(post.author) }} · {{ moment(post.posted_at).fromNow() }}</div>
</div> </div>
@ -19,13 +21,18 @@
{% endif %} {% endif %}
</div> </div>
<div class="row utilities_row"> <div class="row utilities_row">
<div class="col-4">
up {{ post.score }} down
</div>
<div class="col-6"> <div class="col-6">
<a href="{{ url_for('community.show_post', post_id=post.id, _anchor='replies') }}">{{post.reply_count}}</a> additional tools <a href="{{ url_for('community.show_post', post_id=post.id, _anchor='replies') }}">{{ post.reply_count }}</a> additional tools
</div> </div>
<div class="col-2">...</div> <div class="col-2">...</div>
</div> </div>
</div>
<div class="col col-md-2">
<div class="voting_buttons pt-2">
{% include "community/_post_voting_buttons.html" %}
</div>
</div>
</div>
</div> </div>

View file

@ -0,0 +1,21 @@
{% if current_user.is_authenticated and current_user.verified %}
<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">
<span class="fe fe-arrow-up"></span>
{{ post.up_votes }}
</div>
<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">
<span class="fe fe-arrow-down"></span>
{{ post.down_votes }}
</div>
{% else %}
<div class="upvote_button digits_{{ digits(post.up_votes) }} {{ upvoted_class }}">
<span class="fe fe-arrow-up"></span>
{{ post.up_votes }}
</div>
<div class="downvote_button digits_{{ digits(post.down_votes) }} {{ downvoted_class }}">
<span class="fe fe-arrow-down"></span>
{{ post.down_votes }}
</div>
{% endif %}