Files
dakiny-bot/dakiny_bot/classes/voting_highcouncil_views.py
T
imcompiling 354823ca1f
build_develop / build (push) Successful in 40s
Feat: use models in voting highcouncil views.
2025-07-31 13:48:11 -04:00

250 lines
9.4 KiB
Python

"""Contains various views to be used while voting for high council. 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_highcouncil_votes_str, get_election_name
class VoteSelect(nextcord.ui.StringSelect):
"""Vote view string select."""
def __init__(self, options: list["SelectOption"], election_type: str) -> None:
super().__init__(
placeholder="Choose 1",
min_values=1,
max_values=1,
options=options,
)
self.election_type = election_type
async def callback(self, interaction: Interaction) -> None:
"""
Callback override for the StringSelect.
Submits the High Council 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: str = get_election_name()
with Session(engine) as session:
stmt = select(col(ElectionInformation.high_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 Highcouncil 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=self.election_type,
)
try:
session.add(new_vote)
session.commit()
except exc.IntegrityError:
# user already has a vote in the election and election type
session.rollback()
stmt = (
update(Vote)
.where(col(Vote.election_name == election_name))
.where(col(Vote.voter_id == interaction.user.id))
.where(col(Vote.type == self.election_type))
.values(VotedForId=voted_for)
)
session.execute(stmt)
session.commit()
vote_message_str = build_highcouncil_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 VoteHighcouncilView(nextcord.ui.View):
"""The button to press to start voting for HighCouncil."""
def __init__(self, guild: nextcord.Guild):
super().__init__(timeout=None)
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: # noqa: ARG002
"""
Button to vote for Clan Leader.
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
"""
if not isinstance(interaction.user, nextcord.Member):
# the user should only be a member of the guild.
return
# enforce council elect only
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.clan_council_elect}> 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.CLANLEADER))
)
voted_for = session.scalar(stmt)
stmt = (
select(col(CouncilMember.member_id))
.where(col(CouncilMember.election_name == election_name))
.filter(col(CouncilMember.clan_leader_speech_id is not None))
)
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=options, election_type=VoteType.CLANLEADER))
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: # noqa: ARG002
"""
Button to vote for Discord Manager.
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
"""
if not isinstance(interaction.user, nextcord.Member):
# the user should only be a member of the guild.
return
# enforce council elect only
if self.guild.get_role(settings.role_ids.clan_council_elect) not in interaction.user.roles:
await interaction.response.send_message(
f"You must have the <@&{settings.role_ids.clan_council_elect}> 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.DISCORDMANAGER))
)
voted_for = session.scalar(stmt)
stmt = (
select(col(CouncilMember.member_id))
.where(col(CouncilMember.election_name == election_name))
.filter(col(CouncilMember.discord_manager_speech_id is not None))
)
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=options,
election_type=VoteType.DISCORDMANAGER,
)
)
await interaction.response.send_message(
content="Please vote for a Discord Manager!",
view=view,
ephemeral=True,
)