Merge branch 'feature-session-tracker' into staging

This commit is contained in:
2021-12-08 12:02:17 +02:00
23 changed files with 1355 additions and 170 deletions

View File

@@ -20,44 +20,13 @@ user_config = RowTable(
)
@user_config.save_query
def add_pending(pending):
"""
pending:
List of tuples of the form `(userid, pending_coins, pending_time)`.
"""
with lions.conn:
cursor = lions.conn.cursor()
data = execute_values(
cursor,
"""
UPDATE members
SET
coins = coins + t.coin_diff,
tracked_time = tracked_time + t.time_diff
FROM
(VALUES %s)
AS
t (guildid, userid, coin_diff, time_diff)
WHERE
members.guildid = t.guildid
AND
members.userid = t.userid
RETURNING *
""",
pending,
fetch=True
)
return lions._make_rows(*data)
guild_config = RowTable(
'guild_config',
('guildid', 'admin_role', 'mod_role', 'event_log_channel', 'mod_log_channel', 'alert_channel',
'studyban_role',
'studyban_role', 'max_study_bans',
'min_workout_length', 'workout_reward',
'max_tasks', 'task_reward', 'task_reward_limit',
'study_hourly_reward', 'study_hourly_live_bonus',
'study_hourly_reward', 'study_hourly_live_bonus', 'daily_study_cap',
'renting_price', 'renting_category', 'renting_cap', 'renting_role', 'renting_sync_perms',
'accountability_category', 'accountability_lobby', 'accountability_bonus',
'accountability_reward', 'accountability_price',
@@ -88,9 +57,66 @@ lions = RowTable(
attach_as='lions'
)
@lions.save_query
def add_pending(pending):
"""
pending:
List of tuples of the form `(guildid, userid, pending_coins)`.
"""
with lions.conn:
cursor = lions.conn.cursor()
data = execute_values(
cursor,
"""
UPDATE members
SET
coins = coins + t.coin_diff
FROM
(VALUES %s)
AS
t (guildid, userid, coin_diff)
WHERE
members.guildid = t.guildid
AND
members.userid = t.userid
RETURNING *
""",
pending,
fetch=True
)
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_user_blacklist = Table('global_user_blacklist')
ignored_members = Table('ignored_members')

View File

@@ -1,4 +1,5 @@
import pytz
from datetime import datetime, timedelta
from meta import client
from data import tables as tb
@@ -11,7 +12,7 @@ class Lion:
Mostly acts as a transparent interface to the corresponding Row,
but also adds some transaction caching logic to `coins` and `tracked_time`.
"""
__slots__ = ('guildid', 'userid', '_pending_coins', '_pending_time', '_member')
__slots__ = ('guildid', 'userid', '_pending_coins', '_member')
# Members with pending transactions
_pending = {} # userid -> User
@@ -24,7 +25,6 @@ class Lion:
self.userid = userid
self._pending_coins = 0
self._pending_time = 0
self._member = None
@@ -41,6 +41,7 @@ class Lion:
if key in cls._lions:
return cls._lions[key]
else:
# TODO: Debug log
lion = tb.lions.fetch(key)
if not lion:
tb.lions.create_row(
@@ -77,23 +78,103 @@ class Lion:
@property
def settings(self):
"""
The UserSettings object for this user.
The UserSettings interface for this member.
"""
return UserSettings(self.userid)
@property
def guild_settings(self):
"""
The GuildSettings interface for this member.
"""
return GuildSettings(self.guildid)
@property
def time(self):
"""
Amount of time the user has spent studying, accounting for pending values.
Amount of time the user has spent studying, accounting for a current session.
"""
return int(self.data.tracked_time + self._pending_time)
# Base time from cached member data
time = self.data.tracked_time
# Add current session time if it exists
if session := self.session:
time += session.duration
return int(time)
@property
def coins(self):
"""
Number of coins the user has, accounting for the pending value.
Number of coins the user has, accounting for the pending value and current session.
"""
return int(self.data.coins + self._pending_coins)
# Base coin amount from cached member data
coins = self.data.coins
# Add pending coin amount
coins += self._pending_coins
# Add current session coins if applicable
if session := self.session:
coins += session.coins_earned
return int(coins)
@property
def session(self):
"""
The current study session the user is in, if any.
"""
if 'sessions' not in client.objects:
raise ValueError("Cannot retrieve session before Study module is initialised!")
return client.objects['sessions'][self.guildid].get(self.userid, None)
@property
def timezone(self):
"""
The user's configured timezone.
Shortcut to `Lion.settings.timezone.value`.
"""
return self.settings.timezone.value
@property
def day_start(self):
"""
A timezone aware datetime representing the start of the user's day (in their configured timezone).
NOTE: This might not be accurate over DST boundaries.
"""
now = datetime.now(tz=self.timezone)
return now.replace(hour=0, minute=0, second=0, microsecond=0)
@property
def remaining_in_day(self):
return ((self.day_start + timedelta(days=1)) - datetime.now(self.timezone)).total_seconds()
@property
def studied_today(self):
"""
The amount of time, in seconds, that the member has studied today.
Extracted from the session history.
"""
return tb.session_history.queries.study_time_since(self.guildid, self.userid, self.day_start)
@property
def remaining_study_today(self):
"""
Maximum remaining time (in seconds) this member can study today.
May not account for DST boundaries and leap seconds.
"""
studied_today = self.studied_today
study_cap = self.guild_settings.daily_study_cap.value
remaining_in_day = self.remaining_in_day
if remaining_in_day >= (study_cap - studied_today):
remaining = study_cap - studied_today
else:
remaining = remaining_in_day + study_cap
return remaining
def localize(self, naive_utc_dt):
"""
@@ -111,15 +192,6 @@ class Lion:
if flush:
self.flush()
def addTime(self, amount, flush=True):
"""
Add time to a user (in seconds), optionally storing the transaction in pending.
"""
self._pending_time += amount
self._pending[self.key] = self
if flush:
self.flush()
def flush(self):
"""
Flush any pending transactions to the database.
@@ -137,7 +209,7 @@ class Lion:
if lions:
# Build userid to pending coin map
pending = [
(lion.guildid, lion.userid, int(lion._pending_coins), int(lion._pending_time))
(lion.guildid, lion.userid, int(lion._pending_coins))
for lion in lions
]
@@ -147,5 +219,4 @@ class Lion:
# Cleanup pending users
for lion in lions:
lion._pending_coins -= int(lion._pending_coins)
lion._pending_time -= int(lion._pending_time)
cls._pending.pop(lion.key, None)

View File

@@ -61,16 +61,18 @@ async def preload_studying_members(client):
"""
userids = list(set(member.id for guild in client.guilds for ch in guild.voice_channels for member in ch.members))
if userids:
rows = client.data.lions.fetch_rows_where(userid=userids)
users = client.data.user_config.fetch_rows_where(userid=userids)
members = client.data.lions.fetch_rows_where(userid=userids)
client.log(
"Preloaded member data for {} members.".format(len(rows)),
"Preloaded data for {} user with {} members.".format(len(users), len(members)),
context="CORE_LOADING"
)
@module.launch_task
async def launch_lion_sync_loop(client):
asyncio.create_task(_lion_sync_loop())
# Removing the sync loop in favour of the studybadge sync.
# @module.launch_task
# async def launch_lion_sync_loop(client):
# asyncio.create_task(_lion_sync_loop())
@module.unload_task