show if a piece of content has already been voted on

This commit is contained in:
rimu 2024-04-11 14:04:57 +12:00
parent 7b162792ee
commit bd6c71d8bb
8 changed files with 109 additions and 22 deletions

View file

@ -30,7 +30,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
shorten_string, gibberish, community_membership, ap_datetime, \
request_etag_matches, return_304, instance_banned, can_create_post, can_upvote, can_downvote, user_filters_posts, \
joined_communities, moderating_communities, blocked_domains, mimetype_from_url, blocked_instances, \
community_moderators, communities_banned_from, show_ban_message
community_moderators, communities_banned_from, show_ban_message, recently_upvoted_posts, recently_downvoted_posts
from feedgen.feed import FeedGenerator
from datetime import timezone, timedelta
@ -241,12 +241,21 @@ def show_community(community: Community):
prev_url = url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name,
page=posts.prev_num, sort=sort, layout=post_layout) if posts.has_prev and page != 1 else None
# Voting history
if current_user.is_authenticated:
recently_upvoted = recently_upvoted_posts(current_user.id)
recently_downvoted = recently_downvoted_posts(current_user.id)
else:
recently_upvoted = []
recently_downvoted = []
return render_template('community/community.html', community=community, title=community.title, breadcrumbs=breadcrumbs,
is_moderator=is_moderator, is_owner=is_owner, is_admin=is_admin, mods=mod_list, posts=posts, description=description,
og_image=og_image, POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK, SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING,
SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER, SUBSCRIPTION_OWNER=SUBSCRIPTION_OWNER, SUBSCRIPTION_MODERATOR=SUBSCRIPTION_MODERATOR,
etag=f"{community.id}{sort}{post_layout}_{hash(community.last_active)}", related_communities=related_communities,
next_url=next_url, prev_url=prev_url, low_bandwidth=low_bandwidth,
recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted,
rss_feed=f"https://{current_app.config['SERVER_NAME']}/community/{community.link()}/feed", rss_feed_name=f"{community.title} on PieFed",
content_filters=content_filters, moderating_communities=moderating_communities(current_user.get_id()),
joined_communities=joined_communities(current_user.get_id()), sort=sort,

View file

@ -25,7 +25,7 @@ from sqlalchemy_searchable import search
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
ap_datetime, ip_address, retrieve_block_list, shorten_string, markdown_to_text, user_filters_home, \
joined_communities, moderating_communities, parse_page, theme_list, get_request, markdown_to_html, allowlist_html, \
blocked_instances, communities_banned_from, topic_tree
blocked_instances, communities_banned_from, topic_tree, recently_upvoted_posts, recently_downvoted_posts
from app.models import Community, CommunityMember, Post, Site, User, utcnow, Domain, Topic, File, Instance, \
InstanceRole, Notification
from PIL import Image
@ -140,9 +140,18 @@ def home_page(type, sort):
active_communities = active_communities.filter(Community.id.not_in(banned_from))
active_communities = active_communities.order_by(desc(Community.last_active)).limit(5).all()
# Voting history
if current_user.is_authenticated:
recently_upvoted = recently_upvoted_posts(current_user.id)
recently_downvoted = recently_downvoted_posts(current_user.id)
else:
recently_upvoted = []
recently_downvoted = []
return render_template('index.html', posts=posts, active_communities=active_communities, show_post_community=True,
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK,
low_bandwidth=low_bandwidth,
low_bandwidth=low_bandwidth, recently_upvoted=recently_upvoted,
recently_downvoted=recently_downvoted,
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
etag=f"{type}_{sort}_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url,
#rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed",
@ -294,6 +303,9 @@ def list_files(directory):
@bp.route('/test')
def test():
x = recently_upvoted_posts(1)
return x
md = "::: spoiler I'm all for ya having fun and your right to hurt yourself.\n\nI am a former racer, commuter, and professional Buyer for a chain of bike shops. I'm also disabled from the crash involving the 6th and 7th cars that have hit me in the last 170k+ miles of riding. I only barely survived what I simplify as a \"broken neck and back.\" Cars making U-turns are what will get you if you ride long enough, \n\nespecially commuting. It will look like just another person turning in front of you, you'll compensate like usual, and before your brain can even register what is really happening, what was your normal escape route will close and you're going to crash really hard. It is the only kind of crash that your intuition is useless against.\n:::"

View file

@ -24,7 +24,8 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
shorten_string, markdown_to_text, gibberish, ap_datetime, return_304, \
request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking, \
reply_already_exists, reply_is_just_link_to_gif_reaction, confidence, moderating_communities, joined_communities, \
blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message
blocked_instances, blocked_domains, community_moderators, blocked_phrases, show_ban_message, recently_upvoted_posts, \
recently_downvoted_posts, recently_upvoted_post_replies, recently_downvoted_post_replies
def show_post(post_id: int):
@ -239,12 +240,26 @@ def show_post(post_id: int):
breadcrumb.url = '/communities'
breadcrumbs.append(breadcrumb)
# Voting history
if current_user.is_authenticated:
recently_upvoted = recently_upvoted_posts(current_user.id)
recently_downvoted = recently_downvoted_posts(current_user.id)
recently_upvoted_replies = recently_upvoted_post_replies(current_user.id)
recently_downvoted_replies = recently_downvoted_post_replies(current_user.id)
else:
recently_upvoted = []
recently_downvoted = []
recently_upvoted_replies = []
recently_downvoted_replies = []
response = render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
breadcrumbs=breadcrumbs, related_communities=related_communities, mods=mod_list,
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,
noindex=not post.author.indexable,
recently_upvoted=recently_upvoted, recently_downvoted=recently_downvoted,
recently_upvoted_replies=recently_upvoted_replies, recently_downvoted_replies=recently_downvoted_replies,
etag=f"{post.id}{sort}_{hash(post.last_active)}", markdown_editor=current_user.is_authenticated and current_user.markdown_editor,
low_bandwidth=request.cookies.get('low_bandwidth', '0') == '1', SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
moderating_communities=moderating_communities(current_user.get_id()),
@ -259,7 +274,6 @@ def show_post(post_id: int):
@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:
@ -275,7 +289,6 @@ def post_vote(post_id: int, vote_direction):
post.up_votes -= 1
post.down_votes += 1
post.score -= 2
downvoted_class = 'voted_down'
else: # previous vote was down
if vote_direction == 'downvote': # new vote is also down, so remove it
db.session.delete(existing_vote)
@ -286,18 +299,15 @@ def post_vote(post_id: int, vote_direction):
post.up_votes += 1
post.down_votes -= 1
post.score += 2
upvoted_class = 'voted_up'
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)
# upvotes do not increase reputation in low quality communities
@ -346,17 +356,25 @@ def post_vote(post_id: int, vote_direction):
current_user.recalculate_attitude()
db.session.commit()
post.flush_cache()
recently_upvoted = []
recently_downvoted = []
if vote_direction == 'upvote':
recently_upvoted = [post_id]
elif vote_direction == 'downvote':
recently_downvoted = [post_id]
cache.delete_memoized(recently_upvoted_posts, current_user.id)
cache.delete_memoized(recently_downvoted_posts, current_user.id)
template = 'post/_post_voting_buttons.html' if request.args.get('style', '') == '' else 'post/_post_voting_buttons_masonry.html'
return render_template(template, post=post, community=post.community,
upvoted_class=upvoted_class,
downvoted_class=downvoted_class)
return render_template(template, post=post, community=post.community, recently_upvoted=recently_upvoted,
recently_downvoted=recently_downvoted)
@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:
@ -423,9 +441,20 @@ def comment_vote(comment_id, vote_direction):
db.session.commit()
comment.post.flush_cache()
recently_upvoted = []
recently_downvoted = []
if vote_direction == 'upvote':
recently_upvoted = [comment_id]
elif vote_direction == 'downvote':
recently_downvoted = [comment_id]
cache.delete_memoized(recently_upvoted_post_replies, current_user.id)
cache.delete_memoized(recently_downvoted_post_replies, current_user.id)
return render_template('post/_comment_voting_buttons.html', comment=comment,
upvoted_class=upvoted_class,
downvoted_class=downvoted_class, community=comment.community)
recently_upvoted_replies=recently_upvoted,
recently_downvoted_replies=recently_downvoted,
community=comment.community)
@bp.route('/post/<int:post_id>/comment/<int:comment_id>')

