From 107e4fb580eeec936cec92d27cf9344eabab21f9 Mon Sep 17 00:00:00 2001 From: stijndcl Date: Sat, 13 Aug 2022 00:07:48 +0200 Subject: [PATCH] Displaying deadlines --- alembic/versions/08d21b2d1a0a_deadlines.py | 39 ++++++++++++++ database/crud/deadlines.py | 31 +++++++++++ database/schemas/relational.py | 17 ++++++ didier/cogs/school.py | 11 ++++ didier/data/embeds/deadlines.py | 63 ++++++++++++++++++++++ didier/utils/types/string.py | 9 +++- 6 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/08d21b2d1a0a_deadlines.py create mode 100644 database/crud/deadlines.py create mode 100644 didier/data/embeds/deadlines.py diff --git a/alembic/versions/08d21b2d1a0a_deadlines.py b/alembic/versions/08d21b2d1a0a_deadlines.py new file mode 100644 index 0000000..25147cf --- /dev/null +++ b/alembic/versions/08d21b2d1a0a_deadlines.py @@ -0,0 +1,39 @@ +"""Deadlines + +Revision ID: 08d21b2d1a0a +Revises: 3962636f3a3d +Create Date: 2022-08-12 23:44:13.947011 + +""" +import sqlalchemy as sa + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "08d21b2d1a0a" +down_revision = "3962636f3a3d" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "deadlines", + sa.Column("deadline_id", sa.Integer(), nullable=False), + sa.Column("course_id", sa.Integer(), nullable=True), + sa.Column("name", sa.Text(), nullable=False), + sa.Column("deadline", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint( + ["course_id"], + ["ufora_courses.course_id"], + ), + sa.PrimaryKeyConstraint("deadline_id"), + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("deadlines") + # ### end Alembic commands ### diff --git a/database/crud/deadlines.py b/database/crud/deadlines.py new file mode 100644 index 0000000..338a4c3 --- /dev/null +++ b/database/crud/deadlines.py @@ -0,0 +1,31 @@ +from zoneinfo import ZoneInfo + +from dateutil.parser import parse +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from database.schemas.relational import Deadline + +__all__ = ["add_deadline", "get_deadlines"] + + +async def add_deadline(session: AsyncSession, course_id: int, name: str, date_str: str): + """Add a new deadline""" + date_dt = parse(date_str).replace(tzinfo=ZoneInfo("Europe/Brussels")) + + if date_dt.hour == date_dt.minute == date_dt.second == 0: + date_dt.replace(hour=23, minute=59, second=59) + + deadline = Deadline(course_id=course_id, name=name, deadline=date_dt) + session.add(deadline) + await session.commit() + + +async def get_deadlines(session: AsyncSession) -> list[Deadline]: + """Get a list of all deadlines that are currently known + + This includes deadlines that have passed already + """ + statement = select(Deadline).options(selectinload(Deadline.course)) + return (await session.execute(statement)).scalars().all() diff --git a/database/schemas/relational.py b/database/schemas/relational.py index b0bc5e7..6fd27d0 100644 --- a/database/schemas/relational.py +++ b/database/schemas/relational.py @@ -28,6 +28,7 @@ __all__ = [ "CustomCommand", "CustomCommandAlias", "DadJoke", + "Deadline", "Link", "NightlyData", "Task", @@ -110,6 +111,19 @@ class DadJoke(Base): joke: str = Column(Text, nullable=False) +class Deadline(Base): + """A deadline for a university project""" + + __tablename__ = "deadlines" + + deadline_id: int = Column(Integer, primary_key=True) + course_id: int = Column(Integer, ForeignKey("ufora_courses.course_id")) + name: str = Column(Text, nullable=False) + deadline: datetime = Column(DateTime(timezone=True), nullable=False) + + course: UforaCourse = relationship("UforaCourse", back_populates="deadlines", uselist=False, lazy="selectin") + + class Link(Base): """Useful links that go useful places""" @@ -160,6 +174,9 @@ class UforaCourse(Base): aliases: list[UforaCourseAlias] = relationship( "UforaCourseAlias", back_populates="course", cascade="all, delete-orphan", lazy="selectin" ) + deadlines: list[Deadline] = relationship( + "Deadline", back_populates="course", cascade="all, delete-orphan", lazy="selectin" + ) class UforaCourseAlias(Base): diff --git a/didier/cogs/school.py b/didier/cogs/school.py index 881d0d5..3ccf412 100644 --- a/didier/cogs/school.py +++ b/didier/cogs/school.py @@ -5,7 +5,9 @@ from discord import app_commands from discord.ext import commands from database.crud import ufora_courses +from database.crud.deadlines import get_deadlines from didier import Didier +from didier.data.embeds.deadlines import Deadlines from didier.utils.discord.flags.school import StudyGuideFlags @@ -27,6 +29,15 @@ class School(commands.Cog): """Remove the commands when the cog is unloaded""" self.client.tree.remove_command(self._pin_ctx_menu.name, type=self._pin_ctx_menu.type) + @commands.hybrid_command(name="deadlines", description="Show upcoming deadlines") + async def deadlines(self, ctx: commands.Context): + """Show upcoming deadlines""" + async with self.client.postgres_session as session: + deadlines = await get_deadlines(session) + + embed = await Deadlines(deadlines).to_embed() + await ctx.reply(embed=embed, mention_author=False, ephemeral=False) + @commands.command(name="Pin", usage="[Message]") async def pin(self, ctx: commands.Context, message: Optional[discord.Message] = None): """Pin a message in the current channel""" diff --git a/didier/data/embeds/deadlines.py b/didier/data/embeds/deadlines.py new file mode 100644 index 0000000..0c07c40 --- /dev/null +++ b/didier/data/embeds/deadlines.py @@ -0,0 +1,63 @@ +import itertools +from datetime import datetime + +import discord +from overrides import overrides + +from database.schemas.relational import Deadline +from didier.data.embeds.base import EmbedBaseModel +from didier.utils.types.datetime import tz_aware_now +from didier.utils.types.string import get_edu_year_name + +__all__ = ["Deadlines"] + + +class Deadlines(EmbedBaseModel): + """Embed that shows all the deadlines of a semester""" + + deadlines: list[Deadline] + + def __init__(self, deadlines: list[Deadline]): + self.deadlines = deadlines + self.deadlines.sort(key=lambda deadline: deadline.deadline) + + @overrides + async def to_embed(self, **kwargs: dict) -> discord.Embed: + embed = discord.Embed(colour=discord.Colour.dark_gold()) + embed.set_author(name="Upcoming Deadlines") + now = tz_aware_now() + + has_active_deadlines = False + deadlines_grouped: dict[int, list[str]] = {} + + deadline: Deadline + for year, deadline in itertools.groupby(self.deadlines, key=lambda _deadline: _deadline.course.year): + if year not in deadlines_grouped: + deadlines_grouped[year] = [] + + passed = deadline.deadline <= now + if passed: + has_active_deadlines = True + + deadline_str = ( + f"{deadline.course.name} - {deadline.name}: " + ) + + # Strike through deadlines that aren't active anymore + deadlines_grouped[year].append(deadline_str if not passed else f"~~{deadline_str}~~") + + if not has_active_deadlines: + embed.description = "There are currently no upcoming deadlines." + embed.set_image(url="https://c.tenor.com/RUzJ3lDGQUsAAAAC/iron-man-you-can-rest-now.gif") + return embed + + for i in range(5): + if i not in deadlines_grouped: + continue + + name = get_edu_year_name(i) + description = "\n".join(deadlines_grouped[i]) + + embed.add_field(name=name, value=description, inline=False) + + return embed diff --git a/didier/utils/types/string.py b/didier/utils/types/string.py index 015996a..3a26658 100644 --- a/didier/utils/types/string.py +++ b/didier/utils/types/string.py @@ -1,7 +1,7 @@ import math from typing import Optional -__all__ = ["abbreviate", "leading", "pluralize"] +__all__ = ["abbreviate", "leading", "pluralize", "get_edu_year_name"] def abbreviate(text: str, max_length: int) -> str: @@ -43,3 +43,10 @@ def pluralize(word: str, amount: int, plural_form: Optional[str] = None) -> str: return word return plural_form or (word + "s") + + +def get_edu_year_name(year: int) -> str: + """Get the string representation of a university year""" + years = ["1st Bachelor", "2nd Bachelor", "3rd Bachelor", "1st Master", "2nd Master"] + + return years[year]