"""Contains various views to be used while voting. Controls the logic when performing the vote.""" import nextcord from nextcord import SelectOption from nextcord.interactions import Interaction from sqlalchemy import exc, select, update from sqlalchemy.orm import Session from sqlmodel import col from dakiny_bot.dakiny_settings import settings from dakiny_bot.database.db import engine from dakiny_bot.database.models import CouncilMember, ElectionInformation, Vote from dakiny_bot.logger import dakiny_logger from dakiny_bot.utils import VoteType, build_votes_str, get_election_name class VoteSelect(nextcord.ui.StringSelect): """Vote view string select.""" def __init__(self, options: list["SelectOption"]) -> None: super().__init__( placeholder="Choose 1", min_values=1, max_values=1, options=options, ) 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. """ if ( interaction.user is None or not isinstance(interaction.channel, nextcord.TextChannel) or interaction.guild is None ): # ignore none... Don't know what situation this is. # if not textchannel, don't know how callback happened # if no guild, is problem because should only be in guild. return election_name = get_election_name() with Session(engine) as session: stmt = select(col(ElectionInformation.council_vote_msg_id)).where( col(ElectionInformation.name == election_name) ) if (vote_msg_id := session.scalar(stmt)) is None: dakiny_logger.error("Unable to find a Vote Message ID in database.") return voted_for = None if self.values[0] == "Abstain" else int(self.values[0]) new_vote = Vote( election_name=election_name, voter_id=interaction.user.id, voted_for_id=voted_for, type=VoteType.COUNCIL, ) try: session.add(new_vote) session.commit() except exc.IntegrityError: session.rollback() stmt = ( update(Vote) .where(col(Vote.election_name == election_name)) .where(col(Vote.voter_id == interaction.user.id)) .where(col(Vote.type == VoteType.COUNCIL)) .values(VotedForId=voted_for) ) session.execute(stmt) session.commit() vote_message_str = build_votes_str(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, guild: nextcord.Guild): super().__init__(timeout=None) 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 """ if not isinstance(interaction.user, nextcord.Member): # the user should only be a member of the guild. return # enforce members only if self.guild.get_role(settings.role_ids.clan_member) not in interaction.user.roles: await interaction.response.send_message( f"You must have the <@&{settings.role_ids.clan_member}> role to vote.", ephemeral=True, ) return election_name: str = get_election_name() with Session(engine) as session: stmt = ( select(col(Vote.voted_for_id)) .where(col(Vote.election_name == election_name)) .where(col(Vote.voter_id == interaction.user.id)) .where(col(Vote.type == VoteType.COUNCIL)) ) voted_for = session.scalar(stmt) stmt = select(col(CouncilMember.member_id)).where(col(CouncilMember.election_name == election_name)) options = [ SelectOption( label=(f"{member.display_name} ☑️" if voted_for == nominee else f"{member.display_name}"), value=str(nominee), ) for nominee in sorted( session.scalars(stmt), key=lambda nominee: self.guild.get_member(nominee).display_name.lower(), # pyright: ignore[reportOptionalMemberAccess] ) if (member := self.guild.get_member(nominee)) is not None ] options.append( SelectOption( label="Abstain ☑️" if voted_for is None else "Abstain", value="Abstain", ) ) view = nextcord.ui.View() view.add_item(VoteSelect(options)) await interaction.response.send_message( content="Please vote for a clan member!", view=view, ephemeral=True, )