rewrite: Initial rewrite skeleton.
Remove modules that will no longer be required. Move pending modules to pending-rewrite folders.
This commit is contained in:
9
bot/modules/pending-rewrite/stats/__init__.py
Normal file
9
bot/modules/pending-rewrite/stats/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# flake8: noqa
|
||||
from .module import module
|
||||
|
||||
from . import data
|
||||
from . import profile
|
||||
from . import setprofile
|
||||
from . import top_cmd
|
||||
from . import goals
|
||||
from . import achievements
|
||||
485
bot/modules/pending-rewrite/stats/achievements.py
Normal file
485
bot/modules/pending-rewrite/stats/achievements.py
Normal file
@@ -0,0 +1,485 @@
|
||||
from typing import NamedTuple, Optional, Union
|
||||
from datetime import timedelta
|
||||
|
||||
import pytz
|
||||
import discord
|
||||
|
||||
from cmdClient.checks import in_guild
|
||||
from LionContext import LionContext
|
||||
|
||||
from meta import client, conf
|
||||
from core import Lion
|
||||
from data.conditions import NOTNULL, LEQ
|
||||
from utils.lib import utc_now
|
||||
|
||||
from modules.topgg.utils import topgg_upvote_link
|
||||
|
||||
from .module import module
|
||||
|
||||
|
||||
class AchievementLevel(NamedTuple):
|
||||
name: str
|
||||
threshold: Union[int, float]
|
||||
emoji: discord.PartialEmoji
|
||||
|
||||
|
||||
class Achievement:
|
||||
"""
|
||||
ABC for a member or user achievement.
|
||||
"""
|
||||
# Name of the achievement
|
||||
name: str = None
|
||||
|
||||
subtext: str = None
|
||||
congrats_text: str = "Congratulations, you completed this challenge!"
|
||||
|
||||
# List of levels for the achievement. Must always contain a 0 level!
|
||||
levels: list[AchievementLevel] = None
|
||||
|
||||
def __init__(self, guildid: int, userid: int):
|
||||
self.guildid = guildid
|
||||
self.userid = userid
|
||||
|
||||
# Current status of the achievement. None until calculated by `update`.
|
||||
self.value: int = None
|
||||
|
||||
# Current level index in levels. None until calculated by `update`.
|
||||
self.level_id: int = None
|
||||
|
||||
@staticmethod
|
||||
def progress_bar(value, minimum, maximum, width=10) -> str:
|
||||
"""
|
||||
Build a text progress bar representing `value` between `minimum` and `maximum`.
|
||||
"""
|
||||
emojis = conf.emojis
|
||||
|
||||
proportion = (value - minimum) / (maximum - minimum)
|
||||
sections = min(max(int(proportion * width), 0), width)
|
||||
|
||||
bar = []
|
||||
# Starting segment
|
||||
bar.append(str(emojis.progress_left_empty) if sections == 0 else str(emojis.progress_left_full))
|
||||
|
||||
# Full segments up to transition or end
|
||||
if sections >= 2:
|
||||
bar.append(str(emojis.progress_middle_full) * (sections - 2))
|
||||
|
||||
# Transition, if required
|
||||
if 1 < sections < width:
|
||||
bar.append(str(emojis.progress_middle_transition))
|
||||
|
||||
# Empty sections up to end
|
||||
if sections < width:
|
||||
bar.append(str(emojis.progress_middle_empty) * (width - max(sections, 1) - 1))
|
||||
|
||||
# End section
|
||||
bar.append(str(emojis.progress_right_empty) if sections < width else str(emojis.progress_right_full))
|
||||
|
||||
# Join all the sections together and return
|
||||
return ''.join(bar)
|
||||
|
||||
@property
|
||||
def progress_text(self) -> str:
|
||||
"""
|
||||
A brief textual description of the current progress.
|
||||
Intended to be overridden by achievement implementations.
|
||||
"""
|
||||
return f"{int(self.value)}/{self.next_level.threshold if self.next_level else self.level.threshold}"
|
||||
|
||||
def progress_field(self) -> tuple[str, str]:
|
||||
"""
|
||||
Builds the progress field for the achievement display.
|
||||
"""
|
||||
# TODO: Not adjusted for levels
|
||||
# TODO: Add hint if progress is empty?
|
||||
name = f"{self.levels[1].emoji} {self.name} ({self.progress_text})"
|
||||
value = "**0** {progress_bar} **{threshold}**\n*{subtext}*".format(
|
||||
subtext=(self.subtext if self.next_level else self.congrats_text) or '',
|
||||
progress_bar=self.progress_bar(self.value, self.levels[0].threshold, self.levels[1].threshold),
|
||||
threshold=self.levels[1].threshold
|
||||
)
|
||||
return (name, value)
|
||||
|
||||
@classmethod
|
||||
async def fetch(cls, guildid: int, userid: int) -> 'Achievement':
|
||||
"""
|
||||
Fetch an Achievement status for the given member.
|
||||
"""
|
||||
return await cls(guildid, userid).update()
|
||||
|
||||
@property
|
||||
def level(self) -> AchievementLevel:
|
||||
"""
|
||||
The current `AchievementLevel` for this member achievement.
|
||||
"""
|
||||
if self.level_id is None:
|
||||
raise ValueError("Cannot obtain level before first update!")
|
||||
return self.levels[self.level_id]
|
||||
|
||||
@property
|
||||
def next_level(self) -> Optional[AchievementLevel]:
|
||||
"""
|
||||
The next `AchievementLevel` for this member achievement,
|
||||
or `None` if it is at the maximum level.
|
||||
"""
|
||||
if self.level_id is None:
|
||||
raise ValueError("Cannot obtain level before first update!")
|
||||
|
||||
if self.level_id == len(self.levels) - 1:
|
||||
return None
|
||||
else:
|
||||
return self.levels[self.level_id + 1]
|
||||
|
||||
async def update(self) -> 'Achievement':
|
||||
"""
|
||||
Calculate and store the current member achievement status.
|
||||
Returns `self` for easy chaining.
|
||||
"""
|
||||
# First fetch the value
|
||||
self.value = await self._calculate_value()
|
||||
|
||||
# Then determine the current level
|
||||
# Using 0 as a fallback in case the value is negative
|
||||
self.level_id = next(
|
||||
(i for i, level in reversed(list(enumerate(self.levels))) if level.threshold <= self.value),
|
||||
0
|
||||
)
|
||||
|
||||
# And return `self` for chaining
|
||||
return self
|
||||
|
||||
async def _calculate_value(self) -> Union[int, float]:
|
||||
"""
|
||||
Calculate the current `value` of the member achievement.
|
||||
Must be overridden by Achievement implementations.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Workout(Achievement):
|
||||
sorting_index = 8
|
||||
emoji_index = 4
|
||||
name = "It's about Power"
|
||||
subtext = "Workout 50 times"
|
||||
|
||||
levels = [
|
||||
AchievementLevel("Level 0", 0, None),
|
||||
AchievementLevel("Level 1", 50, conf.emojis.active_achievement_4),
|
||||
]
|
||||
|
||||
async def _calculate_value(self) -> int:
|
||||
"""
|
||||
Returns the total number of workouts from this user.
|
||||
"""
|
||||
return client.data.workout_sessions.select_one_where(
|
||||
userid=self.userid,
|
||||
select_columns="COUNT(*)"
|
||||
)[0]
|
||||
|
||||
|
||||
class StudyHours(Achievement):
|
||||
sorting_index = 1
|
||||
emoji_index = 1
|
||||
name = "Dream Big"
|
||||
subtext = "Study a total of 1000 hours"
|
||||
|
||||
levels = [
|
||||
AchievementLevel("Level 0", 0, None),
|
||||
AchievementLevel("Level 1", 1000, conf.emojis.active_achievement_1),
|
||||
]
|
||||
|
||||
async def _calculate_value(self) -> float:
|
||||
"""
|
||||
Returns the total number of hours this user has studied.
|
||||
"""
|
||||
past_session_total = client.data.session_history.select_one_where(
|
||||
userid=self.userid,
|
||||
select_columns="SUM(duration)"
|
||||
)[0] or 0
|
||||
current_session_total = client.data.current_sessions.select_one_where(
|
||||
userid=self.userid,
|
||||
select_columns="SUM(EXTRACT(EPOCH FROM (NOW() - start_time)))"
|
||||
)[0] or 0
|
||||
|
||||
session_total = past_session_total + current_session_total
|
||||
hours = session_total / 3600
|
||||
return hours
|
||||
|
||||
|
||||
class StudyStreak(Achievement):
|
||||
sorting_index = 2
|
||||
emoji_index = 2
|
||||
name = "Consistency is Key"
|
||||
subtext = "Reach a 100-day study streak"
|
||||
|
||||
levels = [
|
||||
AchievementLevel("Level 0", 0, None),
|
||||
AchievementLevel("Level 1", 100, conf.emojis.active_achievement_2)
|
||||
]
|
||||
|
||||
async def _calculate_value(self) -> int:
|
||||
"""
|
||||
Return the user's maximum global study streak.
|
||||
"""
|
||||
lion = Lion.fetch(self.guildid, self.userid)
|
||||
history = client.data.session_history.select_where(
|
||||
userid=self.userid,
|
||||
select_columns=(
|
||||
"start_time",
|
||||
"(start_time + duration * interval '1 second') AS end_time"
|
||||
),
|
||||
_extra="ORDER BY start_time DESC"
|
||||
)
|
||||
|
||||
# Streak statistics
|
||||
streak = 0
|
||||
max_streak = 0
|
||||
|
||||
day_attended = True if 'sessions' in client.objects and lion.session else None
|
||||
date = lion.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)
|
||||
streak = 0
|
||||
|
||||
# Handle loop exit state, i.e. the last streak
|
||||
if day_attended:
|
||||
streak += 1
|
||||
max_streak = max(max_streak, streak)
|
||||
|
||||
return max_streak
|
||||
|
||||
|
||||
class Voting(Achievement):
|
||||
sorting_index = 7
|
||||
emoji_index = 7
|
||||
name = "We're a Team"
|
||||
subtext = "[Vote]({}) 100 times on top.gg".format(topgg_upvote_link)
|
||||
|
||||
levels = [
|
||||
AchievementLevel("Level 0", 0, None),
|
||||
AchievementLevel("Level 1", 100, conf.emojis.active_achievement_7)
|
||||
]
|
||||
|
||||
async def _calculate_value(self) -> int:
|
||||
"""
|
||||
Returns the number of times the user has voted for the bot.
|
||||
"""
|
||||
return client.data.topgg.select_one_where(
|
||||
userid=self.userid,
|
||||
select_columns="COUNT(*)"
|
||||
)[0]
|
||||
|
||||
|
||||
class DaysStudying(Achievement):
|
||||
sorting_index = 3
|
||||
emoji_index = 3
|
||||
name = "Aim For The Moon"
|
||||
subtext = "Study on 90 different days"
|
||||
|
||||
levels = [
|
||||
AchievementLevel("Level 0", 0, None),
|
||||
AchievementLevel("Level 1", 90, conf.emojis.active_achievement_3)
|
||||
]
|
||||
|
||||
async def _calculate_value(self) -> int:
|
||||
"""
|
||||
Returns the number of days the user has studied in total.
|
||||
"""
|
||||
lion = Lion.fetch(self.guildid, self.userid)
|
||||
offset = int(lion.day_start.utcoffset().total_seconds())
|
||||
with client.data.session_history.conn as conn:
|
||||
cursor = conn.cursor()
|
||||
# TODO: Consider DST offset.
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT
|
||||
COUNT(DISTINCT(date_trunc('day', (time AT TIME ZONE 'utc') + interval '{} seconds')))
|
||||
FROM (
|
||||
(SELECT start_time AS time FROM session_history WHERE userid=%s)
|
||||
UNION
|
||||
(SELECT (start_time + duration * interval '1 second') AS time FROM session_history WHERE userid=%s)
|
||||
) AS times;
|
||||
""".format(offset),
|
||||
(self.userid, self.userid)
|
||||
)
|
||||
data = cursor.fetchone()
|
||||
return data[0]
|
||||
|
||||
|
||||
class TasksComplete(Achievement):
|
||||
sorting_index = 4
|
||||
emoji_index = 8
|
||||
name = "One Step at a Time"
|
||||
subtext = "Complete 1000 tasks"
|
||||
|
||||
levels = [
|
||||
AchievementLevel("Level 0", 0, None),
|
||||
AchievementLevel("Level 1", 1000, conf.emojis.active_achievement_8)
|
||||
]
|
||||
|
||||
async def _calculate_value(self) -> int:
|
||||
"""
|
||||
Returns the number of tasks the user has completed.
|
||||
"""
|
||||
return client.data.tasklist.select_one_where(
|
||||
userid=self.userid,
|
||||
completed_at=NOTNULL,
|
||||
select_columns="COUNT(*)"
|
||||
)[0]
|
||||
|
||||
|
||||
class ScheduledSessions(Achievement):
|
||||
sorting_index = 5
|
||||
emoji_index = 5
|
||||
name = "Be Accountable"
|
||||
subtext = "Attend 500 scheduled sessions"
|
||||
|
||||
levels = [
|
||||
AchievementLevel("Level 0", 0, None),
|
||||
AchievementLevel("Level 1", 500, conf.emojis.active_achievement_5)
|
||||
]
|
||||
|
||||
async def _calculate_value(self) -> int:
|
||||
"""
|
||||
Returns the number of scheduled sesions the user has attended.
|
||||
"""
|
||||
return client.data.accountability_member_info.select_one_where(
|
||||
userid=self.userid,
|
||||
start_at=LEQ(utc_now()),
|
||||
select_columns="COUNT(*)",
|
||||
_extra="AND (duration > 0 OR last_joined_at IS NOT NULL)"
|
||||
)[0]
|
||||
|
||||
|
||||
class MonthlyHours(Achievement):
|
||||
sorting_index = 6
|
||||
emoji_index = 6
|
||||
name = "The 30 Days Challenge"
|
||||
subtext = "Study 100 hours in 30 days"
|
||||
|
||||
levels = [
|
||||
AchievementLevel("Level 0", 0, None),
|
||||
AchievementLevel("Level 1", 100, conf.emojis.active_achievement_6)
|
||||
]
|
||||
|
||||
async def _calculate_value(self) -> float:
|
||||
"""
|
||||
Returns the maximum number of hours the user has studied in a month.
|
||||
"""
|
||||
# Get the first session so we know how far back to look
|
||||
first_session = client.data.session_history.select_one_where(
|
||||
userid=self.userid,
|
||||
select_columns="MIN(start_time)"
|
||||
)[0]
|
||||
|
||||
# Get the user's timezone
|
||||
lion = Lion.fetch(self.guildid, self.userid)
|
||||
|
||||
# If the first session doesn't exist, simulate an existing session (to avoid an extra lookup)
|
||||
first_session = first_session or lion.day_start - timedelta(days=1)
|
||||
|
||||
# Build the list of month start timestamps
|
||||
month_start = lion.day_start.replace(day=1)
|
||||
months = [month_start.astimezone(pytz.utc)]
|
||||
|
||||
while month_start >= first_session:
|
||||
month_start -= timedelta(days=1)
|
||||
month_start = month_start.replace(day=1)
|
||||
months.append(month_start.astimezone(pytz.utc))
|
||||
|
||||
# Query the study times
|
||||
data = client.data.session_history.queries.study_times_since(
|
||||
self.guildid, self.userid, *months
|
||||
)
|
||||
cumulative_times = [row[0] or 0 for row in data]
|
||||
times = [nxt - crt for nxt, crt in zip(cumulative_times[1:], cumulative_times[0:])]
|
||||
max_time = max(cumulative_times[0], *times) if len(months) > 1 else cumulative_times[0]
|
||||
|
||||
return max_time / 3600
|
||||
|
||||
|
||||
# Define the displayed achivement order
|
||||
achievements = [
|
||||
Workout,
|
||||
StudyHours,
|
||||
StudyStreak,
|
||||
Voting,
|
||||
DaysStudying,
|
||||
TasksComplete,
|
||||
ScheduledSessions,
|
||||
MonthlyHours
|
||||
]
|
||||
|
||||
|
||||
async def get_achievements_for(member, panel_sort=False):
|
||||
status = [
|
||||
await ach.fetch(member.guild.id, member.id)
|
||||
for ach in sorted(achievements, key=lambda cls: (cls.sorting_index if panel_sort else cls.emoji_index))
|
||||
]
|
||||
return status
|
||||
|
||||
|
||||
@module.cmd(
|
||||
name="achievements",
|
||||
desc="View your progress towards the achievements!",
|
||||
group="Statistics",
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_achievements(ctx: LionContext):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}achievements
|
||||
Description:
|
||||
View your progress towards attaining the achievement badges shown on your `profile`.
|
||||
"""
|
||||
status = await get_achievements_for(ctx.author, panel_sort=True)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Achievements",
|
||||
colour=discord.Colour.orange()
|
||||
)
|
||||
for achievement in status:
|
||||
name, value = achievement.progress_field()
|
||||
embed.add_field(
|
||||
name=name, value=value, inline=False
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
39
bot/modules/pending-rewrite/stats/data.py
Normal file
39
bot/modules/pending-rewrite/stats/data.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from cachetools import TTLCache
|
||||
|
||||
from data import Table, RowTable
|
||||
|
||||
|
||||
profile_tags = Table('member_profile_tags', attach_as='profile_tags')
|
||||
|
||||
|
||||
@profile_tags.save_query
|
||||
def get_tags_for(guildid, userid):
|
||||
rows = profile_tags.select_where(
|
||||
guildid=guildid, userid=userid,
|
||||
_extra="ORDER BY tagid ASC"
|
||||
)
|
||||
return [row['tag'] for row in rows]
|
||||
|
||||
|
||||
weekly_goals = RowTable(
|
||||
'member_weekly_goals',
|
||||
('guildid', 'userid', 'weekid', 'study_goal', 'task_goal'),
|
||||
('guildid', 'userid', 'weekid'),
|
||||
cache=TTLCache(5000, 60 * 60 * 24),
|
||||
attach_as='weekly_goals'
|
||||
)
|
||||
|
||||
|
||||
# NOTE: Not using a RowTable here since these will almost always be mass-selected
|
||||
weekly_tasks = Table('member_weekly_goal_tasks')
|
||||
|
||||
|
||||
monthly_goals = RowTable(
|
||||
'member_monthly_goals',
|
||||
('guildid', 'userid', 'monthid', 'study_goal', 'task_goal'),
|
||||
('guildid', 'userid', 'monthid'),
|
||||
cache=TTLCache(5000, 60 * 60 * 24),
|
||||
attach_as='monthly_goals'
|
||||
)
|
||||
|
||||
monthly_tasks = Table('member_monthly_goal_tasks')
|
||||
332
bot/modules/pending-rewrite/stats/goals.py
Normal file
332
bot/modules/pending-rewrite/stats/goals.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Weekly and Monthly goal display and edit interface.
|
||||
"""
|
||||
from enum import Enum
|
||||
import discord
|
||||
|
||||
from cmdClient.checks import in_guild
|
||||
from cmdClient.lib import SafeCancellation
|
||||
|
||||
from utils.lib import parse_ranges
|
||||
|
||||
from .module import module
|
||||
from .data import weekly_goals, weekly_tasks, monthly_goals, monthly_tasks
|
||||
|
||||
|
||||
MAX_LENGTH = 200
|
||||
MAX_TASKS = 10
|
||||
|
||||
|
||||
class GoalType(Enum):
|
||||
WEEKLY = 0
|
||||
MONTHLY = 1
|
||||
|
||||
|
||||
def index_range_parser(userstr, max):
|
||||
try:
|
||||
indexes = parse_ranges(userstr)
|
||||
except SafeCancellation:
|
||||
raise SafeCancellation(
|
||||
"Couldn't parse the provided task ids! "
|
||||
"Please list the task numbers or ranges separated by a comma, e.g. `0, 2-4`."
|
||||
) from None
|
||||
|
||||
return [index for index in indexes if index <= max]
|
||||
|
||||
|
||||
@module.cmd(
|
||||
"weeklygoals",
|
||||
group="Statistics",
|
||||
desc="Set your weekly goals and view your progress!",
|
||||
aliases=('weeklygoal',),
|
||||
flags=('study=', 'tasks=')
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_weeklygoals(ctx, flags):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}weeklygoals [--study <hours>] [--tasks <number>]
|
||||
{prefix}weeklygoals add <task>
|
||||
{prefix}weeklygoals edit <taskid> <new task>
|
||||
{prefix}weeklygoals check <taskids>
|
||||
{prefix}weeklygoals remove <taskids>
|
||||
Description:
|
||||
Set yourself up to `10` goals for this week and keep yourself accountable!
|
||||
Use `add/edit/check/remove` to edit your goals, similarly to `{prefix}todo`.
|
||||
You can also add multiple tasks at once by writing them on multiple lines.
|
||||
|
||||
You can also track your progress towards a number of hours studied with `--study`, \
|
||||
and aim for a number of tasks completed with `--tasks`.
|
||||
|
||||
Run the command with no arguments or check your profile to see your progress!
|
||||
Examples``:
|
||||
{prefix}weeklygoals add Read chapters 1 to 10.
|
||||
{prefix}weeklygoals check 1
|
||||
{prefix}weeklygoals --study 48h --tasks 60
|
||||
"""
|
||||
await goals_command(ctx, flags, GoalType.WEEKLY)
|
||||
|
||||
|
||||
@module.cmd(
|
||||
"monthlygoals",
|
||||
group="Statistics",
|
||||
desc="Set your monthly goals and view your progress!",
|
||||
aliases=('monthlygoal',),
|
||||
flags=('study=', 'tasks=')
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_monthlygoals(ctx, flags):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}monthlygoals [--study <hours>] [--tasks <number>]
|
||||
{prefix}monthlygoals add <task>
|
||||
{prefix}monthlygoals edit <taskid> <new task>
|
||||
{prefix}monthlygoals check <taskids>
|
||||
{prefix}monthlygoals uncheck <taskids>
|
||||
{prefix}monthlygoals remove <taskids>
|
||||
Description:
|
||||
Set yourself up to `10` goals for this month and keep yourself accountable!
|
||||
Use `add/edit/check/remove` to edit your goals, similarly to `{prefix}todo`.
|
||||
You can also add multiple tasks at once by writing them on multiple lines.
|
||||
|
||||
You can also track your progress towards a number of hours studied with `--study`, \
|
||||
and aim for a number of tasks completed with `--tasks`.
|
||||
|
||||
Run the command with no arguments or check your profile to see your progress!
|
||||
Examples``:
|
||||
{prefix}monthlygoals add Read chapters 1 to 10.
|
||||
{prefix}monthlygoals check 1
|
||||
{prefix}monthlygoals --study 180h --tasks 60
|
||||
"""
|
||||
await goals_command(ctx, flags, GoalType.MONTHLY)
|
||||
|
||||
|
||||
async def goals_command(ctx, flags, goal_type):
|
||||
prefix = ctx.best_prefix
|
||||
if goal_type == GoalType.WEEKLY:
|
||||
name = 'week'
|
||||
goal_table = weekly_goals
|
||||
task_table = weekly_tasks
|
||||
rowkey = 'weekid'
|
||||
rowid = ctx.alion.week_timestamp
|
||||
|
||||
tasklist = task_table.select_where(
|
||||
guildid=ctx.guild.id,
|
||||
userid=ctx.author.id,
|
||||
weekid=rowid,
|
||||
_extra="ORDER BY taskid ASC"
|
||||
)
|
||||
|
||||
max_time = 7 * 16
|
||||
else:
|
||||
name = 'month'
|
||||
goal_table = monthly_goals
|
||||
task_table = monthly_tasks
|
||||
rowid = ctx.alion.month_timestamp
|
||||
rowkey = 'monthid'
|
||||
|
||||
tasklist = task_table.select_where(
|
||||
guildid=ctx.guild.id,
|
||||
userid=ctx.author.id,
|
||||
monthid=rowid,
|
||||
_extra="ORDER BY taskid ASC"
|
||||
)
|
||||
|
||||
max_time = 31 * 16
|
||||
|
||||
# We ensured the `lion` existed with `ctx.alion` above
|
||||
# This also ensures a new tasklist can reference the period member goal key
|
||||
# TODO: Should creation copy the previous existing week?
|
||||
goal_row = goal_table.fetch_or_create((ctx.guild.id, ctx.author.id, rowid))
|
||||
|
||||
if flags['study']:
|
||||
# Set study hour goal
|
||||
time = flags['study'].lower().strip('h ')
|
||||
if not time or not time.isdigit():
|
||||
return await ctx.error_reply(
|
||||
f"Please provide your {name}ly study goal in hours!\n"
|
||||
f"For example, `{prefix}{ctx.alias} --study 48h`"
|
||||
)
|
||||
hours = int(time)
|
||||
if hours > max_time:
|
||||
return await ctx.error_reply(
|
||||
"You can't set your goal this high! Please rest and keep a healthy lifestyle."
|
||||
)
|
||||
|
||||
goal_row.study_goal = hours
|
||||
|
||||
if flags['tasks']:
|
||||
# Set tasks completed goal
|
||||
count = flags['tasks']
|
||||
if not count or not count.isdigit():
|
||||
return await ctx.error_reply(
|
||||
f"Please provide the number of tasks you want to complete this {name}!\n"
|
||||
f"For example, `{prefix}{ctx.alias} --tasks 300`"
|
||||
)
|
||||
if int(count) > 2048:
|
||||
return await ctx.error_reply(
|
||||
"Your task goal is too high!"
|
||||
)
|
||||
goal_row.task_goal = int(count)
|
||||
|
||||
if ctx.args:
|
||||
# If there are arguments, assume task/goal management
|
||||
# Extract the command if it exists, assume add operation if it doesn't
|
||||
splits = ctx.args.split(maxsplit=1)
|
||||
cmd = splits[0].lower().strip()
|
||||
args = splits[1].strip() if len(splits) > 1 else ''
|
||||
|
||||
if cmd in ('check', 'done', 'complete'):
|
||||
if not args:
|
||||
# Show subcommand usage
|
||||
return await ctx.error_reply(
|
||||
f"**Usage:**`{prefix}{ctx.alias} check <taskids>`\n"
|
||||
f"**Example:**`{prefix}{ctx.alias} check 0, 2-4`"
|
||||
)
|
||||
if (indexes := index_range_parser(args, len(tasklist) - 1)):
|
||||
# Check the given indexes
|
||||
# If there are no valid indexes given, just do nothing and fall out to showing the goals
|
||||
task_table.update_where(
|
||||
{'completed': True},
|
||||
taskid=[tasklist[index]['taskid'] for index in indexes]
|
||||
)
|
||||
elif cmd in ('uncheck', 'undone', 'uncomplete'):
|
||||
if not args:
|
||||
# Show subcommand usage
|
||||
return await ctx.error_reply(
|
||||
f"**Usage:**`{prefix}{ctx.alias} uncheck <taskids>`\n"
|
||||
f"**Example:**`{prefix}{ctx.alias} uncheck 0, 2-4`"
|
||||
)
|
||||
if (indexes := index_range_parser(args, len(tasklist) - 1)):
|
||||
# Check the given indexes
|
||||
# If there are no valid indexes given, just do nothing and fall out to showing the goals
|
||||
task_table.update_where(
|
||||
{'completed': False},
|
||||
taskid=[tasklist[index]['taskid'] for index in indexes]
|
||||
)
|
||||
elif cmd in ('remove', 'delete', '-', 'rm'):
|
||||
if not args:
|
||||
# Show subcommand usage
|
||||
return await ctx.error_reply(
|
||||
f"**Usage:**`{prefix}{ctx.alias} remove <taskids>`\n"
|
||||
f"**Example:**`{prefix}{ctx.alias} remove 0, 2-4`"
|
||||
)
|
||||
if (indexes := index_range_parser(args, len(tasklist) - 1)):
|
||||
# Delete the given indexes
|
||||
# If there are no valid indexes given, just do nothing and fall out to showing the goals
|
||||
task_table.delete_where(
|
||||
taskid=[tasklist[index]['taskid'] for index in indexes]
|
||||
)
|
||||
elif cmd == 'edit':
|
||||
if not args or len(splits := args.split(maxsplit=1)) < 2 or not splits[0].isdigit():
|
||||
# Show subcommand usage
|
||||
return await ctx.error_reply(
|
||||
f"**Usage:**`{prefix}{ctx.alias} edit <taskid> <edited task>`\n"
|
||||
f"**Example:**`{prefix}{ctx.alias} edit 2 Fix the scond task`"
|
||||
)
|
||||
index = int(splits[0])
|
||||
new_content = splits[1].strip()
|
||||
|
||||
if index >= len(tasklist):
|
||||
return await ctx.error_reply(
|
||||
f"Task `{index}` doesn't exist to edit!"
|
||||
)
|
||||
|
||||
if len(new_content) > MAX_LENGTH:
|
||||
return await ctx.error_reply(
|
||||
f"Please keep your goals under `{MAX_LENGTH}` characters long."
|
||||
)
|
||||
|
||||
# Passed all checks, edit task
|
||||
task_table.update_where(
|
||||
{'content': new_content},
|
||||
taskid=tasklist[index]['taskid']
|
||||
)
|
||||
else:
|
||||
# Extract the tasks to add
|
||||
if cmd in ('add', '+'):
|
||||
if not args:
|
||||
# Show subcommand usage
|
||||
return await ctx.error_reply(
|
||||
f"**Usage:**`{prefix}{ctx.alias} [add] <new task>`\n"
|
||||
f"**Example:**`{prefix}{ctx.alias} add Read the Studylion help pages.`"
|
||||
)
|
||||
else:
|
||||
args = ctx.args
|
||||
tasks = args.splitlines()
|
||||
|
||||
# Check count
|
||||
if len(tasklist) + len(tasks) > MAX_TASKS:
|
||||
return await ctx.error_reply(
|
||||
f"You can have at most **{MAX_TASKS}** {name}ly goals!"
|
||||
)
|
||||
|
||||
# Check length
|
||||
if any(len(task) > MAX_LENGTH for task in tasks):
|
||||
return await ctx.error_reply(
|
||||
f"Please keep your goals under `{MAX_LENGTH}` characters long."
|
||||
)
|
||||
|
||||
# We passed the checks, add the tasks
|
||||
to_insert = [
|
||||
(ctx.guild.id, ctx.author.id, rowid, task)
|
||||
for task in tasks
|
||||
]
|
||||
task_table.insert_many(
|
||||
*to_insert,
|
||||
insert_keys=('guildid', 'userid', rowkey, 'content')
|
||||
)
|
||||
elif not any((goal_row.study_goal, goal_row.task_goal, tasklist)):
|
||||
# The user hasn't set any goals for this time period
|
||||
# Prompt them with information about how to set a goal
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.orange(),
|
||||
title=f"**You haven't set any goals for this {name} yet! Try the following:**\n"
|
||||
)
|
||||
embed.add_field(
|
||||
name="Aim for a number of study hours with",
|
||||
value=f"`{prefix}{ctx.alias} --study 48h`"
|
||||
)
|
||||
embed.add_field(
|
||||
name="Aim for a number of tasks completed with",
|
||||
value=f"`{prefix}{ctx.alias} --tasks 300`",
|
||||
inline=False
|
||||
)
|
||||
embed.add_field(
|
||||
name=f"Set up to 10 custom goals for the {name}!",
|
||||
value=(
|
||||
f"`{prefix}{ctx.alias} add Write a 200 page thesis.`\n"
|
||||
f"`{prefix}{ctx.alias} edit 1 Write 2 pages of the 200 page thesis.`\n"
|
||||
f"`{prefix}{ctx.alias} done 0, 1, 3-4`\n"
|
||||
f"`{prefix}{ctx.alias} delete 2-4`"
|
||||
),
|
||||
inline=False
|
||||
)
|
||||
return await ctx.reply(embed=embed)
|
||||
|
||||
# Show the goals
|
||||
if goal_type == GoalType.WEEKLY:
|
||||
await display_weekly_goals_for(ctx)
|
||||
else:
|
||||
await display_monthly_goals_for(ctx)
|
||||
|
||||
|
||||
async def display_weekly_goals_for(ctx):
|
||||
"""
|
||||
Display the user's weekly goal summary and progress towards them
|
||||
TODO: Currently a stub, since the system is overidden by the GUI plugin
|
||||
"""
|
||||
# Collect data
|
||||
lion = ctx.alion
|
||||
rowid = lion.week_timestamp
|
||||
goals = weekly_goals.fetch_or_create((ctx.guild.id, ctx.author.id, rowid))
|
||||
tasklist = weekly_tasks.select_where(
|
||||
guildid=ctx.guild.id,
|
||||
userid=ctx.author.id,
|
||||
weekid=rowid
|
||||
)
|
||||
...
|
||||
|
||||
|
||||
async def display_monthly_goals_for(ctx):
|
||||
...
|
||||
4
bot/modules/pending-rewrite/stats/module.py
Normal file
4
bot/modules/pending-rewrite/stats/module.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from LionModule import LionModule
|
||||
|
||||
|
||||
module = LionModule("Statistics")
|
||||
266
bot/modules/pending-rewrite/stats/profile.py
Normal file
266
bot/modules/pending-rewrite/stats/profile.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from datetime import datetime, timedelta
|
||||
import discord
|
||||
from cmdClient.checks import in_guild
|
||||
|
||||
from utils.lib import prop_tabulate, utc_now
|
||||
from data import tables
|
||||
from data.conditions import LEQ
|
||||
from core import Lion
|
||||
|
||||
from modules.study.tracking.data import session_history
|
||||
|
||||
from .module import module
|
||||
|
||||
|
||||
@module.cmd(
|
||||
"stats",
|
||||
group="Statistics",
|
||||
desc="View your personal server study statistics!",
|
||||
aliases=('profile',),
|
||||
allow_before_ready=True
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_stats(ctx):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}stats
|
||||
{prefix}stats <user mention>
|
||||
Description:
|
||||
View the study statistics for yourself or the mentioned user.
|
||||
"""
|
||||
# Identify the target
|
||||
if ctx.args:
|
||||
if not ctx.msg.mentions:
|
||||
return await ctx.error_reply("Please mention a user to view their statistics!")
|
||||
target = ctx.msg.mentions[0]
|
||||
else:
|
||||
target = ctx.author
|
||||
|
||||
# System sync
|
||||
Lion.sync()
|
||||
|
||||
# Fetch the required data
|
||||
lion = Lion.fetch(ctx.guild.id, target.id)
|
||||
|
||||
history = session_history.select_where(
|
||||
guildid=ctx.guild.id,
|
||||
userid=target.id,
|
||||
select_columns=(
|
||||
"start_time",
|
||||
"(start_time + duration * interval '1 second') AS end_time"
|
||||
),
|
||||
_extra="ORDER BY start_time DESC"
|
||||
)
|
||||
|
||||
# Current economy balance (accounting for current session)
|
||||
coins = lion.coins
|
||||
season_time = lion.time
|
||||
workout_total = lion.data.workout_count
|
||||
|
||||
# Leaderboard ranks
|
||||
exclude = set(m.id for m in ctx.guild_settings.unranked_roles.members)
|
||||
exclude.update(ctx.client.user_blacklist())
|
||||
exclude.update(ctx.client.objects['ignored_members'][ctx.guild.id])
|
||||
if target.id in exclude:
|
||||
time_rank = None
|
||||
coin_rank = None
|
||||
else:
|
||||
time_rank, coin_rank = tables.lions.queries.get_member_rank(ctx.guild.id, target.id, list(exclude or [0]))
|
||||
|
||||
# Study time
|
||||
# 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
|
||||
|
||||
# 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(
|
||||
colour=discord.Colour.orange(),
|
||||
title="Study Profile for {}".format(str(target))
|
||||
)
|
||||
embed.set_thumbnail(url=target.avatar_url)
|
||||
|
||||
# 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 = "**{}:{:02}**".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:
|
||||
rank_str = "{}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"
|
||||
)
|
||||
if workout_total:
|
||||
stats['Workouts'] = "**{}** sessions".format(workout_total)
|
||||
if acc_duration:
|
||||
stats['Accountability'] = "**{}** hours (`{:.0f}%` attended)".format(
|
||||
acc_duration // 3600,
|
||||
acc_rate
|
||||
)
|
||||
stats['Study Streak'] = "**{}** days{}".format(
|
||||
current_streak,
|
||||
" (longest **{}** days)".format(max_streak) if max_streak else ''
|
||||
)
|
||||
|
||||
stats_table = prop_tabulate(*zip(*stats.items()))
|
||||
|
||||
# Build the time table
|
||||
time_table = prop_tabulate(
|
||||
('Daily', 'Weekly', 'Monthly', 'All Time'),
|
||||
["{:02}:{:02}".format(t // 3600, (t // 60) % 60) for t in reversed(study_times)]
|
||||
)
|
||||
|
||||
# Populate the embed
|
||||
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 "No 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)
|
||||
227
bot/modules/pending-rewrite/stats/setprofile.py
Normal file
227
bot/modules/pending-rewrite/stats/setprofile.py
Normal file
@@ -0,0 +1,227 @@
|
||||
"""
|
||||
Provides a command to update a member's profile badges.
|
||||
"""
|
||||
import string
|
||||
import discord
|
||||
|
||||
from cmdClient.lib import SafeCancellation
|
||||
from cmdClient.checks import in_guild
|
||||
from wards import guild_moderator
|
||||
|
||||
from .data import profile_tags
|
||||
from .module import module
|
||||
|
||||
|
||||
MAX_TAGS = 10
|
||||
MAX_LENGTH = 30
|
||||
|
||||
|
||||
@module.cmd(
|
||||
"setprofile",
|
||||
group="Personal Settings",
|
||||
desc="Set or update your study profile tags.",
|
||||
aliases=('editprofile', 'mytags'),
|
||||
flags=('clear', 'for')
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_setprofile(ctx, flags):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}setprofile <tag>, <tag>, <tag>, ...
|
||||
{prefix}setprofile <id> <new tag>
|
||||
{prefix}setprofile --clear [--for @user]
|
||||
Description:
|
||||
Set or update the tags appearing in your study server profile.
|
||||
|
||||
Moderators can clear a user's tags with `--clear --for @user`.
|
||||
Examples``:
|
||||
{prefix}setprofile Mathematics, Bioloyg, Medicine, Undergraduate, Europe
|
||||
{prefix}setprofile 2 Biology
|
||||
{prefix}setprofile --clear
|
||||
"""
|
||||
if flags['clear']:
|
||||
if flags['for']:
|
||||
# Moderator-clearing a user's tags
|
||||
# First check moderator permissions
|
||||
if not await guild_moderator.run(ctx):
|
||||
return await ctx.error_reply(
|
||||
"You need to be a server moderator to use this!"
|
||||
)
|
||||
|
||||
# Check input and extract users to clear for
|
||||
if not (users := ctx.msg.mentions):
|
||||
# Show moderator usage
|
||||
return await ctx.error_reply(
|
||||
f"**Usage:** `{ctx.best_prefix}setprofile --clear --for @user`\n"
|
||||
f"**Example:** {ctx.best_prefix}setprofile --clear --for {ctx.author.mention}"
|
||||
)
|
||||
|
||||
# Clear the tags
|
||||
profile_tags.delete_where(
|
||||
guildid=ctx.guild.id,
|
||||
userid=[user.id for user in users]
|
||||
)
|
||||
|
||||
# Ack the moderator
|
||||
await ctx.embed_reply(
|
||||
"Profile tags cleared!"
|
||||
)
|
||||
else:
|
||||
# The author wants to clear their own tags
|
||||
|
||||
# First delete the tags, save the rows for reporting
|
||||
rows = profile_tags.delete_where(
|
||||
guildid=ctx.guild.id,
|
||||
userid=ctx.author.id
|
||||
)
|
||||
|
||||
# Ack the user
|
||||
if not rows:
|
||||
await ctx.embed_reply(
|
||||
"You don't have any profile tags to clear!"
|
||||
)
|
||||
else:
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.green(),
|
||||
description="Successfully cleared your profile!"
|
||||
)
|
||||
embed.add_field(
|
||||
name="Removed tags",
|
||||
value='\n'.join(row['tag'].upper() for row in rows)
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
elif ctx.args:
|
||||
if len(splits := ctx.args.split(maxsplit=1)) > 1 and splits[0].isdigit():
|
||||
# Assume we are editing the provided id
|
||||
tagid = int(splits[0])
|
||||
if tagid > MAX_TAGS:
|
||||
return await ctx.error_reply(
|
||||
f"Sorry, you can have a maximum of `{MAX_TAGS}` tags!"
|
||||
)
|
||||
if tagid == 0:
|
||||
return await ctx.error_reply("Tags start at `1`!")
|
||||
|
||||
# Retrieve the user's current taglist
|
||||
rows = profile_tags.select_where(
|
||||
guildid=ctx.guild.id,
|
||||
userid=ctx.author.id,
|
||||
_extra="ORDER BY tagid ASC"
|
||||
)
|
||||
|
||||
# Parse and validate provided new content
|
||||
content = splits[1].strip().upper()
|
||||
validate_tag(content)
|
||||
|
||||
if tagid > len(rows):
|
||||
# Trying to edit a tag that doesn't exist yet
|
||||
# Just create it instead
|
||||
profile_tags.insert(
|
||||
guildid=ctx.guild.id,
|
||||
userid=ctx.author.id,
|
||||
tag=content
|
||||
)
|
||||
|
||||
# Ack user
|
||||
await ctx.reply(
|
||||
embed=discord.Embed(title="Tag created!", colour=discord.Colour.green())
|
||||
)
|
||||
else:
|
||||
# Get the row id to update
|
||||
to_edit = rows[tagid - 1]['tagid']
|
||||
|
||||
# Update the tag
|
||||
profile_tags.update_where(
|
||||
{'tag': content},
|
||||
tagid=to_edit
|
||||
)
|
||||
|
||||
# Ack user
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.green(),
|
||||
title="Tag updated!"
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
else:
|
||||
# Assume the arguments are a comma separated list of badges
|
||||
# Parse and validate
|
||||
to_add = [split.strip().upper() for line in ctx.args.splitlines() for split in line.split(',')]
|
||||
to_add = [split.replace('<3', '❤️') for split in to_add if split]
|
||||
if not to_add:
|
||||
return await ctx.error_reply("No valid tags given, nothing to do!")
|
||||
|
||||
validate_tag(*to_add)
|
||||
|
||||
if len(to_add) > MAX_TAGS:
|
||||
return await ctx.error_reply(f"You can have a maximum of {MAX_TAGS} tags!")
|
||||
|
||||
# Remove the existing badges
|
||||
deleted_rows = profile_tags.delete_where(
|
||||
guildid=ctx.guild.id,
|
||||
userid=ctx.author.id
|
||||
)
|
||||
|
||||
# Insert the new tags
|
||||
profile_tags.insert_many(
|
||||
*((ctx.guild.id, ctx.author.id, tag) for tag in to_add),
|
||||
insert_keys=('guildid', 'userid', 'tag')
|
||||
)
|
||||
|
||||
# Ack with user
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.green(),
|
||||
title="Profile tags updated!"
|
||||
)
|
||||
embed.add_field(
|
||||
name="New tags",
|
||||
value='\n'.join(to_add)
|
||||
)
|
||||
if deleted_rows:
|
||||
embed.add_field(
|
||||
name="Replaced tags",
|
||||
value='\n'.join(row['tag'].upper() for row in deleted_rows),
|
||||
inline=False
|
||||
)
|
||||
if len(to_add) == 1:
|
||||
embed.set_footer(
|
||||
text=f"TIP: Add multiple tags with {ctx.best_prefix}setprofile tag1, tag2, ..."
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
else:
|
||||
# No input was provided
|
||||
# Show usage and exit
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.red(),
|
||||
description=(
|
||||
"Edit your study profile "
|
||||
"tags so other people can see what you do!"
|
||||
)
|
||||
)
|
||||
embed.add_field(
|
||||
name="Usage",
|
||||
value=(
|
||||
f"`{ctx.best_prefix}setprofile <tag>, <tag>, <tag>, ...`\n"
|
||||
f"`{ctx.best_prefix}setprofile <id> <new tag>`"
|
||||
)
|
||||
)
|
||||
embed.add_field(
|
||||
name="Examples",
|
||||
value=(
|
||||
f"`{ctx.best_prefix}setprofile Mathematics, Bioloyg, Medicine, Undergraduate, Europe`\n"
|
||||
f"`{ctx.best_prefix}setprofile 2 Biology`"
|
||||
),
|
||||
inline=False
|
||||
)
|
||||
await ctx.reply(embed=embed)
|
||||
|
||||
|
||||
def validate_tag(*content):
|
||||
for content in content:
|
||||
if not set(content.replace('❤️', '')).issubset(string.printable):
|
||||
raise SafeCancellation(
|
||||
f"Invalid tag `{content}`!\n"
|
||||
"Tags may only contain alphanumeric and punctuation characters."
|
||||
)
|
||||
if len(content) > MAX_LENGTH:
|
||||
raise SafeCancellation(
|
||||
f"Provided tag is too long! Please keep your tags shorter than {MAX_LENGTH} characters."
|
||||
)
|
||||
119
bot/modules/pending-rewrite/stats/top_cmd.py
Normal file
119
bot/modules/pending-rewrite/stats/top_cmd.py
Normal file
@@ -0,0 +1,119 @@
|
||||
from cmdClient.checks import in_guild
|
||||
|
||||
import data
|
||||
from core import Lion
|
||||
from data import tables
|
||||
from utils import interactive # noqa
|
||||
|
||||
from .module import module
|
||||
|
||||
|
||||
first_emoji = "🥇"
|
||||
second_emoji = "🥈"
|
||||
third_emoji = "🥉"
|
||||
|
||||
|
||||
@module.cmd(
|
||||
"top",
|
||||
desc="View the Study Time leaderboard.",
|
||||
group="Statistics",
|
||||
aliases=('ttop', 'toptime', 'top100'),
|
||||
help_aliases={'top100': "View the Study Time top 100."}
|
||||
)
|
||||
@in_guild()
|
||||
async def cmd_top(ctx):
|
||||
"""
|
||||
Usage``:
|
||||
{prefix}top
|
||||
{prefix}top100
|
||||
Description:
|
||||
Display the study time leaderboard, or the top 100.
|
||||
|
||||
Use the paging reactions or send `p<n>` to switch pages (e.g. `p11` to switch to page 11).
|
||||
"""
|
||||
# Handle args
|
||||
if ctx.args and not ctx.args == "100":
|
||||
return await ctx.error_reply(
|
||||
"**Usage:**`{prefix}top` or `{prefix}top100`.".format(prefix=ctx.best_prefix)
|
||||
)
|
||||
top100 = (ctx.args == "100" or ctx.alias == "top100")
|
||||
|
||||
# Fetch the leaderboard
|
||||
exclude = set(m.id for m in ctx.guild_settings.unranked_roles.members)
|
||||
exclude.update(ctx.client.user_blacklist())
|
||||
exclude.update(ctx.client.objects['ignored_members'][ctx.guild.id])
|
||||
|
||||
args = {
|
||||
'guildid': ctx.guild.id,
|
||||
'select_columns': ('userid', 'total_tracked_time::INTEGER'),
|
||||
'_extra': "AND total_tracked_time > 0 ORDER BY total_tracked_time DESC " + ("LIMIT 100" if top100 else "")
|
||||
}
|
||||
if exclude:
|
||||
args['userid'] = data.NOT(list(exclude))
|
||||
|
||||
user_data = tables.members_totals.select_where(**args)
|
||||
|
||||
# Quit early if the leaderboard is empty
|
||||
if not user_data:
|
||||
return await ctx.reply("No leaderboard entries yet!")
|
||||
|
||||
# Extract entries
|
||||
author_index = None
|
||||
entries = []
|
||||
for i, (userid, time) in enumerate(user_data):
|
||||
member = ctx.guild.get_member(userid)
|
||||
name = member.display_name if member else str(userid)
|
||||
name = name.replace('*', ' ').replace('_', ' ')
|
||||
|
||||
num_str = "{}.".format(i+1)
|
||||
|
||||
hours = time // 3600
|
||||
minutes = time // 60 % 60
|
||||
seconds = time % 60
|
||||
|
||||
time_str = "{}:{:02}:{:02}".format(
|
||||
hours,
|
||||
minutes,
|
||||
seconds
|
||||
)
|
||||
|
||||
if ctx.author.id == userid:
|
||||
author_index = i
|
||||
|
||||
entries.append((num_str, name, time_str))
|
||||
|
||||
# Extract blocks
|
||||
blocks = [entries[i:i+20] for i in range(0, len(entries), 20)]
|
||||
block_count = len(blocks)
|
||||
|
||||
# Build strings
|
||||
header = "Study Time Top 100" if top100 else "Study Time Leaderboard"
|
||||
if block_count > 1:
|
||||
header += " (Page {{page}}/{})".format(block_count)
|
||||
|
||||
# Build pages
|
||||
pages = []
|
||||
for i, block in enumerate(blocks):
|
||||
max_num_l, max_name_l, max_time_l = [max(len(e[i]) for e in block) for i in (0, 1, 2)]
|
||||
body = '\n'.join(
|
||||
"{:>{}} {:<{}} \t {:>{}} {} {}".format(
|
||||
entry[0], max_num_l,
|
||||
entry[1], max_name_l + 2,
|
||||
entry[2], max_time_l + 1,
|
||||
first_emoji if i == 0 and j == 0 else (
|
||||
second_emoji if i == 0 and j == 1 else (
|
||||
third_emoji if i == 0 and j == 2 else ''
|
||||
)
|
||||
),
|
||||
"⮜" if author_index is not None and author_index == i * 20 + j else ""
|
||||
)
|
||||
for j, entry in enumerate(block)
|
||||
)
|
||||
title = header.format(page=i+1)
|
||||
line = '='*len(title)
|
||||
pages.append(
|
||||
"```md\n{}\n{}\n{}```".format(title, line, body)
|
||||
)
|
||||
|
||||
# Finally, page the results
|
||||
await ctx.pager(pages, start_at=(author_index or 0)//20 if not top100 else 0)
|
||||
Reference in New Issue
Block a user