Skip to content
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ wheels/
.env

# Local development
data/
data/

# AI nonsense
.commandcode/taste/
26 changes: 20 additions & 6 deletions nephthys/events/app_home_opened.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging
import traceback
from dataclasses import dataclass
from typing import Any

from prometheus_client import Histogram
Expand All @@ -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


Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand All @@ -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]

Expand Down
34 changes: 24 additions & 10 deletions nephthys/scripts/add_dummy_data.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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),
)
Expand All @@ -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 <num_records>")
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))
11 changes: 11 additions & 0 deletions nephthys/utils/slack.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
import re
from typing import Any
from typing import Dict

Expand Down Expand Up @@ -111,6 +112,16 @@ async def manage_home_switcher(ack: AsyncAck, body, client: AsyncWebClient):
app.action(view.id)(manage_home_switcher)


@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]["action_id"].rsplit("-", 1)[-1])
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)
Expand Down
96 changes: 58 additions & 38 deletions nephthys/views/home/assigned.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
import pytz
from blockkit import Actions
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
from nephthys.database.tables import User
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

# 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

async def get_assigned_tickets_view(user: User | None):
header = get_header(user, AppHomeView.ASSIGNED_TICKETS)

def pagination_buttons(page: int, total_pages: int) -> Actions:
buttons = Actions()
for p in range(1, total_pages + 1):
buttons.add_element(
Button(
text=f"Page {p}",
action_id=f"assigned-tickets-page-{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:
return error_screen(
Expand All @@ -19,17 +42,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(
(Ticket.assigned_to == user.id) & (Ticket.status != TicketStatus.CLOSED)
total = await Ticket.count().where(
(Ticket.assigned_to == user) & (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) & (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())
Expand All @@ -38,39 +72,25 @@ 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())

if total_pages > 1:
ticket_blocks.append(pagination_buttons(page, total_pages))

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()
35 changes: 13 additions & 22 deletions nephthys/views/home/components/error_screen.py
Original file line number Diff line number Diff line change
@@ -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()
Loading