Add study guide commands, get auto-completion for full course names based on aliases

pull/119/head
stijndcl 2022-07-15 23:06:40 +02:00
parent 72c3acbcc2
commit 5b47397f29
7 changed files with 97 additions and 52 deletions

View File

@ -26,7 +26,7 @@ extend-ignore =
E203, E203,
# Don't require docstrings when overriding a method, # Don't require docstrings when overriding a method,
# the base method should have a docstring but the rest not # the base method should have a docstring but the rest not
ignore-decorator=overrides ignore-decorators=overrides
max-line-length = 120 max-line-length = 120
# Disable some rules for entire files # Disable some rules for entire files
per-file-ignores = per-file-ignores =

View File

@ -1,5 +1,6 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from overrides import overrides
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database.crud import ufora_courses from database.crud import ufora_courses
@ -47,19 +48,47 @@ class DatabaseCache(ABC):
class UforaCourseCache(DatabaseCache): class UforaCourseCache(DatabaseCache):
"""Cache to store the names of Ufora courses""" """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): async def refresh(self, database_session: AsyncSession):
self.clear() self.clear()
courses = await ufora_courses.get_all_courses(database_session) 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: for course in courses:
aliases = list(map(lambda x: x.alias, course.aliases)) for alias in course.aliases:
self.data.extend([course.name, *aliases]) # Store aliases in lowercase
self.aliases[alias.alias.lower()] = course.name
self.data.sort() self.data.sort()
self.data_transformed = list(map(str.lower, self.data)) 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 CacheManager:
"""Class that keeps track of all caches""" """Class that keeps track of all caches"""

View File

@ -4,6 +4,7 @@ import discord
from discord import app_commands from discord import app_commands
from discord.ext import commands from discord.ext import commands
from database.crud import ufora_courses
from didier import Didier from didier import Didier
@ -57,6 +58,32 @@ class School(commands.Cog):
await message.add_reaction("📌") await message.add_reaction("📌")
return await interaction.response.send_message("📌", ephemeral=True) 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): async def setup(client: Didier):
"""Load the cog""" """Load the cog"""

View File

@ -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}")

View File

@ -15,7 +15,8 @@ omit = [
"./didier/cogs/*", "./didier/cogs/*",
"./didier/didier.py", "./didier/didier.py",
"./didier/data/*", "./didier/data/*",
"./didier/utils/discord/colours.py" "./didier/utils/discord/colours.py",
"./didier/utils/discord/constants.py"
] ]
[tool.isort] [tool.isort]

View File

@ -1,5 +1,4 @@
import asyncio import asyncio
import datetime
from typing import AsyncGenerator, Generator from typing import AsyncGenerator, Generator
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -7,11 +6,9 @@ import pytest
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from database.engine import engine from database.engine import engine
from database.models import Base, UforaAnnouncement, UforaCourse, UforaCourseAlias from database.models import Base
from didier import Didier from didier import Didier
"""General fixtures"""
@pytest.fixture(scope="session") @pytest.fixture(scope="session")
def event_loop() -> Generator: def event_loop() -> Generator:
@ -57,34 +54,3 @@ def mock_client() -> Didier:
mock_client.user = mock_user mock_client.user = mock_user
return mock_client 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

View File

@ -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