diff --git a/netbot/cog_tickets.py b/netbot/cog_tickets.py index 497a2c9..8dc52a2 100644 --- a/netbot/cog_tickets.py +++ b/netbot/cog_tickets.py @@ -496,9 +496,10 @@ async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str): await ctx.respond(f"Error creating ticket with title={title}") - @ticket.command(name="alert", description="Alert collaborators on a ticket") - @option("ticket_id", description="ID of ticket to alert", autocomplete=basic_autocomplete(default_ticket)) - async def alert_ticket(self, ctx: discord.ApplicationContext, ticket_id:int): + @ticket.command(name="notify", description="Notify collaborators on a ticket") + @option("message", description="Message to send with notification") + async def notify(self, ctx: discord.ApplicationContext, message:str = ""): + ticket_id = self.bot.parse_thread_title(ctx.channel.name) ticket = self.redmine.ticket_mgr.get(ticket_id, include="watchers") # inclde the option watchers/collaborators field if ticket: # * notify owner and collaborators of *notable* (not all) status changes of a ticket @@ -511,8 +512,9 @@ async def alert_ticket(self, ctx: discord.ApplicationContext, ticket_id:int): if not thread: await ctx.respond(f"ERROR: No thread for ticket ID: {ticket_id}, assign a fall-back") ## TODO return - msg = f"Ticket {ticket.id} is about will expire soon." - await thread.send(self.bot.formatter.format_ticket_alert(ticket, discord_ids, msg)) + if message == "": + message = f"Ticket {ticket.id} is about will expire soon." + await thread.send(self.bot.formatter.format_ticket_alert(ticket, discord_ids, message)) await ctx.respond("Alert sent.") else: await ctx.respond(f"ERROR: Unkown ticket ID: {ticket_id}") ## TODO format error message diff --git a/netbot/formatting.py b/netbot/formatting.py index dd0b992..29649cf 100644 --- a/netbot/formatting.py +++ b/netbot/formatting.py @@ -258,10 +258,10 @@ def format_expiration_notification(self, ticket:Ticket, discord_ids: list[str]): return f"ALERT: Expiring ticket: {self.redmine_link(ticket)} {' '.join(ids_str)}" - def format_ticket_alert(self, ticket: Ticket, discord_ids: list[str], msg: str) -> str: - ids_str = ["<@" + id + ">" for id in discord_ids] + def format_ticket_alert(self, ticket: Ticket, discord_ids: set[int], msg: str) -> str: + ids_str = [f"<@{id}>" for id in discord_ids] log.debug(f"ids_str={ids_str}, discord_ids={discord_ids}") - return f"ALERT #{self.redmine_link(ticket)} {' '.join(ids_str)}: {msg}" + return f"⚠️ {self.redmine_link(ticket)} {' '.join(ids_str)}: {msg}" def ticket_color(self, ticket:Ticket) -> discord.Color: diff --git a/netbot/netbot.py b/netbot/netbot.py index d0673ea..66dcb74 100755 --- a/netbot/netbot.py +++ b/netbot/netbot.py @@ -403,7 +403,7 @@ def find_ticket_thread(self, ticket_id:int) -> discord.Thread|None: return None # not found - def extract_ids_from_ticket(self, ticket: Ticket) -> set[str]: + def extract_ids_from_ticket(self, ticket: Ticket) -> set[int]: """Extract the Discord IDs from users interested in a ticket, using owner and collaborators""" # owner and watchers @@ -419,7 +419,7 @@ def extract_ids_from_ticket(self, ticket: Ticket) -> set[str]: user = self.redmine.user_mgr.cache.get(named.id) #expected to be cached if user: if user.discord_id: - discord_ids.add(user.discord_id) + discord_ids.add(user.discord_id.id) else: log.info(f"No Discord ID for {named}") else: diff --git a/tests/test_users.py b/tests/test_users.py new file mode 100644 index 0000000..bbb6e08 --- /dev/null +++ b/tests/test_users.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Redmine user manager test cases""" + +import unittest +import logging +import json +from unittest.mock import MagicMock, patch + +from dotenv import load_dotenv + +from redmine.model import NamedId, DISCORD_ID_FIELD + +from tests import test_utils + + +log = logging.getLogger(__name__) + +class TestUserManager(test_utils.MockRedmineTestCase): + """Mocked testing of user manager""" + + @patch('redmine.session.RedmineSession.get') + @patch('redmine.session.RedmineSession.put') + def test_create_discord_mapping(self, mock_put:MagicMock, mock_get:MagicMock): + # create a new user + user = test_utils.mock_user(unittest.TestCase.id(self)) + + # add discord details + discord_id = test_utils.randint() + discord_name = test_utils.randstr() + user.set_custom_field(2, DISCORD_ID_FIELD, f"{discord_id}|{discord_name}") + + # set up mock get return with the updated user + mock_get.return_value = { "user": user.asdict() } + + # TEST create the discord mapping + updated = self.user_mgr.create_discord_mapping(user, discord_id, discord_name) + + # this should call put on session. + # mock_put ... use to test for proper calling info + mock_put.assert_called_once() + # check params to PUT? + + # get is called after the PUT to get a fresh user, mocked above. + # confirm it is correct + self.assertIsNotNone(updated) + self.assertIsNotNone(updated.discord_id) + self.assertEqual(discord_id, updated.discord_id.id) + self.assertEqual(discord_name, updated.discord_id.name) + + mock_get.assert_called_once() + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG, + format="{asctime} {levelname:<8s} {name:<16} {message}", + datefmt='%Y-%m-%d %H:%M:%S', + style='{') + logging.getLogger("urllib3.connectionpool").setLevel(logging.INFO) + + unittest.main() \ No newline at end of file