From 05c1e052d0ed9ed0cfed2e522b255407bfc4c01d Mon Sep 17 00:00:00 2001 From: bradleysigma <42644678+bradleysigma@users.noreply.github.com> Date: Sat, 28 Oct 2023 09:28:08 +1000 Subject: [PATCH 1/2] Remove Ununimplemented (#168) * Remove Ununimplemented * Delete cookbook.py --------- Co-authored-by: Andrew Brown <92134285+andrewj-brown@users.noreply.github.com> --- unimplemented/calendar.py | 74 --------------- unimplemented/channel_log.py | 13 --- unimplemented/coin.py | 20 ---- unimplemented/cookbook.py | 10 -- unimplemented/dominos.py | 116 ----------------------- unimplemented/emoji_log.py | 37 -------- unimplemented/emojify.py | 173 ----------------------------------- unimplemented/history.py | 56 ------------ unimplemented/id.py | 9 -- unimplemented/link.py | 163 --------------------------------- unimplemented/pastexams.py | 67 -------------- unimplemented/wavie.py | 22 ----- unimplemented/whoami.py | 18 ---- unimplemented/xkcd.py | 125 ------------------------- 14 files changed, 903 deletions(-) delete mode 100644 unimplemented/calendar.py delete mode 100644 unimplemented/channel_log.py delete mode 100644 unimplemented/coin.py delete mode 100644 unimplemented/cookbook.py delete mode 100644 unimplemented/dominos.py delete mode 100644 unimplemented/emoji_log.py delete mode 100644 unimplemented/emojify.py delete mode 100644 unimplemented/history.py delete mode 100644 unimplemented/id.py delete mode 100644 unimplemented/link.py delete mode 100644 unimplemented/pastexams.py delete mode 100644 unimplemented/wavie.py delete mode 100644 unimplemented/whoami.py delete mode 100644 unimplemented/xkcd.py diff --git a/unimplemented/calendar.py b/unimplemented/calendar.py deleted file mode 100644 index a14806b0..00000000 --- a/unimplemented/calendar.py +++ /dev/null @@ -1,74 +0,0 @@ -from datetime import datetime -from icalendar import Calendar, Event -from uuid import uuid4 as uuid -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status, success_status -from uqcsbot.utils.uq_course_utils import (get_course_assessment, - get_parsed_assessment_due_date, - HttpException, - CourseNotFoundException, - ProfileNotFoundException, - DateSyntaxException) - -# Maximum number of courses supported by !calendar to reduce call abuse. -COURSE_LIMIT = 6 - - -def get_calendar(assessment): - """ - Returns a compiled calendar containing the given assessment. - """ - calendar = Calendar() - for assessment_item in assessment: - course, task, due_date, weight = assessment_item - event = Event() - event['uid'] = str(uuid()) - event['summary'] = f'{course} ({weight}): {task}' - try: - start_datetime, end_datetime = get_parsed_assessment_due_date(assessment_item) - except DateSyntaxException as e: - bot.logger.error(e.message) - # If we can't parse a date, set its due date to today - # and let the user know through its summary. - # TODO(mitch): Keep track of these instances to attempt to accurately - # parse them in future. Will require manual detection + parsing. - start_datetime = end_datetime = datetime.today() - event['summary'] = ("WARNING: DATE PARSING FAILED\n" - "Please manually set date for event!\n" - "The provided due date from UQ was" - + f" '{due_date}\'. {event['summary']}") - event.add('dtstart', start_datetime) - event.add('dtend', end_datetime) - calendar.add_component(event) - return calendar.to_ical() - - -@bot.on_command('calendar') -@success_status -@loading_status -def handle_calendar(command: Command): - """ - `!calendar [COURSE CODE 2] ...` - Returns a compiled - calendar containing all the assessment for a given list of course codes. - """ - channel = bot.channels.get(command.channel_id) - course_names = command.arg.split() if command.has_arg() else [channel.name] - - if len(course_names) > COURSE_LIMIT: - bot.post_message(channel, f'Cannot process more than {COURSE_LIMIT} courses.') - return - - try: - assessment = get_course_assessment(course_names) - except HttpException as e: - bot.logger.error(e.message) - bot.post_message(channel, f'An error occurred, please try again.') - return - except (CourseNotFoundException, ProfileNotFoundException) as e: - bot.post_message(channel, e.message) - return - - user_direct_channel = bot.channels.get(command.user_id) - bot.api.files.upload(title='Importable calendar containing your assessment!', - channels=user_direct_channel.id, filetype='text/calendar', - filename='assessment.ics', file=get_calendar(assessment)) diff --git a/unimplemented/channel_log.py b/unimplemented/channel_log.py deleted file mode 100644 index d1c5c175..00000000 --- a/unimplemented/channel_log.py +++ /dev/null @@ -1,13 +0,0 @@ -from uqcsbot import bot - - -@bot.on("channel_created") -def channel_log(evt: dict): - """ - Notes when channels are created in #uqcs-meta - - @no_help - """ - bot.post_message(bot.channels.get("uqcs-meta"), - 'New Channel Created: ' - + f'<#{evt.get("channel").get("id")}|{evt.get("channel").get("name")}>') diff --git a/unimplemented/coin.py b/unimplemented/coin.py deleted file mode 100644 index ca5d2ae4..00000000 --- a/unimplemented/coin.py +++ /dev/null @@ -1,20 +0,0 @@ -from random import choice -from uqcsbot import bot, Command - - -@bot.on_command("coin") -def handle_coin(command: Command): - """ - `!coin [number]` - Flips 1 or more coins. - """ - if command.has_arg() and command.arg.isnumeric(): - flips = min(max(int(command.arg), 1), 500) - else: - flips = 1 - - response = [] - emoji = (':heads:', ':tails:') - for i in range(flips): - response.append(choice(emoji)) - - bot.post_message(command.channel_id, "".join(response)) diff --git a/unimplemented/cookbook.py b/unimplemented/cookbook.py deleted file mode 100644 index 34d5948a..00000000 --- a/unimplemented/cookbook.py +++ /dev/null @@ -1,10 +0,0 @@ -from uqcsbot import bot, Command - - -@bot.on_command("cookbook") -def handle_cookbook(command: Command): - """ - `!cookbook` - Returns the URL of the UQCS student-compiled cookbook (pdf). - """ - bot.post_message(command.channel_id, "It's A Cookbook!\n" - "https://github.com/UQComputingSociety/cookbook") diff --git a/unimplemented/dominos.py b/unimplemented/dominos.py deleted file mode 100644 index a75318be..00000000 --- a/unimplemented/dominos.py +++ /dev/null @@ -1,116 +0,0 @@ -import argparse -from uqcsbot import bot, Command -from bs4 import BeautifulSoup -from datetime import datetime -from requests.exceptions import RequestException -from typing import List -from uqcsbot.utils.command_utils import loading_status, UsageSyntaxException -import requests - -MAX_COUPONS = 10 # Prevents abuse -COUPONESE_DOMINOS_URL = 'https://www.couponese.com/store/dominos.com.au/' - - -class Coupon: - def __init__(self, code: str, expiry_date: str, description: str) -> None: - self.code = code - self.expiry_date = expiry_date - self.description = description - - def is_valid(self) -> bool: - try: - expiry_date = datetime.strptime(self.expiry_date, '%Y-%m-%d') - now = datetime.now() - return all([expiry_date.year >= now.year, expiry_date.month >= now.month, - expiry_date.day >= now.day]) - except ValueError: - return True - - def keyword_matches(self, keyword: str) -> bool: - return keyword.lower() in self.description.lower() - - -@bot.on_command("dominos") -@loading_status -def handle_dominos(command: Command): - """ - `!dominos [--num] N [--expiry] ` - Returns a list of dominos coupons (default: 5 | max: 10) - """ - command_args = command.arg.split() if command.has_arg() else [] - - parser = argparse.ArgumentParser() - - def usage_error(*args, **kwargs): - raise UsageSyntaxException() - parser.error = usage_error # type: ignore - parser.add_argument('-n', '--num', default=5, type=int) - parser.add_argument('-e', '--expiry', action='store_true') - parser.add_argument('keywords', nargs='*') - - args = parser.parse_args(command_args) - coupons_amount = min(args.num, MAX_COUPONS) - coupons = get_coupons(coupons_amount, args.expiry, args.keywords) - - message = "" - for coupon in coupons: - message += f"Code: *{coupon.code}* - {coupon.description}\n" - bot.post_message(command.channel_id, message) - - -def filter_coupons(coupons: List[Coupon], keywords: List[str]) -> List[Coupon]: - """ - Filters coupons iff a keyword is found in the description. - """ - return [coupon for coupon in coupons if - any(coupon.keyword_matches(keyword) for keyword in keywords)] - - -def get_coupons(n: int, ignore_expiry: bool, keywords: List[str]) -> List[Coupon]: - """ - Returns a list of n Coupons - """ - - coupon_page = get_coupon_page() - if coupon_page is None: - return None - - coupons = get_coupons_from_page(coupon_page) - - if not ignore_expiry: - coupons = [coupon for coupon in coupons if coupon.is_valid()] - - if keywords: - coupons = filter_coupons(coupons, keywords) - return coupons[:n] - - -def get_coupons_from_page(coupon_page: bytes) -> List[Coupon]: - """ - Strips results from html page and returns a list of Coupon(s) - """ - soup = BeautifulSoup(coupon_page, 'html.parser') - soup_coupons = soup.find_all(class_="ov-coupon") - - coupons = [] - - for soup_coupon in soup_coupons: - expiry_date_str = soup_coupon.find(class_='ov-expiry').get_text(strip=True) - description = soup_coupon.find(class_='ov-desc').get_text(strip=True) - code = soup_coupon.find(class_='ov-code').get_text(strip=True) - coupon = Coupon(code, expiry_date_str, description) - coupons.append(coupon) - - return coupons - - -def get_coupon_page() -> bytes: - """ - Gets the coupon page HTML - """ - try: - response = requests.get(COUPONESE_DOMINOS_URL) - return response.content - except RequestException as e: - bot.logger.error(e.response.content) - return None diff --git a/unimplemented/emoji_log.py b/unimplemented/emoji_log.py deleted file mode 100644 index aedb8d12..00000000 --- a/unimplemented/emoji_log.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Logs emoji addition/removal to emoji-request for audit purposes -""" -from uqcsbot import bot - - -@bot.on("emoji_changed") -def emoji_log(evt: dict): - """ - Notes when emojis are added or deleted. - - @no_help - """ - emoji_request = bot.channels.get("emoji-request") - subtype = evt.get("subtype") - - if subtype == 'add': - name = evt["name"] - value = evt["value"] - - if value.startswith('alias:'): - _, alias = value.split('alias:') - - bot.post_message(emoji_request, - f'Emoji alias added: `:{name}:` :arrow_right: `:{alias}:` (:{name}:)') - - else: - message = bot.post_message(emoji_request, f'Emoji added: :{name}: (`:{name}:`)') - bot.api.reactions.add(channel=message["channel"], - timestamp=message["ts"], name=name) - - elif subtype == 'remove': - names = evt.get("names") - removed = ', '.join(f'`:{name}:`' for name in names) - plural = 's' if len(names) > 1 else '' - - bot.post_message(emoji_request, f'Emoji{plural} removed: {removed}') diff --git a/unimplemented/emojify.py b/unimplemented/emojify.py deleted file mode 100644 index af131ec3..00000000 --- a/unimplemented/emojify.py +++ /dev/null @@ -1,173 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status -from typing import Dict, List - -from collections import defaultdict -from random import shuffle, choice - - -@bot.on_command("emojify") -@loading_status -def handle_emojify(command: Command): - ''' - `!emojify text` - converts text to emoji. - ''' - master: Dict[str, List[str]] = defaultdict(lambda: [":grey_question:"]) - - # letters - master['A'] = [":adobe:", ":airbnb:", ":amazon:", ":anarchism:", - ":arch:", ":atlassian:", ":office_access:", ":capital_a_agile:", - choice([":card-ace-clubs:", ":card-ace-diamonds:", - ":card-ace-hearts:", ":card-ace-spades:"])] - master['B'] = [":bhinking:", ":bitcoin:", ":blutes:"] - master['C'] = [":c:", ":clang:", ":cplusplus:", ":copyright:", ":clipchamp:", - ":clipchamp_old:"] - master['D'] = [":d:", ":disney:", ":deloitte:"] - master['E'] = [":ecorp:", ":emacs:", ":erlang:", ":ie10:", ":thonk_slow:", ":edge:", - ":expedia_group:"] - master['F'] = [":f:", ":facebook:", ":flutter:", ":figma:"] - master['G'] = [":g+:", ":google:", ":nintendo_gamecube:", ":gatsbyjs:", ":gmod:"] - master['H'] = [":hackerrank:", ":homejoy:"] - master['I'] = [":information_source:", ":indoorooshs:"] - master['J'] = [choice([":card-jack-clubs:", ":card-jack-diamonds:", - ":card-jack-hearts:", ":card-jack-spades:"])] - master['K'] = [":kickstarter:", ":kotlin:", - choice([":card-king-clubs:", ":card-king-diamonds:", - ":card-king-hearts:", ":card-king-spades:"])] - master['L'] = [":l:", ":lime:", ":l_plate:", ":ti_nekro:"] - master['M'] = [":gmail:", ":maccas:", ":mcgrathnicol:", ":melange_mining:", ":mtg:", ":mxnet:", - ":jmod:"] - master['N'] = [":nano:", ":neovim:", ":netscape_navigator:", ":notion:", - ":nginx:", ":nintendo_64:", ":office_onenote:", ":netflix-n:"] - master['O'] = [":office_outlook:", ":oracle:", ":o_:", ":tetris_o:", ":ubuntu:", - choice([":portal_blue:", ":portal_orange:"])] - master['P'] = [":auspost:", ":office_powerpoint:", ":office_publisher:", - ":pinterest:", ":paypal:", ":producthunt:", ":uqpain:"] - master['Q'] = [":quora:", ":quantium:", choice([":card-queen-clubs:", ":card-queen-diamonds:", - ":card-queen-hearts:", ":card-queen-spades:"])] - master['R'] = [":r-project:", ":rust:", ":redroom:", ":registered:"] - master['S'] = [":s:", ":skedulo:", ":stanford:", ":stripe_s:", ":sublime:", ":tetris_s:"] - master['T'] = [":tanda:", choice([":telstra:", ":telstra-pink:"]), - ":tesla:", ":tetris_t:", ":torchwood:", ":tumblr:", ":nyt:"] - master['U'] = [":uber:", ":uqu:", ":the_horns:", ":proctoru:", ":ubiquiti:"] - master['V'] = [":vim:", ":vue:", ":vuetify:", ":v:"] - master['W'] = [":office_word:", ":washio:", ":wesfarmers:", ":westpac:", - ":weyland_consortium:", ":wikipedia_w:", ":woolworths:"] - master['X'] = [":atlassian_old:", ":aginicx:", ":sonarr:", ":x-files:", ":xbox:", - ":x:", ":flag-scotland:", ":office_excel:"] - master['Y'] = [":hackernews:"] - master['Z'] = [":tetris_z:"] - - # numbers - master['0'] = [":chrome:", ":suncorp:", ":disney_zero:", ":firefox:", - ":mars:", ":0_:", choice([":dvd:", ":cd:"])] - master['1'] = [":techone:", ":testtube:", ":thonk_ping:", ":first_place_medal:", - ":critical_fail:", ":slack_unread_1:"] - master['2'] = [":second_place_medal:", choice([":card-2-clubs:", ":card-2-diamonds:", - ":card-2-hearts:", ":card-2-spades:"])] - master['3'] = [":css:", ":third_place_medal:", choice([":card-3-clubs:", ":card-3-diamonds:", - ":card-3-hearts:", ":card-3-spades:"])] - master['4'] = [choice([":card-4-clubs:", ":card-4-diamonds:", - ":card-4-hearts:"]), ":card-4-spades:"] - master['5'] = [":html:", choice([":card-5-clubs:", ":card-5-diamonds:", - ":card-5-hearts:", ":card-5-spades:"])] - master['6'] = [choice([":card-6-clubs:", ":card-6-diamonds:", - ":card-6-hearts:", ":card-6-spades:"])] - master['7'] = [choice([":card-7-clubs:", ":card-7-diamonds:", - ":card-7-hearts:", ":card-7-spades:"])] - master['8'] = [":8ball:", choice([":card-8-clubs:", ":card-8-diamonds:", - ":card-8-hearts:", ":card-8-spades:"])] - master['9'] = [choice([":card-9-clubs:", ":card-9-diamonds:", - ":card-9-hearts:", ":card-9-spades:"])] - - # whitespace - master[' '] = [":whitespace:"] - master['\n'] = ["\n"] - - # other ascii characters (sorted by ascii value) - master['!'] = [":exclamation:"] - master['"'] = [choice([":ldquo:", ":rdquo:"]), ":pig_nose:"] - master['\''] = [":apostrophe:"] - master['#'] = [":slack_old:", ":csharp:"] - master['$'] = [":thonk_money:", ":moneybag:"] - # '&' converts to '&' - master['&'] = [":ampersand:", ":dnd:"] - master['('] = [":lparen:"] - master[')'] = [":rparen:"] - master['*'] = [":day:", ":nab:", ":youtried:", ":msn_star:", ":rune_prayer:", ":wolfram:", - ":shuriken:", ":mtg_s:", ":aoc:", ":jetstar:"] - master['+'] = [":tf2_medic:", ":flag-ch:", ":flag-england:"] - master['-'] = [":no_entry:"] - master['.'] = [":full_stop_big:"] - master[','] = [":comma:"] - master['/'] = [":slash:"] - master[';'] = [":semi-colon:"] - # '>' converts to '>' - master['>'] = [":accenture:", ":implying:", ":plex:", ":powershell:"] - master['?'] = [":question:"] - master['@'] = [":whip:"] - master['^'] = [":this:", ":typographical_carrot:", ":arrow_up:", - ":this_but_it's_an_actual_caret:"] - master['~'] = [":wavy_dash:"] - - # slack/uqcsbot convert the following to other symbols - - # greek letters - # 'Α' converts to 'A' - master['Α'] = [":alpha:"] - # 'Β' converts to 'B' - master['Β'] = [":beta:"] - # 'Δ' converts to 'D' - master['Δ'] = [":optiver:"] - # 'Λ' converts to 'L' - master['Λ'] = [":halflife:", ":haskell:", ":lambda:", ":racket:", - choice([":uqcs:", ":scrollinguqcs:", ":scrollinguqcs_alt:", ":uqcs_mono:"])] - # 'Π' converts to 'P' - master['Π'] = [":pi:"] - # 'Φ' converts to 'PH' - master['Φ'] = [":phyrexia_blue:"] - # 'Σ' converts to 'S' - master['Σ'] = [":polymathian:", ":sigma:"] - - # other symbols (sorted by unicode value) - # '…' converts to '...' - master['…'] = [":lastpass:"] - # '€' converts to 'EUR' - master['€'] = [":martian_euro:"] - # '√' converts to '[?]' - master['√'] = [":sqrt:"] - # '∞' converts to '[?]' - master['∞'] = [":arduino:", ":visualstudio:", ":infinitely:"] - # '∴' converts to '[?]' - master['∴'] = [":julia:"] - - master['人'] = [":人:"] - - master[chr(127)] = [":delet_this:"] - - text = "" - if command.has_arg(): - text = command.arg.upper() - # revert HTML conversions - text = text.replace(">", ">") - text = text.replace("<", "<") - text = text.replace("&", "&") - - lexicon = {} - for character in set(text+'…'): - full, part = divmod((text+'…').count(character), len(master[character])) - shuffle(master[character]) - lexicon[character] = full * master[character] + master[character][:part] - shuffle(lexicon[character]) - - ellipsis = lexicon['…'].pop() - - response = "" - for character in text: - emoji = lexicon[character].pop() - if len(response + emoji + ellipsis) > 4000: - response += ellipsis - break - response += emoji - - bot.post_message(command.channel_id, response) diff --git a/unimplemented/history.py b/unimplemented/history.py deleted file mode 100644 index 2dd62122..00000000 --- a/unimplemented/history.py +++ /dev/null @@ -1,56 +0,0 @@ -from uqcsbot import bot -from datetime import datetime -from pytz import timezone, utc -from random import choice - - -class Pin: - """ - Class for pins, with channel, age in years, user and pin text - """ - def __init__(self, channel: str, years: int, user: str, text: str): - self.channel = channel - self.years = years - self.user = user - self.text = text - - def message(self) -> str: - return (f"On this day, {self.years} years ago, <@{self.user}> said" - f"\n>>>{self.text}") - - def origin(self): - return bot.channels.get(self.channel) - - -@bot.on_schedule('cron', hour=12, minute=0, timezone='Australia/Brisbane') -def daily_history() -> None: - """ - Selets a random pin that was posted on this date some years ago, - and reposts it in the same channel - """ - anniversary = [] - today = datetime.now(utc).astimezone(timezone('Australia/Brisbane')).date() - - # for every channel - for channel in bot.api.conversations.list(types="public_channel")['channels']: - # skip archived channels - if channel['is_archived']: - continue - - for pin in bot.api.pins.list(channel=channel['id'])['items']: - # messily get the date the pin was originally posted - pin_date = (datetime.fromtimestamp(int(float(pin['message']['ts'])), tz=utc) - .astimezone(timezone('Australia/Brisbane')).date()) - # if same date as today - if pin_date.month == today.month and pin_date.day == today.day: - # add pin to possibilities - anniversary.append(Pin(channel=channel['name'], years=today.year-pin_date.year, - user=pin['message']['user'], text=pin['message']['text'])) - - # if no pins were posted on this date, do nothing - if not anniversary: - return - - # randomly select a pin, and post it in the original channel - selected = choice(anniversary) - bot.post_message(selected.origin(), selected.message()) diff --git a/unimplemented/id.py b/unimplemented/id.py deleted file mode 100644 index 8d7ff00a..00000000 --- a/unimplemented/id.py +++ /dev/null @@ -1,9 +0,0 @@ -from uqcsbot import bot, Command - - -@bot.on_command("id") -def handle_id(command: Command): - """ - `!id` - Returns the calling user's Slack ID. - """ - bot.post_message(command.channel_id, f'You are Number `{command.user_id}`') diff --git a/unimplemented/link.py b/unimplemented/link.py deleted file mode 100644 index 83674dc0..00000000 --- a/unimplemented/link.py +++ /dev/null @@ -1,163 +0,0 @@ -from argparse import ArgumentParser -from enum import Enum -from typing import Optional, Tuple - -from slackblocks import Attachment, Color, SectionBlock -from sqlalchemy.exc import NoResultFound - -from uqcsbot import bot, Command -from uqcsbot.models import Link -from uqcsbot.utils.command_utils import loading_status - - -class LinkScope(Enum): - """ - Possible requested scopes for setting or retrieving a link. - """ - CHANNEL = "channel" - GLOBAL = "global" - - -class SetResult(Enum): - """ - Possible outcomes of the set link operation. - """ - NEEDS_OVERRIDE = "Link already exists, use `-f` to override" - OVERRIDE_SUCCESS = "Successfully overrode link" - NEW_LINK_SUCCESS = "Successfully added link" - - -def set_link_value(key: str, value: str, channel: str, - override: bool, link_scope: Optional[LinkScope] = None) -> Tuple[SetResult, str]: - """ - Sets a corresponding value for a particular key. Keys are set to global by default but this can - be overridden by passing the channel flag. Existing links can only be overridden if the - override flag is passed. - :param key: the lookup key for users to search the value by - :param value: the value to associate with the key - :param channel: the name of the channel the set operation was initiated in - :param link_scope: defines the scope to set the link in, defaults to global if not provided - :param override: required to be True if an association already exists and needs to be updated - :return: a SetResult status and the value associated with the given key/channel combination - """ - link_channel = channel if link_scope == LinkScope.CHANNEL else None - session = bot.create_db_session() - - try: - exists = session.query(Link).filter(Link.key == key, - Link.channel == link_channel).one() - if exists and not override: - return SetResult.NEEDS_OVERRIDE, exists.value - session.delete(exists) - result = SetResult.OVERRIDE_SUCCESS - except NoResultFound: - result = SetResult.NEW_LINK_SUCCESS - session.add(Link(key=key, channel=link_channel, value=value)) - session.commit() - session.close() - return result, value - - -def get_link_value(key: str, - channel: str, - link_scope: Optional[LinkScope] = None) -> Tuple[Optional[str], Optional[str]]: - """ - Gets the value associated with a given key (and optionally channel). If a channel association - exists, this is returned, otherwise a global association is returned. If no association exists - then None is returned. The default behaviour can be overridden by passing the global flag to - force retrieval of a global association when a channel association exists. - :param key: the key to look up - :param channel: the name of the channel the lookup request was made from - :param link_scope: the requested scope to retrieve the link from (if supplied) - :return: the associated value if an association exists, else None, and the source - (global/channel) if any else None - """ - session = bot.create_db_session() - channel_match = session.query(Link).filter(Link.key == key, - Link.channel == channel).one_or_none() - global_match = session.query(Link).filter(Link.key == key, - Link.channel == None).one_or_none() # noqa: E711 - session.close() - - if link_scope == LinkScope.GLOBAL: - return (global_match.value, "global") if global_match else (None, None) - - if link_scope == LinkScope.CHANNEL: - return (channel_match.value, "channel") if channel_match else (None, None) - - if channel_match: - return channel_match.value, "channel" - - if global_match: - return global_match.value, "global" - - return None, None - - -@bot.on_command('link') -@loading_status -def handle_link(command: Command) -> None: - """ - `!link [-c | -g] [-f] key [value [value ...]]` - Set and retrieve information in a key value - store. Links can be set to be channel specific or global. Links are set as global by default, - and channel specific links are retrieved by default unless overridden with the respective flag. - """ - parser = ArgumentParser("!link", add_help=False) - parser.add_argument("key", type=str, help="Lookup key") - parser.add_argument("value", type=str, help="Value to associate with key", nargs="*") - flag_group = parser.add_mutually_exclusive_group() - flag_group.add_argument("-c", "--channel", action="store_true", dest="channel_flag", - help="Ensure a channel link is retrieved, or none is") - flag_group.add_argument("-g", "--global", action="store_true", dest="global_flag", - help="Ignore channel link and force retrieval of global") - parser.add_argument("-f", "--force-override", action="store_true", dest="override", - help="Must be passed if overriding a link") - - try: - args = parser.parse_args(command.arg.split() if command.has_arg() else []) - except SystemExit: - # Incorrect Usage - return bot.post_message(command.channel_id, "", - attachments=[Attachment(SectionBlock(str(parser.format_help())), - color=Color.YELLOW)._resolve()]) - - channel = bot.channels.get(command.channel_id) - if not channel: - return bot.post_message(command.channel_id, "", attachments=[ - Attachment(SectionBlock("Cannot find channel name, please try again."), - color=Color.YELLOW)._resolve() - ]) - - channel_name = channel.name - - link_scope = LinkScope.CHANNEL if args.channel_flag else \ - LinkScope.GLOBAL if args.global_flag else None - - # Retrieve a link - if not args.value: - link_value, source = get_link_value(key=args.key, - channel=channel_name, - link_scope=link_scope) - channel_text = f" in channel `{channel_name}`" if args.channel_flag else "" - if link_value: - source_text = source if source == 'global' else channel_name - response = f"{args.key} ({source_text}): {link_value}" - else: - response = f"No link found for key: `{args.key}`" + channel_text - color = Color.GREEN if link_value else Color.RED - return bot.post_message(command.channel_id, "", attachments=[ - Attachment(SectionBlock(response), color=color)._resolve() - ]) - - # Set a link - if args.key and args.value: - result, current_value = set_link_value(key=args.key, - channel=channel_name, - value=" ".join(args.value), - override=args.override, - link_scope=link_scope) - color = Color.YELLOW if result == SetResult.NEEDS_OVERRIDE else Color.GREEN - scope = channel_name if args.channel_flag else 'global' - response = f"{args.key} ({scope}): {current_value}" - attachment = Attachment(SectionBlock(response), color=color)._resolve() - bot.post_message(command.channel_id, f"{result.value}:", attachments=[attachment]) diff --git a/unimplemented/pastexams.py b/unimplemented/pastexams.py deleted file mode 100644 index 21cfa759..00000000 --- a/unimplemented/pastexams.py +++ /dev/null @@ -1,67 +0,0 @@ -from uqcsbot import bot, Command -from bs4 import BeautifulSoup -from typing import Iterable, Tuple -import requests -from uqcsbot.utils.command_utils import loading_status - - -@bot.on_command('pastexams') -@loading_status -def handle_pastexams(command: Command): - """ - `!pastexams [COURSE CODE]` - Retrieves past exams for a given course code. - If unspecified, will attempt to find the ECP - for the channel the command was called from. - """ - channel = bot.channels.get(command.channel_id) - course_code = command.arg if command.has_arg() else channel.name - bot.post_message(channel, get_past_exams(course_code)) - - -def get_exam_data(soup: BeautifulSoup) -> Iterable[Tuple[str, str]]: - """ - Takes the soup object of the page and generates each result in the format: - ('year Sem X:', link) - """ - - # The exams are stored in a table with the structure: - # Row 1: A bunch of informational text - # Row 2: Semester information - # Row 3: Links to Exams - # Rows two and three are what we care about. Of those the first column is just a row title so - # we ignore that as well - - exam_table_rows = soup.find('table', class_='maintable').contents - semesters = exam_table_rows[1].find_all('td')[1:] # All columns in row 2 excluding the first - # Gets the content from each td. Text is separated by a
thus result is in the format - # (year,
, 'Sem.x' - semesters = [semester.contents for semester in semesters] - - # Same thing but for links - links = exam_table_rows[2].find_all('td')[1:] - links = [link.find('a')['href'] for link in links] - - for (year, _, semester_id), link in zip(semesters, links): - semester_str = semester_id.replace('.', ' ') - yield f'{year} {semester_str}', link - - -def get_past_exams(course_code: str) -> str: - """ - Gets the past exams for the course with the specified course code. - Returns intuitive error messages if this fails. - """ - url = 'https://www.library.uq.edu.au/exams/papers.php?' - http_response = requests.get(url, params={'stub': course_code}) - - if http_response.status_code != requests.codes.ok: - return "There was a problem getting a response" - - # Check if the course code exists - soup = BeautifulSoup(http_response.content, 'html.parser') - no_course = soup.find('div', class_='page').find('div').contents[0] - if "Sorry. We have not found any past exams for this course" in no_course: - return f"The course code {course_code} did not return any results" - - return '>>>' + '\n'.join((f'*{semester}*: <{link}|PDF>' - for semester, link in get_exam_data(soup))) diff --git a/unimplemented/wavie.py b/unimplemented/wavie.py deleted file mode 100644 index be81b47f..00000000 --- a/unimplemented/wavie.py +++ /dev/null @@ -1,22 +0,0 @@ -from uqcsbot import bot -import logging - - -logger = logging.getLogger(__name__) - - -@bot.on('message') -def wave(evt): - """ - :wave: reacts to "person joined/left this channel" - - @no_help - """ - if evt.get('subtype') not in ['channel_join', 'channel_leave']: - return - chan = bot.channels.get(evt['channel']) - if chan is not None and chan.name == 'announcements': - return - result = bot.api.reactions.add(name='wave', channel=chan.id, timestamp=evt['ts']) - if not result.get('ok'): - logger.error(f"Error adding reaction: {result}") diff --git a/unimplemented/whoami.py b/unimplemented/whoami.py deleted file mode 100644 index 0d1983b7..00000000 --- a/unimplemented/whoami.py +++ /dev/null @@ -1,18 +0,0 @@ -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import success_status - - -@bot.on_command("whoami") -@success_status -def handle_whoami(command: Command): - """ - `!whoami` - Returns the Slack information for the calling user. - """ - response = bot.api.users.info(user=command.user_id) - if not response['ok']: - message = 'An error occurred, please try again.' - else: - user_info = response['user'] - message = f'Your vital statistics: \n```{user_info}```' - user_direct_channel = bot.channels.get(command.user_id) - bot.post_message(user_direct_channel, message) diff --git a/unimplemented/xkcd.py b/unimplemented/xkcd.py deleted file mode 100644 index eaf491a6..00000000 --- a/unimplemented/xkcd.py +++ /dev/null @@ -1,125 +0,0 @@ -import datetime -import requests -import feedparser -import re -from urllib.parse import quote -from uqcsbot import bot, Command -from uqcsbot.utils.command_utils import loading_status - - -# HTTP Endpoints -XKCD_BASE_URL = "https://xkcd.com/" -XKCD_RSS_URL = "https://xkcd.com/rss.xml" -RELEVANT_XKCD_URL = 'https://relevantxkcd.appspot.com/process' - - -def get_by_id(comic_number: int) -> str: - """ - Gets an xkcd comic based on its unique ID/sequence number. - :param comic_number: the ID number of the xkcd comic to retrieve. - :return: a response containing either a comic URL or an error message. - """ - if comic_number <= 0: - return "Invalid xkcd ID, it must be a positive integer." - url = f"{XKCD_BASE_URL}{str(comic_number)}" - response = requests.get(url) - if response.status_code != 200: - return "Could not retrieve an xkcd with that ID (are there even that many?)" - return url - - -def get_by_search_phrase(search_phrase: str) -> str: - """ - Uses the site relevantxkcd.appspot.com to identify the - most appropriate xkcd comic based on the phrase provided. - :param search_phrase: the phrase to find an xkcd comic related to. - :return: the URL of the most relevant comic for that search phrase. - """ - params = {"action": "xkcd", "query": quote(search_phrase)} - response = requests.get(RELEVANT_XKCD_URL, params=params) - # Response consists of a newline delimited list, with two irrelevant first parameters - relevant_comics = response.content.decode().split("\n")[2:] - # Each line consists of "comic_id image_url" - best_response = relevant_comics[0].split(" ") - comic_number = int(best_response[0]) - return get_by_id(comic_number) - - -def get_latest() -> str: - """ - Gets the most recently published xkcd comic by examining the RSS feed. - :return: the URL to the latest xkcd comic. - """ - rss = feedparser.parse(XKCD_RSS_URL) - entries = rss['entries'] - if len(entries) > 0: - i = 0 - latest = entries[i]['guid'] - if not re.match(r"https://xkcd\.com/\d+/", latest): - i += 1 - latest = entries[i]['guid'] - else: - latest = 'https://xkcd.com/2200/' - return latest - - -def is_id(argument: str) -> bool: - """ - Determines whether the given argument is a valid id (i.e. an integer). - :param argument: the string argument to evaluate - :return: true if the argument can be evaluated as an interger, false otherwise - """ - try: - int(argument) - except ValueError: - return False - else: - return True - - -@bot.on_command('xkcd') -@loading_status -def handle_xkcd(command: Command) -> None: - """ - `!xkcd [COMIC_ID|SEARCH_PHRASE]` - Returns the xkcd comic associated - with the given COMIC_ID (an integer) or matching the SEARCH_PHRASE. - Providing no arguments will return the most recent comic. - """ - if command.has_arg(): - argument = command.arg - if is_id(argument): - comic_number = int(argument) - response = get_by_id(comic_number) - else: - response = get_by_search_phrase(command.arg) - else: - response = get_latest() - - bot.post_message(command.channel_id, response, unfurl_links=True, unfurl_media=True) - - -@bot.on_schedule('cron', hour=14, minute=1, day_of_week='mon,wed,fri', - timezone='Australia/Brisbane') -def new_xkcd() -> None: - """ - Posts new xkcd comic when they are released every Monday, - Wednesday & Friday at 4AM UTC or 2PM Brisbane time. - - @no_help - """ - link = get_latest() - - day = datetime.datetime.today().weekday() - if (day == 0): # Monday - message = "It's Monday, 4 days till Friday; here's the" - elif (day == 2): # Wednesday - message = "Half way through the week, time for the" - elif (day == 4): # Friday - message = (":musical_note: It's Friday, Friday\nGotta get down on Friday\n" - "Everybody's lookin' forward to the") - else: - message = "@pah It is day " + str(day) + ", please fix me... Here's the" - message += " latest xkcd comic " - - general = bot.channels.get("general") - bot.post_message(general.id, message + link, unfurl_links=True, unfurl_media=True) From eaadaf8c00184759fc2621f9df1e83503a8a89e5 Mon Sep 17 00:00:00 2001 From: Isaac Beh Date: Sat, 28 Oct 2023 09:30:18 +1000 Subject: [PATCH 2/2] Whatsdue: Added Sorting (#142) * Whatsdue: Added sorting by type. * Whatsdue: Added course ECP links as an option * Whatsdue: Fixed grammar for ECP link(s) * Whatsdue: Added weeks_to_show to reduce output * Removed surplus old TODOs and added typing to get_weight_as_int() * Changed get_weight_as_int() to return 0 if no weight can be parsed * Revert "Changed get_weight_as_int() to return 0 if no weight can be parsed" This reverts commit 709071dd3e20271bdd48bdaf036a865d3e4aa8cd. --------- Co-authored-by: Andrew Brown <92134285+andrewj-brown@users.noreply.github.com> --- uqcsbot/utils/uq_course_utils.py | 132 +++++++++++++++++++------------ uqcsbot/whatsdue.py | 85 ++++++++++++++------ 2 files changed, 143 insertions(+), 74 deletions(-) diff --git a/uqcsbot/utils/uq_course_utils.py b/uqcsbot/utils/uq_course_utils.py index a4c2d9db..93432227 100644 --- a/uqcsbot/utils/uq_course_utils.py +++ b/uqcsbot/utils/uq_course_utils.py @@ -3,9 +3,10 @@ from datetime import datetime from dateutil import parser from bs4 import BeautifulSoup, element -from functools import partial from typing import List, Dict, Optional, Literal, Tuple +from dataclasses import dataclass import json +import re BASE_COURSE_URL = "https://my.uq.edu.au/programs-courses/course.html?course_code=" BASE_ASSESSMENT_URL = ( @@ -105,6 +106,69 @@ def _estimate_current_semester() -> SemesterType: return "Summer" +@dataclass +class AssessmentItem: + course_name: str + task: str + due_date: str + weight: str + + def get_parsed_due_date(self): + """ + Returns the parsed due date for the given assessment item as a datetime + object. If the date cannot be parsed, a DateSyntaxException is raised. + """ + if self.due_date == "Examination Period": + return get_current_exam_period() + parser_info = parser.parserinfo(dayfirst=True) + try: + # If a date range is detected, attempt to split into start and end + # dates. Else, attempt to just parse the whole thing. + if " - " in self.due_date: + start_date, end_date = self.due_date.split(" - ", 1) + start_datetime = parser.parse(start_date, parser_info) + end_datetime = parser.parse(end_date, parser_info) + return start_datetime, end_datetime + due_datetime = parser.parse(self.due_date, parser_info) + return due_datetime, due_datetime + except Exception: + raise DateSyntaxException(self.due_date, self.course_name) + + def is_after(self, cutoff: datetime): + """ + Returns whether the assessment occurs after the given cutoff. + """ + try: + start_datetime, end_datetime = self.get_parsed_due_date() + except DateSyntaxException: + # If we can't parse a date, we're better off keeping it just in case. + return True + return end_datetime >= cutoff if end_datetime else start_datetime >= cutoff + + def is_before(self, cutoff: datetime): + """ + Returns whether the assessment occurs before the given cutoff. + """ + try: + start_datetime, _ = self.get_parsed_due_date() + except DateSyntaxException: + # TODO bot.logger.error(e.message) + # If we can't parse a date, we're better off keeping it just in case. + # TODO(mitch): Keep track of these instances to attempt to accurately + # parse them in future. Will require manual detection + parsing. + return True + return start_datetime <= cutoff + + def get_weight_as_int(self) -> Optional[int]: + """ + Trys to get the weight percentage of an assessment as a percentage. Will return None + if a percentage can not be obtained. + """ + if match := re.match(r"\d+", self.weight): + return int(match.group(0)) + return None + + class DateSyntaxException(Exception): """ Raised when an unparsable date syntax is encountered. @@ -234,14 +298,14 @@ def get_course_profile_url( return url -def get_course_profile_id(course_name: str, offering: Optional[Offering]): +def get_course_profile_id(course_name: str, offering: Optional[Offering] = None) -> int: """ Returns the ID to the latest course profile for the given course. """ profile_url = get_course_profile_url(course_name, offering=offering) # The profile url looks like this # https://course-profiles.uq.edu.au/student_section_loader/section_1/100728 - return profile_url[profile_url.rindex("/") + 1 :] + return int(profile_url[profile_url.rindex("/") + 1 :]) def get_current_exam_period(): @@ -270,44 +334,6 @@ def get_current_exam_period(): return start_datetime, end_datetime -def get_parsed_assessment_due_date(assessment_item: Tuple[str, str, str, str]): - """ - Returns the parsed due date for the given assessment item as a datetime - object. If the date cannot be parsed, a DateSyntaxException is raised. - """ - course_name, _, due_date, _ = assessment_item - if due_date == "Examination Period": - return get_current_exam_period() - parser_info = parser.parserinfo(dayfirst=True) - try: - # If a date range is detected, attempt to split into start and end - # dates. Else, attempt to just parse the whole thing. - if " - " in due_date: - start_date, end_date = due_date.split(" - ", 1) - start_datetime = parser.parse(start_date, parser_info) - end_datetime = parser.parse(end_date, parser_info) - return start_datetime, end_datetime - due_datetime = parser.parse(due_date, parser_info) - return due_datetime, due_datetime - except Exception: - raise DateSyntaxException(due_date, course_name) - - -def is_assessment_after_cutoff(assessment: Tuple[str, str, str, str], cutoff: datetime): - """ - Returns whether the assessment occurs after the given cutoff. - """ - try: - start_datetime, end_datetime = get_parsed_assessment_due_date(assessment) - except DateSyntaxException: - # TODO bot.logger.error(e.message) - # If we can't parse a date, we're better off keeping it just in case. - # TODO(mitch): Keep track of these instances to attempt to accurately - # parse them in future. Will require manual detection + parsing. - return True - return end_datetime >= cutoff if end_datetime else start_datetime >= cutoff - - def get_course_assessment_page( course_names: List[str], offering: Optional[Offering] ) -> str: @@ -316,17 +342,18 @@ def get_course_assessment_page( url to the assessment table for the provided courses """ profile_ids = map( - lambda course: get_course_profile_id(course, offering=offering), course_names + lambda course: str(get_course_profile_id(course, offering=offering)), + course_names, ) return BASE_ASSESSMENT_URL + ",".join(profile_ids) def get_course_assessment( course_names: List[str], - cutoff: Optional[datetime] = None, + cutoff: Tuple[Optional[datetime], Optional[datetime]] = (None, None), assessment_url: Optional[str] = None, offering: Optional[Offering] = None, -) -> List[Tuple[str, str, str, str]]: +) -> List[AssessmentItem]: """ Returns all the course assessment for the given courses that occur after the given cutoff. @@ -346,9 +373,12 @@ def get_course_assessment( assessment = assessment_table.findAll("tr")[1:] parsed_assessment = map(get_parsed_assessment_item, assessment) # If no cutoff is specified, set cutoff to UNIX epoch (i.e. filter nothing). - cutoff = cutoff or datetime.min - assessment_filter = partial(is_assessment_after_cutoff, cutoff=cutoff) - filtered_assessment = filter(assessment_filter, parsed_assessment) + cutoff_min = cutoff[0] or datetime.min + cutoff_max = cutoff[1] or datetime.max + filtered_assessment = filter( + lambda item: item.is_after(cutoff_min) and item.is_before(cutoff_max), + parsed_assessment, + ) return list(filtered_assessment) @@ -360,8 +390,8 @@ def get_element_inner_html(dom_element: element.Tag): def get_parsed_assessment_item( - assessment_item: element.Tag, -) -> Tuple[str, str, str, str]: + assessment_item_tag: element.Tag, +) -> AssessmentItem: """ Returns the parsed assessment details for the given assessment item table row element. @@ -371,7 +401,7 @@ def get_parsed_assessment_item( This is likely insufficient to handle every course's structure, and thus is subject to change. """ - course_name, task, due_date, weight = assessment_item.findAll("div") + course_name, task, due_date, weight = assessment_item_tag.findAll("div") # Handles courses of the form 'CSSE1001 - Sem 1 2018 - St Lucia - Internal'. # Thus, this bit of code will extract the course. course_name = course_name.text.strip().split(" - ")[0] @@ -384,7 +414,7 @@ def get_parsed_assessment_item( # Handles weights of the form '30%
Alternative to oral presentation'. # 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) + return AssessmentItem(course_name, task, due_date, weight) class Exam: diff --git a/uqcsbot/whatsdue.py b/uqcsbot/whatsdue.py index 9dd61c68..7b3a51cb 100644 --- a/uqcsbot/whatsdue.py +++ b/uqcsbot/whatsdue.py @@ -1,6 +1,6 @@ -from datetime import datetime +from datetime import datetime, timedelta import logging -from typing import Optional +from typing import Optional, Callable, Literal, Dict import discord from discord import app_commands @@ -9,14 +9,39 @@ from uqcsbot.yelling import yelling_exemptor from uqcsbot.utils.uq_course_utils import ( + DateSyntaxException, Offering, CourseNotFoundException, HttpException, ProfileNotFoundException, + AssessmentItem, get_course_assessment, get_course_assessment_page, + get_course_profile_id, ) +AssessmentSortType = Literal["Date", "Course Name", "Weight"] +ECP_ASSESSMENT_URL = ( + "https://course-profiles.uq.edu.au/student_section_loader/section_5/" +) + + +def sort_by_date(item: AssessmentItem): + """Provides a key to sort assessment dates by. If the date cannot be parsed, will put it with items occuring during exam block.""" + try: + return item.get_parsed_due_date()[0] + except DateSyntaxException: + return datetime.max + + +SORT_METHODS: Dict[ + AssessmentSortType, Callable[[AssessmentItem], int | str | datetime] +] = { + "Date": sort_by_date, + "Course Name": (lambda item: item.course_name), + "Weight": (lambda item: item.get_weight_as_int() or 0), +} + class WhatsDue(commands.Cog): def __init__(self, bot: commands.Bot): @@ -26,15 +51,14 @@ def __init__(self, bot: commands.Bot): @app_commands.describe( fulloutput="Display the full list of assessment. Defaults to False, which only " + "shows assessment due from today onwards.", + weeks_to_show="Only show assessment due within this number of weeks. If 0 (default), show all assessment.", semester="The semester to get assessment for. Defaults to what UQCSbot believes is the current semester.", campus="The campus the course is held at. Defaults to St Lucia. Note that many external courses are 'hosted' at St Lucia.", mode="The mode of the course. Defaults to Internal.", - course1="Course code", - course2="Course code", - course3="Course code", - course4="Course code", - course5="Course code", - course6="Course code", + courses="Course codes seperated by spaces", + sort_order="The order to sort courses by. Defualts to Date.", + reverse_sort="Whether to reverse the sort order. Defaults to false.", + show_ecp_links="Show the first ECP link for each course page. Defaults to false.", ) @yelling_exemptor( input_args=["course1", "course2", "course3", "course4", "course5", "course6"] @@ -42,16 +66,15 @@ def __init__(self, bot: commands.Bot): async def whatsdue( self, interaction: discord.Interaction, - course1: str, - course2: Optional[str], - course3: Optional[str], - course4: Optional[str], - course5: Optional[str], - course6: Optional[str], + courses: str, fulloutput: bool = False, + weeks_to_show: int = 0, semester: Optional[Offering.SemesterType] = None, campus: Offering.CampusType = "St Lucia", mode: Offering.ModeType = "Internal", + sort_order: AssessmentSortType = "Date", + reverse_sort: bool = False, + show_ecp_links: bool = False, ): """ Returns all the assessment for a given list of course codes that are scheduled to occur. @@ -60,15 +83,19 @@ async def whatsdue( await interaction.response.defer(thinking=True) - possible_courses = [course1, course2, course3, course4, course5, course6] - course_names = [c.upper() for c in possible_courses if c != None] + course_names = [c.upper() for c in courses.split()] offering = Offering(semester=semester, campus=campus, mode=mode) # If full output is not specified, set the cutoff to today's date. - cutoff = None if fulloutput else datetime.today() + cutoff = ( + None if fulloutput else datetime.today(), + datetime.today() + timedelta(weeks=weeks_to_show) + if weeks_to_show > 0 + else None, + ) try: - asses_page = get_course_assessment_page(course_names, offering) - assessment = get_course_assessment(course_names, cutoff, asses_page) + assessment_page = get_course_assessment_page(course_names, offering) + assessment = get_course_assessment(course_names, cutoff, assessment_page) except HttpException as e: logging.error(e.message) await interaction.edit_original_response( @@ -81,15 +108,15 @@ async def whatsdue( embed = discord.Embed( title=f"What's Due: {', '.join(course_names)}", - url=asses_page, + url=assessment_page, description="*WARNING: Assessment information may vary/change/be entirely different! Use at your own discretion. Check your ECP for a true list of assessment.*", ) if assessment: + assessment.sort(key=SORT_METHODS[sort_order], reverse=reverse_sort) for assessment_item in assessment: - course, task, due, weight = assessment_item embed.add_field( - name=course, - value=f"`{weight}` {task} **({due})**", + name=assessment_item.course_name, + value=f"`{assessment_item.weight}` {assessment_item.task} **({assessment_item.due_date})**", inline=False, ) elif fulloutput: @@ -103,6 +130,18 @@ async def whatsdue( value=f"Nothing seems to be due soon", ) + if show_ecp_links: + ecp_links = [ + f"[{course_name}]({ECP_ASSESSMENT_URL + str(get_course_profile_id(course_name))})" + for course_name in course_names + ] + embed.add_field( + name=f"Potential ECP {'Link' if len(course_names) == 1 else 'Links'}", + value=" ".join(ecp_links) + + "\nNote that these may not be the correct ECPs. Check the year and offering type.", + inline=False, + ) + if not fulloutput: embed.set_footer( text="Note: This may not be the full assessment list. Set fulloutput to True to see a potentially more complete list, or check your ECP for a true list of assessment."