diff --git a/classes/submission.py b/classes/submission.py index 405cdd4..2d73c68 100644 --- a/classes/submission.py +++ b/classes/submission.py @@ -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: diff --git a/cogs/message_listener/__init__.py b/cogs/message_listener/__init__.py index a1468d7..7de08f8 100644 --- a/cogs/message_listener/__init__.py +++ b/cogs/message_listener/__init__.py @@ -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. diff --git a/cogs/submission/submission_views.py b/cogs/submission/submission_views.py index 8954edf..52dd2bf 100644 --- a/cogs/submission/submission_views.py +++ b/cogs/submission/submission_views.py @@ -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 diff --git a/cogs/submission/verification_view.py b/cogs/submission/verification_view.py index 3ce5480..9b02ee9 100644 --- a/cogs/submission/verification_view.py +++ b/cogs/submission/verification_view.py @@ -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) diff --git a/utilities/utils.py b/utilities/utils.py index 62b394a..1dac3bf 100644 --- a/utilities/utils.py +++ b/utilities/utils.py @@ -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)