Convert bot to use a submissions table.

This commit is contained in:
2023-03-29 23:01:12 -04:00
parent 226d7c77d7
commit 5b078c4a56
5 changed files with 141 additions and 246 deletions
+15 -154
View File
@@ -2,6 +2,7 @@
Contains classes and enum for tracking data on submissions.
"""
from dataclasses import dataclass, field
from enum import Enum
from nextcord import Embed
@@ -10,8 +11,10 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error
from utilities.sql_data_classes import Category, CategoryGroup, engine
from utilities.utils import score_float_to_str
# TODO use the fancy enums
class SubmissionState(Enum):
"""
Enum for which state a submission is in
@@ -25,158 +28,23 @@ class SubmissionState(Enum):
VERIFY = 6
# TODO remake into a data class
@dataclass
class Submission:
"""
Class for an individual submission. Contains submission information such as
guild id, message id, table, category, username, score and submission state.
"""
def __init__(self, guild_id: int, group_id: int, username: str) -> None:
"""
Default constructor for Submission. Just need to know guild_id, table, and username.
Score and category are defaulted at empty, message id is 0, and submission state
is SubmissionState.CATEGORY.
"""
self._category_id: int = 0
self._guild_id: int = guild_id
self._message_id: int = 0
self._score: str = ""
self._submission_state: SubmissionState = SubmissionState.CATEGORY
self._group_id: int = group_id
self._username: str = username
category_id: int = field(init=False, default=0)
group_id: int
guild_id: int
is_time_record: bool = field(init=False, default=False)
message_id: int = field(init=False, default=0)
score: float = field(init=False, default=0.0)
submission_state: SubmissionState = field(init=False, default=SubmissionState.CATEGORY)
username: str
@property
def category_id(self) -> int:
"""
Getter for Submission's Category id.
Returns:
int: Category id.
"""
return self._category_id
@category_id.setter
def category_id(self, category: int) -> None:
"""
Setter for Submission's Category id.
Args:
category (int): Category id
"""
self._category_id = category
@property
def guild_id(self) -> int:
"""
Getter for guild id Submission is happening in. Will not change.
Returns:
int: guild id
"""
return self._guild_id
@property
def message_id(self) -> int:
"""
Getter for message id that Submission has a view on.
Returns:
int: message id
"""
return self._message_id
@message_id.setter
def message_id(self, m_id: int) -> None:
"""
Setter for message id that Submission has a view on.
Args:
m_id (int): message id
"""
self._message_id = m_id
@property
def score(self) -> str:
"""
Getter for score.
Returns:
str: score
"""
return self._score
@score.setter
def score(self, score: str) -> None:
"""
Setter for score.
Args:
score (str): score
"""
self._score = score
@property
def submission_state(self) -> SubmissionState:
"""
Getter for state Submission is in.
Returns:
SubmissionState: State the Submission is in
"""
return self._submission_state
@submission_state.setter
def submission_state(self, state: SubmissionState) -> None:
"""
Setter for state Submission is in.
Args:
state (SubmissionState): State to change to
"""
self._submission_state = state
@property
def group_id(self) -> str:
"""
Getter for group Submission is for.
Returns:
str: group id
"""
return self._group_id
@group_id.setter
def group_id(self, table: str) -> None:
"""
Getter for table Submission is for.
Args:
table (str): group id
"""
self._group_id = table
@property
def username(self) -> str:
"""
Getter for Submission owner's username.
Returns:
str: Submission owner's username
"""
return self._username
@username.setter
def username(self, username: str) -> None:
"""
Setter for Submission owner's username.
Args:
username (str): Submission owner's username
"""
self._username = username
def get_embeds(self) -> "list[Embed]":
def get_embed(self) -> Embed:
"""Creates an embed based off of the submission state and fields filled out."""
state: SubmissionState = self.submission_state
@@ -210,16 +78,9 @@ class Submission:
)
embed.add_field(name="Category", value=session.scalar(stmt), inline=False)
if self.score:
embed.add_field(name="Score", value=self.score, inline=False)
embed.add_field(name="Score", value=score_float_to_str(self.score, self.is_time_record), inline=False)
embeds = [embed]
if state is SubmissionState.VERIFY:
embed = Embed(title="Bot Information", description="Please disregard.")
embed.add_field(name="GroupId", value=self.group_id)
embed.add_field(name="CategoryId", value=self.category_id)
embeds.append(embed)
return embeds
return embed
class SubmissionMap:
+35 -52
View File
@@ -12,7 +12,8 @@ from sqlalchemy.orm import Session
# pylint: disable=import-error
from classes.submission import SubmissionState
from highscores import submission_objects
from utilities.sql_data_classes import Category, Guild, engine
from utilities.sql_data_classes import Guild, engine
from utilities.utils import score_str_to_float
class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -41,60 +42,42 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
await self.bot.get_guild(guild_id).get_channel(submission_channel_id).fetch_message(message_id)
)
# need check if correct user and correct channel
if message.channel.id == submission_channel_id:
state = submission.submission_state
if state == SubmissionState.SCORE and message.content:
# need check if correct user and correct channel
if message.channel.id == submission_channel_id:
state = submission.submission_state
if state == SubmissionState.SCORE and message.content:
# check with regex if score matches correctly
is_time_record = submission.is_time_record
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == submission.category_id)
# check with regex if score matches correctly
is_time_record = session.scalar(stmt)
time_regex = re.search("^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", message.content)
int_regex = re.search("^[0-9]+$", message.content)
if time_regex is None and is_time_record:
embeds = submission_message.embeds[0]
embeds.add_field(
name="Error",
value="Time record must follow format MM:SS.ms, SS.ms, or SS.",
)
elif int_regex is None and not is_time_record:
embeds = submission_message.embeds[0]
embeds.add_field(name="Error", value="Number record must only be number.")
else:
score = message.content
if is_time_record:
# standardize time score format.
if re.search("^[1-9]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# score is already fine.
pass
elif re.search("^[0-9]{2}(\\.[0-9])?$", score):
# need to add leading colon and 0
score = "0:" + score
elif re.search("^[0-9](\\.[0-9])?$", score):
# need to add 0 in seconds and leading colon and 0
score = "0:0" + score
elif re.search("^[0]?[0-9]:[0-9]{2}(\\.[0-9])?$", score):
# if there is a minute 10ths place 0, remove
score = score.lstrip("0")
submission.score = score
embeds = submission.get_embeds()
await submission_message.edit(
embeds=embeds,
)
elif state == SubmissionState.PROOF and len(message.attachments) > 0:
await submission_message.edit(
files=[await attch.to_file() for attch in message.attachments],
)
elif state == SubmissionState.NAME and message.content:
submission.username = message.content
embeds = submission.get_embeds()
await submission_message.edit(
embeds=embeds,
time_regex = re.search("^([0-9]{1,2}:[0-9]|[0-9])?[0-9](\\.[0-9])?$", message.content)
int_regex = re.search("^[0-9]+$", message.content)
if time_regex is None and is_time_record:
embed = submission_message.embeds[0]
embed.add_field(
name="Error",
value="Time record must follow format MM:SS.ms, SS.ms, or SS.",
)
elif int_regex is None and not is_time_record:
embed = submission_message.embeds[0]
embed.add_field(name="Error", value="Number record must only be number.")
else:
submission.score = score_str_to_float(message.content, is_time_record)
embed = submission.get_embed()
await submission_message.edit(
embed=embed,
)
elif state == SubmissionState.PROOF and len(message.attachments) > 0:
await submission_message.edit(
files=[await attch.to_file() for attch in message.attachments],
)
elif state == SubmissionState.NAME and message.content:
submission.username = message.content
embed = submission.get_embed()
await submission_message.edit(
embed=embed,
)
await message.delete()
await message.delete()
# This function will be called when this extension is loaded.
+30 -12
View File
@@ -10,7 +10,7 @@ from sqlalchemy.orm import Session, aliased
# pylint: disable=import-error
from classes.submission import Submission, SubmissionState
from highscores import submission_objects
from utilities.sql_data_classes import Category, CategoryGroup, Guild, engine
from utilities.sql_data_classes import Category, CategoryGroup, Guild, Submissions, engine
from .verification_view import VerificationView
@@ -30,7 +30,7 @@ def category_select_options(group_id: int) -> "list[nextcord.SelectOption]":
return options
############ Items used in submission process ############
# Items used in submission process ########################
class SubmissionDropdown(nextcord.ui.Select):
@@ -47,9 +47,14 @@ class SubmissionDropdown(nextcord.ui.Select):
async def callback(self, interaction: nextcord.Interaction):
submission = submission_objects.get_user_submission(interaction.user.id)
submission.category_id = self.values[0]
embeds = submission.get_embeds()
with Session(engine) as session:
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == submission.category_id)
submission.is_time_record = session.scalar(stmt)
embed = submission.get_embed()
await interaction.message.edit(
embeds=embeds,
embed=embed,
)
@@ -116,27 +121,40 @@ class SubmitButton(nextcord.ui.Button):
stmt = select(Guild.VerificationChannel).where(Guild.GuildId == interaction.guild_id)
verification_channel_id = session.scalar(stmt)
channel = interaction.guild.get_channel(verification_channel_id)
submission = submission_objects.get_user_submission(interaction.user.id)
active_submission = submission_objects.get_user_submission(interaction.user.id)
error_response = "Registered Verification channel does not exist or was never registered. \
Contact server admins."
assert channel is not None, await channel.send(error_response)
submission.submission_state = SubmissionState.VERIFY
active_submission.submission_state = SubmissionState.VERIFY
embeds = submission.get_embeds()
embed = active_submission.get_embed()
await channel.send(
verification_message = await channel.send(
files=[await attch.to_file() for attch in interaction.message.attachments],
view=VerificationView(),
embeds=embeds,
embed=embed,
content=f"Submission from <@{interaction.user.id}>.",
)
await interaction.message.edit(content=f"<@{interaction.user.id}> Submitted!", view=None)
new_submission = Submissions(
CategoryId=active_submission.category_id,
GroupId=active_submission.group_id,
IsTimeRecord=active_submission.is_time_record,
Score=active_submission.score,
SubmissionMessageId=interaction.message.id,
UserName=active_submission.username,
VerificationMessageId=verification_message.id,
)
submission_objects.remove_user_submission(interaction.user.id)
with Session(engine) as session:
session.add(new_submission)
session.commit()
############ Submission Views ############
# Submission Views ########################
class SubmissionCreateButton(nextcord.ui.View):
@@ -175,7 +193,7 @@ class SubmissionCreateButton(nextcord.ui.View):
submission_objects.add_submission(interaction.user.id, submission)
embeds = submission.get_embeds()
embed = submission.get_embed()
submission_message = await channel.send(
f"<@{interaction.user.id}>",
@@ -183,7 +201,7 @@ class SubmissionCreateButton(nextcord.ui.View):
self._bot,
interaction.user.id,
),
embeds=embeds,
embed=embed,
)
# update the message_id once its created
+22 -17
View File
@@ -1,6 +1,10 @@
"""View for verification buttons"""
import nextcord
from sqlalchemy import select
from sqlalchemy.orm import Session
from utilities.sql_data_classes import Submissions, engine
# pylint: disable=import-error
from utilities.utils import submit_score
@@ -17,33 +21,34 @@ class VerificationView(nextcord.ui.View):
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for denying score submission."""
with Session(engine) as session:
stmt = select(Submissions).where(Submissions.VerificationMessageId == interaction.message.id)
submission = session.scalar(stmt)
# let user know submission was denied
# TODO need to access bot here
# delete the submission
session.delete(stmt)
session.commit()
await interaction.message.delete()
@nextcord.ui.button(label="Approve", style=nextcord.ButtonStyle.green, custom_id="approve-button-submission")
async def approve_button(
self, button: nextcord.ui.Button, interaction: nextcord.Interaction
): # pylint: disable=unused-argument
"""Button for approving score submission."""
for embed in interaction.message.embeds:
content = embed.to_dict()
for field in content["fields"]:
if field["name"] == "GroupId":
group_id = int(field["value"])
elif field["name"] == "CategoryId":
category_id = int(field["value"])
elif field["name"] == "Score":
score = field["value"]
elif field["name"] == "User Name":
user = field["value"]
with Session(engine) as session:
stmt = select(Submissions).where(Submissions.VerificationMessageId == interaction.message.id)
submission = session.scalar(stmt)
try:
await submit_score(interaction, group_id, category_id, user, score)
await submit_score(
interaction, submission.GroupId, submission.CategoryId, submission.UserName, submission.Score
)
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
# remove the bot information embed. it should always be the first one.
await interaction.message.edit(content=content, view=None, embed=interaction.message.embeds[0])
await interaction.message.edit(content=content, view=None)
# TODO send message to let user know submission was accepted. Need access to bot here.
except UnboundLocalError:
await interaction.send("Instruct user to submit with all fields.")
await interaction.message.edit(view=None)
+39 -11
View File
@@ -321,6 +321,44 @@ def get_time_str(time_in: float) -> str:
return time.strftime(f"%-M:%S.{milliseconds}", time.gmtime(time_in))
def score_float_to_str(score: float, is_time_record: bool) -> str:
"""
Converts a float representation of a score into a string. Easier to transport the score as one data type.
Calls get_time_str if the score is a time record, otherwise just converts the string.
Args:
score (float): float representation of the score.
is_time_record (bool): true if the score is a time record.
Returns:
str: string representation of the score.
"""
if is_time_record:
return get_time_str(score)
else:
return str(score)
def score_str_to_float(score: str, is_time_record: bool) -> float:
"""
Converts a string representation of score into a float. Easier to transport the score as one data type.
Operates differently depending on if the score is a time record or an int.
Args:
score (str): string representation of the score.
is_time_record (bool): true if the score is a time record.
Returns:
float: float representation of the score.
"""
if is_time_record:
minutes = float(score.split(":", maxsplit=1)[0])
seconds = float(score.split(":", maxsplit=1)[1])
return (minutes * 60) + seconds
else:
return float(score)
async def send_highscore_message(session: Session, guild: nextcord.Guild, group: CategoryGroup) -> nextcord.Message:
"""
Function takes care of checking if a message exists for a group.
@@ -351,7 +389,7 @@ async def send_highscore_message(session: Session, guild: nextcord.Guild, group:
async def submit_score(
interaction: nextcord.Interaction, group_id: int, category_id: int, username: str, score: str
interaction: nextcord.Interaction, group_id: int, category_id: int, username: str, score: float
) -> None:
"""
Adds passed score to group. Then updates group message by calling send_highscore_message.
@@ -366,16 +404,6 @@ async def submit_score(
"""
with Session(engine) as session:
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == category_id)
is_time_record = session.scalar(stmt)
if is_time_record:
minutes = float(score.split(":", maxsplit=1)[0])
seconds = float(score.split(":", maxsplit=1)[1])
score = (minutes * 60) + seconds
else:
score = float(score)
stmt = select(Score).where(Score.UserName == username).where(Score.CategoryId == category_id)
existing_score = session.scalar(stmt)