forked from MarseyWorld/MarseyWorld
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 itmaster
parent
0258bfb356
commit
8873171f5c
|
@ -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])
|
||||||
|
|
|
@ -2,6 +2,7 @@ import qrcode
|
||||||
import io
|
import io
|
||||||
import time
|
import time
|
||||||
import math
|
import math
|
||||||
|
from files.classes.leaderboard import Leaderboard
|
||||||
from files.classes.views import *
|
from files.classes.views import *
|
||||||
from files.classes.transactions import *
|
from files.classes.transactions import *
|
||||||
from files.helpers.alerts import *
|
from files.helpers.alerts import *
|
||||||
|
@ -344,80 +345,28 @@ def transfer_bux(v, username):
|
||||||
@app.get("/leaderboard")
|
@app.get("/leaderboard")
|
||||||
@auth_required
|
@auth_required
|
||||||
def leaderboard(v):
|
def leaderboard(v):
|
||||||
|
LEADERBOARD_LIMIT = 25
|
||||||
users = g.db.query(User)
|
users = g.db.query(User)
|
||||||
|
|
||||||
def get_leaderboard(order_by, limit=25):
|
coins = Leaderboard("Coins", "coins", "coins", "Coins", None, Leaderboard.get_simple_lb, User.coins, v, v.coins, g.db, users, LEADERBOARD_LIMIT)
|
||||||
leaderboard = users.order_by(order_by.desc()).limit(limit).all()
|
subscribers = Leaderboard("Followers", "followers", "followers", "Followers", None, Leaderboard.get_simple_lb, User.stored_subscriber_count, v, v.stored_subs, g.db, users, LEADERBOARD_LIMIT)
|
||||||
position = None
|
posts = Leaderboard("Posts", "post count", "posts", "Posts", None, Leaderboard.get_simple_lb, User.post_count, v, v.post_count, users, LEADERBOARD_LIMIT)
|
||||||
if v not in leaderboard:
|
comments = Leaderboard("Comments", "comment count", "comments", "Comments", None, Leaderboard.get_simple_lb, User.post_count, v, v.post_count, users, LEADERBOARD_LIMIT)
|
||||||
sq = g.db.query(User.id, func.rank().over(order_by=order_by.desc()).label("rank")).subquery()
|
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)
|
||||||
position = g.db.query(sq.c.id, sq.c.rank).filter(sq.c.id == v.id).limit(1).one()[1]
|
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)
|
||||||
return (leaderboard, position)
|
truecoins = Leaderboard("Truescore", "truescore", "truescore", "Truescore", None, Leaderboard.get_simple_lb, User.truecoins, v, v.truecoins, db, users, LEADERBOARD_LIMIT)
|
||||||
|
|
||||||
coins = get_leaderboard(User.coins)
|
badges = Leaderboard("Badges", "badges", "badges", "Badges", None, Leaderboard.get_badge_marsey_lb, Badge.user_id, v, None, db, None, LEADERBOARD_LIMIT)
|
||||||
subscribers = get_leaderboard(User.stored_subscriber_count)
|
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
|
||||||
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)
|
|
||||||
|
|
||||||
def count_and_label(criteria):
|
blocks = Leaderboard("Blocked", "most blocked", "blocked", "Blocked By", "blockers", Leaderboard.get_blockers_lb, UserBlock.target_id, v, None, db, None, LEADERBOARD_LIMIT)
|
||||||
return func.count(criteria).label("count")
|
|
||||||
|
|
||||||
def rank_filtered_rank_label_by_desc(criteria):
|
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)
|
||||||
return func.rank().over(order_by=func.count(criteria).desc()).label("rank")
|
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_2(lb_criteria, limit=25):
|
leaderboards = [coins, coins_spent, truecoins, subscribers, posts, comments, received_awards, badges, marseys, blocks, owned_hats, designed_hats]
|
||||||
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())
|
return render_template("leaderboard.html", v=v, leaderboards=leaderboards)
|
||||||
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)
|
|
||||||
|
|
||||||
badges = get_leaderboard_2(Badge.user_id)
|
|
||||||
marseys = get_leaderboard_2(Marsey.author_id) if SITE_NAME == 'rDrama' else (None, None)
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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])
|
|
||||||
|
|
||||||
@app.get("/<id>/css")
|
@app.get("/<id>/css")
|
||||||
def get_css(id):
|
def get_css(id):
|
||||||
|
|
|
@ -1,16 +1,11 @@
|
||||||
{% extends "settings2.html" %}
|
{% extends "settings2.html" %}
|
||||||
{% block pagetitle %}Leaderboard{% endblock %}
|
{% block pagetitle %}Leaderboard{% endblock %}
|
||||||
{% block content %}
|
{% 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)
|
|
||||||
] -%}
|
|
||||||
<pre class="d-none d-md-inline-block"></pre>
|
<pre class="d-none d-md-inline-block"></pre>
|
||||||
<div id="leaderboard-contents" style="text-align: center; margin-bottom: 1.5rem; font-size: 1.2rem;">
|
<div id="leaderboard-contents" style="text-align: center; margin-bottom: 1.5rem; font-size: 1.2rem;">
|
||||||
{% for lb in LEADERBOARDS %}
|
{% for lb in leaderboards %}
|
||||||
{% if lb[2] %}
|
{% if lb %}
|
||||||
<a href="#leaderboard-{{lb[0]}}">{{lb[1]}}</a>{% if lb[3] %} •{% endif %}
|
<a href="#leaderboard-{{lb.html_id}}">{{lb.header_name}}</a>{% if not loop.last %} •{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,70 +23,37 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro leaderboard_table_header(column_name) %}
|
{% macro leaderboard_table(lb, position, id, header_name, v_value) %}
|
||||||
|
<h5 class="font-weight-bolder text-center pt-2 pb-3"><a id="leaderboard-{{id}}">Top {{lb.limit}} by {{table_header_name}}</a></h5>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
{# TODO: check at some point if the nesting divs are intentional #}
|
||||||
|
<table class="table table-striped mb-5">
|
||||||
<thead class="bg-primary text-white">
|
<thead class="bg-primary text-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>#</th>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>{{column_name}}</th>
|
<th>{{lb.table_column_name}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
{% endmacro %}
|
|
||||||
|
|
||||||
{% macro leaderboard_table(lb, position, id, total_count, header_name, column_name, attr_name) %}
|
|
||||||
<h5 class="font-weight-bolder text-center pt-2 pb-3"><a id="leaderboard-{{id}}">Top {{total_count}} by {{header_name}}</a></h5>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
{# TODO: check at some point if the nesting divs are intentional #}
|
|
||||||
<table class="table table-striped mb-5">
|
|
||||||
{{leaderboard_table_header(column_name)}}
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for user in lb %}
|
{% for user in lb.all_users %}
|
||||||
{% if v.id == user.id %}
|
{% if v.id == user.id %}
|
||||||
{% set style="class=\"self\"" %}
|
{% set style="class=\"self\"" %}
|
||||||
{% endif %}
|
{% 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 %}
|
{% endfor %}
|
||||||
{% if position %}
|
{% 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 %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{{leaderboard_table(users1, pos1, 'coins', 25, 'coins', 'Coins', 'coins')}}
|
{% for lb in leaderboards %}
|
||||||
{{leaderboard_table(users7, pos7, 'spent', 25, 'coins spent in shop', 'Coins', 'coins_spent')}}
|
{% if lb %}
|
||||||
{{leaderboard_table(users10, pos10, 'truescore', 25, 'truescore', 'Truescore', 'truecoins')}}
|
{{leaderboard_table(lb, lb.v_position, lb.html_id, lb.header_name, lb.v_value)}}
|
||||||
{{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) %}
|
|
||||||
<h5 class="font-weight-bolder text-center pt-2 pb-3"><a id="leaderboard-{{id}}">Top {{total_count}} by {{header_name}}</a></h5>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<table class="table table-striped mb-5">
|
|
||||||
{{leaderboard_table_header(column_name)}}
|
|
||||||
<tbody>
|
|
||||||
{% for user, num in lb %}
|
|
||||||
{% if v.id == user.id %}
|
|
||||||
{% set style="class=\"self\"" %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{format_user_in_table(user, style, loop.index, num, user_relative_url)}}
|
|
||||||
{% endfor %}
|
{% 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 %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% 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')}}
|
|
||||||
|
|
||||||
<a id="leader--top-btn" href="#leaderboard-contents" role="button"
|
<a id="leader--top-btn" href="#leaderboard-contents" role="button"
|
||||||
style="position: fixed; bottom: 5rem; right: 2rem; font-size: 3rem;">
|
style="position: fixed; bottom: 5rem; right: 2rem; font-size: 3rem;">
|
||||||
|
|
Loading…
Reference in New Issue