Compare commits

...

13 Commits

Author SHA1 Message Date
Stijn De Clercq 41a1527e72 Fix typo in test 2021-08-08 22:24:05 +02:00
Stijn De Clercq 07e48e4046 Rename job 2021-08-08 22:23:23 +02:00
Stijn De Clercq aa1e28937c Add extra tests for schedules & timeformatters 2021-08-08 22:22:11 +02:00
Stijn De Clercq a97a35a13b Fix job name 2021-08-08 20:43:05 +02:00
Stijn De Clercq 76df656128 run tests on commit 2021-08-08 20:40:50 +02:00
Stijn De Clercq 445ca84834 Write some tests, fix imports 2021-08-08 20:26:53 +02:00
Stijn De Clercq 85f29e7afa Check for semester end 2021-08-08 19:32:41 +02:00
Stijn De Clercq e9ea063876 Show custom embed for weekends 2021-08-08 18:33:32 +02:00
Stijn De Clercq a198a83153 Clean up imports 2021-08-08 18:24:53 +02:00
Stijn De Clercq 8c6d3682b4 Remove old les code, fix food 2021-08-08 18:24:11 +02:00
Stijn De Clercq 9cebb8280e Special online links 2021-08-08 18:14:56 +02:00
Stijn De Clercq e8301ce8a2 Creating embeds 2021-08-08 18:12:16 +02:00
Stijn De Clercq b3854324d4 Special weeks 2021-08-08 15:18:45 +02:00
18 changed files with 428 additions and 464 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, schedule
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, les_rework 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)
@ -45,49 +45,20 @@ class School(commands.Cog):
# @commands.check(checks.allowedChannels) # @commands.check(checks.allowedChannels)
@help.Category(category=Category.School) @help.Category(category=Category.School)
async def les(self, ctx, day=None): async def les(self, ctx, day=None):
date = les_rework.find_target_date(day) date = les.find_target_date(day)
s = schedule.Schedule(date, day is not None)
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)
# 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)

View File

