diff --git a/.flake8 b/.flake8 index cab8ba8..259dc4e 100644 --- a/.flake8 +++ b/.flake8 @@ -26,7 +26,7 @@ extend-ignore = E203, # Don't require docstrings when overriding a method, # the base method should have a docstring but the rest not -ignore-decorators=overrides +ignore-decorator=overrides max-line-length = 120 # Disable some rules for entire files per-file-ignores = diff --git a/alembic/versions/581ae6511b98_add_dad_jokes.py b/alembic/versions/581ae6511b98_add_dad_jokes.py deleted file mode 100644 index b3bed89..0000000 --- a/alembic/versions/581ae6511b98_add_dad_jokes.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Add dad jokes - -Revision ID: 581ae6511b98 -Revises: 632b69cdadde -Create Date: 2022-07-15 23:37:08.147611 - -""" -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision = "581ae6511b98" -down_revision = "632b69cdadde" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "dad_jokes", - sa.Column("dad_joke_id", sa.Integer(), nullable=False), - sa.Column("joke", sa.Text(), nullable=False), - sa.PrimaryKeyConstraint("dad_joke_id"), - ) - # ### end Alembic commands ### - - -def downgrade() -> None: - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("dad_jokes") - # ### end Alembic commands ### diff --git a/database/crud/custom_commands.py b/database/crud/custom_commands.py index 85ecf56..c6377c6 100644 --- a/database/crud/custom_commands.py +++ b/database/crud/custom_commands.py @@ -33,7 +33,6 @@ async def create_command(session: AsyncSession, name: str, response: str) -> Cus command = CustomCommand(name=name, indexed_name=clean_name(name), response=response) session.add(command) await session.commit() - return command diff --git a/database/crud/dad_jokes.py b/database/crud/dad_jokes.py deleted file mode 100644 index 30aa010..0000000 --- a/database/crud/dad_jokes.py +++ /dev/null @@ -1,42 +0,0 @@ -from typing import Optional - -from sqlalchemy import func, select -from sqlalchemy.ext.asyncio import AsyncSession - -from database.exceptions.not_found import NoResultFoundException -from database.models import DadJoke - -__all__ = ["add_dad_joke", "edit_dad_joke", "get_random_dad_joke"] - - -async def add_dad_joke(session: AsyncSession, joke: str) -> DadJoke: - """Add a new dad joke to the database""" - dad_joke = DadJoke(joke=joke) - session.add(dad_joke) - await session.commit() - - return dad_joke - - -async def edit_dad_joke(session: AsyncSession, joke_id: int, new_joke: str) -> DadJoke: - """Edit an existing dad joke""" - statement = select(DadJoke).where(DadJoke.dad_joke_id == joke_id) - dad_joke: Optional[DadJoke] = (await session.execute(statement)).scalar_one_or_none() - if dad_joke is None: - raise NoResultFoundException - - dad_joke.joke = new_joke - session.add(dad_joke) - await session.commit() - - return dad_joke - - -async def get_random_dad_joke(session: AsyncSession) -> DadJoke: - """Return a random database entry""" - statement = select(DadJoke).order_by(func.random()) - row = (await session.execute(statement)).first() - if row is None: - raise NoResultFoundException - - return row[0] diff --git a/database/models.py b/database/models.py index d663204..71d4f3c 100644 --- a/database/models.py +++ b/database/models.py @@ -14,7 +14,6 @@ __all__ = [ "Bank", "CustomCommand", "CustomCommandAlias", - "DadJoke", "NightlyData", "UforaAnnouncement", "UforaCourse", @@ -74,15 +73,6 @@ class CustomCommandAlias(Base): command: CustomCommand = relationship("CustomCommand", back_populates="aliases", uselist=False, lazy="selectin") -class DadJoke(Base): - """When I finally understood asymptotic notation, it was a big "oh" moment""" - - __tablename__ = "dad_jokes" - - dad_joke_id: int = Column(Integer, primary_key=True) - joke: str = Column(Text, nullable=False) - - class NightlyData(Base): """Data for a user's Nightly stats""" diff --git a/database/utils/caches.py b/database/utils/caches.py index edc3a5e..5e5be4f 100644 --- a/database/utils/caches.py +++ b/database/utils/caches.py @@ -1,6 +1,5 @@ from abc import ABC, abstractmethod -from overrides import overrides from sqlalchemy.ext.asyncio import AsyncSession from database.crud import ufora_courses @@ -48,47 +47,19 @@ class DatabaseCache(ABC): class UforaCourseCache(DatabaseCache): """Cache to store the names of Ufora courses""" - # Also store the aliases to add additional support - aliases: dict[str, str] = {} - - @overrides - def clear(self): - self.aliases.clear() - super().clear() - - @overrides async def refresh(self, database_session: AsyncSession): self.clear() courses = await ufora_courses.get_all_courses(database_session) - self.data = list(map(lambda c: c.name, courses)) - - # Load the aliases + # Load the course names + all the aliases for course in courses: - for alias in course.aliases: - # Store aliases in lowercase - self.aliases[alias.alias.lower()] = course.name + aliases = list(map(lambda x: x.alias, course.aliases)) + self.data.extend([course.name, *aliases]) self.data.sort() self.data_transformed = list(map(str.lower, self.data)) - @overrides - def get_autocomplete_suggestions(self, query: str): - query = query.lower() - results = set() - - # Return the original (not-lowercase) version - for index, course in enumerate(self.data_transformed): - if query in course: - results.add(self.data[index]) - - for alias, course in self.aliases.items(): - if query in alias: - results.add(course) - - return sorted(list(results)) - class CacheManager: """Class that keeps track of all caches""" diff --git a/didier/cogs/fun.py b/didier/cogs/fun.py deleted file mode 100644 index ddc119b..0000000 --- a/didier/cogs/fun.py +++ /dev/null @@ -1,29 +0,0 @@ -from discord.ext import commands - -from database.crud.dad_jokes import get_random_dad_joke -from didier import Didier - - -class Fun(commands.Cog): - """Cog with lots of random fun stuff""" - - client: Didier - - def __init__(self, client: Didier): - self.client = client - - @commands.hybrid_command( - name="dadjoke", - aliases=["Dad", "Dj"], - description="Why does Yoda's code always crash? Because there is no try.", - ) - async def dad_joke(self, ctx: commands.Context): - """Get a random dad joke""" - async with self.client.db_session as session: - joke = await get_random_dad_joke(session) - return await ctx.reply(joke.joke, mention_author=False) - - -async def setup(client: Didier): - """Load the cog""" - await client.add_cog(Fun(client)) diff --git a/didier/cogs/owner.py b/didier/cogs/owner.py index f43df42..ca72cd6 100644 --- a/didier/cogs/owner.py +++ b/didier/cogs/owner.py @@ -9,7 +9,7 @@ from database.exceptions.constraints import DuplicateInsertException from database.exceptions.not_found import NoResultFoundException from didier import Didier from didier.data.flags.owner import EditCustomFlags -from didier.views.modals import AddDadJoke, CreateCustomCommand, EditCustomCommand +from didier.data.modals.custom_commands import CreateCustomCommand, EditCustomCommand class Owner(commands.Cog): @@ -29,6 +29,7 @@ class Owner(commands.Cog): This means that we don't have to add is_owner() to every single command separately """ + # pylint: disable=W0236 # Pylint thinks this can't be async, but it can return await self.client.is_owner(ctx.author) @commands.command(name="Error") @@ -47,7 +48,7 @@ class Owner(commands.Cog): await ctx.message.add_reaction("🔄") - @commands.group(name="Add", aliases=["Create"], case_insensitive=True, invoke_without_command=False) + @commands.group(name="Add", case_insensitive=True, invoke_without_command=False) async def add_msg(self, ctx: commands.Context): """Command group for [add X] message commands""" @@ -87,17 +88,6 @@ class Owner(commands.Cog): modal = CreateCustomCommand(self.client) await interaction.response.send_modal(modal) - @add_slash.command(name="dadjoke", description="Add a dad joke") - async def add_dad_joke_slash(self, interaction: discord.Interaction): - """Slash command to add a dad joke""" - if not await self.client.is_owner(interaction.user): - return interaction.response.send_message( - "Je hebt geen toestemming om dit commando uit te voeren.", ephemeral=True - ) - - modal = AddDadJoke(self.client) - await interaction.response.send_modal(modal) - @commands.group(name="Edit", case_insensitive=True, invoke_without_command=False) async def edit_msg(self, ctx: commands.Context): """Command group for [edit X] commands""" diff --git a/didier/cogs/school.py b/didier/cogs/school.py index 716c59d..9f9cadf 100644 --- a/didier/cogs/school.py +++ b/didier/cogs/school.py @@ -4,9 +4,7 @@ import discord from discord import app_commands from discord.ext import commands -from database.crud import ufora_courses from didier import Didier -from didier.data import constants class School(commands.Cog): @@ -59,31 +57,6 @@ class School(commands.Cog): await message.add_reaction("📌") return await interaction.response.send_message("📌", ephemeral=True) - @commands.hybrid_command( - name="fiche", description="Stuurt de link naar de studiefiche voor [Vak]", aliases=["guide", "studiefiche"] - ) - @app_commands.describe(course="vak") - async def study_guide(self, ctx: commands.Context, course: str): - """Create links to study guides""" - async with self.client.db_session as session: - ufora_course = await ufora_courses.get_course_by_name(session, course) - - if ufora_course is None: - return await ctx.reply(f"Geen vak gevonden voor ``{course}``", ephemeral=True) - - return await ctx.reply( - f"https://studiekiezer.ugent.be/studiefiche/nl/{ufora_course.code}/{constants.CURRENT_YEAR}", - mention_author=False, - ) - - @study_guide.autocomplete("course") - async def study_guide_autocomplete(self, _: discord.Interaction, current: str) -> list[app_commands.Choice[str]]: - """Autocompletion for the 'course'-parameter""" - return [ - app_commands.Choice(name=course, value=course) - for course in self.client.database_caches.ufora_courses.get_autocomplete_suggestions(current) - ] - async def setup(client: Didier): """Load the cog""" diff --git a/didier/cogs/tasks.py b/didier/cogs/tasks.py index b8488ae..285945c 100644 --- a/didier/cogs/tasks.py +++ b/didier/cogs/tasks.py @@ -14,6 +14,7 @@ class Tasks(commands.Cog): client: Didier def __init__(self, client: Didier): + # pylint: disable=no-member self.client = client # Only pull announcements if a token was provided diff --git a/didier/data/constants.py b/didier/data/constants.py index cea951c..d5c1021 100644 --- a/didier/data/constants.py +++ b/didier/data/constants.py @@ -1,10 +1 @@ -# The year in which we were in 1Ba -import settings - -FIRST_YEAR = 2019 -# Year to use when adding the current year of our education -# to find the academic year -OFFSET_FIRST_YEAR = FIRST_YEAR - 1 -# The current academic year -CURRENT_YEAR = OFFSET_FIRST_YEAR + settings.YEAR PREFIXES = ["didier", "big d"] diff --git a/didier/views/__init__.py b/didier/data/modals/__init__.py similarity index 100% rename from didier/views/__init__.py rename to didier/data/modals/__init__.py diff --git a/didier/views/modals/custom_commands.py b/didier/data/modals/custom_commands.py similarity index 100% rename from didier/views/modals/custom_commands.py rename to didier/data/modals/custom_commands.py diff --git a/didier/exceptions/config.py b/didier/exceptions/config.py new file mode 100644 index 0000000..df73f7e --- /dev/null +++ b/didier/exceptions/config.py @@ -0,0 +1,12 @@ +__all__ = ["MissingEnvironmentVariable"] + + +class MissingEnvironmentVariable(RuntimeError): + """Exception raised when an environment variable is missing + + These are not necessarily checked on startup, because they may be unused + during a given test run, and random unrelated crashes would be annoying + """ + + def __init__(self, variable: str): + super().__init__(f"Missing environment variable: {variable}") diff --git a/didier/views/modals/__init__.py b/didier/views/modals/__init__.py deleted file mode 100644 index b28a4de..0000000 --- a/didier/views/modals/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .custom_commands import CreateCustomCommand, EditCustomCommand -from .dad_jokes import AddDadJoke - -__all__ = ["AddDadJoke", "CreateCustomCommand", "EditCustomCommand"] diff --git a/didier/views/modals/dad_jokes.py b/didier/views/modals/dad_jokes.py deleted file mode 100644 index 9632197..0000000 --- a/didier/views/modals/dad_jokes.py +++ /dev/null @@ -1,37 +0,0 @@ -import traceback - -import discord -from overrides import overrides - -from database.crud.dad_jokes import add_dad_joke -from didier import Didier - -__all__ = ["AddDadJoke"] - - -class AddDadJoke(discord.ui.Modal, title="Add Dad Joke"): - """Modal to add a new dad joke""" - - name: discord.ui.TextInput = discord.ui.TextInput( - label="Joke", - placeholder="I sold our vacuum cleaner, it was just gathering dust.", - style=discord.TextStyle.long, - ) - - client: Didier - - def __init__(self, client: Didier, *args, **kwargs): - super().__init__(*args, **kwargs) - self.client = client - - @overrides - async def on_submit(self, interaction: discord.Interaction): - async with self.client.db_session as session: - joke = await add_dad_joke(session, str(self.name.value)) - - await interaction.response.send_message(f"Successfully added joke #{joke.dad_joke_id}", ephemeral=True) - - @overrides - async def on_error(self, interaction: discord.Interaction, error: Exception): # type: ignore - await interaction.response.send_message("Something went wrong.", ephemeral=True) - traceback.print_tb(error.__traceback__) diff --git a/pyproject.toml b/pyproject.toml index 75d3054..2a28e94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,8 +15,7 @@ omit = [ "./didier/cogs/*", "./didier/didier.py", "./didier/data/*", - "./didier/utils/discord/colours.py", - "./didier/utils/discord/constants.py" + "./didier/utils/discord/colours.py" ] [tool.isort] @@ -39,5 +38,5 @@ env = [ "DB_PASSWORD = pytest", "DB_HOST = localhost", "DB_PORT = 5433", - "DISCORD_TOKEN = token" + "DISC_TOKEN = token" ] diff --git a/settings.py b/settings.py index bb2296f..bc35ab4 100644 --- a/settings.py +++ b/settings.py @@ -29,8 +29,6 @@ __all__ = [ """General config""" SANDBOX: bool = env.bool("SANDBOX", True) LOGFILE: str = env.str("LOGFILE", "didier.log") -SEMESTER: int = env.int("SEMESTER", 2) -YEAR: int = env.int("YEAR", 3) """Database""" DB_NAME: str = env.str("DB_NAME", "didier") diff --git a/tests/conftest.py b/tests/conftest.py index b2a1e04..c8ab65f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import asyncio +import datetime from typing import AsyncGenerator, Generator from unittest.mock import MagicMock @@ -6,9 +7,11 @@ import pytest from sqlalchemy.ext.asyncio import AsyncSession from database.engine import engine -from database.models import Base +from database.models import Base, UforaAnnouncement, UforaCourse, UforaCourseAlias from didier import Didier +"""General fixtures""" + @pytest.fixture(scope="session") def event_loop() -> Generator: @@ -54,3 +57,34 @@ def mock_client() -> Didier: mock_client.user = mock_user return mock_client + + +"""Fixtures to put fake data in the database""" + + +@pytest.fixture +async def ufora_course(database_session: AsyncSession) -> UforaCourse: + """Fixture to create a course""" + course = UforaCourse(name="test", code="code", year=1, log_announcements=True) + database_session.add(course) + await database_session.commit() + return course + + +@pytest.fixture +async def ufora_course_with_alias(database_session: AsyncSession, ufora_course: UforaCourse) -> UforaCourse: + """Fixture to create a course with an alias""" + alias = UforaCourseAlias(course_id=ufora_course.course_id, alias="alias") + database_session.add(alias) + await database_session.commit() + await database_session.refresh(ufora_course) + return ufora_course + + +@pytest.fixture +async def ufora_announcement(ufora_course: UforaCourse, database_session: AsyncSession) -> UforaAnnouncement: + """Fixture to create an announcement""" + announcement = UforaAnnouncement(course_id=ufora_course.course_id, publication_date=datetime.datetime.now()) + database_session.add(announcement) + await database_session.commit() + return announcement diff --git a/tests/test_database/conftest.py b/tests/test_database/conftest.py deleted file mode 100644 index de1e939..0000000 --- a/tests/test_database/conftest.py +++ /dev/null @@ -1,34 +0,0 @@ -import datetime - -import pytest -from sqlalchemy.ext.asyncio import AsyncSession - -from database.models import UforaAnnouncement, UforaCourse, UforaCourseAlias - - -@pytest.fixture -async def ufora_course(database_session: AsyncSession) -> UforaCourse: - """Fixture to create a course""" - course = UforaCourse(name="test", code="code", year=1, log_announcements=True) - database_session.add(course) - await database_session.commit() - return course - - -@pytest.fixture -async def ufora_course_with_alias(database_session: AsyncSession, ufora_course: UforaCourse) -> UforaCourse: - """Fixture to create a course with an alias""" - alias = UforaCourseAlias(course_id=ufora_course.course_id, alias="alias") - database_session.add(alias) - await database_session.commit() - await database_session.refresh(ufora_course) - return ufora_course - - -@pytest.fixture -async def ufora_announcement(ufora_course: UforaCourse, database_session: AsyncSession) -> UforaAnnouncement: - """Fixture to create an announcement""" - announcement = UforaAnnouncement(course_id=ufora_course.course_id, publication_date=datetime.datetime.now()) - database_session.add(announcement) - await database_session.commit() - return announcement