Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ticket 787 - Clean up UX for Discord bot ticket commands #20

Merged
merged 3 commits into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions cog_scn.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from discord.ext import commands

from model import Message
from redmine import Client
from redmine import Client, BLOCKED_TEAM_NAME


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -188,15 +188,22 @@ async def teams(self, ctx:discord.ApplicationContext, teamname:str=None):
await ctx.respond(f"Unknown team name: {teamname}") # error
else:
# all teams
teams = self.redmine.get_teams()
teams = self.redmine.user_mgr.cache.get_teams()
buff = ""
for teamname in teams:
team = self.redmine.get_team(teamname)
#await self.print_team(ctx, team)
for team in teams:
buff += self.format_team(team)
await ctx.respond(buff[:2000]) # truncate!


@scn.command(description="list blocked email")
async def blocked(self, ctx:discord.ApplicationContext):
team = self.redmine.get_team(BLOCKED_TEAM_NAME)
if team:
await ctx.respond(self.format_team(team))
else:
await ctx.respond(f"Expected team {BLOCKED_TEAM_NAME} not configured") # error


# ticket 484 - http://10.10.0.218/issues/484
# block users based on name (not discord membership)
@scn.command(description="block specific a email address and reject all related tickets")
Expand Down
132 changes: 81 additions & 51 deletions cog_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

import discord

from discord.commands import option
from discord.commands import option, SlashCommandGroup

from discord.ext import commands

from model import Message, Ticket
Expand All @@ -29,6 +30,8 @@ def __init__(self, bot):

# see https://github.com/Pycord-Development/pycord/blob/master/examples/app_commands/slash_cog_groups.py

ticket = SlashCommandGroup("ticket", "ticket commands")


# figure out what the term refers to
# could be ticket#, team name, user name or search term
Expand All @@ -47,8 +50,9 @@ def resolve_query_term(self, term):
return self.redmine.search_tickets(term)


@commands.slash_command() # guild_ids=[...] # Create a slash command for the supplied guilds.
async def tickets(self, ctx: discord.ApplicationContext, params: str = ""):
@ticket.command(description="Query tickets")
@option("term", description="Ticket query term, should includes ticket ID, ticket owner, team or any term used for a text match.")
async def query(self, ctx: discord.ApplicationContext, term: str):
"""List tickets for you, or filtered by parameter"""
# different options: none, me (default), [group-name], intake, tracker name
# buid index for trackers, groups
Expand All @@ -58,58 +62,85 @@ async def tickets(self, ctx: discord.ApplicationContext, params: str = ""):
user = self.redmine.user_mgr.find(ctx.user.name)
log.debug(f"found user mapping for {ctx.user.name}: {user}")

args = params.split()
args = term.split()

if len(args) == 0 or args[0] == "me":
if args[0] == "me":
await self.bot.formatter.print_tickets("My Tickets", self.redmine.my_tickets(user.login), ctx)
elif len(args) == 1:
query = args[0]
results = self.resolve_query_term(query)
await self.bot.formatter.print_tickets(f"Search for '{query}'", results, ctx)

