"""Contains various views to be used while voting. Controls the logic when performing the vote.""" import nextcord from config import DakinyConfig from database_setup.sql_data_classes import ( CouncilMembers, ElectionInformation, Votes, engine, ) from nextcord import SelectOption from nextcord.interactions import Interaction from sqlalchemy import exc, select, update from sqlalchemy.orm import Session from utils import VoteType, build_votes_str, get_election_name class VoteSelect(nextcord.ui.StringSelect): """Vote view string select.""" def __init__(self, options: list["SelectOption"], config: DakinyConfig) -> None: super().__init__( placeholder="Choose 1", min_values=1, max_values=1, options=options, ) self.config = config async def callback(self, interaction: Interaction) -> None: """ Callback override for the StringSelect. Submits the vote to database if a user is selected. Handles adding vs editing a vote. Returns message informing user that vote has been recorded. Args: interaction (Interaction): Interaction with this callback. """ election_name = get_election_name(self.config) with Session(engine) as session: stmt = select(ElectionInformation.CouncilVoteMsgId).where(ElectionInformation.ElectionName == election_name) vote_msg_id = session.scalar(stmt) voted_for = None if self.values[0] == "Abstain" else int(self.values[0]) new_vote = Votes( ElectionName=election_name, VoterId=interaction.user.id, VotedForId=voted_for, Type=VoteType.COUNCIL, ) try: session.add(new_vote) session.commit() except exc.IntegrityError: session.rollback() stmt = ( update(Votes) .where(Votes.ElectionName == election_name) .where(Votes.VoterId == interaction.user.id) .where(Votes.Type == VoteType.COUNCIL) .values(VotedForId=voted_for) ) session.execute(stmt) session.commit() vote_message_str = build_votes_str(self.config, interaction.guild) vote_msg: nextcord.Message = await interaction.channel.fetch_message(vote_msg_id) await vote_msg.edit(content=vote_message_str) msg = f"You have voted for <@{voted_for}>" if voted_for is not None else "You have abstained from voting." await interaction.edit(content=msg, view=None) class VoteView(nextcord.ui.View): """The button to press to start your nomination.""" 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: # noqa: ARG002 """ Enforces only members vote. Gets election cycle, creates voting options (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 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_name: str = get_election_name(self.config) with Session(engine) as session: stmt = ( select(Votes.VotedForId) .where(Votes.ElectionName == election_name) .where(Votes.VoterId == interaction.user.id) .where(Votes.Type == VoteType.COUNCIL) ) voted_for = session.scalar(stmt) stmt = select(CouncilMembers.MemberId).where(CouncilMembers.ElectionName == election_name) 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 sorted( session.scalars(stmt), key=lambda nominee: self.guild.get_member(nominee).display_name.lower(), ) ] 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)) await interaction.response.send_message( content="Please vote for a clan member!", view=view, ephemeral=True, )