Skip to content

Commit

Permalink
adding ticket lists to EPIC embeds. cleaning up and refining ticket d…
Browse files Browse the repository at this point in the history
…isplay
  • Loading branch information
Paul Philion committed Aug 13, 2024
1 parent d8b9742 commit fa38a1a
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 27 deletions.
4 changes: 2 additions & 2 deletions cog_scn.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from discord.commands import SlashCommandGroup
from discord.ext import commands
from discord.types import embed

from model import Message, User
from redmine import Client, BLOCKED_TEAM_NAME
Expand Down Expand Up @@ -305,8 +306,7 @@ async def epics(self, ctx:discord.ApplicationContext):
# get the epics.
epics = self.redmine.ticket_mgr.get_epics()
# format the epics and respond
msg = self.bot.formatter.format_epics(epics)
await ctx.respond(msg)
await ctx.respond(embed=self.bot.formatter.epics_embed(ctx, epics))

@scn.command(description="list blocked email")
async def blocked(self, ctx:discord.ApplicationContext):
Expand Down
4 changes: 2 additions & 2 deletions cog_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
log = logging.getLogger(__name__)


def setup(bot):
def setup(bot:NetBot):
bot.add_cog(TicketsCog(bot))
log.info("initialized tickets cog")

Expand Down Expand Up @@ -202,7 +202,7 @@ class TicketsCog(commands.Cog):
"""encapsulate Discord ticket functions"""
def __init__(self, bot:NetBot):
self.bot:NetBot = bot
self.redmine: Client = bot.redmine
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")
Expand Down
76 changes: 60 additions & 16 deletions formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import discord

from model import Ticket, User, NamedId
from model import Ticket, SubTicket, User
import synctime

log = logging.getLogger(__name__)
Expand All @@ -27,8 +27,12 @@
'Urgent': '⚠️',
'Immediate': '❗',
'EPIC': '🎇',
'?': '❓',
}

def get_emoji(key:str) -> str:
return EMOJI.get(key, EMOJI['?'])


