From a8def8fd69313b9f1a944358e64d69975013b175 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Tue, 24 Oct 2023 16:49:54 -0400 Subject: [PATCH 01/25] First start on nomination modal. --- classes/election_cycle.py | 9 +++++-- classes/nominate_views.py | 53 +++++++++++++++++++++++++++++++++++++++ cogs/commands/test.py | 4 ++- dakiny_bot.py | 2 ++ 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 classes/nominate_views.py diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 274f856..38f2182 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -8,6 +8,7 @@ import nextcord from sqlalchemy import select from sqlalchemy.orm import Session +from classes.nominate_views import NominateView from config import DakinyConfig from database_setup.sql_data_classes import ElectionInformation, engine from utils import get_datetime @@ -166,8 +167,12 @@ class ElectionCycle: # election start message is returned as an array of string because it is too long. send each string # individually. messages = self.build_election_announcement() - for message in messages: - await election_channel.send(message) + for index, message in enumerate(messages): + # if last item, add the nominate button + if index + 1 == len(messages): + await election_channel.send(message, view=NominateView()) + else: + await election_channel.send(message) async def voting_start_action(self) -> None: """ diff --git a/classes/nominate_views.py b/classes/nominate_views.py new file mode 100644 index 0000000..9d6fb14 --- /dev/null +++ b/classes/nominate_views.py @@ -0,0 +1,53 @@ +""" +Contains various views to be used in the nomination process. +""" + +import nextcord +from nextcord.interactions import Interaction + + +class NominateModal(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): + super().__init__( + "Nomination", + timeout=5 * 60, # 5 minutes + ) + + self.speech = nextcord.ui.TextInput( + label="Nomination Speech", + style=nextcord.TextInputStyle.paragraph, + placeholder="Please write your speech here.", + required=True, + max_length=1500, + ) + 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. + + """ + # TODO add nominee to database, make sure user hasn't nominated themself before, + # make the nomination message better + + await interaction.channel.send(self.speech.value) + + +class NominateView(nextcord.ui.View): + """ + The button to press to start your nomination. + """ + + def __init__(self): + super().__init__(timeout=None) + + @nextcord.ui.button(label="Nominate Yourself", style=nextcord.ButtonStyle.primary, custom_id="nominate") + async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: + await interaction.response.send_modal(NominateModal()) diff --git a/cogs/commands/test.py b/cogs/commands/test.py index 948a331..3bf93a9 100644 --- a/cogs/commands/test.py +++ b/cogs/commands/test.py @@ -6,6 +6,7 @@ import nextcord from nextcord.ext import commands from classes.election_cycle import ElectionCycle +from config import DakinyConfig class TestCog(commands.Cog, name="Test commands."): @@ -51,7 +52,8 @@ class TestCog(commands.Cog, name="Test commands."): interaction (nextcord.Interaction): _description_ """ - cycle = ElectionCycle(0, self.bot) + config: DakinyConfig = self.bot.config + cycle = ElectionCycle(4, config, self.bot.get_guild(config.params.guild_id)) await cycle.election_start_action() @test.subcommand() diff --git a/dakiny_bot.py b/dakiny_bot.py index 076cd15..a60c266 100644 --- a/dakiny_bot.py +++ b/dakiny_bot.py @@ -8,6 +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_views import NominateView from config import DakinyConfig cs = ConfigStore.instance() @@ -49,6 +50,7 @@ def main(cfg: DakinyConfig): """When Discord is connected""" assert bot.user is not None create_database # pylint: disable=pointless-statement + bot.add_view(NominateView()) print(f"{bot.user.name} has connected to Discord!") # Run Discord bot From f86225849c8b89eefe1d7d1b246a2aaa26abba72 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Tue, 24 Oct 2023 16:50:49 -0400 Subject: [PATCH 02/25] add admin perms to testing command... --- cogs/commands/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cogs/commands/test.py b/cogs/commands/test.py index 3bf93a9..966b283 100644 --- a/cogs/commands/test.py +++ b/cogs/commands/test.py @@ -20,7 +20,7 @@ class TestCog(commands.Cog, name="Test commands."): """ self.bot = bot - @nextcord.slash_command() + @nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True)) async def test(self, interaction: nextcord.Interaction): """ Parent slash command for help. Never called. From c013c85f806aae543653303bf457c9a072e73692 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Wed, 25 Oct 2023 14:51:37 -0400 Subject: [PATCH 03/25] Cleaned up nomination message. --- classes/nominate_views.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/classes/nominate_views.py b/classes/nominate_views.py index 9d6fb14..8429c5b 100644 --- a/classes/nominate_views.py +++ b/classes/nominate_views.py @@ -35,9 +35,12 @@ class NominateModal(nextcord.ui.Modal): """ # TODO add nominee to database, make sure user hasn't nominated themself before, - # make the nomination message better - await interaction.channel.send(self.speech.value) + nomination_message = f"**Clan Council Nominee: <@{interaction.user.id}>**\n" + nomination_message += "========================================================\n\n" + nomination_message += self.speech.value + + await interaction.channel.send(nomination_message) class NominateView(nextcord.ui.View): From ad58f3a9cba7932e7dfd1cee22f47b9dfa49480e Mon Sep 17 00:00:00 2001 From: imcompiling Date: Wed, 25 Oct 2023 15:11:36 -0400 Subject: [PATCH 04/25] Correct typo. Remove invalid docstring sentence. --- classes/election_cycle.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 38f2182..d85c6a8 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -32,15 +32,15 @@ class ElectionCycle: self.config: DakinyConfig = config self.cycle = cycle - self.elecion_cycle = self.config.election_cycles[self.cycle] - self.election_start: datetime = get_datetime(self.elecion_cycle.election_start) - self.term_start: datetime = get_datetime(self.elecion_cycle.term_start) - self.term_end: datetime = get_datetime(self.elecion_cycle.term_end) - self.voting_start: datetime = get_datetime(self.elecion_cycle.voting_start) - self.voting_end: datetime = get_datetime(self.elecion_cycle.voting_end) - self.highcouncil_election_announce: datetime = get_datetime(self.elecion_cycle.highcouncil_election_announce) - self.highcouncil_election_start: datetime = get_datetime(self.elecion_cycle.highcouncil_election_start) - self.highcouncil_election_end: datetime = get_datetime(self.elecion_cycle.highcouncil_election_end) + self.election_cycle = self.config.election_cycles[self.cycle] + self.election_start: datetime = get_datetime(self.election_cycle.election_start) + self.term_start: datetime = get_datetime(self.election_cycle.term_start) + self.term_end: datetime = get_datetime(self.election_cycle.term_end) + self.voting_start: datetime = get_datetime(self.election_cycle.voting_start) + self.voting_end: datetime = get_datetime(self.election_cycle.voting_end) + self.highcouncil_election_announce: datetime = get_datetime(self.election_cycle.highcouncil_election_announce) + self.highcouncil_election_start: datetime = get_datetime(self.election_cycle.highcouncil_election_start) + self.highcouncil_election_end: datetime = get_datetime(self.election_cycle.highcouncil_election_end) self.guild: nextcord.Guild = guild self.election_over: bool = False @@ -157,7 +157,7 @@ class ElectionCycle: # create new entry into database. election_channel: nextcord.TextChannel = await category.create_text_channel(quarter_name) clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id) - await election_channel.set_permissions(clan_member_role, send_messages=True, add_reactions=True) + await election_channel.set_permissions(clan_member_role, add_reactions=True) with Session(engine) as session: new_cycle = ElectionInformation(ElectionCycle=quarter_name, ElectionChannel=election_channel.id) session.add(new_cycle) @@ -178,7 +178,6 @@ class ElectionCycle: """ Sends message informing members that voting has started. Removes write permission from the general clan member role. - Adds the voting emote to all messages not sent by the vote. (This is temporary until voting is managed.) """ message = self.build_vote_start() clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id) @@ -226,7 +225,7 @@ class ElectionCycle: str: the name of the election cycle """ - return f"vote-{self.election_start.year}-{self.elecion_cycle.name}" + return f"vote-{self.election_start.year}-{self.election_cycle.name}" def get_election_info(self) -> ElectionInformation: """ From 63e3b3edb27d95d758e41176847791ebf2da8c77 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Wed, 25 Oct 2023 15:20:03 -0400 Subject: [PATCH 05/25] Middle of adding user to council member table. Need to move function for getting election name to utils. --- classes/election_cycle.py | 18 +++--------------- classes/nominate_views.py | 10 ++++++++++ utils.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index d85c6a8..13c49b0 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -11,7 +11,7 @@ from sqlalchemy.orm import Session from classes.nominate_views import NominateView from config import DakinyConfig from database_setup.sql_data_classes import ElectionInformation, engine -from utils import get_datetime +from utils import get_datetime, get_election_cycle_name class ElectionCycle: @@ -152,7 +152,7 @@ class ElectionCycle: """ # create new channel for election category: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.active_category_id) - quarter_name: str = self.get_election_cycle_name() + quarter_name: str = get_election_cycle_name() # create new entry into database. election_channel: nextcord.TextChannel = await category.create_text_channel(quarter_name) @@ -217,16 +217,6 @@ class ElectionCycle: await election_channel.move(beginning=True, category=archive) - def get_election_cycle_name(self) -> str: - """ - Builds the unique name of the current election cycle - - Returns: - str: the name of the election cycle - """ - - return f"vote-{self.election_start.year}-{self.election_cycle.name}" - def get_election_info(self) -> ElectionInformation: """ Gets information for current election. @@ -236,7 +226,5 @@ class ElectionCycle: """ with Session(engine) as session: - stmt = select(ElectionInformation).where( - ElectionInformation.ElectionCycle == self.get_election_cycle_name() - ) + stmt = select(ElectionInformation).where(ElectionInformation.ElectionCycle == get_election_cycle_name()) return session.scalar(stmt) diff --git a/classes/nominate_views.py b/classes/nominate_views.py index 8429c5b..9375da4 100644 --- a/classes/nominate_views.py +++ b/classes/nominate_views.py @@ -4,6 +4,10 @@ Contains various views to be used in the nomination process. import nextcord from nextcord.interactions import Interaction +from sqlalchemy.orm import Session + +from database_setup.sql_data_classes import CouncilMembers, engine +from utils import get_election_cycle_name class NominateModal(nextcord.ui.Modal): @@ -42,6 +46,12 @@ class NominateModal(nextcord.ui.Modal): await interaction.channel.send(nomination_message) + with Session(engine) as session: + council_member = CouncilMembers( + ElectionCycle=get_election_cycle_name(self.bot.config), MemberId=interaction.user.id + ) + session.add(council_member) + class NominateView(nextcord.ui.View): """ diff --git a/utils.py b/utils.py index b0f0d92..c046f7e 100644 --- a/utils.py +++ b/utils.py @@ -4,6 +4,8 @@ File contains util functions. from datetime import datetime +from config import DakinyConfig + def get_datetime(day: str) -> datetime: """ @@ -24,3 +26,15 @@ def get_datetime(day: str) -> datetime: year += 1 return datetime(year, month, day) + + +def get_election_cycle_name(config: DakinyConfig) -> str: + """ + Builds the unique name of the current election cycle + + Returns: + str: the name of the election cycle + """ + # TODO finish + + return f"vote-{election_start.year}-{election_cycle.name}" From 647af0353d427f544e142a218e118aa5d90d19a3 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 2 Nov 2023 14:44:26 -0400 Subject: [PATCH 06/25] move functions to get election cycle index and name into utils. --- classes/election_cycle.py | 6 ++++-- cogs/loops/daily_loop.py | 20 ++++++-------------- utils.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 13c49b0..689b2fb 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -152,7 +152,7 @@ class ElectionCycle: """ # create new channel for election category: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.active_category_id) - quarter_name: str = get_election_cycle_name() + quarter_name: str = get_election_cycle_name(self.config) # create new entry into database. election_channel: nextcord.TextChannel = await category.create_text_channel(quarter_name) @@ -226,5 +226,7 @@ class ElectionCycle: """ with Session(engine) as session: - stmt = select(ElectionInformation).where(ElectionInformation.ElectionCycle == get_election_cycle_name()) + stmt = select(ElectionInformation).where( + ElectionInformation.ElectionCycle == get_election_cycle_name(self.config) + ) return session.scalar(stmt) diff --git a/cogs/loops/daily_loop.py b/cogs/loops/daily_loop.py index 30ed4f7..ffb8caf 100644 --- a/cogs/loops/daily_loop.py +++ b/cogs/loops/daily_loop.py @@ -15,7 +15,7 @@ from classes.election_cycle import ElectionCycle from config import DakinyConfig from dakiny_bot import birthday_list from database_setup.sql_data_classes import UserBirthday, engine -from utils import get_datetime +from utils import get_current_cycle class DailyLoop(commands.Cog, name="Daily Loop"): @@ -40,7 +40,7 @@ class DailyLoop(commands.Cog, name="Daily Loop"): """ if self.current_election is None: - self.current_election = self.get_current_cycle() + self.current_election = self.get_election_cycle() if self.current_election is not None: await self.current_election.check_date() @@ -65,9 +65,9 @@ class DailyLoop(commands.Cog, name="Daily Loop"): await self.bot.wait_until_ready() # Bot needs to know guilds to get the current cycle. - self.current_election = self.get_current_cycle() + self.current_election = self.get_election_cycle() - def get_current_cycle(self) -> ElectionCycle: + def get_election_cycle(self) -> ElectionCycle: """ Creates an object for the current election cycle if one is active. An election is considered active between the election start and the term start. @@ -76,18 +76,10 @@ class DailyLoop(commands.Cog, name="Daily Loop"): ElectionCycle: Current election cycle, None if there is no election happening. """ - today = datetime.today() config: DakinyConfig = self.bot.config - current_election = None + cycle = get_current_cycle(config) - for cycle, election in enumerate(config.election_cycles): - if ( - today.date() >= get_datetime(election.election_start).date() - and today.date() <= get_datetime(election.term_start).date() - ): - current_election = ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id)) - - return current_election + return ElectionCycle(cycle, config, self.bot.get_guild(config.params.guild_id)) def update_birthdays(self) -> None: """ diff --git a/utils.py b/utils.py index c046f7e..d36ab45 100644 --- a/utils.py +++ b/utils.py @@ -35,6 +35,30 @@ def get_election_cycle_name(config: DakinyConfig) -> str: Returns: str: the name of the election cycle """ - # TODO finish + cycle = get_current_cycle(config) + current_cycle = config.election_cycles[cycle] - return f"vote-{election_start.year}-{election_cycle.name}" + return f"vote-{get_datetime(current_cycle.election_start).year}-{current_cycle.name}" + + +def get_current_cycle(config: DakinyConfig) -> int: + """ + Returns index for active election. An election is considered active between the election start and the term start. + + Args: + config (DakinyConfig): Config object to use + + Returns: + int: cycle index for active election. -1 for no active cycle + """ + + today = datetime.today() + + for cycle, election in enumerate(config.election_cycles): + if ( + today.date() >= get_datetime(election.election_start).date() + and today.date() <= get_datetime(election.term_start).date() + ): + return cycle + + return -1 From 133ff79b94666b928f9bec06571570b0c4611d29 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 2 Nov 2023 15:05:48 -0400 Subject: [PATCH 07/25] Pass config along so that nominate can add to database with election cycle name. --- classes/election_cycle.py | 2 +- classes/nominate_views.py | 12 ++++++++---- cogs/commands/test.py | 3 ++- dakiny_bot.py | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 689b2fb..009f0b8 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -170,7 +170,7 @@ class ElectionCycle: for index, message in enumerate(messages): # if last item, add the nominate button if index + 1 == len(messages): - await election_channel.send(message, view=NominateView()) + await election_channel.send(message, view=NominateView(self.config)) else: await election_channel.send(message) diff --git a/classes/nominate_views.py b/classes/nominate_views.py index 9375da4..1fe125d 100644 --- a/classes/nominate_views.py +++ b/classes/nominate_views.py @@ -6,6 +6,7 @@ import nextcord from nextcord.interactions import Interaction 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 @@ -15,7 +16,7 @@ class NominateModal(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): + def __init__(self, config): super().__init__( "Nomination", timeout=5 * 60, # 5 minutes @@ -29,6 +30,7 @@ class NominateModal(nextcord.ui.Modal): max_length=1500, ) self.add_item(self.speech) + self.config: DakinyConfig = config async def callback(self, interaction: Interaction) -> None: """ @@ -48,9 +50,10 @@ class NominateModal(nextcord.ui.Modal): with Session(engine) as session: council_member = CouncilMembers( - ElectionCycle=get_election_cycle_name(self.bot.config), MemberId=interaction.user.id + ElectionCycle=get_election_cycle_name(self.config), MemberId=interaction.user.id ) session.add(council_member) + session.commit() class NominateView(nextcord.ui.View): @@ -58,9 +61,10 @@ class NominateView(nextcord.ui.View): The button to press to start your nomination. """ - def __init__(self): + def __init__(self, config): super().__init__(timeout=None) + self.config: DakinyConfig = config @nextcord.ui.button(label="Nominate Yourself", style=nextcord.ButtonStyle.primary, custom_id="nominate") async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: - await interaction.response.send_modal(NominateModal()) + await interaction.response.send_modal(NominateModal(self.config)) diff --git a/cogs/commands/test.py b/cogs/commands/test.py index 966b283..049d909 100644 --- a/cogs/commands/test.py +++ b/cogs/commands/test.py @@ -7,6 +7,7 @@ from nextcord.ext import commands from classes.election_cycle import ElectionCycle from config import DakinyConfig +from utils import get_current_cycle class TestCog(commands.Cog, name="Test commands."): @@ -53,7 +54,7 @@ class TestCog(commands.Cog, name="Test commands."): """ config: DakinyConfig = self.bot.config - cycle = ElectionCycle(4, config, self.bot.get_guild(config.params.guild_id)) + cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id)) await cycle.election_start_action() @test.subcommand() diff --git a/dakiny_bot.py b/dakiny_bot.py index a60c266..1608fe0 100644 --- a/dakiny_bot.py +++ b/dakiny_bot.py @@ -50,7 +50,7 @@ def main(cfg: DakinyConfig): """When Discord is connected""" assert bot.user is not None create_database # pylint: disable=pointless-statement - bot.add_view(NominateView()) + bot.add_view(NominateView(cfg)) print(f"{bot.user.name} has connected to Discord!") # Run Discord bot From 19baf84c80728bce876eb09741aaa1ccb9ccc222 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 2 Nov 2023 15:12:28 -0400 Subject: [PATCH 08/25] Add speech msg id to database, add the id when submitting a nomination. --- classes/nominate_views.py | 4 ++-- database_setup/database.sql | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/classes/nominate_views.py b/classes/nominate_views.py index 1fe125d..a184844 100644 --- a/classes/nominate_views.py +++ b/classes/nominate_views.py @@ -46,11 +46,11 @@ class NominateModal(nextcord.ui.Modal): nomination_message += "========================================================\n\n" nomination_message += self.speech.value - await interaction.channel.send(nomination_message) + msg = await interaction.channel.send(nomination_message) with Session(engine) as session: council_member = CouncilMembers( - ElectionCycle=get_election_cycle_name(self.config), MemberId=interaction.user.id + ElectionCycle=get_election_cycle_name(self.config), MemberId=interaction.user.id, SpeechMsgId=msg.id ) session.add(council_member) session.commit() diff --git a/database_setup/database.sql b/database_setup/database.sql index 9271b25..d1d6e75 100644 --- a/database_setup/database.sql +++ b/database_setup/database.sql @@ -7,6 +7,7 @@ CREATE TABLE ElectionInformation( CREATE TABLE CouncilMembers( ElectionCycle varchar(30) not null unique, MemberId bigint unsigned not null, + SpeechMsgId bigint unsigned not null, Elected boolean DEFAULT false, HighCouncilElect boolean DEFAULT false, DiscordManager boolean DEFAULT false, From a0f160c801d1e5a8436c5e0c8de0a1cefad1491d Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 2 Nov 2023 16:22:02 -0400 Subject: [PATCH 09/25] Rework to allow users to edit their nomination message. --- classes/nominate_views.py | 91 +++++++++++++++++++++++++++++++++------ 1 file changed, 77 insertions(+), 14 deletions(-) diff --git a/classes/nominate_views.py b/classes/nominate_views.py index a184844..b66075f 100644 --- a/classes/nominate_views.py +++ b/classes/nominate_views.py @@ -4,23 +4,29 @@ Contains various views to be used in the 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 get_election_cycle_name +NOMINEE_SPLITTER = "========================================================\n\n" + class NominateModal(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): + def __init__(self, config: DakinyConfig, usr_id: int, usr_nominated: bool, speech_content: str): super().__init__( "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", @@ -28,9 +34,9 @@ class NominateModal(nextcord.ui.Modal): placeholder="Please write your speech here.", required=True, max_length=1500, + default_value=speech_content, ) self.add_item(self.speech) - self.config: DakinyConfig = config async def callback(self, interaction: Interaction) -> None: """ @@ -40,20 +46,33 @@ class NominateModal(nextcord.ui.Modal): interaction (Interaction): Interaction with user. """ - # TODO add nominee to database, make sure user hasn't nominated themself before, - nomination_message = f"**Clan Council Nominee: <@{interaction.user.id}>**\n" - nomination_message += "========================================================\n\n" - nomination_message += self.speech.value - - msg = await interaction.channel.send(nomination_message) + speech = decorate_nomination(interaction.user.id, self.speech.value) with Session(engine) as session: - council_member = CouncilMembers( - ElectionCycle=get_election_cycle_name(self.config), MemberId=interaction.user.id, SpeechMsgId=msg.id - ) - session.add(council_member) - session.commit() + if not self.usr_nominated: + msg = await interaction.channel.send(speech) + + council_member = CouncilMembers( + ElectionCycle=get_election_cycle_name(self.config), + MemberId=interaction.user.id, + SpeechMsgId=msg.id, + ) + session.add(council_member) + session.commit() + else: + # user already nominated, just need to update the election speech + cycle = get_election_cycle_name(self.config) + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == cycle) + .where(CouncilMembers.MemberId == self.usr_id) + ) + obj = session.scalar(stmt) + obj.SpeechMsgContents = self.speech.value + + msg: nextcord.Message = await interaction.channel.fetch_message(obj.SpeechMsgId) + await msg.edit(content=decorate_nomination(interaction.user.id, self.speech.value)) class NominateView(nextcord.ui.View): @@ -67,4 +86,48 @@ class NominateView(nextcord.ui.View): @nextcord.ui.button(label="Nominate Yourself", style=nextcord.ButtonStyle.primary, custom_id="nominate") async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: - await interaction.response.send_modal(NominateModal(self.config)) + """ + 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 + """ + + 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) + if obj is not None: + usr_nominated = True + msg = await interaction.channel.fetch_message(obj.SpeechMsgId) + speech_content = msg.content.split(NOMINEE_SPLITTER)[1] + + await interaction.response.send_modal( + NominateModal(self.config, interaction.user.id, usr_nominated, speech_content) + ) + + +def decorate_nomination(user_id, speech) -> str: + """ + Decorates a nomination speech so that it sits within multiple speeches better. + + Args: + user_id (_type_): ID of user sending the speech + speech (_type_): Speech from the user + + Returns: + str: Decorated speech + """ + msg = f"**Clan Council Nominee: <@{user_id}>**\n" + msg += NOMINEE_SPLITTER + msg += speech + + return msg From e7bd6080179c0b361a1e776440377afa209138d2 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 2 Nov 2023 17:01:19 -0400 Subject: [PATCH 10/25] Add nominees and voting total to voting message. --- bot_messages/vote_start.txt | 6 +++++- classes/election_cycle.py | 24 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/bot_messages/vote_start.txt b/bot_messages/vote_start.txt index f67865e..5f59e83 100644 --- a/bot_messages/vote_start.txt +++ b/bot_messages/vote_start.txt @@ -11,4 +11,8 @@ After a candidate receives 3 votes, their spot is secured in the new council, bu Votes for multiple candidates from the same person will not be counted. RS3 Clan Members -OSRS Clan Members \ No newline at end of file +OSRS Clan Members + +==================== +Clan nominees: +{clan_nominees} \ No newline at end of file diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 009f0b8..59c435c 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -10,7 +10,7 @@ from sqlalchemy.orm import Session from classes.nominate_views import NominateView from config import DakinyConfig -from database_setup.sql_data_classes import ElectionInformation, engine +from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine from utils import get_datetime, get_election_cycle_name @@ -121,11 +121,33 @@ class ElectionCycle: message = file.read() vote_end: datetime = get_datetime(self.config.election_cycles[self.cycle].voting_end) + votes: dict[int, int] = {} + + with Session(engine) as session: + stmt = select(CouncilMembers).where(ElectionCycle == get_election_cycle_name(self.config)) + + for nominee in session.scalars(stmt): + votes[nominee.MemberId] = 0 + + stmt = select(Votes).where(ElectionCycle == get_election_cycle_name(self.config)) + + for vote in session.scalars(stmt): + votes[vote.VotedForId] += 1 + + nominees_str: str = "" + for user_id, num_votes in votes.items(): + nominees_str += f"<@{user_id}>: " + + for num in range(num_votes): + nominees_str += "☑️" + nominees_str += "\n" + return message.format( clan_member_id=self.config.params.clan_member_role_id, voting_emote_id=self.config.params.voting_emote_id, voting_end=vote_end.strftime("%B %d"), voting_end_day=vote_end.strftime("%A"), + clan_nominees=nominees_str, ) async def check_date(self) -> None: From 626cca8b814026129193f81d761f09fba4fb45f8 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 2 Nov 2023 17:26:05 -0400 Subject: [PATCH 11/25] Start work on vote selection. Modal does not work. --- classes/election_cycle.py | 3 ++- classes/voting_views.py | 52 +++++++++++++++++++++++++++++++++++++++ cogs/commands/test.py | 3 ++- dakiny_bot.py | 2 ++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 classes/voting_views.py diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 59c435c..57e5a9a 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -9,6 +9,7 @@ from sqlalchemy import select from sqlalchemy.orm import Session from classes.nominate_views import NominateView +from classes.voting_views import VoteView from config import DakinyConfig from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine from utils import get_datetime, get_election_cycle_name @@ -204,7 +205,7 @@ class ElectionCycle: message = self.build_vote_start() clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id) election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel) - await election_channel.send(message) + await election_channel.send(message, view=VoteView()) await election_channel.set_permissions(clan_member_role, send_messages=False) async def voting_end_action(self) -> None: diff --git a/classes/voting_views.py b/classes/voting_views.py new file mode 100644 index 0000000..b76ee27 --- /dev/null +++ b/classes/voting_views.py @@ -0,0 +1,52 @@ +""" +Contains various views to be used while voting. Controls the logic when performing the vote. +""" + +import nextcord +from nextcord import SelectOption +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 get_election_cycle_name + + +class VoteModal(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): + super().__init__( + "Nomination", + timeout=5 * 60, # 5 minutes + ) + + self.list = nextcord.ui.StringSelect( + min_values=1, + max_values=1, + options=[SelectOption(label="one"), SelectOption(label="two"), SelectOption(label="three")], + ) + self.add_item(self.list) + + +class VoteView(nextcord.ui.View): + """ + The button to press to start your nomination. + """ + + def __init__(self): + super().__init__(timeout=None) + + @nextcord.ui.button(label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote") + async def vote(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: + """ + TODO + + Args: + button (nextcord.ui.Button): Unused. idk why its needed in the decorator + interaction (nextcord.Interaction): Interaction of user who pressed the button + """ + + await interaction.response.send_modal(VoteModal()) diff --git a/cogs/commands/test.py b/cogs/commands/test.py index 049d909..ae73956 100644 --- a/cogs/commands/test.py +++ b/cogs/commands/test.py @@ -65,5 +65,6 @@ class TestCog(commands.Cog, name="Test commands."): Args: interaction (nextcord.Interaction): _description_ """ - cycle = ElectionCycle(0, self.bot) + config: DakinyConfig = self.bot.config + cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id)) await cycle.voting_start_action() diff --git a/dakiny_bot.py b/dakiny_bot.py index 1608fe0..8347da1 100644 --- a/dakiny_bot.py +++ b/dakiny_bot.py @@ -9,6 +9,7 @@ from nextcord.ext import commands import database_setup.create_database as create_database from classes.nominate_views import NominateView +from classes.voting_views import VoteView from config import DakinyConfig cs = ConfigStore.instance() @@ -51,6 +52,7 @@ def main(cfg: DakinyConfig): assert bot.user is not None create_database # pylint: disable=pointless-statement bot.add_view(NominateView(cfg)) + bot.add_view(VoteView()) print(f"{bot.user.name} has connected to Discord!") # Run Discord bot From 5b1b4cfecbc6ac1eda3ed9a47b97b01ac9c1376f Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 2 Nov 2023 19:52:20 -0400 Subject: [PATCH 12/25] Change modal to a view. Still need to fill out functionality. Funtionality needed: put into database, check on view creation of the user has voted already. If so, then change the content to show they already voted and who they voted for. Alert that they can change their vote. --- classes/voting_views.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/classes/voting_views.py b/classes/voting_views.py index b76ee27..36c476d 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -2,8 +2,12 @@ Contains various views to be used while voting. Controls the logic when performing the vote. """ +from typing import List, Optional + import nextcord from nextcord import SelectOption +from nextcord.interactions import Interaction +from nextcord.utils import MISSING from sqlalchemy import select from sqlalchemy.orm import Session @@ -12,23 +16,27 @@ from database_setup.sql_data_classes import CouncilMembers, engine from utils import get_election_cycle_name -class VoteModal(nextcord.ui.Modal): +class VoteActionView(nextcord.ui.View): """ Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. """ def __init__(self): - super().__init__( - "Nomination", - timeout=5 * 60, # 5 minutes - ) + super().__init__(timeout=300) + self.add_item(VoteSelect()) - self.list = nextcord.ui.StringSelect( + +class VoteSelect(nextcord.ui.StringSelect): + def __init__(self) -> None: + super().__init__( + placeholder="Choose 1", min_values=1, max_values=1, options=[SelectOption(label="one"), SelectOption(label="two"), SelectOption(label="three")], ) - self.add_item(self.list) + + async def callback(self, interaction: Interaction): + await interaction.edit(content=self.values[0], view=None) class VoteView(nextcord.ui.View): @@ -49,4 +57,6 @@ class VoteView(nextcord.ui.View): interaction (nextcord.Interaction): Interaction of user who pressed the button """ - await interaction.response.send_modal(VoteModal()) + await interaction.response.send_message( + content="Please vote for a clan member!", view=VoteActionView(), ephemeral=True + ) From ff68e7df1c2730317d895f95ead17b47f05b07fc Mon Sep 17 00:00:00 2001 From: imcompiling Date: Thu, 2 Nov 2023 19:57:54 -0400 Subject: [PATCH 13/25] Allow for abstain votes. --- database_setup/database.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database_setup/database.sql b/database_setup/database.sql index d1d6e75..f959ccd 100644 --- a/database_setup/database.sql +++ b/database_setup/database.sql @@ -19,7 +19,7 @@ CREATE TABLE CouncilMembers( CREATE TABLE Votes( ElectionCycle varchar(30) not null unique, VoterId bigint unsigned not null, - VotedForId bigint unsigned not null, + VotedForId bigint unsigned, FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle), PRIMARY KEY (ElectionCycle, VoterId) ); From 9c588a18aff0f2c23c6cae219a67cc71bf28e834 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 11:00:55 -0400 Subject: [PATCH 14/25] Need to track the message where votes are presented. --- database_setup/database.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/database_setup/database.sql b/database_setup/database.sql index f959ccd..7996551 100644 --- a/database_setup/database.sql +++ b/database_setup/database.sql @@ -1,6 +1,7 @@ CREATE TABLE ElectionInformation( ElectionCycle varchar(30) not null unique, ElectionChannel bigint unsigned not null, + CouncilVoteMsgId bigint unsigned, PRIMARY KEY (ElectionCycle) ); From a5ef579767c91d89f4508ec524d14ec30730bd66 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 11:53:47 -0400 Subject: [PATCH 15/25] ElectionName is not unique in the other tables. --- database_setup/database.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database_setup/database.sql b/database_setup/database.sql index 7996551..299e621 100644 --- a/database_setup/database.sql +++ b/database_setup/database.sql @@ -6,7 +6,7 @@ CREATE TABLE ElectionInformation( ); CREATE TABLE CouncilMembers( - ElectionCycle varchar(30) not null unique, + ElectionCycle varchar(30) not null, MemberId bigint unsigned not null, SpeechMsgId bigint unsigned not null, Elected boolean DEFAULT false, @@ -18,7 +18,7 @@ CREATE TABLE CouncilMembers( ); CREATE TABLE Votes( - ElectionCycle varchar(30) not null unique, + ElectionCycle varchar(30) not null, VoterId bigint unsigned not null, VotedForId bigint unsigned, FOREIGN KEY (ElectionCycle) REFERENCES ElectionInformation(ElectionCycle), From fa3284b9e313e2cf54dbbb68ae8d4a44d1943559 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 11:55:21 -0400 Subject: [PATCH 16/25] Pull the vote count message out of vote start. Make its own builder function in utils. --- bot_messages/vote_start.txt | 6 +----- classes/election_cycle.py | 22 --------------------- utils.py | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/bot_messages/vote_start.txt b/bot_messages/vote_start.txt index 5f59e83..f67865e 100644 --- a/bot_messages/vote_start.txt +++ b/bot_messages/vote_start.txt @@ -11,8 +11,4 @@ After a candidate receives 3 votes, their spot is secured in the new council, bu Votes for multiple candidates from the same person will not be counted. RS3 Clan Members -OSRS Clan Members - -==================== -Clan nominees: -{clan_nominees} \ No newline at end of file +OSRS Clan Members \ No newline at end of file diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 57e5a9a..ce16cfc 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -122,33 +122,11 @@ class ElectionCycle: message = file.read() vote_end: datetime = get_datetime(self.config.election_cycles[self.cycle].voting_end) - votes: dict[int, int] = {} - - with Session(engine) as session: - stmt = select(CouncilMembers).where(ElectionCycle == get_election_cycle_name(self.config)) - - for nominee in session.scalars(stmt): - votes[nominee.MemberId] = 0 - - stmt = select(Votes).where(ElectionCycle == get_election_cycle_name(self.config)) - - for vote in session.scalars(stmt): - votes[vote.VotedForId] += 1 - - nominees_str: str = "" - for user_id, num_votes in votes.items(): - nominees_str += f"<@{user_id}>: " - - for num in range(num_votes): - nominees_str += "☑️" - nominees_str += "\n" - return message.format( clan_member_id=self.config.params.clan_member_role_id, voting_emote_id=self.config.params.voting_emote_id, voting_end=vote_end.strftime("%B %d"), voting_end_day=vote_end.strftime("%A"), - clan_nominees=nominees_str, ) async def check_date(self) -> None: diff --git a/utils.py b/utils.py index d36ab45..2b11006 100644 --- a/utils.py +++ b/utils.py @@ -4,7 +4,11 @@ File contains util functions. from datetime import datetime +from sqlalchemy import select +from sqlalchemy.orm import Session + from config import DakinyConfig +from database_setup.sql_data_classes import CouncilMembers, Votes, engine def get_datetime(day: str) -> datetime: @@ -62,3 +66,38 @@ def get_current_cycle(config: DakinyConfig) -> int: return cycle return -1 + + +def build_votes_str(config: DakinyConfig) -> str: + """ + Creates a string that contains all 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. + """ + + votes: dict[int, int] = {} + + with Session(engine) as session: + stmt = select(CouncilMembers).where(CouncilMembers.ElectionCycle == get_election_cycle_name(config)) + + for nominee in session.scalars(stmt): + votes[nominee.MemberId] = 0 + + stmt = select(Votes).where(Votes.ElectionCycle == get_election_cycle_name(config)) + + for vote in session.scalars(stmt): + votes[vote.VotedForId] += 1 + + votes_str: str = "====================" + for user_id, num_votes in votes.items(): + votes_str += f"<@{user_id}>: " + + for num in range(num_votes): + votes_str += "☑️" + votes_str += "\n" + + return votes_str From 14f2e13772e1eceadf803a3ccbbc88a1ac9decf5 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 11:55:57 -0400 Subject: [PATCH 17/25] General rework to the voting views. --- classes/election_cycle.py | 14 +++++++++--- classes/voting_views.py | 47 +++++++++++++++++++++++---------------- dakiny_bot.py | 2 +- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index ce16cfc..210c2f3 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -11,8 +11,8 @@ from sqlalchemy.orm import Session from classes.nominate_views import NominateView from classes.voting_views import VoteView from config import DakinyConfig -from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine -from utils import get_datetime, get_election_cycle_name +from database_setup.sql_data_classes import ElectionInformation, engine +from utils import build_votes_str, get_datetime, get_election_cycle_name class ElectionCycle: @@ -183,7 +183,15 @@ class ElectionCycle: message = self.build_vote_start() clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id) election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel) - await election_channel.send(message, view=VoteView()) + await election_channel.send(message, view=VoteView(self.config, self.guild)) + vote_msg = await election_channel.send(build_votes_str(self.config)) + + with Session(engine) as session: + stmt = select(ElectionInformation).where(ElectionCycle == get_election_cycle_name(self.config)) + info = session.scalar(stmt) + info.CouncilVoteMsgId = vote_msg.id + session.commit() + await election_channel.set_permissions(clan_member_role, send_messages=False) async def voting_end_action(self) -> None: diff --git a/classes/voting_views.py b/classes/voting_views.py index 36c476d..9c34966 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -2,40 +2,39 @@ Contains various views to be used while voting. Controls the logic when performing the vote. """ -from typing import List, Optional +from typing import List import nextcord from nextcord import SelectOption +from nextcord.ext import commands from nextcord.interactions import Interaction -from nextcord.utils import MISSING 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 get_election_cycle_name - - -class VoteActionView(nextcord.ui.View): - """ - Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel. - """ - - def __init__(self): - super().__init__(timeout=300) - self.add_item(VoteSelect()) +from config import DakinyConfig, ElectionCycle +from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, engine +from utils import build_votes_str, get_election_cycle_name class VoteSelect(nextcord.ui.StringSelect): - def __init__(self) -> None: + def __init__(self, options: List["SelectOption"], config: DakinyConfig) -> None: super().__init__( placeholder="Choose 1", min_values=1, max_values=1, - options=[SelectOption(label="one"), SelectOption(label="two"), SelectOption(label="three")], + options=options, ) + self.config = config async def callback(self, interaction: Interaction): + vote_message_str = build_votes_str(self.config) + with Session(engine) as session: + stmt = select(ElectionInformation.CouncilVoteMsgId).where( + ElectionCycle == get_election_cycle_name(self.config) + ) + vote_msg_id = session.scalar(stmt) + vote_msg = await interaction.channel.fetch_message(vote_msg_id) + await vote_msg.edit(content=vote_message_str) await interaction.edit(content=self.values[0], view=None) @@ -44,8 +43,10 @@ class VoteView(nextcord.ui.View): The button to press to start your nomination. """ - def __init__(self): + def __init__(self, config: DakinyConfig, guild: nextcord.Guild): super().__init__(timeout=None) + self.config = config + self.guild = guild @nextcord.ui.button(label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote") async def vote(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None: @@ -57,6 +58,14 @@ class VoteView(nextcord.ui.View): interaction (nextcord.Interaction): Interaction of user who pressed the button """ + # TODO get the clan members to vote for. Check if user has voted and add a checkmark to that name. + options = [SelectOption(label="one"), SelectOption(label="two"), SelectOption(label="three")] + + view = nextcord.ui.View() + view.add_item(VoteSelect(options=options, config=self.config)) + await interaction.response.send_message( - content="Please vote for a clan member!", view=VoteActionView(), ephemeral=True + content="Please vote for a clan member!", + view=view, + ephemeral=True, ) diff --git a/dakiny_bot.py b/dakiny_bot.py index 8347da1..0c0cc53 100644 --- a/dakiny_bot.py +++ b/dakiny_bot.py @@ -52,7 +52,7 @@ def main(cfg: DakinyConfig): assert bot.user is not None create_database # pylint: disable=pointless-statement bot.add_view(NominateView(cfg)) - bot.add_view(VoteView()) + bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id))) print(f"{bot.user.name} has connected to Discord!") # Run Discord bot From 81e2885e8dac3ee810624a6c71c1b23fde1a2ae5 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 11:56:47 -0400 Subject: [PATCH 18/25] Change newlines in votes string. --- utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/utils.py b/utils.py index 2b11006..b3a9f23 100644 --- a/utils.py +++ b/utils.py @@ -94,10 +94,9 @@ def build_votes_str(config: DakinyConfig) -> str: votes_str: str = "====================" for user_id, num_votes in votes.items(): - votes_str += f"<@{user_id}>: " + votes_str += f"\n<@{user_id}>: " for num in range(num_votes): votes_str += "☑️" - votes_str += "\n" return votes_str From dcea91156470fc905358933ea93cfe64f85a6525 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 12:05:36 -0400 Subject: [PATCH 19/25] Fix bug getting ElectionInformation object correctly. --- classes/election_cycle.py | 8 +++++--- classes/voting_views.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 210c2f3..2688e19 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -183,11 +183,13 @@ class ElectionCycle: message = self.build_vote_start() clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id) election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel) - await election_channel.send(message, view=VoteView(self.config, self.guild)) - vote_msg = await election_channel.send(build_votes_str(self.config)) + await election_channel.send(message) + vote_msg = await election_channel.send(build_votes_str(self.config), view=VoteView(self.config, self.guild)) with Session(engine) as session: - stmt = select(ElectionInformation).where(ElectionCycle == get_election_cycle_name(self.config)) + stmt = select(ElectionInformation).where( + ElectionInformation.ElectionCycle == get_election_cycle_name(self.config) + ) info = session.scalar(stmt) info.CouncilVoteMsgId = vote_msg.id session.commit() diff --git a/classes/voting_views.py b/classes/voting_views.py index 9c34966..542eb95 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -30,7 +30,7 @@ class VoteSelect(nextcord.ui.StringSelect): vote_message_str = build_votes_str(self.config) with Session(engine) as session: stmt = select(ElectionInformation.CouncilVoteMsgId).where( - ElectionCycle == get_election_cycle_name(self.config) + ElectionInformation.ElectionCycle == get_election_cycle_name(self.config) ) vote_msg_id = session.scalar(stmt) vote_msg = await interaction.channel.fetch_message(vote_msg_id) From 181b1da879267c39177bc877fe0fbc30015ef464 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 13:24:34 -0400 Subject: [PATCH 20/25] Don't need voting emote anymore. --- bot_messages/vote_start.txt | 2 +- classes/election_cycle.py | 1 - config.py | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/bot_messages/vote_start.txt b/bot_messages/vote_start.txt index f67865e..afd1b0d 100644 --- a/bot_messages/vote_start.txt +++ b/bot_messages/vote_start.txt @@ -1,7 +1,7 @@ ==================== <@&{clan_member_id}>, it's time to vote for clan council. -React with <{voting_emote_id}> to vote. +Press the Vote button to vote! Voting ends next {voting_end_day}, {voting_end}. Results will then be announced. diff --git a/classes/election_cycle.py b/classes/election_cycle.py index 2688e19..bc50221 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -124,7 +124,6 @@ class ElectionCycle: return message.format( clan_member_id=self.config.params.clan_member_role_id, - voting_emote_id=self.config.params.voting_emote_id, voting_end=vote_end.strftime("%B %d"), voting_end_day=vote_end.strftime("%A"), ) diff --git a/config.py b/config.py index 084c72e..1dfc500 100644 --- a/config.py +++ b/config.py @@ -22,7 +22,6 @@ class Params: general_channel_id: int guild_id: int high_council_role_id: int - voting_emote_id: str @dataclass From 289b752678bc20d41d6f15ddc22b69548796a199 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 13:33:12 -0400 Subject: [PATCH 21/25] Voting interacts with database. --- classes/voting_views.py | 48 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/classes/voting_views.py b/classes/voting_views.py index 542eb95..86afc4f 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -27,15 +27,34 @@ class VoteSelect(nextcord.ui.StringSelect): self.config = config async def callback(self, interaction: Interaction): - vote_message_str = build_votes_str(self.config) + election_name = get_election_cycle_name(self.config) with Session(engine) as session: stmt = select(ElectionInformation.CouncilVoteMsgId).where( - ElectionInformation.ElectionCycle == get_election_cycle_name(self.config) + ElectionInformation.ElectionCycle == election_name ) vote_msg_id = session.scalar(stmt) + 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 = ( + select(Votes) + .where(Votes.ElectionCycle == election_name) + .where(Votes.VoterId == interaction.user.id) + ) + vote = session.scalar(stmt) + vote.VotedForId = voted_for + session.commit() + + vote_message_str = build_votes_str(self.config) vote_msg = await interaction.channel.fetch_message(vote_msg_id) await vote_msg.edit(content=vote_message_str) - await interaction.edit(content=self.values[0], view=None) + await interaction.edit(content=f"You have voted for <@{self.values[0]}>", view=None) class VoteView(nextcord.ui.View): @@ -58,8 +77,27 @@ class VoteView(nextcord.ui.View): interaction (nextcord.Interaction): Interaction of user who pressed the button """ - # TODO get the clan members to vote for. Check if user has voted and add a checkmark to that name. - options = [SelectOption(label="one"), SelectOption(label="two"), SelectOption(label="three")] + election: str = get_election_cycle_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 == election) + + options = [ + SelectOption( + label=f"{self.guild.get_member(nominee).name} ☑️" + if voted_for == nominee + else f"{self.guild.get_member(nominee).name}", + value=str(nominee), + ) + for nominee in session.scalars(stmt) + ] view = nextcord.ui.View() view.add_item(VoteSelect(options=options, config=self.config)) From 80c4f053bc17abad1a6cac3d1b1c1f18362df19f Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 13:38:23 -0400 Subject: [PATCH 22/25] Add abstaining from voting. --- classes/voting_views.py | 21 ++++++++++++++++----- utils.py | 3 ++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/classes/voting_views.py b/classes/voting_views.py index 86afc4f..5c86712 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -6,13 +6,12 @@ from typing import List import nextcord from nextcord import SelectOption -from nextcord.ext import commands from nextcord.interactions import Interaction -from sqlalchemy import select +from sqlalchemy import exc, select from sqlalchemy.orm import Session -from config import DakinyConfig, ElectionCycle -from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, engine +from config import DakinyConfig +from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine from utils import build_votes_str, get_election_cycle_name @@ -33,7 +32,12 @@ class VoteSelect(nextcord.ui.StringSelect): ElectionInformation.ElectionCycle == election_name ) 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: @@ -54,7 +58,13 @@ class VoteSelect(nextcord.ui.StringSelect): vote_message_str = build_votes_str(self.config) vote_msg = await interaction.channel.fetch_message(vote_msg_id) await vote_msg.edit(content=vote_message_str) - await interaction.edit(content=f"You have voted for <@{self.values[0]}>", view=None) + + 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 VoteView(nextcord.ui.View): @@ -98,6 +108,7 @@ class VoteView(nextcord.ui.View): ) 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)) diff --git a/utils.py b/utils.py index b3a9f23..8f2774b 100644 --- a/utils.py +++ b/utils.py @@ -90,7 +90,8 @@ def build_votes_str(config: DakinyConfig) -> str: stmt = select(Votes).where(Votes.ElectionCycle == get_election_cycle_name(config)) for vote in session.scalars(stmt): - votes[vote.VotedForId] += 1 + if vote.VotedForId is not None: + votes[vote.VotedForId] += 1 votes_str: str = "====================" for user_id, num_votes in votes.items(): From 5803c336c07233d2aae76cdebd6caed06274ea9b Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 13:39:42 -0400 Subject: [PATCH 23/25] Display nickname while voting. --- classes/voting_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/classes/voting_views.py b/classes/voting_views.py index 5c86712..85e6a24 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -101,9 +101,9 @@ class VoteView(nextcord.ui.View): options = [ SelectOption( - label=f"{self.guild.get_member(nominee).name} ☑️" + label=f"{self.guild.get_member(nominee).nick} ☑️" if voted_for == nominee - else f"{self.guild.get_member(nominee).name}", + else f"{self.guild.get_member(nominee).nick}", value=str(nominee), ) for nominee in session.scalars(stmt) From 66e7159022693a83913ddd81f1bb6ed7ae760ae8 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 14:27:21 -0400 Subject: [PATCH 24/25] Enforce having clan member role for voting. --- classes/voting_views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/classes/voting_views.py b/classes/voting_views.py index 85e6a24..7933591 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -87,6 +87,13 @@ class VoteView(nextcord.ui.View): interaction (nextcord.Interaction): Interaction of user who pressed the button """ + # enforce members only + if self.guild.get_role(self.config.params.clan_member_role_id) not in interaction.user.roles: + await interaction.response.send_message( + f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.", ephemeral=True + ) + return + election: str = get_election_cycle_name(self.config) with Session(engine) as session: From 3fb65a7b0a87c35624f8ea9b3f2df302224eae60 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Fri, 3 Nov 2023 15:06:52 -0400 Subject: [PATCH 25/25] Process voting end. Give the elect role and set elected in database. --- classes/election_cycle.py | 35 ++++++++++++++++++++++++++++++++++- classes/voting_views.py | 2 +- cogs/commands/test.py | 23 +++++++++++++++++++++-- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/classes/election_cycle.py b/classes/election_cycle.py index bc50221..a7c02ac 100644 --- a/classes/election_cycle.py +++ b/classes/election_cycle.py @@ -2,6 +2,7 @@ Contains the election cycle class """ +import collections from datetime import datetime import nextcord @@ -11,7 +12,7 @@ from sqlalchemy.orm import Session from classes.nominate_views import NominateView from classes.voting_views import VoteView from config import DakinyConfig -from database_setup.sql_data_classes import ElectionInformation, engine +from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine from utils import build_votes_str, get_datetime, get_election_cycle_name @@ -199,10 +200,42 @@ class ElectionCycle: """ Performs required actions for when voting time has ended. """ + message = self.build_highcouncil_election_announce() council_elect_role: nextcord.Role = self.guild.get_role(self.config.params.clan_council_elect_role_id) election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel) clan_member_role: nextcord.Role = self.guild.get_role(self.config.params.clan_member_role_id) + + # remove vote button from vote message + election_name = get_election_cycle_name(self.config) + with Session(engine) as session: + stmt = select(ElectionInformation.CouncilVoteMsgId).where( + ElectionInformation.ElectionCycle == election_name + ) + vote_msg_id = session.scalar(stmt) + vote_msg: nextcord.Message = await election_channel.fetch_message(vote_msg_id) + await vote_msg.edit(view=None) + + # add the highcouncil role to everyone with 3 or more votes. set elected to true in database + with Session(engine) as session: + stmt = select(Votes.VotedForId).where(Votes.ElectionCycle == election_name) + vote_count = collections.Counter(session.scalars(stmt)) + + for user_id, votes in vote_count.items(): + if votes >= 3: + stmt = ( + select(CouncilMembers) + .where(CouncilMembers.ElectionCycle == election_name) + .where(CouncilMembers.MemberId == user_id) + ) + council_member = session.scalar(stmt) + council_member.Elected = True + session.commit() + + user: nextcord.Member = self.guild.get_member(user_id) + + await user.add_roles(council_elect_role) + await election_channel.send(message) await election_channel.set_permissions(council_elect_role, send_messages=True, add_reactions=True) await election_channel.set_permissions(clan_member_role, add_reactions=False) diff --git a/classes/voting_views.py b/classes/voting_views.py index 7933591..6444554 100644 --- a/classes/voting_views.py +++ b/classes/voting_views.py @@ -56,7 +56,7 @@ class VoteSelect(nextcord.ui.StringSelect): session.commit() vote_message_str = build_votes_str(self.config) - vote_msg = await interaction.channel.fetch_message(vote_msg_id) + 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: diff --git a/cogs/commands/test.py b/cogs/commands/test.py index ae73956..6b3a863 100644 --- a/cogs/commands/test.py +++ b/cogs/commands/test.py @@ -3,7 +3,7 @@ Commands for testing. """ import nextcord -from nextcord.ext import commands +from nextcord.ext import application_checks, commands from classes.election_cycle import ElectionCycle from config import DakinyConfig @@ -21,7 +21,14 @@ class TestCog(commands.Cog, name="Test commands."): """ self.bot = bot - @nextcord.slash_command(default_member_permissions=nextcord.Permissions(administrator=True)) + def is_me(): + def predicate(interaction: nextcord.Interaction): + return interaction.user.id == 206602713511886849 + + return application_checks.check(predicate) + + @nextcord.slash_command() + @is_me() async def test(self, interaction: nextcord.Interaction): """ Parent slash command for help. Never called. @@ -68,3 +75,15 @@ class TestCog(commands.Cog, name="Test commands."): config: DakinyConfig = self.bot.config cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id)) await cycle.voting_start_action() + + @test.subcommand() + async def voting_end(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.voting_end_action()