(stats): Rewrite to include session data.

Complete `stats` command rewrite to include session data.
Added `get_member_rank` query to get accurate time and coin ranks.
This commit is contained in:
2021-12-06 20:48:28 +02:00
parent 179b6ebf4e
commit 971446ffb5
2 changed files with 255 additions and 42 deletions

View File

@@ -50,8 +50,6 @@ lions = RowTable(
attach_as='lions' attach_as='lions'
) )
lion_ranks = Table('member_ranks', attach_as='lion_ranks')
@lions.save_query @lions.save_query
def add_pending(pending): def add_pending(pending):
@@ -83,6 +81,35 @@ def add_pending(pending):
return lions._make_rows(*data) return lions._make_rows(*data)
lion_ranks = Table('member_ranks', attach_as='lion_ranks')
@lions.save_query
def get_member_rank(guildid, userid, untracked):
"""
Get the time and coin ranking for the given member, ignoring the provided untracked members.
"""
with lions.conn as conn:
with conn.cursor() as curs:
curs.execute(
"""
SELECT
time_rank, coin_rank
FROM (
SELECT
userid,
row_number() OVER (ORDER BY total_tracked_time DESC, userid ASC) AS time_rank,
row_number() OVER (ORDER BY total_coins DESC, userid ASC) AS coin_rank
FROM members_totals
WHERE
guildid=%s AND userid NOT IN %s
) AS guild_ranks WHERE userid=%s
""",
(guildid, tuple(untracked), userid)
)
return curs.fetchone() or (None, None)
global_guild_blacklist = Table('global_guild_blacklist') global_guild_blacklist = Table('global_guild_blacklist')
global_user_blacklist = Table('global_user_blacklist') global_user_blacklist = Table('global_user_blacklist')
ignored_members = Table('ignored_members') ignored_members = Table('ignored_members')

View File

