diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 0c33832..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,15 +0,0 @@ -name: Run Tests - -on: - push: - -jobs: - python-tests: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: '3.9.5' - - run: pip3 install -r requirements.txt - - run: pytest tests \ No newline at end of file diff --git a/cogs/school.py b/cogs/school.py index 29f8ec0..89e68fe 100644 --- a/cogs/school.py +++ b/cogs/school.py @@ -1,11 +1,13 @@ -from data import schedule +import random + +from data import constants from decorators import help import discord from discord.ext import commands +from enums.courses import years from enums.help_categories import Category -from functions import config, eten, les -from functions.stringFormatters import capitalize -from functions.timeFormatters import intToWeekday, skip_weekends +from functions import checks, eten, les +import json class School(commands.Cog): @@ -21,9 +23,7 @@ class School(commands.Cog): # @commands.check(checks.allowedChannels) @help.Category(category=Category.School) async def eten(self, ctx, *day): - day_dt = les.find_target_date(day if day else None) - day_dt = skip_weekends(day_dt) - day = intToWeekday(day_dt.weekday()) + day = les.getWeekDay(None if len(day) == 0 else day)[1] # Create embed menu = eten.etenScript(day) @@ -41,24 +41,51 @@ class School(commands.Cog): embed.set_footer(text="Omwille van de coronamaatregelen is er een beperkter aanbod, en kan je enkel nog eten afhalen. Ter plaatse eten is niet meer mogelijk.") await ctx.send(embed=embed) - # @commands.command(name="Les", aliases=["Class", "Classes", "Sched", "Schedule"], usage="[Jaargang]* [Dag]*") + @commands.command(name="Les", aliases=["Class", "Classes", "Sched", "Schedule"], usage="[Jaargang]* [Dag]*") # @commands.check(checks.allowedChannels) - # @help.Category(category=Category.School) - async def les(self, ctx, day=None): - date = les.find_target_date(day) + @help.Category(category=Category.School) + async def les(self, ctx, *day): + return + # parsed = les.parseArgs(day) + # + # # Invalid arguments + # if not parsed[0]: + # return await ctx.send(parsed[1]) + # + # day, dayDatetime, semester, year = parsed[1:] + # + # # Customize the user's schedule + # schedule = self.customizeSchedule(ctx, year, semester) + # + # # Create the embed + # embed = les.createEmbed(day, dayDatetime, semester, year, schedule) + # + # await ctx.send(embed=embed) - # Person explicitly requested a weekend-day - if day is not None and day.lower() in ("morgen", "overmorgen") and date.weekday() > 4: - return await ctx.send(f"{capitalize(day)} is het weekend.") + # Add all the user's courses + def customizeSchedule(self, ctx, year, semester): + schedule = les.getSchedule(semester, year) - date = skip_weekends(date) + COC = self.client.get_guild(int(constants.CallOfCode)) - s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), day is not None) + if COC is None: + return schedule - if s.semester_over: - return await ctx.send("Het semester is afgelopen.") + member = COC.get_member(ctx.author.id) - return await ctx.send(embed=s.create_schedule().to_embed()) + for role in member.roles: + for univYear in years: + for course in univYear: + if course.value["year"] < year and course.value["id"] == role.id and course.value["semester"] == semester: + with open("files/schedules/{}{}.json".format(course.value["year"], course.value["semester"]), + "r") as fp: + sched2 = json.load(fp) + + for val in sched2: + if val["course"] == course.value["name"]: + val["custom"] = course.value["year"] + schedule.append(val) + return schedule @commands.command(name="Pin", usage="[Message]") @help.Category(category=Category.School) diff --git a/data/schedule.py b/data/schedule.py deleted file mode 100644 index f286b60..0000000 --- a/data/schedule.py +++ /dev/null @@ -1,353 +0,0 @@ -from abc import ABC, abstractmethod -from dacite import from_dict -from dataclasses import dataclass, field -from datetime import datetime, timedelta -from discord import Colour, Embed -from enums.platform import Platform, get_platform -from functions.timeFormatters import fromArray, intToWeekday, timeFromInt -import json -from typing import Dict, Optional, List - - -@dataclass -class Holiday: - start_date: List[int] - end_date: List[int] - start_date_parsed: datetime = field(init=False) - end_date_parsed: datetime = field(init=False) - duration: timedelta = field(init=False) - - def __post_init__(self): - self.start_date_parsed = fromArray(self.start_date) - self.end_date_parsed = fromArray(self.end_date) - self.duration = self.end_date_parsed - self.start_date_parsed - - def has_passed(self, current_day: datetime) -> bool: - """ - Check if a holiday has passed already - """ - return current_day > self.end_date_parsed - - -@dataclass -class Course: - name: str - - def __str__(self): - return self.name - - -@dataclass -class Location: - campus: str - building: str - room: str - - def __str__(self): - return f"{self.campus} {self.building} {self.room}" - - -@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[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): - """ - Construct a Timeslot from a dict of data - """ - special = False - - if "weeks" in slot_dict and str(current_week) in slot_dict["weeks"]: - # If at least one thing was changed, this slot requires extra attention - special = True - # Overwrite the normal data with the customized entries - slot_dict.update(slot_dict["weeks"][str(current_week)]) - - # Only happens online, not on-campus - online_only = slot_dict["weeks"][str(current_week)].get("online_only", False) - if online_only: - slot_dict.pop("location") - - course = Course(course_dict["course"]) - start_time = slot_dict["time"]["start"] - end_time = slot_dict["time"]["end"] - - # Location can be none if a class is online-only - location = from_dict(Location, slot_dict["location"]) if "location" in slot_dict else None - - # Find platform & link if this class is online - online_platform: Platform = get_platform(slot_dict.get("online", None)) - - # Custom online link for this day if it exists, else the general link for this platform - online_link = \ - slot_dict["online_link"] if "online_link" in slot_dict else \ - 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) - - -@dataclass -class Schedule: - day: datetime - 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) - - 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() - self.week = self.get_week() - - # # 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.targeted_weekday else self.day.weekday() - # - # # Show schedule for after holidays - # if self.current_holiday is not None: - # # Set day to day after holiday - # self.day = self.current_holiday.end_date_parsed + timedelta(days=1) - # - # # Find the next [DAY] after the holidays - # if target_weekday != -1: - # self.day = forward_to_weekday(self.day, target_weekday) - - self.weekday_str = intToWeekday(self.day.weekday()) - - 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_parsed > self.day: - continue - - # In the past: add the offset - if holiday.has_passed(self.day): - # Add 1 because Monday-Sunday is only 6 days, but should be counted as a week - self.holiday_offset += (holiday.duration.days + 1) // 7 - elif holiday.start_date_parsed <= self.day <= holiday.end_date_parsed: - self.current_holiday = holiday - - def load_schedule_file(self) -> Dict: - """ - Load the schedule from the JSON file - """ - with open(f"files/schedules/{self.year}{self.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 - # Every week would be one behind - # Also subtract all passed holidays - return (diff.days // 7) - self.holiday_offset + 1 - - def find_slots_for_course(self, course_dict: Dict) -> List[Timeslot]: - """ - Create time timeslots for a course - """ - slots_today = [] - - # 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(): - continue - - slots_today.append(slot) - - # Create Timeslots - slots_today = list(map(lambda x: Timeslot.from_slot_dict(x, course_dict, self.week), slots_today)) - - return slots_today - - def create_schedule(self): - """ - Create the schedule for the current week - """ - 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 deleted file mode 100644 index 436309f..0000000 --- a/enums/platform.py +++ /dev/null @@ -1,30 +0,0 @@ -from enum import Enum -from typing import Optional - - -class Platform(Enum): - """ - An Enum to represent online class platforms - Name: The name of the platform - Rep: A shorter, lowercased & space-less version - """ - 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"} - - -def get_platform(rep: Optional[str]) -> Optional[Platform]: - """ - Find the platform that corresponds to the given name - """ - if rep is None: - return None - - for platform in Platform: - if platform.value["rep"] == rep: - return platform - - return None diff --git a/files/config.json b/files/config.json index 3d513ac..dd9aec4 100644 --- a/files/config.json +++ b/files/config.json @@ -1 +1 @@ -{"semester": "1", "year": "3", "years": 3, "jpl": 161733, "jpl_day": 24} \ No newline at end of file +{"semester": "2", "year": "2", "years": 2, "jpl": 161733, "jpl_day": 24} \ No newline at end of file diff --git a/files/schedules/31.json b/files/schedules/31.json deleted file mode 100644 index 8edbf90..0000000 --- a/files/schedules/31.json +++ /dev/null @@ -1,139 +0,0 @@ -{ - "semester_start": [1, 7, 2021], - "semester_end": [1, 8, 2021], - "holidays": [ - { - "start_date": [2, 7, 2021, 23, 59, 59], - "end_date": [10, 8, 2021, 23, 59, 59] - } - ], - "schedule": [ - { - "course": "Computerarchitectuur", - "online_links": { - "zoom": "https://ufora.ugent.be/d2l/ext/rp/228912/lti/framedlaunch/556e197e-e87b-4c27-be5d-53adc7a41826", - "msteams": "https://teams.microsoft.com/l/team/19%3ad7295f0bc4634a61b461504d4a7134b3%40thread.tacv2/conversations?groupId=8755cb96-1ef5-4ea3-b806-eeebf8a85ae8&tenantId=d7811cde-ecef-496c-8f91-a1786241b99c" - }, - "slots": [ - ] - }, - { - "course": "Multimedia", - "online_links": { - "zoom": "https://ugent-be.zoom.us/j/94248831947?pwd=ZCt4UnBLSzViZnFEQmkzWE5SYnF2QT09" - }, - "slots": [ - { - "location": { - "campus": "Sterre", - "building": "S9", - "room": "A3" - }, - "time": { - "day": "woensdag", - "start": 1130, - "end": 1330 - } - }, - { - "online": "zoom", - "time": { - "day": "vrijdag", - "start": 1300, - "end": 1530 - } - } - ] - }, - { - "course": "Wetenschappelijk Rekenen", - "online_links": { - "zoom": "https://ufora.ugent.be/d2l/ext/rp/236404/lti/framedlaunch/556e197e-e87b-4c27-be5d-53adc7a41826" - }, - "slots": [ - { - "online": "zoom", - "time": { - "day": "dinsdag", - "start": 1130, - "end": 1300 - } - }, - { - "online": "zoom", - "time": { - "day": "woensdag", - "start": 1500, - "end": 1800 - } - }, - { - "online": "zoom", - "time": { - "day": "donderdag", - "start": 830, - "end": 1000 - } - } - ] - }, - { - "course": "Software Engineering Lab 1", - "online_links": { - "zoom": "https://ufora.ugent.be/d2l/ext/rp/235800/lti/framedlaunch/556e197e-e87b-4c27-be5d-53adc7a41826", - "msteams": "https://teams.microsoft.com/l/team/19%3a4dfd5b2fb1ae4aa9b72706aa3a0d6867%40thread.tacv2/conversations?groupId=256d5c58-5d53-43f5-9436-497b0c852c75&tenantId=d7811cde-ecef-496c-8f91-a1786241b99c" - }, - "slots": [ - { - "online": "msteams", - "time": { - "day": "dinsdag", - "start": 1430, - "end": 1700 - } - }, - { - "online": "msteams", - "time": { - "day": "vrijdag", - "start": 830, - "end": 1130 - } - } - ] - }, - { - "course": "Webdevelopment", - "online_links": { - "zoom": "https://ugent-be.zoom.us/j/93166767783?pwd=MWdvb1BnNnlPSnAyNk52QmRzdjcwdz09" - }, - "slots": [ - { - "weeks": { - "6": { - "canceled": true - } - }, - "location": { - "campus": "Sterre", - "building": "S9", - "room": "A3" - }, - "time": { - "day": "woensdag", - "start": 900, - "end": 1100 - } - }, - { - "online": "zoom", - "time": { - "day": "donderdag", - "start": 1000, - "end": 1300 - } - } - ] - } - ] -} \ No newline at end of file diff --git a/functions/eten.py b/functions/eten.py index e95d2d2..efba819 100644 --- a/functions/eten.py +++ b/functions/eten.py @@ -26,7 +26,7 @@ def etenScript(weekDag): # Fetch from API try: - menu = requests.get("https://zeus.ugent.be/hydra/api/2.0/resto/menu/nl-sterre/{}/{}/{}.json".format(d.year, d.month, d.day)).json() + menu = requests.get("https://zeus.ugent.be/hydra/api/2.0/resto/menu/nl/{}/{}/{}.json".format(d.year, d.month, d.day)).json() # Print menu for s in menu["meals"]: diff --git a/functions/les.py b/functions/les.py index c3a7cfb..822491e 100644 --- a/functions/les.py +++ b/functions/les.py @@ -1,27 +1,340 @@ -from datetime import datetime, timedelta -from functions.timeFormatters import dateTimeNow, weekdayToInt, forward_to_weekday -from typing import Optional +import datetime +import discord +from functions import config, timeFormatters, stringFormatters +from functions.numbers import clamp +import json -def find_target_date(arg: Optional[str]) -> datetime: +# TODO use constants & enums instead of hardcoding platform names +# also make the naming in the jsons more consistent + +def createCourseString(courses): + courseString = "" + for course in sorted(courses, key=lambda item: item["slot"]["time"][1]): + # Add a ":" to the hour + add a leading "0" if needed + start = timeFormatters.timeFromInt(course["slot"]["time"][1]) + end = timeFormatters.timeFromInt(course["slot"]["time"][2]) + courseString += "{} - {}: {} {}\n".format(start, end, + str(course["course"]), getLocation(course["slot"])) + return courseString + + +def createEmbed(day, dayDatetime, semester, year, schedule): + # Create a date object to check the current week + startDate = 1612224000 + currentTime = dayDatetime.timestamp() + + # TODO don't clamp because week 1 is calculated as week 0!! + week = clamp(timeFormatters.timeIn(currentTime - startDate, "weeks")[0], 1, 13) + + # Compensate for easter holidays + # Sorry but I don't have time to make a clean solution for this rn + # this will have to do + # Does -1 instead of -2 because weeks were 0-indexed all along + week -= 1 + + title, week = getTitle(day, dayDatetime, week) + + # Add all courses & their corresponding times + locations of today + courses, extras, prev, online = getCourses(schedule, day, week) + + embed = discord.Embed(colour=discord.Colour.blue(), title=title) + embed.set_author(name="Lessenrooster voor {}{} Bachelor".format(year, "ste" if year == 1 else "de")) + + if len(courses) == 0: + embed.add_field(name="Geen Les", value="Geen Les", inline=False) + else: + courseString = createCourseString(courses) + # TODO uncomment this when covid rules slow down + # courseString += "\nGroep {} heeft vandaag online les.".format(1 if week % 2 == 0 else 2) + embed.description = courseString + + if prev: + embed.add_field(name="Vakken uit vorige jaren", value=createCourseString(prev), inline=False) + + if extras: + embed.add_field(name="Extra", value="\n".join(getExtras(extra) for extra in extras), inline=False) + + # Add online links - temporarily removed because everything is online right now + if online: + uniqueLinks: dict = getUniqueLinks(online) + embed.add_field(name="Online Links", value="\n".join( + sorted(getLinks(onlineClass, links) for onlineClass, links in uniqueLinks.items()))) + + embed.set_footer(text="Semester {} | Lesweek {}".format(semester, round(week))) + return embed + + +def findDate(targetWeekday): """ - Find the requested date out of the user's arguments + Function that finds the datetime object that corresponds to + the next occurence of [targetWeekday]. + :param targetWeekday: The weekday to find """ - # Start at current date - day: datetime = dateTimeNow() + now = timeFormatters.dateTimeNow() + while now.weekday() != targetWeekday: + now = now + datetime.timedelta(days=1) + return now - # 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) - return day +def getCourses(schedule, day, week): + """ + Function that creates a list of all courses of this day, + a list of all online links, and extra information for these courses. + :param schedule: A user's (customized) schedule + :param day: The current weekday + :param week: The current week + """ + # Add all courses & their corresponding times + locations of today + courses = [] + extras = [] + prev = [] + onlineLinks = [] + + for course in schedule: + for slot in course["slots"]: + if day in slot["time"]: + # Basic dict containing the course name & the class' time slot + classDic = {"course": course["course"], "slot": slot} + + # Class was canceled + if "canceled" in slot and "weeks" in slot and week in slot["weeks"]: + extras.append(classDic) + continue + + # Add online links for those at home + # Check if link hasn't been added yet + if "online" in slot and not any(el["course"] == course["course"] and + # Avoid KeyErrors: if either of these don't have an online link yet, + # add it as well + ("online" not in el or el["online"] == slot["online"]) + for el in onlineLinks): + # Some courses have multiple links on the same day, + # add all of them + if "bongo" in slot["online"].lower(): + onlineDic = {"course": course["course"], "online": "Bongo Virtual Classroom", + "link": course["bongo"]} + onlineLinks.append(onlineDic) + + if "zoom" in slot["online"].lower(): + onlineDic = {"course": course["course"], "online": "ZOOM", "link": course["zoom"]} + onlineLinks.append(onlineDic) + + if "teams" in slot["online"].lower(): + onlineDic = {"course": course["course"], "online": "MS Teams", "link": course["msteams"]} + onlineLinks.append(onlineDic) + + # Add this class' bongo, msteams & zoom links + if "bongo" in course: + classDic["slot"]["bongo"] = course["bongo"] + + if "msteams" in course: + classDic["slot"]["msteams"] = course["msteams"] + + if "zoom" in course: + classDic["slot"]["zoom"] = course["zoom"] + + if "custom" in course: + prev.append(classDic) + + # Check for special classes + if "weeks" in slot and "online" not in slot: + if week in slot["weeks"]: + if "custom" not in course: + courses.append(classDic) + extras.append(classDic) + elif "weeks" in slot and "online" in slot and "group" not in slot: + # This class is only online for this week + if week in slot["weeks"]: + if "custom" not in course: + courses.append(classDic) + extras.append(classDic) + else: + # Nothing special happening, just add it to the list of courses + # in case this is a course for everyone in this year + if "custom" not in course: + courses.append(classDic) + + # Filter out normal courses that are replaced with special courses + for extra in extras: + for course in courses: + if course["slot"]["time"] == extra["slot"]["time"] and course != extra: + courses.remove(course) + break + + # Sort online links alphabetically + onlineLinks.sort(key=lambda x: x["course"]) + + # Remove links of canceled classes + for element in onlineLinks: + if not any(c["course"] == element["course"] for c in courses): + onlineLinks.remove(element) + + return courses, extras, prev, onlineLinks + + +def getExtras(extra): + """ + Function that returns a formatted string giving clear info + when a course is happening somewhere else (or canceled). + """ + start = timeFormatters.timeFromInt(extra["slot"]["time"][1]) + end = timeFormatters.timeFromInt(extra["slot"]["time"][2]) + + location = getLocation(extra["slot"]) + + if "canceled" in extra["slot"]: + return "De les **{}** van **{}** tot **{}** gaat vandaag uitzonderlijk **niet** door.".format( + extra["course"], start, end + ) + + if "group" in extra["slot"]: + return "**Groep {}** heeft vandaag uitzonderlijk **{}** **{}** van **{} tot {}**.".format( + extra["slot"]["group"], extra["course"], location, + start, end + ) + elif "online" in extra["slot"]: + return "**{}** gaat vandaag uitzonderlijk **online** door {} van **{} tot {}**.".format( + extra["course"], location[7:], + start, end + ) + else: + return "**{}** vindt vandaag uitzonderlijk plaats **{}** van **{} tot {}**.".format( + extra["course"], location, + start, end + ) + + +def getUniqueLinks(onlineClasses): + """ + Function that returns a dict of all online unique online links for every class + in case some classes have multiple links on the same day. + """ + # Create a list of all unique course names + courseNames = list(set(oc["course"] for oc in onlineClasses)) + uniqueLinks: dict = {} + + # Add every link of every class into the dict + for name in courseNames: + uniqueLinks[name] = {} + for oc in onlineClasses: + if oc["course"] == name: + # Add the link for this platform + uniqueLinks[name][oc["online"]] = oc["link"] + + return uniqueLinks + + +def getLinks(onlineClass, links): + """ + Function that returns a formatted string giving a hyperlink + to every online link for this class today. + """ + return "{}: {}".format(onlineClass, + " | ".join( + ["**[{}]({})**".format(platform, url) for platform, url in + links.items()]) + ) + + +def getLocation(slot): + """ + Function that returns a formatted string indicating where this course + is happening. + """ + if "canceled" in slot: + return None + + # TODO fix this because it's ugly + if "online" in slot: + return "online @ **[{}]({})**".format(slot["online"], + slot["zoom"] if slot["online"] == "ZOOM" else slot["msteams"] if slot[ + "online"] == "MS Teams" else + slot["bongo"]) + + # Check for courses in multiple locations + if "locations" in slot: + # Language - 'en' for the last one + return ", ".join(getLocation(location) for location in slot["locations"][:-1]) \ + + " en " + getLocation(slot["locations"][-1]) + return "in {} {} {}".format(slot["campus"], slot["building"], slot["room"]) + + +def getSchedule(semester, year): + with open("files/schedules/{}{}.json".format(year, semester), "r") as fp: + schedule = json.load(fp) + + return schedule + + +def getTitle(day, dayDT, week): + # now = timeFormatters.dateTimeNow() + # if timeFormatters.weekdayToInt(day) < now.weekday(): + # week += 1 + + day = day[0].upper() + day[1:].lower() + + titleString = "{} {}/{}/{}".format(day, stringFormatters.leadingZero(dayDT.day), + stringFormatters.leadingZero(dayDT.month), dayDT.year) + return titleString, week + + +# Returns the day of the week, while keeping track of weekends +def getWeekDay(day=None): + weekDays = ["maandag", "dinsdag", "woensdag", "donderdag", "vrijdag"] + + # Get current day of the week + dayNumber = datetime.datetime.today().weekday() + # If a day or a modifier was passed, show that day instead + if day is not None: + if day[0] == "morgen": + dayNumber += 1 + elif day[0] == "overmorgen": + dayNumber += 2 + else: + for i in range(5): + if weekDays[i].startswith(day): + dayNumber = i + # Weekends should be skipped + dayNumber = dayNumber % 7 + if dayNumber > 4: + dayNumber = 0 + + # Get daystring + return dayNumber, weekDays[dayNumber] + + +def parseArgs(day): + semester = int(config.get("semester")) + year = int(config.get("year")) + years_counter = int(config.get("years")) + # Check if a schedule or a day was called + if len(day) == 0: + day = [] + else: + # Only either of them was passed + if len(day) == 1: + # Called a schedule + if day[0].isdigit(): + if 0 < int(day[0]) < years_counter + 1: + year = int(day[0]) + day = [] + else: + return [False, "Dit is geen geldige jaargang."] + # elif: calling a weekday is automatically handled below, + # so checking is obsolete + else: + # TODO check other direction (di 1) in else + # Both were passed + if day[0].isdigit(): + if 0 < int(day[0]) < years_counter + 1: + year = int(day[0]) + # day = [] + else: + return [False, "Dit is geen geldige jaargang."] + # Cut the schedule from the string + day = day[1:] + day = getWeekDay(None if len(day) == 0 else day)[1] + dayDatetime = findDate(timeFormatters.weekdayToInt(day)) + + return [True, day, dayDatetime, semester, year] diff --git a/functions/scraping.py b/functions/scraping.py index 7b642ec..6735c5c 100644 --- a/functions/scraping.py +++ b/functions/scraping.py @@ -54,11 +54,11 @@ def getMatchweek(): bs = BeautifulSoup(resp.text, "html.parser") matchdays = bs.find_all("section", attrs={"class": "sc-matchdays"}) - if len(matchdays) == 0: + if len(matchdays) < 2: return None # Table header - header = matchdays[0] + header = matchdays[1] # Regex to find current matchday r = re.compile(r"speeldag\s*\d+", flags=re.I) diff --git a/functions/timeFormatters.py b/functions/timeFormatters.py index 4460156..b79dd74 100644 --- a/functions/timeFormatters.py +++ b/functions/timeFormatters.py @@ -1,12 +1,8 @@ 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() @@ -138,66 +134,17 @@ def getPlural(amount, unit): return dic[unit.lower()]["s" if amount == 1 else "p"] -def weekdayToInt(day: str) -> int: +def weekdayToInt(day): days = {"maandag": 0, "dinsdag": 1, "woensdag": 2, "donderdag": 3, "vrijdag": 4, "zaterdag": 5, "zondag": 6} - - # Allow abbreviations - for d, i in days.items(): - if d.startswith(day): - return i - - return -1 + return days[day.lower()] def intToWeekday(day): return ["Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag", "Zondag"][day] -def fromString(timeString: str, formatString="%d/%m/%Y", tzinfo=pytz.timezone("Europe/Brussels")): +def fromString(timeString: str, formatString="%d/%m/%Y"): """ Constructs a datetime object from an input string """ - 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]) - - 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}") - - -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 + datetime.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 + datetime.timedelta(days=(weekday - current)) + return datetime.datetime.strptime(timeString, formatString) diff --git a/requirements.txt b/requirements.txt index dc9dcba..77d3ae8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,4 @@ yarl==1.4.2 feedparser==6.0.2 googletrans==4.0.0rc1 quart==0.15.1 -Quart-CORS==0.5.0 -attrs~=21.2.0 -dacite~=1.6.0 -pytest==6.2.4 \ No newline at end of file +Quart-CORS==0.5.0 \ No newline at end of file diff --git a/tests/data/__init__.py b/tests/data/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/data/test_schedule.py b/tests/data/test_schedule.py deleted file mode 100644 index 5c9da8c..0000000 --- a/tests/data/test_schedule.py +++ /dev/null @@ -1,93 +0,0 @@ -from data import schedule -from datetime import datetime -from enums.platform import Platform -import pytz -from unittest import TestCase -from unittest.mock import patch - - -class TestSchedule(TestCase): - def test_holiday_has_passed(self): - tz = pytz.timezone("Europe/Brussels") - before = datetime(2020, 8, 8, tzinfo=tz) - during = datetime(2021, 6, 2, tzinfo=tz) - after = datetime(2021, 8, 8, tzinfo=tz) - - holiday = schedule.Holiday([1, 6, 2021], [2, 7, 2021]) - - self.assertFalse(holiday.has_passed(before)) - self.assertFalse(holiday.has_passed(during)) - self.assertTrue(holiday.has_passed(after)) - - def test_timeslot_link(self): - slot = schedule.Timeslot(schedule.Course("a"), 1234, 5678) - self.assertEqual(None, slot.get_link_str()) - - slot = schedule.Timeslot(schedule.Course("a"), 1234, 5678, online_link="link", online_platform=Platform.Zoom) - self.assertEqual("[Zoom](link)", slot.get_link_str()) - - @patch("data.schedule.Schedule.check_holidays") - @patch("data.schedule.Schedule.load_schedule_file") - def test_schedule_semester_over(self, mock_load, mock_check_holidays): - mock_load.return_value = {"semester_start": [1, 2, 2020], "semester_end": [4, 5, 2021]} - dt = datetime(2021, 8, 8, tzinfo=pytz.timezone("Europe/Brussels")) - - s = schedule.Schedule(dt, 3, 1) - self.assertTrue(s.semester_over) - - # Check that the code stopped running in case the semester is over - mock_check_holidays.assert_not_called() - - @patch("data.schedule.Schedule.load_schedule_file") - def test_schedule_holidays(self, mock_load): - mock_load.return_value = { - "semester_start": [6, 7, 2021], "semester_end": [20, 8, 2021], - "holidays": [ - {"start_date": [1, 8, 2021], "end_date": [10, 8, 2021]} - ] - } - - # During holiday - dt = datetime(2021, 8, 8, tzinfo=pytz.timezone("Europe/Brussels")) - s = schedule.Schedule(dt, 3, 1) - self.assertNotEqual(None, s.current_holiday) - - # Not during holiday - dt = datetime(2021, 8, 15, tzinfo=pytz.timezone("Europe/Brussels")) - s = schedule.Schedule(dt, 3, 1) - self.assertEqual(None, s.current_holiday) - - @patch("data.schedule.Schedule.load_schedule_file") - def test_schedule_holiday_offset(self, mock_load): - # Week 1, no holidays - mock_load.return_value = { - "semester_start": [2, 8, 2021], "semester_end": [20, 8, 2021] - } - - dt = datetime(2021, 8, 6, tzinfo=pytz.timezone("Europe/Brussels")) - s = schedule.Schedule(dt, 3, 1) - self.assertEqual(1, s.get_week()) - - # Week 1, one off-day doesn't change the week - mock_load.return_value = { - "semester_start": [2, 8, 2021], "semester_end": [20, 8, 2021], - "holidays": [ - {"start_date": [5, 8, 2021], "end_date": [5, 8, 2021]} - ] - } - - s = schedule.Schedule(dt, 3, 1) - self.assertEqual(1, s.get_week()) - - # Week 3, with a one-week holiday in between - mock_load.return_value = { - "semester_start": [2, 8, 2021], "semester_end": [20, 8, 2021], - "holidays": [ - {"start_date": [5, 8, 2021], "end_date": [5, 8, 2021]}, - {"start_date": [9, 8, 2021], "end_date": [15, 8, 2021]} - ] - } - - dt = datetime(2021, 8, 19, tzinfo=pytz.timezone("Europe/Brussels")) - s = schedule.Schedule(dt, 3, 1) - self.assertEqual(2, s.get_week()) diff --git a/tests/functions/__init__.py b/tests/functions/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/functions/test_timeFormatters.py b/tests/functions/test_timeFormatters.py deleted file mode 100644 index 790c013..0000000 --- a/tests/functions/test_timeFormatters.py +++ /dev/null @@ -1,64 +0,0 @@ -from datetime import datetime -from functions import timeFormatters -import unittest - - -class TestTimeFormatters(unittest.TestCase): - def test_leadingZero(self): - self.assertEqual("0123", timeFormatters.leadingZero("123")) - self.assertEqual("0123", timeFormatters.leadingZero("0123")) - - def test_delimiter(self): - self.assertEqual("01:23", timeFormatters.delimiter("0123")) - self.assertEqual("01.23", timeFormatters.delimiter("0123", delim=".")) - - def test_timeFromInt(self): - self.assertEqual("01:23", timeFormatters.timeFromInt(123)) - self.assertEqual("12:34", timeFormatters.timeFromInt(1234)) - - def test_fromArray(self): - # Only day/month/year - d, m, y = 1, 2, 2021 - inp = [d, m, y] - - dt = timeFormatters.fromArray(inp) - self.assertEqual(d, dt.day) - self.assertEqual(m, dt.month) - self.assertEqual(y, dt.year) - - # Also hours/minutes/seconds - d, m, y, hh, mm, ss = 1, 2, 2021, 1, 2, 3 - inp = [d, m, y, hh, mm, ss] - - dt = timeFormatters.fromArray(inp) - self.assertEqual(d, dt.day) - self.assertEqual(m, dt.month) - self.assertEqual(y, dt.year) - self.assertEqual(hh, dt.hour) - self.assertEqual(mm, dt.minute) - self.assertEqual(ss, dt.second) - - def test_skipWeekends(self): - # Already a weekday - weekday = datetime(2021, 8, 11) - skipped = timeFormatters.skip_weekends(weekday) - self.assertEqual(weekday, skipped) - - # Weekend - weekend = datetime(2021, 8, 7) - skipped = timeFormatters.skip_weekends(weekend) - self.assertEqual(0, skipped.weekday()) - - def test_forwardToWeekday(self): - mo = datetime(2021, 8, 10) - # Before - forwarded = timeFormatters.forward_to_weekday(mo, 2) - self.assertEqual(1, (forwarded - mo).days) - - # Same day - forwarded = timeFormatters.forward_to_weekday(mo, 1) - self.assertEqual(7, (forwarded - mo).days) - - # After - forwarded = timeFormatters.forward_to_weekday(mo, 0) - self.assertEqual(6, (forwarded - mo).days) diff --git a/tests/run_tests.py b/tests/run_tests.py new file mode 100644 index 0000000..4b64cea --- /dev/null +++ b/tests/run_tests.py @@ -0,0 +1,6 @@ +import unittest + + +if __name__ == "__main__": + suite = unittest.TestLoader().discover('.', pattern="test_*.py") + unittest.TextTestRunner(verbosity=3).run(suite) diff --git a/tests/__init__.py b/tests/test_data/__init__.py similarity index 100% rename from tests/__init__.py rename to tests/test_data/__init__.py diff --git a/tests/data/test_regexes.py b/tests/test_data/test_regexes.py similarity index 100% rename from tests/data/test_regexes.py rename to tests/test_data/test_regexes.py diff --git a/tests/data/test_snipe.py b/tests/test_data/test_snipe.py similarity index 100% rename from tests/data/test_snipe.py rename to tests/test_data/test_snipe.py