Merge pull request #84 from stijndcl/les_rework

Les rework + tests
pull/86/head
Stijn De Clercq 2021-08-08 22:47:01 +02:00 committed by GitHub
commit 153aa04490
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 795 additions and 391 deletions

15
.github/workflows/test.yml vendored 100644
View File

@ -0,0 +1,15 @@
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

View File

@ -1,13 +1,11 @@
import random from data import schedule
from data import constants
from decorators import help from decorators import help
import discord import discord
from discord.ext import commands from discord.ext import commands
from enums.courses import years
from enums.help_categories import Category from enums.help_categories import Category
from functions import checks, eten, les from functions import config, eten, les
import json from functions.stringFormatters import capitalize
from functions.timeFormatters import intToWeekday, skip_weekends
class School(commands.Cog): class School(commands.Cog):
@ -23,7 +21,9 @@ class School(commands.Cog):
# @commands.check(checks.allowedChannels) # @commands.check(checks.allowedChannels)
@help.Category(category=Category.School) @help.Category(category=Category.School)
async def eten(self, ctx, *day): async def eten(self, ctx, *day):
day = les.getWeekDay(None if len(day) == 0 else day)[1] day_dt = les.find_target_date(day if day else None)
day_dt = skip_weekends(day_dt)
day = intToWeekday(day_dt.weekday())
# Create embed # Create embed
menu = eten.etenScript(day) menu = eten.etenScript(day)
@ -44,48 +44,21 @@ class School(commands.Cog):
@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) # @commands.check(checks.allowedChannels)
@help.Category(category=Category.School) @help.Category(category=Category.School)
async def les(self, ctx, *day): async def les(self, ctx, day=None):
return date = les.find_target_date(day)
# 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)
# Add all the user's courses # Person explicitly requested a weekend-day
def customizeSchedule(self, ctx, year, semester): if day is not None and day.lower() in ("morgen", "overmorgen") and date.weekday() > 4:
schedule = les.getSchedule(semester, year) return await ctx.send(f"{capitalize(day)} is het weekend.")
COC = self.client.get_guild(int(constants.CallOfCode)) date = skip_weekends(date)
if COC is None: s = schedule.Schedule(date, int(config.get("year")), int(config.get("semester")), day is not None)
return schedule
member = COC.get_member(ctx.author.id) if s.semester_over:
return await ctx.send("Het semester is afgelopen.")
for role in member.roles: return await ctx.send(embed=s.create_schedule().to_embed())
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]") @commands.command(name="Pin", usage="[Message]")
@help.Category(category=Category.School) @help.Category(category=Category.School)

353
data/schedule.py 100644
View File

@ -0,0 +1,353 @@
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))

30
enums/platform.py 100644
View File

@ -0,0 +1,30 @@
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

View File

@ -1 +1 @@
{"semester": "2", "year": "2", "years": 2, "jpl": 161733, "jpl_day": 24} {"semester": "1", "year": "3", "years": 3, "jpl": 161733, "jpl_day": 24}

View File

@ -0,0 +1,139 @@
{
"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
}
}
]
}
]
}

View File

@ -26,7 +26,7 @@ def etenScript(weekDag):
# Fetch from API # Fetch from API
try: try:
menu = requests.get("https://zeus.ugent.be/hydra/api/2.0/resto/menu/nl/{}/{}/{}.json".format(d.year, d.month, d.day)).json() menu = requests.get("https://zeus.ugent.be/hydra/api/2.0/resto/menu/nl-sterre/{}/{}/{}.json".format(d.year, d.month, d.day)).json()
# Print menu # Print menu
for s in menu["meals"]: for s in menu["meals"]:

View File

@ -1,340 +1,27 @@
import datetime from datetime import datetime, timedelta
import discord from functions.timeFormatters import dateTimeNow, weekdayToInt, forward_to_weekday
from functions import config, timeFormatters, stringFormatters from typing import Optional
from functions.numbers import clamp
import json
# TODO use constants & enums instead of hardcoding platform names def find_target_date(arg: Optional[str]) -> datetime:
# 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):
""" """
Function that finds the datetime object that corresponds to Find the requested date out of the user's arguments
the next occurence of [targetWeekday].
:param targetWeekday: The weekday to find
""" """
now = timeFormatters.dateTimeNow() # Start at current date
while now.weekday() != targetWeekday: day: datetime = dateTimeNow()
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)
def getCourses(schedule, day, week): return day
"""
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]

View File

@ -1,8 +1,12 @@
import datetime import datetime
from typing import List
import dateutil.relativedelta import dateutil.relativedelta
import pytz import pytz
import time import time
from functions import stringFormatters
def epochToDate(epochTimeStamp, strFormat="%d/%m/%Y om %H:%M:%S"): def epochToDate(epochTimeStamp, strFormat="%d/%m/%Y om %H:%M:%S"):
now = dateTimeNow() now = dateTimeNow()
@ -134,17 +138,66 @@ def getPlural(amount, unit):
return dic[unit.lower()]["s" if amount == 1 else "p"] return dic[unit.lower()]["s" if amount == 1 else "p"]
def weekdayToInt(day): def weekdayToInt(day: str) -> int:
days = {"maandag": 0, "dinsdag": 1, "woensdag": 2, "donderdag": 3, "vrijdag": 4, "zaterdag": 5, "zondag": 6} days = {"maandag": 0, "dinsdag": 1, "woensdag": 2, "donderdag": 3, "vrijdag": 4, "zaterdag": 5, "zondag": 6}
return days[day.lower()]
# Allow abbreviations
for d, i in days.items():
if d.startswith(day):
return i
return -1
def intToWeekday(day): def intToWeekday(day):
return ["Maandag", "Dinsdag", "Woensdag", "Donderdag", "Vrijdag", "Zaterdag", "Zondag"][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 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])
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))

View File

@ -14,4 +14,7 @@ yarl==1.4.2
feedparser==6.0.2 feedparser==6.0.2
googletrans==4.0.0rc1 googletrans==4.0.0rc1
quart==0.15.1 quart==0.15.1
Quart-CORS==0.5.0 Quart-CORS==0.5.0
attrs~=21.2.0
dacite~=1.6.0
pytest==6.2.4

View File

View File

@ -0,0 +1,93 @@
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())

View File

View File

@ -0,0 +1,64 @@
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)

View File

@ -1,6 +0,0 @@
import unittest
if __name__ == "__main__":
suite = unittest.TestLoader().discover('.', pattern="test_*.py")
unittest.TextTestRunner(verbosity=3).run(suite)