Skip to content

Commit

Permalink
ticket-1203: added ticket parent command and supporting integration t…
Browse files Browse the repository at this point in the history
…est case
  • Loading branch information
Paul Philion committed Nov 26, 2024
1 parent eb75a0f commit 86694e9
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 1 deletion.
15 changes: 15 additions & 0 deletions docs/devlog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Netbot Development Log

## 2024-11-26

Starting work on ticket #1203, `/ticket parent`.

Should be simple to add a new sub-command to `cog_tickets.py`.

Using TDD, let's start with addind a test case, `test_parent_command`

Got that test *failing*, then implemented `parent()` method.

Took awhile to get everything working. Biggest issue was tracking down the name of the parameter requred to actually set the parent: `parent_issue_id`

Once that was figure out, everything was working end-to-end and captured in a full integration test.


## 2024-11-01

Looking at due date parsing, after someone raised a question about format.
Expand Down
38 changes: 38 additions & 0 deletions netbot/cog_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ async def find_event_for_ticket(self, ctx: discord.ApplicationContext, ticket_id
if event.name.startswith(title_prefix):
return event


@staticmethod
def parse_human_date(date_str: str) -> dt.date:
# should the parser be cached?
Expand All @@ -650,6 +651,7 @@ def parse_human_date(date_str: str) -> dt.date:
'PREFER_DATES_FROM': 'future',
'REQUIRE_PARTS': ['day', 'month', 'year']})


@ticket.command(name="due", description="Set a due date for the ticket")
@option("date", description="Due date, in a supported format: 2024-11-01, 11/1/24, next week , 2 months, in 5 days")
async def due(self, ctx: discord.ApplicationContext, date:str):
Expand Down Expand Up @@ -713,6 +715,42 @@ async def edit_description(self, ctx: discord.ApplicationContext):
await ctx.respond(f"Cannot find ticket for {ctx.channel}")


@ticket.command(name="parent", description="Set a parent ticket for ")
@option("parent_ticket", description="The ID of the parent ticket")
async def parent(self, ctx: discord.ApplicationContext, parent_ticket:int):
# /ticket parent 234 <- Get *this* ticket and set the parent to 234.

# get ticket Id from thread
ticket_id = NetBot.parse_thread_title(ctx.channel.name)
if not ticket_id:
# error - no ticket ID
await ctx.respond("Command only valid in ticket thread. No ticket info found in this thread.")
return

# validate user
user = self.redmine.user_mgr.find_discord_user(ctx.user.name)
if not user:
await ctx.respond(f"ERROR: Discord user without redmine config: {ctx.user.name}. Create with `/scn add`")
return

# check that parent_ticket is valid
parent = self.redmine.ticket_mgr.get(parent_ticket)
if not parent:
await ctx.respond(f"ERROR: Unknow ticket #: {parent_ticket}")
return

# update the ticket
params = {
"parent_issue_id": parent_ticket,
}
updated = self.redmine.ticket_mgr.update(ticket_id, params, user.login)
ticket_link = self.bot.formatter.redmine_link(updated)
parent_link = self.bot.formatter.redmine_link(parent)
await ctx.respond(
f"Updated parent of {ticket_link} -> {parent_link}",
embed=self.bot.formatter.ticket_embed(ctx, updated))


@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))
14 changes: 14 additions & 0 deletions netbot/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ def discord_link(self, ctx: discord.ApplicationContext, ticket:Ticket) -> str:
return thread.jump_url


def ticket_link(self, ctx: discord.ApplicationContext, ticket_id:int) -> str:
"""Given a ticket ID, construct a link for the ticket, first looking for related Discord thread"""
# handle none context gracefully
if ctx:
thread = ctx.bot.find_ticket_thread(ticket_id)
if thread:
return thread.jump_url
# didn't find context or ticket
return f"[`#{ticket_id}`]({self.base_url}/issues/{ticket_id})"


def format_tracker(self, tracker:NamedId|None) -> str:
if tracker:
return f"{get_emoji(tracker.name)} {tracker.name}"
Expand Down Expand Up @@ -324,6 +335,9 @@ def ticket_embed(self, ctx: discord.ApplicationContext, ticket:Ticket) -> discor
if ticket.watchers:
embed.add_field(name="Collaborators", value=self.format_collaborators(ctx, ticket))

if ticket.parent:
embed.add_field(name="Parent", value=self.ticket_link(ctx, ticket.parent.id))

# list the sub-tickets
if ticket.children:
buff = ""
Expand Down
50 changes: 50 additions & 0 deletions tests/test_cog_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -494,3 +494,53 @@ async def test_due_command(self):
# delete the ticket and confirm
self.redmine.ticket_mgr.remove(ticket.id)
self.assertIsNone(self.redmine.ticket_mgr.get(ticket.id))


async def test_parent_command(self):
parent_ticket = child_ticket = None
try:
# create a parent ticket
# create a child ticket
# invoke the `parent` command
# validate the parent ticket has subticket child.
parent_ticket = self.create_test_ticket()
child_ticket = self.create_test_ticket()

# build the context without ticket. should fail
ctx = self.build_context()
ctx.channel = AsyncMock(discord.TextChannel)
ctx.channel.name = "Invalid Channel Name"
await self.cog.parent(ctx, parent_ticket.id)
self.assertIn("Command only valid in ticket thread.", ctx.respond.call_args.args[0])

# build the context including ticket, use invalid date
ctx2 = self.build_context()
ctx2.channel = AsyncMock(discord.TextChannel)
ctx2.channel.name = f"Ticket #{child_ticket.id}"
ctx2.channel.id = test_utils.randint()
await self.cog.parent(ctx2, parent_ticket.id)
embed = ctx2.respond.call_args.kwargs['embed']
self.assertIn(str(child_ticket.id), embed.title)
# This checks the field in the embed, BUT it's not mocked correctly in the context
# found = False
# for field in embed.fields:
# if field.name == "Parent":
# print(f"---------- {field.value}")
# self.assertTrue(field.value.endswith(parent_ticket.id))
# found = True
# self.assertTrue(found, "Parent field not found in embed")

# validate the ticket
check = self.redmine.ticket_mgr.get(child_ticket.id)
self.assertIsNotNone(check.parent)
self.assertEqual(parent_ticket.id, check.parent.id)

finally:
if child_ticket:
# delete the ticket and confirm
self.redmine.ticket_mgr.remove(child_ticket.id)
self.assertIsNone(self.redmine.ticket_mgr.get(child_ticket.id))
if parent_ticket:
# delete the ticket and confirm
self.redmine.ticket_mgr.remove(parent_ticket.id)
self.assertIsNone(self.redmine.ticket_mgr.get(parent_ticket.id))
2 changes: 1 addition & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ def setUpClass(cls):


def create_test_message(self) -> Message:
subject = f"TEST {self.tag} {unittest.TestCase.id(self)}"
subject = f"TEST {self.tag} {unittest.TestCase.id(self)} {randint()}"
text = f"This is a ticket for {unittest.TestCase.id(self)} with {self.tag}."
message = Message(self.user.mail, subject, f"to-{self.tag}@example.com", f"cc-{self.tag}@example.com")
message.set_note(text)
Expand Down

0 comments on commit 86694e9

Please sign in to comment.