From d42fc5929af5726b88125dc1dab0705003c63191 Mon Sep 17 00:00:00 2001 From: MMK21Hub <50421330+MMK21Hub@users.noreply.github.com> Date: Wed, 8 Jul 2026 17:44:57 +0100 Subject: [PATCH 1/4] Migrate views/home/assigned.py to blockkit Testing out https://commandcode.ai/ with this one --- .gitignore | 5 +- nephthys/views/home/assigned.py | 56 ++++++++----------- .../views/home/components/error_screen.py | 35 +++++------- 3 files changed, 39 insertions(+), 57 deletions(-) diff --git a/.gitignore b/.gitignore index d20ab484..cbc9cb1e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ wheels/ .env # Local development -data/ \ No newline at end of file +data/ + +# AI nonsense +.commandcode/taste/ diff --git a/nephthys/views/home/assigned.py b/nephthys/views/home/assigned.py index 841b2b31..c4d3287e 100644 --- a/nephthys/views/home/assigned.py +++ b/nephthys/views/home/assigned.py @@ -1,4 +1,9 @@ import pytz +from blockkit import Button +from blockkit import Divider +from blockkit import Header +from blockkit import Home +from blockkit import Section from nephthys.database.enums import TicketStatus from nephthys.database.tables import Ticket @@ -6,11 +11,11 @@ from nephthys.utils.ticket_methods import get_question_message_link from nephthys.views.home import AppHomeView from nephthys.views.home.components.error_screen import error_screen -from nephthys.views.home.components.header import get_header +from nephthys.views.home.components.header import get_header_components async def get_assigned_tickets_view(user: User | None): - header = get_header(user, AppHomeView.ASSIGNED_TICKETS) + header = get_header_components(user, AppHomeView.ASSIGNED_TICKETS) if not user or not user.helper: return error_screen( @@ -38,39 +43,22 @@ async def get_assigned_tickets_view(user: User | None): f"<@{ticket.opened_by.slack_id}>" if ticket.opened_by else "unknown user" ) ticket_blocks.append( - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": f"*{ticket.title}*\n _from {opened_by_str}. {time_ago_str}_", - }, - "accessory": { - "type": "button", - "text": { - "type": "plain_text", - "text": ":rac_info: view ticket", - "emoji": True, - }, - "action_id": f"view-ticket-{ticket.msg_ts}", - "url": get_question_message_link(ticket), - "value": ticket.msg_ts, - }, - } + Section( + text=f"*{ticket.title}*\n _from {opened_by_str}. {time_ago_str}_", + accessory=Button( + text=":rac_info: view ticket", + action_id=f"view-ticket-{ticket.msg_ts}", + url=get_question_message_link(ticket), + value=ticket.msg_ts, + ), + ) ) - ticket_blocks.append({"type": "divider"}) + ticket_blocks.append(Divider()) - return { - "type": "home", - "blocks": [ + return Home( + [ *header, - { - "type": "header", - "text": { - "type": "plain_text", - "text": ":rac_cute: here are your assigned tickets <3", - "emoji": True, - }, - }, + Header(":rac_cute: here are your assigned tickets <3"), *ticket_blocks, - ], - } + ] + ).build() diff --git a/nephthys/views/home/components/error_screen.py b/nephthys/views/home/components/error_screen.py index fc1e5c4d..47b705a9 100644 --- a/nephthys/views/home/components/error_screen.py +++ b/nephthys/views/home/components/error_screen.py @@ -1,24 +1,15 @@ -def error_screen(header: list[dict], title: str, message: str) -> dict: +from blockkit import Header +from blockkit import Home +from blockkit import Section +from blockkit.core import ModalBlock + + +def error_screen(header: list[ModalBlock], title: str, message: str) -> dict: """A basic error screen that can be rendered as a view if permissions are missing, or something like that""" - return { - "type": "home", - "blocks": [ + return Home( + [ *header, - { - "type": "header", - "text": { - "type": "plain_text", - "text": title, - "emoji": True, - }, - }, - { - "type": "section", - "text": { - "type": "plain_text", - "text": message, - "emoji": True, - }, - }, - ], - } + Header(title), + Section(message), + ] + ).build() From e6311c87da34fd3a4a9541570e96ddac4c87f3f1 Mon Sep 17 00:00:00 2001 From: MMK21Hub <50421330+MMK21Hub@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:58:14 +0100 Subject: [PATCH 2/4] Add pagination to assigned tickets view --- nephthys/events/app_home_opened.py | 26 +++++++++++++++----- nephthys/utils/slack.py | 10 ++++++++ nephthys/views/home/assigned.py | 39 +++++++++++++++++++++++++++--- 3 files changed, 66 insertions(+), 9 deletions(-) diff --git a/nephthys/events/app_home_opened.py b/nephthys/events/app_home_opened.py index c89041eb..376fb82f 100644 --- a/nephthys/events/app_home_opened.py +++ b/nephthys/events/app_home_opened.py @@ -1,5 +1,6 @@ import logging import traceback +from dataclasses import dataclass from typing import Any from prometheus_client import Histogram @@ -20,6 +21,12 @@ from nephthys.views.home.team_tags import get_team_tags_view +@dataclass +class RequestedView: + home_type: AppHomeView + page: int | None = None + + DEFAULT_VIEW = AppHomeView.DASHBOARD @@ -53,11 +60,16 @@ async def on_app_home_opened(event: dict[str, Any], client: AsyncWebClient): # Map of the last-requested view for each Slack user # This prevents a view that took a while to render overwriting the view you want # Entries are deleted once the view is published -last_requested_views: dict[str, AppHomeView] = {} +last_requested_views: dict[str, RequestedView] = {} -async def open_app_home(home_type: AppHomeView, client: AsyncWebClient, user_id: str): - last_requested_views[user_id] = home_type +async def open_app_home( + home_type: AppHomeView, + client: AsyncWebClient, + user_id: str, + page: int | None = None, +): + last_requested_views[user_id] = RequestedView(home_type, page) try: await client.views_publish(view=get_loading_view(home_type), user_id=user_id) @@ -73,7 +85,7 @@ async def open_app_home(home_type: AppHomeView, client: AsyncWebClient, user_id: case AppHomeView.DASHBOARD: view = await get_dashboard_view(slack_user=user_id, db_user=user) case AppHomeView.ASSIGNED_TICKETS: - view = await get_assigned_tickets_view(user) + view = await get_assigned_tickets_view(user, page=page or 1) case AppHomeView.TEAM_TAGS: view = await get_team_tags_view(user) case AppHomeView.CATEGORY_TAGS: @@ -84,8 +96,10 @@ async def open_app_home(home_type: AppHomeView, client: AsyncWebClient, user_id: # Check that the request hasn't been superseded by another request while we were rendering user_last_requested_view = last_requested_views.get(user_id) if user_last_requested_view: - if user_last_requested_view != home_type: - logging.info(f"Ignoring stale view request ({user_id}, {home_type})") + if user_last_requested_view != RequestedView(home_type, page): + logging.info( + f"Ignoring stale view request slack_id={user_id} view={home_type} page={page}" + ) return del last_requested_views[user_id] diff --git a/nephthys/utils/slack.py b/nephthys/utils/slack.py index 9166f062..62799dbd 100644 --- a/nephthys/utils/slack.py +++ b/nephthys/utils/slack.py @@ -111,6 +111,16 @@ async def manage_home_switcher(ack: AsyncAck, body, client: AsyncWebClient): app.action(view.id)(manage_home_switcher) +@app.action("assigned-tickets-page") +async def handle_assigned_tickets_page( + ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient +): + await ack() + user_id = body["user"]["id"] + page = int(body["actions"][0]["value"]) + await open_app_home(AppHomeView.ASSIGNED_TICKETS, client, user_id, page=page) + + @app.event("member_joined_channel") async def handle_member_joined_channel(event: Dict[str, Any], client: AsyncWebClient): await channel_join(ack=AsyncAck(), event=event, client=client) diff --git a/nephthys/views/home/assigned.py b/nephthys/views/home/assigned.py index c4d3287e..4755df6b 100644 --- a/nephthys/views/home/assigned.py +++ b/nephthys/views/home/assigned.py @@ -1,4 +1,5 @@ import pytz +from blockkit import Actions from blockkit import Button from blockkit import Divider from blockkit import Header @@ -13,8 +14,26 @@ from nephthys.views.home.components.error_screen import error_screen from nephthys.views.home.components.header import get_header_components +# Each ticket needs 2 blocks, and the heading takes up 3 blocks. +# Maximum blocks per view is 100. +TICKETS_PER_PAGE = 48 -async def get_assigned_tickets_view(user: User | None): + +def pagination_buttons(page: int, total_pages: int) -> Actions: + buttons = Actions() + for p in range(1, total_pages + 1): + buttons.add_element( + Button( + text=str(p), + action_id="assigned-tickets-page", + value=str(p), + style=Button.PRIMARY if p == page else None, + ) + ) + return buttons + + +async def get_assigned_tickets_view(user: User | None, page: int = 1): header = get_header_components(user, AppHomeView.ASSIGNED_TICKETS) if not user or not user.helper: @@ -24,17 +43,28 @@ async def get_assigned_tickets_view(user: User | None): ":rac_believes_in_theory_about_green_lizards_and_space_lasers: only helpers can be assigned to tickets, so you have none - zero responsibility!", ) - tickets = await Ticket.objects(Ticket.opened_by).where( + total = await Ticket.count().where( (Ticket.assigned_to == user.id) & (Ticket.status != TicketStatus.CLOSED) ) - if not tickets: + if total == 0: return error_screen( header, ":rac_cute: no assigned tickets", ":rac_believes_in_theory_about_green_lizards_and_space_lasers: you don't have any assigned tickets right now!", ) + total_pages = (total + TICKETS_PER_PAGE - 1) // TICKETS_PER_PAGE + page = max(1, min(page, total_pages)) + + tickets = ( + await Ticket.objects(Ticket.opened_by) + .where((Ticket.assigned_to == user.id) & (Ticket.status != TicketStatus.CLOSED)) + .order_by(Ticket.created_at, ascending=True) + .limit(TICKETS_PER_PAGE) + .offset((page - 1) * TICKETS_PER_PAGE) + ) + ticket_blocks = [] for ticket in tickets: unix_ts = int(ticket.created_at.timestamp()) @@ -55,6 +85,9 @@ async def get_assigned_tickets_view(user: User | None): ) ticket_blocks.append(Divider()) + if total_pages > 1: + ticket_blocks.append(pagination_buttons(page, total_pages)) + return Home( [ *header, From 4155b89f5c1209601976921d2df4054e28b7f8f2 Mon Sep 17 00:00:00 2001 From: MMK21Hub <50421330+MMK21Hub@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:23:08 +0100 Subject: [PATCH 3/4] Update the dummy data script --- nephthys/scripts/add_dummy_data.py | 34 +++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/nephthys/scripts/add_dummy_data.py b/nephthys/scripts/add_dummy_data.py index 31038057..6bef17ad 100644 --- a/nephthys/scripts/add_dummy_data.py +++ b/nephthys/scripts/add_dummy_data.py @@ -1,16 +1,25 @@ +import argparse import asyncio import logging from datetime import datetime from datetime import timedelta from random import randint -from sys import argv from nephthys.database.enums import TicketStatus from nephthys.database.tables import Ticket +from nephthys.database.tables import User from nephthys.utils.logging import send_heartbeat -async def create_dummy_records(num_records: int): +async def get_default_user() -> User: + user = await User.objects().first() + if not user: + raise RuntimeError("No users found in database") + return user + + +async def create_dummy_records(num_records: int, assigned_to: int | None = None): + default_user = await get_default_user() # We generate random timestamps from up to 2 weeks ago timestamp_a = datetime.now() - timedelta(weeks=2) timestamp_b = datetime.now() @@ -22,10 +31,10 @@ async def create_dummy_records(num_records: int): description=f"Test ticket number {i}", msg_ts=f"{timestamp}.{i}", ticket_ts=f"{timestamp + 2}.{i}", - opened_by=1, + opened_by=default_user, status=TicketStatus.CLOSED, - closed_by=1, - assigned_to=1, + closed_by=default_user, + assigned_to=assigned_to or default_user, assigned_at=datetime.fromtimestamp(timestamp + 10), closed_at=datetime.fromtimestamp(timestamp + 110), ) @@ -35,8 +44,13 @@ async def create_dummy_records(num_records: int): if __name__ == "__main__": - if len(argv) < 2: - print("Usage: python add_dummy_data.py ") - exit(1) - num_records = int(argv[1]) - asyncio.run(create_dummy_records(num_records)) + parser = argparse.ArgumentParser() + parser.add_argument( + "--count", type=int, required=True, help="number of tickets to create" + ) + parser.add_argument( + "--assigned-to", type=int, help="assign all tickets to this user id" + ) + args = parser.parse_args() + + asyncio.run(create_dummy_records(args.count, args.assigned_to)) From 92e7d96f03aea30929992ec219165c89f9a86390 Mon Sep 17 00:00:00 2001 From: MMK21Hub <50421330+MMK21Hub@users.noreply.github.com> Date: Wed, 8 Jul 2026 20:23:30 +0100 Subject: [PATCH 4/4] Fix some blockkit errors --- nephthys/utils/slack.py | 5 +++-- nephthys/views/home/assigned.py | 15 +++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/nephthys/utils/slack.py b/nephthys/utils/slack.py index 62799dbd..6aec8f89 100644 --- a/nephthys/utils/slack.py +++ b/nephthys/utils/slack.py @@ -1,4 +1,5 @@ import logging +import re from typing import Any from typing import Dict @@ -111,13 +112,13 @@ async def manage_home_switcher(ack: AsyncAck, body, client: AsyncWebClient): app.action(view.id)(manage_home_switcher) -@app.action("assigned-tickets-page") +@app.action(re.compile(r"assigned-tickets-page-\d+")) async def handle_assigned_tickets_page( ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient ): await ack() user_id = body["user"]["id"] - page = int(body["actions"][0]["value"]) + page = int(body["actions"][0]["action_id"].rsplit("-", 1)[-1]) await open_app_home(AppHomeView.ASSIGNED_TICKETS, client, user_id, page=page) diff --git a/nephthys/views/home/assigned.py b/nephthys/views/home/assigned.py index 4755df6b..fb1b9222 100644 --- a/nephthys/views/home/assigned.py +++ b/nephthys/views/home/assigned.py @@ -14,9 +14,9 @@ from nephthys.views.home.components.error_screen import error_screen from nephthys.views.home.components.header import get_header_components -# Each ticket needs 2 blocks, and the heading takes up 3 blocks. -# Maximum blocks per view is 100. -TICKETS_PER_PAGE = 48 +# Each ticket needs 2 blocks, and the heading takes up 5 blocks (I think) +# Maximum Slack blocks per view is 100. +TICKETS_PER_PAGE = 47 def pagination_buttons(page: int, total_pages: int) -> Actions: @@ -24,9 +24,8 @@ def pagination_buttons(page: int, total_pages: int) -> Actions: for p in range(1, total_pages + 1): buttons.add_element( Button( - text=str(p), - action_id="assigned-tickets-page", - value=str(p), + text=f"Page {p}", + action_id=f"assigned-tickets-page-{p}", style=Button.PRIMARY if p == page else None, ) ) @@ -44,7 +43,7 @@ async def get_assigned_tickets_view(user: User | None, page: int = 1): ) total = await Ticket.count().where( - (Ticket.assigned_to == user.id) & (Ticket.status != TicketStatus.CLOSED) + (Ticket.assigned_to == user) & (Ticket.status != TicketStatus.CLOSED) ) if total == 0: @@ -59,7 +58,7 @@ async def get_assigned_tickets_view(user: User | None, page: int = 1): tickets = ( await Ticket.objects(Ticket.opened_by) - .where((Ticket.assigned_to == user.id) & (Ticket.status != TicketStatus.CLOSED)) + .where((Ticket.assigned_to == user) & (Ticket.status != TicketStatus.CLOSED)) .order_by(Ticket.created_at, ascending=True) .limit(TICKETS_PER_PAGE) .offset((page - 1) * TICKETS_PER_PAGE)