Compare commits

..
Author SHA1 Message Date
imcompiling b73cddab53 Add ruff to project. 2024-10-05 11:52:28 -04:00
imcompiling 8f88c76e2e Add pyproject for poetry support. 2024-10-05 11:47:12 -04:00
imcompiling c977a4e958 Bug fix #64, was using wrong variable.
build_develop / build (push) Successful in 46s
Utils.py got formatted by ruff.
2024-09-30 12:55:34 -04:00
imcompiling e2e8d8de30 Don't need to print the help string.
build_develop / build (push) Successful in 1m20s
build_release / build (push) Successful in 20s
2024-09-29 23:31:49 -04:00
imcompiling 7f007ffa1f Merge commit '02e7a6df3a13bb19056f31f616101980b668ae43'
build_develop / build (push) Successful in 2m20s
2023-08-30 09:04:36 -04:00
imcompiling 913f915bf0 Clarify create category option description. 2023-08-30 09:04:24 -04:00
imcompiling 02e7a6df3a Comment out troublesome code. I believe that this causes users to have trouble submitting multiple in a row over 5 minutes. 2023-08-29 21:01:13 -04:00
imcompiling 7be869b2b6 Update registry location.
build_develop / build (push) Successful in 1m14s
2023-07-29 18:34:56 -04:00
imcompiling 15c4776918 its async. must await.
build_develop / build (push) Failing after 1m29s
2023-07-25 13:38:48 -04:00
imcompiling d3102b57cc Add new birthday action.
build_develop / build (push) Has been cancelled
2023-07-25 11:19:56 -04:00
imcompiling 9bef20de5b Clarify wording in submission process.
build
2023-04-30 21:54:02 -04:00
13 changed files with 503 additions and 193 deletions
+8 -2
View File
@@ -14,7 +14,13 @@ jobs:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Log into container registry
uses: docker/login-action@v2
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: Build Docker Image
run: docker build -t viker.cheezers.net:5000/agon:develop .
run: docker build -t gitea.cheezers.org/imcompiling/agon:develop .
- name: Push Docker Image
run: docker push viker.cheezers.net:5000/agon:develop
run: docker push gitea.cheezers.org/imcompiling/agon:develop
+9 -3
View File
@@ -14,11 +14,17 @@ jobs:
fetch-depth: 0
- name: Install Docker
uses: https://github.com/papodaca/install-docker-action@v1
- name: Log into container registry
uses: docker/login-action@v2
with:
registry: gitea.cheezers.org
username: imcompiling
password: ${{ secrets.PASSWORD }}
- name: Set env
run: echo "GIT_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Build Docker Images
run: docker build -t viker.cheezers.net:5000/agon:latest -t viker.cheezers.net:5000/agon:${{env.GIT_TAG}} .
run: docker build -t gitea.cheezers.org/imcompiling/agon:latest -t gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}} .
- name: Push Latest Image
run: docker push viker.cheezers.net:5000/agon:latest
run: docker push gitea.cheezers.org/imcompiling/agon:latest
- name: Push Tagged Image
run: docker push viker.cheezers.net:5000/agon:${{env.GIT_TAG}}
run: docker push gitea.cheezers.org/imcompiling/agon:${{env.GIT_TAG}}
+1
View File
@@ -105,6 +105,7 @@ venv/
ENV/
env.bak/
venv.bak/
poetry.lock
# Spyder project settings
.spyderproject
+159 -18
View File
@@ -2,7 +2,6 @@
Contains classes and enum for tracking data on submissions.
"""
from dataclasses import dataclass, field
from enum import Enum
from nextcord import Embed
@@ -11,10 +10,8 @@ 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
@@ -28,34 +25,171 @@ class SubmissionState(Enum):
VERIFY = 6
@dataclass
# TODO remake into a data class
class Submission:
"""
Class for an individual submission. Contains submission information such as
guild id, message id, table, category, username, score and submission state.
"""
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
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
def get_embed(self) -> Embed:
@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]":
"""Creates an embed based off of the submission state and fields filled out."""
state: SubmissionState = self.submission_state
if state is SubmissionState.CATEGORY:
description = "Step 1: Choose your category!"
elif state is SubmissionState.SCORE:
description = "Step 2: Input your score!"
description = "Step 2: Send a message to input your score!"
elif state is SubmissionState.PROOF:
description = "Step 3: Submit your proof!"
description = "Step 3: Send a message with a picture attached to submit your proof!"
elif state is SubmissionState.NAME:
description = "Step 4 (Optional): Change name on submission! (proceed to next if skipping)"
description = (
"Step 4 (Optional): Send a message to change name on submission! (proceed to next if skipping)"
)
elif state is SubmissionState.SUBMIT:
description = "Step 5: Submit if everything looks correct!"
elif state is SubmissionState.VERIFY:
@@ -78,9 +212,16 @@ class Submission:
)
embed.add_field(name="Category", value=session.scalar(stmt), inline=False)
if self.score:
embed.add_field(name="Score", value=score_float_to_str(self.score, self.is_time_record), inline=False)
embed.add_field(name="Score", value=self.score, inline=False)
return embed
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
class SubmissionMap:
+1 -1
View File
@@ -40,7 +40,7 @@ class CreateCog(commands.Cog, name="Create Cog"):
interaction: nextcord.Interaction,
group_id: str = nextcord.SlashOption(name="group", description="Group to add Category to.", required=True),
category_name: str = nextcord.SlashOption(description="Name of new Category.", required=True),
is_ascending: bool = nextcord.SlashOption(description="If higher value is better score.", required=True),
is_ascending: bool = nextcord.SlashOption(description="If lower value is a better score.", required=True),
is_time_record: bool = nextcord.SlashOption(
description="If the score should be shown in time format. Cannot be changed.", required=True
),
-1
View File
@@ -42,6 +42,5 @@ This channel will be where all user submission messages are sent.
3.) Set up your highscore tables! Use "/create group" to first create a group message in the channel you would like and then use "/create category" to create a category in that group message!"""
# pylint: enable=line-too-long
print(help_str)
await interaction.response.send_message(content=help_str)
+70 -35
View File
@@ -3,6 +3,7 @@ Defines cog for on message listener.
"""
import re
from datetime import datetime
import nextcord
from nextcord.ext import commands
@@ -12,8 +13,7 @@ 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 Guild, engine
from utilities.utils import score_str_to_float
from utilities.sql_data_classes import Category, Guild, engine
class SubmissionListener(commands.Cog, name="Message Listener"):
@@ -29,6 +29,8 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
if message.author.id == self.bot.user.id:
return
await birthday_action(message)
guild_id = message.guild.id
if submission_objects.has_user(message.author.id):
@@ -42,42 +44,59 @@ 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:
# check with regex if score matches correctly
is_time_record = submission.is_time_record
# 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:
stmt = select(Category.IsTimeRecord).where(Category.CategoryId == submission.category_id)
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.",
# 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,
)
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.
@@ -85,3 +104,19 @@ class SubmissionListener(commands.Cog, name="Message Listener"):
def setup(bot: commands.Bot):
"""Adds function to bot"""
bot.add_cog(SubmissionListener(bot))
async def birthday_action(message: nextcord.Message) -> None:
"""
Does the birthday thing, currently for chris only.
Args:
message (nextcord.Message): The message to check for birthday thing.
"""
today = datetime.today()
birthday = datetime(today.year, 7, 31)
chris = 642330132958281750
if today.date() == birthday.date():
if message.author.id == chris or chris in message.raw_mentions:
await message.add_reaction("🎂")
+18 -36
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, Submissions, engine
from utilities.sql_data_classes import Category, CategoryGroup, Guild, 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,14 +47,9 @@ 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]
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()
embeds = submission.get_embeds()
await interaction.message.edit(
embed=embed,
embeds=embeds,
)
@@ -105,8 +100,8 @@ class NextButton(nextcord.ui.Button):
else:
state = SubmissionState(state.value + 1)
submission.submission_state = state
embed = submission.get_embed()
await interaction.edit(embed=embed, view=SubmissionView(self._bot, interaction.user.id))
embeds = submission.get_embeds()
await interaction.edit(embeds=embeds, view=SubmissionView(self._bot, interaction.user.id))
class SubmitButton(nextcord.ui.Button):
@@ -121,40 +116,27 @@ 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)
active_submission = submission_objects.get_user_submission(interaction.user.id)
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)
active_submission.submission_state = SubmissionState.VERIFY
submission.submission_state = SubmissionState.VERIFY
embed = active_submission.get_embed()
embeds = submission.get_embeds()
verification_message = await channel.send(
await channel.send(
files=[await attch.to_file() for attch in interaction.message.attachments],
view=VerificationView(),
embed=embed,
embeds=embeds,
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):
@@ -189,11 +171,11 @@ class SubmissionCreateButton(nextcord.ui.View):
# a user should only have 1 submission active at once.
# split username on pipe. Decided to store whole username...
username: str = interaction.user.display_name.split("|")[0]
submission: Submission = Submission(group_id, interaction.guild_id, username)
submission: Submission = Submission(interaction.guild_id, group_id, username)
submission_objects.add_submission(interaction.user.id, submission)
embed = submission.get_embed()
embeds = submission.get_embeds()
submission_message = await channel.send(
f"<@{interaction.user.id}>",
@@ -201,7 +183,7 @@ class SubmissionCreateButton(nextcord.ui.View):
self._bot,
interaction.user.id,
),
embed=embed,
embeds=embeds,
)
# update the message_id once its created
@@ -249,6 +231,6 @@ class SubmissionView(nextcord.ui.View):
return check
async def on_timeout(self) -> None:
# remove user from submission objects after time out. Allows user to start new submission.
submission_objects.remove_user_submission(self._submission_user)
# async def on_timeout(self) -> None:
# # remove user from submission objects after time out. Allows user to start new submission.
# submission_objects.remove_user_submission(self._submission_user)
+17 -22
View File
@@ -1,10 +1,6 @@
"""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
@@ -21,34 +17,33 @@ 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
with Session(engine) as session:
stmt = select(Submissions).where(Submissions.VerificationMessageId == interaction.message.id)
submission = session.scalar(stmt)
"""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"]
try:
await submit_score(
interaction, submission.GroupId, submission.CategoryId, submission.UserName, submission.Score
)
await submit_score(interaction, group_id, category_id, user, score)
content = interaction.message.content
content += f"\nApproved by <@{interaction.user.id}>."
await interaction.message.edit(content=content, view=None)
# TODO send message to let user know submission was accepted. Need access to bot here.
# 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])
except UnboundLocalError:
await interaction.send("Instruct user to submit with all fields.")
await interaction.message.edit(view=None)
-11
View File
@@ -42,15 +42,4 @@ CREATE TABLE Scores(
CategoryId int unsigned not null,
FOREIGN KEY (CategoryId) REFERENCES Categories(CategoryId) ON DELETE CASCADE,
PRIMARY KEY (CategoryId, UserName)
);
CREATE TABLE Submissions(
CategoryId int unsigned not null,
GroupId int unsigned not null,
IsTimeRecord boolean not null,
Score float not null,
SubmissionMessageId bigint unsigned not null,
UserName varchar(30) not null,
VerificationMessageId bigint unsigned not null,
PRIMARY KEY (CategoryId, GroupId, UserName)
);
+130
View File
@@ -0,0 +1,130 @@
[tool.poetry]
name = "agon"
version = "0.1.0"
description = "A highscores discord bot"
authors = ["imcompiling <[email protected]>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
nextcord = ">=2.3.2"
python-dotenv = ">=0.20.0"
pandas = ">=1.5.2"
sqlalchemy = "<2.0"
pymysql = ">=1.0.2"
cryptography = ">=2.8"
[tool.poetry.group.dev.dependencies]
ruff = "^0.6.9"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.ruff]
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
"dakiny-messages", # Don't need to lint this git repo.
]
# Same as Black.
line-length = 120
indent-width = 4
# Assume Python 3.8
target-version = "py312"
[tool.ruff.lint]
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
# McCabe complexity (`C901`) by default.
select = ["ALL"]
ignore = [
"ANN101",
"B008", # I build SlashOptions as arguments.
"COM812",
"COM819",
"D107", # don't want docstrings on init functions
"D203",
"D206",
"D212",
"D300",
"E111",
"E114",
"E117",
"ISC001",
"ISC002",
"PD011", # doesn't like me using '.values' on embeds....
"Q000",
"Q001",
"Q002",
"Q003",
"W191",
]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.lint.flake8-annotations]
mypy-init-return = true
[tool.ruff.format]
# Like Black, use double quotes for strings.
quote-style = "double"
# Like Black, indent with spaces, rather than tabs.
indent-style = "space"
# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false
# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"
# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = true
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
+2 -3
View File
@@ -14,8 +14,7 @@ Base = automap_base()
Base.prepare(autoload_with=engine)
# define the classes
Category = Base.classes.Categories
CategoryGroup = Base.classes.CategoryGroups
Guild = Base.classes.Guilds
CategoryGroup = Base.classes.CategoryGroups
Category = Base.classes.Categories
Score = Base.classes.Scores
Submissions = Base.classes.Submissions
+88 -61
View File
@@ -16,7 +16,9 @@ from utilities.sql_data_classes import Category, CategoryGroup, Guild, Score, en
MAX_LINE_LENGTH = 142
def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "dict[str, str]":
def autofill_categories(
guild_id: int, group_id: int, category_begin: str
) -> "dict[str, str]":
"""
Returns all category names that start with category_begin.
If category_begin is empty, returns list of all category names.
@@ -40,16 +42,20 @@ def autofill_categories(guild_id: int, group_id: int, category_begin: str) -> "d
)
cat_group_alias = aliased(CategoryGroup, subq)
stmt = select(Category).where(Category.GroupId == cat_group_alias.GroupId)
categories_dict: "dict[str, str]" = {cat.CategoryName: str(cat.CategoryId) for cat in session.scalars(stmt)}
categories_dict: "dict[str, str]" = {
cat.CategoryName: str(cat.CategoryId) for cat in session.scalars(stmt)
}
# if string empty, return all category names
if not category_begin:
return dict(list(category_begin.items())[:25])
return dict(list(categories_dict.items())[:25])
# else find all categories that start with category_begin
# TODO is it better to have sql handle the starts with or python?
categories: "dict[str, str]" = {
cat_name: cat_id for cat_name, cat_id in categories_dict.items() if category_begin.lower() in cat_name.lower()
cat_name: cat_id
for cat_name, cat_id in categories_dict.items()
if category_begin.lower() in cat_name.lower()
}
return dict(list(categories.items())[:25])
@@ -71,7 +77,9 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
stmt = select(CategoryGroup).where(CategoryGroup.GuildId == guild_id)
# dict has to have key values of same type for discord.
group_dict: "dict[str, str]" = {group.GroupName: str(group.GroupId) for group in session.scalars(stmt)}
group_dict: "dict[str, str]" = {
group.GroupName: str(group.GroupId) for group in session.scalars(stmt)
}
# if string empty, return all group names
if not group_begin:
@@ -79,13 +87,17 @@ def autofill_groups(guild_id: int, group_begin: str) -> "dict[str, str]":
# else find all groups that start with group_begin
groups: "dict[str, str]" = {
group_name: group_id for group_name, group_id in group_dict.items() if group_begin.lower() in group_name.lower()
group_name: group_id
for group_name, group_id in group_dict.items()
if group_begin.lower() in group_name.lower()
}
return dict(list(groups.items())[:25])
def autofill_users_in_group_category(guild_id: int, group_id: int, category_id: int, user_begin: str) -> "list[str]":
def autofill_users_in_group_category(
guild_id: int, group_id: int, category_id: int, user_begin: str
) -> "list[str]":
"""
Returns list of users that match user_begin. Either all users in group or just users in category if category is
not None. Might be expensive due to unioning all users in group.
@@ -112,7 +124,9 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
# If no category id, get all users in group.
if category_id == 0:
subq = select(Category.CategoryId).where(Category.GroupId == category_group.GroupId)
subq = select(Category.CategoryId).where(
Category.GroupId == category_group.GroupId
)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(subq))
else:
@@ -124,7 +138,9 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
.subquery()
)
category = aliased(Category, subq)
stmt = select(distinct(Score.UserName)).where(Score.CategoryId == category.CategoryId)
stmt = select(distinct(Score.UserName)).where(
Score.CategoryId == category.CategoryId
)
users_list: "list[str]" = list(session.scalars(stmt))
@@ -133,7 +149,9 @@ def autofill_users_in_group_category(guild_id: int, group_id: int, category_id:
return users_list[:25]
# else find all users that start with user_begin
users: "list[str]" = [user for user in users_list if user_begin.lower() in user.lower()]
users: "list[str]" = [
user for user in users_list if user_begin.lower() in user.lower()
]
return users[:25]
@@ -151,9 +169,15 @@ def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
"""
with Session(engine) as session:
group_ids = select(CategoryGroup.GroupId).where(CategoryGroup.GuildId == guild_id)
category_ids = select(Category.CategoryId).where(Category.GroupId.in_(group_ids))
stmt = select(distinct(Score.UserName)).where(Score.CategoryId.in_(category_ids))
group_ids = select(CategoryGroup.GroupId).where(
CategoryGroup.GuildId == guild_id
)
category_ids = select(Category.CategoryId).where(
Category.GroupId.in_(group_ids)
)
stmt = select(distinct(Score.UserName)).where(
Score.CategoryId.in_(category_ids)
)
users_list: "list[str]" = list(session.scalars(stmt))
# if string empty, return all users
@@ -161,7 +185,9 @@ def autofill_users_in_guild(guild_id: int, user_begin: str) -> "list[str]":
return users_list[:25]
# else find all users that start with user_begin
users: "list[str]" = [user for user in users_list if user_begin.lower() in user.lower()]
users: "list[str]" = [
user for user in users_list if user_begin.lower() in user.lower()
]
return users[:25]
@@ -207,7 +233,10 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
category_ids.append(cat.CategoryId)
# get all scores in category group, load into data frame.
d_f = pd.read_sql_query(sql=session.query(Score).where(Score.CategoryId.in_(category_ids)).statement, con=engine)
d_f = pd.read_sql_query(
sql=session.query(Score).where(Score.CategoryId.in_(category_ids)).statement,
con=engine,
)
# this list is what will be printed in the discord message
# first string will be category list, others will be score printouts
@@ -225,17 +254,26 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
# process data. Want each group sorted correctly and only top X results
processed_group = (
group_df.sort_values("Score", ascending=category.IsAscending).head(category.Size).reset_index()
group_df.sort_values("Score", ascending=category.IsAscending)
.head(category.Size)
.reset_index()
)
# add each score to list
for ind in range(max_cat_size): # need to subtract to deal with category name string
for ind in range(
max_cat_size
): # need to subtract to deal with category name string
if ind in processed_group.index:
username = processed_group["UserName"][ind]
score = processed_group["Score"][ind]
str_list[ind + 1] += format_score_string(
username, score, group.UsernameLength, category.IsTimeRecord, ind + 1, pad_length
username,
score,
group.UsernameLength,
category.IsTimeRecord,
ind + 1,
pad_length,
)
else:
# if not enough entries, pad out
@@ -275,7 +313,12 @@ def format_highscore_message(session: Session, group: CategoryGroup) -> str:
def format_score_string(
username: str, score: float, username_length: int, is_time_record: bool, rank: int, pad_length: int
username: str,
score: float,
username_length: int,
is_time_record: bool,
rank: int,
pad_length: int,
) -> str:
"""
Builds the string to show a user's score.
@@ -321,45 +364,9 @@ 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:
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.
Creates and sends/edits the message.
@@ -389,7 +396,11 @@ 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: float
interaction: nextcord.Interaction,
group_id: int,
category_id: int,
username: str,
score: str,
) -> None:
"""
Adds passed score to group. Then updates group message by calling send_highscore_message.
@@ -404,7 +415,21 @@ async def submit_score(
"""
with Session(engine) as session:
stmt = select(Score).where(Score.UserName == username).where(Score.CategoryId == category_id)
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)
if existing_score:
@@ -431,7 +456,9 @@ def add_preset_configuration(guild_id: int, config_str: str) -> None:
"""
with Session(engine) as session:
with open(f"example_config/{config_str}.json", encoding="utf-8", mode="r") as file:
with open(
f"example_config/{config_str}.json", encoding="utf-8", mode="r"
) as file:
config = json.load(file)
for group_name, categorys in config["tables"].items():