COLOR = {
'Resolved': discord.Color.dark_green(),
Expand Down Expand Up @@ -90,16 +94,15 @@ def format_registered_users(self, users: list[User]) -> str:
def build_legend(self, tickets:list[Ticket]) -> dict[str,str]:
# builds an icon-based legend from the status and
# priorities of a list of tickets

legend = {}

# TODO: Ordering?
#TODO: Ordering?
for ticket in tickets:
# track which symbols are used for status and priority
if ticket.status.name not in legend:
legend[ticket.status.name] = EMOJI[ticket.status.name]
legend[ticket.status.name] = get_emoji(ticket.status.name)
if ticket.priority.name not in legend:
legend[ticket.priority.name] = EMOJI[ticket.priority.name]
legend[ticket.priority.name] = get_emoji(ticket.priority.name)

return legend

Expand Down Expand Up @@ -138,13 +141,18 @@ def format_ticket_row(self, ticket:Ticket) -> str:
# link is mostly hidden, so we can't use the length to format.
# but the length of the ticket id can be used
link_padding = ' ' * (5 - len(str(ticket.id))) # field width = 6
status = EMOJI[ticket.status.name]
priority = EMOJI[ticket.priority.name]

status = get_emoji(ticket.status.name) if ticket.status else get_emoji('?')
priority = get_emoji(ticket.priority.name) if ticket.priority else get_emoji('?')
age = synctime.age_str(ticket.updated_on)
assigned = ticket.assigned_to.name if ticket.assigned_to else ""
return f"`{link_padding}`{link}` {status} {priority} {age:<10} {assigned:<18} `{ticket.subject[:60]}"


def format_subticket(self, ticket:SubTicket) -> str:
return f"[{ticket.id}]({self.base_url}/issues/{ticket.id}) - {ticket.subject}"


def format_discord_note(self, note) -> str:
"""Format a note for Discord"""
age = synctime.age_str(note.created_on)
Expand All @@ -153,8 +161,8 @@ def format_discord_note(self, note) -> str:

def format_ticket(self, ticket:Ticket) -> str:
link = self.format_link(ticket)
status = f"{EMOJI[ticket.status.name]} {ticket.status.name}"
priority = f"{EMOJI[ticket.priority.name]} {ticket.priority.name}"
status = f"{get_emoji(ticket.status.name)} {ticket.status.name}"
priority = f"{get_emoji(ticket.priority.name)} {ticket.priority.name}"
assigned = ticket.assigned_to.name if ticket.assigned_to else ""
return " ".join([link, priority, status, ticket.tracker.name, assigned, ticket.subject])

Expand All @@ -174,8 +182,8 @@ def format_ticket_details(self, ticket:Ticket) -> str:
# ### Description
# description text
#link_padding = ' ' * (5 - len(str(ticket.id))) # field width = 6
status = f"{EMOJI[ticket.status.name]} {ticket.status}"
priority = f"{EMOJI[ticket.priority.name]} {ticket.priority}"
status = f"{get_emoji(ticket.status.name)} {ticket.status}"
priority = f"{get_emoji(ticket.priority.name)} {ticket.priority}"
created_age = synctime.age_str(ticket.created_on)
updated_age = synctime.age_str(ticket.updated_on)
assigned = ticket.assigned_to.name if ticket.assigned_to else ""
Expand Down Expand Up @@ -209,14 +217,14 @@ def format_ticket_alert(self, ticket: Ticket, discord_ids: list[str], msg: str)
ids_str = ["@" + id for id in discord_ids]
return f"ALERT #{self.format_link(ticket)} {' '.join(ids_str)}: {msg}"


# OLD REMOVE
def format_epic(self, name: str, epic: list[Ticket]) -> str:
buff = f"**{name}**\n"
for ticket in epic:
buff += self.format_ticket_row(ticket)
return buff


# OLD REMOVE
def format_epics(self, epics: dict[str,list[Ticket]]) -> str:
buff = ""
for name, epic in epics.items():
Expand Down Expand Up @@ -247,9 +255,10 @@ def get_user_id(self, ctx: discord.ApplicationContext, ticket:Ticket) -> str:
user = ctx.bot.redmine.user_mgr.get(ticket.assigned_to.id)
if user and user.discord_id:
member = self.lookup_discord_user(ctx, user.discord_id)
return f"<@!{member.id}>"
else:
return ticket.assigned
if member:
return f"<@!{member.id}>"

return ticket.assigned


def ticket_embed(self, ctx: discord.ApplicationContext, ticket:Ticket) -> discord.Embed:
Expand All @@ -269,6 +278,18 @@ def ticket_embed(self, ctx: discord.ApplicationContext, ticket:Ticket) -> discor
if ticket.assigned_to:
embed.add_field(name="Owner", value=self.get_user_id(ctx, ticket))

if ticket.priority.name == "EPIC":
# list the sub-tickets
epic = ctx.bot.redmine.get_ticket(ticket.id, include="children")
if epic.children:
buff = ""
for child in epic.children:
buff += "- " + self.format_subticket(child) + "\n"
embed.add_field(name="Tickets", value=buff, inline=False)

#subtickets = []
#embed.add_field(name="Tickets", value=self.format_tickets("", subtickets))

# thread & redmine links
thread = ctx.bot.find_ticket_thread(ticket.id)
if thread:
Expand All @@ -278,6 +299,29 @@ def ticket_embed(self, ctx: discord.ApplicationContext, ticket:Ticket) -> discor
return embed


def epics_embed(self, ctx: discord.ApplicationContext, epics: dict[str,list[Ticket]]) -> discord.Embed:
"""Build an embed panel with full ticket details"""
embed = discord.Embed(color=discord.Color.blurple())

for tracker_name, tickets in epics.items():
for epic in tickets:
subject = epic.subject[6:] if epic.subject.startswith("[EPIC]") else epic.subject
subject = f"{ get_emoji('EPIC') } {subject} (#{epic.id})"
embed.add_field(name=subject, value=epic.description, inline=False)
if epic.assigned_to:
embed.add_field(name="Owner", value=self.get_user_id(ctx, epic))
embed.add_field(name="Tracker", value=epic.tracker.name)
embed.add_field(name="Age", value=epic.age_str)
embed.add_field(name="Redmine", value=self.format_link(epic))

if epic.children:
buff = ""
for child in epic.children:
buff += "- " + self.format_subticket(child) + "\n"
embed.add_field(name="", value=buff, inline=False)
return embed


def help_embed(self, ctx: discord.ApplicationContext) -> discord.Embed:
"""Build an embed panel with help"""
embed = discord.Embed(
Expand Down
15 changes: 14 additions & 1 deletion model.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ def __str__(self):
return f"users:({[u.login + ',' for u in self.users]}), total={self.total_count}, {self.limit}/{self.offset}"


@dataclass
class SubTicket:
"""Minimal ticket metadata included when include=children"""
id: int
tracker: NamedId
subject: str

def __post_init__(self):
self.tracker = NamedId(**self.tracker)


SYNC_FIELD_NAME = "syncdata"
TO_CC_FIELD_NAME = "To/CC"

Expand Down Expand Up @@ -252,7 +263,7 @@ class Ticket():
assigned_to: NamedId|None = None
custom_fields: list[CustomField]|None = None
journals: list[TicketNote]|None = None
children: list |None = None
children: list[SubTicket]|None = None
watchers: list[NamedId]|None = None


Expand Down Expand Up @@ -281,6 +292,8 @@ def __post_init__(self):
self.journals = [TicketNote(**note) for note in self.journals]
if self.category and isinstance(self.category, dict):
self.category = NamedId(**self.category)
if self.children and len(self.children) > 0 and isinstance(self.children[0], dict):
self.children = [SubTicket(**child) for child in self.children]

if self.watchers:
self.watchers = [NamedId(**named) for named in self.watchers]
Expand Down
16 changes: 10 additions & 6 deletions tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,26 +227,30 @@ def get_tickets(self, ticket_ids: list[int]) -> list[Ticket]:
log.info(f"Unknown ticket numbers: {ticket_ids}")
return []


def get_epics(self) -> dict[str, list[Ticket]]:
"""Get all the open epics, organized by tracker"""
# query tickets pri = epic
# http://localhost/issues.json?priority_id=14
epic_priority_id = 14 # fixme - lookup based on "EPIC", from redmine.get_priorities()
response = self.session.get(f"/issues.json?priority_id={epic_priority_id}&limit=100")
response = self.session.get(f"/issues.json?priority_id={epic_priority_id}include=children&limit=100")
if not response:
return None
return {}

epics = defaultdict(list)
result = TicketsResult(**response)
if result.total_count > 0:
# iterate to slot by tracker
for epic in result.issues:
tracker_name = epic.tracker.name
# re-get the ticket, to include the children ticket
ticket = self.get(epic.id, include="children")
#log.info(f" {ticket} {ticket.children}")c

tracker_name = ticket.tracker.name if ticket and ticket.tracker else "none"
if tracker_name not in epics.keys():
# create the tracker list
epics[tracker_name] = [epic]
epics[tracker_name] = [ticket] # create list with 1st element
else:
epics[tracker_name].append(epic)
epics[tracker_name].append(ticket)

return epics

Expand Down

0 comments on commit fa38a1a

Please sign in to comment.