mirror of
https://codeberg.org/rimu/pyfedi
synced 2025-01-23 19:36:56 -08:00
sort posts by top, hot, new
This commit is contained in:
parent
acfe35d98d
commit
b431a79518
16 changed files with 298 additions and 30 deletions
|
@ -701,7 +701,7 @@ def process_inbox_request(request_json, activitypublog_id, ip_address):
|
||||||
user.instance.ip_address = ip_address
|
user.instance.ip_address = ip_address
|
||||||
user.instance.dormant = False
|
user.instance.dormant = False
|
||||||
if 'community' in vars() and community is not None:
|
if 'community' in vars() and community is not None:
|
||||||
if community.is_local() and request_json['type'] not in ['Announce', 'Follow', 'Accept']:
|
if community.is_local() and request_json['type'] not in ['Announce', 'Follow', 'Accept', 'Undo']:
|
||||||
announce_activity_to_followers(community, user, request_json)
|
announce_activity_to_followers(community, user, request_json)
|
||||||
# community.flush_cache()
|
# community.flush_cache()
|
||||||
if 'post' in vars() and post is not None:
|
if 'post' in vars() and post is not None:
|
||||||
|
|
|
@ -20,7 +20,7 @@ from PIL import Image, ImageOps
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from app.utils import get_request, allowlist_html, html_to_markdown, get_setting, ap_datetime, markdown_to_html, \
|
from app.utils import get_request, allowlist_html, html_to_markdown, get_setting, ap_datetime, markdown_to_html, \
|
||||||
is_image_url, domain_from_url, gibberish, ensure_directory_exists, markdown_to_text, head_request
|
is_image_url, domain_from_url, gibberish, ensure_directory_exists, markdown_to_text, head_request, post_ranking
|
||||||
|
|
||||||
|
|
||||||
def public_key():
|
def public_key():
|
||||||
|
@ -729,6 +729,8 @@ def downvote_post(post, user):
|
||||||
db.session.add(vote)
|
db.session.add(vote)
|
||||||
else:
|
else:
|
||||||
pass # they have already downvoted this post
|
pass # they have already downvoted this post
|
||||||
|
post.ranking = post_ranking(post.score, post.posted_at)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def downvote_post_reply(comment, user):
|
def downvote_post_reply(comment, user):
|
||||||
|
@ -831,6 +833,8 @@ def upvote_post(post, user):
|
||||||
effect = 0
|
effect = 0
|
||||||
post.author.reputation += effect
|
post.author.reputation += effect
|
||||||
db.session.add(vote)
|
db.session.add(vote)
|
||||||
|
post.ranking = post_ranking(post.score, post.posted_at)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def delete_post_or_comment(user_ap_id, community_ap_id, to_be_deleted_ap_id):
|
def delete_post_or_comment(user_ap_id, community_ap_id, to_be_deleted_ap_id):
|
||||||
|
@ -972,6 +976,7 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
||||||
post.image = image
|
post.image = image
|
||||||
if post is not None:
|
if post is not None:
|
||||||
db.session.add(post)
|
db.session.add(post)
|
||||||
|
post.ranking = post_ranking(post.score, post.posted_at)
|
||||||
community.post_count += 1
|
community.post_count += 1
|
||||||
community.last_active = utcnow()
|
community.last_active = utcnow()
|
||||||
activity_log.result = 'success'
|
activity_log.result = 'success'
|
||||||
|
@ -986,7 +991,7 @@ def create_post(activity_log: ActivityPubLog, community: Community, request_json
|
||||||
db.session.add(vote)
|
db.session.add(vote)
|
||||||
post.up_votes += 1
|
post.up_votes += 1
|
||||||
post.score += 1
|
post.score += 1
|
||||||
post.ranking += 1
|
post.ranking = post_ranking(post.score, post.posted_at)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return post
|
return post
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ from app.utils import get_setting, render_template, allowlist_html, markdown_to_
|
||||||
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \
|
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, community_membership, ap_datetime, \
|
||||||
request_etag_matches, return_304, instance_banned, can_create, can_upvote, can_downvote
|
request_etag_matches, return_304, instance_banned, can_create, can_upvote, can_downvote
|
||||||
from feedgen.feed import FeedGenerator
|
from feedgen.feed import FeedGenerator
|
||||||
from datetime import timezone
|
from datetime import timezone, timedelta
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/add_local', methods=['GET', 'POST'])
|
@bp.route('/add_local', methods=['GET', 'POST'])
|
||||||
|
@ -98,6 +98,7 @@ def show_community(community: Community):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
page = request.args.get('page', 1, type=int)
|
page = request.args.get('page', 1, type=int)
|
||||||
|
sort = request.args.get('sort', '')
|
||||||
|
|
||||||
mods = community.moderators()
|
mods = community.moderators()
|
||||||
|
|
||||||
|
@ -113,17 +114,24 @@ def show_community(community: Community):
|
||||||
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
|
mod_list = User.query.filter(User.id.in_(mod_user_ids)).all()
|
||||||
|
|
||||||
if current_user.is_anonymous or current_user.ignore_bots:
|
if current_user.is_anonymous or current_user.ignore_bots:
|
||||||
posts = community.posts.filter(Post.from_bot == False).order_by(desc(Post.last_active)).paginate(page=page, per_page=100, error_out=False)
|
posts = community.posts.filter(Post.from_bot == False)
|
||||||
else:
|
else:
|
||||||
posts = community.posts.order_by(desc(Post.last_active)).paginate(page=page, per_page=100, error_out=False)
|
posts = community.posts
|
||||||
|
if sort == '' or sort == 'hot':
|
||||||
|
posts = posts.order_by(desc(Post.ranking))
|
||||||
|
elif sort == 'top':
|
||||||
|
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=7)).order_by(desc(Post.score))
|
||||||
|
elif sort == 'new':
|
||||||
|
posts = posts.order_by(desc(Post.posted_at))
|
||||||
|
posts = posts.paginate(page=page, per_page=100, error_out=False)
|
||||||
|
|
||||||
description = shorten_string(community.description, 150) if community.description else None
|
description = shorten_string(community.description, 150) if community.description else None
|
||||||
og_image = community.image.source_url if community.image_id else None
|
og_image = community.image.source_url if community.image_id else None
|
||||||
|
|
||||||
next_url = url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name,
|
next_url = url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name,
|
||||||
page=posts.next_num) if posts.has_next else None
|
page=posts.next_num, sort=sort) if posts.has_next else None
|
||||||
prev_url = url_for('activitypub.community_profile', actor=community.ap_id if community.ap_id is not None else community.name,
|
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) if posts.has_prev and page != 1 else None
|
page=posts.prev_num, sort=sort) if posts.has_prev and page != 1 else None
|
||||||
|
|
||||||
return render_template('community/community.html', community=community, title=community.title,
|
return render_template('community/community.html', community=community, title=community.title,
|
||||||
is_moderator=is_moderator, is_owner=is_owner, is_admin=is_admin, mods=mod_list, posts=posts, description=description,
|
is_moderator=is_moderator, is_owner=is_owner, is_admin=is_admin, mods=mod_list, posts=posts, description=description,
|
||||||
|
|
|
@ -15,7 +15,7 @@ from app.constants import POST_TYPE_ARTICLE, POST_TYPE_LINK, POST_TYPE_IMAGE
|
||||||
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
|
from app.models import Community, File, BannedInstances, PostReply, PostVote, Post, utcnow, CommunityMember, Site, \
|
||||||
Instance
|
Instance
|
||||||
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, validate_image, allowlist_html, \
|
from app.utils import get_request, gibberish, markdown_to_html, domain_from_url, validate_image, allowlist_html, \
|
||||||
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain
|
html_to_markdown, is_image_url, ensure_directory_exists, inbox_domain, post_ranking
|
||||||
from sqlalchemy import desc, text
|
from sqlalchemy import desc, text
|
||||||
import os
|
import os
|
||||||
from opengraph_parse import parse_page
|
from opengraph_parse import parse_page
|
||||||
|
@ -103,6 +103,7 @@ def retrieve_mods_and_backfill(community_id: int):
|
||||||
post = post_json_to_model(activity['object']['object'], user, community)
|
post = post_json_to_model(activity['object']['object'], user, community)
|
||||||
post.ap_create_id = activity['object']['id']
|
post.ap_create_id = activity['object']['id']
|
||||||
post.ap_announce_id = activity['id']
|
post.ap_announce_id = activity['id']
|
||||||
|
post.ranking = post_ranking(post.score, post.posted_at)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
activities_processed += 1
|
activities_processed += 1
|
||||||
|
@ -267,7 +268,9 @@ def save_post(form, post: Post):
|
||||||
post.score = -1
|
post.score = -1
|
||||||
post.ranking = -1
|
post.ranking = -1
|
||||||
db.session.add(postvote)
|
db.session.add(postvote)
|
||||||
|
post.ranking = post_ranking(post.score, utcnow())
|
||||||
db.session.add(post)
|
db.session.add(post)
|
||||||
|
|
||||||
g.site.last_active = utcnow()
|
g.site.last_active = utcnow()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from math import log
|
||||||
|
|
||||||
from sqlalchemy.sql.operators import or_
|
from sqlalchemy.sql.operators import or_
|
||||||
|
|
||||||
from app import db, cache
|
from app import db, cache
|
||||||
|
@ -12,7 +15,7 @@ from sqlalchemy import select, desc
|
||||||
from sqlalchemy_searchable import search
|
from sqlalchemy_searchable import search
|
||||||
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
|
from app.utils import render_template, get_setting, gibberish, request_etag_matches, return_304, blocked_domains, \
|
||||||
ap_datetime, ip_address
|
ap_datetime, ip_address
|
||||||
from app.models import Community, CommunityMember, Post, Site, User
|
from app.models import Community, CommunityMember, Post, Site, User, utcnow
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/', methods=['HEAD', 'GET', 'POST'])
|
@bp.route('/', methods=['HEAD', 'GET', 'POST'])
|
||||||
|
@ -40,7 +43,7 @@ def index():
|
||||||
if domains_ids:
|
if domains_ids:
|
||||||
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
||||||
|
|
||||||
posts = posts.order_by(desc(Post.last_active)).paginate(page=page, per_page=100, error_out=False)
|
posts = posts.order_by(desc(Post.ranking)).paginate(page=page, per_page=100, error_out=False)
|
||||||
|
|
||||||
next_url = url_for('main.index', page=posts.next_num) if posts.has_next else None
|
next_url = url_for('main.index', page=posts.next_num) if posts.has_next else None
|
||||||
prev_url = url_for('main.index', page=posts.prev_num) if posts.has_prev and page != 1 else None
|
prev_url = url_for('main.index', page=posts.prev_num) if posts.has_prev and page != 1 else None
|
||||||
|
@ -54,6 +57,78 @@ def index():
|
||||||
rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", rss_feed_name=f"Posts on " + g.site.name)
|
rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed", rss_feed_name=f"Posts on " + g.site.name)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/new', methods=['HEAD', 'GET', 'POST'])
|
||||||
|
def new_posts():
|
||||||
|
verification_warning()
|
||||||
|
|
||||||
|
# If nothing has changed since their last visit, return HTTP 304
|
||||||
|
current_etag = f"new_{hash(str(g.site.last_active))}"
|
||||||
|
if current_user.is_anonymous and request_etag_matches(current_etag):
|
||||||
|
return return_304(current_etag)
|
||||||
|
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
|
||||||
|
if current_user.is_anonymous:
|
||||||
|
posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
|
||||||
|
else:
|
||||||
|
posts = Post.query.join(CommunityMember, Post.community_id == CommunityMember.community_id).filter(
|
||||||
|
CommunityMember.is_banned == False)
|
||||||
|
posts = posts.join(User, CommunityMember.user_id == User.id).filter(User.id == current_user.id)
|
||||||
|
domains_ids = blocked_domains(current_user.id)
|
||||||
|
if domains_ids:
|
||||||
|
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
||||||
|
|
||||||
|
posts = posts.order_by(desc(Post.posted_at)).paginate(page=page, per_page=100, error_out=False)
|
||||||
|
|
||||||
|
next_url = url_for('main.new_posts', page=posts.next_num) if posts.has_next else None
|
||||||
|
prev_url = url_for('main.new_posts', page=posts.prev_num) if posts.has_prev and page != 1 else None
|
||||||
|
|
||||||
|
active_communities = Community.query.filter_by(banned=False).order_by(desc(Community.last_active)).limit(5).all()
|
||||||
|
|
||||||
|
return render_template('new_posts.html', posts=posts, active_communities=active_communities,
|
||||||
|
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK,
|
||||||
|
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||||
|
etag=f"home_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url,
|
||||||
|
rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed",
|
||||||
|
rss_feed_name=f"Posts on " + g.site.name)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/top', methods=['HEAD', 'GET', 'POST'])
|
||||||
|
def top_posts():
|
||||||
|
verification_warning()
|
||||||
|
|
||||||
|
# If nothing has changed since their last visit, return HTTP 304
|
||||||
|
current_etag = f"best_{hash(str(g.site.last_active))}"
|
||||||
|
if current_user.is_anonymous and request_etag_matches(current_etag):
|
||||||
|
return return_304(current_etag)
|
||||||
|
|
||||||
|
page = request.args.get('page', 1, type=int)
|
||||||
|
|
||||||
|
if current_user.is_anonymous:
|
||||||
|
posts = Post.query.filter(Post.from_bot == False, Post.nsfw == False, Post.nsfl == False)
|
||||||
|
else:
|
||||||
|
posts = Post.query.join(CommunityMember, Post.community_id == CommunityMember.community_id).filter(
|
||||||
|
CommunityMember.is_banned == False)
|
||||||
|
posts = posts.join(User, CommunityMember.user_id == User.id).filter(User.id == current_user.id)
|
||||||
|
domains_ids = blocked_domains(current_user.id)
|
||||||
|
if domains_ids:
|
||||||
|
posts = posts.filter(or_(Post.domain_id.not_in(domains_ids), Post.domain_id == None))
|
||||||
|
|
||||||
|
posts = posts.filter(Post.posted_at > utcnow() - timedelta(days=1)).order_by(desc(Post.score)).paginate(page=page, per_page=100, error_out=False)
|
||||||
|
|
||||||
|
next_url = url_for('main.top_posts', page=posts.next_num) if posts.has_next else None
|
||||||
|
prev_url = url_for('main.top_posts', page=posts.prev_num) if posts.has_prev and page != 1 else None
|
||||||
|
|
||||||
|
active_communities = Community.query.filter_by(banned=False).order_by(desc(Community.last_active)).limit(5).all()
|
||||||
|
|
||||||
|
return render_template('top_posts.html', posts=posts, active_communities=active_communities,
|
||||||
|
POST_TYPE_IMAGE=POST_TYPE_IMAGE, POST_TYPE_LINK=POST_TYPE_LINK,
|
||||||
|
SUBSCRIPTION_PENDING=SUBSCRIPTION_PENDING, SUBSCRIPTION_MEMBER=SUBSCRIPTION_MEMBER,
|
||||||
|
etag=f"home_{hash(str(g.site.last_active))}", next_url=next_url, prev_url=prev_url,
|
||||||
|
rss_feed=f"https://{current_app.config['SERVER_NAME']}/feed",
|
||||||
|
rss_feed_name=f"Posts on " + g.site.name)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/communities', methods=['GET'])
|
@bp.route('/communities', methods=['GET'])
|
||||||
def list_communities():
|
def list_communities():
|
||||||
verification_warning()
|
verification_warning()
|
||||||
|
@ -99,10 +174,12 @@ def robots():
|
||||||
|
|
||||||
@bp.route('/test')
|
@bp.route('/test')
|
||||||
def test():
|
def test():
|
||||||
ip = request.headers.get('X-Forwarded-For') or request.remote_addr
|
return 'done'
|
||||||
if ',' in ip: # Remove all but first ip addresses
|
|
||||||
ip = ip[:ip.index(',')].strip()
|
#ip = request.headers.get('X-Forwarded-For') or request.remote_addr
|
||||||
return ip
|
#if ',' in ip: # Remove all but first ip addresses
|
||||||
|
# ip = ip[:ip.index(',')].strip()
|
||||||
|
#return ip
|
||||||
|
|
||||||
|
|
||||||
def verification_warning():
|
def verification_warning():
|
||||||
|
|
|
@ -18,7 +18,7 @@ from app.models import Post, PostReply, \
|
||||||
from app.post import bp
|
from app.post 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, ap_datetime, return_304, \
|
shorten_string, markdown_to_text, domain_from_url, validate_image, gibberish, ap_datetime, return_304, \
|
||||||
request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote
|
request_etag_matches, ip_address, user_ip_banned, instance_banned, can_downvote, can_upvote, post_ranking
|
||||||
|
|
||||||
|
|
||||||
def show_post(post_id: int):
|
def show_post(post_id: int):
|
||||||
|
@ -155,11 +155,11 @@ def show_post(post_id: int):
|
||||||
og_image = post.image.source_url if post.image_id else None
|
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
|
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,
|
return render_template('post/post.html', title=post.title, post=post, is_moderator=is_moderator, community=post.community,
|
||||||
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,
|
||||||
description=description, og_image=og_image, POST_TYPE_IMAGE=constants.POST_TYPE_IMAGE,
|
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,
|
POST_TYPE_LINK=constants.POST_TYPE_LINK, POST_TYPE_ARTICLE=constants.POST_TYPE_ARTICLE,
|
||||||
etag=f"{post.id}_{hash(post.last_active)}", markdown_editor=True, community=community)
|
etag=f"{post.id}_{hash(post.last_active)}", markdown_editor=True)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/post/<int:post_id>/<vote_direction>', methods=['GET', 'POST'])
|
@bp.route('/post/<int:post_id>/<vote_direction>', methods=['GET', 'POST'])
|
||||||
|
@ -247,11 +247,12 @@ def post_vote(post_id: int, vote_direction):
|
||||||
current_user.last_seen = utcnow()
|
current_user.last_seen = utcnow()
|
||||||
current_user.ip_address = ip_address()
|
current_user.ip_address = ip_address()
|
||||||
if not current_user.banned:
|
if not current_user.banned:
|
||||||
|
post.ranking = post_ranking(post.score, post.created_at)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
current_user.recalculate_attitude()
|
current_user.recalculate_attitude()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
post.flush_cache()
|
post.flush_cache()
|
||||||
return render_template('post/_post_voting_buttons.html', post=post,
|
return render_template('post/_post_voting_buttons.html', post=post, community=post.community,
|
||||||
upvoted_class=upvoted_class,
|
upvoted_class=upvoted_class,
|
||||||
downvoted_class=downvoted_class)
|
downvoted_class=downvoted_class)
|
||||||
|
|
||||||
|
@ -327,7 +328,7 @@ def comment_vote(comment_id, vote_direction):
|
||||||
comment.post.flush_cache()
|
comment.post.flush_cache()
|
||||||
return render_template('post/_voting_buttons.html', comment=comment,
|
return render_template('post/_voting_buttons.html', comment=comment,
|
||||||
upvoted_class=upvoted_class,
|
upvoted_class=upvoted_class,
|
||||||
downvoted_class=downvoted_class)
|
downvoted_class=downvoted_class, community=comment.community)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/post/<int:post_id>/comment/<int:comment_id>')
|
@bp.route('/post/<int:post_id>/comment/<int:comment_id>')
|
||||||
|
|
11
app/templates/_home_nav.html
Normal file
11
app/templates/_home_nav.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="btn-group mt-1 mb-2">
|
||||||
|
<a href="/" class="btn {{ 'btn-primary' if request.path == '/' else 'btn-outline-secondary' }}" rel="nofollow">
|
||||||
|
{{ _('Hot') }}
|
||||||
|
</a>
|
||||||
|
<a href="/top" class="btn {{ 'btn-primary' if request.path == '/top' else 'btn-outline-secondary' }}" rel="nofollow">
|
||||||
|
{{ _('Top') }}
|
||||||
|
</a>
|
||||||
|
<a href="/new" class="btn {{ 'btn-primary' if request.path == '/new' else 'btn-outline-secondary' }}" rel="nofollow">
|
||||||
|
{{ _('New') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
|
@ -40,7 +40,7 @@
|
||||||
</table>
|
</table>
|
||||||
<nav aria-label="Pagination" class="mt-4">
|
<nav aria-label="Pagination" class="mt-4">
|
||||||
{% if prev_url %}
|
{% if prev_url %}
|
||||||
<a href="{{ prev_url }}" class="btn btn-primary">
|
<a href="{{ prev_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
11
app/templates/community/_community_nav.html
Normal file
11
app/templates/community/_community_nav.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="btn-group mt-1 mb-2">
|
||||||
|
<a href="?sort=hot" class="btn {{ 'btn-primary' if request.args.get('sort', '') == '' or request.args.get('sort', '') == 'hot' else 'btn-outline-secondary' }}" rel="nofollow">
|
||||||
|
{{ _('Hot') }}
|
||||||
|
</a>
|
||||||
|
<a href="?sort=top" class="btn {{ 'btn-primary' if request.args.get('sort', '') == 'top' else 'btn-outline-secondary' }}" rel="nofollow">
|
||||||
|
{{ _('Top') }}
|
||||||
|
</a>
|
||||||
|
<a href="?sort=new" class="btn {{ 'btn-primary' if request.args.get('sort', '') == 'new' else 'btn-outline-secondary' }}" rel="nofollow">
|
||||||
|
{{ _('New') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
|
@ -35,6 +35,7 @@
|
||||||
</nav>
|
</nav>
|
||||||
<h1 class="mt-2">{{ community.title }}</h1>
|
<h1 class="mt-2">{{ community.title }}</h1>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% include "community/_community_nav.html" %}
|
||||||
<div class="post_list">
|
<div class="post_list">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include 'post/_post_teaser.html' %}
|
{% include 'post/_post_teaser.html' %}
|
||||||
|
@ -45,12 +46,12 @@
|
||||||
|
|
||||||
<nav aria-label="Pagination" class="mt-4">
|
<nav aria-label="Pagination" class="mt-4">
|
||||||
{% if prev_url %}
|
{% if prev_url %}
|
||||||
<a href="{{ prev_url }}" class="btn btn-primary">
|
<a href="{{ prev_url }}" class="btn btn-primary" rel='nofollow'>
|
||||||
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if next_url %}
|
{% if next_url %}
|
||||||
<a href="{{ next_url }}" class="btn btn-primary">
|
<a href="{{ next_url }}" class="btn btn-primary" rel='nofollow'>
|
||||||
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
{% 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">
|
||||||
<h1 class="mt-2">{{ _('Recent posts') }}</h1>
|
{% include "_home_nav.html" %}
|
||||||
<div class="post_list">
|
<div class="post_list">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% include 'post/_post_teaser.html' %}
|
{% include 'post/_post_teaser.html' %}
|
||||||
|
@ -15,12 +15,12 @@
|
||||||
|
|
||||||
<nav aria-label="Pagination" class="mt-4">
|
<nav aria-label="Pagination" class="mt-4">
|
||||||
{% if prev_url %}
|
{% if prev_url %}
|
||||||
<a href="{{ prev_url }}" class="btn btn-primary">
|
<a href="{{ prev_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if next_url %}
|
{% if next_url %}
|
||||||
<a href="{{ next_url }}" class="btn btn-primary">
|
<a href="{{ next_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -15,12 +15,9 @@
|
||||||
{{ _('Subscribed') }}
|
{{ _('Subscribed') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
|
|
||||||
<a href="{{ url_for('community.add_local') }}" class="btn btn-outline-secondary">{{ _('Create local') }}</a>
|
<a href="{{ url_for('community.add_local') }}" class="btn btn-outline-secondary">{{ _('Create local') }}</a>
|
||||||
<a href="{{ url_for('community.add_remote') }}" class="btn btn-outline-secondary">{{ _('Add remote') }}</a>
|
<a href="{{ url_for('community.add_remote') }}" class="btn btn-outline-secondary">{{ _('Add remote') }}</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
71
app/templates/new_posts.html
Normal file
71
app/templates/new_posts.html
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% from 'bootstrap5/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
{% include "_home_nav.html" %}
|
||||||
|
<div class="post_list">
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include 'post/_post_teaser.html' %}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ _('No posts yet.') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="Pagination" class="mt-4">
|
||||||
|
{% if prev_url %}
|
||||||
|
<a href="{{ prev_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_url %}
|
||||||
|
<a href="{{ next_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get">
|
||||||
|
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search') }}" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('Active communities') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for community in active_communities %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" />
|
||||||
|
{{ community.display_name() }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="mt-4"><a class="btn btn-primary" href="/communities">Explore communities</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('About %(site_name)s', site_name=g.site.name) }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>{{ g.site.description|safe }}</strong></p>
|
||||||
|
<p>{{ g.site.sidebar|safe }}</p>
|
||||||
|
<p class="mt-4">
|
||||||
|
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
71
app/templates/top_posts.html
Normal file
71
app/templates/top_posts.html
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% from 'bootstrap5/form.html' import render_form %}
|
||||||
|
|
||||||
|
{% block app_content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-8 position-relative main_pane">
|
||||||
|
{% include "_home_nav.html" %}
|
||||||
|
<div class="post_list">
|
||||||
|
{% for post in posts %}
|
||||||
|
{% include 'post/_post_teaser.html' %}
|
||||||
|
{% else %}
|
||||||
|
<p>{{ _('No posts yet.') }}</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="Pagination" class="mt-4">
|
||||||
|
{% if prev_url %}
|
||||||
|
<a href="{{ prev_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
<span aria-hidden="true">←</span> {{ _('Previous page') }}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if next_url %}
|
||||||
|
<a href="{{ next_url }}" class="btn btn-primary" rel="nofollow">
|
||||||
|
{{ _('Next page') }} <span aria-hidden="true">→</span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="get">
|
||||||
|
<input type="search" name="search" class="form-control mt-2" placeholder="{{ _('Search') }}" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('Active communities') }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for community in active_communities %}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a href="/c/{{ community.link() }}"><img src="{{ community.icon_image() }}" class="community_icon rounded-circle" loading="lazy" />
|
||||||
|
{{ community.display_name() }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="mt-4"><a class="btn btn-primary" href="/communities">Explore communities</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mt-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>{{ _('About %(site_name)s', site_name=g.site.name) }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p><strong>{{ g.site.description|safe }}</strong></p>
|
||||||
|
<p>{{ g.site.sidebar|safe }}</p>
|
||||||
|
<p class="mt-4">
|
||||||
|
<a class="no-underline" href="{{ rss_feed }}" rel="nofollow"><span class="fe fe-rss"></span> RSS feed</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
13
app/utils.py
13
app/utils.py
|
@ -454,3 +454,16 @@ def awaken_dormant_instance(instance):
|
||||||
instance.gone_forever = True
|
instance.gone_forever = True
|
||||||
instance.dormant = True
|
instance.dormant = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
epoch = datetime(1970, 1, 1)
|
||||||
|
|
||||||
|
def epoch_seconds(date):
|
||||||
|
td = date - epoch
|
||||||
|
return td.days * 86400 + td.seconds + (float(td.microseconds) / 1000000)
|
||||||
|
|
||||||
|
def post_ranking(score, date: datetime):
|
||||||
|
order = math.log(max(abs(score), 1), 10)
|
||||||
|
sign = 1 if score > 0 else -1 if score < 0 else 0
|
||||||
|
seconds = epoch_seconds(date) - 1685766018
|
||||||
|
return round(sign * order + seconds / 45000, 7)
|
||||||
|
|
|
@ -16,7 +16,6 @@ class Config(object):
|
||||||
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
|
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
|
||||||
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
|
||||||
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
|
||||||
#SQLALCHEMY_ECHO = True # set to true to see debugging SQL in console
|
|
||||||
RECAPTCHA3_PUBLIC_KEY = os.environ.get("RECAPTCHA3_PUBLIC_KEY")
|
RECAPTCHA3_PUBLIC_KEY = os.environ.get("RECAPTCHA3_PUBLIC_KEY")
|
||||||
RECAPTCHA3_PRIVATE_KEY = os.environ.get("RECAPTCHA3_PRIVATE_KEY")
|
RECAPTCHA3_PRIVATE_KEY = os.environ.get("RECAPTCHA3_PRIVATE_KEY")
|
||||||
MODE = os.environ.get('MODE') or 'development'
|
MODE = os.environ.get('MODE') or 'development'
|
||||||
|
|
Loading…
Reference in a new issue