""" Contains the election cycle class """ import collections from datetime import datetime, timedelta import nextcord 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 build_votes_str, get_datetime, get_election_cycle_name class ElectionCycle: """ This class represents all the dates required for an election cycle along with all the functions needed to operate the cycle. """ def __init__(self, cycle: int, config: DakinyConfig, guild: nextcord.Guild) -> None: """ Generates all dates relevant to the election cycle determined by the config and cycle. Args: cycle (int): Which election cycle this cycle is. config (DakinyConfig): The entire config object. guild (nextcord.Guild): The guild that this election is running in. """ self.config: DakinyConfig = config self.cycle = cycle 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 def build_election_announcement(self) -> list[str]: """ Builds the messages to announce the election is starting. Returns: list[str]: A list of message contents in the correct order. Must be a string because the message is too long. """ with open("bot_messages/election_announcement.txt", mode="r", encoding="utf8") as file: message = file.read() next_election = get_datetime( self.config.election_cycles[(self.cycle + 1) % len(self.config.election_cycles)].election_start ) message = message.format( clan_council_id=self.config.params.clan_council_role_id, high_council_id=self.config.params.high_council_role_id, election_start=self.election_start.strftime("%B %d"), term_start=self.term_start.strftime("%B %d"), term_end=self.term_end.strftime("%B %d"), voting_start=self.voting_start.strftime("%B %d"), voting_end=self.voting_end.strftime("%B %d"), highcouncil_election=self.highcouncil_election_start.strftime("%B %d"), next_election_start=next_election.strftime("%B %d"), ) return message.split("---message_break---") def build_highcouncil_election_announce(self) -> str: """ Builds message to announce highcouncil election start. Returns: str: Message to send. """ with open("bot_messages/highcouncil_election.txt", mode="r", encoding="utf8") as file: message = file.read() return message.format( clan_council_elect_role_id=self.config.params.clan_council_elect_role_id, highcouncil_election_announce=self.highcouncil_election_announce.strftime("%B %d"), highcouncil_election_start=self.highcouncil_election_start.strftime("%B %d"), highcouncil_election_end=self.highcouncil_election_end.strftime("%B %d"), term_start=self.term_start.strftime("%B %d"), ) def build_highcouncil_vote_start(self) -> str: """ Builds the message to announce that highcouncil voting has started. Returns: str: Message to send. """ with open("bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8") as file: message = file.read() return message.format( clan_council_elect_role_id=self.config.params.clan_council_elect_role_id, term_start=self.term_start.strftime("%B %d"), ) def build_vote_start(self) -> str: """ Builds the voting start message out of template and configurations. Returns: str: Message to send """ with open("bot_messages/vote_start.txt", mode="r", encoding="utf8") as file: message = file.read() vote_end: datetime = get_datetime(self.config.election_cycles[self.cycle].voting_end) return message.format( clan_member_id=self.config.params.clan_member_role_id, voting_end=vote_end.strftime("%B %d"), voting_end_day=vote_end.strftime("%A"), ) async def check_date(self) -> None: """ Function checks current date to see if something needs to be run. """ today = datetime.today() # check all dates if action is needed if today.date() == self.election_start.date(): await self.election_start_action() elif today.date() == self.voting_start.date(): await self.voting_start_action() elif (self.voting_end - timedelta(3)).date() <= today.date() < self.voting_end.date(): await self.election_reminder() elif today.date() == self.voting_end.date(): await self.voting_end_action() elif today.date() == self.highcouncil_election_start.date(): await self.highcouncil_election_start_action() elif today.date() == self.term_start.date(): await self.term_start_action() async def election_start_action(self) -> None: """ Performs required actions for when the election cycle starts. """ # 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(self.config) # 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, add_reactions=True) with Session(engine) as session: new_cycle = ElectionInformation(ElectionCycle=quarter_name, ElectionChannel=election_channel.id) session.add(new_cycle) session.commit() # post message announcing election # 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 index, message in enumerate(messages): # if last item, add the nominate button if index + 1 == len(messages): await election_channel.send(message, view=NominateView(self.config)) else: await election_channel.send(message) async def voting_start_action(self) -> None: """ Sends message informing members that voting has started. Removes write permission from the general clan member role. """ 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) 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( ElectionInformation.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: """ 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) async def highcouncil_election_start_action(self) -> None: """ Performs required actions for when the highcouncil is elected. """ message = self.build_highcouncil_vote_start() election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel) await election_channel.send(message) async def term_start_action(self) -> None: """ Moves the election channel into the archive category. """ self.election_over = True election_channel: nextcord.TextChannel = self.guild.get_channel(self.get_election_info().ElectionChannel) archive: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.archive_channel_group_id) await election_channel.move(beginning=True, category=archive) def get_election_info(self) -> ElectionInformation: """ Gets information for current election. Returns: ElectionInformation: The information from the database about the current election. """ with Session(engine) as session: stmt = select(ElectionInformation).where( ElectionInformation.ElectionCycle == get_election_cycle_name(self.config) ) return session.scalar(stmt) async def election_reminder(self) -> None: """ Sends a message to general chat reminder clan about election. """ general: nextcord.TextChannel = self.guild.get_channel(self.config.params.general_channel_id) message = f"Reminder to <@&{self.config.params.clan_member_role_id}>! Clan elections are happening! Head over " message += f"to <#{self.get_election_info().ElectionChannel}> to cast your vote!" await general.send(content=message)