From 8873171f5c420ecd6f08eac7b82bfac9847c645b Mon Sep 17 00:00:00 2001 From: justcool393 Date: Fri, 28 Oct 2022 03:42:02 -0500 Subject: [PATCH] leaderboards: refactor leaderboards to their own class right now, leaderboards are complex enough that they ought to be upgraded to at least a second class thing. this commit provides an *okay* implementation of a per-request leaderboard there are many things to be done, including caching, persistence, etc i don't like this like 80 parameter __init__ but it's what i've got without overengineering it imo this is potentially already overdoing it --- files/classes/leaderboard.py | 88 ++++++++++++++++++++++++++++++++ files/routes/users.py | 83 ++++++------------------------ files/templates/leaderboard.html | 72 ++++++-------------------- 3 files changed, 121 insertions(+), 122 deletions(-) create mode 100644 files/classes/leaderboard.py diff --git a/files/classes/leaderboard.py b/files/classes/leaderboard.py new file mode 100644 index 000000000..ead31d0a0 --- /dev/null +++ b/files/classes/leaderboard.py @@ -0,0 +1,88 @@ +from typing import Any, Callable, Optional, Tuple +from sqlalchemy import func + +from .badges import Badge +from .marsey import Marsey +from .user import User +from .userblock import UserBlock + +class Leaderboard: + """Represents an request-context leaderboard. None of this is persisted yet, + although this is probably a good idea to do at some point. + """ + all_users = None + v_position = 0 + v_value = None + value_func = None + + def __init__(self, header_name:str, table_header_name:str, html_id:str, table_column_name:str, + user_relative_url:Optional[str], query_function:Callable[..., Tuple[Any, Any, Any]], + criteria, v:User, value_func:Optional[Callable[[User], int]], db, users, limit=25): + self.header_name = header_name + self.table_header_name = table_header_name + self.html_id = html_id + self.table_column_name = table_column_name + self.user_relative_url = user_relative_url + self.limit = limit + lb = query_function(criteria, v, db, users, limit) + self.all_users = lb[0] + self.v_position = lb[1] + self.v_value = lb[2] + if not self.v_value: + if value_func: + self.value_func = value_func + self.v_value = value_func(v) + else: + self.value_func = lambda u: u[1] + + def get_simple_lb(cls, order_by, v:User, db, users, limit): + leaderboard = users.order_by(order_by.desc()).limit(limit).all() + position = None + if v not in leaderboard: + sq = db.query(User.id, func.rank().over(order_by=order_by.desc()).label("rank")).subquery() + position = db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] + return (leaderboard, position, None) + + def count_and_label(cls, criteria): + return func.count(criteria).label("count") + + def rank_filtered_rank_label_by_desc(cls, criteria): + return func.rank().over(order_by=func.count(criteria).desc()).label("rank") + + def get_badge_marsey_lb(cls, lb_criteria, v:User, db, users:Any, limit): + sq = db.query(lb_criteria, cls.count_and_label(lb_criteria), cls.rank_filtered_rank_label_by_desc(lb_criteria)).group_by(lb_criteria).subquery() + sq_criteria = None + if lb_criteria == Badge.user_id: + sq_criteria = User.id == sq.c.user_id + elif lb_criteria == 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 = db.query(User, sq.c.count).join(sq, sq_criteria).order_by(sq.c.count.desc()) + position = db.query(User.id, sq.c.rank, sq.c.count).join(sq, sq_criteria).filter(User.id == v.id).one_or_none() + if position: position = (position[1], position[2]) + else: position = (leaderboard.count() + 1, 0) + leaderboard = leaderboard.limit(limit).all() + return (leaderboard, position[0], position[1]) + + def get_blockers_lb(cls, lb_criteria, v:User, db, users:Any, limit): + if lb_criteria != UserBlock.target_id: + raise ValueError("This leaderboard function only supports UserBlock.target_id") + sq = db.query(lb_criteria, cls.count_and_label(lb_criteria)).group_by(lb_criteria).subquery() + leaderboard = db.query(User, sq.c.count).join(User, User.id == sq.c.target_id).order_by(sq.c.count.desc()) + + sq = db.query(lb_criteria, cls.count_and_label(lb_criteria), cls.rank_filtered_rank_label_by_desc(lb_criteria)).group_by(lb_criteria).subquery() + position = db.query(sq.c.rank, sq.c.count).join(User, User.id == sq.c.target_id).filter(sq.c.target_id == v.id).limit(1).one_or_none() + if not position: position = (leaderboard.count() + 1, 0) + leaderboard = leaderboard.limit(limit).all() + return (leaderboard, position[0], position[1]) + + def get_hat_lb(cls, lb_criteria, v:User, db, users:Any, limit): + leaderboard = db.query(User.id, func.count(lb_criteria)).join(lb_criteria).group_by(User).order_by(func.count(lb_criteria).desc()) + sq = db.query(User.id, cls.count_and_label(lb_criteria), cls.rank_filtered_rank_label_by_desc(lb_criteria)).join(lb_criteria).group_by(User).subquery() + position = db.query(sq.c.rank, sq.c.count).filter(sq.c.id == v.id).limit(1).one_or_none() + if not position: position = (leaderboard.count() + 1, 0) + leaderboard = leaderboard.limit(limit).all() + return (leaderboard, position[0], position[1]) + diff --git a/files/routes/users.py b/files/routes/users.py index e817ce855..bc385d30a 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -2,6 +2,7 @@ import qrcode import io import time import math +from files.classes.leaderboard import Leaderboard from files.classes.views import * from files.classes.transactions import * from files.helpers.alerts import * @@ -344,80 +345,28 @@ def transfer_bux(v, username): @app.get("/leaderboard") @auth_required def leaderboard(v): + LEADERBOARD_LIMIT = 25 users = g.db.query(User) - def get_leaderboard(order_by, limit=25): - leaderboard = users.order_by(order_by.desc()).limit(limit).all() - position = None - if v not in leaderboard: - sq = g.db.query(User.id, func.rank().over(order_by=order_by.desc()).label("rank")).subquery() - position = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1] - return (leaderboard, position) - - coins = get_leaderboard(User.coins) - subscribers = get_leaderboard(User.stored_subscriber_count) - posts = get_leaderboard(User.post_count) - comments = get_leaderboard(User.comment_count) - received_awards = get_leaderboard(User.received_award_count) - coins_spent = get_leaderboard(User.coins_spent) - truecoins = get_leaderboard(User.truecoins) + coins = Leaderboard("Coins", "coins", "coins", "Coins", None, Leaderboard.get_simple_lb, User.coins, v, v.coins, g.db, users, LEADERBOARD_LIMIT) + subscribers = Leaderboard("Followers", "followers", "followers", "Followers", None, Leaderboard.get_simple_lb, User.stored_subscriber_count, v, v.stored_subs, g.db, users, LEADERBOARD_LIMIT) + posts = Leaderboard("Posts", "post count", "posts", "Posts", None, Leaderboard.get_simple_lb, User.post_count, v, v.post_count, users, LEADERBOARD_LIMIT) + comments = Leaderboard("Comments", "comment count", "comments", "Comments", None, Leaderboard.get_simple_lb, User.post_count, v, v.post_count, users, LEADERBOARD_LIMIT) + received_awards = Leaderboard("Awards", "received awards", "awards", "Awards", None, Leaderboard.get_simple_lb, User.received_award_count, v, v.received_award_count, db, users, LEADERBOARD_LIMIT) + coins_spent = Leaderboard("Spent in shop", "coins spent in shop", "spent", "Coins", None, Leaderboard.get_simple_lb, User.coins_spent, v, v.coins_spent, db, users, LEADERBOARD_LIMIT) + truecoins = Leaderboard("Truescore", "truescore", "truescore", "Truescore", None, Leaderboard.get_simple_lb, User.truecoins, v, v.truecoins, db, users, LEADERBOARD_LIMIT) - def count_and_label(criteria): - return func.count(criteria).label("count") - - def rank_filtered_rank_label_by_desc(criteria): - return func.rank().over(order_by=func.count(criteria).desc()).label("rank") + badges = Leaderboard("Badges", "badges", "badges", "Badges", None, Leaderboard.get_badge_marsey_lb, Badge.user_id, v, None, db, None, LEADERBOARD_LIMIT) + marseys = Leaderboard("Marseys", "Marseys made", "marseys", "Marseys", None, Leaderboard.get_badge_marsey_lb, Marsey.author_id, v, None, db, None, LEADERBOARD_LIMIT) if SITE_NAME == 'rDrama' else None - def get_leaderboard_2(lb_criteria, limit=25): - sq = g.db.query(lb_criteria, count_and_label(lb_criteria), rank_filtered_rank_label_by_desc(lb_criteria)).group_by(lb_criteria).subquery() - sq_criteria = None - if lb_criteria == Badge.user_id: - sq_criteria = User.id == sq.c.user_id - elif lb_criteria == 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 = g.db.query(User, sq.c.count).join(sq, sq_criteria).order_by(sq.c.count.desc()) - position = g.db.query(User.id, sq.c.rank, sq.c.count).join(sq, sq_criteria).filter(User.id == v.id).one_or_none() - if position: position = (position[1], position[2]) - else: position = (leaderboard.count() + 1, 0) - leaderboard = leaderboard.limit(limit).all() - return (leaderboard, position) + blocks = Leaderboard("Blocked", "most blocked", "blocked", "Blocked By", "blockers", Leaderboard.get_blockers_lb, UserBlock.target_id, v, None, db, None, LEADERBOARD_LIMIT) - badges = get_leaderboard_2(Badge.user_id) - marseys = get_leaderboard_2(Marsey.author_id) if SITE_NAME == 'rDrama' else (None, None) + owned_hats = Leaderboard("Owned hats", "owned hats", "owned-hats", "Owned Hats", None, Leaderboard.get_hat_lb, User.owned_hats, v, None, db, None, LEADERBOARD_LIMIT) + designed_hats = Leaderboard("Designed hats", "designed hats", "designed-hats", "Designed Hats", None, Leaderboard.get_hat_lb, User.designed_hats, v, None, db, None, LEADERBOARD_LIMIT) - def get_leaderboard_3(lb_criteria, limit=25): - if lb_criteria != UserBlock.target_id: - raise ValueError("This leaderboard function only supports UserBlock.target_id") - sq = g.db.query(lb_criteria, count_and_label(lb_criteria)).group_by(lb_criteria).subquery() - leaderboard = g.db.query(User, sq.c.count).join(User, User.id == sq.c.target_id).order_by(sq.c.count.desc()) - - sq = g.db.query(lb_criteria, count_and_label(lb_criteria), rank_filtered_rank_label_by_desc(lb_criteria)).group_by(lb_criteria).subquery() - position = g.db.query(sq.c.rank, sq.c.count).join(User, User.id == sq.c.target_id).filter(sq.c.target_id == v.id).limit(1).one_or_none() - if not position: position = (leaderboard.count() + 1, 0) - leaderboard = leaderboard.limit(limit).all() - return (leaderboard, position) + leaderboards = [coins, coins_spent, truecoins, subscribers, posts, comments, received_awards, badges, marseys, blocks, owned_hats, designed_hats] - blocks = get_leaderboard_3(UserBlock.target_id) - - def get_leaderboard_4(lb_criteria, limit=25): - leaderboard = g.db.query(User.id, func.count(lb_criteria)).join(lb_criteria).group_by(User).order_by(func.count(lb_criteria).desc()) - sq = g.db.query(User.id, count_and_label(lb_criteria), rank_filtered_rank_label_by_desc(lb_criteria)).join(lb_criteria).group_by(User).subquery() - position = g.db.query(sq.c.rank, sq.c.count).filter(sq.c.id == v.id).limit(1).one_or_none() - if not position: position = (leaderboard.count() + 1, 0) - leaderboard = leaderboard.limit(limit).all() - return (leaderboard, position) - - owned_hats = get_leaderboard_4(User.owned_hats) - designed_hats = get_leaderboard_4(User.designed_hats) - - return render_template("leaderboard.html", v=v, users1=coins[0], pos1=coins[1], users2=subscribers[0], pos2=subscribers[1], - users3=posts[0], pos3=posts[1], users4=comments[0], pos4=comments[1], users5=received_awards[0], pos5=received_awards[1], - users7=coins_spent[0], pos7=coins_spent[1], users10=truecoins[0], pos10=truecoins[1], users11=badges[0], pos11=badges[1], - users12=marseys[0], pos12=marseys[1], users16=blocks[0], pos16=blocks[1], users17=owned_hats[0], pos17=owned_hats[1], - users18=designed_hats[0], pos18=designed_hats[1]) + return render_template("leaderboard.html", v=v, leaderboards=leaderboards) @app.get("//css") def get_css(id): diff --git a/files/templates/leaderboard.html b/files/templates/leaderboard.html index f33ed9ac7..6ff1707fa 100644 --- a/files/templates/leaderboard.html +++ b/files/templates/leaderboard.html @@ -1,16 +1,11 @@ {% extends "settings2.html" %} {% block pagetitle %}Leaderboard{% endblock %} {% block content %} -{%- set LEADERBOARDS = [ - ('coins', 'Coins', True, True), ('spent', 'Spent in shop', True, True), ('truescore', 'Truescore', True, True), ('followers', 'Followers', True, True), - ('posts', 'Posts', True, True), ('comments', 'Comments', True, True), ('awards', 'Awards', True, True), ('badges', 'Badges', True, True), - ('marseys', 'Marseys', SITE_NAME == 'rDrama', True), ('blocked', 'Blocked', True, True), ('owned-hats', 'Owned hats', True, True), ('designed-hats', 'Designed hats', True, False) -] -%}

 
- {% for lb in LEADERBOARDS %} - {% if lb[2] %} - {{lb[1]}}{% if lb[3] %} •{% endif %} + {% for lb in leaderboards %} + {% if lb %} + {{lb.header_name}}{% if not loop.last %} •{% endif %} {% endif %} {% endfor %}
@@ -28,70 +23,37 @@ {% endmacro %} -{% macro leaderboard_table_header(column_name) %} +{% macro leaderboard_table(lb, position, id, header_name, v_value) %} +
Top {{lb.limit}} by {{table_header_name}}
+
+{# TODO: check at some point if the nesting divs are intentional #} + - + -{% endmacro %} - -{% macro leaderboard_table(lb, position, id, total_count, header_name, column_name, attr_name) %} -
Top {{total_count}} by {{header_name}}
-
-{# TODO: check at some point if the nesting divs are intentional #} -
# Name{{column_name}}{{lb.table_column_name}}
- {{leaderboard_table_header(column_name)}} - {% for user in lb %} + {% for user in lb.all_users %} {% if v.id == user.id %} {% set style="class=\"self\"" %} {% endif %} - {{format_user_in_table(user, style, loop.index, user[attr_name])}} + {{format_user_in_table(user, style, loop.index, lb.value_func(user))}} {% endfor %} {% if position %} - {{format_user_in_table(v, "style=\"border-top:2px solid var(--primary)\"", position, v[attr_name])}} + {{format_user_in_table(v, "style=\"border-top:2px solid var(--primary)\"", position, v_value)}} {% endif %}
{% endmacro %} -{{leaderboard_table(users1, pos1, 'coins', 25, 'coins', 'Coins', 'coins')}} -{{leaderboard_table(users7, pos7, 'spent', 25, 'coins spent in shop', 'Coins', 'coins_spent')}} -{{leaderboard_table(users10, pos10, 'truescore', 25, 'truescore', 'Truescore', 'truecoins')}} -{{leaderboard_table(users2, pos2, 'followers', 25, 'followers', 'Followers', 'stored_subscriber_count')}} -{{leaderboard_table(users3, pos3, 'posts', 25, 'post count', 'Posts', 'post_count')}} -{{leaderboard_table(users4, pos4, 'comments', 25, 'comment count', 'Comments', 'comment_count')}} -{{leaderboard_table(users5, pos5, 'awards', 25, 'received awards', 'Awards', 'awards')}} - -{% macro leaderboard_table_2(lb, position, id, total_count, header_name, column_name, user_relative_url) %} -
Top {{total_count}} by {{header_name}}
-
- - {{leaderboard_table_header(column_name)}} - - {% for user, num in lb %} - {% if v.id == user.id %} - {% set style="class=\"self\"" %} - {% endif %} - {{format_user_in_table(user, style, loop.index, num, user_relative_url)}} - {% endfor %} - {% if position and (position[0] > total_count or not position[1]) %} - {{format_user_in_table(v, "style=\"border-top:2px solid var(--primary)\"", position[0], position[1], user_relative_url)}} - {% endif %} - -
-{% endmacro %} - -{{leaderboard_table_2(users11, pos11, 'badges', 25, 'badges', 'Badges')}} -{% if users12 %} - {{leaderboard_table_2(users12, pos12, 'marseys', 25, 'Marseys made', 'Marseys')}} -{% endif %} -{{leaderboard_table_2(users16, pos16, 'blocked', 25, 'most blocked', 'Blocked By', 'blockers')}} -{{leaderboard_table_2(users17, pos17, 'owned-hats', 25, 'owned hats', 'Owned Hats')}} -{{leaderboard_table_2(users18, pos18, 'designed-hats', 25, 'designed hats', 'Designed Hats')}} +{% for lb in leaderboards %} + {% if lb %} + {{leaderboard_table(lb, lb.v_position, lb.html_id, lb.header_name, lb.v_value)}} + {% endif %} +{% endfor %}