View file

@ -1,6 +1,6 @@
{% if current_user.is_authenticated and current_user.verified %}
{% if can_upvote(current_user, community) %}
<div class="upvote_button {{ upvoted_class }}" role="button" aria-label="{{ _('UpVote button.') }}" aria-live="assertive"
<div class="upvote_button {{ 'voted_up' if in_sorted_list(recently_upvoted_replies, comment.id) }}" role="button" aria-label="{{ _('UpVote button.') }}" aria-live="assertive"
hx-post="/comment/{{ comment.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_new" tabindex="0">
<span class="fe fe-arrow-up"></span>
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
@ -8,7 +8,7 @@
{% endif %}
<span title="{{ comment.up_votes }}, {{ comment.down_votes }}" aria-live="assertive" aria-label="{{ _('Score: ') }}{{ comment.up_votes - comment.down_votes }}.">{{ comment.up_votes - comment.down_votes }}</span>
{% if can_downvote(current_user, community) %}
<div class="downvote_button {{ downvoted_class }}" role="button" aria-label="{{ _('DownVote button.') }}" aria-live="assertive"
<div class="downvote_button {{ 'voted_down' if in_sorted_list(recently_downvoted_replies, comment.id) }}" role="button" aria-label="{{ _('DownVote button.') }}" aria-live="assertive"
hx-post="/comment/{{ comment.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_new" tabindex="0">
<span class="fe fe-arrow-down"></span>
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">

