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-1207: Adding admin override for for assign, progress and collaborate #29

Merged
merged 2 commits into from
Sep 25, 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
20 changes: 20 additions & 0 deletions docs/devlog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
## Development Log

### 2024-09-24

Starting work on ticket 1207, to add admin overrides for all actions that default to self.

Added admin-specific params to allow member assignement for:
* collaborate
* progress
* assign

Adding ticket ID autofill if in ticket thread:
* collaborate
* progress
* assign
* unassign
* resolve
* tracker
* priority
* subject


### 2024-09-05
*Haven't updated in a year. Sorry.*
*I've got the notes, I just haven't put them here. I can if they are needed.*
Expand Down
11 changes: 8 additions & 3 deletions netbot/cog_scn.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

import discord

from discord.commands import SlashCommandGroup
from discord.commands import option, SlashCommandGroup
from discord.ext import commands
from discord.utils import basic_autocomplete

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

from netbot.netbot import default_ticket

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -164,7 +166,10 @@ def is_admin(self, user: discord.Member) -> bool:
return False


@scn.command()
# FIXME rename to "register"?
@scn.command(description="Add a Discord user to redmine")
@option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket))
@option("member", description="Discord member collaborating with ticket", optional=True)
async def add(self, ctx:discord.ApplicationContext, redmine_login:str, member:discord.Member=None):
"""add a Discord user to the Redmine ticketing integration"""
discord_name = ctx.user.name # by default, assume current user
Expand Down Expand Up @@ -327,7 +332,7 @@ async def block(self, ctx:discord.ApplicationContext, username:str):
# add the user to the blocked list
self.redmine.user_mgr.block(user)
# search and reject all tickets from that user
for ticket in self.redmine.get_tickets_by(user):
for ticket in self.redmine.ticket_mgr.get_by(user):
self.redmine.reject_ticket(ticket.id)
await ctx.respond(f"Blocked user: {user.login} and rejected all created tickets")
else:
Expand Down
113 changes: 60 additions & 53 deletions netbot/cog_tickets.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from discord.utils import basic_autocomplete
from redmine.model import Message, Ticket
from redmine.redmine import Client
from netbot.netbot import NetBot, TEAM_MAPPING, CHANNEL_MAPPING
from netbot.netbot import NetBot, TEAM_MAPPING, CHANNEL_MAPPING, default_ticket


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -197,16 +197,6 @@ async def callback(self, interaction: discord.Interaction):
await interaction.response.send_message(f"EditView.callback() {interaction.data}")


# WHERE SHOULD THIS GO?
# returns ticket id or none
def default_ticket(ctx: discord.AutocompleteContext) -> list[int]:
# examine the thread
ticket_id = ctx.bot.parse_thread_title(ctx.interaction.channel.name)
if ticket_id:
return [ticket_id]
else:
return []

# distinct from above. takes app-context
def default_term(ctx: discord.ApplicationContext) -> str:
# examine the thread
Expand Down Expand Up @@ -237,7 +227,7 @@ def resolve_query_term(self, term) -> list[Ticket]:
# special cases: ticket num and team name
try:
int_id = int(term)
ticket = self.redmine.get_ticket(int_id, include="children") # get the children
ticket = self.redmine.ticket_mgr.get(int_id, include="children") # get the children
if ticket:
log.debug(f"QQQ<: {ticket}")
return [ticket]
Expand Down Expand Up @@ -312,60 +302,68 @@ async def query(self, ctx: discord.ApplicationContext, term:str = ""):
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, include="children")
ticket = self.redmine.ticket_mgr.get(ticket_id, include="children")
if ticket:
await self.bot.formatter.print_ticket(ticket, ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.") # print error



@ticket.command(description="Collaborate on a ticket")
@option("ticket_id", description="ticket ID")
async def collaborate(self, ctx: discord.ApplicationContext, ticket_id:int):
@option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket))
@option("member", description="Discord member collaborating with ticket", optional=True)
async def collaborate(self, ctx: discord.ApplicationContext, ticket_id:int, member:discord.Member=None):
"""Add yourself as a collaborator on a ticket"""
# lookup the user
user = self.redmine.user_mgr.find(ctx.user.name)
user_name = ctx.user.name
if member:
if self.bot.is_admin(ctx.user):
log.info(f"ADMIN: {ctx.user.name} invoked collaborate on behalf of {member.name}")
user_name = member.name
user = self.redmine.user_mgr.find(user_name)
if not user:
await ctx.respond(f"User {ctx.user.name} not mapped to redmine. Use `/scn add [redmine-user]` to create the mapping.")
return
ticket = self.redmine.get_ticket(ticket_id)