@ticket.command(description="Get ticket details")
@option("term", description="ticket ID")
async def details(self, ctx: discord.ApplicationContext, ticket_id:int):
"""Update status on a ticket, using: unassign, resolve, progress"""
#log.debug(f"found user mapping for {ctx.user.name}: {user}")
ticket = self.redmine.get_ticket(ticket_id)
if ticket:
await self.bot.formatter.print_ticket(ticket, ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.") # print error


@commands.slash_command()
async def ticket(self, ctx: discord.ApplicationContext, ticket_id:int, action:str="show"):
@ticket.command(description="Unassign a ticket")
@option("ticket_id", description="ticket ID")
async def unassign(self, ctx: discord.ApplicationContext, ticket_id:int):
"""Update status on a ticket, using: unassign, resolve, progress"""
try:
# lookup the user
user = self.redmine.user_mgr.find(ctx.user.name)
log.debug(f"found user mapping for {ctx.user.name}: {user}")

match action:
case "show":
ticket = self.redmine.get_ticket(ticket_id)
if ticket:
await self.bot.formatter.print_ticket(ticket, ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.")
case "details":
# FIXME
ticket = self.redmine.get_ticket(ticket_id)
if ticket:
await self.bot.formatter.print_ticket(ticket, ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.")
case "unassign":
self.redmine.unassign_ticket(ticket_id, user.login)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
case "resolve":
self.redmine.resolve_ticket(ticket_id, user.login)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
case "progress":
self.redmine.progress_ticket(ticket_id, user.login)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
#case "note":
# msg = ???
# self.redmine.append_message(ticket_id, user.login, msg)
case "assign":
self.redmine.assign_ticket(ticket_id, user.login)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
case _:
await ctx.respond("unknown command: {action}")
except Exception as e:
log.exception(e)
await ctx.respond(f"Error {action} {ticket_id}: {e}")
# lookup the user
user = self.redmine.user_mgr.find(ctx.user.name)
#log.debug(f"found user mapping for {ctx.user.name}: {user}")
ticket = self.redmine.get_ticket(ticket_id)
if ticket:
self.redmine.unassign_ticket(ticket_id, user.login)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.") # print error


@ticket.command(description="Resolve a ticket")
@option("ticket_id", description="ticket ID")
async def resolve(self, ctx: discord.ApplicationContext, ticket_id:int):
"""Update status on a ticket, using: unassign, resolve, progress"""
# lookup the user
user = self.redmine.user_mgr.find(ctx.user.name)
#log.debug(f"found user mapping for {ctx.user.name}: {user}")
ticket = self.redmine.get_ticket(ticket_id)
if ticket:
self.redmine.resolve_ticket(ticket_id, user.login)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.") # print error


@ticket.command(description="Mark a ticket in-progress")
@option("ticket_id", description="ticket ID")
async def progress(self, ctx: discord.ApplicationContext, ticket_id:int):
"""Update status on a ticket, using: unassign, resolve, progress"""
# lookup the user
user = self.redmine.user_mgr.find(ctx.user.name)
#log.debug(f"found user mapping for {ctx.user.name}: {user}")
ticket = self.redmine.get_ticket(ticket_id)
if ticket:
self.redmine.progress_ticket(ticket_id, user.login)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.") # print error


@ticket.command(description="Assign a ticket")
@option("ticket_id", description="ticket ID")
async def assign(self, ctx: discord.ApplicationContext, ticket_id:int):
"""Update status on a ticket, using: unassign, resolve, progress"""
# lookup the user
user = self.redmine.user_mgr.find(ctx.user.name)
#log.debug(f"found user mapping for {ctx.user.name}: {user}")
ticket = self.redmine.get_ticket(ticket_id)
if ticket:
self.redmine.assign_ticket(ticket_id, user.login)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.") # print error


async def create_thread(self, ticket:Ticket, ctx:discord.ApplicationContext):
Expand All @@ -122,7 +153,7 @@ async def create_thread(self, ticket:Ticket, ctx:discord.ApplicationContext):
return thread


@commands.slash_command(name="new", description="Create a new ticket")
@ticket.command(name="new", description="Create a new ticket")
@option("title", description="Title of the new SCN ticket")
@option("add_thread", description="Create a Discord thread for the new ticket", default=False)
async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str):
Expand All @@ -131,7 +162,6 @@ async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str):
await ctx.respond(f"Unknown user: {ctx.user.name}")
return
channel_name = ctx.channel.name
# text templating
text = f"ticket created by Discord user {ctx.user.name} -> {user.login}, with the text: {title}"
message = Message(from_addr=user.mail, subject=title, to=ctx.channel.name)
message.set_note(text)
Expand All @@ -152,15 +182,15 @@ async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str):
else:
log.debug(f"not tracker for {channel_name}")
# create related discord thread
await self.thread_ticket(ctx, ticket.id)
await self.thread(ctx, ticket.id)
#await self.bot.formatter.print_ticket(ticket, ctx)
else:
await ctx.respond(f"Error creating ticket with title={title}")


@commands.slash_command(name="thread", description="Create a Discord thread for the specified ticket")
@ticket.command(description="Thread a Redmine ticket in Discord")
@option("ticket_id", description="ID of tick to create thread for")
async def thread_ticket(self, ctx: discord.ApplicationContext, ticket_id:int):
async def thread(self, ctx: discord.ApplicationContext, ticket_id:int):
ticket = self.redmine.get_ticket(ticket_id)
if ticket:
# create the thread...
Expand Down
105 changes: 79 additions & 26 deletions docs/netbot-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,42 +102,95 @@ List the blocked users.

Should be the same as `/scn teams blocked`.


## Ticket Commands

The `/tickets` command is used to information about tickets in Redmine
The `/ticket` slash command is used to information about tickets in Redmine. A number of commands are provided:
* query
* details
* new
* thread
* assign
* unassign
* progress
* resolve

### `/ticket query me` - Query my tickets

The the list of tickets for *me* (you, the reader) to work on.

```
/ticket query me
```

### `/ticket query [term]` - Query tickets about *term*

*term* can be a user, a team or any term that will be used for a full text search for tickets (open and closed) that match the *term*.

```
/ticket query test
```
will return any tickets that reference the term "test".

