Displaying deadlines

pull/127/head
stijndcl 2022-08-13 00:07:48 +02:00
parent a510e2fe4a
commit 107e4fb580
6 changed files with 169 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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}: <t:{round(datetime.timestamp(deadline.deadline))}:R>"
)
# 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

View File

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