From 68ac7484c6ef50285f9fe626db6a8f952ed1b463 Mon Sep 17 00:00:00 2001 From: Paul Philion Date: Thu, 17 Oct 2024 15:39:06 -0700 Subject: [PATCH 1/2] Adding /ticket due command to set due date and update calendar --- netbot/cog_tickets.py | 39 +++++++++++++++++++++++++++++++++++++-- redmine/session.py | 1 + redmine/synctime.py | 10 +++++++++- requirements.txt | 6 ++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/netbot/cog_tickets.py b/netbot/cog_tickets.py index 8dc52a2..ff10a04 100644 --- a/netbot/cog_tickets.py +++ b/netbot/cog_tickets.py @@ -3,16 +3,19 @@ """encapsulate Discord ticket functions""" import logging +import datetime as dt import discord - from discord.commands import option, SlashCommandGroup - from discord.ext import commands from discord.enums import InputTextStyle from discord.ui.item import Item, V from discord.utils import basic_autocomplete + +import dateparser + from redmine.model import Message, Ticket +from redmine import synctime from redmine.redmine import Client from netbot.netbot import NetBot, TEAM_MAPPING, CHANNEL_MAPPING, default_ticket @@ -629,6 +632,38 @@ async def subject(self, ctx: discord.ApplicationContext, ticket_id:int, subject: embed=self.bot.formatter.ticket_embed(ctx, updated)) + @ticket.command(name="due", description="Set a due date for the ticket") + @option("date", description="New ticket due date") + async def due(self, ctx: discord.ApplicationContext, date_str:str): + # automatiuc date conversion? + # get the ticket ID from the thread: + ticket_id = ctx.bot.parse_thread_title(ctx.channel.name) + if ticket_id: + # got a valid ticket, update it + # standard date string, date format, etc. + due_date = dateparser.parse(date_str) + if due_date: + due_str = synctime.date_str(due_date) + ticket = self.redmine.ticket_mgr.update(ticket_id, {"due_date": due_str}) + if ticket: + # valid ticket, create an event + event_name = f"Ticket {ticket.id} Due" + event = await ctx.guild.create_scheduled_event( + name = event_name, + description = ticket.subject, + start_time = due_date, + end_time = due_date + dt.timedelta(minutes=10), + location = ctx.channel.name) + await ctx.respond(f"Updated due date on {ticket_id} to {due_str}, scheduled event {event}.") + else: + await ctx.respond(f"Problem updating ticket {ticket_id}, unknown ticket ID.") + else: + await ctx.respond(f"Invalid date value entered. Unable to parse `{date_str}`") + else: + # no ticket available. + await ctx.respond("Command only valid in ticket thread. No ticket info found in this thread.") + + @ticket.command(name="help", description="Display hepl about ticket management") async def help(self, ctx: discord.ApplicationContext): await ctx.respond(embed=self.bot.formatter.help_embed(ctx)) diff --git a/redmine/session.py b/redmine/session.py index 21ab892..d7885f9 100644 --- a/redmine/session.py +++ b/redmine/session.py @@ -95,6 +95,7 @@ def put(self, resource: str, data:str, impersonate_id:str|None=None) -> None: if r.ok: log.debug(f"PUT {resource}: {data}") else: + log.warning(f"Request: {data}, impersonate_id={impersonate_id}") raise RedmineException(f"PUT {resource} by {impersonate_id} failed, status=[{r.status_code}] {r.reason}", r.headers['X-Request-Id']) diff --git a/redmine/synctime.py b/redmine/synctime.py index f683035..a5a0460 100644 --- a/redmine/synctime.py +++ b/redmine/synctime.py @@ -10,8 +10,11 @@ log = logging.getLogger(__name__) +# 2014-01-02 +DATE_FORMAT = "%Y-%m-%d" + # 2014-01-02T08:12:32Z -ZULU_FORMAT = "%Y-%m-%dT%H:%M:%SZ" +ZULU_FORMAT = DATE_FORMAT + "T%H:%M:%SZ" def now() -> dt.datetime: @@ -47,6 +50,11 @@ def age_str(time:dt.datetime) -> str: return humanize.naturaldelta(age(time)) +def date_str(timestamp:dt.datetime) -> str: + """convert a datetime to the UTC Zulu string redmine expects""" + return timestamp.strftime(DATE_FORMAT) + + def zulu(timestamp:dt.datetime) -> str: """convert a datetime to the UTC Zulu string redmine expects""" return timestamp.strftime(ZULU_FORMAT) diff --git a/requirements.txt b/requirements.txt index f54f71e..ed1c9fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ charset-normalizer==3.2.0 click==8.1.7 coverage==7.4.0 cryptography==42.0.0 +dateparser==1.2.0 dill==0.3.8 frozenlist==1.4.0 humanize==4.8.0 @@ -28,11 +29,16 @@ Pygments==2.16.1 pylint==3.0.3 pynetbox==7.0.1 pyOpenSSL==24.0.0 +python-dateutil==2.9.0.post0 python-dotenv==1.0.0 python-gnupg==0.5.2 +pytz==2024.2 +regex==2024.9.11 requests==2.31.0 rich==13.6.0 ruff==0.3.0 +six==1.16.0 tomlkit==0.12.3 +tzlocal==5.2 urllib3==2.0.4 yarl==1.9.2 From 54a5a6a73866754ec5fcf25548c0db9b34997e13 Mon Sep 17 00:00:00 2001 From: Paul Philion Date: Thu, 17 Oct 2024 17:47:46 -0700 Subject: [PATCH 2/2] Adding check for existing event, and updating it. --- netbot/cog_tickets.py | 45 ++++++++++++++++++++++++++++++++++--------- redmine/synctime.py | 1 + 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/netbot/cog_tickets.py b/netbot/cog_tickets.py index ff10a04..d531d50 100644 --- a/netbot/cog_tickets.py +++ b/netbot/cog_tickets.py @@ -6,6 +6,7 @@ import datetime as dt import discord +from discord import ScheduledEvent from discord.commands import option, SlashCommandGroup from discord.ext import commands from discord.enums import InputTextStyle @@ -219,6 +220,7 @@ def __init__(self, bot:NetBot): self.bot:NetBot = bot self.redmine: Client = bot.redmine + # see https://github.com/Pycord-Development/pycord/blob/master/examples/app_commands/slash_cog_groups.py ticket = SlashCommandGroup("ticket", "ticket commands") @@ -632,6 +634,13 @@ async def subject(self, ctx: discord.ApplicationContext, ticket_id:int, subject: embed=self.bot.formatter.ticket_embed(ctx, updated)) + async def find_event_for_ticket(self, ctx: discord.ApplicationContext, ticket_id:int) -> ScheduledEvent: + title_prefix = f"Ticket #{ticket_id}" + for event in await ctx.guild.fetch_scheduled_events(): + if event.name.startswith(title_prefix): + return event + + @ticket.command(name="due", description="Set a due date for the ticket") @option("date", description="New ticket due date") async def due(self, ctx: discord.ApplicationContext, date_str:str): @@ -641,20 +650,38 @@ async def due(self, ctx: discord.ApplicationContext, date_str:str): if ticket_id: # got a valid ticket, update it # standard date string, date format, etc. - due_date = dateparser.parse(date_str) + due_date = dateparser.parse(date_str, settings={ + 'RETURN_AS_TIMEZONE_AWARE': True, + 'PREFER_DATES_FROM': 'future', + 'REQUIRE_PARTS': ['day', 'month', 'year']}) if due_date: due_str = synctime.date_str(due_date) ticket = self.redmine.ticket_mgr.update(ticket_id, {"due_date": due_str}) if ticket: # valid ticket, create an event - event_name = f"Ticket {ticket.id} Due" - event = await ctx.guild.create_scheduled_event( - name = event_name, - description = ticket.subject, - start_time = due_date, - end_time = due_date + dt.timedelta(minutes=10), - location = ctx.channel.name) - await ctx.respond(f"Updated due date on {ticket_id} to {due_str}, scheduled event {event}.") + # check for time, default to 9am if 0 + if due_date.hour == 0: + due_date.hour = 9 # DEFAULT "meeting" time, 9am local time (for the bot) + + event_name = f"Ticket #{ticket.id} Due" + + # check for existing event + existing = await self.find_event_for_ticket(ctx, ticket.id) + if existing: + await existing.edit( + start_time = due_date, + end_time = due_date + dt.timedelta(hours=1)) # DEFAULT "meeting" length, one hour) + log.info(f"Updated existing DUE event: {existing.name}") + await ctx.respond(f"Updated due date on {ticket_id} to {due_date.strftime(synctime.DATETIME_FORMAT)}") + else: + event = await ctx.guild.create_scheduled_event( + name = event_name, + description = ticket.subject, + start_time = due_date, + end_time = due_date + dt.timedelta(hours=1), # DEFAULT "meeting" length, one hour + location = ctx.channel.name) + log.info(f"created event {event} for ticket={ticket.id}") + await ctx.send(f"Updated due date on ticket #{ticket_id} to {due_date}.") else: await ctx.respond(f"Problem updating ticket {ticket_id}, unknown ticket ID.") else: diff --git a/redmine/synctime.py b/redmine/synctime.py index a5a0460..cca2f96 100644 --- a/redmine/synctime.py +++ b/redmine/synctime.py @@ -12,6 +12,7 @@ # 2014-01-02 DATE_FORMAT = "%Y-%m-%d" +DATETIME_FORMAT = DATE_FORMAT + " %H:%M" # 2014-01-02T08:12:32Z ZULU_FORMAT = DATE_FORMAT + "T%H:%M:%SZ"