### /tickets
- My tickets
### `/ticket details [ticket-id]` - Show details about a specific ticket

### /tickets team
- Tickets assigned to team [team]
Show ticket *ticket-id* with full details.

### /tickets user
- Tickets assigned to a specific [user]
```
/ticket details 787
```
will display all the ticket information for ticket ID 787

### /tickets query-term
- Full text search for all tickets (open and closed) that match the query-term
### `/ticket new [title]` - Create a new ticket

### /ticket 42
- Show ticket info for ticket 42
Create a new ticket, with the associated title. This will also create a new Discord thread for the ticket. If the command is issued in an existing thread, that thread is used and synched with redmine.

### /ticket 42 details
- Show ticket 42 with all notes (in a decent format)
```
/ticket new Upgrade the server to the v1.62 LTS
```
Will create a new ticket with the title "Upgrade the server to the v1.62 LTS", and a thread for the new ticket in Discord.

### `/ticket thread [ticket-id]` - Create a Discord thread for a ticket

### /ticket 42 assign
- Assign ticket 42 to yourself (the invoking user)
- `/ticket 42 assign @bob` would assign ticket 42 to Bob.
Create a new discord thread from an existing ticket *ticket-id*, using the ticket title as the thread title. The thread is created in the channel it is invoked in, and all notes from the existing ticket are copied into the thread.

### /ticket 42 unassign
- Mark ticket 42 new and unassigned.
```
/ticket thread 787
```
will create a new thread for ticket 787 in the current Discord channel.

### /ticket 42 progress
- Set ticket 42 it to in-progress.
### `/ticket assign [ticket-id]` - Assign a ticket

### /ticket 42 resolve
- Mark the ticket resolved.
Assign ticket *ticket-id* to yourself (the invoking user).

### /new Title of a new ticket to be created
- Create a new ticket with the supplied title.
- FUTURE: Will popup a modal dialog to collect ...
- OR just supply params with default values for tracker, etc.
```
/ticket assign 787
```
will assign ticket 787 to you.

### `/ticket unassign [ticket-id]` - Unassign a ticket

Mark ticket *ticket-id* new and unassigned, so it can be assigned for someone else to work on.

```
/ticket unassign 787
```
will unassign (you) from ticker 787 and set it's status to "new" and it's owner to "intake".

### `/ticket progress [ticket-id]` - Set a ticket to in-progress

Mark ticket *ticket-id* to in-progress and assign it to yourself.

```
/ticket progress 787
```
will set the status of ticket 787 to in-progress and assign it to you.

### `/ticket resolve [ticket-id]` - Resolve a ticket

Mark ticket *ticket-id* resolved.

```
/ticket thread 787
```
will mark ticket 787 resolved. Well done!
2 changes: 0 additions & 2 deletions model.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,6 @@ def get_sync_record(self, expected_channel: int = 0) -> synctime.SyncRecord | No
# Parse custom_field into datetime
# lookup field by name
token = self.get_custom_field(SYNC_FIELD_NAME)
#log.info(f"### found '{token}' for #{self.id}:{SYNC_FIELD_NAME}")
#log.info(f"### custom field: {self.custom_fields}")
if token:
record = synctime.SyncRecord.from_token(self.id, token)
log.debug(f"created sync_rec from token: {record}")
Expand Down
2 changes: 1 addition & 1 deletion netbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,7 @@ def main():

def setup_logging():
"""set up logging for netbot"""
logging.basicConfig(level=logging.INFO,
logging.basicConfig(level=logging.DEBUG,
format="{asctime} {levelname:<8s} {name:<16} {message}", style='{')
logging.getLogger("discord.gateway").setLevel(logging.WARNING)
logging.getLogger("discord.http").setLevel(logging.WARNING)
Expand Down
7 changes: 4 additions & 3 deletions redmine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import synctime
from session import RedmineSession
from model import Message, Ticket, User
from model import Message, Ticket, User, Team
from users import UserManager
from tickets import TicketManager, SCN_PROJECT_ID

Expand Down Expand Up @@ -170,8 +170,9 @@ def unassign_ticket(self, ticket_id, user_id=None):
def resolve_ticket(self, ticket_id, user_id=None):
return self.ticket_mgr.resolve_ticket(ticket_id, user_id)

def get_team(self, teamname:str):
return self.user_mgr.get_team_by_name(teamname) # FIXME consistent naming
def get_team(self, teamname:str) -> Team:
#return self.user_mgr.get_team_by_name(teamname) # FIXME consistent naming
return self.user_mgr.cache.get_team_by_name(teamname)

def update_sync_record(self, record:synctime.SyncRecord):
log.debug(f"Updating sync record in redmine: {record}")
Expand Down
Loading
Loading