mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 11:26:56 -08:00
show if a piece of content has already been voted on
This commit is contained in:
parent
7b162792ee
commit
bd6c71d8bb
8 changed files with 109 additions and 22 deletions
|
@ -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,
|
||||
|
|
|
@ -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:::"
|
||||
|
||||
|
|
|
@ -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>')
|
||||
|
|
|
@ -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;">
|
||||
|
|
|
@ -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) }}
|
||||
|
|
|
@ -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;">
|
||||
|
|
35
app/utils.py
35
app/utils.py
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue