Compare commits

..
Author SHA1 Message Date
imcompiling 1c8e8c4d5a Merge branch 'main' into mock-election
build_develop / build (push) Successful in 1m29s
2023-11-07 14:44:40 -05:00
imcompiling 0b5423a62b Merge branch 'main' into mock-election
build_develop / build (push) Successful in 1m22s
2023-11-06 19:47:39 -05:00
imcompiling dbbb7c1c62 Change votes to 2 for mock test
build_develop / build (push) Successful in 1m26s
2023-11-04 09:20:19 -04:00
7 changed files with 104 additions and 267 deletions
+47 -129
View File
@@ -6,18 +6,13 @@ import collections
from datetime import datetime, timedelta
import nextcord
from sqlalchemy import select, true
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 database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine
from utils import build_votes_str, get_datetime, get_election_cycle_name
@@ -45,15 +40,9 @@ class ElectionCycle:
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.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
@@ -66,15 +55,11 @@ class ElectionCycle:
long.
"""
with open(
"bot_messages/election_announcement.txt", mode="r", encoding="utf8"
) as file:
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
self.config.election_cycles[(self.cycle + 1) % len(self.config.election_cycles)].election_start
)
message = message.format(
@@ -99,22 +84,14 @@ class ElectionCycle:
str: Message to send.
"""
with open(
"bot_messages/highcouncil_election.txt", mode="r", encoding="utf8"
) as file:
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"
),
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"),
)
@@ -126,9 +103,7 @@ class ElectionCycle:
str: Message to send.
"""
with open(
"bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8"
) as file:
with open("bot_messages/highcouncil_vote.txt", mode="r", encoding="utf8") as file:
message = file.read()
return message.format(
@@ -146,9 +121,7 @@ class ElectionCycle:
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
)
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,
@@ -167,11 +140,7 @@ class ElectionCycle:
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()
):
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()
@@ -185,21 +154,15 @@ class ElectionCycle:
Performs required actions for when the election cycle starts.
"""
# create new channel for election
parent: nextcord.TextChannel = self.guild.get_channel(
self.config.params.voting_parent_id
)
category: nextcord.CategoryChannel = self.guild.get_channel(self.config.params.active_category_id)
quarter_name: str = get_election_cycle_name(self.config)
thread_msg: nextcord.Message = await parent.send(content=quarter_name)
# create new entry into database.
thread: nextcord.Thread = await parent.create_thread(
name=quarter_name, message=thread_msg
)
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, ThreadId=thread.id
)
new_cycle = ElectionInformation(ElectionCycle=quarter_name, ElectionChannel=election_channel.id)
session.add(new_cycle)
session.commit()
@@ -210,9 +173,9 @@ class ElectionCycle:
for index, message in enumerate(messages):
# if last item, add the nominate button
if index + 1 == len(messages):
await thread.send(message, view=NominateView(self.config, self.guild))
await election_channel.send(message, view=NominateView(self.config))
else:
await thread.send(message)
await election_channel.send(message)
async def voting_start_action(self) -> None:
"""
@@ -220,35 +183,30 @@ class ElectionCycle:
Removes write permission from the general clan member role.
"""
message = self.build_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
await election_thread.send(message)
vote_msg = await election_thread.send(
build_votes_str(self.config), view=VoteView(self.config, self.guild)
)
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)
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_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
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)
@@ -257,7 +215,7 @@ class ElectionCycle:
ElectionInformation.ElectionCycle == election_name
)
vote_msg_id = session.scalar(stmt)
vote_msg: nextcord.Message = await election_thread.fetch_message(vote_msg_id)
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
@@ -266,7 +224,7 @@ class ElectionCycle:
vote_count = collections.Counter(session.scalars(stmt))
for user_id, votes in vote_count.items():
if votes >= 3:
if votes >= 2:
stmt = (
select(CouncilMembers)
.where(CouncilMembers.ElectionCycle == election_name)
@@ -276,14 +234,13 @@ class ElectionCycle:
council_member.Elected = True
session.commit()
member: nextcord.Member = self.guild.get_member(user_id)
user: nextcord.Member = self.guild.get_member(user_id)
await member.add_roles(
council_elect_role,
reason=f"{member.display_name} has won a nomination and become a Council Elect.",
)
await user.add_roles(council_elect_role)
await election_thread.send(message)
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:
"""
@@ -291,55 +248,19 @@ class ElectionCycle:
"""
message = self.build_highcouncil_vote_start()
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
await election_thread.send(message)
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:
"""
Marks the election as over.
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)
elect_role: nextcord.Role = self.guild.get_role(
self.config.params.clan_council_elect_role_id
)
council_role: nextcord.Role = self.guild.get_role(
self.config.params.clan_council_role_id
)
# role management
with Session(engine) as session:
stmt = (
select(CouncilMembers.MemberId)
.where(
CouncilMembers.ElectionCycle == get_election_cycle_name(self.config)
)
.where(CouncilMembers.Elected == true())
)
for member_id in session.scalars(stmt):
member = self.guild.get_member(member_id)
await member.remove_roles(
elect_role,
reason=f"{member.display_name} no longer needs the elect role.",
)
await member.add_roles(
council_role,
reason=f"{member.display_name} has been elected to council.",
)
election_thread: nextcord.Thread = self.guild.get_thread(
self.get_election_info().ThreadId
)
await election_thread.send(
"This election cycle is now over. Thank you everyone for participating!"
)
await election_thread.edit(archived=True, locked=True)
await election_channel.move(beginning=True, category=archive)
def get_election_info(self) -> ElectionInformation:
"""
@@ -351,8 +272,7 @@ class ElectionCycle:
with Session(engine) as session:
stmt = select(ElectionInformation).where(
ElectionInformation.ElectionCycle
== get_election_cycle_name(self.config)
ElectionInformation.ElectionCycle == get_election_cycle_name(self.config)
)
return session.scalar(stmt)
@@ -361,11 +281,9 @@ class ElectionCycle:
Sends a message to general chat reminder clan about election.
"""
general: nextcord.TextChannel = self.guild.get_channel(
self.config.params.general_channel_id
)
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().ThreadId}> to cast your vote!"
message += f"to <#{self.get_election_info().ElectionChannel}> to cast your vote!"
await general.send(content=message)
+7 -37
View File
@@ -19,13 +19,7 @@ class NominateModal(nextcord.ui.Modal):
Modal for nominating oneself. User writes a speech and then the callback sends the speech into the correct channel.
"""
def __init__(
self,
config: DakinyConfig,
usr_id: int,
usr_nominated: bool,
speech_content: str,
):
def __init__(self, config: DakinyConfig, usr_id: int, usr_nominated: bool, speech_content: str):
super().__init__(
"Nomination",
timeout=5 * 60, # 5 minutes
@@ -76,12 +70,8 @@ class NominateModal(nextcord.ui.Modal):
)
obj = session.scalar(stmt)
msg: nextcord.Message = await interaction.channel.fetch_message(
obj.SpeechMsgId
)
await msg.edit(
content=decorate_nomination(interaction.user.id, self.speech.value)
)
msg: nextcord.Message = await interaction.channel.fetch_message(obj.SpeechMsgId)
await msg.edit(content=decorate_nomination(interaction.user.id, self.speech.value))
class NominateView(nextcord.ui.View):
@@ -89,19 +79,12 @@ class NominateView(nextcord.ui.View):
The button to press to start your nomination.
"""
def __init__(self, config: DakinyConfig, guild: nextcord.Guild):
def __init__(self, config):
super().__init__(timeout=None)
self.config: DakinyConfig = config
self.guild: nextcord.Guild = guild
@nextcord.ui.button(
label="Nominate Yourself",
style=nextcord.ButtonStyle.primary,
custom_id="nominate",
)
async def nominate(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
) -> None:
@nextcord.ui.button(label="Nominate Yourself", style=nextcord.ButtonStyle.primary, custom_id="nominate")
async def nominate(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
"""
Gathers previous information about the user if they have already nominated themselves, then launches
the nomination modal to get their speech.
@@ -111,17 +94,6 @@ class NominateView(nextcord.ui.View):
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
cycle = get_election_cycle_name(self.config)
usr_nominated = False
speech_content = ""
@@ -138,9 +110,7 @@ class NominateView(nextcord.ui.View):
speech_content = msg.content.split(NOMINEE_SPLITTER)[1]
await interaction.response.send_modal(
NominateModal(
self.config, interaction.user.id, usr_nominated, speech_content
)
NominateModal(self.config, interaction.user.id, usr_nominated, speech_content)
)
+16 -43
View File
@@ -7,16 +7,11 @@ from typing import List
import nextcord
from nextcord import SelectOption
from nextcord.interactions import Interaction
from sqlalchemy import exc, select, update
from sqlalchemy import exc, select
from sqlalchemy.orm import Session
from config import DakinyConfig
from database_setup.sql_data_classes import (
CouncilMembers,
ElectionInformation,
Votes,
engine,
)
from database_setup.sql_data_classes import CouncilMembers, ElectionInformation, Votes, engine
from utils import build_votes_str, get_election_cycle_name
@@ -43,11 +38,7 @@ class VoteSelect(nextcord.ui.StringSelect):
else:
voted_for = int(self.values[0])
new_vote = Votes(
ElectionCycle=election_name,
VoterId=interaction.user.id,
VotedForId=voted_for,
)
new_vote = Votes(ElectionCycle=election_name, VoterId=interaction.user.id, VotedForId=voted_for)
try:
session.add(new_vote)
@@ -56,18 +47,16 @@ class VoteSelect(nextcord.ui.StringSelect):
except exc.IntegrityError:
session.rollback()
stmt = (
update(Votes)
select(Votes)
.where(Votes.ElectionCycle == election_name)
.where(Votes.VoterId == interaction.user.id)
.values(VotedForId=voted_for)
)
session.execute(stmt)
vote = session.scalar(stmt)
vote.VotedForId = voted_for
session.commit()
vote_message_str = build_votes_str(self.config)
vote_msg: nextcord.Message = await interaction.channel.fetch_message(
vote_msg_id
)
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:
@@ -88,15 +77,10 @@ class VoteView(nextcord.ui.View):
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:
@nextcord.ui.button(label="Vote", style=nextcord.ButtonStyle.primary, custom_id="vote")
async def vote(self, button: nextcord.ui.Button, interaction: nextcord.Interaction) -> None:
"""
Enforces only members vote. Gets election cycle, creates voting options (checking if user has voted already),
then sends the voting view.
TODO
Args:
button (nextcord.ui.Button): Unused. idk why its needed in the decorator
@@ -104,13 +88,9 @@ class VoteView(nextcord.ui.View):
"""
# enforce members only
if (
self.guild.get_role(self.config.params.clan_member_role_id)
not in interaction.user.roles
):
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,
f"You must have the <@&{self.config.params.clan_member_role_id}> role to vote.", ephemeral=True
)
return
@@ -124,25 +104,18 @@ class VoteView(nextcord.ui.View):
)
voted_for = session.scalar(stmt)
stmt = select(CouncilMembers.MemberId).where(
CouncilMembers.ElectionCycle == election
)
stmt = select(CouncilMembers.MemberId).where(CouncilMembers.ElectionCycle == election)
options = [
SelectOption(
label=f"{self.guild.get_member(nominee).display_name} ☑️"
label=f"{self.guild.get_member(nominee).nick} ☑️"
if voted_for == nominee
else f"{self.guild.get_member(nominee).display_name}",
else f"{self.guild.get_member(nominee).nick}",
value=str(nominee),
)
for nominee in session.scalars(stmt)
]
options.append(
SelectOption(
label="Abstain ☑️" if voted_for is None else "Abstain",
value="Abstain",
)
)
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))
+29 -52
View File
@@ -10,17 +10,6 @@ from config import DakinyConfig
from utils import get_current_cycle
def is_me():
"""
predicate that makes sure only I can run the test commands.
"""
def predicate(interaction: nextcord.Interaction):
return interaction.user.id == 206602713511886849
return application_checks.check(predicate)
class TestCog(commands.Cog, name="Test commands."):
"""
Class holds commands for testing.
@@ -32,14 +21,37 @@ class TestCog(commands.Cog, name="Test commands."):
"""
self.bot = bot
def is_me():
def predicate(interaction: nextcord.Interaction):
return interaction.user.id == 206602713511886849
return application_checks.check(predicate)
@nextcord.slash_command()
@is_me()
async def test(self, interaction: nextcord.Interaction):
"""
Parent slash command for help. Never called.
"""
@test.subcommand()
@is_me()
async def msg_builder(self, interaction: nextcord.Interaction):
"""
Tests the message builder.
Args:
interaction (nextcord.Interaction): Interaction that invoked the command.
"""
cycle = ElectionCycle(0, self.bot)
message = cycle.build_election_announcement()
# doesn't tell discord that the message was responded to. Is a test command so I don't really care atm.
for m in message:
await interaction.channel.send(content=m)
@test.subcommand()
async def election_start(self, interaction: nextcord.Interaction):
"""
idc
@@ -49,15 +61,10 @@ class TestCog(commands.Cog, name="Test commands."):
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
await cycle.election_start_action()
@test.subcommand()
@is_me()
async def voting_start(self, interaction: nextcord.Interaction):
"""
tests the voting start action
@@ -66,15 +73,10 @@ class TestCog(commands.Cog, name="Test commands."):
interaction (nextcord.Interaction): _description_
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
await cycle.voting_start_action()
@test.subcommand()
@is_me()
async def voting_end(self, interaction: nextcord.Interaction):
"""
tests the voting start action
@@ -83,42 +85,17 @@ class TestCog(commands.Cog, name="Test commands."):
interaction (nextcord.Interaction): _description_
"""
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
await cycle.voting_end_action()
@test.subcommand()
@is_me()
async def election_reminder(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
await cycle.election_reminder()
@test.subcommand()
@is_me()
async def daily(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
cycle = ElectionCycle(get_current_cycle(config), config, self.bot.get_guild(config.params.guild_id))
await cycle.check_date()
@test.subcommand()
@is_me()
async def term_start(self, interaction: nextcord.Interaction):
config: DakinyConfig = self.bot.config
cycle = ElectionCycle(
get_current_cycle(config),
config,
self.bot.get_guild(config.params.guild_id),
)
await cycle.term_start_action()
+2 -1
View File
@@ -10,8 +10,10 @@ class Params:
Class defines the various configuration parameters.
"""
active_category_id: int
admin_channel_id: int # might want to rename this...
announcement_category_id: int
archive_channel_group_id: int
clan_council_elect_role_id: int
clan_council_role_id: int
clan_info_channel_id: int
@@ -20,7 +22,6 @@ class Params:
general_channel_id: int
guild_id: int
high_council_role_id: int
voting_parent_id: int
@dataclass
+2 -4
View File
@@ -20,9 +20,7 @@ birthday_list: list[int] = []
logger = logging.getLogger("nextcord")
logger.setLevel(logging.DEBUG)
handler = logging.FileHandler(filename="nextcord.log", encoding="utf-8", mode="w")
handler.setFormatter(
logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s")
)
handler.setFormatter(logging.Formatter("%(asctime)s:%(levelname)s:%(name)s: %(message)s"))
logger.addHandler(handler)
@@ -53,7 +51,7 @@ def main(cfg: DakinyConfig):
"""When Discord is connected"""
assert bot.user is not None
create_database # pylint: disable=pointless-statement
bot.add_view(NominateView(cfg, bot.get_guild(cfg.params.guild_id)))
bot.add_view(NominateView(cfg))
bot.add_view(VoteView(cfg, bot.get_guild(cfg.params.guild_id)))
print(f"{bot.user.name} has connected to Discord!")
+1 -1
View File
@@ -1,6 +1,6 @@
CREATE TABLE ElectionInformation(
ElectionCycle varchar(30) not null unique,
ThreadId bigint unsigned not null,
ElectionChannel bigint unsigned not null,
CouncilVoteMsgId bigint unsigned,
PRIMARY KEY (ElectionCycle)
);