Skip to content

Commit dbc1aee

Browse files
author
Paul Philion
committed
Adding /record_time command
1 parent a194a3a commit dbc1aee

8 files changed

Lines changed: 89 additions & 14 deletions

File tree

docs/devlog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Netbot Development Log
22

3+
## 2025-09-08, v0.5
4+
5+
Added `/record_time` command: Captures hours (float), program (popup selector based on /enumerations/time_entry_activities.json) ans an options note.
6+
37
## 2025-01-30
48

59
When deploying the lastest, found a problem with certain emails. The regex used to strip Fordarded emails contains the "match everything" wildcard. Except `.*` does not match new-lines.

netbot/cog_tickets.py

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import datetime as dt
77

88
import discord
9-
from discord import ScheduledEvent
9+
from discord import ScheduledEvent, OptionChoice
1010
from discord.commands import option, SlashCommandGroup
1111
from discord.ext import commands
1212
from discord.enums import InputTextStyle
@@ -19,16 +19,39 @@
1919
from redmine import synctime
2020
from redmine.redmine import Client
2121
from netbot.netbot import NetBot, TEAM_MAPPING, CHANNEL_MAPPING, default_ticket
22+
from . import config
23+
24+
# FIXME
25+
# try for empty responses.
26+
# await interaction.response.defer()
27+
2228

2329

2430
log = logging.getLogger(__name__)
2531

32+
# cached data for fast option-choices popup menus
33+
_programs: list[OptionChoice] = [
34+
OptionChoice("Community Networks (General)", value=15),
35+
OptionChoice("Grant: Seattle TMF 2023-4", value=16),
36+
OptionChoice("Grant: Benton Fellowship 2024", value=17),
37+
]
38+
#_bot = None
2639

2740
def setup(bot:NetBot):
41+
#_programs = get_program_options()
42+
#log.warning(f"initilized programs: {_programs}")
2843
bot.add_cog(TicketsCog(bot))
44+
2945
log.info("initialized tickets cog")
3046

3147

48+
def get_program_options() -> list[OptionChoice]:
49+
programs = []
50+
for k, v in config.programs.items():
51+
programs.append(OptionChoice(k, value=v))
52+
return programs
53+
54+
3255
def get_trackers(ctx: discord.AutocompleteContext):
3356
"""Returns a list of trackers that begin with the characters entered so far."""
3457
trackers = ctx.bot.redmine.ticket_mgr.get_trackers() # this is expected to be cached
@@ -115,6 +138,34 @@ async def callback(self, interaction: discord.Interaction):
115138
)
116139

117140

141+
class ProgramSelect(discord.ui.Select):
142+
"""Popup menu to select ticket tracker"""
143+
def __init__(self, bot: discord.Bot):
144+
self.bot = bot
145+
146+
# Get the possible trackers
147+
options = []
148+
for name, value in self.bot.redmine.ticket_mgr.get_programs().items():
149+
options.append(discord.SelectOption(label=name))
150+
151+
# The placeholder is what will be shown when no option is selected.
152+
# The min and max values indicate we can only pick one of the three options.
153+
# The options parameter, contents shown above, define the dropdown options.
154+
super().__init__(
155+
placeholder="Select program...",
156+
min_values=1,
157+
max_values=1,
158+
options=options,
159+
)
160+
161+
async def callback(self, interaction: discord.Interaction):
162+
# Use the interaction object to send a response message containing
163+
# the user's favourite colour or choice. The self object refers to the
164+
# Select object, and the values attribute gets a list of the user's
165+
# selected options. We only want the first one.
166+
log.info(f"PROGRAM SELECT {interaction.user} {interaction.data}")
167+
await interaction.response.send_message(f"ProgramSelect.callback() - selected program {self.values[0]}")
168+
118169
class StatusSelect(discord.ui.Select):
119170
"""Popup menu to select ticket status"""
120171
def __init__(self, bot: discord.Bot):
@@ -808,9 +859,12 @@ async def help(self, ctx: discord.ApplicationContext):
808859
await ctx.respond(embed=self.bot.formatter.help_embed(ctx))
809860

810861

811-
@discord.slash_command(name="testime", description="Record time against a program")
812-
async def recordTime(self, ctx: discord.ApplicationContext, hours: float, program: str, note: str = ""):
813-
log.info(f">>> {hours} {program} {note}")
862+
@discord.slash_command(name="record_time", description="Record time against a program")
863+
@option("hours", description="Hours worked on the program: `3.5`")
864+
@option("program_id", choices=get_program_options(), description="Select the program or grant", required=True)
865+
@option("note", description="Optional: Additional comments")
866+
async def recordTime(self, ctx: discord.ApplicationContext, hours: float, program_id: int, note: str = ""):
867+
log.info(f">>> {hours} {program_id} {note}")
814868
redmine: Client = ctx.bot.redmine
815869

