diff --git a/cogs/school.py b/cogs/school.py index 72fd9a4..7d7ce9c 100644 --- a/cogs/school.py +++ b/cogs/school.py @@ -6,7 +6,7 @@ import discord from discord.ext import commands from enums.courses import years from enums.help_categories import Category -from functions import checks, eten, les, les_rework +from functions import checks, config, eten, les, les_rework import json @@ -46,8 +46,8 @@ class School(commands.Cog): @help.Category(category=Category.School) async def les(self, ctx, day=None): date = les_rework.find_target_date(day) - s = schedule.Schedule(date, day is not None) - return + s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), day is not None) + return await ctx.send(embed=s.create_schedule().to_embed()) # parsed = les.parseArgs(day) # # # Invalid arguments diff --git a/data/schedule.py b/data/schedule.py index e494855..c298b27 100644 --- a/data/schedule.py +++ b/data/schedule.py @@ -1,10 +1,11 @@ +from abc import ABC, abstractmethod import dacite +from discord import Colour, Embed from dacite import from_dict from dataclasses import dataclass, field from datetime import datetime, timedelta from enums.platform import Platform, get_platform -from functions.config import get -from functions.timeFormatters import fromArray, forward_to_weekday, intToWeekday +from functions.timeFormatters import fromArray, intToWeekday, timeFromInt import json from typing import Dict, Optional, List @@ -33,6 +34,9 @@ class Holiday: class Course: name: str + def __str__(self): + return self.name + @dataclass class Location: @@ -40,6 +44,9 @@ class Location: building: str room: str + def __str__(self): + return f"{self.campus} {self.building} {self.room}" + @dataclass class Timeslot: @@ -52,6 +59,33 @@ class Timeslot: online_link: Optional[str] = None online_platform: Optional[Platform] = None + def __str__(self): + time_str = f"{timeFromInt(self.start_time)} - {timeFromInt(self.end_time)}" + + return f"{time_str}: {self.course} {self._get_location_str()}" + + def get_link_str(self) -> Optional[str]: + if self.online_link is None or self.online_platform is None: + return None + + return f"[{self.online_platform.value.get('name')}]({self.online_link})" + + def _get_location_str(self, offline_prefix="in", online_prefix="**online** @") -> str: + return f"{offline_prefix} {self.location}" if self.location is not None \ + else f"{online_prefix} **{self.get_link_str()}**" + + def get_special_fmt_str(self) -> Optional[str]: + if not self.canceled and not self.is_special: + return None + + # This class was canceled + if self.canceled: + return f"{self.course} van {timeFromInt(self.start_time)} gaat vandaag **niet** door." + + # Something else is wrong + return f"{self.course} gaat vandaag door van **{timeFromInt(self.start_time)}** tot " \ + f"**{timeFromInt(self.end_time)}** {self._get_location_str(online_prefix='op')}" + @staticmethod def from_slot_dict(slot_dict: Dict, course_dict: Dict, current_week: int): """ @@ -79,7 +113,7 @@ class Timeslot: # Find platform & link if this class is online online_platform: Platform = get_platform(slot_dict.get("online", None)) - online_link = course_dict["online_links"][Platform.value["rep"]] if online_platform is not None else None + online_link = course_dict["online_links"][online_platform.value["rep"]] if online_platform is not None else None return Timeslot(course=course, start_time=start_time, end_time=end_time, canceled="canceled" in slot_dict, is_special=special, location=location, online_platform=online_platform, online_link=online_link) @@ -88,14 +122,17 @@ class Timeslot: @dataclass class Schedule: day: datetime - targetted_weekday: bool = False + year: int + semester: int + targeted_weekday: bool = False + week: int = field(init=False) schedule_dict: Dict = field(init=False) start_date: datetime = field(init=False) end_date: datetime = field(init=False) semester_over: bool = False holiday_offset: int = 0 current_holiday: Optional[Holiday] = None - _weekday_str: str = field(init=False) + weekday_str: str = field(init=False) def __post_init__(self): self.schedule_dict: Dict = self.load_schedule_file() @@ -108,11 +145,12 @@ class Schedule: return self.check_holidays() + self.week = self.get_week() # TODO show a custom embed when no class instead of fast-forwarding # # Store the target weekday (in case it exists) so we can ask for the next # # friday after the holiday, for example - # target_weekday = -1 if not self.targetted_weekday else self.day.weekday() + # target_weekday = -1 if not self.targeted_weekday else self.day.weekday() # # # Show schedule for after holidays # if self.current_holiday is not None: @@ -123,9 +161,7 @@ class Schedule: # if target_weekday != -1: # self.day = forward_to_weekday(self.day, target_weekday) - self._weekday_str = intToWeekday(self.day.weekday()) - - print(self.day) + self.weekday_str = intToWeekday(self.day.weekday()) def check_holidays(self): """ @@ -140,7 +176,7 @@ class Schedule: # In the past: add the offset if holiday.has_passed(self.day): - self.holiday_offset += (self.day - holiday.end_date_parsed) // 7 + self.holiday_offset += (self.day - holiday.end_date_parsed).days // 7 elif holiday.start_date_parsed <= self.day <= holiday.end_date_parsed: self.current_holiday = holiday @@ -148,10 +184,7 @@ class Schedule: """ Load the schedule from the JSON file """ - semester = get("semester") - year = get("year") - - with open(f"files/schedules/{year}{semester}.json", "r") as fp: + with open(f"files/schedules/{self.year}{self.semester}.json", "r") as fp: return json.load(fp) def get_week(self) -> int: @@ -167,7 +200,7 @@ class Schedule: # Add +1 at the end because week 1 would be 0 as it's not over yet return (diff.days // 7) + self.holiday_offset + 1 - def find_slots_for_course(self, course_dict: Dict, current_week: int) -> List[Timeslot]: + def find_slots_for_course(self, course_dict: Dict) -> List[Timeslot]: """ Create time timeslots for a course """ @@ -176,13 +209,13 @@ class Schedule: # First create a list of all slots of today for slot in course_dict["slots"]: # This slot is for a different day - if slot["time"]["day"] != self._weekday_str.lower(): + if slot["time"]["day"] != self.weekday_str.lower(): continue slots_today.append(slot) # Create Timeslots - slots_today = list(map(lambda x: Timeslot.from_slot_dict(x, course_dict, current_week), slots_today)) + slots_today = list(map(lambda x: Timeslot.from_slot_dict(x, course_dict, self.week), slots_today)) return slots_today @@ -190,6 +223,124 @@ class Schedule: """ Create the schedule for the current week """ - week: int = self.get_week() - slots: List[List[Timeslot]] = [self.find_slots_for_course(course, week) for course in self.schedule_dict["schedule"]] + if self.current_holiday is not None: + return HolidayEmbed(self) + + slots: List[List[Timeslot]] = [self.find_slots_for_course(course) for course in self.schedule_dict["schedule"]] slots_flattened = [item for sublist in slots for item in sublist] + + # Sort by timestamp + slots_flattened.sort(key=lambda x: x.start_time) + not_canceled = list(filter(lambda x: not x.canceled, slots_flattened)) + + # All classes are canceled + if not not_canceled: + return NoClassEmbed(self, slots_flattened) + + return ScheduleEmbed(self, slots_flattened, not_canceled) + + +@dataclass +class LesEmbed(ABC): + """ + Abstract base class for Les embeds + """ + schedule: Schedule + + def get_author(self) -> str: + level = "Bachelor" if self.schedule.year < 4 else "Master" + year = self.schedule.year if self.schedule.year < 4 else self.schedule.year - 3 + suffix = "ste" if self.schedule.year == 1 else "de" + return f"Lessenrooster voor {year}{suffix} {level}" + + def get_title(self) -> str: + date = self.schedule.day.strftime("%d/%m/%Y") + return f"{self.schedule.weekday_str} {date}" + + def get_footer(self) -> str: + return f"Semester {self.schedule.semester} | Lesweek {self.schedule.week}" + + def get_extras(self) -> str: + return "" + + def get_online_links(self) -> str: + return "" + + @abstractmethod + def get_description(self) -> str: + pass + + def to_embed(self) -> Embed: + embed = Embed(title=self.get_title(), colour=Colour.blue()) + embed.set_author(name=self.get_author()) + embed.set_footer(text=self.get_footer()) + + embed.description = self.get_description() + + # Add links if there are any + links = self.get_online_links() + if links: + embed.add_field(name="Online links", value=links, inline=False) + + # Add extras if there are any + extras = self.get_extras() + if extras: + embed.add_field(name="Extra", value=extras, inline=False) + + return embed + + +@dataclass +class HolidayEmbed(LesEmbed): + """ + Class for a Les embed sent during holidays + """ + def get_description(self) -> str: + date = self.schedule.current_holiday.end_date_parsed.strftime("%d/%m/%Y") + return f"Het is momenteel **vakantie** tot en met **{date}**." + + +@dataclass +class NoClassEmbed(LesEmbed): + """ + Class for a Les embed when all classes are canceled or there are none at all + """ + slots: List[Timeslot] + + def get_description(self) -> str: + return "Geen les" + + def get_extras(self) -> str: + canceled = list(filter(lambda x: x.canceled, self.slots)) + if not canceled: + return "" + + return "\n".join(list(entry.get_special_fmt_str() for entry in canceled)) + + +@dataclass +class ScheduleEmbed(LesEmbed): + """ + Class for a successful schedule + """ + slots: List[Timeslot] + slots_not_canceled: List[Timeslot] + + def get_description(self) -> str: + return "\n".join(list(f"{entry}" for entry in self.slots_not_canceled)) + + def get_extras(self) -> str: + special = list(filter(lambda x: x.is_special or x.canceled, self.slots)) + + if not special: + return "" + + return "\n".join(list(entry.get_special_fmt_str() for entry in special)) + + def get_online_links(self) -> str: + has_link = list(filter(lambda x: x.online_link is not None, self.slots)) + + if not has_link: + return "" + + return "\n".join(list(f"{entry.course}: **{entry.get_link_str()}**" for entry in has_link)) diff --git a/enums/platform.py b/enums/platform.py index b9a9520..436309f 100644 --- a/enums/platform.py +++ b/enums/platform.py @@ -8,8 +8,10 @@ class Platform(Enum): Name: The name of the platform Rep: A shorter, lowercased & space-less version """ - Bongo = {"name": "Bongo Virtual Classroom", "rep": "bongo"} + BongoVC = {"name": "Bongo Virtual Classroom", "rep": "bongo"} + GatherTown = {"name": "Gather Town", "rep": "gather"} MSTeams = {"name": "MS Teams", "rep": "msteams"} + OpenCast = {"name": "OpenCast", "rep": "opencast"} Ufora = {"name": "Ufora", "rep": "ufora"} Zoom = {"name": "Zoom", "rep": "zoom"} diff --git a/files/schedules/31.json b/files/schedules/31.json index a11e805..f40f33d 100644 --- a/files/schedules/31.json +++ b/files/schedules/31.json @@ -3,8 +3,8 @@ "semester_end": [16, 8, 2021], "holidays": [ { - "start_date": [2, 7, 2021], - "end_date": [10, 8, 2021] + "start_date": [2, 7, 2021, 23, 59, 59], + "end_date": [10, 8, 2021, 23, 59, 59] } ], "schedule": [ @@ -110,7 +110,7 @@ "slots": [ { "weeks": { - "1": { + "6": { "canceled": true } }, diff --git a/functions/timeFormatters.py b/functions/timeFormatters.py index 3d49476..4460156 100644 --- a/functions/timeFormatters.py +++ b/functions/timeFormatters.py @@ -165,6 +165,13 @@ def fromArray(data: List[int]) -> datetime: month = stringFormatters.leadingZero(str(data[1])) year = str(data[2]) + if len(data) == 6: + hour = stringFormatters.leadingZero(str(data[3])) + minute = stringFormatters.leadingZero(str(data[4])) + second = stringFormatters.leadingZero(str(data[5])) + + return fromString(f"{day}/{month}/{year} {hour}:{minute}:{second}", formatString="%d/%m/%Y %H:%M:%S") + return fromString(f"{day}/{month}/{year}") diff --git a/tests/test_data/test_schedule.py b/tests/test_data/test_schedule.py new file mode 100644 index 0000000..5d7d235 --- /dev/null +++ b/tests/test_data/test_schedule.py @@ -0,0 +1,5 @@ +import unittest + + +class TestSchedule(unittest.TestCase): + pass