leaderboard refactor (#526)

This commit is contained in:
justcool393 2023-02-24 04:31:17 -08:00 committed by GitHub
parent 22ad4f5d23
commit 44919507e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 436 additions and 600 deletions

View file

@ -0,0 +1,252 @@
from dataclasses import dataclass
from typing import Any, Callable, Final, Optional
from sqlalchemy import Column, func
from sqlalchemy.orm import scoped_session, Query
from files.helpers.const import LEADERBOARD_LIMIT
from files.classes.badges import Badge
from files.classes.marsey import Marsey
from files.classes.user import User
from files.classes.userblock import UserBlock
from files.helpers.get import get_accounts_dict
@dataclass(frozen=True, slots=True)
class LeaderboardMeta:
header_name:str
table_header_name:str
html_id:str
table_column_name:str
user_relative_url:Optional[str]
limit:int=LEADERBOARD_LIMIT
class Leaderboard:
def __init__(self, v:Optional[User], meta:LeaderboardMeta) -> None:
self.v:Optional[User] = v
self.meta:LeaderboardMeta = meta
@property
def all_users(self) -> list[User]:
raise NotImplementedError()
@property
def v_position(self) -> Optional[int]:
raise NotImplementedError()
@property
def v_value(self) -> Optional[int]:
raise NotImplementedError()
@property
def v_appears_in_ranking(self) -> bool:
return self.v_position is not None and self.v_position <= len(self.all_users)
@property
def user_func(self) -> Callable[[Any], User]:
return lambda u:u
@property
def value_func(self) -> Callable[[User], int]:
raise NotImplementedError()
class SimpleLeaderboard(Leaderboard):
def __init__(self, v:User, meta:LeaderboardMeta, db:scoped_session, users_query:Query, column:Column):
super().__init__(v, meta)
self.db:scoped_session = db
self.users_query:Query = users_query
self.column:Column = column
self._calculate()
def _calculate(self) -> None:
self._all_users = self.users_query.order_by(self.column.desc()).limit(self.meta.limit).all()
if self.v not in self._all_users:
sq = self.db.query(User.id, self.column, func.rank().over(order_by=self.column.desc()).label("rank")).subquery()
sq_data = self.db.query(sq.c.id, sq.c.column, sq.c.rank).filter(sq.c.id == self.v.id).limit(1).one()
self._v_value:int = sq_data[1]
self._v_position:int = sq_data[2]
@property
def all_users(self) -> list[User]:
return self._all_users
@property
def v_position(self) -> int:
return self._v_position
@property
def v_value(self) -> int:
return self._v_value
@property
def value_func(self) -> Callable[[User], int]:
return lambda u:getattr(u, self.column.name)
class _CountedAndRankedLeaderboard(Leaderboard):
@classmethod
def count_and_label(cls, criteria):
return func.count(criteria).label("count")
@classmethod
def rank_filtered_rank_label_by_desc(cls, criteria):
return func.rank().over(order_by=func.count(criteria).desc()).label("rank")
class BadgeMarseyLeaderboard(_CountedAndRankedLeaderboard):
def __init__(self, v:User, meta:LeaderboardMeta, db:scoped_session, column:Column):
super().__init__(v, meta)
self.db:scoped_session = db
self.column = column
self._calculate()
def _calculate(self):
sq = self.db.query(self.column, self.count_and_label(self.column), self.rank_filtered_rank_label_by_desc(self.column)).group_by(self.column).subquery()
sq_criteria = None
if self.column == Badge.user_id:
sq_criteria = User.id == sq.c.user_id
elif self.column == Marsey.author_id:
sq_criteria = User.id == sq.c.author_id
else:
raise ValueError("This leaderboard function only supports Badge.user_id and Marsey.author_id")
leaderboard = self.db.query(User, sq.c.count).join(sq, sq_criteria).order_by(sq.c.count.desc())
position:Optional[tuple[int, int, int]] = self.db.query(User.id, sq.c.rank, sq.c.count).join(sq, sq_criteria).filter(User.id == self.v.id).one_or_none()
if position and position[1]:
self._v_position = position[1]
self._v_value = position[2]
else:
self._v_position = leaderboard.count() + 1
self._v_value = 0
self._all_users = {k:v for k, v in leaderboard.limit(self.meta.limit).all()}
@property
def all_users(self) -> list[User]:
return list(self._all_users.keys())
@property
def v_position(self) -> int:
return self._v_position
@property
def v_value(self) -> int:
return self._v_value
@property
def value_func(self) -> Callable[[User], int]:
return lambda u:self._all_users[u]
class UserBlockLeaderboard(_CountedAndRankedLeaderboard):
def __init__(self, v:User, meta:LeaderboardMeta, db:scoped_session, column:Column):
super().__init__(v, meta)
self.db:scoped_session = db
self.column = column
self._calculate()
def _calculate(self):
if self.column != UserBlock.target_id:
raise ValueError("This leaderboard function only supports UserBlock.target_id")
sq = self.db.query(self.column, self.count_and_label(self.column)).group_by(self.column).subquery()
leaderboard = self.db.query(User, sq.c.count).join(User, User.id == sq.c.target_id).order_by(sq.c.count.desc())
sq = self.db.query(self.column, self.count_and_label(self.column), self.rank_filtered_rank_label_by_desc(self.column)).group_by(self.column).subquery()
position = self.db.query(sq.c.rank, sq.c.count).join(User, User.id == sq.c.target_id).filter(sq.c.target_id == self.v.id).limit(1).one_or_none()
if not position: position = (leaderboard.count() + 1, 0)
leaderboard = leaderboard.limit(self.meta.limit).all()
self._all_users = {k:v for k, v in leaderboard}
self._v_position = position[0]
self._v_value = position[1]
return (leaderboard, position[0], position[1])
@property
def all_users(self) -> list[User]:
return list(self._all_users.keys())
@property
def v_position(self) -> int:
return self._v_position
@property
def v_value(self) -> int:
return self._v_value
class RawSqlLeaderboard(Leaderboard):
def __init__(self, meta:LeaderboardMeta, db:scoped_session, query:str) -> None: # should be LiteralString on py3.11+
super().__init__(None, meta)
self.db = db
self._calculate(query)
def _calculate(self, query:str):
self.result = {result[0]:list(result) for result in self.db.execute(query).all()}
users = get_accounts_dict(self.result.keys(), db=self.db)
if users is None:
raise Exception("Some users don't exist when they should (was a user deleted?)")
for user in users: # I know.
self.result[user].append(users[user])
@property
def all_users(self) -> list[User]:
return [result[2] for result in self.result.values()]
@property
def v_position(self) -> Optional[int]:
return None
@property
def v_value(self) -> Optional[int]:
return None
@property
def v_appears_in_ranking(self) -> bool:
return True # we set this to True here to try and not grab the data
@property
def user_func(self) -> Callable[[Any], User]:
return lambda u:u
@property
def value_func(self) -> Callable[[User], int]:
return lambda u:self.result[u.id][1]
class ReceivedDownvotesLeaderboard(RawSqlLeaderboard):
_query: Final[str] = """
WITH cv_for_user AS (
SELECT
comments.author_id AS target_id,
COUNT(*)
FROM commentvotes cv
JOIN comments ON comments.id = cv.comment_id
WHERE vote_type = -1
GROUP BY comments.author_id
), sv_for_user AS (
SELECT
submissions.author_id AS target_id,
COUNT(*)
FROM votes sv
JOIN submissions ON submissions.id = sv.submission_id
WHERE vote_type = -1
GROUP BY submissions.author_id
)
SELECT
COALESCE(cvfu.target_id, svfu.target_id) AS target_id,
(COALESCE(cvfu.count, 0) + COALESCE(svfu.count, 0)) AS count
FROM cv_for_user cvfu
FULL OUTER JOIN sv_for_user svfu
ON cvfu.target_id = svfu.target_id
ORDER BY count DESC LIMIT 25
"""
def __init__(self, meta:LeaderboardMeta, db:scoped_session) -> None:
super().__init__(meta, db, self._query)
class GivenUpvotesLeaderboard(RawSqlLeaderboard):
_query: Final[str] = """
SELECT
COALESCE(cvbu.user_id, svbu.user_id) AS user_id,
(COALESCE(cvbu.count, 0) + COALESCE(svbu.count, 0)) AS count
FROM (SELECT user_id, COUNT(*) FROM votes WHERE vote_type = 1 GROUP BY user_id) AS svbu
FULL OUTER JOIN (SELECT user_id, COUNT(*) FROM commentvotes WHERE vote_type = 1 GROUP BY user_id) AS cvbu
ON cvbu.user_id = svbu.user_id
ORDER BY count DESC LIMIT 25
"""
def __init__(self, meta:LeaderboardMeta, db:scoped_session) -> None:
super().__init__(meta, db, self._query)

View file

@ -650,3 +650,9 @@ class User(Base):
l = [i.strip() for i in self.custom_filter_list.split('\n')] if self.custom_filter_list else []
l = [i for i in l if i]
return l
# Permissions
@property
def can_see_shadowbanned(self):
return self.admin_level >= PERMS['USER_SHADOWBAN'] or self.shadowbanned

View file

@ -1,11 +1,13 @@
from os import environ, listdir
import re
from copy import deepcopy
from json import loads
from os import environ
from typing import Final
from flask import request
from files.__main__ import db_session
from files.classes.sub import Sub
from files.classes.marsey import Marsey
from flask import request
SITE = environ.get("DOMAIN", '').strip()
SITE_ID = environ.get("SITE_ID", '').strip()
@ -33,6 +35,8 @@ BUG_THREAD = 0
WELCOME_MSG = f"Welcome to {SITE_TITLE}! Please read [the rules](/rules) first. Then [read some of our current conversations](/) and feel free to comment or post!\n\nWe encourage people to comment even if they aren't sure they fit in; as long as your comment follows [community rules](/rules), we are happy to have posters from all backgrounds, education levels, and specialties."
ROLES={}
LEADERBOARD_LIMIT: Final[int] = 25
THEMES = {"TheMotte", "dramblr", "reddit", "transparent", "win98", "dark",
"light", "coffee", "tron", "4chan", "midnight"}
SORTS_COMMON = {
@ -99,6 +103,7 @@ FEATURES = {
PERMS = {
"DEBUG_LOGIN_TO_OTHERS": 3,
"USER_SHADOWBAN": 2,
}
AWARDS = {}

View file

@ -3,7 +3,7 @@ from typing import Callable, Iterable, List, Optional, Type, Union
from flask import g
from sqlalchemy import and_, or_, func
from sqlalchemy.orm import Query, selectinload
from sqlalchemy.orm import Query, scoped_session, selectinload
from files.classes import *
from files.helpers.const import AUTOJANNY_ID
@ -95,6 +95,24 @@ def get_account(
return user
def get_accounts_dict(ids:Union[Iterable[str], Iterable[int]],
v:Optional[User]=None, graceful=False,
include_shadowbanned=True,
db:Optional[scoped_session]=None) -> Optional[dict[int, User]]:
if not db: db = g.db
if not ids: return {}
try:
ids = set([int(id) for id in ids])
except:
if graceful: return None
abort(404)
users = db.query(User).filter(User.id.in_(ids))
if not (include_shadowbanned or (v and v.can_see_shadowbanned)):
users = users.filter(User.shadowbanned == None)
users = users.all()
if len(users) != len(ids) and not graceful: abort(404)
return {u.id:u for u in users}
def get_post(
i:Union[str,int],

59
files/helpers/services.py Normal file
View file

@ -0,0 +1,59 @@
import sys
import gevent
from pusher_push_notifications import PushNotifications
from sqlalchemy.orm import scoped_session
from files.classes.leaderboard import (LeaderboardMeta, ReceivedDownvotesLeaderboard,
GivenUpvotesLeaderboard)
from files.helpers.assetcache import assetcache_path
from files.helpers.const import PUSHER_ID, PUSHER_KEY, SITE_FULL, SITE_ID
from files.__main__ import app, db_session
if PUSHER_ID != 'blahblahblah':
beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY)
else:
beams_client = None
def pusher_thread2(interests, notifbody, username):
if not beams_client: return
beams_client.publish_to_interests(
interests=[interests],
publish_body={
'web': {
'notification': {
'title': f'New message from @{username}',
'body': notifbody,
'deep_link': f'{SITE_FULL}/notifications?messages=true',
'icon': SITE_FULL + assetcache_path(f'images/{SITE_ID}/icon.webp'),
}
},
'fcm': {
'notification': {
'title': f'New message from @{username}',
'body': notifbody,
},
'data': {
'url': '/notifications?messages=true',
}
}
},
)
sys.stdout.flush()
_lb_received_downvotes_meta = LeaderboardMeta("Downvotes", "received downvotes", "received-downvotes", "downvotes", "downvoted")
_lb_given_upvotes_meta = LeaderboardMeta("Upvotes", "given upvotes", "given-upvotes", "upvotes", "upvoting")
def leaderboard_thread():
global lb_downvotes_received, lb_upvotes_given
db:scoped_session = db_session() # type: ignore
lb_downvotes_received = ReceivedDownvotesLeaderboard(_lb_received_downvotes_meta, db)
lb_upvotes_given = GivenUpvotesLeaderboard(_lb_given_upvotes_meta, db)
db.close()
sys.stdout.flush()
if app.config["ENABLE_SERVICES"]:
gevent.spawn(leaderboard_thread())

View file

@ -2,6 +2,8 @@ import qrcode
import io
import time
import math
from files.classes.leaderboard import SimpleLeaderboard, BadgeMarseyLeaderboard, UserBlockLeaderboard, LeaderboardMeta
from files.classes.views import ViewerRelationship
from files.helpers.alerts import *
from files.helpers.media import process_image
@ -12,69 +14,14 @@ from files.helpers.assetcache import assetcache_path
from files.helpers.contentsorting import apply_time_filter, sort_objects
from files.mail import *
from flask import *
from files.__main__ import app, limiter, db_session
from pusher_push_notifications import PushNotifications
from files.__main__ import app, limiter
from collections import Counter
import gevent
from sys import stdout
if PUSHER_ID != 'blahblahblah':
beams_client = PushNotifications(instance_id=PUSHER_ID, secret_key=PUSHER_KEY)
def pusher_thread2(interests, notifbody, username):
beams_client.publish_to_interests(
interests=[interests],
publish_body={
'web': {
'notification': {
'title': f'New message from @{username}',
'body': notifbody,
'deep_link': f'{SITE_FULL}/notifications?messages=true',
'icon': SITE_FULL + assetcache_path(f'images/{SITE_ID}/icon.webp'),
}
},
'fcm': {
'notification': {
'title': f'New message from @{username}',
'body': notifbody,
},
'data': {
'url': '/notifications?messages=true',
}
}
},
)
stdout.flush()
def leaderboard_thread():
global users9, users9_25, users13, users13_25
db = db_session()
votes1 = db.query(Submission.author_id, func.count(Submission.author_id)).join(Vote, Vote.submission_id==Submission.id).filter(Vote.vote_type==-1).group_by(Submission.author_id).order_by(func.count(Submission.author_id).desc()).all()
votes2 = db.query(Comment.author_id, func.count(Comment.author_id)).join(CommentVote, CommentVote.comment_id==Comment.id).filter(CommentVote.vote_type==-1).group_by(Comment.author_id).order_by(func.count(Comment.author_id).desc()).all()
votes3 = Counter(dict(votes1)) + Counter(dict(votes2))
users8 = db.query(User).filter(User.id.in_(votes3.keys())).all()
users9 = []
for user in users8: users9.append((user, votes3[user.id]))
users9 = sorted(users9, key=lambda x: x[1], reverse=True)
users9_25 = users9[:25]
votes1 = db.query(Vote.user_id, func.count(Vote.user_id)).filter(Vote.vote_type==1).group_by(Vote.user_id).order_by(func.count(Vote.user_id).desc()).all()
votes2 = db.query(CommentVote.user_id, func.count(CommentVote.user_id)).filter(CommentVote.vote_type==1).group_by(CommentVote.user_id).order_by(func.count(CommentVote.user_id).desc()).all()
votes3 = Counter(dict(votes1)) + Counter(dict(votes2))
users14 = db.query(User).filter(User.id.in_(votes3.keys())).all()
users13 = []
for user in users14:
users13.append((user, votes3[user.id]-user.post_count-user.comment_count))
users13 = sorted(users13, key=lambda x: x[1], reverse=True)
users13_25 = users13[:25]
db.close()
stdout.flush()
if app.config["ENABLE_SERVICES"]:
gevent.spawn(leaderboard_thread())
# warning: do not move currently. these have import-time side effects but
# until this is refactored to be not completely awful, there's not really
# a better option.
from files.helpers.services import *
@app.get("/@<username>/upvoters/<uid>/posts")
@admin_level_required(3)
@ -412,73 +359,26 @@ def transfer_bux(v, username):
@app.get("/leaderboard")
@admin_level_required(2)
def leaderboard(v):
def leaderboard(v:User):
users:Query = g.db.query(User)
if not v.can_see_shadowbanned:
users = users.filter(User.shadowbanned == None)
users = g.db.query(User)
coins = SimpleLeaderboard(v, LeaderboardMeta("Coins", "coins", "coins", "Coins", None), g.db, users, User.coins)
subscribers = SimpleLeaderboard(v, LeaderboardMeta("Followers", "followers", "followers", "Followers", "followers"), g.db, users, User.stored_subscriber_count)
posts = SimpleLeaderboard(v, LeaderboardMeta("Posts", "post count", "posts", "Posts", ""), g.db, users, User.post_count)
comments = SimpleLeaderboard(v, LeaderboardMeta("Comments", "comment count", "comments", "Comments", "comments"), g.db, users, User.comment_count)
received_awards = SimpleLeaderboard(v, LeaderboardMeta("Awards", "received awards", "awards", "Awards", None), g.db, users, User.received_award_count)
coins_spent = SimpleLeaderboard(v, LeaderboardMeta("Spent in shop", "coins spent in shop", "spent", "Coins", None), g.db, users, User.coins_spent)
truescore = SimpleLeaderboard(v, LeaderboardMeta("Truescore", "truescore", "truescore", "Truescore", None), g.db, users, User.truecoins)
badges = BadgeMarseyLeaderboard(v, LeaderboardMeta("Badges", "badges", "badges", "Badges", None), g.db, Badge.user_id)
blocks = UserBlockLeaderboard(v, LeaderboardMeta("Blocked", "most blocked", "blocked", "Blocked By", "blockers"), g.db, UserBlock.target_id)
users1 = users.order_by(User.coins.desc()).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.coins.desc()).label("rank")).subquery()
pos1 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
# note: lb_downvotes_received and lb_upvotes_given are global variables
# that are populated by leaderboard_thread() in files.helpers.services
leaderboards = [coins, coins_spent, truescore, subscribers, posts, comments, received_awards, badges, blocks, lb_downvotes_received, lb_upvotes_given]
users2 = users.order_by(User.stored_subscriber_count.desc()).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.stored_subscriber_count.desc()).label("rank")).subquery()
pos2 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
users3 = users.order_by(User.post_count.desc()).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.post_count.desc()).label("rank")).subquery()
pos3 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
users4 = users.order_by(User.comment_count.desc()).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.comment_count.desc()).label("rank")).subquery()
pos4 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
users5 = users.order_by(User.received_award_count.desc()).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.received_award_count.desc()).label("rank")).subquery()
pos5 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
users6 = None
pos6 = None
users7 = users.order_by(User.coins_spent.desc()).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.coins_spent.desc()).label("rank")).subquery()
pos7 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
try:
pos9 = [x[0].id for x in users9].index(v.id)
pos9 = (pos9+1, users9[pos9][1])
except: pos9 = (len(users9)+1, 0)
users10 = users.order_by(User.truecoins.desc()).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.truecoins.desc()).label("rank")).subquery()
pos10 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
sq = g.db.query(Badge.user_id, func.count(Badge.user_id).label("count"), func.rank().over(order_by=func.count(Badge.user_id).desc()).label("rank")).group_by(Badge.user_id).subquery()
users11 = g.db.query(User, sq.c.count).join(sq, User.id==sq.c.user_id).order_by(sq.c.count.desc())
pos11 = g.db.query(User.id, sq.c.rank, sq.c.count).join(sq, User.id==sq.c.user_id).filter(User.id == v.id).one_or_none()
if pos11: pos11 = (pos11[1],pos11[2])
else: pos11 = (users11.count()+1, 0)
users11 = users11.limit(25).all()
if pos11[1] < 25 and v not in (x[0] for x in users11):
pos11 = (26, pos11[1])
users12 = None
pos12 = None
try:
pos13 = [x[0].id for x in users13].index(v.id)
pos13 = (pos13+1, users13[pos13][1])
except: pos13 = (len(users13)+1, 0)
users14 = users.order_by(User.winnings.desc()).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.winnings.desc()).label("rank")).subquery()
pos14 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
users15 = users.order_by(User.winnings).limit(25).all()
sq = g.db.query(User.id, func.rank().over(order_by=User.winnings).label("rank")).subquery()
pos15 = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
return render_template("leaderboard.html", v=v, users1=users1, pos1=pos1, users2=users2, pos2=pos2, users3=users3, pos3=pos3, users4=users4, pos4=pos4, users5=users5, pos5=pos5, users6=users6, pos6=pos6, users7=users7, pos7=pos7, users9=users9_25, pos9=pos9, users10=users10, pos10=pos10, users11=users11, pos11=pos11, users12=users12, pos12=pos12, users13=users13_25, pos13=pos13, users14=users14, pos14=pos14, users15=users15, pos15=pos15)
return render_template("leaderboard.html", v=v, leaderboards=leaderboards)
@app.get("/@<username>/css")
def get_css(username):

