diff --git a/DakinySettings.py b/DakinySettings.py index 5939729..e0655bb 100644 --- a/DakinySettings.py +++ b/DakinySettings.py @@ -35,16 +35,6 @@ class Election: voting_start: str -@dataclass -class EventSettings: - """Class defines settings editable by Clan Council.""" - - host_points: int - participate_points: int - verify: bool - weekly_limit: int - - @dataclass class Params: """Class defines start parameters that shouldn't be changed.""" @@ -54,6 +44,19 @@ class Params: guild_id: int +@dataclass +class PointSettings: + """Class defines point settings editable by Clan Council.""" + + clan_points: int + clan_points_weekly_limit: int + events_weekly_limit: int + host_points: int + participate_points: int + verify_clan_points: bool + verify_events: bool + + @dataclass class RoleIds: """Class holds all role ids needed by bot.""" @@ -71,8 +74,8 @@ class DakinySettings(YAMLWizard): channel_ids: ChannelIds election_cycles: list[Election] - event_settings: EventSettings params: Params + point_settings: PointSettings role_ids: RoleIds def save(self) -> None: diff --git a/cogs/commands/record.py b/cogs/commands/record.py index 487d9f0..069fb13 100644 --- a/cogs/commands/record.py +++ b/cogs/commands/record.py @@ -5,7 +5,7 @@ from discord import TextInputStyle from nextcord.ext import commands from DakinySettings import settings -from utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, record_event_points +from utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, PointType, record_points class RecordCog(commands.Cog, name="Record Commands"): @@ -117,16 +117,18 @@ class RecordCog(commands.Cog, name="Record Commands"): await interaction.response.send_message("Please only provide 1 recipient.", ephemeral=True) return elif recipient is not None: # noqa: RET505 - record_event_points( + record_points( [str(recipient.id)], - settings.event_settings.participate_points, + settings.point_settings.clan_points, + PointType.CLAN, is_member=True, ) recipient_str = f"<@{recipient.id}>" elif non_discord_recipient is not None: - record_event_points( + record_points( [non_discord_recipient], - settings.event_settings.participate_points, + settings.point_settings.clan_points, + PointType.CLAN, is_member=False, ) recipient_str = f'"{non_discord_recipient}"' @@ -180,7 +182,7 @@ class EventView(nextcord.ui.View): Removes view and embed from recording message. Sends point information to database. """ - if settings.event_settings.verify: + if settings.point_settings.verify_events: verification_channel: nextcord.TextChannel = interaction.guild.get_channel(settings.channel_ids.admin) verification_view = EventVerification( self.event_participants.values.members, interaction.message.embeds[0].fields[1].value, self.host_id @@ -337,18 +339,20 @@ def process_event(discord_users: list[nextcord.Member], non_discord_users: str, """ if discord_users: discord_users_list: list[str] = [str(user.id) for user in discord_users if user.id is not host_id] - record_event_points( + record_points( discord_users_list, - settings.event_settings.participate_points, + settings.point_settings.participate_points, + PointType.EVENT, is_member=True, ) if non_discord_users: non_discord_users_list: list[str] = non_discord_users.split("\n") - record_event_points( + record_points( non_discord_users_list, - settings.event_settings.participate_points, + settings.point_settings.participate_points, + PointType.EVENT, is_member=False, ) - record_event_points([host_id], settings.event_settings.host_points, is_member=True) + record_points([host_id], settings.point_settings.host_points, PointType.EVENT, is_member=True) diff --git a/cogs/commands/settings.py b/cogs/commands/settings.py index 73aafe3..de3914c 100644 --- a/cogs/commands/settings.py +++ b/cogs/commands/settings.py @@ -144,10 +144,67 @@ class SettingsCog(commands.Cog, name="Settings Commands"): await interaction.response.send_message(f"Voting channel set to <#{channel_id}>.", ephemeral=True) @settings.subcommand() - async def event_settings(self, interaction: nextcord.Interaction) -> None: + async def point_settings(self, interaction: nextcord.Interaction) -> None: """Subcommand parent. Never called.""" - @event_settings.subcommand(description="Sets Event Host Points value.") + @point_settings.subcommand(description="Sets Points value.") + async def clan_points( + self, + interaction: nextcord.Interaction, + points: int = nextcord.SlashOption( + required=True, description="Number of Clan Points the recipient earns.", min_value=0 + ), + ) -> None: + """ + Sets DakinySettings point_settings.clan_points to the given number of points. + + Args: + interaction (nextcord.Interaction): Discord interaction of command. + points (int): Number of points to assign to PointSettings.points. + """ + settings.point_settings.clan_points = points + settings.save() + await interaction.response.send_message(f"Points set to {points}.", ephemeral=True) + + @point_settings.subcommand(description="Sets Event Weekly Limit value for Clan Points.") + async def clan_points_weekly_limit( + self, + interaction: nextcord.Interaction, + points: int = nextcord.SlashOption( + required=True, description="Number of Clan Points a clan member can earn in a week.", min_value=0 + ), + ) -> None: + """ + Sets DakinySettings point_settings.clan_points_weekly_limit to the given number of points. + + Args: + interaction (nextcord.Interaction): Discord interaction of command. + points (int): Number of points to set. + """ + settings.point_settings.clan_points_weekly_limit = points + settings.save() + await interaction.response.send_message(f"Clan Points Weekly Limit set to {points}.", ephemeral=True) + + @point_settings.subcommand(description="Sets Event Weekly Limit value.") + async def events_weekly_limit( + self, + interaction: nextcord.Interaction, + points: int = nextcord.SlashOption( + required=True, description="Number of Event Points a clan member can earn in a week.", min_value=0 + ), + ) -> None: + """ + Sets DakinySettings point_settings.events_weekly_limit to the given number of points. + + Args: + interaction (nextcord.Interaction): Discord interaction of command. + points (int): Number of points to set. + """ + settings.point_settings.events_weekly_limit = points + settings.save() + await interaction.response.send_message(f"Event Weekly Limit set to {points}.", ephemeral=True) + + @point_settings.subcommand(description="Sets Event Host Points value.") async def host_points( self, interaction: nextcord.Interaction, @@ -156,17 +213,17 @@ class SettingsCog(commands.Cog, name="Settings Commands"): ), ) -> None: """ - Sets DakinySettings event_settings.host_points to the given number of points. + Sets DakinySettings point_settings.host_points to the given number of points. Args: interaction (nextcord.Interaction): Discord interaction of command. points (int): Number of points to set. """ - settings.event_settings.host_points = points + settings.point_settings.host_points = points settings.save() await interaction.response.send_message(f"Event Host Points set to {points}.", ephemeral=True) - @event_settings.subcommand(description="Sets Event Participation Points value.") + @point_settings.subcommand(description="Sets Event Participation Points value.") async def participate_points( self, interaction: nextcord.Interaction, @@ -177,18 +234,18 @@ class SettingsCog(commands.Cog, name="Settings Commands"): ), ) -> None: """ - Sets DakinySettings event_settings.participate_points to the given number of points. + Sets DakinySettings point_settings.participate_points to the given number of points. Args: interaction (nextcord.Interaction): Discord interaction of command. points (int): Number of points to set. """ - settings.event_settings.participate_points = points + settings.point_settings.participate_points = points settings.save() await interaction.response.send_message(f"Event Participation Points set to {points}.", ephemeral=True) - @event_settings.subcommand(description="Sets Event Participation Points value.") - async def verify( + @point_settings.subcommand(description="Sets whether to verify a clan point entry.") + async def verify_clan_points( self, interaction: nextcord.Interaction, do_verification: bool = nextcord.SlashOption( # noqa: FBT001 @@ -197,34 +254,35 @@ class SettingsCog(commands.Cog, name="Settings Commands"): ), ) -> None: """ - Sets DakinySettings event_settings.verify to the given number of points. + Sets DakinySettings point_settings.verify_clan_points to the given number of points. Args: interaction (nextcord.Interaction): Discord interaction of command. do_verification (bool): If the bot should verify events. """ - settings.event_settings.verify = do_verification + settings.point_settings.verify_clan_points = do_verification settings.save() await interaction.response.send_message(f"Event verification set to {do_verification}.", ephemeral=True) - @event_settings.subcommand(description="Sets Event Weekly Limit value.") - async def weekly_limit( + @point_settings.subcommand(description="Sets whether an event.") + async def verify_events( self, interaction: nextcord.Interaction, - points: int = nextcord.SlashOption( - required=True, description="Number of Event Points a clan member can earn in a week.", min_value=0 + do_verification: bool = nextcord.SlashOption( # noqa: FBT001 + required=True, + description="Whether an event should be verified or not.", ), ) -> None: """ - Sets DakinySettings event_settings.weekly_limit to the given number of points. + Sets DakinySettings point_settings.verify_events to the given number of points. Args: interaction (nextcord.Interaction): Discord interaction of command. - points (int): Number of points to set. + do_verification (bool): If the bot should verify events. """ - settings.event_settings.weekly_limit = points + settings.point_settings.verify_events = do_verification settings.save() - await interaction.response.send_message(f"Event Weekly Limit set to {points}.", ephemeral=True) + await interaction.response.send_message(f"Event verification set to {do_verification}.", ephemeral=True) @settings.subcommand() async def role_ids(self, interaction: nextcord.Interaction) -> None: diff --git a/cogs/commands/view.py b/cogs/commands/view.py index 65ff0c2..3852d6e 100644 --- a/cogs/commands/view.py +++ b/cogs/commands/view.py @@ -7,12 +7,12 @@ from sqlalchemy import select from sqlalchemy.orm import Session from database_setup.sql_data_classes import ( - EventPoints, + Points, Presences, Votes, engine, ) -from utils import MAX_DISCORD_MESSAGE_LENGTH, VoteType +from utils import GUILDS, MAX_DISCORD_MESSAGE_LENGTH, PointType, VoteType class ViewCog(commands.Cog, name="View Commands"): @@ -21,7 +21,7 @@ class ViewCog(commands.Cog, name="View Commands"): def __init__(self) -> None: """Default constructor for ViewCog class. Does nothing.""" - @nextcord.slash_command() + @nextcord.slash_command(guild_ids=GUILDS) async def view(self, interaction: nextcord.Interaction) -> None: """Parent slash command for set. Never called.""" @@ -49,12 +49,12 @@ class ViewCog(commands.Cog, name="View Commands"): with Session(engine) as session: if quarter_name: ppdf = pd.read_sql_query( - sql=session.query(EventPoints).where(EventPoints.QuarterName.startswith(quarter_name)).statement, + sql=session.query(Points).where(Points.QuarterName.startswith(quarter_name)).statement, con=engine, ) else: ppdf = pd.read_sql_query( - sql=session.query(EventPoints).statement, + sql=session.query(Points).statement, con=engine, ) @@ -63,30 +63,53 @@ class ViewCog(commands.Cog, name="View Commands"): return quarters = ppdf.groupby("QuarterName") - content = "" - for quarter in quarters.groups: - content += f"**{quarter}**\n" - unsorted_all = quarters.get_group(quarter).groupby("DiscordMember") - members = unsorted_all.get_group(name=True) + content = create_points_string(interaction, quarters, PointType.EVENT) - # there can be no non-discord members sometimes. - try: - sorted_non_members = unsorted_all.get_group(name=False).sort_values(by="UserId") - except KeyError: - sorted_non_members = None - sorted_members = members.sort_values( - by="UserId", - key=lambda user_ids: ( - [interaction.guild.get_member(int(user_id)).display_name.lower() for user_id in user_ids] - ), + if len(content) > MAX_DISCORD_MESSAGE_LENGTH: + await interaction.followup.send("Message length is too long, please select search to limit results.") + return + + await interaction.followup.send(content=content, ephemeral=True) + + @view.subcommand(description="Show event points.") + async def clan_points( + self, + interaction: nextcord.Interaction, + quarter_name: str = nextcord.SlashOption( + description="The quarter to view", + required=False, + ), + ) -> None: + """ + View the clan points. + + Builds a message with the clan points for the quarter that matches with quarter_name. + + Args: + interaction (nextcord.Interaction): Interaction with the message. + quarter_name (str, optional): Same of quarter to autofill with. Is SlashOption. + """ + # just in case + await interaction.response.defer(ephemeral=True) + + with Session(engine) as session: + if quarter_name: + ppdf = pd.read_sql_query( + sql=session.query(Points).where(Points.QuarterName.startswith(quarter_name)).statement, + con=engine, ) - sorted_all = pd.concat([sorted_members, sorted_non_members], ignore_index=True, sort=False) - for row in sorted_all.itertuples(): - userid = row.UserId - points = row.Points - discord_member = row.DiscordMember - content += f"<@{userid}>: {points}\n" if discord_member else f"{userid}: {points}\n" - content += "\n" + else: + ppdf = pd.read_sql_query( + sql=session.query(Points).statement, + con=engine, + ) + + if ppdf.empty: + await interaction.followup.send(f'Quarter "{quarter_name}" is invalid.', ephemeral=True) + return + + quarters = ppdf.groupby("QuarterName") + content = create_points_string(interaction, quarters, PointType.CLAN) if len(content) > MAX_DISCORD_MESSAGE_LENGTH: await interaction.followup.send("Message length is too long, please select search to limit results.") @@ -201,7 +224,7 @@ class ViewCog(commands.Cog, name="View Commands"): quarter_name (str): String to run autocomplete on. """ with Session(engine) as session: - stmt = select(EventPoints.QuarterName).distinct() + stmt = select(Points.QuarterName).distinct() quarters = session.scalars(stmt) # if empty compare string @@ -213,3 +236,43 @@ class ViewCog(commands.Cog, name="View Commands"): quarter for quarter in quarters if quarter.lower().startswith(quarter_name.lower()) ] await interaction.response.send_autocomplete(autofilled_quarters[:25]) + + +def create_points_string(interaction: nextcord.Interaction, quarters: any, point_type: PointType) -> str: + """ + Creates the string to show how many clan/event points users have. + + Args: + interaction (nextcord.Interaction): Discord interaction of the command. + quarters (any): Pandas objects grouped by quarters. + point_type (PointType): The type of points to create the message for. + + Returns: + str: The string with the message content. + """ + content: str = "" + for quarter in quarters.groups: + content += f"**{quarter}**\n" + unsorted_all = quarters.get_group(quarter).groupby("DiscordMember") + members = unsorted_all.get_group(name=True) + + # there can be no non-discord members sometimes. + try: + sorted_non_members = unsorted_all.get_group(name=False).sort_values(by="UserId") + except KeyError: + sorted_non_members = None + sorted_members = members.sort_values( + by="UserId", + key=lambda user_ids: ( + [interaction.guild.get_member(int(user_id)).display_name.lower() for user_id in user_ids] + ), + ) + sorted_all = pd.concat([sorted_members, sorted_non_members], ignore_index=True, sort=False) + for row in sorted_all.itertuples(): + userid = row.UserId + points = row.EventPoints if point_type == PointType.EVENT else row.ClanPoints + discord_member = row.DiscordMember + if points > 0: + content += f"<@{userid}>: {points}\n" if discord_member else f"{userid}: {points}\n" + content += "\n" + return content diff --git a/database_setup/database.sql b/database_setup/database.sql index 26e1fb9..7a7ff25 100644 --- a/database_setup/database.sql +++ b/database_setup/database.sql @@ -41,11 +41,12 @@ CREATE TABLE Presences ( Presence varchar(100) not null ); -CREATE TABLE EventPoints ( +CREATE TABLE Points ( QuarterName varchar(30) not null, UserId varchar(32) not null, DiscordMember boolean not null, - Points int unsigned DEFAULT 0, + EventPoints int unsigned DEFAULT 0, + ClanPoints int unsigned DEFAULT 0, PRIMARY KEY (QuarterName, UserId) ); @@ -54,9 +55,10 @@ CREATE TABLE Messages ( Id bigint unsigned not null ); -CREATE TABLE EventPointsLimit ( +CREATE TABLE PointsLimit ( UserId varchar(32) not null, - Points int unsigned DEFAULT 0, + EventPoints int unsigned DEFAULT 0, + ClanPoints int unsigned DEFAULT 0, Week varchar(7) not null, PRIMARY KEY (UserId, Week) ); \ No newline at end of file diff --git a/database_setup/sql_data_classes.py b/database_setup/sql_data_classes.py index abbdd8c..aaf4d59 100644 --- a/database_setup/sql_data_classes.py +++ b/database_setup/sql_data_classes.py @@ -10,11 +10,11 @@ Base = automap_base() Base.prepare(autoload_with=engine) # define the classes +Points = Base.classes.Points +PointsLimit = Base.classes.PointsLimit CouncilMembers = Base.classes.CouncilMembers ElectionInformation = Base.classes.ElectionInformation -EventPointsLimit = Base.classes.EventPointsLimit Messages = Base.classes.Messages -EventPoints = Base.classes.EventPoints Presences = Base.classes.Presences UserBirthday = Base.classes.UserBirthday Votes = Base.classes.Votes diff --git a/pyproject.toml b/pyproject.toml index 94b1c0b..15859a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dataclass-wizard = "^0.22.3" [tool.poetry.group.dev.dependencies] -ruff = "^0.6.3" +ruff = "^0.6.9" [build-system] requires = ["poetry-core"] diff --git a/utils.py b/utils.py index 80cac9b..fc21c61 100644 --- a/utils.py +++ b/utils.py @@ -15,9 +15,9 @@ from sqlalchemy.orm import Session from DakinySettings import settings from database_setup.sql_data_classes import ( CouncilMembers, - EventPoints, - EventPointsLimit, Messages, + Points, + PointsLimit, Votes, engine, ) @@ -30,6 +30,7 @@ MAX_DISCORD_MESSAGE_LENGTH = 2000 GUILDS = [1000247361639030804, 239758371358638080] VoteType = StrEnum("VoteType", ["COUNCIL", "DISCORDMANAGER", "CLANLEADER"]) +PointType = StrEnum("PointType", ["CLAN", "EVENT"]) def get_datetime(day: str) -> datetime: @@ -290,41 +291,46 @@ def decorate_dm_nomination(user_id: int, speech: str) -> str: return msg -def record_event_points(users: Sequence[str], add_points: int, *, is_member: bool) -> None: +def record_points(users: Sequence[str], add_points: int, point_type: PointType, *, is_member: bool) -> None: """ - Adds event points to all users listed. + Adds points to all users listed. Checks if they don't exist in order to make a new entry, otherwise - adds points to the existing value. Can be used for hosting or participating. + adds points to the existing value. Can be used for hosting or participating in events or for the clan + points. Args: users (Sequence[int]): users that participated and need points. add_points (int): number of points to add + point_type (PointType): which point type to add to is_member (bool): whether the users are a discord member or not """ quarter_name = get_quarter_name() with Session(engine) as session: for user in users: - stmt = select(EventPoints).where(EventPoints.UserId == user).where(EventPoints.QuarterName == quarter_name) + stmt = select(Points).where(Points.UserId == user).where(Points.QuarterName == quarter_name) - points = session.scalar(stmt) - if points is None: - add_points = get_allowed_points(session, settings.event_settings.weekly_limit, user, add_points) - new_points = EventPoints( + points_obj = session.scalar(stmt) + add_points = get_allowed_points(session, point_type, user, add_points) + add_clan_points = add_points if point_type == PointType.CLAN else 0 + add_event_points = add_points if point_type == PointType.EVENT else 0 + if points_obj is None: + new_points = Points( QuarterName=quarter_name, UserId=user, DiscordMember=is_member, - Points=add_points, + EventPoints=add_event_points, + ClanPoints=add_clan_points, ) session.add(new_points) else: - add_points = get_allowed_points(session, settings.event_settings.weekly_limit, user, add_points) - points.Points += add_points + points_obj.EventPoints += add_event_points + points_obj.ClanPoints += add_clan_points session.commit() -def get_allowed_points(session: Session, points_limit: int, user: str, points: int) -> int: +def get_allowed_points(session: Session, point_type: PointType, user: str, points: int) -> int: """ Gets the maximum number of points a user can be given. @@ -334,7 +340,7 @@ def get_allowed_points(session: Session, points_limit: int, user: str, points: i Args: session (Session): Open connection to database. - points_limit (int): Maximum number of points a user can be given in a week. + point_type (PointType): Which type of points to check the limit for. user (str): user to be given points. Used to check a user's limit. points (int): maximum number of points to give to user. @@ -344,18 +350,29 @@ def get_allowed_points(session: Session, points_limit: int, user: str, points: i # check for limit today = datetime.now(tz=UTC).date() this_week = f"{today.isocalendar().year}-{today.isocalendar().week}" - stmt = select(EventPointsLimit).where(EventPointsLimit.UserId == user).where(EventPointsLimit.Week == this_week) + points_limit = ( + settings.point_settings.events_weekly_limit + if point_type == PointType.EVENT + else settings.point_settings.clan_points_weekly_limit + ) + stmt = select(PointsLimit).where(PointsLimit.UserId == user).where(PointsLimit.Week == this_week) # if under limit, can add up to however many points would make the current limit equal to the # allowed limit. - limit = session.scalar(stmt) - if limit is None: - new_limit = EventPointsLimit(UserId=user, Points=points, Week=this_week) + limit_obj = session.scalar(stmt) + if limit_obj is None: + clan_points = points if point_type == PointType.CLAN else 0 + event_points = points if point_type == PointType.EVENT else 0 + new_limit = PointsLimit(UserId=user, Week=this_week, EventPoints=event_points, ClanPoints=clan_points) add_points = points session.add(new_limit) else: - add_points = points if limit.Points + points <= points_limit else (max(0, points_limit - limit.Points)) - limit.Points += add_points + curr_points = limit_obj.EventPoints if point_type == PointType.EVENT else limit_obj.ClanPoints + add_points = points if curr_points + points <= points_limit else (max(0, points_limit - curr_points)) + if point_type == PointType.EVENT: + limit_obj.EventPoints += add_points + else: + limit_obj.ClanPoints += add_points session.commit()