From 654c74e622e78c4fed1413db0bb2ff7eae3064b6 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 21:52:14 -0500 Subject: [PATCH 01/19] Move a function to utils. --- classes/nominate_views.py | 22 +--------------------- utils.py | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/classes/nominate_views.py b/classes/nominate_views.py index 23dda9e..177ae76 100644 --- a/classes/nominate_views.py +++ b/classes/nominate_views.py @@ -9,9 +9,7 @@ from sqlalchemy.orm import Session from config import DakinyConfig from database_setup.sql_data_classes import CouncilMembers, engine -from utils import get_election_cycle_name - -NOMINEE_SPLITTER = "========================================\n\n" +from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name class NominateModal(nextcord.ui.Modal): @@ -142,21 +140,3 @@ class NominateView(nextcord.ui.View): self.config, interaction.user.id, usr_nominated, speech_content ) ) - - -def decorate_nomination(user_id: int, speech: str) -> str: - """ - Decorates a nomination speech so that it sits within multiple speeches better. - - Args: - user_id (int): ID of user sending the speech - speech (str): Speech from the user - - Returns: - str: Decorated speech - """ - msg = f"**Clan Council Nominee: <@{user_id}>**\n" - msg += NOMINEE_SPLITTER - msg += speech - - return msg diff --git a/utils.py b/utils.py index 8f2774b..2b64515 100644 --- a/utils.py +++ b/utils.py @@ -10,6 +10,8 @@ from sqlalchemy.orm import Session from config import DakinyConfig from database_setup.sql_data_classes import CouncilMembers, Votes, engine +NOMINEE_SPLITTER = "========================================\n\n" + def get_datetime(day: str) -> datetime: """ @@ -87,7 +89,9 @@ def build_votes_str(config: DakinyConfig) -> str: for nominee in session.scalars(stmt): votes[nominee.MemberId] = 0 - stmt = select(Votes).where(Votes.ElectionCycle == get_election_cycle_name(config)) + stmt = select(Votes).where( + Votes.ElectionCycle == get_election_cycle_name(config) + ) for vote in session.scalars(stmt): if vote.VotedForId is not None: @@ -101,3 +105,21 @@ def build_votes_str(config: DakinyConfig) -> str: votes_str += "☑️" return votes_str + + +def decorate_nomination(user_id: int, speech: str) -> str: + """ + Decorates a nomination speech so that it sits within multiple speeches better. + + Args: + user_id (int): ID of user sending the speech + speech (str): Speech from the user + + Returns: + str: Decorated speech + """ + msg = f"**Clan Council Nominee: <@{user_id}>**\n" + msg += NOMINEE_SPLITTER + msg += speech + + return msg From b292f956e8808448c0b2c99a279f4b0518509ab5 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 21:52:56 -0500 Subject: [PATCH 02/19] Add functions for different election names within same cycle. --- utils.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/utils.py b/utils.py index 2b64515..1e3cc84 100644 --- a/utils.py +++ b/utils.py @@ -38,13 +38,46 @@ def get_election_cycle_name(config: DakinyConfig) -> str: """ Builds the unique name of the current election cycle + Args: + config (DakinyConfig): the config used to know the election cycle. + Returns: str: the name of the election cycle """ cycle = get_current_cycle(config) current_cycle = config.election_cycles[cycle] - return f"vote-{get_datetime(current_cycle.election_start).year}-{current_cycle.name}" + return ( + f"vote-{get_datetime(current_cycle.election_start).year}-{current_cycle.name}" + ) + + +def get_dm_election_name(config: DakinyConfig) -> str: + """ + creates the name of the discord manager election + + Args: + config (DakinyConfig): Config sent to get_election_cycle_name + + Returns: + str: the name of the election + """ + + return f"{get_election_cycle_name(config)}-dm" + + +def get_cl_election_name(config: DakinyConfig) -> str: + """ + creates the name of the clan leader election + + Args: + config (DakinyConfig): Config sent to get_election_cycle_name + + Returns: + str: the name of the election + """ + + return f"{get_election_cycle_name(config)}-cl" def get_current_cycle(config: DakinyConfig) -> int: @@ -84,7 +117,9 @@ def build_votes_str(config: DakinyConfig) -> str: votes: dict[int, int] = {} with Session(engine) as session: - stmt = select(CouncilMembers).where(CouncilMembers.ElectionCycle == get_election_cycle_name(config)) + stmt = select(CouncilMembers).where( + CouncilMembers.ElectionCycle == get_election_cycle_name(config) + ) for nominee in session.scalars(stmt): votes[nominee.MemberId] = 0 From 1c33139f007c1e889c4a9b9fd46fdfb0de57b1fb Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 21:53:11 -0500 Subject: [PATCH 03/19] Modify database for highcouncil elections. --- database_setup/database.sql | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/database_setup/database.sql b/database_setup/database.sql index 64f77a9..4ae81a5 100644 --- a/database_setup/database.sql +++ b/database_setup/database.sql @@ -8,11 +8,14 @@ CREATE TABLE ElectionInformation( CREATE TABLE CouncilMembers( ElectionCycle varchar(30) not null, MemberId bigint unsigned not null, - SpeechMsgId bigint unsigned not null, + SpeechMsgId bigint unsigned not null, Elected boolean DEFAULT false, - HighCouncilElect boolean DEFAULT false, + DiscordManagerElect boolean DEFAULT false, DiscordManager boolean DEFAULT false, + ClanLeaderElect boolean DEFAULT false, ClanLeader boolean DEFAULT false, + DiscordManagerSpeechId bigint unsigned, + ClanLeaderSpeechId bigint unsigned, FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle), PRIMARY KEY (ElectionCycle, MemberId) ); From 505f27110d62a981bc0770075749ea62250ab5bd Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 21:59:21 -0500 Subject: [PATCH 04/19] Don't need booleans, use ids as booleans. --- database_setup/database.sql | 2 -- 1 file changed, 2 deletions(-) diff --git a/database_setup/database.sql b/database_setup/database.sql index 4ae81a5..7d3b40b 100644 --- a/database_setup/database.sql +++ b/database_setup/database.sql @@ -10,9 +10,7 @@ CREATE TABLE CouncilMembers( MemberId bigint unsigned not null, SpeechMsgId bigint unsigned not null, Elected boolean DEFAULT false, - DiscordManagerElect boolean DEFAULT false, DiscordManager boolean DEFAULT false, - ClanLeaderElect boolean DEFAULT false, ClanLeader boolean DEFAULT false, DiscordManagerSpeechId bigint unsigned, ClanLeaderSpeechId bigint unsigned, From 6e8f999611bde96679f0250c68af100ba11ffb02 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 22:06:39 -0500 Subject: [PATCH 05/19] Create discord manager nominate views. --- classes/nominate_dm_views.py | 141 +++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 classes/nominate_dm_views.py diff --git a/classes/nominate_dm_views.py b/classes/nominate_dm_views.py new file mode 100644 index 0000000..5a0ea09 --- /dev/null +++ b/classes/nominate_dm_views.py @@ -0,0 +1,141 @@ +""" +Contains various views to be used in the discord manager nomination process. +""" + +import nextcord +from nextcord.interactions import Interaction +from sqlalchemy import select +from sqlalchemy.orm import Session + +from config import DakinyConfig +from database_setup.sql_data_classes import CouncilMembers, engine +from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name + + +class NominateDMModal(nextcord.ui.Modal): + """ + Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. + """ + + def __init__( + self, + config: DakinyConfig, + usr_id: int, + usr_nominated: bool, + speech_content: str, + ): + super().__init__( + "Discord Manager Nomination", + timeout=5 * 60, # 5 minutes + ) + self.usr_id: int = usr_id + self.config: DakinyConfig = config + self.usr_nominated = usr_nominated + + self.speech = nextcord.ui.TextInput( + label="Nomination Speech", + style=nextcord.TextInputStyle.paragraph, + placeholder="Please write your speech here.", + required=True, + max_length=1500, + default_value=speech_content, + ) + self.add_item(self.speech) + + async def callback(self, interaction: Interaction) -> None: + """ + Sends the nomination speech into the voting channel and enters the user into the voting database. + + Args: + interaction (Interaction): Interaction with user. + + """ + + speech = decorate_nomination(interaction.user.id, self.speech.value) + + with Session(engine) as session: + # user already in table + cycle = get_election_cycle_name(self.config) + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.MemberId == self.usr_id) + ) + council_elect = session.scalar(stmt) + + if not self.usr_nominated: + msg = await interaction.channel.send(speech) + + council_elect.DiscordManagerSpeechId = msg.id + session.commit() + else: + msg: nextcord.Message = await interaction.channel.fetch_message( + council_elect.DiscordManagerSpeechId + ) + await msg.edit( + content=decorate_nomination(interaction.user.id, self.speech.value) + ) + + +class NominateDMView(nextcord.ui.View): + """ + The button to press to start your Discord Manager nomination. + """ + + def __init__(self, config: DakinyConfig, guild: nextcord.Guild): + super().__init__(timeout=None) + self.config: DakinyConfig = config + self.guild: nextcord.Guild = guild + + @nextcord.ui.button( + label="Nominate for Discord Manager", + style=nextcord.ButtonStyle.primary, + custom_id="nominate-dm", + ) + async def nominate( + self, button: nextcord.ui.Button, interaction: nextcord.Interaction + ) -> None: + """ + Gathers previous information about the user if they have already nominated themselves, then launches + the nomination modal to get their speech. + + Args: + button (nextcord.ui.Button): Unused. idk why its needed in the decorator + interaction (nextcord.Interaction): Interaction of user who pressed the button + """ + + # enforce council elect only + if ( + self.guild.get_role(self.config.params.clan_council_elect_role_id) + not in interaction.user.roles + ): + await interaction.response.send_message( + f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.", + ephemeral=True, + ) + return + + cycle = get_election_cycle_name(self.config) + usr_nominated = False + speech_content = "" + with Session(engine) as session: + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.MemberId == interaction.user.id) + ) + obj = session.scalar(stmt) + + # user needs to exist to get here anyways... + if obj.DiscordManagerSpeechId is not None: + usr_nominated = True + msg = await interaction.channel.fetch_message( + obj.DiscordManagerSpeechId + ) + speech_content = msg.content.split(NOMINEE_SPLITTER)[1] + + await interaction.response.send_modal( + NominateDMModal( + self.config, interaction.user.id, usr_nominated, speech_content + ) + ) From ee13019a0f2d6371b901a8378a81557df4e28403 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 22:11:52 -0500 Subject: [PATCH 06/19] Create clan leader nomination views. --- classes/nominate_cl_views.py | 139 +++++++++++++++++++++++++++++++++++ dakiny_bot.py | 4 + 2 files changed, 143 insertions(+) create mode 100644 classes/nominate_cl_views.py diff --git a/classes/nominate_cl_views.py b/classes/nominate_cl_views.py new file mode 100644 index 0000000..297b105 --- /dev/null +++ b/classes/nominate_cl_views.py @@ -0,0 +1,139 @@ +""" +Contains various views to be used in the clan leader nomination process. +""" + +import nextcord +from nextcord.interactions import Interaction +from sqlalchemy import select +from sqlalchemy.orm import Session + +from config import DakinyConfig +from database_setup.sql_data_classes import CouncilMembers, engine +from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name + + +class NominateCLModal(nextcord.ui.Modal): + """ + Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. + """ + + def __init__( + self, + config: DakinyConfig, + usr_id: int, + usr_nominated: bool, + speech_content: str, + ): + super().__init__( + "Clan Leader Nomination", + timeout=5 * 60, # 5 minutes + ) + self.usr_id: int = usr_id + self.config: DakinyConfig = config + self.usr_nominated = usr_nominated + + self.speech = nextcord.ui.TextInput( + label="Nomination Speech", + style=nextcord.TextInputStyle.paragraph, + placeholder="Please write your speech here.", + required=True, + max_length=1500, + default_value=speech_content, + ) + self.add_item(self.speech) + + async def callback(self, interaction: Interaction) -> None: + """ + Sends the nomination speech into the voting channel and enters the user into the voting database. + + Args: + interaction (Interaction): Interaction with user. + + """ + + speech = decorate_nomination(interaction.user.id, self.speech.value) + + with Session(engine) as session: + # user already in table + cycle = get_election_cycle_name(self.config) + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.MemberId == self.usr_id) + ) + council_elect = session.scalar(stmt) + + if not self.usr_nominated: + msg = await interaction.channel.send(speech) + + council_elect.ClanLeaderSpeechId = msg.id + session.commit() + else: + msg: nextcord.Message = await interaction.channel.fetch_message( + council_elect.ClanLeaderSpeechId + ) + await msg.edit( + content=decorate_nomination(interaction.user.id, self.speech.value) + ) + + +class NominateCLView(nextcord.ui.View): + """ + The button to press to start your Clan Leader nomination. + """ + + def __init__(self, config: DakinyConfig, guild: nextcord.Guild): + super().__init__(timeout=None) + self.config: DakinyConfig = config + self.guild: nextcord.Guild = guild + + @nextcord.ui.button( + label="Nominate for Clan Leader", + style=nextcord.ButtonStyle.primary, + custom_id="nominate-cl", + ) + async def nominate( + self, button: nextcord.ui.Button, interaction: nextcord.Interaction + ) -> None: + """ + Gathers previous information about the user if they have already nominated themselves, then launches + the nomination modal to get their speech. + + Args: + button (nextcord.ui.Button): Unused. idk why its needed in the decorator + interaction (nextcord.Interaction): Interaction of user who pressed the button + """ + + # enforce council elect only + if ( + self.guild.get_role(self.config.params.clan_council_elect_role_id) + not in interaction.user.roles + ): + await interaction.response.send_message( + f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.", + ephemeral=True, + ) + return + + cycle = get_election_cycle_name(self.config) + usr_nominated = False + speech_content = "" + with Session(engine) as session: + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.MemberId == interaction.user.id) + ) + obj = session.scalar(stmt) + + # user needs to exist to get here anyways... + if obj.ClanLeaderSpeechId is not None: + usr_nominated = True + msg = await interaction.channel.fetch_message(obj.ClanLeaderSpeechId) + speech_content = msg.content.split(NOMINEE_SPLITTER)[1] + + await interaction.response.send_modal( + NominateCLModal( + self.config, interaction.user.id, usr_nominated, speech_content + ) + ) diff --git a/dakiny_bot.py b/dakiny_bot.py index ab9bcf6..bef7ff6 100644 --- a/dakiny_bot.py +++ b/dakiny_bot.py @@ -8,6 +8,8 @@ from hydra.core.config_store import ConfigStore from nextcord.ext import commands import database_setup.create_database as create_database +from classes.nominate_cl_views import NominateCLView +from classes.nominate_dm_views import NominateDMView from classes.nominate_views import NominateView from classes.voting_views import VoteView from config import DakinyConfig @@ -54,6 +56,8 @@ def main(cfg: DakinyConfig): assert bot.user is not None create_database # pylint: disable=pointless-statement bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id))) + bot.add_view(NominateCLView(cfg, bot.get_guild(cfg.params.guild_id))) + bot.add_view(NominateDMView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id))) print(f"{bot.user.name} has connected to Discord!") From 88d8e6178c985839f7c651b99b6dbcc5c042c8e6 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 22:20:36 -0500 Subject: [PATCH 07/19] Need to have one view. --- classes/nominate_cl_views.py | 139 ------------------ ...views.py => nominate_highcouncil_views.py} | 124 +++++++++++++++- dakiny_bot.py | 6 +- 3 files changed, 122 insertions(+), 147 deletions(-) delete mode 100644 classes/nominate_cl_views.py rename classes/{nominate_dm_views.py => nominate_highcouncil_views.py} (51%) diff --git a/classes/nominate_cl_views.py b/classes/nominate_cl_views.py deleted file mode 100644 index 297b105..0000000 --- a/classes/nominate_cl_views.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -Contains various views to be used in the clan leader nomination process. -""" - -import nextcord -from nextcord.interactions import Interaction -from sqlalchemy import select -from sqlalchemy.orm import Session - -from config import DakinyConfig -from database_setup.sql_data_classes import CouncilMembers, engine -from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name - - -class NominateCLModal(nextcord.ui.Modal): - """ - Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. - """ - - def __init__( - self, - config: DakinyConfig, - usr_id: int, - usr_nominated: bool, - speech_content: str, - ): - super().__init__( - "Clan Leader Nomination", - timeout=5 * 60, # 5 minutes - ) - self.usr_id: int = usr_id - self.config: DakinyConfig = config - self.usr_nominated = usr_nominated - - self.speech = nextcord.ui.TextInput( - label="Nomination Speech", - style=nextcord.TextInputStyle.paragraph, - placeholder="Please write your speech here.", - required=True, - max_length=1500, - default_value=speech_content, - ) - self.add_item(self.speech) - - async def callback(self, interaction: Interaction) -> None: - """ - Sends the nomination speech into the voting channel and enters the user into the voting database. - - Args: - interaction (Interaction): Interaction with user. - - """ - - speech = decorate_nomination(interaction.user.id, self.speech.value) - - with Session(engine) as session: - # user already in table - cycle = get_election_cycle_name(self.config) - stmt = ( - select(CouncilMembers) - .where(CouncilMembers.ElectionCycle == cycle) - .where(CouncilMembers.MemberId == self.usr_id) - ) - council_elect = session.scalar(stmt) - - if not self.usr_nominated: - msg = await interaction.channel.send(speech) - - council_elect.ClanLeaderSpeechId = msg.id - session.commit() - else: - msg: nextcord.Message = await interaction.channel.fetch_message( - council_elect.ClanLeaderSpeechId - ) - await msg.edit( - content=decorate_nomination(interaction.user.id, self.speech.value) - ) - - -class NominateCLView(nextcord.ui.View): - """ - The button to press to start your Clan Leader nomination. - """ - - def __init__(self, config: DakinyConfig, guild: nextcord.Guild): - super().__init__(timeout=None) - self.config: DakinyConfig = config - self.guild: nextcord.Guild = guild - - @nextcord.ui.button( - label="Nominate for Clan Leader", - style=nextcord.ButtonStyle.primary, - custom_id="nominate-cl", - ) - async def nominate( - self, button: nextcord.ui.Button, interaction: nextcord.Interaction - ) -> None: - """ - Gathers previous information about the user if they have already nominated themselves, then launches - the nomination modal to get their speech. - - Args: - button (nextcord.ui.Button): Unused. idk why its needed in the decorator - interaction (nextcord.Interaction): Interaction of user who pressed the button - """ - - # enforce council elect only - if ( - self.guild.get_role(self.config.params.clan_council_elect_role_id) - not in interaction.user.roles - ): - await interaction.response.send_message( - f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.", - ephemeral=True, - ) - return - - cycle = get_election_cycle_name(self.config) - usr_nominated = False - speech_content = "" - with Session(engine) as session: - stmt = ( - select(CouncilMembers) - .where(CouncilMembers.ElectionCycle == cycle) - .where(CouncilMembers.MemberId == interaction.user.id) - ) - obj = session.scalar(stmt) - - # user needs to exist to get here anyways... - if obj.ClanLeaderSpeechId is not None: - usr_nominated = True - msg = await interaction.channel.fetch_message(obj.ClanLeaderSpeechId) - speech_content = msg.content.split(NOMINEE_SPLITTER)[1] - - await interaction.response.send_modal( - NominateCLModal( - self.config, interaction.user.id, usr_nominated, speech_content - ) - ) diff --git a/classes/nominate_dm_views.py b/classes/nominate_highcouncil_views.py similarity index 51% rename from classes/nominate_dm_views.py rename to classes/nominate_highcouncil_views.py index 5a0ea09..36fc005 100644 --- a/classes/nominate_dm_views.py +++ b/classes/nominate_highcouncil_views.py @@ -1,5 +1,5 @@ """ -Contains various views to be used in the discord manager nomination process. +Contains various views to be used in the high council nomination process. """ import nextcord @@ -77,9 +77,74 @@ class NominateDMModal(nextcord.ui.Modal): ) -class NominateDMView(nextcord.ui.View): +class NominateCLModal(nextcord.ui.Modal): """ - The button to press to start your Discord Manager nomination. + Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. + """ + + def __init__( + self, + config: DakinyConfig, + usr_id: int, + usr_nominated: bool, + speech_content: str, + ): + super().__init__( + "Clan Leader Nomination", + timeout=5 * 60, # 5 minutes + ) + self.usr_id: int = usr_id + self.config: DakinyConfig = config + self.usr_nominated = usr_nominated + + self.speech = nextcord.ui.TextInput( + label="Nomination Speech", + style=nextcord.TextInputStyle.paragraph, + placeholder="Please write your speech here.", + required=True, + max_length=1500, + default_value=speech_content, + ) + self.add_item(self.speech) + + async def callback(self, interaction: Interaction) -> None: + """ + Sends the nomination speech into the voting channel and enters the user into the voting database. + + Args: + interaction (Interaction): Interaction with user. + + """ + + speech = decorate_nomination(interaction.user.id, self.speech.value) + + with Session(engine) as session: + # user already in table + cycle = get_election_cycle_name(self.config) + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.MemberId == self.usr_id) + ) + council_elect = session.scalar(stmt) + + if not self.usr_nominated: + msg = await interaction.channel.send(speech) + + council_elect.ClanLeaderSpeechId = msg.id + session.commit() + else: + msg: nextcord.Message = await interaction.channel.fetch_message( + council_elect.ClanLeaderSpeechId + ) + await msg.edit( + content=decorate_nomination(interaction.user.id, self.speech.value) + ) + + +class NominateHighCouncilView(nextcord.ui.View): + """ + The buttons to press to start your High Council nomination. """ def __init__(self, config: DakinyConfig, guild: nextcord.Guild): @@ -92,7 +157,7 @@ class NominateDMView(nextcord.ui.View): style=nextcord.ButtonStyle.primary, custom_id="nominate-dm", ) - async def nominate( + async def nominate_dm( self, button: nextcord.ui.Button, interaction: nextcord.Interaction ) -> None: """ @@ -139,3 +204,54 @@ class NominateDMView(nextcord.ui.View): self.config, interaction.user.id, usr_nominated, speech_content ) ) + + @nextcord.ui.button( + label="Nominate for Clan Leader", + style=nextcord.ButtonStyle.primary, + custom_id="nominate-cl", + ) + async def nominate_cl( + self, button: nextcord.ui.Button, interaction: nextcord.Interaction + ) -> None: + """ + Gathers previous information about the user if they have already nominated themselves, then launches + the nomination modal to get their speech. + + Args: + button (nextcord.ui.Button): Unused. idk why its needed in the decorator + interaction (nextcord.Interaction): Interaction of user who pressed the button + """ + + # enforce council elect only + if ( + self.guild.get_role(self.config.params.clan_council_elect_role_id) + not in interaction.user.roles + ): + await interaction.response.send_message( + f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.", + ephemeral=True, + ) + return + + cycle = get_election_cycle_name(self.config) + usr_nominated = False + speech_content = "" + with Session(engine) as session: + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.MemberId == interaction.user.id) + ) + obj = session.scalar(stmt) + + # user needs to exist to get here anyways... + if obj.ClanLeaderSpeechId is not None: + usr_nominated = True + msg = await interaction.channel.fetch_message(obj.ClanLeaderSpeechId) + speech_content = msg.content.split(NOMINEE_SPLITTER)[1] + + await interaction.response.send_modal( + NominateCLModal( + self.config, interaction.user.id, usr_nominated, speech_content + ) + ) diff --git a/dakiny_bot.py b/dakiny_bot.py index bef7ff6..0d3cadf 100644 --- a/dakiny_bot.py +++ b/dakiny_bot.py @@ -8,8 +8,7 @@ from hydra.core.config_store import ConfigStore from nextcord.ext import commands import database_setup.create_database as create_database -from classes.nominate_cl_views import NominateCLView -from classes.nominate_dm_views import NominateDMView +from classes.nominate_highcouncil_views import NominateHighCouncilView from classes.nominate_views import NominateView from classes.voting_views import VoteView from config import DakinyConfig @@ -56,8 +55,7 @@ def main(cfg: DakinyConfig): assert bot.user is not None create_database # pylint: disable=pointless-statement bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id))) - bot.add_view(NominateCLView(cfg, bot.get_guild(cfg.params.guild_id))) - bot.add_view(NominateDMView(cfg, bot.get_guild(cfg.params.guild_id))) + bot.add_view(NominateHighCouncilView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id))) print(f"{bot.user.name} has connected to Discord!") From be507f397095f62dcb2ddf0499fff3ea00c7fc58 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 22:21:35 -0500 Subject: [PATCH 08/19] Add highcouncil nominate view to message. --- classes/election_cycle.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 345deb3..a9e1789 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -9,6 +9,7 @@ import nextcord from sqlalchemy import select, true from sqlalchemy.orm import Session +from classes.nominate_highcouncil_views import NominateHighCouncilView from classes.nominate_views import NominateView from classes.voting_views import VoteView from config import DakinyConfig @@ -283,7 +284,9 @@ class ElectionCycle: reason=f"{member.display_name} has won a nomination and become a Council Elect.", ) - await election_thread.send(message) + await election_thread.send( + message, view=NominateHighCouncilView(self.config, self.guild) + ) async def highcouncil_election_start_action(self) -> None: """ From 8e27e03709be0272bc4840a014a3fecbdd7c9f67 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 22:30:45 -0500 Subject: [PATCH 09/19] Need a message to track highcouncil votes. --- classes/election_cycle.py | 11 ++++++++++- database_setup/database.sql | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index a9e1789..f167cc1 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -297,7 +297,16 @@ class ElectionCycle: election_thread: nextcord.Thread = self.guild.get_thread( self.get_election_info().ThreadId ) - await election_thread.send(message) + vote_msg = await election_thread.send(message) + + with Session(engine) as session: + stmt = select(ElectionInformation).where( + ElectionInformation.ElectionCycle + == get_election_cycle_name(self.config) + ) + info = session.scalar(stmt) + info.HighCouncilVoteMsgId = vote_msg.id + session.commit() async def term_start_action(self) -> None: """ diff --git a/database_setup/database.sql b/database_setup/database.sql index 7d3b40b..e976e69 100644 --- a/database_setup/database.sql +++ b/database_setup/database.sql @@ -2,6 +2,7 @@ CREATE TABLE ElectionInformation( ElectionCycle varchar(30) not null unique, ThreadId bigint unsigned not null, CouncilVoteMsgId bigint unsigned, + HighCouncilVoteMsgId bigint unsigned, PRIMARY KEY (ElectionCycle) ); From a7eafdd52aad935cf0621f7d138d0d81e74a7920 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 23:01:51 -0500 Subject: [PATCH 10/19] Create highcouncil voting views. --- classes/election_cycle.py | 5 +- classes/voting_highcouncil_views.py | 253 ++++++++++++++++++++++++++++ dakiny_bot.py | 2 + 3 files changed, 259 insertions(+), 1 deletion(-) create mode 100644 classes/voting_highcouncil_views.py diff --git a/classes/election_cycle.py b/classes/election_cycle.py index f167cc1..5cdca3a 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -11,6 +11,7 @@ from sqlalchemy.orm import Session from classes.nominate_highcouncil_views import NominateHighCouncilView from classes.nominate_views import NominateView +from classes.voting_highcouncil_views import VoteHighcouncilView from classes.voting_views import VoteView from config import DakinyConfig from database_setup.sql_data_classes import ( @@ -297,7 +298,9 @@ class ElectionCycle: election_thread: nextcord.Thread = self.guild.get_thread( self.get_election_info().ThreadId ) - vote_msg = await election_thread.send(message) + vote_msg = await election_thread.send( + message, view=VoteHighcouncilView(self.config, self.guild) + ) with Session(engine) as session: stmt = select(ElectionInformation).where( diff --git a/classes/voting_highcouncil_views.py b/classes/voting_highcouncil_views.py new file mode 100644 index 0000000..2a6aa5f --- /dev/null +++ b/classes/voting_highcouncil_views.py @@ -0,0 +1,253 @@ +""" +Contains various views to be used while voting for high council. Controls the logic when performing the vote. +""" + +from typing import List + +import nextcord +from nextcord import SelectOption +from nextcord.interactions import Interaction +from sqlalchemy import exc, select, update +from sqlalchemy.orm import Session + +from config import DakinyConfig +from database_setup.sql_data_classes import ( + CouncilMembers, + ElectionInformation, + Votes, + engine, +) +from utils import ( + build_highcouncil_votes_str, + get_cl_election_name, + get_dm_election_name, + get_election_cycle_name, +) + + +class VoteSelect(nextcord.ui.StringSelect): + def __init__( + self, options: List["SelectOption"], config: DakinyConfig, election_type: str + ) -> None: + super().__init__( + placeholder="Choose 1", + min_values=1, + max_values=1, + options=options, + ) + self.config = config + self.election_type = election_type + + async def callback(self, interaction: Interaction): + # decide on election type + match self.election_type: + case "cl": + election_name: str = get_cl_election_name(self.config) + case "dm": + election_name: str = get_dm_election_name(self.config) + case _: + # should never happen + raise ValueError('election type was not "cl" or "dm"') + + cycle: str = get_election_cycle_name(self.config) + with Session(engine) as session: + stmt = select(ElectionInformation.HighCouncilVoteMsgId).where( + ElectionInformation.ElectionCycle == cycle + ) + vote_msg_id = session.scalar(stmt) + + if self.values[0] == "Abstain": + voted_for = None + else: + voted_for = int(self.values[0]) + + new_vote = Votes( + ElectionCycle=election_name, + VoterId=interaction.user.id, + VotedForId=voted_for, + ) + + try: + session.add(new_vote) + session.commit() + + except exc.IntegrityError: + session.rollback() + stmt = ( + update(Votes) + .where(Votes.ElectionCycle == election_name) + .where(Votes.VoterId == interaction.user.id) + .values(VotedForId=voted_for) + ) + session.execute(stmt) + session.commit() + + vote_message_str = build_highcouncil_votes_str(self.config) + vote_msg: nextcord.Message = await interaction.channel.fetch_message( + vote_msg_id + ) + await vote_msg.edit(content=vote_message_str) + + if voted_for is not None: + msg = f"You have voted for <@{voted_for}>" + else: + msg = "You have abstained from voting." + + await interaction.edit(content=msg, view=None) + + +class VoteHighcouncilView(nextcord.ui.View): + """ + The button to press to start voting for HighCouncil. + """ + + def __init__(self, config: DakinyConfig, guild: nextcord.Guild): + super().__init__(timeout=None) + self.config = config + self.guild = guild + + @nextcord.ui.button( + label="Vote for Clan Leader", + style=nextcord.ButtonStyle.primary, + custom_id="vote-cl", + ) + async def vote_cl( + self, button: nextcord.ui.Button, interaction: nextcord.Interaction + ) -> None: + """ + Enforces only council elect vote. Gets election cycle, creates voting + options for clan leader (checking if user has voted already), + then sends the voting view. + + Args: + button (nextcord.ui.Button): Unused. idk why its needed in the decorator + interaction (nextcord.Interaction): Interaction of user who pressed the button + """ + + # enforce council elect only + if ( + self.guild.get_role(self.config.params.clan_council_elect_role_id) + not in interaction.user.roles + ): + await interaction.response.send_message( + f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.", + ephemeral=True, + ) + return + + cycle: str = get_election_cycle_name(self.config) + election: str = get_cl_election_name(self.config) + + with Session(engine) as session: + stmt = ( + select(Votes.VotedForId) + .where(Votes.ElectionCycle == election) + .where(Votes.VoterId == interaction.user.id) + ) + voted_for = session.scalar(stmt) + + stmt = ( + select(CouncilMembers.MemberId) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.ClanLeaderSpeechId is not None) + ) + + options = [ + SelectOption( + label=f"{self.guild.get_member(nominee).display_name} ☑️" + if voted_for == nominee + else f"{self.guild.get_member(nominee).display_name}", + value=str(nominee), + ) + for nominee in session.scalars(stmt) + ] + options.append( + SelectOption( + label="Abstain ☑️" if voted_for is None else "Abstain", + value="Abstain", + ) + ) + + view = nextcord.ui.View() + view.add_item( + VoteSelect(options=options, config=self.config, election_type="cl") + ) + + await interaction.response.send_message( + content="Please vote for a Clan Leader!", + view=view, + ephemeral=True, + ) + + @nextcord.ui.button( + label="Vote for Discord Manager", + style=nextcord.ButtonStyle.primary, + custom_id="vote-dm", + ) + async def vote_dm( + self, button: nextcord.ui.Button, interaction: nextcord.Interaction + ) -> None: + """ + Enforces only council elect vote. Gets election cycle, creates voting + options for discord manager (checking if user has voted already), + then sends the voting view. + + Args: + button (nextcord.ui.Button): Unused. idk why its needed in the decorator + interaction (nextcord.Interaction): Interaction of user who pressed the button + """ + + # enforce council elect only + if ( + self.guild.get_role(self.config.params.clan_council_elect_role_id) + not in interaction.user.roles + ): + await interaction.response.send_message( + f"You must have the <@&{self.config.params.clan_council_elect_role_id}> role to vote.", + ephemeral=True, + ) + return + + cycle: str = get_election_cycle_name(self.config) + election: str = get_dm_election_name(self.config) + + with Session(engine) as session: + stmt = ( + select(Votes.VotedForId) + .where(Votes.ElectionCycle == election) + .where(Votes.VoterId == interaction.user.id) + ) + voted_for = session.scalar(stmt) + + stmt = ( + select(CouncilMembers.MemberId) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.DiscordManagerSpeechId is not None) + ) + + options = [ + SelectOption( + label=f"{self.guild.get_member(nominee).display_name} ☑️" + if voted_for == nominee + else f"{self.guild.get_member(nominee).display_name}", + value=str(nominee), + ) + for nominee in session.scalars(stmt) + ] + options.append( + SelectOption( + label="Abstain ☑️" if voted_for is None else "Abstain", + value="Abstain", + ) + ) + + view = nextcord.ui.View() + view.add_item( + VoteSelect(options=options, config=self.config, election_type="dm") + ) + + await interaction.response.send_message( + content="Please vote for a Discord Manager!", + view=view, + ephemeral=True, + ) diff --git a/dakiny_bot.py b/dakiny_bot.py index 0d3cadf..b1679fb 100644 --- a/dakiny_bot.py +++ b/dakiny_bot.py @@ -10,6 +10,7 @@ from nextcord.ext import commands import database_setup.create_database as create_database from classes.nominate_highcouncil_views import NominateHighCouncilView from classes.nominate_views import NominateView +from classes.voting_highcouncil_views import VoteHighcouncilView from classes.voting_views import VoteView from config import DakinyConfig @@ -57,6 +58,7 @@ def main(cfg: DakinyConfig): bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(NominateHighCouncilView(cfg, bot.get_guild(cfg.params.guild_id))) bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id))) + bot.add_view(VoteHighcouncilView(cfg, bot.get_guild(cfg.params.guild_id))) print(f"{bot.user.name} has connected to Discord!") # Run Discord bot From a1896416fc948781bd8c86a33ec845b76ee21dce Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 23:14:23 -0500 Subject: [PATCH 11/19] Make highcouncil vote string builder. Break out repeated code into helper function. --- utils.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 71 insertions(+), 5 deletions(-) diff --git a/utils.py b/utils.py index 1e3cc84..f6f3851 100644 --- a/utils.py +++ b/utils.py @@ -133,15 +133,81 @@ def build_votes_str(config: DakinyConfig) -> str: votes[vote.VotedForId] += 1 votes_str: str = "====================" - for user_id, num_votes in votes.items(): - votes_str += f"\n<@{user_id}>: " - - for num in range(num_votes): - votes_str += "☑️" + votes_str += user_vote_str(votes) return votes_str +def build_highcouncil_votes_str(config: DakinyConfig) -> str: + """ + Creates a string that contains all high council nominees and their votes to be sent as a discord message. + + Args: + config (DakinyConfig): Daking config object + + Returns: + str: String that contains all nominees and their votes. + """ + + cl_votes: dict[int, int] = {} + dm_votes: dict[int, int] = {} + + with Session(engine) as session: + stmt = select(CouncilMembers).where( + CouncilMembers.ElectionCycle == get_cl_election_name(config) + ) + + for nominee in session.scalars(stmt): + cl_votes[nominee.MemberId] = 0 + + stmt = select(Votes).where( + Votes.ElectionCycle == get_election_cycle_name(config) + ) + + for vote in session.scalars(stmt): + if vote.VotedForId is not None: + cl_votes[vote.VotedForId] += 1 + + stmt = select(CouncilMembers).where( + CouncilMembers.ElectionCycle == get_dm_election_name(config) + ) + + for nominee in session.scalars(stmt): + dm_votes[nominee.MemberId] = 0 + + stmt = select(Votes).where( + Votes.ElectionCycle == get_election_cycle_name(config) + ) + + for vote in session.scalars(stmt): + if vote.VotedForId is not None: + dm_votes[vote.VotedForId] += 1 + + # build the votes string + votes_str: str = "Clan Leader Votes\n====================" + votes_str += user_vote_str(cl_votes) + + votes_str += "\n\nDiscord Manager Votes\n====================" + votes_str += user_vote_str(dm_votes) + + return votes_str + + +def user_vote_str(votes: dict[int, int]) -> str: + """ + Creates the string representing how many votes each user has. + + Args: + votes (dict[int, int]): Dict of all users and how many votes they have. + + Returns: + str: The string showing names and number of votes. + """ + + for user_id, num_votes in votes.items(): + return f"\n<@{user_id}>: {'☑️' * num_votes}" + + def decorate_nomination(user_id: int, speech: str) -> str: """ Decorates a nomination speech so that it sits within multiple speeches better. From 18d433ce8cd9f6cc9a283ef1c64f9e7133a875e2 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sun, 17 Dec 2023 12:33:05 -0500 Subject: [PATCH 12/19] Add highcouncil winners message. --- classes/election_cycle.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 5cdca3a..45710b9 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -20,7 +20,13 @@ from database_setup.sql_data_classes import ( Votes, engine, ) -from utils import build_votes_str, get_datetime, get_election_cycle_name +from utils import ( + build_votes_str, + get_cl_election_name, + get_datetime, + get_dm_election_name, + get_election_cycle_name, +) class ElectionCycle: @@ -346,10 +352,25 @@ class ElectionCycle: reason=f"{member.display_name} has been elected to council.", ) + # announce high council members + dm_election = get_dm_election_name(self.config) + cl_election = get_cl_election_name(self.config) + stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == dm_election) + dm_votes = collections.Counter(session.scalars(stmt)) + stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == cl_election) + cl_votes = collections.Counter(session.scalars(stmt)) + + dm_winner = max(dm_votes, key=dm_votes.get) + cl_winner = max(cl_votes, key=cl_votes.get) + election_thread: nextcord.Thread = self.guild.get_thread( self.get_election_info().ThreadId ) + highcouncil_msg: str = f"Congratulations for our high council members!\n Clan Leader: <@{cl_winner}>\n" + highcouncil_msg += f"Discord Manager: <@{dm_winner}>" + await election_thread.send(highcouncil_msg) + await election_thread.send( "This election cycle is now over. Thank you everyone for participating!" ) From 08cf614a60020165a8b935899091884bba93929c Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sun, 17 Dec 2023 12:45:41 -0500 Subject: [PATCH 13/19] Add new test command. --- cogs/commands/test.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cogs/commands/test.py b/cogs/commands/test.py index 82e63a2..ad7233a 100644 --- a/cogs/commands/test.py +++ b/cogs/commands/test.py @@ -90,6 +90,23 @@ class TestCog(commands.Cog, name="Test commands."): ) await cycle.voting_end_action() + @test.subcommand() + @is_me() + async def highcouncil_vote(self, interaction: nextcord.Interaction): + """ + tests the voting start action + + Args: + interaction (nextcord.Interaction): _description_ + """ + config: DakinyConfig = self.bot.config + cycle = ElectionCycle( + get_current_cycle(config), + config, + self.bot.get_guild(config.params.guild_id), + ) + await cycle.highcouncil_election_start_action() + @test.subcommand() @is_me() async def election_reminder(self, interaction: nextcord.Interaction): From d75c64ba2830d90986fe37f5eaa5a91a8ad5dfbf Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sun, 17 Dec 2023 12:49:44 -0500 Subject: [PATCH 14/19] Update message decorator for each type of highcouncil elect. --- classes/nominate_highcouncil_views.py | 19 ++++++++++---- utils.py | 36 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/classes/nominate_highcouncil_views.py b/classes/nominate_highcouncil_views.py index 36fc005..98e841c 100644 --- a/classes/nominate_highcouncil_views.py +++ b/classes/nominate_highcouncil_views.py @@ -9,7 +9,12 @@ from sqlalchemy.orm import Session from config import DakinyConfig from database_setup.sql_data_classes import CouncilMembers, engine -from utils import NOMINEE_SPLITTER, decorate_nomination, get_election_cycle_name +from utils import ( + NOMINEE_SPLITTER, + decorate_cl_nomination, + decorate_dm_nomination, + get_election_cycle_name, +) class NominateDMModal(nextcord.ui.Modal): @@ -51,7 +56,7 @@ class NominateDMModal(nextcord.ui.Modal): """ - speech = decorate_nomination(interaction.user.id, self.speech.value) + speech = decorate_dm_nomination(interaction.user.id, self.speech.value) with Session(engine) as session: # user already in table @@ -73,7 +78,9 @@ class NominateDMModal(nextcord.ui.Modal): council_elect.DiscordManagerSpeechId ) await msg.edit( - content=decorate_nomination(interaction.user.id, self.speech.value) + content=decorate_dm_nomination( + interaction.user.id, self.speech.value + ) ) @@ -116,7 +123,7 @@ class NominateCLModal(nextcord.ui.Modal): """ - speech = decorate_nomination(interaction.user.id, self.speech.value) + speech = decorate_cl_nomination(interaction.user.id, self.speech.value) with Session(engine) as session: # user already in table @@ -138,7 +145,9 @@ class NominateCLModal(nextcord.ui.Modal): council_elect.ClanLeaderSpeechId ) await msg.edit( - content=decorate_nomination(interaction.user.id, self.speech.value) + content=decorate_cl_nomination( + interaction.user.id, self.speech.value + ) ) diff --git a/utils.py b/utils.py index f6f3851..408ebde 100644 --- a/utils.py +++ b/utils.py @@ -224,3 +224,39 @@ def decorate_nomination(user_id: int, speech: str) -> str: msg += speech return msg + + +def decorate_cl_nomination(user_id: int, speech: str) -> str: + """ + Decorates a nomination speech so that it sits within multiple speeches better. + + Args: + user_id (int): ID of user sending the speech + speech (str): Speech from the user + + Returns: + str: Decorated speech + """ + msg = f"**Clan Leader Nominee: <@{user_id}>**\n" + msg += NOMINEE_SPLITTER + msg += speech + + return msg + + +def decorate_dm_nomination(user_id: int, speech: str) -> str: + """ + Decorates a nomination speech so that it sits within multiple speeches better. + + Args: + user_id (int): ID of user sending the speech + speech (str): Speech from the user + + Returns: + str: Decorated speech + """ + msg = f"**Discord Manager Nominee: <@{user_id}>**\n" + msg += NOMINEE_SPLITTER + msg += speech + + return msg From 55259a2653ad87323c8514527b96aad5240bb63f Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sun, 17 Dec 2023 13:01:12 -0500 Subject: [PATCH 15/19] Add comment about ties in highcouncil elect. --- classes/election_cycle.py | 1 + 1 file changed, 1 insertion(+) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 45710b9..fdf9c63 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -353,6 +353,7 @@ class ElectionCycle: ) # announce high council members + # ties are undefined behavior. dm_election = get_dm_election_name(self.config) cl_election = get_cl_election_name(self.config) stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == dm_election) From 09ad45c897a0a688e53a673a15c938f5c473cf74 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sun, 17 Dec 2023 13:52:57 -0500 Subject: [PATCH 16/19] Correct election name mixup. Use the highcouncil build string. --- classes/election_cycle.py | 5 ++++- cogs/commands/test.py | 2 +- utils.py | 12 ++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index fdf9c63..cbc5c01 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -21,6 +21,7 @@ from database_setup.sql_data_classes import ( engine, ) from utils import ( + build_highcouncil_votes_str, build_votes_str, get_cl_election_name, get_datetime, @@ -304,8 +305,10 @@ class ElectionCycle: election_thread: nextcord.Thread = self.guild.get_thread( self.get_election_info().ThreadId ) + await election_thread.send(message) vote_msg = await election_thread.send( - message, view=VoteHighcouncilView(self.config, self.guild) + build_highcouncil_votes_str(self.config), + view=VoteHighcouncilView(self.config, self.guild), ) with Session(engine) as session: diff --git a/cogs/commands/test.py b/cogs/commands/test.py index ad7233a..42c8148 100644 --- a/cogs/commands/test.py +++ b/cogs/commands/test.py @@ -32,7 +32,7 @@ class TestCog(commands.Cog, name="Test commands."): """ self.bot = bot - @nextcord.slash_command() + @nextcord.slash_command(guild_ids=[1000247361639030804]) async def test(self, interaction: nextcord.Interaction): """ Parent slash command for help. Never called. diff --git a/utils.py b/utils.py index 408ebde..58804b4 100644 --- a/utils.py +++ b/utils.py @@ -154,30 +154,26 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str: with Session(engine) as session: stmt = select(CouncilMembers).where( - CouncilMembers.ElectionCycle == get_cl_election_name(config) + CouncilMembers.ElectionCycle == get_election_cycle_name(config) ) for nominee in session.scalars(stmt): cl_votes[nominee.MemberId] = 0 - stmt = select(Votes).where( - Votes.ElectionCycle == get_election_cycle_name(config) - ) + stmt = select(Votes).where(Votes.ElectionCycle == get_cl_election_name(config)) for vote in session.scalars(stmt): if vote.VotedForId is not None: cl_votes[vote.VotedForId] += 1 stmt = select(CouncilMembers).where( - CouncilMembers.ElectionCycle == get_dm_election_name(config) + CouncilMembers.ElectionCycle == get_election_cycle_name(config) ) for nominee in session.scalars(stmt): dm_votes[nominee.MemberId] = 0 - stmt = select(Votes).where( - Votes.ElectionCycle == get_election_cycle_name(config) - ) + stmt = select(Votes).where(Votes.ElectionCycle == get_dm_election_name(config)) for vote in session.scalars(stmt): if vote.VotedForId is not None: From 644eb1dc3182b38bf9bd4a04e57e4db6b229f497 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sun, 17 Dec 2023 14:11:04 -0500 Subject: [PATCH 17/19] Fix none query issue. --- classes/voting_highcouncil_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/voting_highcouncil_views.py b/classes/voting_highcouncil_views.py index 2a6aa5f..44ff2b6 100644 --- a/classes/voting_highcouncil_views.py +++ b/classes/voting_highcouncil_views.py @@ -149,7 +149,7 @@ class VoteHighcouncilView(nextcord.ui.View): stmt = ( select(CouncilMembers.MemberId) .where(CouncilMembers.ElectionCycle == cycle) - .where(CouncilMembers.ClanLeaderSpeechId is not None) + .filter(CouncilMembers.ClanLeaderSpeechId.is_not(None)) ) options = [ @@ -222,7 +222,7 @@ class VoteHighcouncilView(nextcord.ui.View): stmt = ( select(CouncilMembers.MemberId) .where(CouncilMembers.ElectionCycle == cycle) - .where(CouncilMembers.DiscordManagerSpeechId is not None) + .filter(CouncilMembers.DiscordManagerSpeechId.is_not(None)) ) options = [ From b8e35b5e90ad322b0e7877fcffa4773b5de530e7 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sun, 17 Dec 2023 14:17:35 -0500 Subject: [PATCH 18/19] Fix bugs with the voting results string builder. --- utils.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/utils.py b/utils.py index 58804b4..7fb2c93 100644 --- a/utils.py +++ b/utils.py @@ -153,8 +153,10 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str: dm_votes: dict[int, int] = {} with Session(engine) as session: - stmt = select(CouncilMembers).where( - CouncilMembers.ElectionCycle == get_election_cycle_name(config) + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == get_election_cycle_name(config)) + .filter(CouncilMembers.ClanLeaderSpeechId.is_not(None)) ) for nominee in session.scalars(stmt): @@ -166,8 +168,10 @@ def build_highcouncil_votes_str(config: DakinyConfig) -> str: if vote.VotedForId is not None: cl_votes[vote.VotedForId] += 1 - stmt = select(CouncilMembers).where( - CouncilMembers.ElectionCycle == get_election_cycle_name(config) + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == get_election_cycle_name(config)) + .filter(CouncilMembers.DiscordManagerSpeechId.is_not(None)) ) for nominee in session.scalars(stmt): @@ -199,9 +203,11 @@ def user_vote_str(votes: dict[int, int]) -> str: Returns: str: The string showing names and number of votes. """ + ret_str: str = "" for user_id, num_votes in votes.items(): - return f"\n<@{user_id}>: {'☑️' * num_votes}" + ret_str += f"\n<@{user_id}>: {'☑️' * num_votes}" + return ret_str def decorate_nomination(user_id: int, speech: str) -> str: From d449dc6d1a6104f18aa03863695a8c75fbfb6e48 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sun, 17 Dec 2023 14:29:03 -0500 Subject: [PATCH 19/19] Clear voting buttons for highcouncil on end. --- classes/election_cycle.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index cbc5c01..9ba4c57 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -334,8 +334,8 @@ class ElectionCycle: self.config.params.clan_council_role_id ) - # role management with Session(engine) as session: + # role management stmt = ( select(CouncilMembers.MemberId) .where( @@ -371,6 +371,14 @@ class ElectionCycle: self.get_election_info().ThreadId ) + # clear view from vote message + stmt = select(ElectionInformation.HighCouncilVoteMsgId).where( + ElectionInformation.ElectionCycle == get_election_cycle_name(self.config) + ) + vote_msg_id = session.scalar(stmt) + vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id) + await vote_msg.edit(view=None) + highcouncil_msg: str = f"Congratulations for our high council members!\n Clan Leader: <@{cl_winner}>\n" highcouncil_msg += f"Discord Manager: <@{dm_winner}>" await election_thread.send(highcouncil_msg)