From a7eafdd52aad935cf0621f7d138d0d81e74a7920 Mon Sep 17 00:00:00 2001 From: imcompiling Date: Sat, 16 Dec 2023 23:01:51 -0500 Subject: [PATCH] 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