@@ -1,18 +1,22 @@
import datetime from datetime import datetime, timedelta
import discord import discord
from cmdClient.checks import in_guild from cmdClient.checks import in_guild
from utils.lib import strfdur from utils.lib import strfdur, prop_tabulate, utc_now
from data import tables from data import tables
from data.conditions import LEQ
from core import Lion from core import Lion
from .tracking.data import study_time_since, session_history
from .module import module from .module import module
@module.cmd( @module.cmd(
"stats", "stats",
group="Statistics", group="Statistics",
desc="View a summary of your study statistics!" desc="View a summary of your study statistics!",
aliases=('profile',)
) )
@in_guild() @in_guild()
async def cmd_stats(ctx): async def cmd_stats(ctx):
@@ -23,6 +27,7 @@ async def cmd_stats(ctx):
Description: Description:
View the study statistics for yourself or the mentioned user. View the study statistics for yourself or the mentioned user.
""" """
# Identify the target
if ctx.args: if ctx.args:
if not ctx.msg.mentions: if not ctx.msg.mentions:
return await ctx.error_reply("Please mention a user to view their statistics!") return await ctx.error_reply("Please mention a user to view their statistics!")
@@ -30,54 +35,235 @@ async def cmd_stats(ctx):
else: else:
target = ctx.author target = ctx.author
# Collect the required target data # System sync
Lion.sync()
# Fetch the required data
lion = Lion.fetch(ctx.guild.id, target.id) lion = Lion.fetch(ctx.guild.id, target.id)
rank_data = tables.lion_ranks.select_one_where(
history = session_history.select_where(
guildid=ctx.guild.id,
userid=target.id, userid=target.id,
guildid=ctx.guild.id select_columns=(
"start_time",
"(start_time + duration * interval '1 second') AS end_time"
),
_extra="ORDER BY start_time DESC"
) )
# Extract and format data # Current economy balance (accounting for current session)
time = strfdur(lion.time)
coins = lion.coins coins = lion.coins
workouts = lion.data.workout_count season_time = lion.time
if lion.data.last_study_badgeid: workout_total = lion.data.workout_count
badge_row = tables.study_badges.fetch(lion.data.last_study_badgeid)
league = "<@&{}>".format(badge_row.roleid) # Leaderboard ranks
exclude = set(m.id for m in ctx.guild_settings.unranked_roles.members)
exclude.update(ctx.client.objects['blacklisted_users'])
exclude.update(ctx.client.objects['ignored_members'][ctx.guild.id])
if target.id in exclude:
time_rank = None
coin_rank = None
else: else:
league = "No league yet!" time_rank, coin_rank = tables.lions.queries.get_member_rank(ctx.guild.id, target.id, list(exclude or [0]))
time_lb_pos = rank_data['time_rank'] # Study time
coin_lb_pos = rank_data['coin_rank'] # First get the all/month/week/day timestamps
day_start = lion.day_start
period_timestamps = (
datetime(1970, 1, 1),
day_start.replace(day=1),
day_start - timedelta(days=day_start.weekday()),
day_start
)
study_times = [0, 0, 0, 0]
for i, timestamp in enumerate(period_timestamps):
study_time = tables.session_history.queries.study_time_since(ctx.guild.id, target.id, timestamp)
if not study_time:
# So we don't make unecessary database calls
break
study_times[i] = study_time
# Build embed # Streak statistics
streak = 0
current_streak = None
max_streak = 0
day_attended = True if 'sessions' in ctx.client.objects and lion.session else None
date = day_start
daydiff = timedelta(days=1)
periods = [(row['start_time'], row['end_time']) for row in history]
i = 0
while i < len(periods):
row = periods[i]
i += 1
if row[1] > date:
# They attended this day
day_attended = True
continue
elif day_attended is None:
# Didn't attend today, but don't break streak
day_attended = False
date -= daydiff
i -= 1
continue
elif not day_attended:
# Didn't attend the day, streak broken
date -= daydiff
i -= 1
pass
else:
# Attended the day
streak += 1
# Move window to the previous day and try the row again
day_attended = False
prev_date = date
date -= daydiff
i -= 1
# Special case, when the last session started in the previous day
# Then the day is already attended
if i > 1 and date < periods[i-2][0] <= prev_date:
day_attended = True
continue
max_streak = max(max_streak, streak)
if current_streak is None:
current_streak = streak
streak = 0
# Handle loop exit state, i.e. the last streak
if day_attended:
streak += 1
max_streak = max(max_streak, streak)
if current_streak is None:
current_streak = streak
# Accountability stats
accountability = tables.accountability_member_info.select_where(
userid=target.id,
start_at=LEQ(utc_now()),
select_columns=("*", "(duration > 0 OR last_joined_at IS NOT NULL) AS attended"),
_extra="ORDER BY start_at DESC"
)
if len(accountability):
acc_duration = sum(row['duration'] for row in accountability)
acc_attended = sum(row['attended'] for row in accountability)
acc_total = len(accountability)
acc_rate = (acc_attended * 100) / acc_total
else:
acc_duration = 0
acc_rate = 0
# Study League
guild_badges = tables.study_badges.fetch_rows_where(guildid=ctx.guild.id)
if lion.data.last_study_badgeid:
current_badge = tables.study_badges.fetch(lion.data.last_study_badgeid)
else:
current_badge = None
next_badge = min(
(badge for badge in guild_badges
if badge.required_time > (current_badge.required_time if current_badge else 0)),
key=lambda badge: badge.required_time,
default=None
)
# We have all the data
# Now start building the embed
embed = discord.Embed( embed = discord.Embed(
colour=discord.Colour.blue(), colour=discord.Colour.orange(),
timestamp=datetime.datetime.utcnow(), title="Study Profile for {}".format(str(target))
title="Revision Statistics"
).set_footer(text=str(target), icon_url=target.avatar_url).set_thumbnail(url=target.avatar_url)
embed.add_field(
name="📚 Study Time",
value=time
) )
embed.add_field( embed.set_thumbnail(url=target.avatar_url)
name="🦁 Revision League",
value=league # Add studying since if they have studied
if history:
embed.set_footer(text="Studying Since")
embed.timestamp = history[-1]['start_time']
# Set the description based on season time and server rank
if season_time:
time_str = "**{}:{}**".format(
season_time // 3600,
(season_time // 60) % 60
)
if time_rank is None:
rank_str = None
elif time_rank == 1:
rank_str = "1st"
elif time_rank == 2:
rank_str = "2nd"
elif time_rank == 3:
rank_str = "3rd"
else:
time_rank = "{}th".format(time_rank)
embed.description = "{} has studied for **{}**{}{}".format(
target.mention,
time_str,
" this season" if study_times[0] - season_time > 60 else "",
", and is ranked **{}** in the server!".format(rank_str) if rank_str else "."
)
else:
embed.description = "{} hasn't studied in this server yet!".format(target.mention)
# Build the stats table
stats = {}
stats['Coins Earned'] = "**{}** LC".format(
coins,
# "Rank `{}`".format(coin_rank) if coins and coin_rank else "Unranked"
) )
embed.add_field( if workout_total:
name="🦁 LionCoins", stats['Workouts'] = "**{}** sessions".format(workout_total)
value=coins if acc_duration:
stats['Accountability'] = "**{}** hours (`{:.0f}%` attended)".format(
acc_duration // 3600,
acc_rate
)
stats['Study Streak'] = "**{}** days{}".format(
streak,
" (longest **{}** days)".format(max_streak) if max_streak else ''
) )
embed.add_field(
name="🏆 Leaderboard Position", stats_table = prop_tabulate(*zip(*stats.items()))
value="Time: {}\n LC: {}".format(time_lb_pos, coin_lb_pos)
) # Build the time table
embed.add_field( time_table = prop_tabulate(
name="💪 Workouts", ('Daily', 'Weekly', 'Monthly', 'All Time'),
value=workouts ["{:02}:{:02}".format(t // 3600, (t // 60) % 60) for t in reversed(study_times)]
)
embed.add_field(
name="📋 Attendence",
value="TBD"
) )
# The order they are added depends on the size of the stats table
if len(stats) >= 4:
embed.add_field(name="Statistics", value=stats_table)
embed.add_field(name="Study Time", value=time_table)
else:
embed.add_field(name="Study Time", value=time_table)
embed.add_field(name="Statistics", value=stats_table)
# Add the study league field
if current_badge or next_badge:
current_str = (
"You are currently in <@&{}>!".format(current_badge.roleid) if current_badge else "Not league yet!"
)
if next_badge:
needed = max(next_badge.required_time - season_time, 0)
next_str = "Study for **{:02}:{:02}** more to achieve <@&{}>.".format(
needed // 3600,
(needed // 60) % 60,
next_badge.roleid
)
else:
next_str = "You have reached the highest league! Congratulations!"
embed.add_field(
name="Study League",
value="{}\n{}".format(current_str, next_str),
inline=False
)
await ctx.reply(embed=embed) await ctx.reply(embed=embed)