mirror of
https://github.com/nexy7574/LCC-bot.git
synced 2024-09-19 10:03:40 +01:00
Add timetable system
This commit is contained in:
parent
d389d0cb30
commit
c684b6f329
6 changed files with 424 additions and 13 deletions
|
@ -78,18 +78,22 @@ class AssignmentsCog(commands.Cog):
|
|||
async def reminder_loop(self):
|
||||
if not self.bot.is_ready():
|
||||
await self.bot.wait_until_ready()
|
||||
view_command = "</{0.name} view:{0.id}>".format(
|
||||
self.bot.get_application_command(
|
||||
"assignments",
|
||||
type=discord.SlashCommandGroup
|
||||
)
|
||||
)
|
||||
edit_command = "</{0.name} edit:{0.id}>".format(
|
||||
self.bot.get_application_command(
|
||||
"assignments",
|
||||
type=discord.SlashCommandGroup
|
||||
)
|
||||
)
|
||||
try:
|
||||
view_command = "</{0.name} view:{0.id}>".format(
|
||||
self.bot.get_application_command(
|
||||
"assignments",
|
||||
type=discord.SlashCommandGroup
|
||||
)
|
||||
)
|
||||
edit_command = "</{0.name} edit:{0.id}>".format(
|
||||
self.bot.get_application_command(
|
||||
"assignments",
|
||||
type=discord.SlashCommandGroup
|
||||
)
|
||||
)
|
||||
except AttributeError:
|
||||
view_command = "`/assignments view`"
|
||||
edit_command = "`/assignments edit`"
|
||||
allowed_mentions = discord.AllowedMentions(everyone=True) if not config.dev else discord.AllowedMentions.none()
|
||||
guild = self.bot.get_guild(config.guilds[0])
|
||||
general = discord.utils.get(guild.text_channels, name="general")
|
||||
|
|
176
cogs/timetable.py
Normal file
176
cogs/timetable.py
Normal file
|
@ -0,0 +1,176 @@
|
|||
import asyncio
|
||||
from typing import Optional, Union
|
||||
|
||||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
import json
|
||||
from pathlib import Path
|
||||
from utils import console
|
||||
from datetime import time, datetime, timedelta
|
||||
|
||||
|
||||
def schedule_times():
|
||||
times = []
|
||||
for h in range(24):
|
||||
for m in range(0, 60, 15):
|
||||
times.append(time(h, m, 0))
|
||||
return times
|
||||
|
||||
|
||||
class TimeTableCog(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
with (Path.cwd() / "utils" / "timetable.json").open() as file:
|
||||
self.timetable = json.load(file)
|
||||
self.update_status.start()
|
||||
|
||||
def cog_unload(self):
|
||||
self.update_status.stop()
|
||||
|
||||
def current_lesson(self, date: datetime = None) -> Optional[dict]:
|
||||
date = date or datetime.now()
|
||||
lessons = self.timetable[date.strftime("%A").lower()]
|
||||
if not lessons:
|
||||
return
|
||||
for lesson in lessons:
|
||||
now = date.time()
|
||||
start_time = time(*lesson["start"])
|
||||
end_time = time(*lesson["end"])
|
||||
if now >= end_time:
|
||||
continue
|
||||
elif now < start_time:
|
||||
continue
|
||||
else:
|
||||
# We are now currently actively in the lesson.
|
||||
lesson = lesson.copy()
|
||||
end_datetime = date.replace(hour=end_time.hour, minute=end_time.minute)
|
||||
lesson["end"] = end_time
|
||||
lesson["start"] = start_time
|
||||
lesson["end_datetime"] = end_datetime
|
||||
return lesson
|
||||
|
||||
def next_lesson(self, date: datetime = None) -> Optional[dict]:
|
||||
date = date or datetime.now()
|
||||
lessons = self.timetable.get(date.strftime("%A").lower(), []).copy()
|
||||
if not lessons:
|
||||
return
|
||||
for lesson in lessons:
|
||||
now = date.time()
|
||||
start_time = time(*lesson["start"])
|
||||
end_time = time(*lesson["end"])
|
||||
if now > end_time:
|
||||
continue
|
||||
elif now < start_time:
|
||||
lesson = lesson.copy()
|
||||
end_datetime = date.replace(hour=start_time.hour, minute=start_time.minute)
|
||||
lesson["end"] = end_time
|
||||
lesson["start"] = start_time
|
||||
lesson["start_datetime"] = end_datetime
|
||||
return lesson
|
||||
|
||||
def absolute_next_lesson(self, date: datetime = None) -> dict:
|
||||
# this function wastes so many CPU cycles.
|
||||
# Its also so computationally expensive that its async and cast to a thread to avoid blocking.
|
||||
# Why do I not just do this the smart way? I'm lazy and have a headache.
|
||||
lesson = None
|
||||
date = date or datetime.now()
|
||||
while lesson is None:
|
||||
lesson = self.next_lesson(date)
|
||||
if lesson is None:
|
||||
date += timedelta(minutes=5)
|
||||
else:
|
||||
break
|
||||
return lesson
|
||||
|
||||
async def update_timetable_message(
|
||||
self,
|
||||
message: Union[discord.Message, discord.Interaction],
|
||||
date: datetime = None,
|
||||
*,
|
||||
no_prefix: bool = False,
|
||||
):
|
||||
date = date or datetime.now()
|
||||
lesson = self.current_lesson(date)
|
||||
if not lesson:
|
||||
next_lesson = self.next_lesson(date)
|
||||
if not next_lesson:
|
||||
next_lesson = await asyncio.to_thread(
|
||||
self.absolute_next_lesson
|
||||
)
|
||||
text = "[tt] No more lessons today!\n" \
|
||||
f"[tt] Next Lesson: {next_lesson['name']!r} with {next_lesson['tutor']} in " \
|
||||
f"{next_lesson['room']} - " \
|
||||
f"Starts {discord.utils.format_dt(next_lesson['start_datetime'], 'R')}"
|
||||
|
||||
else:
|
||||
text = f"[tt] Next Lesson: {next_lesson['name']!r} with {next_lesson['tutor']} in " \
|
||||
f"{next_lesson['room']} - Starts {discord.utils.format_dt(next_lesson['start_datetime'], 'R')}"
|
||||
else:
|
||||
text = f"[tt] Current Lesson: {lesson['name']!r} with {lesson['tutor']} in {lesson['room']} - " \
|
||||
f"ends {discord.utils.format_dt(lesson['end_datetime'], 'R')}"
|
||||
|
||||
if no_prefix:
|
||||
text = text.replace("[tt] ", "")
|
||||
await message.edit(content=text, allowed_mentions=discord.AllowedMentions.none())
|
||||
|
||||
# noinspection DuplicatedCode
|
||||
@tasks.loop(time=schedule_times())
|
||||
async def update_status(self):
|
||||
if not self.bot.is_ready():
|
||||
await self.bot.wait_until_ready()
|
||||
guild: discord.Guild = self.bot.get_guild(994710566612500550)
|
||||
channel = discord.utils.get(guild.text_channels, name="timetable")
|
||||
channel = channel or discord.utils.get(guild.text_channels, name="general")
|
||||
channel: discord.TextChannel
|
||||
|
||||
async for _message in channel.history(limit=20, oldest_first=False):
|
||||
if _message.author == self.bot.user and _message.content.startswith("[tt]"):
|
||||
message = _message
|
||||
break
|
||||
else:
|
||||
message = await channel.send("[tt] (loading)")
|
||||
|
||||
message: discord.Message
|
||||
await self.update_timetable_message(message)
|
||||
|
||||
@commands.slash_command()
|
||||
async def lesson(self, ctx: discord.ApplicationContext, *, date: str = None):
|
||||
"""Shows the current/next lesson."""
|
||||
if date:
|
||||
try:
|
||||
date = datetime.strptime(date, "%d/%m/%Y %H:%M")
|
||||
except ValueError:
|
||||
return await ctx.respond("Invalid date (DD/MM/YYYY HH:MM).")
|
||||
else:
|
||||
date = datetime.now()
|
||||
await ctx.defer()
|
||||
await self.update_timetable_message(ctx.interaction, date, no_prefix=True)
|
||||
|
||||
@commands.slash_command(name="timetable")
|
||||
async def _timetable(self, ctx: discord.ApplicationContext, date: str = None):
|
||||
"""Shows the timetable for today/the specified date"""
|
||||
if date:
|
||||
try:
|
||||
date = datetime.strptime(date, "%d/%m/%Y")
|
||||
except ValueError:
|
||||
return await ctx.respond("Invalid date (DD/MM/YYYY).")
|
||||
else:
|
||||
date = datetime.now()
|
||||
|
||||
lessons = self.timetable[date.strftime("%A").lower()]
|
||||
if not lessons:
|
||||
return await ctx.respond(f"No lessons on {discord.utils.format_dt(date, 'D')}.")
|
||||
|
||||
blocks = [f"```\nTimetable for {date.strftime('%A')}:\n```"]
|
||||
for lesson in lessons:
|
||||
start_datetime = date.replace(hour=lesson["start"][0], minute=lesson["start"][1])
|
||||
end_datetime = date.replace(hour=lesson["end"][0], minute=lesson["end"][1])
|
||||
text = f"{discord.utils.format_dt(start_datetime, 't')} to {discord.utils.format_dt(end_datetime, 't')}" \
|
||||
f":\n> Lesson Name: {lesson['name']!r}" \
|
||||
f"> Tutor: **{lesson['tutor']}**\n> Room: `{lesson['room']}`"
|
||||
blocks.append(text)
|
||||
await ctx.respond("\n\n".join(blocks))
|
||||
|
||||
|
||||
def setup(bot):
|
||||
bot.add_cog(TimeTableCog(bot))
|
10
main.py
10
main.py
|
@ -11,7 +11,15 @@ bot = commands.Bot(
|
|||
intents=discord.Intents.default() + discord.Intents.members
|
||||
)
|
||||
|
||||
for ext in ["jishaku", "cogs.verify", "cogs.mod", "cogs.events", "cogs.assignments"]:
|
||||
extensions = [
|
||||
"jishaku",
|
||||
"cogs.verify",
|
||||
"cogs.mod",
|
||||
"cogs.events",
|
||||
"cogs.assignments",
|
||||
"cogs.timetable"
|
||||
]
|
||||
for ext in extensions:
|
||||
bot.load_extension(ext)
|
||||
console.log(f"Loaded extension [green]{ext}")
|
||||
bot.loop.run_until_complete(registry.create_all())
|
||||
|
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
100
tests/test_timetable.py
Normal file
100
tests/test_timetable.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
import pytest
|
||||
from pathlib import Path
|
||||
from utils import Tutors
|
||||
from typing import Union
|
||||
import warnings
|
||||
import json
|
||||
|
||||
file = (Path(__file__).parent.parent / "utils" / "timetable.json").resolve()
|
||||
|
||||
|
||||
def is_sane_time(time: list[int, int]) -> Union[bool, AssertionError]:
|
||||
def inner():
|
||||
hour, minute = time
|
||||
assert hour >= 9, "We aren't in college before 9am"
|
||||
assert hour <= 5, "We aren't in college after 5pm"
|
||||
assert minute in range(0, 60), "Invalid minute range - must be between (inclusive) 0 & 59"
|
||||
if minute % 15 != 0:
|
||||
warnings.warn(
|
||||
UserWarning(
|
||||
"Time '%s:%s' is probably not a valid timetable time, as lessons are every 15 minutes."
|
||||
)
|
||||
)
|
||||
return True
|
||||
try:
|
||||
return inner()
|
||||
except AssertionError as e:
|
||||
return e
|
||||
|
||||
|
||||
def get_lesson_times() -> list[list[int, int]]:
|
||||
data = json.loads(file.read_text())
|
||||
master = []
|
||||
for day, lessons in data.items():
|
||||
for lesson in lessons:
|
||||
master.append(lesson["start"])
|
||||
master.append(lesson["end"])
|
||||
return master
|
||||
|
||||
|
||||
@pytest.mark.dependency()
|
||||
def test_exists():
|
||||
assert file.exists()
|
||||
assert file.is_file()
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_exists"])
|
||||
def test_can_read():
|
||||
try:
|
||||
file.read_text()
|
||||
except (IOError, OSError):
|
||||
assert 0, "Unable to read file."
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_exists", "test_can_read"])
|
||||
def test_valid_json():
|
||||
try:
|
||||
data = json.loads(file.read_text())
|
||||
except (IOError, OSError, json.JSONDecodeError):
|
||||
data = None
|
||||
assert data is not None
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_exists", "test_can_read", "test_valid_json"])
|
||||
def test_valid_structure():
|
||||
data = json.loads(file.read_text())
|
||||
assert isinstance(data, dict)
|
||||
assert len(data) == 5, "insufficient days"
|
||||
for key, value in data.items():
|
||||
assert isinstance(key, str)
|
||||
assert key.islower()
|
||||
assert isinstance(value, list)
|
||||
for entry in value:
|
||||
assert isinstance(entry, dict)
|
||||
required_keys = {"name": 0, "start": 0, "end": 0, "tutor": 0, "room": 0}
|
||||
for entry_key, entry_value in entry.items():
|
||||
assert isinstance(entry_key, str)
|
||||
assert entry_key in required_keys, f"unknown key {entry_key!r}"
|
||||
required_keys[entry_key] = 1
|
||||
if isinstance(entry_value, list):
|
||||
assert len(entry_value) == 2
|
||||
assert [isinstance(x, int) for x in entry_value]
|
||||
else:
|
||||
assert isinstance(entry_value, str)
|
||||
|
||||
assert all(required_keys[k] for k in required_keys.keys())
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_exists", "test_can_read", "test_valid_json", "test_valid_structure"])
|
||||
@pytest.mark.parametrize("time", get_lesson_times())
|
||||
def test_sane_times(time: list[int, int]):
|
||||
assert is_sane_time(time), "insane lesson time: %s:%s" % time
|
||||
|
||||
|
||||
@pytest.mark.dependency(depends=["test_exists", "test_can_read", "test_valid_json"])
|
||||
def test_sane_tutors():
|
||||
data = json.loads(file.read_text())
|
||||
for day, lessons in data.items():
|
||||
for lesson in lessons:
|
||||
exists = getattr(Tutors, lesson["tutor"].upper(), None) is not None
|
||||
assert exists, "Tutor %s is invalid" % lesson["tutor"]
|
123
utils/timetable.json
Normal file
123
utils/timetable.json
Normal file
|
@ -0,0 +1,123 @@
|
|||
{
|
||||
"monday": [
|
||||
{
|
||||
"name": "Unit 1, 2 & 3 (PCE)",
|
||||
"start": [9, 0],
|
||||
"end": [10, 15],
|
||||
"tutor": "Jay",
|
||||
"room": "A113"
|
||||
},
|
||||
{
|
||||
"name": "Unit 10 & 11",
|
||||
"start": [10, 30],
|
||||
"end": [11, 45],
|
||||
"tutor": "Zach",
|
||||
"room": "A145"
|
||||
}
|
||||
],
|
||||
"tuesday": [
|
||||
{
|
||||
"name": "Unit 5",
|
||||
"start": [9, 0],
|
||||
"end": [10, 15],
|
||||
"tutor": "Zach",
|
||||
"room": "A229"
|
||||
},
|
||||
{
|
||||
"name": "1:1",
|
||||
"start": [10, 30],
|
||||
"end": [11, 45],
|
||||
"tutor": "Other",
|
||||
"room": "A113"
|
||||
},
|
||||
{
|
||||
"name": "Unit 7, 9 & 12",
|
||||
"start": [11, 45],
|
||||
"end": [13, 0],
|
||||
"tutor": "Ian",
|
||||
"room": "A145"
|
||||
},
|
||||
{
|
||||
"name": "Unit 1, 2, 6 & 8",
|
||||
"start": [15, 30],
|
||||
"end": [16, 45],
|
||||
"tutor": "Rebecca",
|
||||
"room": "A047"
|
||||
}
|
||||
],
|
||||
"wednesday": [],
|
||||
"thursday": [
|
||||
{
|
||||
"name": "Employer Set Project",
|
||||
"start": [9, 0],
|
||||
"end": [10, 15],
|
||||
"tutor": "Zach",
|
||||
"room": "A145"
|
||||
},
|
||||
{
|
||||
"name": "Unit 3 & 4",
|
||||
"start": [10, 30],
|
||||
"end": [11, 45],
|
||||
"tutor": "Lupupa",
|
||||
"room": "A145"
|
||||
},
|
||||
{
|
||||
"name": "Unit 1, 2, 6 & 8",
|
||||
"start": [11, 45],
|
||||
"end": [13, 0],
|
||||
"tutor": "Rebecca",
|
||||
"room": "A145"
|
||||
},
|
||||
{
|
||||
"name": "Unit 3 & 4",
|
||||
"start": [14, 0],
|
||||
"end": [15, 15],
|
||||
"tutor": "Lupupa",
|
||||
"room": "A145"
|
||||
},
|
||||
{
|
||||
"name": "Employer Readiness",
|
||||
"start": [15, 30],
|
||||
"end": [16, 45],
|
||||
"tutor": "Other",
|
||||
"room": "A227"
|
||||
}
|
||||
],
|
||||
"friday": [
|
||||
{
|
||||
"name": "Unit 5",
|
||||
"start": [9, 0],
|
||||
"end": [10, 15],
|
||||
"tutor": "Zach",
|
||||
"room": "A145"
|
||||
},
|
||||
{
|
||||
"name": "IBM Badges",
|
||||
"start": [10, 30],
|
||||
"end": [11, 45],
|
||||
"tutor": "Other",
|
||||
"room": "A145"
|
||||
},
|
||||
{
|
||||
"name": "1, 2, 6 & 8",
|
||||
"start": [11, 45],
|
||||
"end": [13, 0],
|
||||
"tutor": "Rebecca",
|
||||
"room": "A145"
|
||||
},
|
||||
{
|
||||
"name": "Group Tutorial",
|
||||
"start": [14, 0],
|
||||
"end": [15, 15],
|
||||
"tutor": "Other",
|
||||
"room": "Oracle"
|
||||
},
|
||||
{
|
||||
"name": "Unit 1, 2 & 3 (PCE)",
|
||||
"start": [15, 30],
|
||||
"end": [16, 45],
|
||||
"tutor": "Jay",
|
||||
"room": "A126"
|
||||
}
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue