diff --git a/data/les.py b/data/les.py deleted file mode 100644 index d2b6472..0000000 --- a/data/les.py +++ /dev/null @@ -1,143 +0,0 @@ -from enums.platforms import Platforms -from functions.timeFormatters import timeFromInt - - -# A container class for schedules -class Schedule: - def __init__(self, schedule: dict): - self.courses = [Course(course) for course in schedule] - self.customs = [] # Courses that only the person that called the schedule has - self.extra = [] # Courses that need special attention (canceled, online, ...) - - def addCustom(self, course): - """ - Function that adds a course into the list of courses, - useful for adding a user's custom courses - """ - self.customs.append(Course(course)) - - -# A container class for courses -class Course: - # Initialize a course from the dict that the JSON contains - def __init__(self, courseInfo: dict): - self.courseInfo = courseInfo - self.name = courseInfo["course"] - - self.slots = [] - self.initSlots() - - self.platforms = {} - self.initPlatforms() - - def initSlots(self): - """ - Function that creates Slot instances & adds them to the list - """ - for slot in self.courseInfo["slots"]: - self.slots.append(Slot(self, slot)) - - def initPlatforms(self): - """ - Function that creates Platform instances & adds them into the dict - """ - for platform in Platforms: - if platform["rep"] in self.courseInfo: - self.platforms[platform["rep"]] = Platform(platform["name"], self.courseInfo[platform["rep"]]) - - def getSlotsOnDay(self, day: str, week: int): - """ - Function that returns a list of all slots of this course - on a given day of the week - - This list then has duplicate days filtered out depending on - whether or not there is a special class on this day - """ - slots = [] - specials = [] - - for slot in self.slots: - # Skip slots on other days - if slot.day != day: - continue - - # This is a special slot that should only be added - # if the week corresponds - if slot.weeks and week not in slot.weeks: - continue - - if slot.special: - specials.append(slot) - else: - slots.append(slot) - -# Filter doubles out (special classes, ...) - for special in specials: - for slot in slots: - if slot.start == special.start and slot.end == special.end: - slots.remove(slot) - - return slots, specials - - -# TODO add an is_online field to the JSON to allow toggling -# temporary online classes easier -# A slot in a course -class Slot: - def __init__(self, course: Course, slot: dict): - self.course = course - self.day = slot["time"][0] - self.start = timeFromInt(slot["time"][1]) - self.end = timeFromInt(slot["time"][2]) - self.weeks = [] if "weeks" not in slot else slot["weeks"] - self.canceled = "canceled" in slot # Boolean indicating whether or not this class has been canceled - self.special = "weeks" in slot or self.canceled # Boolean indicating if this class is special or generic - - # TODO check if on-campus, else None - self.locations = self.setLocations(slot) - self.platform = self.course.platforms[slot["online"]] - - def setLocations(self, slot: dict): - """ - Function that creates a list of Location instances - """ - locations = [] - - # Slot has multiple locations - if "locations" in slot: - for location in slot["locations"]: - locations.append(Location(location)) - else: - # Slot has only one location - locations.append(Location(slot)) - - return locations - - def getLocations(self): - """ - Function that creates a string representation for this - slot's locations - """ - if self.locations is None: - return "" - - def getOnline(self): - pass - - -# A location where a course might take place -class Location: - def __init__(self, slot: dict): - self.campus = slot["campus"] - self.building = slot["building"] - self.room = slot["room"] - - def __str__(self): - return " ".join([self.campus, self.building, self.room]) - - -# A streaming platform -class Platform: - def __init__(self, name, url): - self.name = name - self.url = url diff --git a/data/schedule.py b/data/schedule.py new file mode 100644 index 0000000..45cdcd2 --- /dev/null +++ b/data/schedule.py @@ -0,0 +1,128 @@ +import json +from dacite import from_dict +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from enums.platforms import Platforms +from functions.config import get +from typing import Dict, Optional, List + +from functions.timeFormatters import fromArray + + +@dataclass +class Holiday: + start_list: List[int] + end_list: List[int] + start_date: datetime = field(init=False) + end_date: datetime = field(init=False) + duration: timedelta = field(init=False) + + def __post_init__(self): + self.start_date = fromArray(self.start_list) + self.end_date = fromArray(self.end_list) + self.duration = self.end_date - self.start_date + + def has_passed(self, current_day: datetime) -> bool: + """ + Check if a holiday has passed already + """ + return current_day > self.end_date + + +@dataclass +class Course: + day: str + week: int + course_dict: Dict + + +@dataclass +class Location: + campus: str + building: str + room: str + + +@dataclass +class Timeslot: + course: Course + start_time: int + end_time: int + canceled: bool = False + is_special: bool = False + location: Optional[Location] = None + online_link: Optional[str] = None + online_platform: Optional[Platforms] = None + + +@dataclass +class Schedule: + day: datetime + schedule_dict: Dict = field(init=False) + start_date: datetime + end_date: datetime + semester_over: bool = False + holiday_offset: int = 0 + current_holiday: Optional[Holiday] = None + + def __post_init__(self): + self.schedule_dict: Dict = self.load_schedule_file() + self.start_date = fromArray(self.schedule_dict["semester_start"]) + self.end_date = fromArray(self.schedule_dict["semester_end"]) + + # Semester is over + if self.end_date <= self.day: + self.semester_over = True + return + + self.check_holidays() + + # Show schedule for after holidays + if self.current_holiday is not None: + self.day = self.current_holiday.end_date + timedelta(days=1) + + def check_holidays(self): + """ + Do all holiday-related stuff here to avoid multiple loops + """ + for hol_entry in self.schedule_dict.get("holidays", []): + holiday: Holiday = from_dict(Holiday, hol_entry) + + # Hasn't happened yet, don't care + if holiday.start_date > self.day: + continue + + # In the past: add the offset + if holiday.has_passed(self.day): + self.holiday_offset += (self.day - holiday.end_date) // 7 + elif holiday.start_date <= self.day <= holiday.end_date: + self.current_holiday = holiday + + def load_schedule_file(self) -> Dict: + """ + Load the schedule from the JSON file + """ + semester = get("semester") + year = get("year") + + with open(f"files/{year}{semester}.json", "r") as fp: + return json.load(fp) + + def get_week(self) -> int: + """ + Get the current week of the semester + """ + diff: timedelta = self.day - self.start_date + + # Hasn't started yet, show week 1 + if diff.days < 0: + return 1 + + # 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 create_schedule(self): + """ + Create the schedule for the current week + """ + week: int = self.get_week() diff --git a/functions/les_rework.py b/functions/les_rework.py new file mode 100644 index 0000000..6646a4a --- /dev/null +++ b/functions/les_rework.py @@ -0,0 +1,58 @@ +from datetime import datetime, timedelta +from timeFormatters import dateTimeNow, weekdayToInt +from typing import Optional + + +def find_target_date(arg: Optional[str]) -> datetime: + """ + Find the requested date out of the user's arguments + """ + # Start at current date + day: datetime = dateTimeNow() + + # If no offset was provided, check the time + # otherwise the argument overrides it + if arg is None: + # When the command is used after 6 pm, show schedule + # for the next day instead + if day.hour > 18: + day += timedelta(days=1) + elif 0 <= (weekday := weekdayToInt(arg)) <= 4: # Weekday provided + day = forward_to_weekday(day, weekday) + elif arg.lower() == "morgen": # Tomorrow's schedule + day += timedelta(days=1) + elif arg.lower() == "overmorgen": # Day after tomorrow's schedule + day += timedelta(days=2) + + # Don't land on a weekend + day = skip_weekends(day) + + return day + + +def skip_weekends(day: datetime) -> datetime: + """ + Increment the current date if it's not a weekday + """ + weekday = day.weekday() + + # Friday is weekday 4 + if weekday > 4: + return day + timedelta(days=(7 - weekday)) + + return day + + +def forward_to_weekday(day: datetime, weekday: int) -> datetime: + """ + Increment a date until the weekday is the same as the one provided + Finds the "next" [weekday] + """ + current = day.weekday() + + # This avoids negative numbers below, and shows + # next week in case the days are the same + if weekday >= current: + weekday += 7 + + return day + timedelta(days=(weekday - current)) diff --git a/functions/timeFormatters.py b/functions/timeFormatters.py index b79dd74..58e6712 100644 --- a/functions/timeFormatters.py +++ b/functions/timeFormatters.py @@ -1,8 +1,12 @@ import datetime +from typing import List + import dateutil.relativedelta import pytz import time +from functions import stringFormatters + def epochToDate(epochTimeStamp, strFormat="%d/%m/%Y om %H:%M:%S"): now = dateTimeNow() @@ -134,8 +138,12 @@ def getPlural(amount, unit): return dic[unit.lower()]["s" if amount == 1 else "p"] -def weekdayToInt(day): +def weekdayToInt(day) -> int: days = {"maandag": 0, "dinsdag": 1, "woensdag": 2, "donderdag": 3, "vrijdag": 4, "zaterdag": 5, "zondag": 6} + + if day.lower() not in days: + return -1 + return days[day.lower()] @@ -143,8 +151,16 @@ def intToWeekday(day): return ["Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag", "Zondag"][day] -def fromString(timeString: str, formatString="%d/%m/%Y"): +def fromString(timeString: str, formatString="%d/%m/%Y", tzinfo=pytz.timezone("Europe/Brussels")): """ Constructs a datetime object from an input string """ - return datetime.datetime.strptime(timeString, formatString) + return datetime.datetime.strptime(timeString, formatString).replace(tzinfo=tzinfo) + + +def fromArray(data: List[int]) -> datetime: + day = stringFormatters.leadingZero(str(data[0])) + month = stringFormatters.leadingZero(str(data[1])) + year = str(data[2]) + + return fromString(f"{day}/{month}/{year}") diff --git a/requirements.txt b/requirements.txt index 77d3ae8..083a9f5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,6 @@ yarl==1.4.2 feedparser==6.0.2 googletrans==4.0.0rc1 quart==0.15.1 -Quart-CORS==0.5.0 \ No newline at end of file +Quart-CORS==0.5.0 +attrs~=21.2.0 +dacite~=1.6.0 \ No newline at end of file