rewrite: Profile, Stats, Leaderboard.
This commit is contained in:
@@ -5,6 +5,7 @@ from data import NULL
|
||||
from meta import LionBot
|
||||
from gui.cards import WeeklyGoalCard, MonthlyGoalCard
|
||||
from gui.base import CardMode
|
||||
from tracking.text.data import TextTrackerData
|
||||
|
||||
from ..data import StatsData
|
||||
from ..lib import extract_weekid, extract_monthid, apply_week_offset, apply_month_offset
|
||||
@@ -41,7 +42,15 @@ async def get_goals_card(
|
||||
key = {'guildid': guildid or 0, 'userid': userid, 'monthid': periodid}
|
||||
|
||||
# Extract goals and tasks
|
||||
goals = await goal_model.fetch(*key.values())
|
||||
# TODO: Major data model fixy fixy here
|
||||
if guildid:
|
||||
goals = await goal_model.fetch_or_create(*key.values())
|
||||
else:
|
||||
goals = await goal_model.fetch(*key.values())
|
||||
if not goals:
|
||||
from collections import defaultdict
|
||||
goals = defaultdict(lambda: -1)
|
||||
|
||||
task_rows = await tasks_model.fetch_where(**key)
|
||||
tasks = [(i, row.content, bool(row.completed)) for i, row in enumerate(task_rows)]
|
||||
|
||||
@@ -58,11 +67,17 @@ async def get_goals_card(
|
||||
tasks_completed = results[0]['total'] if results else 0
|
||||
|
||||
# Set and compute correct middle goal column
|
||||
# if mode in (CardMode.VOICE, CardMode.STUDY):
|
||||
if True:
|
||||
if mode in (CardMode.VOICE, CardMode.STUDY):
|
||||
model = data.VoiceSessionStats
|
||||
middle_completed = (await model.study_times_between(guildid or None, userid, start, end))[0]
|
||||
middle_goal = goals['study_goal']
|
||||
elif mode is CardMode.TEXT:
|
||||
model = TextTrackerData.TextSessions
|
||||
middle_goal = goals['message_goal']
|
||||
if guildid:
|
||||
middle_completed = (await model.member_messages_between(guildid, userid, start, end))[0]
|
||||
else:
|
||||
middle_completed = (await model.user_messages_between(userid, start, end))[0]
|
||||
|
||||
# Compute schedule session progress
|
||||
# TODO
|
||||
|
||||
74
src/modules/statistics/graphics/leaderboard.py
Normal file
74
src/modules/statistics/graphics/leaderboard.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from meta import LionBot
|
||||
|
||||
from gui.cards import LeaderboardCard
|
||||
from gui.base import CardMode
|
||||
|
||||
|
||||
async def get_leaderboard_card(
|
||||
bot: LionBot, highlightid: int, guildid: int,
|
||||
mode: CardMode,
|
||||
entry_data: list[tuple[int, int, int]], # userid, position, time
|
||||
):
|
||||
"""
|
||||
Render a leaderboard card with given parameters.
|
||||
"""
|
||||
guild = bot.get_guild(guildid)
|
||||
if guild is None:
|
||||
raise ValueError("Attempting to build leaderboard for non-existent guild!")
|
||||
|
||||
# Need to do two passes here in case we need to do a db request for the avatars or names
|
||||
avatars = {}
|
||||
names = {}
|
||||
missing = []
|
||||
for userid, _, _ in entry_data:
|
||||
hash = name = None
|
||||
if guild and (member := guild.get_member(userid)):
|
||||
hash = member.avatar.key
|
||||
name = member.display_name
|
||||
elif (user := bot.get_user(userid)):
|
||||
hash = user.avatar.key
|
||||
name = user.name
|
||||
elif (user_data := bot.core.data.User._cache_.get((userid,))):
|
||||
hash = user_data.avatar_hash
|
||||
name = user_data.name
|
||||
|
||||
if hash:
|
||||
avatars[userid] = hash
|
||||
names[userid] = name or 'Unknown'
|
||||
else:
|
||||
missing.append(userid)
|
||||
|
||||
if missing:
|
||||
# We were unable to retrieve information for some userids
|
||||
# Bulk-fetch missing users from data
|
||||
data = await bot.core.data.User.fetch_where(userid=missing)
|
||||
for user_data in data:
|
||||
avatars[user_data.userid] = user_data.avatar_hash
|
||||
names[user_data.userid] = user_data.name or 'Unknown'
|
||||
missing.remove(user_data.userid)
|
||||
|
||||
if missing:
|
||||
# Some of the users were missing from data
|
||||
# This should be impossible (by FKEY constraints on sessions)
|
||||
# But just in case...
|
||||
for userid in missing:
|
||||
avatars[userid] = None
|
||||
names[userid] = str(userid)
|
||||
|
||||
highlight = None
|
||||
entries = []
|
||||
for userid, position, duration in entry_data:
|
||||
entries.append(
|
||||
(userid, position, duration, names[userid], (userid, avatars[userid]))
|
||||
)
|
||||
if userid == highlightid:
|
||||
highlight = position
|
||||
|
||||
# Request Card
|
||||
card = LeaderboardCard(
|
||||
skin={'mode': mode},
|
||||
server_name=guild.name,
|
||||
entries=entries,
|
||||
highlight=highlight
|
||||
)
|
||||
return card
|
||||
@@ -6,6 +6,7 @@ from data import ORDER
|
||||
from meta import LionBot
|
||||
from gui.cards import MonthlyStatsCard
|
||||
from gui.base import CardMode
|
||||
from tracking.text.data import TextTrackerData
|
||||
|
||||
from ..data import StatsData
|
||||
from ..lib import apply_month_offset
|
||||
@@ -34,8 +35,23 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
[0]*(calendar.monthrange(month.year, month.month)[1]) for month in months
|
||||
]
|
||||
|
||||
# TODO: Select model based on card mode
|
||||
model = data.VoiceSessionStats
|
||||
if mode is CardMode.VOICE:
|
||||
model = data.VoiceSessionStats
|
||||
req = model.study_times_between
|
||||
reqkey = (guildid or None, userid,)
|
||||
elif mode is CardMode.TEXT:
|
||||
model = TextTrackerData.TextSessions
|
||||
if guildid:
|
||||
req = model.member_messages_between
|
||||
reqkey = (guildid, userid,)
|
||||
else:
|
||||
req = model.user_messages_between
|
||||
reqkey = (userid,)
|
||||
else:
|
||||
# TODO: ANKI
|
||||
model = data.VoiceSessionStats
|
||||
req = model.study_times_between
|
||||
reqkey = (guildid or None, userid,)
|
||||
|
||||
# Get first session
|
||||
query = model.table.select_where().order_by('start_time', ORDER.ASC).limit(1)
|
||||
@@ -50,10 +66,10 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
longest_streak = 0
|
||||
else:
|
||||
first_day = first_session.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
first_month = first_day.replace(day=1)
|
||||
# first_month = first_day.replace(day=1)
|
||||
|
||||
# Build list of day starts up to now, or end of requested month
|
||||
requests = []
|
||||
requests = [first_day]
|
||||
end_of_req = target_end if offset else today
|
||||
day = first_day
|
||||
while day <= end_of_req:
|
||||
@@ -61,7 +77,7 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
requests.append(day)
|
||||
|
||||
# Request times between requested days
|
||||
day_stats = await model.study_times_between(guildid or None, userid, *requests)
|
||||
day_stats = await req(*reqkey, *requests)
|
||||
|
||||
# Compute current streak and longest streak
|
||||
current_streak = 0
|
||||
@@ -79,7 +95,10 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
if day < months[0]:
|
||||
break
|
||||
i = offsets[(day.year, day.month)]
|
||||
monthly[i][day.day - 1] = stat / 3600
|
||||
if mode in (CardMode.VOICE, CardMode.STUDY):
|
||||
monthly[i][day.day - 1] = stat / 3600
|
||||
else:
|
||||
monthly[i][day.day - 1] = stat
|
||||
|
||||
# Get member profile
|
||||
if user:
|
||||
|
||||
91
src/modules/statistics/graphics/profile.py
Normal file
91
src/modules/statistics/graphics/profile.py
Normal file
@@ -0,0 +1,91 @@
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import discord
|
||||
|
||||
from meta import LionBot
|
||||
from gui.cards import ProfileCard
|
||||
|
||||
from modules.ranks.cog import RankCog
|
||||
from modules.ranks.utils import format_stat_range
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..cog import StatsCog
|
||||
|
||||
|
||||
async def get_profile_card(bot: LionBot, userid: int, guildid: int):
|
||||
ranks: Optional[RankCog] = bot.get_cog('RankCog')
|
||||
stats: Optional[StatsCog] = bot.get_cog('StatsCog')
|
||||
if ranks is None or stats is None:
|
||||
return
|
||||
|
||||
guild = bot.get_guild(guildid)
|
||||
if guild is None:
|
||||
return
|
||||
|
||||
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||
luser = lion.luser
|
||||
member = await lion.fetch_member()
|
||||
|
||||
if member:
|
||||
username = (member.display_name, '#' + str(member.discriminator))
|
||||
avatar = member.avatar.key
|
||||
else:
|
||||
username = (lion.data.display_name, "#????")
|
||||
avatar = luser.data.avatar_hash
|
||||
|
||||
profile_badges = await stats.data.ProfileTag.fetch_tags(guildid, userid)
|
||||
|
||||
# Fetch current and next guild rank
|
||||
season_rank = await ranks.get_member_rank(guildid, userid)
|
||||
rank_type = lion.lguild.config.get('rank_type').value
|
||||
crank = season_rank.current_rank
|
||||
nrank = season_rank.next_rank
|
||||
if crank:
|
||||
roleid = crank.roleid
|
||||
role = guild.get_role(roleid)
|
||||
name = role.name if role else str(role.id)
|
||||
minimum = crank.required
|
||||
maximum = nrank.required if nrank else None
|
||||
rangestr = format_stat_range(rank_type, minimum, maximum)
|
||||
if maximum is None:
|
||||
rangestr = f"≥ {rangestr}"
|
||||
current_rank = (name, rangestr)
|
||||
|
||||
if maximum:
|
||||
rank_progress = (season_rank.stat - minimum) / (maximum - minimum)
|
||||
else:
|
||||
rank_progress = 1
|
||||
else:
|
||||
current_rank = None
|
||||
rank_progress = 0
|
||||
|
||||
if nrank:
|
||||
roleid = nrank.roleid
|
||||
role = guild.get_role(roleid)
|
||||
name = role.name if role else str(role.id)
|
||||
minimum = nrank.required
|
||||
|
||||
guild_ranks = await ranks.get_guild_ranks(guildid)
|
||||
nnrank = next((rank for rank in guild_ranks if rank.required > nrank.required), None)
|
||||
maximum = nnrank.required if nnrank else None
|
||||
rangestr = format_stat_range(rank_type, minimum, maximum)
|
||||
if maximum is None:
|
||||
rangestr = f"≥ {rangestr}"
|
||||
next_rank = (name, rangestr)
|
||||
else:
|
||||
next_rank = None
|
||||
|
||||
achievements = (0, 1)
|
||||
|
||||
card = ProfileCard(
|
||||
user=username,
|
||||
avatar=(userid, avatar),
|
||||
coins=lion.data.coins, gems=luser.data.gems, gifts=0,
|
||||
profile_badges=profile_badges,
|
||||
achievements=achievements,
|
||||
current_rank=current_rank,
|
||||
rank_progress=rank_progress,
|
||||
next_rank=next_rank
|
||||
)
|
||||
return card
|
||||
@@ -5,15 +5,16 @@ import discord
|
||||
|
||||
from meta import LionBot
|
||||
from gui.cards import StatsCard
|
||||
from gui.base import CardMode
|
||||
|
||||
from ..data import StatsData
|
||||
|
||||
|
||||
async def get_stats_card(bot: LionBot, userid: int, guildid: int):
|
||||
async def get_stats_card(bot: LionBot, userid: int, guildid: int, mode: CardMode):
|
||||
data: StatsData = bot.get_cog('StatsCog').data
|
||||
|
||||
# TODO: Workouts
|
||||
# TODO: Leaderboard rankings
|
||||
# TODO: Leaderboard rankings for this season or all time
|
||||
guildid = guildid or 0
|
||||
|
||||
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||
@@ -31,20 +32,40 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int):
|
||||
)
|
||||
|
||||
# Extract the study times for each period
|
||||
study_times = await data.VoiceSessionStats.study_times_since(guildid, userid, *period_timestamps)
|
||||
print("Study times", study_times)
|
||||
if mode in (CardMode.STUDY, CardMode.VOICE):
|
||||
model = data.VoiceSessionStats
|
||||
refkey = (guildid, userid)
|
||||
ref_since = model.study_times_since
|
||||
ref_between = model.study_times_between
|
||||
elif mode is CardMode.TEXT:
|
||||
if guildid:
|
||||
model = data.MemberExp
|
||||
refkey = (guildid, userid)
|
||||
else:
|
||||
model = data.UserExp
|
||||
refkey = (userid,)
|
||||
ref_since = model.xp_since
|
||||
ref_between = model.xp_between
|
||||
else:
|
||||
# TODO ANKI
|
||||
model = data.VoiceSessionStats
|
||||
refkey = (guildid, userid)
|
||||
ref_since = model.study_times_since
|
||||
ref_between = model.study_times_between
|
||||
|
||||
study_times = await ref_since(*refkey, *period_timestamps)
|
||||
print("Period study times: ", study_times)
|
||||
|
||||
# Calculate streak data by requesting times per day
|
||||
# First calculate starting timestamps for each day
|
||||
days = list(range(0, today.day + 2))
|
||||
day_timestamps = [month_start + timedelta(days=day - 1) for day in days]
|
||||
study_times = await data.VoiceSessionStats.study_times_between(guildid, userid, *day_timestamps)
|
||||
print("Study times", study_times)
|
||||
study_times_month = await ref_between(*refkey, *day_timestamps)
|
||||
|
||||
# Then extract streak tuples
|
||||
streaks = []
|
||||
streak_start = None
|
||||
for day, stime in zip(days, study_times):
|
||||
for day, stime in zip(days, study_times_month):
|
||||
stime = stime or 0
|
||||
if stime > 0 and streak_start is None:
|
||||
streak_start = day
|
||||
@@ -59,5 +80,6 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int):
|
||||
list(reversed(study_times)),
|
||||
100,
|
||||
streaks,
|
||||
skin={'mode': mode}
|
||||
)
|
||||
return card
|
||||
|
||||
@@ -5,6 +5,7 @@ from data import ORDER
|
||||
from meta import LionBot
|
||||
from gui.cards import WeeklyStatsCard
|
||||
from gui.base import CardMode
|
||||
from tracking.text.data import TextTrackerData
|
||||
|
||||
from ..data import StatsData
|
||||
|
||||
@@ -20,13 +21,27 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
user = await bot.fetch_user(userid)
|
||||
today = lion.today
|
||||
week_start = today - timedelta(days=today.weekday()) - timedelta(weeks=offset)
|
||||
days = [week_start + timedelta(i) for i in range(-7, 7 if offset else (today.weekday() + 1))]
|
||||
days = [week_start + timedelta(i) for i in range(-7, 8 if offset else (today.weekday() + 2))]
|
||||
|
||||
# TODO: Select statistics model based on mode
|
||||
model = data.VoiceSessionStats
|
||||
if mode is CardMode.VOICE:
|
||||
model = data.VoiceSessionStats
|
||||
day_stats = await model.study_times_between(guildid or None, userid, *days)
|
||||
day_stats = list(map(lambda n: n // 3600, day_stats))
|
||||
elif mode is CardMode.TEXT:
|
||||
model = TextTrackerData.TextSessions
|
||||
if guildid:
|
||||
day_stats = await model.member_messages_between(guildid, userid, *days)
|
||||
else:
|
||||
day_stats = await model.user_messages_between(userid, *days)
|
||||
else:
|
||||
# TODO: ANKI
|
||||
model = data.VoiceSessionStats
|
||||
day_stats = await model.study_times_between(guildid or None, userid, *days)
|
||||
day_stats = list(map(lambda n: n // 3600, day_stats))
|
||||
|
||||
# Get user session rows
|
||||
query = model.table.select_where()
|
||||
query = model.table.select_where(model.start_time >= days[0])
|
||||
if guildid:
|
||||
query = query.where(userid=userid, guildid=guildid).order_by('start_time', ORDER.ASC)
|
||||
else:
|
||||
@@ -34,7 +49,6 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
sessions = await query
|
||||
|
||||
# Extract quantities per-day
|
||||
day_stats = await model.study_times_between(guildid or None, userid, *days)
|
||||
for i in range(14 - len(day_stats)):
|
||||
day_stats.append(0)
|
||||
|
||||
@@ -49,9 +63,9 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
timezone=str(lion.timezone),
|
||||
now=lion.now.timestamp(),
|
||||
week=week_start.timestamp(),
|
||||
daily=tuple(map(lambda n: n/3600, day_stats)),
|
||||
daily=tuple(map(int, day_stats)),
|
||||
sessions=[
|
||||
(int(session['start_time'].timestamp()), int(session['end_time'].timestamp()))
|
||||
(int(session['start_time'].timestamp()), int(session['start_time'].timestamp() + int(session['duration'])))
|
||||
for session in sessions
|
||||
],
|
||||
skin={'mode': mode}
|
||||
|
||||
Reference in New Issue
Block a user