diff --git a/uqcsbot/__main__.py b/uqcsbot/__main__.py index 13cc0b4..b91d4f1 100644 --- a/uqcsbot/__main__.py +++ b/uqcsbot/__main__.py @@ -56,6 +56,7 @@ async def main(): "manage_cogs", "member_counter", "minecraft", + "past_exams", "phonetics", "remindme", "snailrace", diff --git a/uqcsbot/past_exams.py b/uqcsbot/past_exams.py new file mode 100644 index 0000000..b51f032 --- /dev/null +++ b/uqcsbot/past_exams.py @@ -0,0 +1,109 @@ +from typing import Optional, Literal +import logging +from random import choice + +import discord +from discord import app_commands +from discord.ext import commands + +from uqcsbot.utils.uq_course_utils import ( + get_past_exams, + get_past_exams_page_url, + HttpException, +) + +SemesterType = Optional[Literal["Sem 1", "Sem 2", "Summer"]] + + +class PastExams(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @app_commands.command() + @app_commands.describe( + course_code="The course to find a past exam for.", + year="The year to find exams for. Leave blank for all years.", + semester="The semester to find exams for. Leave blank for all semesters.", + random_exam="Whether to select a single random exam.", + ) + async def pastexams( + self, + interaction: discord.Interaction, + course_code: str, + year: Optional[int] = None, + semester: SemesterType = None, + random_exam: bool = False, + ): + """ + Returns a list of past exams, or, if specified, a past exam for a specific year. + """ + await interaction.response.defer(thinking=True) + + try: + past_exams = get_past_exams(course_code) + except HttpException as exception: + logging.warning( + f"Received a HTTP response code {exception.status_code}. Error information: {exception.message}" + ) + await interaction.edit_original_response( + content=f"Could not successfully contact UQ for past exams." + ) + return + if not past_exams: + await interaction.edit_original_response( + content=f"No past exams could be found for {course_code}." + ) + return + + if semester: + past_exams = list( + filter(lambda exam: exam.semester == semester, past_exams) + ) + if year: + past_exams = list(filter(lambda exam: exam.year == year, past_exams)) + + if not past_exams: + await interaction.edit_original_response( + content=f"No past exams could be found for {course_code} matching your specifications." + ) + return + + if random_exam: + past_exams = [choice(past_exams)] + + if len(past_exams) == 1: + exam = past_exams[0] + embed = discord.Embed( + title=f"Past exam for {course_code.upper()}", + description=f"[{exam.year} {exam.semester}]({exam.link})", + ) + embed.set_footer( + text="The above link will require a UQ SSO login to access." + ) + await interaction.edit_original_response(embed=embed) + return + + description = "" + if year is not None or semester is not None: + description = "Only showing exams for " + " ".join( + str(restriction) + for restriction in [year, semester] + if restriction is not None + ) + embed = discord.Embed( + title=f"Past exams for {course_code.upper()}", + url=get_past_exams_page_url(course_code), + description=description, + ) + for exam in past_exams: + embed.add_field( + name="", + value=f"[{exam.year} {exam.semester}]({exam.link})", + inline=True, + ) + embed.set_footer(text="The above links will require a UQ SSO login to access.") + await interaction.edit_original_response(embed=embed) + + +async def setup(bot: commands.Bot): + await bot.add_cog(PastExams(bot)) diff --git a/uqcsbot/utils/uq_course_utils.py b/uqcsbot/utils/uq_course_utils.py index b5f7992..cd06a94 100644 --- a/uqcsbot/utils/uq_course_utils.py +++ b/uqcsbot/utils/uq_course_utils.py @@ -5,7 +5,8 @@ from bs4 import BeautifulSoup from functools import partial from binascii import hexlify -from typing import List, Dict, Optional +from typing import List, Dict, Optional, Iterable +import json BASE_COURSE_URL = "https://my.uq.edu.au/programs-courses/course.html?course_code=" BASE_ASSESSMENT_URL = ( @@ -14,6 +15,7 @@ ) BASE_CALENDAR_URL = "http://www.uq.edu.au/events/calendar_view.php?category_id=16&year=" OFFERING_PARAMETER = "offer" +BASE_PAST_EXAMS_URL = "https://api.library.uq.edu.au/v1/exams/search/" class DateSyntaxException(Exception): @@ -255,3 +257,48 @@ def get_parsed_assessment_item(assessment_item): # Thus, this bit of code will keep only the weight portion of the field. weight = get_element_inner_html(weight).strip().split("
")[0] return (course_name, task, due_date, weight) + + +class Exam: + """ + Stores the information of a past exam, including its year, semester and link. + """ + + def __init__(self, year: int, semester: str, link: str) -> None: + self.year = year + self.semester = semester + self.link = link + + +def get_past_exams_page_url(course_code: str) -> str: + """ + Returns the URL of the UQ library past exam page + """ + return BASE_PAST_EXAMS_URL + course_code + + +def get_past_exams(course_code: str) -> List[Exam]: + """ + Takes the course code and generates each result in the format: + ('year Sem X:', link) + """ + url = get_past_exams_page_url(course_code) + http_response = requests.get(url) + if http_response.status_code != requests.codes.ok: + raise HttpException(url, http_response.status_code) + # The UQ library API has some funky nested lists within the output, so there will be a a few "[0]" lying about + exam_list_json = json.loads(http_response.content)["papers"] + + # Check if the course code exists + if not exam_list_json: + return [] + exam_list_json = exam_list_json[0] + + exam_list = [] + for exam_json in exam_list_json: + year = int(exam_json[0]["examYear"]) + # Semesters are given as "Sem.1", so we will change this to "Sem 1" + semester = exam_json[0]["examPeriod"].replace(".", " ") + link = exam_json[0]["paperUrl"] + exam_list.append(Exam(year, semester, link)) + return exam_list