@ -1,10 +1,10 @@
import dacite from abc import ABC, abstractmethod
from dacite import from_dict from dacite import from_dict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timedelta from datetime import datetime, timedelta
from discord import Colour, Embed
from enums.platform import Platform, get_platform from enums.platform import Platform, get_platform
from functions.config import get from functions.timeFormatters import fromArray, intToWeekday, timeFromInt
from functions.timeFormatters import fromArray, forward_to_weekday, intToWeekday
import json import json
from typing import Dict, Optional, List from typing import Dict, Optional, List
@ -33,6 +33,9 @@ class Holiday:
class Course: class Course:
name: str name: str
def __str__(self):
return self.name
@dataclass @dataclass
class Location: class Location:
@ -40,6 +43,9 @@ class Location:
building: str building: str
room: str room: str
def __str__(self):
return f"{self.campus} {self.building} {self.room}"
@dataclass @dataclass
class Timeslot: class Timeslot:
@ -52,47 +58,86 @@ class Timeslot:
online_link: Optional[str] = None online_link: Optional[str] = None
online_platform: Optional[Platform] = 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 @staticmethod
def from_slot_dict(slot_dict: Dict, course_dict: Dict, current_week: int): def from_slot_dict(slot_dict: Dict, course_dict: Dict, current_week: int):
""" """
Construct a Timeslot from a dict of data Construct a Timeslot from a dict of data
""" """
special = False
if "weeks" in slot_dict and str(current_week) in slot_dict["weeks"]: if "weeks" in slot_dict and str(current_week) in slot_dict["weeks"]:
return Timeslot.special_from_dict(slot_dict, course_dict, str(current_week)) # 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"]) course = Course(course_dict["course"])
start_time = slot_dict["time"]["start"] start_time = slot_dict["time"]["start"]
end_time = slot_dict["time"]["end"] end_time = slot_dict["time"]["end"]
# Location can be none if a class is online-only # Location can be none if a class is online-only
location = dacite.from_dict(Location, slot_dict["location"]) if "location" in slot_dict else None location = from_dict(Location, slot_dict["location"]) if "location" in slot_dict else None
# Find platform & link if this class is online # Find platform & link if this class is online
online_platform: Platform = get_platform(slot_dict.get("online", None)) 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
return Timeslot(course=course, start_time=start_time, end_time=end_time, canceled=False, is_special=False, # Custom online link for this day if it exists, else the general link for this platform
location=location, online_platform=online_platform, online_link=online_link) 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)
@staticmethod
def special_from_dict(slot_dict: Dict, course_dict: Dict, current_week: str):
"""
Create a SPECIAL Timeslot from a dict and data
"""
course = Course(course_dict["course"])
# TODO
@dataclass @dataclass
class Schedule: class Schedule:
day: datetime 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) schedule_dict: Dict = field(init=False)
start_date: datetime = field(init=False) start_date: datetime = field(init=False)
end_date: datetime = field(init=False) end_date: datetime = field(init=False)
semester_over: bool = False semester_over: bool = False
holiday_offset: int = 0 holiday_offset: int = 0
current_holiday: Optional[Holiday] = None current_holiday: Optional[Holiday] = None
_weekday_str: str = field(init=False) weekday_str: str = field(init=False)
def __post_init__(self): def __post_init__(self):
self.schedule_dict: Dict = self.load_schedule_file() self.schedule_dict: Dict = self.load_schedule_file()
@ -100,16 +145,16 @@ class Schedule:
self.end_date = fromArray(self.schedule_dict["semester_end"]) self.end_date = fromArray(self.schedule_dict["semester_end"])
# Semester is over # Semester is over
if self.end_date <= self.day: if self.end_date < self.day:
self.semester_over = True self.semester_over = True
return return
self.check_holidays() 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 # # Store the target weekday (in case it exists) so we can ask for the next
# # friday after the holiday, for example # # 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 # # Show schedule for after holidays
# if self.current_holiday is not None: # if self.current_holiday is not None:
@ -120,9 +165,7 @@ class Schedule:
# if target_weekday != -1: # if target_weekday != -1:
# self.day = forward_to_weekday(self.day, target_weekday) # self.day = forward_to_weekday(self.day, target_weekday)
self._weekday_str = intToWeekday(self.day.weekday()) self.weekday_str = intToWeekday(self.day.weekday())
print(self.day)
def check_holidays(self): def check_holidays(self):
""" """
@ -137,7 +180,8 @@ class Schedule:
# In the past: add the offset # In the past: add the offset
if holiday.has_passed(self.day): if holiday.has_passed(self.day):
self.holiday_offset += (self.day - holiday.end_date_parsed) // 7 # 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: elif holiday.start_date_parsed <= self.day <= holiday.end_date_parsed:
self.current_holiday = holiday self.current_holiday = holiday
@ -145,10 +189,7 @@ class Schedule:
""" """
Load the schedule from the JSON file Load the schedule from the JSON file
""" """
semester = get_platform("semester") with open(f"files/schedules/{self.year}{self.semester}.json", "r") as fp:
year = get_platform("year")
with open(f"files/schedules/{year}{semester}.json", "r") as fp:
return json.load(fp) return json.load(fp)
def get_week(self) -> int: def get_week(self) -> int:
@ -162,9 +203,11 @@ class Schedule:
return 1 return 1
# Add +1 at the end because week 1 would be 0 as it's not over yet # 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 # 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, current_week: int) -> List[Timeslot]: def find_slots_for_course(self, course_dict: Dict) -> List[Timeslot]:
""" """
Create time timeslots for a course Create time timeslots for a course
""" """
@ -173,13 +216,13 @@ class Schedule:
# First create a list of all slots of today # First create a list of all slots of today
for slot in course_dict["slots"]: for slot in course_dict["slots"]:
# This slot is for a different day # This slot is for a different day
if slot["time"]["day"] != self._weekday_str.lower(): if slot["time"]["day"] != self.weekday_str.lower():
continue continue
slots_today.append(slot) slots_today.append(slot)
# Create Timeslots # 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 return slots_today
@ -187,6 +230,124 @@ class Schedule:
""" """
Create the schedule for the current week Create the schedule for the current week
""" """
week: int = self.get_week() if self.current_holiday is not None:
slots: List[List[Timeslot]] = [self.find_slots_for_course(course, week) for course in self.schedule_dict["schedule"]] 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] 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))

View File

@ -8,8 +8,10 @@ class Platform(Enum):
Name: The name of the platform Name: The name of the platform
Rep: A shorter, lowercased & space-less version 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"} MSTeams = {"name": "MS Teams", "rep": "msteams"}
OpenCast = {"name": "OpenCast", "rep": "opencast"}
Ufora = {"name": "Ufora", "rep": "ufora"} Ufora = {"name": "Ufora", "rep": "ufora"}
Zoom = {"name": "Zoom", "rep": "zoom"} Zoom = {"name": "Zoom", "rep": "zoom"}

View File

@ -1,10 +1,10 @@
{ {
"semester_start": [1, 7, 2021], "semester_start": [1, 7, 2021],
"semester_end": [16, 8, 2021], "semester_end": [1, 8, 2021],
"holidays": [ "holidays": [
{ {
"start_date": [2, 7, 2021], "start_date": [2, 7, 2021, 23, 59, 59],
"end_date": [10, 8, 2021] "end_date": [10, 8, 2021, 23, 59, 59]
} }
], ],
"schedule": [ "schedule": [
@ -36,7 +36,7 @@
} }
}, },
{ {
"online": "ZOOM", "online": "zoom",
"time": { "time": {
"day": "vrijdag", "day": "vrijdag",
"start": 1300, "start": 1300,
@ -52,7 +52,7 @@
}, },
"slots": [ "slots": [
{ {
"online": "ZOOM", "online": "zoom",
"time": { "time": {
"day": "dinsdag", "day": "dinsdag",
"start": 1130, "start": 1130,
@ -60,7 +60,7 @@
} }
}, },
{ {
"online": "ZOOM", "online": "zoom",
"time": { "time": {
"day": "woensdag", "day": "woensdag",
"start": 1500, "start": 1500,
@ -68,7 +68,7 @@
} }
}, },
{ {
"online": "ZOOM", "online": "zoom",
"time": { "time": {
"day": "donderdag", "day": "donderdag",
"start": 830, "start": 830,
@ -85,7 +85,7 @@
}, },
"slots": [ "slots": [
{ {
"online": "MS Teams", "online": "msteams",
"time": { "time": {
"day": "dinsdag", "day": "dinsdag",
"start": 1430, "start": 1430,
@ -93,7 +93,7 @@
} }
}, },
{ {
"online": "MS Teams", "online": "msteams",
"time": { "time": {
"day": "vrijdag", "day": "vrijdag",
"start": 830, "start": 830,
@ -110,7 +110,7 @@
"slots": [ "slots": [
{ {
"weeks": { "weeks": {
"1": { "6": {
"canceled": true "canceled": true
} }
}, },
@ -126,7 +126,7 @@
} }
}, },
{ {
"online": "ZOOM", "online": "zoom",
"time": { "time": {
"day": "donderdag", "day": "donderdag",
"start": 1000, "start": 1000,

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,31 +0,0 @@
from datetime import datetime, timedelta
from functions.timeFormatters import dateTimeNow, weekdayToInt, forward_to_weekday, skip_weekends
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)
# TODO show a different embed when "(over)morgen" is requested & it lands on a weekend
# Don't land on a weekend
day = skip_weekends(day)
return day

View File

@ -165,6 +165,13 @@ def fromArray(data: List[int]) -> datetime:
month = stringFormatters.leadingZero(str(data[1])) month = stringFormatters.leadingZero(str(data[1]))
year = str(data[2]) 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}") return fromString(f"{day}/{month}/{year}")

View File

@ -16,4 +16,5 @@ 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 attrs~=21.2.0
dacite~=1.6.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)