diff --git a/src/settings/data.py b/src/settings/data.py index 8c1d2edc..c6061f45 100644 --- a/src/settings/data.py +++ b/src/settings/data.py @@ -70,8 +70,7 @@ class ModelData: ) # If we didn't update any rows, create a new row if not rows: - await model.table.fetch_or_create(**model._dict_from_id, **{cls._column: data}) - ... + await model.fetch_or_create(**model._dict_from_id(parent_id), **{cls._column: data}) if cls._cache is not None: cls._cache[parent_id] = data diff --git a/src/settings/setting_types.py b/src/settings/setting_types.py index c7d1a2de..c70f5cb0 100644 --- a/src/settings/setting_types.py +++ b/src/settings/setting_types.py @@ -4,10 +4,12 @@ from enum import Enum import pytz import discord import itertools +import datetime as dt from discord import ui from discord.ui.button import button, Button, ButtonStyle +from dateutil.parser import parse, ParserError -from meta.context import context +from meta.context import ctx_bot from meta.errors import UserInputError from utils.lib import strfdur, parse_duration from babel import ctx_translator @@ -139,8 +141,8 @@ class ChannelSetting(Generic[ParentID, CT], InteractiveSetting[ParentID, int, CT If the channel cannot be found, returns a `discord.Object` instead. """ if data is not None: - ctx = context.get() - channel = ctx.bot.get_channel(data) + bot = ctx_bot.get() + channel = bot.get_channel(data) if channel is None: channel = discord.Object(id=data) return channel @@ -158,7 +160,7 @@ class ChannelSetting(Generic[ParentID, CT], InteractiveSetting[ParentID, int, CT if data: return "<#{}>".format(data) else: - return None + return "Not Set" @property def input_formatted(self) -> str: @@ -229,8 +231,8 @@ class MessageablelSetting(ChannelSetting): If the channel cannot be found, returns a `discord.PartialMessageable` instead. """ if data is not None: - ctx = context.get() - channel = ctx.bot.get_channel(data) + bot = ctx_bot.get() + channel = bot.get_channel(data) if channel is None: channel = ctx.bot.get_partial_messageable(data, guild_id=parent_id) return channel @@ -277,8 +279,8 @@ class RoleSetting(InteractiveSetting[ParentID, int, Union[discord.Role, discord. role = None guildid = cls._get_guildid(parent_id) - ctx = context.get() - guild = ctx.bot.get_guild(guildid) + bot = ctx_bot.get() + guild = bot.get_guild(guildid) if guild is not None: role = guild.get_role(data) if role is None: @@ -650,8 +652,8 @@ class GuildIDSetting(InteractiveSetting[ParentID, int, int]): If the guild is in cache, attach the name as well. """ if data is not None: - ctx = context.get() - guild = ctx.bot.get_guild(data) + bot = ctx_bot.get() + guild = bot.get_guild(data) if guild is not None: return f"`{data}` ({guild.name})" else: @@ -670,7 +672,6 @@ class TimezoneSetting(InteractiveSetting[ParentID, str, TZT]): # Maybe list e.g. Europe (Austria - Iceland) and Europe (Ireland - Ukraine) separately # TODO Definitely need autocomplete here - accepts = "A timezone name." _accepts = ( "A timezone name from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) " "(e.g. `Europe/London`)." @@ -745,6 +746,60 @@ class TimezoneSetting(InteractiveSetting[ParentID, str, TZT]): return f"`{data}`" +class TimestampSetting(InteractiveSetting[ParentID, str, dt.datetime]): + """ + Typed Setting ABC representing a fixed point in time. + + Data is assumed to be a timezone aware datetime object. + Value is the same as data. + Parsing accepts YYYY-MM-DD [HH:MM] [+TZ] + Display uses a discord timestamp. + """ + _accepts = "A timestamp in the form yyyy-mm-dd HH:MM" + + @classmethod + def _data_from_value(cls, parent_id: ParentID, value, **kwargs): + return value + + @classmethod + def _data_to_value(cls, parent_id: ParentID, data, **kwargs): + return data + + @classmethod + async def _parse_string(cls, parent_id: ParentID, string: str, **kwargs): + string = string.strip() + if string.lower() in ('', 'none', '0'): + ts = None + else: + local_tz = await cls._timezone_from_id(parent_id, **kwargs) + default = dt.datetime.now(tz=local_tz).replace( + hour=0, minute=0, + second=0, microsecond=0 + ) + try: + ts = parse(string, fuzzy=True, default=default) + except ParserError: + # TOLOCALISE: + raise UserInputError("Invalid date string passed") + return ts + + @classmethod + def _format_data(cls, parent_id: ParentID, data, **kwargs): + if data is None: + return "Not Set" + else: + return "".format(int(data.timestamp())) + + @classmethod + async def _timezone_from_id(cls, parent_id: ParentID, **kwargs): + """ + Extract the parsing timezone from the given parent id. + + Should generally be overriden for interactive settings. + """ + return pytz.UTC + + ET = TypeVar('ET', bound='Enum') @@ -1046,7 +1101,7 @@ class ChannelListSetting(ListSetting, InteractiveSetting): _setting = ChannelSetting -class RoleListSetting(InteractiveSetting, ListSetting): +class RoleListSetting(ListSetting, InteractiveSetting): """ List of roles """ diff --git a/src/settings/ui.py b/src/settings/ui.py index 00db61cb..6f357a48 100644 --- a/src/settings/ui.py +++ b/src/settings/ui.py @@ -257,7 +257,7 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]): return '\n'.join(( self.display_name, '=' * len(self.display_name), - self.long_desc, + self.desc, f"\nAccepts: {self.accepts}" ))