ticket = self.redmine.ticket_mgr.get(ticket_id)
if ticket:
self.redmine.ticket_mgr.collaborate(ticket_id, user)
await self.bot.formatter.print_ticket(self.redmine.get_ticket(ticket_id), ctx)
self.redmine.ticket_mgr.collaborate(ticket.id, user)
await self.bot.formatter.print_ticket(self.redmine.ticket_mgr.get(ticket.id), ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.") # print error


@ticket.command(description="Unassign a ticket")
@option("ticket_id", description="ticket ID")
@option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket))
async def unassign(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)
if not user:
await ctx.respond(f"User {ctx.user.name} not mapped to redmine. Use `/scn add` to create the mapping.") # error
return
ticket = self.redmine.get_ticket(ticket_id)

ticket = self.redmine.ticket_mgr.get(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)
self.redmine.ticket_mgr.unassign(ticket.id, user.login)
await self.bot.formatter.print_ticket(self.redmine.ticket_mgr.get(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")
@option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket))
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)
if not user:
await ctx.respond(f"User {ctx.user.name} not mapped to redmine. Use `/scn add` to create the mapping.") # error
return
ticket = self.redmine.get_ticket(ticket_id)

ticket = self.redmine.ticket_mgr.get(ticket_id)
if ticket:
updated = self.redmine.resolve_ticket(ticket_id, user.login)
updated = self.redmine.ticket_mgr.resolve(ticket_id, user.login)
ticket_link = self.bot.formatter.format_link(ticket)
await ctx.respond(
f"Updated {ticket_link}, status: {ticket.status} -> {updated.status}",
Expand All @@ -375,16 +373,22 @@ async def resolve(self, ctx: discord.ApplicationContext, ticket_id:int):


@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"""
@option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket))
@option("member", description="Discord member taking ownership", optional=True)
async def progress(self, ctx: discord.ApplicationContext, ticket_id:int, member:discord.Member=None):
"""Update status on a ticket, using: progress"""
# lookup the user
user = self.redmine.user_mgr.find(ctx.user.name)
user_name = ctx.user.name
if member:
if self.bot.is_admin(ctx.user):
log.info(f"ADMIN: {ctx.user.name} invoked progress on behalf of {member.name}")
user_name = member.name
user = self.redmine.user_mgr.find(user_name)
if not user:
await ctx.respond(f"User {ctx.user.name} not mapped to redmine. Use `/scn add` to create the mapping.") # error
await ctx.respond(f"User {ctx.user.name} not mapped to redmine. Use `/scn add [redmine-user]` to create the mapping.")
return

ticket = self.redmine.get_ticket(ticket_id)
ticket = self.redmine.ticket_mgr.get(ticket_id)
if ticket:
updated = self.redmine.progress_ticket(ticket_id, user.login)
ticket_link = self.bot.formatter.format_link(ticket)
Expand All @@ -396,18 +400,24 @@ async def progress(self, ctx: discord.ApplicationContext, ticket_id:int):


@ticket.command(description="Assign a ticket")
@option("ticket_id", description="ticket ID")
async def assign(self, ctx: discord.ApplicationContext, ticket_id:int):
@option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket))
@option("member", description="Discord member taking ownership", optional=True)
async def assign(self, ctx: discord.ApplicationContext, ticket_id:int, member:discord.Member=None):
# lookup the user
user = self.redmine.user_mgr.find(ctx.user.name)
user_name = ctx.user.name
if member:
if self.bot.is_admin(ctx.user):
log.info(f"ADMIN: {ctx.user.name} invoked assign on behalf of {member.name}")
user_name = member.name
user = self.redmine.user_mgr.find(user_name)
if not user:
await ctx.respond(f"User {ctx.user.name} not mapped to redmine. Use `/scn add` to create the mapping.") # error
await ctx.respond(f"User {ctx.user.name} not mapped to redmine. Use `/scn add [redmine-user]` to create the mapping.")
return

ticket = self.redmine.get_ticket(ticket_id)
ticket = self.redmine.ticket_mgr.get(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)
await self.bot.formatter.print_ticket(self.redmine.ticket_mgr.get(ticket_id), ctx)
else:
await ctx.respond(f"Ticket {ticket_id} not found.") # print error

Expand Down Expand Up @@ -487,13 +497,9 @@ async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str):


@ticket.command(name="alert", description="Alert collaborators on a ticket")
@option("ticket_id", description="ID of ticket to alert")
async def alert_ticket(self, ctx: discord.ApplicationContext, ticket_id:int=0):
if not ticket_id:
# check thread for ticket id
ticket_id = self.bot.parse_thread_title(ctx.channel.name)

ticket = self.redmine.get_ticket(ticket_id, include="watchers") # inclde the option watchers/collaborators field
@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 = 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
# * user @reference for notify
Expand All @@ -515,7 +521,7 @@ async def alert_ticket(self, ctx: discord.ApplicationContext, ticket_id:int=0):
@ticket.command(description="Thread a Redmine ticket in Discord")
@option("ticket_id", description="ID of tick to create thread for")
async def thread(self, ctx: discord.ApplicationContext, ticket_id:int):
ticket = self.redmine.get_ticket(ticket_id)
ticket = self.redmine.ticket_mgr.get(ticket_id)
if ticket:
ticket_link = self.bot.formatter.format_link(ticket)

Expand Down Expand Up @@ -547,11 +553,11 @@ async def thread(self, ctx: discord.ApplicationContext, ticket_id:int):


@ticket.command(name="tracker", description="Update the tracker of a ticket")
@option("ticket_id", description="ID of ticket to update")
@option("ticket_id", description="ID of ticket to update", autocomplete=basic_autocomplete(default_ticket))
@option("tracker", description="Track to assign to ticket", autocomplete=get_trackers)
async def tracker(self, ctx: discord.ApplicationContext, ticket_id:int, tracker:str):
user = self.redmine.user_mgr.find_discord_user(ctx.user.name)
ticket = self.redmine.get_ticket(ticket_id)
ticket = self.redmine.ticket_mgr.get(ticket_id)
if ticket:
ticket_link = self.bot.formatter.format_link(ticket)

Expand All @@ -570,11 +576,11 @@ async def tracker(self, ctx: discord.ApplicationContext, ticket_id:int, tracker:


@ticket.command(name="priority", description="Update the tracker of a ticket")
@option("ticket_id", description="ID of ticket to update")
@option("ticket_id", description="ID of ticket to update", autocomplete=basic_autocomplete(default_ticket))
@option("priority", description="Priority to assign to ticket", autocomplete=get_priorities)
async def priority(self, ctx: discord.ApplicationContext, ticket_id:int, priority_str:str):
user = self.redmine.user_mgr.find_discord_user(ctx.user.name)
ticket = self.redmine.get_ticket(ticket_id)
ticket = self.redmine.ticket_mgr.get(ticket_id)
if ticket:
# look up the priority
priority = self.bot.lookup_priority(priority_str)
Expand All @@ -597,15 +603,15 @@ async def priority(self, ctx: discord.ApplicationContext, ticket_id:int, priorit


@ticket.command(name="subject", description="Update the subject of a ticket")
@option("ticket_id", description="ID of ticket to update")
@option("ticket_id", description="ID of ticket to update", autocomplete=basic_autocomplete(default_ticket))
@option("subject", description="Updated subject")
async def subject(self, ctx: discord.ApplicationContext, ticket_id:int, subject:str):
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

ticket = self.redmine.get_ticket(ticket_id)
ticket = self.redmine.ticket_mgr.get(ticket_id)
if not ticket:
await ctx.respond(f"ERROR: Unkown ticket ID: {ticket_id}")
return
Expand All @@ -620,6 +626,7 @@ async def subject(self, ctx: discord.ApplicationContext, ticket_id:int, subject:
f"Updated subject of {ticket_link} to: {updated.subject}",
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))
25 changes: 23 additions & 2 deletions netbot/netbot.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@
"uw-research-nsf": "research-team",
}

# utility method to get a list of (one) ticket from the title of the channel, or empty list
# TODO could be moved to NetBot
def default_ticket(ctx: discord.AutocompleteContext) -> list[int]:
# examine the thread
ticket_id = ctx.bot.parse_thread_title(ctx.interaction.channel.name)
if ticket_id:
return [ticket_id]
else:
return []


class NetbotException(Exception):
"""netbot exception"""

Expand Down Expand Up @@ -106,7 +117,6 @@ def run_bot(self):

super().run(os.getenv('DISCORD_TOKEN'))


async def on_ready(self):
#log.info(f"Logged in as {self.user} (ID: {self.user.id})")
#log.debug(f"bot: {self}, guilds: {self.guilds}")
Expand Down Expand Up @@ -269,7 +279,7 @@ async def sync_thread(self, thread:discord.Thread):
# get the ticket id from the thread name
ticket_id = self.parse_thread_title(thread.name)

ticket = self.redmine.get_ticket(ticket_id, include="journals")
ticket = self.redmine.ticket_mgr.get(ticket_id, include="journals")
if ticket:
completed = await self.synchronize_ticket(ticket, thread)
# note: synchronize_ticket returns True only when successfully completing a sync
Expand Down Expand Up @@ -441,6 +451,17 @@ def extract_ids_from_ticket(self, ticket: Ticket) -> list[str]:
return []


def is_admin(self, user: discord.Member) -> bool:
"""Check if the given Discord memeber is in a authorized role"""
# search user for "auth" role
for role in user.roles:
if "auth" == role.name: ## FIXME
return True

# auth role not found
return False


def main():
"""netbot main function"""
log.info(f"loading .env for {__name__}")
Expand Down
Loading
Loading