Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added /pastexams #129

Merged
merged 11 commits into from
Jul 13, 2023
1 change: 1 addition & 0 deletions uqcsbot/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ async def main():
"manage_cogs",
"member_counter",
"minecraft",
"past_exams",
"phonetics",
"remindme",
"snailrace",
Expand Down
109 changes: 109 additions & 0 deletions uqcsbot/past_exams.py
Original file line number Diff line number Diff line change
@@ -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)
49Indium marked this conversation as resolved.
Show resolved Hide resolved


async def setup(bot: commands.Bot):
await bot.add_cog(PastExams(bot))
49 changes: 48 additions & 1 deletion uqcsbot/utils/uq_course_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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):
Expand Down Expand Up @@ -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("<br/>")[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