View file

@ -0,0 +1 @@
{% if v and v.admin_level >= PERMS['USER_SHADOWBAN'] and user.shadowbanned %}<i class="fas fa-user-times text-admin" data-bs-toggle="tooltip" data-bs-placement="bottom" title='Shadowbanned by @{{user.shadowbanner}}{% if user.ban_reason %} for "{{user.ban_reason}}"{% endif %}'></i>{% endif %}

View file

@ -1,484 +1,62 @@
{% extends "settings2.html" %}
{% block pagetitle %}Leaderboard{% endblock %}
{% block content %}
<pre class="d-none d-md-inline-block"></pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by coins</h5>
<pre></pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Coins</th>
</tr>
</thead>
{% for user in users1 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.coins}}</td>
</tr>
{% endfor %}
{% if pos1 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos1}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.coins}}</td>
</tr>
{% endif %}
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by coins spent in shop</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Coins</th>
</tr>
</thead>
{% for user in users7 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.coins_spent}}</td>
</tr>
{% endfor %}
{% if pos7 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos7}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.coins_spent}}</td>
</tr>
{% endif %}
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by truescore</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Truescore</th>
</tr>
</thead>
<tbody id="followers-table">
{% for user in users10 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.truecoins}}</td>
</tr>
{% endfor %}
{% if pos10 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos10}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.truecoins}}</td>
</tr>
{% endif %}
</tbody>
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by followers</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Followers</th>
</tr>
</thead>
{% for user in users2 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.stored_subscriber_count}}</td>
</tr>
{% endfor %}
{% if pos2 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos2}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.stored_subscriber_count}}</td>
</tr>
{% endif %}
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by post count</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Posts</th>
</tr>
</thead>
{% for user in users3 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.post_count}}</td>
</tr>
{% endfor %}
{% if pos3 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos3}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.post_count}}</td>
</tr>
{% endif %}
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by comment count</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Comments</th>
</tr>
</thead>
{% for user in users4 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.comment_count}}</td>
</tr>
{% endfor %}
{% if pos4 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos4}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.comment_count}}</td>
</tr>
{% endif %}
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by received awards</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Awards</th>
</tr>
</thead>
{% for user in users5 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.received_award_count}}</td>
</tr>
{% endfor %}
{% if pos5 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos5}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.received_award_count}}</td>
</tr>
{% endif %}
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by received downvotes</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Downvotes</th>
</tr>
</thead>
<tbody id="followers-table">
{% for user in users9 %}
<tr {% if v.id == user[0].id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user[0].namecolor}};font-weight:bold" href="/@{{user[0].username}}"><img loading="lazy" src="{{user[0].profile_url}}" class="pp20"><span {% if user[0].patron %}class="patron" style="background-color:#{{user[0].namecolor}}"{% endif %}>{{user[0].username}}</span></a></td>
<td>{{user[1]}}</td>
</tr>
{% endfor %}
{% if pos9 and (pos9[0] > 25 or not pos9[1]) %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos9[0]}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{pos9[1]}}</td>
</tr>
{% endif %}
</tbody>
</table>
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by badges</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Badges</th>
</tr>
</thead>
<tbody id="followers-table">
{% for user in users11 %}
<tr {% if v.id == user[0].id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user[0].namecolor}};font-weight:bold" href="/@{{user[0].username}}"><img loading="lazy" src="{{user[0].profile_url}}" class="pp20"><span {% if user[0].patron %}class="patron" style="background-color:#{{user[0].namecolor}}"{% endif %}>{{user[0].username}}</span></a></td>
<td>{{user[1]}}</td>
</tr>
{% endfor %}
{% if pos11 and (pos11[0] > 25 or not pos11[1]) %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos11[0]}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{pos11[1]}}</td>
</tr>
{% endif %}
</tbody>
</table>
{% if users6 %}
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by based count</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Based count</th>
</tr>
</thead>
{% for user in users6 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.basedcount}}</td>
</tr>
{% endfor %}
{% if pos6 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos6}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.basedcount}}</td>
</tr>
{% endif %}
</table>
{% endif %}
{% if users12 %}
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by marseys made</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Marseys</th>
</tr>
</thead>
<tbody id="followers-table">
{% for user in users12 %}
<tr {% if v.id == user[0].id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user[0].namecolor}};font-weight:bold" href="/@{{user[0].username}}"><img loading="lazy" src="{{user[0].profile_url}}" class="pp20"><span {% if user[0].patron %}class="patron" style="background-color:#{{user[0].namecolor}}"{% endif %}>{{user[0].username}}</span></a></td>
<td>{{user[1]}}</td>
</tr>
{% endfor %}
{% if pos12 and (pos12[0] > 25 or not pos12[1]) %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos12[0]}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{pos12[1]}}</td>
</tr>
<div id="leaderboard-contents" style="text-align: center; margin-bottom: 1.5rem; font-size: 1.2rem;">
{% for lb in leaderboards %}
{% if lb %}
<a href="#leaderboard-{{lb.html_id}}">{{lb.meta.header_name}}</a>{% if not loop.last %} &bull;{% endif %}
{% endif %}
</tbody>
</table>
{% endif %}
{% endfor %}
</div>
{% if users13 %}
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Top 25 by upvotes given</h5>
<pre>
</pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Upvotes</th>
</tr>
</thead>
<tbody id="followers-table">
{% for user in users13 %}
<tr {% if v.id == user[0].id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user[0].namecolor}};font-weight:bold" href="/@{{user[0].username}}"><img loading="lazy" src="{{user[0].profile_url}}" class="pp20"><span {% if user[0].patron %}class="patron" style="background-color:#{{user[0].namecolor}}"{% endif %}>{{user[0].username}}</span></a></td>
<td>{{user[1]}}</td>
</tr>
{% endfor %}
{% if pos13 and (pos13[0] > 25 or not pos13[1]) %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos13[0]}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{pos13[1]}}</td>
</tr>
{% macro format_user_in_table(user, style, position_no, value, user_relative_url) %}
{% set value = value | int %}
<tr {{style | safe}}>
<td>{{position_no}}</td>
<td>{% include "user_in_table.html" %}</td>
{% if user_relative_url is not none %}
<td><a href="/@{{user.username}}/{{user_relative_url}}">{{"{:,}".format(value)}}</a></td>
{% else %}
<td>{{"{:,}".format(value)}}</td>
{% endif %}
</tbody>
</table>
{% endif %}
</tr>
{% endmacro %}
<h5 style="font-weight:bold;text-align: center">Top 25 by winnings</h5>
<pre></pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Winnings</th>
</tr>
</thead>
{% for user in users14 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.winnings}}</td>
</tr>
{% endfor %}
{% if pos14 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos14}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.winnings}}</td>
</tr>
{% endif %}
{% macro leaderboard_table(lb) %}
<h5 class="font-weight-bolder text-center pt-2 pb-3"><span id="leaderboard-{{lb.meta.html_id}}">Top {{lb.limit}} {% if lb.meta.table_header_name != 'most blocked' %}by{% endif %} {{lb.meta.table_header_name}}</span></h5>
<div class="overflow-x-auto">
<table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>{{lb.meta.table_column_name}}</th>
</tr>
</thead>
<tbody>
{% for user in lb.all_users %}
{% set user2 = lb.user_func(user) %}
{% if v.id == user2.id %}
{% set style="class=\"self\"" %}
{% endif %}
{{format_user_in_table(user2, style, loop.index, lb.value_func(user), lb.meta.user_relative_url)}}
{% endfor %}
{% if lb.v_position and not lb.v_appears_in_ranking %}
{{format_user_in_table(v, "style=\"border-top:2px solid var(--primary)\"", lb.v_position, lb.v_value, lb.meta.user_relative_url)}}
{% endif %}
</tbody>
</table>
</div>
{% endmacro %}
<pre>
</pre>
<h5 style="font-weight:bold;text-align: center">Bottom 25 by winnings</h5>
<pre></pre>
<div class="overflow-x-auto"><table class="table table-striped mb-5">
<thead class="bg-primary text-white">
<tr>
<th>#</th>
<th>Name</th>
<th>Winnings</th>
</tr>
</thead>
{% for user in users15 %}
<tr {% if v.id == user.id %}class="self"{% endif %}>
<td>{{loop.index}}</td>
<td><a style="color:#{{user.namecolor}};font-weight:bold" href="/@{{user.username}}"><img loading="lazy" src="{{user.profile_url}}" class="pp20"><span {% if user.patron %}class="patron" style="background-color:#{{user.namecolor}}"{% endif %}>{{user.username}}</span></a></td>
<td>{{user.winnings}}</td>
</tr>
{% for lb in leaderboards %}
{% if lb %}
{{leaderboard_table(lb)}}
{% endif %}
{% endfor %}
{% if pos15 > 25 %}
<tr style="border-top:2px solid var(--primary)">
<td>{{pos15}}</td>
<td><a style="color:#{{v.namecolor}};font-weight:bold" href="/@{{v.username}}"><img loading="lazy" src="{{v.profile_url}}" class="pp20"><span {% if v.patron %}class="patron" style="background-color:#{{v.namecolor}}"{% endif %}>{{v.username}}</span></a></td>
<td>{{v.winnings}}</td>
</tr>
{% endif %}
</table>
<pre>
</pre>
<a id="leader--top-btn" href="#leaderboard-contents"
style="position: fixed; bottom: 5rem; right: 2rem; font-size: 3rem;">
<i class="fas fa-arrow-alt-circle-up"></i>
</a>
{% endblock %}

View file

@ -0,0 +1,17 @@
{%- include 'admin/shadowbanned_tooltip.html' -%}
{% macro username_display(user, name=None, distinguish=0, display_class="") %}
{% set display_name = user.username %}
{% set display_color_fg = "ffffff" if user.patron else user.name_color %}
{% set display_color_bg = user.name_color if (user.patron and not distinguish) else None %}
{% set display_class = "mod" if distinguish else ("rounded-bg" if user.patron else "") %}
<span class="font-weight-bold {{display_class}}" style="color: #{{display_color_fg}};{% if display_color_bg %} background-color:#{{display_color_bg}};{% endif %}">{{display_name}}</span>
{% endmacro %}
{% if user %}
<a data-sort-key="{{user.username.lower()}}" href="/@{{user.username}}">
<span class="profile-pic-20-wrapper">
<img loading="lazy" src="{{user.profile_url}}" class="pp20">
</span>
{{username_display(user)}}
</a>
{% endif %}