View file

@ -1,6 +1,6 @@
{% if current_user.is_authenticated and current_user.verified %}
{% if can_upvote(current_user, post.community) %}
<div class="upvote_button {{ upvoted_class }}" role="button" aria-label="{{ _('UpVote button, %(count)d upvotes so far.', count=post.up_votes) }}" aria-live="assertive"
<div class="upvote_button {{ 'voted_up' if in_sorted_list(recently_upvoted, post.id) }}" role="button" aria-label="{{ _('UpVote button, %(count)d upvotes so far.', count=post.up_votes) }}" aria-live="assertive"
hx-post="/post/{{ post.id }}/upvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons" tabindex="0">
<span class="fe fe-arrow-up"></span>
{{ shorten_number(post.up_votes) }}
@ -8,7 +8,7 @@
</div>
{% endif %}
{% if can_downvote(current_user, post.community) %}
<div class="downvote_button {{ downvoted_class }}" role="button" aria-label="{{ _('DownVote button, %(count)d downvotes so far.', count=post.down_votes) }}" aria-live="assertive"
<div class="downvote_button {{ 'voted_down' if in_sorted_list(recently_downvoted, post.id) }}" role="button" aria-label="{{ _('DownVote button, %(count)d downvotes so far.', count=post.down_votes) }}" aria-live="assertive"
hx-post="/post/{{ post.id }}/downvote" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons" tabindex="0">
<span class="fe fe-arrow-down"></span>
{{ shorten_number(post.down_votes) }}

View file

