From d60a8772a3ff79a137ba939df59345f8aa68c173 Mon Sep 17 00:00:00 2001 From: Conatum Date: Thu, 30 Dec 2021 11:26:58 +0200 Subject: [PATCH] feature (stats): Weekly and monthly goals. Add a new editing interface for weekly and monthly goals. Textual viewing interface is currently a stub. Add `month_timestamp` and `week_timestamp` lion properties. --- bot/core/lion.py | 31 ++++++++++++++++++ bot/modules/stats/__init__.py | 1 + bot/modules/stats/data.py | 28 ++++++++++++++++- bot/modules/stats/setprofile.py | 8 +++-- data/migration/v6-v7/migration.sql | 50 ++++++++++++++++++++++++++++++ data/schema.sql | 48 ++++++++++++++++++++++++++++ 6 files changed, 163 insertions(+), 3 deletions(-) diff --git a/bot/core/lion.py b/bot/core/lion.py index c803cff8..4b3baea9 100644 --- a/bot/core/lion.py +++ b/bot/core/lion.py @@ -146,6 +146,37 @@ class Lion: now = datetime.now(tz=self.timezone) return now.replace(hour=0, minute=0, second=0, microsecond=0) + @property + def day_timestamp(self): + """ + EPOCH timestamp representing the current day for the user. + NOTE: This is the timestamp of the start of the current UTC day with the same date as the user's day. + This is *not* the start of the current user's day, either in UTC or their own timezone. + This may also not be the start of the current day in UTC (consider 23:00 for a user in UTC-2). + """ + now = datetime.now(tz=self.timezone) + day_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + return int(day_start.replace(tzinfo=pytz.utc).timestamp()) + + @property + def week_timestamp(self): + """ + EPOCH timestamp representing the current week for the user. + """ + now = datetime.now(tz=self.timezone) + day_start = now.replace(hour=0, minute=0, second=0, microsecond=0) + week_start = day_start - timedelta(days=day_start.weekday()) + return int(week_start.replace(tzinfo=pytz.utc).timestamp()) + + @property + def month_timestamp(self): + """ + EPOCH timestamp representing the current month for the user. + """ + now = datetime.now(tz=self.timezone) + month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + return int(month_start.replace(tzinfo=pytz.utc).timestamp()) + @property def remaining_in_day(self): return ((self.day_start + timedelta(days=1)) - datetime.now(self.timezone)).total_seconds() diff --git a/bot/modules/stats/__init__.py b/bot/modules/stats/__init__.py index 4478e63d..cf342274 100644 --- a/bot/modules/stats/__init__.py +++ b/bot/modules/stats/__init__.py @@ -4,3 +4,4 @@ from . import data from . import profile from . import setprofile from . import top_cmd +from . import goals diff --git a/bot/modules/stats/data.py b/bot/modules/stats/data.py index 79bcb7a9..234b226c 100644 --- a/bot/modules/stats/data.py +++ b/bot/modules/stats/data.py @@ -1,4 +1,6 @@ -from data import Table +from cachetools import TTLCache + +from data import Table, RowTable profile_tags = Table('member_profile_tags', attach_as='profile_tags') @@ -11,3 +13,27 @@ def get_tags_for(guildid, 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') diff --git a/bot/modules/stats/setprofile.py b/bot/modules/stats/setprofile.py index 8eeafffd..9618b388 100644 --- a/bot/modules/stats/setprofile.py +++ b/bot/modules/stats/setprofile.py @@ -167,7 +167,7 @@ async def cmd_setprofile(ctx, flags): # Ack with user embed = discord.Embed( colour=discord.Colour.green(), - description="Profile tags updated!" + title="Profile tags updated!" ) embed.add_field( name="New tags", @@ -175,10 +175,14 @@ async def cmd_setprofile(ctx, flags): ) if deleted_rows: embed.add_field( - name="Previous tags", + 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 diff --git a/data/migration/v6-v7/migration.sql b/data/migration/v6-v7/migration.sql index 3d4db031..905f4452 100644 --- a/data/migration/v6-v7/migration.sql +++ b/data/migration/v6-v7/migration.sql @@ -21,6 +21,56 @@ CREATE TABLE member_profile_tags( _timestamp TIMESTAMPTZ DEFAULT now(), FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) ); +CREATE INDEX member_profile_tags_members ON member_profile_tags (guildid, userid); +-- New member weekly and monthly goals +CREATE TABLE member_weekly_goals( + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + weekid INTEGER NOT NULL, -- Epoch time of the start of the UTC week + study_goal INTEGER, + task_goal INTEGER, + _timestamp TIMESTAMPTZ DEFAULT now(), + PRIMARY KEY (guildid, userid, weekid), + FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) ON DELETE CASCADE +); +CREATE INDEX member_weekly_goals_members ON member_weekly_goals (guildid, userid); + +CREATE TABLE member_weekly_goal_tasks( + taskid SERIAL PRIMARY KEY, + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + weekid INTEGER NOT NULL, + content TEXT NOT NULL, + completed BOOLEAN NOT NULL DEFAULT FALSE, + _timestamp TIMESTAMPTZ DEFAULT now(), + FOREIGN KEY (weekid, guildid, userid) REFERENCES member_weekly_goals (weekid, guildid, userid) ON DELETE CASCADE +); +CREATE INDEX member_weekly_goal_tasks_members_weekly ON member_weekly_goal_tasks (guildid, userid, weekid); + +CREATE TABLE member_monthly_goals( + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + monthid INTEGER NOT NULL, -- Epoch time of the start of the UTC month + study_goal INTEGER, + task_goal INTEGER, + _timestamp TIMESTAMPTZ DEFAULT now(), + PRIMARY KEY (guildid, userid, monthid), + FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) ON DELETE CASCADE +); +CREATE INDEX member_monthly_goals_members ON member_monthly_goals (guildid, userid); + +CREATE TABLE member_monthly_goal_tasks( + taskid SERIAL PRIMARY KEY, + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + monthid INTEGER NOT NULL, + content TEXT NOT NULL, + completed BOOLEAN NOT NULL DEFAULT FALSE, + _timestamp TIMESTAMPTZ DEFAULT now(), + FOREIGN KEY (monthid, guildid, userid) REFERENCES member_monthly_goals (monthid, guildid, userid) ON DELETE CASCADE +); +CREATE INDEX member_monthly_goal_tasks_members_monthly ON member_monthly_goal_tasks (guildid, userid, monthid); + INSERT INTO VersionHistory (version, author) VALUES (7, 'v6-v7 migration'); diff --git a/data/schema.sql b/data/schema.sql index 5edb3add..92f40c2f 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -692,9 +692,57 @@ CREATE TABLE member_profile_tags( _timestamp TIMESTAMPTZ DEFAULT now(), FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) ); +CREATE INDEX member_profile_tags_members ON member_profile_tags (guildid, userid); -- }}} -- Member goals {{{ +CREATE TABLE member_weekly_goals( + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + weekid INTEGER NOT NULL, -- Epoch time of the start of the UTC week + study_goal INTEGER, + task_goal INTEGER, + _timestamp TIMESTAMPTZ DEFAULT now(), + PRIMARY KEY (guildid, userid, weekid), + FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) ON DELETE CASCADE +); +CREATE INDEX member_weekly_goals_members ON member_weekly_goals (guildid, userid); + +CREATE TABLE member_weekly_goal_tasks( + taskid SERIAL PRIMARY KEY, + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + weekid INTEGER NOT NULL, + content TEXT NOT NULL, + completed BOOLEAN NOT NULL DEFAULT FALSE, + _timestamp TIMESTAMPTZ DEFAULT now(), + FOREIGN KEY (weekid, guildid, userid) REFERENCES member_weekly_goals (weekid, guildid, userid) ON DELETE CASCADE +); +CREATE INDEX member_weekly_goal_tasks_members_weekly ON member_weekly_goal_tasks (guildid, userid, weekid); + +CREATE TABLE member_monthly_goals( + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + monthid INTEGER NOT NULL, -- Epoch time of the start of the UTC month + study_goal INTEGER, + task_goal INTEGER, + _timestamp TIMESTAMPTZ DEFAULT now(), + PRIMARY KEY (guildid, userid, monthid), + FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) ON DELETE CASCADE +); +CREATE INDEX member_monthly_goals_members ON member_monthly_goals (guildid, userid); + +CREATE TABLE member_monthly_goal_tasks( + taskid SERIAL PRIMARY KEY, + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + monthid INTEGER NOT NULL, + content TEXT NOT NULL, + completed BOOLEAN NOT NULL DEFAULT FALSE, + _timestamp TIMESTAMPTZ DEFAULT now(), + FOREIGN KEY (monthid, guildid, userid) REFERENCES member_monthly_goals (monthid, guildid, userid) ON DELETE CASCADE +); +CREATE INDEX member_monthly_goal_tasks_members_monthly ON member_monthly_goal_tasks (guildid, userid, monthid); -- }}}