From 5b47397f297b80b0ac37b27efdd32febd1acf49d Mon Sep 17 00:00:00 2001 From: stijndcl Date: Fri, 15 Jul 2022 23:06:40 +0200 Subject: [PATCH] Add study guide commands, get auto-completion for full course names based on aliases --- .flake8 | 2 +- database/utils/caches.py | 35 +++++++++++++++++++++++++++++--- didier/cogs/school.py | 27 +++++++++++++++++++++++++ didier/exceptions/config.py | 12 ----------- pyproject.toml | 3 ++- tests/conftest.py | 36 +-------------------------------- tests/test_database/conftest.py | 34 +++++++++++++++++++++++++++++++ 7 files changed, 97 insertions(+), 52 deletions(-) delete mode 100644 didier/exceptions/config.py create mode 100644 tests/test_database/conftest.py diff --git a/.flake8 b/.flake8 index 259dc4e..cab8ba8 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-decorator=overrides +ignore-decorators=overrides max-line-length = 120 # Disable some rules for entire files per-file-ignores = diff --git a/database/utils/caches.py b/database/utils/caches.py index 5e5be4f..edc3a5e 100644 --- a/database/utils/caches.py +++ b/database/utils/caches.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod +from overrides import overrides from sqlalchemy.ext.asyncio import AsyncSession from database.crud import ufora_courses @@ -47,19 +48,47 @@ 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) - # Load the course names + all the aliases + self.data = list(map(lambda c: c.name, courses)) + + # Load the aliases for course in courses: - aliases = list(map(lambda x: x.alias, course.aliases)) - self.data.extend([course.name, *aliases]) + for alias in course.aliases: + # Store aliases in lowercase + self.aliases[alias.alias.lower()] = course.name 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/school.py b/didier/cogs/school.py index 9f9cadf..2ca3ad2 100644 --- a/didier/cogs/school.py +++ b/didier/cogs/school.py @@ -4,6 +4,7 @@ import discord from discord import app_commands from discord.ext import commands +from database.crud import ufora_courses from didier import Didier @@ -57,6 +58,32 @@ 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) + + # TODO load from config + year = 2018 + 3 + return await ctx.reply( + f"https://studiekiezer.ugent.be/studiefiche/nl/{ufora_course.code}/{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/exceptions/config.py b/didier/exceptions/config.py deleted file mode 100644 index df73f7e..0000000 --- a/didier/exceptions/config.py +++ /dev/null @@ -1,12 +0,0 @@ -__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/pyproject.toml b/pyproject.toml index 2a28e94..7d77992 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,8 @@ omit = [ "./didier/cogs/*", "./didier/didier.py", "./didier/data/*", - "./didier/utils/discord/colours.py" + "./didier/utils/discord/colours.py", + "./didier/utils/discord/constants.py" ] [tool.isort] diff --git a/tests/conftest.py b/tests/conftest.py index c8ab65f..b2a1e04 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,4 @@ import asyncio -import datetime from typing import AsyncGenerator, Generator from unittest.mock import MagicMock @@ -7,11 +6,9 @@ import pytest from sqlalchemy.ext.asyncio import AsyncSession from database.engine import engine -from database.models import Base, UforaAnnouncement, UforaCourse, UforaCourseAlias +from database.models import Base from didier import Didier -"""General fixtures""" - @pytest.fixture(scope="session") def event_loop() -> Generator: @@ -57,34 +54,3 @@ 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 new file mode 100644 index 0000000..de1e939 --- /dev/null +++ b/tests/test_database/conftest.py @@ -0,0 +1,34 @@ +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