816870
user = redmine.user_mgr.find(ctx.user.name)
@@ -827,6 +881,8 @@ async def recordTime(self, ctx: discord.ApplicationContext, hours: float, progra
827881
await ticket_cog.create_new_ticket(ctx, note)
828882
autoresolve = True
829883

830-
redmine.ticket_mgr.record_time(ticket_id, user, hours, program, note)
884+
redmine.ticket_mgr.record_time(ticket_id, user, hours, program_id, note)
831885
if autoresolve:
832886
redmine.ticket_mgr.resolve(ticket_id)
887+
888+
await ctx.respond(f"Recorded {hours} on {program_id} for {user.discord}")

netbot/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
# programs - list of active programs {name:program_id}
3+
programs: dict[str,int] = {}

netbot/netbot.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from redmine.redmine import Client
1717

1818
from .formatting import DiscordFormatter
19-
19+
from . import config
2020

2121
log = logging.getLogger(__name__)
2222

@@ -49,6 +49,7 @@
4949

5050
FALLBACK_TEAM = "intake-team"
5151

52+
PROG_CACHE = None
5253

5354
# utility method to get a list of (one) ticket from the title of the channel, or empty list
5455
def default_ticket(ctx: discord.AutocompleteContext) -> list[int]:
@@ -63,7 +64,6 @@ def default_ticket(ctx: discord.AutocompleteContext) -> list[int]:
6364
class NetbotException(Exception):
6465
"""netbot exception"""
6566

66-
6767
class NetBot(commands.Bot):
6868
"""netbot"""
6969
def __init__(self, client: Client):
@@ -82,6 +82,8 @@ def __init__(self, client: Client):
8282
self.formatter = DiscordFormatter(client.url)
8383

8484
self.redmine = client
85+
config.programs = client.ticket_mgr.get_programs()
86+
8587
#guilds = os.getenv('DISCORD_GUILDS').split(', ')
8688
#if guilds:
8789
# log.info(f"setting guilds: {guilds}")
@@ -116,6 +118,7 @@ def run_bot(self):
116118

117119
# log.debug(f"Loaded teams: {self.teams}")
118120

121+
119122
def sync_team(self, role:discord.Role, team:Team):
120123
team_ids = set()
121124
for member in role.members:
@@ -646,6 +649,7 @@ def setup_logging(log_level = logging.INFO):
646649

647650
log.debug("log level set to debug")
648651

652+
649653
if __name__ == '__main__':
650654
setup_logging()
651655
main()

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[project]
22
name = "netbot"
3-
version = "0.1.0"
4-
description = "Add your description here"
3+
version = "0.5.0"
4+
description = "A Discord bot to capture tickets and todos."
55
readme = "README.md"
66
requires-python = ">=3.13"
77
dependencies = [

redmine/tickets.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,12 @@ def load_programs(self) -> dict[str,int]:
9393
return programs
9494

9595

96+
def get_programs(self) -> dict[str,int]:
97+
if len(self.programs) == 0:
98+
self.programs = self.load_programs()
99+
return self.programs
100+
101+
96102
def get_program(self, program_name: str) -> int:
97103
return self.programs.get(program_name, self.default_program)
98104

@@ -624,7 +630,7 @@ def resolve(self, ticket_id, user_id=None):
624630
return self.update(ticket_id, {"status_id": "3"}, user_id) # '3' is the status_id, it doesn't accept "Resolved"
625631

626632

627-
def record_time(self, ticket_id:int, user:User, hours:float, program:str, note:str) -> TimeEntry:
633+
def record_time(self, ticket_id:int, user:User, hours:float, program_id:int, note:str) -> TimeEntry:
628634
# time_entry (required): a hash of the time entry attributes, including:
629635
# issue_id or project_id (only one is required): the issue id or project id to log time on (both are integers); note that project ids can only be found using the API (e.g. at /projects.json)
630636
# spent_on: the date the time was spent (default to the current date); format is e.g. 2020-12-24
@@ -637,7 +643,7 @@ def record_time(self, ticket_id:int, user:User, hours:float, program:str, note:s
637643
'issue_id': ticket_id,
638644
# 'spent_on': "2020-12-24", # (default to the current date)
639645
'hours': hours,
640-
'activity_id': self.get_program(program),
646+
'activity_id': program_id, #self.get_program(program),
641647
'comments': note,
642648
'user_id': user.id,
643649
}

tests/test_cog_tickets.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -593,9 +593,10 @@ async def test_record_time(self):
593593
ctx.channel.name = f"Ticket #{ticket.id}"
594594
ctx.channel.id = ticket.id
595595

596-
activity = "Development"
596+
activity = "Community Networks (General)"
597+
activity_id = self.tickets_mgr.get_program(activity)
597598
notes = "this is a test"
598-
await self.cog.recordTime(ctx, 2.33, activity, notes)
599+
await self.cog.recordTime(ctx, 2.33, activity_id, notes)
599600

600601
records = self.redmine.ticket_mgr.get_time_records(issue_id=ticket.id)
601602
self.assertIsNotNone(records)

tests/test_utils.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from redmine.tickets import SCN_PROJECT_ID
2020
from redmine.session import RedmineSession
2121
from redmine.redmine import Client
22-
from netbot.netbot import NetBot, setup_logging
22+
from netbot.netbot import NetBot, setup_logging, config
2323
from tests.mock_session import MockSession
2424

2525

@@ -233,6 +233,7 @@ class RedmineTestCase(unittest.TestCase):
233233
def setUpClass(cls):
234234
sess = RedmineSession.fromenv()
235235
cls.redmine = Client.from_session(sess, SCN_PROJECT_ID)
236+
config.programs = cls.redmine.ticket_mgr.get_programs()
236237
cls.user_mgr = cls.redmine.user_mgr
237238
cls.tickets_mgr = cls.redmine.ticket_mgr
238239
init_test_users(cls.user_mgr)

0 commit comments

Comments
 (0)