@ -1,13 +1,13 @@
{% if current_user.is_authenticated and current_user.verified %}
{% if can_upvote(current_user, post.community) %}
<div class="upvote_button {{ upvoted_class }}" role="button" aria-label="{{ _('UpVote') }}" aria-live="assertive"
<div class="upvote_button {{ 'voted_up' if in_sorted_list(recently_upvoted, post.id) }}" role="button" aria-label="{{ _('UpVote') }}" aria-live="assertive"
hx-post="/post/{{ post.id }}/upvote?style=masonry" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_masonry" tabindex="0" title="{{ post.up_votes }} upvotes">
<span class="fe fe-arrow-up"></span>
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">
</div>
{% endif %}
{% if can_downvote(current_user, post.community) %}
<div class="downvote_button {{ downvoted_class }}" role="button" aria-label="{{ _('DownVote') }}" aria-live="assertive"
<div class="downvote_button {{ 'voted_down' if in_sorted_list(recently_downvoted, post.id) }}" role="button" aria-label="{{ _('DownVote') }}" aria-live="assertive"
hx-post="/post/{{ post.id }}/downvote?style=masonry" hx-trigger="click throttle:1s" hx-target="closest .voting_buttons_masonry" tabindex="0" title="{{ post.down_votes }} downvotes">
<span class="fe fe-arrow-down"></span>
<img class="htmx-indicator" src="/static/images/spinner.svg" alt="" style="opacity: 0;">

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import bisect
import hashlib
import mimetypes
import random
@ -861,3 +862,37 @@ def show_ban_message():
resp = make_response(redirect(url_for('main.index')))
resp.set_cookie('sesion', '17489047567495', expires=datetime(year=2099, month=12, day=30))
return resp
# search a sorted list using a binary search. Faster than using 'in' with a unsorted list.
def in_sorted_list(arr, target):
index = bisect.bisect_left(arr, target)
return index < len(arr) and arr[index] == target
@cache.memoize(timeout=600)
def recently_upvoted_posts(user_id) -> List[int]:
post_ids = db.session.execute(text('SELECT post_id FROM "post_vote" WHERE user_id = :user_id AND effect > 0 ORDER BY id DESC LIMIT 1000'),
{'user_id': user_id}).scalars()
return sorted(post_ids) # sorted so that in_sorted_list can be used
@cache.memoize(timeout=600)
def recently_downvoted_posts(user_id) -> List[int]:
post_ids = db.session.execute(text('SELECT post_id FROM "post_vote" WHERE user_id = :user_id AND effect < 0 ORDER BY id DESC LIMIT 1000'),
{'user_id': user_id}).scalars()
return sorted(post_ids)
@cache.memoize(timeout=600)
def recently_upvoted_post_replies(user_id) -> List[int]:
reply_ids = db.session.execute(text('SELECT post_reply_id FROM "post_reply_vote" WHERE user_id = :user_id AND effect > 0 ORDER BY id DESC LIMIT 1000'),
{'user_id': user_id}).scalars()
return sorted(reply_ids) # sorted so that in_sorted_list can be used
@cache.memoize(timeout=600)
def recently_downvoted_post_replies(user_id) -> List[int]:
reply_ids = db.session.execute(text('SELECT post_reply_id FROM "post_reply_vote" WHERE user_id = :user_id AND effect < 0 ORDER BY id DESC LIMIT 1000'),
{'user_id': user_id}).scalars()
return sorted(reply_ids)

View file

@ -11,7 +11,8 @@ from flask import session, g, json, request, current_app
from app.constants import POST_TYPE_LINK, POST_TYPE_IMAGE, POST_TYPE_ARTICLE
from app.models import Site
from app.utils import getmtime, gibberish, shorten_string, shorten_url, digits, user_access, community_membership, \
can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href
can_create_post, can_upvote, can_downvote, shorten_number, ap_datetime, current_theme, community_link_to_href, \
in_sorted_list
app = create_app()
cli.register(app)
@ -42,6 +43,7 @@ with app.app_context():
app.jinja_env.globals['can_create'] = can_create_post
app.jinja_env.globals['can_upvote'] = can_upvote
app.jinja_env.globals['can_downvote'] = can_downvote
app.jinja_env.globals['in_sorted_list'] = in_sorted_list
app.jinja_env.globals['theme'] = current_theme
app.jinja_env.globals['file_exists'] = os.path.exists
app.jinja_env.filters['community_links'] = community_link_to_href