mirror of
https://github.com/System-End/nephthys.git
synced 2026-04-19 16:28:16 +00:00
First iteration of "question tags" (#136)
* Rename tags to team tags * Add a TagType enum with TEAM and QUESTION options * Ticket#questionTag exists now * Revert "Add a TagType enum with TEAM and QUESTION options" This reverts commit c9a17f4003aa2ce470f82810ac031fe83325f3d6. * Add a new QuestionTag model * Add another dropdown to the backend msg * create tag => create team tag * Add creating tags (from the backend channel) * Create a single function for sending the backend message Previously this code was duplicated across 2 places, making it inconsistent * Rename a bunch of files; implement upating question tags on tickets * Add a log! * Fix ticket tag dropdowns in old msgs * Auto-select the current question tag when a backend msg is posted * Update backend message with the current tag * Allow clearing the question tag * Dynamically get question tag list * Document question tags * typo * Change some log msgs to debug * Only make the DB call if current_question_tag_id is present * Remove unused variable * Add createdAt to QuestionTag * Ensure "reopened by [user]" is preserved when backend message is edited
This commit is contained in:
parent
983f895737
commit
b156145811
18 changed files with 498 additions and 140 deletions
12
README.md
12
README.md
|
|
@ -4,9 +4,17 @@ Nephthys is the bot powering #summer-of-making-help and #identity-help in the Ha
|
|||
|
||||
## Features
|
||||
|
||||
### Tags
|
||||
### Question tags
|
||||
|
||||
You can tag tickets in the private tickets channel or with the macro `?tag <tag_name>`. This will DM the people who are specialised in responding to those issues and have it show up in their assigned tickets.
|
||||
Quite often in help channels, the same question gets asked again and again. Helpers can associate tickets with a "question tag", which are pre-defined questions/issue such as "What's the payout range?", "How to top up grant?", or "404 on /shop". We can then keep track of how these questions trend over time, to provide a feedback loop for people building the event/platform/YSWS.
|
||||
|
||||
They can be added to tickets in the private tickets channel.
|
||||
|
||||
### Team tags
|
||||
|
||||
Team tags let you tag tickets that are the responsibility of a specific group of people (or perhaps just one person). E.g. you could have tags for Fufillment, Hack Club Auth, Onboarding flow, etc.
|
||||
|
||||
You can add team tags to tickets in the private tickets channel or with the macro `?tag <tag_name>`. This will DM the people who are specialised in responding to those issues and have it show up in their assigned tickets.
|
||||
You can assign yourself to get notified for specific tags on the app home
|
||||
|
||||
### Macros
|
||||
|
|
|
|||
88
nephthys/actions/assign_question_tag.py
Normal file
88
nephthys/actions/assign_question_tag.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import logging
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
|
||||
from slack_bolt.async_app import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.events.message.send_backend_message import backend_message_blocks
|
||||
from nephthys.events.message.send_backend_message import backend_message_fallback_text
|
||||
from nephthys.utils.env import env
|
||||
|
||||
|
||||
async def assign_question_tag_callback(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
selected = body["actions"][0]["selected_option"]
|
||||
selected_value = selected["value"] if selected else None
|
||||
if selected_value and selected_value.lower() == "none":
|
||||
return
|
||||
try:
|
||||
tag_id = int(selected_value) if selected_value else None
|
||||
except ValueError as e:
|
||||
raise ValueError(f"Invalid tag ID: {selected_value}") from e
|
||||
|
||||
channel_id = body["channel"]["id"]
|
||||
ts = body["message"]["ts"]
|
||||
|
||||
user = await env.db.user.find_unique(where={"slackId": user_id})
|
||||
if not user or not user.helper:
|
||||
logging.warning(
|
||||
f"Unauthorized user attempted to assign question tag user_id={user_id}"
|
||||
)
|
||||
await client.chat_postEphemeral(
|
||||
channel=channel_id,
|
||||
user=user_id,
|
||||
text="You are not authorized to assign tags.",
|
||||
)
|
||||
return
|
||||
|
||||
ticket = await env.db.ticket.update(
|
||||
where={"ticketTs": ts},
|
||||
data={
|
||||
"questionTag": (
|
||||
{"connect": {"id": tag_id}}
|
||||
if tag_id is not None
|
||||
else {"disconnect": True}
|
||||
)
|
||||
},
|
||||
include={"openedBy": True, "reopenedBy": True},
|
||||
)
|
||||
if not ticket:
|
||||
logging.error(
|
||||
f"Failed to find corresponding ticket to update question tag ticket_ts={ts}"
|
||||
)
|
||||
return
|
||||
|
||||
other_tickets = await env.db.ticket.count(
|
||||
where={
|
||||
"openedById": ticket.openedById,
|
||||
"id": {"not": ticket.id},
|
||||
}
|
||||
)
|
||||
if not ticket.openedBy:
|
||||
logging.error(f"Cannot find who opened ticket ticket_id={ticket.id}")
|
||||
return
|
||||
# Update the backend message so it has the new tag selected
|
||||
await client.chat_update(
|
||||
channel=channel_id,
|
||||
ts=ts,
|
||||
text=backend_message_fallback_text(
|
||||
author_user_id=ticket.openedBy.slackId,
|
||||
description=ticket.description,
|
||||
reopened_by=ticket.reopenedBy,
|
||||
),
|
||||
blocks=await backend_message_blocks(
|
||||
author_user_id=ticket.openedBy.slackId,
|
||||
msg_ts=ticket.msgTs,
|
||||
past_tickets=other_tickets,
|
||||
current_question_tag_id=tag_id,
|
||||
reopened_by=ticket.reopenedBy,
|
||||
),
|
||||
)
|
||||
|
||||
logging.info(
|
||||
f"Updated question tag on ticket ticket_id={ticket.id} tag_id={tag_id}"
|
||||
)
|
||||
|
|
@ -11,7 +11,7 @@ from nephthys.utils.ticket_methods import get_backend_message_link
|
|||
from nephthys.utils.ticket_methods import get_question_message_link
|
||||
|
||||
|
||||
async def assign_tag_callback(
|
||||
async def assign_team_tag_callback(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await ack()
|
||||
49
nephthys/actions/create_question_tag.py
Normal file
49
nephthys/actions/create_question_tag.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from slack_bolt.async_app import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
from nephthys.views.modals.create_question_tag import get_create_question_tag_modal
|
||||
|
||||
|
||||
async def create_question_tag_view_callback(
|
||||
ack: AsyncAck, body: dict, client: AsyncWebClient
|
||||
):
|
||||
"""
|
||||
Callback for the create question tag view submission
|
||||
"""
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
|
||||
user = await env.db.user.find_unique(where={"slackId": user_id})
|
||||
if not user or not user.helper:
|
||||
await send_heartbeat(
|
||||
f"Attempted to create question tag by non-helper <@{user_id}>"
|
||||
)
|
||||
return
|
||||
|
||||
label = body["view"]["state"]["values"]["tag_label"]["tag_label"]["value"]
|
||||
await env.db.questiontag.create(data={"label": label})
|
||||
|
||||
# await open_app_home("question-tags", client, user_id)
|
||||
|
||||
|
||||
async def create_question_tag_btn_callback(
|
||||
ack: AsyncAck, body: dict, client: AsyncWebClient
|
||||
):
|
||||
"""
|
||||
Open modal to create a question tag
|
||||
"""
|
||||
await ack()
|
||||
user_id = body["user"]["id"]
|
||||
trigger_id = body["trigger_id"]
|
||||
|
||||
user = await env.db.user.find_unique(where={"slackId": user_id})
|
||||
if not user or not user.helper:
|
||||
await send_heartbeat(
|
||||
f"Attempted to open create-question-tag modal by non-helper <@{user_id}>"
|
||||
)
|
||||
return
|
||||
|
||||
view = get_create_question_tag_modal()
|
||||
await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id)
|
||||
|
|
@ -4,10 +4,12 @@ from slack_sdk.web.async_client import AsyncWebClient
|
|||
from nephthys.events.app_home_opened import open_app_home
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
from nephthys.views.modals.create_tag import get_create_tag_modal
|
||||
from nephthys.views.modals.create_team_tag import get_create_team_tag_modal
|
||||
|
||||
|
||||
async def create_tag_view_callback(ack: AsyncAck, body: dict, client: AsyncWebClient):
|
||||
async def create_team_tag_view_callback(
|
||||
ack: AsyncAck, body: dict, client: AsyncWebClient
|
||||
):
|
||||
"""
|
||||
Callback for the create tag view submission
|
||||
"""
|
||||
|
|
@ -22,10 +24,12 @@ async def create_tag_view_callback(ack: AsyncAck, body: dict, client: AsyncWebCl
|
|||
name = body["view"]["state"]["values"]["tag_name"]["tag_name"]["value"]
|
||||
await env.db.tag.create(data={"name": name})
|
||||
|
||||
await open_app_home("tags", client, user_id)
|
||||
await open_app_home("team-tags", client, user_id)
|
||||
|
||||
|
||||
async def create_tag_btn_callback(ack: AsyncAck, body: dict, client: AsyncWebClient):
|
||||
async def create_team_tag_btn_callback(
|
||||
ack: AsyncAck, body: dict, client: AsyncWebClient
|
||||
):
|
||||
"""
|
||||
Open modal to create a tag
|
||||
"""
|
||||
|
|
@ -40,5 +44,5 @@ async def create_tag_btn_callback(ack: AsyncAck, body: dict, client: AsyncWebCli
|
|||
)
|
||||
return
|
||||
|
||||
view = get_create_tag_modal()
|
||||
view = get_create_team_tag_modal()
|
||||
await client.views_open(trigger_id=trigger_id, view=view, user_id=user_id)
|
||||
|
|
@ -41,4 +41,4 @@ async def tag_subscribe_callback(
|
|||
}
|
||||
)
|
||||
|
||||
await open_app_home("tags", client, slack_id)
|
||||
await open_app_home("team-tags", client, slack_id)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from nephthys.views.home.error import get_error_view
|
|||
from nephthys.views.home.helper import get_helper_view
|
||||
from nephthys.views.home.loading import get_loading_view
|
||||
from nephthys.views.home.stats import get_stats_view
|
||||
from nephthys.views.home.tags import get_manage_tags_view
|
||||
from nephthys.views.home.team_tags import get_team_tags_view
|
||||
|
||||
DEFAULT_VIEW = "dashboard"
|
||||
|
||||
|
|
@ -54,8 +54,8 @@ async def open_app_home(home_type: str, client: AsyncWebClient, user_id: str):
|
|||
view = await get_helper_view(slack_user=user_id, db_user=user)
|
||||
case "assigned-tickets":
|
||||
view = await get_assigned_tickets_view(user)
|
||||
case "tags":
|
||||
view = await get_manage_tags_view(user)
|
||||
case "team-tags":
|
||||
view = await get_team_tags_view(user)
|
||||
case "my-stats":
|
||||
view = await get_stats_view(user)
|
||||
case _:
|
||||
|
|
|
|||
128
nephthys/events/message/send_backend_message.py
Normal file
128
nephthys/events/message/send_backend_message.py
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.utils.env import env
|
||||
from prisma.models import User
|
||||
|
||||
|
||||
async def backend_message_blocks(
|
||||
author_user_id: str,
|
||||
msg_ts: str,
|
||||
past_tickets: int,
|
||||
current_question_tag_id: int | None = None,
|
||||
reopened_by: User | None = None,
|
||||
) -> list[dict]:
|
||||
thread_url = f"https://hackclub.slack.com/archives/{env.slack_help_channel}/p{msg_ts.replace('.', '')}"
|
||||
if current_question_tag_id is not None:
|
||||
options = [
|
||||
{
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": tag.label,
|
||||
},
|
||||
"value": f"{tag.id}",
|
||||
}
|
||||
for tag in await env.db.questiontag.find_many()
|
||||
]
|
||||
initial_option = [
|
||||
option
|
||||
for option in options
|
||||
if option["value"] == f"{current_question_tag_id}"
|
||||
][0]
|
||||
else:
|
||||
initial_option = None
|
||||
question_tags_dropdown = {
|
||||
"type": "input",
|
||||
"label": {"type": "plain_text", "text": "Question tag", "emoji": True},
|
||||
"element": {
|
||||
"type": "external_select",
|
||||
"action_id": "question-tag-list",
|
||||
"placeholder": {
|
||||
"type": "plain_text",
|
||||
"text": "Which question tag fits?",
|
||||
},
|
||||
"min_query_length": 0,
|
||||
},
|
||||
}
|
||||
if initial_option:
|
||||
question_tags_dropdown["element"]["initial_option"] = initial_option
|
||||
|
||||
return [
|
||||
question_tags_dropdown,
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": "If none of the existing tags fit :point_right:",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":wrench: new question tag",
|
||||
"emoji": True,
|
||||
},
|
||||
"action_id": "create-question-tag",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"label": {"type": "plain_text", "text": "Team tags", "emoji": True},
|
||||
"element": {
|
||||
"action_id": "team-tag-list",
|
||||
"type": "multi_external_select",
|
||||
"placeholder": {"type": "plain_text", "text": "Select tags"},
|
||||
"min_query_length": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": (
|
||||
f"Reopened by <@{reopened_by.slackId}>. Originally submitted by <@{author_user_id}>. <{thread_url}|View thread>."
|
||||
if reopened_by
|
||||
else f"Submitted by <@{author_user_id}>. They have {past_tickets} past tickets. <{thread_url}|View thread>."
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def backend_message_fallback_text(
|
||||
author_user_id: str,
|
||||
description: str,
|
||||
reopened_by: User | None = None,
|
||||
) -> str:
|
||||
return (
|
||||
f"Reopened ticket from <@{author_user_id}>: {description}"
|
||||
if reopened_by
|
||||
else f"New question from <@{author_user_id}>: {description}"
|
||||
)
|
||||
|
||||
|
||||
async def send_backend_message(
|
||||
author_user_id: str,
|
||||
msg_ts: str,
|
||||
description: str,
|
||||
past_tickets: int,
|
||||
client: AsyncWebClient,
|
||||
current_question_tag_id: int | None = None,
|
||||
reopened_by: User | None = None,
|
||||
display_name: str | None = None,
|
||||
profile_pic: str | None = None,
|
||||
):
|
||||
"""Send a "backend" message to the tickets channel with ticket details."""
|
||||
|
||||
return await client.chat_postMessage(
|
||||
channel=env.slack_ticket_channel,
|
||||
text=backend_message_fallback_text(author_user_id, description, reopened_by),
|
||||
blocks=await backend_message_blocks(
|
||||
author_user_id, msg_ts, past_tickets, current_question_tag_id, reopened_by
|
||||
),
|
||||
username=display_name,
|
||||
icon_url=profile_pic,
|
||||
unfurl_links=True,
|
||||
unfurl_media=True,
|
||||
)
|
||||
|
|
@ -8,6 +8,7 @@ from prometheus_client import Histogram
|
|||
from slack_sdk.errors import SlackApiError
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.events.message.send_backend_message import send_backend_message
|
||||
from nephthys.macros import run_macro
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
|
|
@ -104,49 +105,6 @@ async def handle_message_in_thread(event: Dict[str, Any], db_user: User | None):
|
|||
)
|
||||
|
||||
|
||||
async def send_ticket_message(
|
||||
event: Dict[str, Any],
|
||||
client: AsyncWebClient,
|
||||
past_tickets: int,
|
||||
display_name: str | None,
|
||||
profile_pic: str | None,
|
||||
):
|
||||
"""Send a "backend" message to the tickets channel with ticket details."""
|
||||
user = event.get("user", "unknown")
|
||||
text = event.get("text", "")
|
||||
thread_url = f"https://hackclub.slack.com/archives/{env.slack_help_channel}/p{event['ts'].replace('.', '')}"
|
||||
|
||||
return await client.chat_postMessage(
|
||||
channel=env.slack_ticket_channel,
|
||||
text=f"New message from <@{user}>: {text}",
|
||||
blocks=[
|
||||
{
|
||||
"type": "input",
|
||||
"label": {"type": "plain_text", "text": "Tag ticket", "emoji": True},
|
||||
"element": {
|
||||
"action_id": "tag-list",
|
||||
"type": "multi_external_select",
|
||||
"placeholder": {"type": "plain_text", "text": "Select tags"},
|
||||
"min_query_length": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": f"Submitted by <@{user}>. They have {past_tickets} past tickets. <{thread_url}|View thread>.",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
username=display_name,
|
||||
icon_url=profile_pic,
|
||||
unfurl_links=True,
|
||||
unfurl_media=True,
|
||||
)
|
||||
|
||||
|
||||
async def handle_new_question(
|
||||
event: Dict[str, Any], client: AsyncWebClient, db_user: User | None
|
||||
):
|
||||
|
|
@ -183,10 +141,12 @@ async def handle_new_question(
|
|||
)
|
||||
|
||||
async with perf_timer("Sending backend ticket message"):
|
||||
ticket_message = await send_ticket_message(
|
||||
event,
|
||||
client,
|
||||
ticket_message = await send_backend_message(
|
||||
author_user_id=author_id,
|
||||
description=text,
|
||||
msg_ts=event["ts"],
|
||||
past_tickets=past_tickets,
|
||||
client=client,
|
||||
display_name=author.display_name(),
|
||||
profile_pic=author.profile_pic_512x() or "",
|
||||
)
|
||||
|
|
@ -282,7 +242,16 @@ async def send_user_facing_message(
|
|||
"style": "primary",
|
||||
"action_id": "mark_resolved",
|
||||
"value": f"{event['ts']}",
|
||||
}
|
||||
},
|
||||
# {
|
||||
# "type": "button",
|
||||
# "text": {
|
||||
# "type": "plain_text",
|
||||
# "text": "Manage tags (helpers only)",
|
||||
# },
|
||||
# "action_id": "manage_tags_from_thread",
|
||||
# "value": f"{event['ts']}",
|
||||
# },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -2,11 +2,11 @@ import logging
|
|||
|
||||
from slack_sdk.errors import SlackApiError
|
||||
|
||||
from nephthys.events.message.send_backend_message import send_backend_message
|
||||
from nephthys.macros.types import Macro
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.logging import send_heartbeat
|
||||
from nephthys.utils.slack_user import get_user_profile
|
||||
from nephthys.utils.ticket_methods import get_question_message_link
|
||||
from nephthys.utils.ticket_methods import reply_to_ticket
|
||||
from prisma.enums import TicketStatus
|
||||
|
||||
|
|
@ -28,6 +28,7 @@ class Reopen(Macro):
|
|||
data={
|
||||
"status": TicketStatus.OPEN,
|
||||
"closedBy": {"disconnect": True},
|
||||
"reopenedBy": {"connect": {"id": helper.id}},
|
||||
"closedAt": None,
|
||||
},
|
||||
)
|
||||
|
|
@ -45,40 +46,23 @@ class Reopen(Macro):
|
|||
return
|
||||
author_id = ticket.openedBy.slackId
|
||||
author = await get_user_profile(author_id)
|
||||
thread_url = get_question_message_link(ticket)
|
||||
other_tickets = await env.db.ticket.count(
|
||||
where={
|
||||
"openedById": ticket.openedById,
|
||||
"id": {"not": ticket.id},
|
||||
}
|
||||
)
|
||||
|
||||
backend_message = await env.slack_client.chat_postMessage(
|
||||
channel=env.slack_ticket_channel,
|
||||
text=f"Reopened ticket from <@{author_id}>: {ticket.description}",
|
||||
blocks=[
|
||||
{
|
||||
"type": "input",
|
||||
"label": {
|
||||
"type": "plain_text",
|
||||
"text": "Tag ticket",
|
||||
"emoji": True,
|
||||
},
|
||||
"element": {
|
||||
"action_id": "tag-list",
|
||||
"type": "multi_external_select",
|
||||
"placeholder": {"type": "plain_text", "text": "Select tags"},
|
||||
"min_query_length": 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "context",
|
||||
"elements": [
|
||||
{
|
||||
"type": "mrkdwn",
|
||||
"text": f"Reopened by <@{helper.slackId}>. Originally submitted by <@{author_id}>. <{thread_url}|View thread>.",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
username=author.display_name(),
|
||||
icon_url=author.profile_pic_512x() or "",
|
||||
unfurl_links=True,
|
||||
unfurl_media=True,
|
||||
backend_message = await send_backend_message(
|
||||
author_user_id=author_id,
|
||||
description=ticket.description,
|
||||
msg_ts=ticket.msgTs,
|
||||
past_tickets=other_tickets,
|
||||
client=env.slack_client,
|
||||
current_question_tag_id=ticket.questionTagId,
|
||||
reopened_by=helper,
|
||||
display_name=author.display_name(),
|
||||
profile_pic=author.profile_pic_512x(),
|
||||
)
|
||||
|
||||
new_ticket_ts = backend_message["ts"]
|
||||
|
|
|
|||
30
nephthys/options/question_tags.py
Normal file
30
nephthys/options/question_tags.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import logging
|
||||
|
||||
from thefuzz import fuzz
|
||||
from thefuzz import process
|
||||
|
||||
from nephthys.utils.env import env
|
||||
|
||||
|
||||
async def get_question_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]:
|
||||
tags = await env.db.questiontag.find_many()
|
||||
if not tags:
|
||||
return []
|
||||
|
||||
keyword = payload.get("value")
|
||||
if keyword:
|
||||
tag_names = [tag.label for tag in tags]
|
||||
scores = process.extract(keyword, tag_names, scorer=fuzz.ratio, limit=100)
|
||||
matching_tags = [tags[tag_names.index(score[0])] for score in scores]
|
||||
else:
|
||||
matching_tags = tags
|
||||
|
||||
res = [
|
||||
{
|
||||
"text": {"type": "plain_text", "text": f"{tag.label}"},
|
||||
"value": str(tag.id),
|
||||
}
|
||||
for tag in matching_tags
|
||||
]
|
||||
logging.debug(res)
|
||||
return res
|
||||
|
|
@ -6,7 +6,7 @@ from thefuzz import process
|
|||
from nephthys.utils.env import env
|
||||
|
||||
|
||||
async def get_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]:
|
||||
async def get_team_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]:
|
||||
tags = await env.db.tag.find_many()
|
||||
if not tags:
|
||||
return []
|
||||
|
|
@ -25,5 +25,5 @@ async def get_tags(payload: dict) -> list[dict[str, dict[str, str] | str]]:
|
|||
}
|
||||
for tag in tags
|
||||
]
|
||||
logging.info(res)
|
||||
logging.debug(res)
|
||||
return res
|
||||
|
|
@ -6,9 +6,12 @@ from slack_bolt.async_app import AsyncApp
|
|||
from slack_bolt.context.ack.async_ack import AsyncAck
|
||||
from slack_sdk.web.async_client import AsyncWebClient
|
||||
|
||||
from nephthys.actions.assign_tag import assign_tag_callback
|
||||
from nephthys.actions.create_tag import create_tag_btn_callback
|
||||
from nephthys.actions.create_tag import create_tag_view_callback
|
||||
from nephthys.actions.assign_question_tag import assign_question_tag_callback
|
||||
from nephthys.actions.assign_team_tag import assign_team_tag_callback
|
||||
from nephthys.actions.create_question_tag import create_question_tag_btn_callback
|
||||
from nephthys.actions.create_question_tag import create_question_tag_view_callback
|
||||
from nephthys.actions.create_team_tag import create_team_tag_btn_callback
|
||||
from nephthys.actions.create_team_tag import create_team_tag_view_callback
|
||||
from nephthys.actions.resolve import resolve
|
||||
from nephthys.actions.tag_subscribe import tag_subscribe_callback
|
||||
from nephthys.commands.dm_magic_link import dm_magic_link_cmd_callback
|
||||
|
|
@ -16,9 +19,10 @@ from nephthys.events.app_home_opened import on_app_home_opened
|
|||
from nephthys.events.app_home_opened import open_app_home
|
||||
from nephthys.events.channel_join import channel_join
|
||||
from nephthys.events.channel_left import channel_left
|
||||
from nephthys.events.message import on_message
|
||||
from nephthys.events.message_creation import on_message
|
||||
from nephthys.events.message_deletion import on_message_deletion
|
||||
from nephthys.options.tags import get_tags
|
||||
from nephthys.options.question_tags import get_question_tags
|
||||
from nephthys.options.team_tags import get_team_tags
|
||||
from nephthys.utils.env import env
|
||||
from nephthys.utils.performance import perf_timer
|
||||
|
||||
|
|
@ -51,9 +55,16 @@ async def handle_mark_resolved_button(
|
|||
await resolve(value, resolver, client)
|
||||
|
||||
|
||||
@app.options("tag-list")
|
||||
async def handle_tag_list_options(ack: AsyncAck, payload: dict):
|
||||
tags = await get_tags(payload)
|
||||
@app.options("tag-list") # compat with old backend msgs
|
||||
@app.options("team-tag-list")
|
||||
async def handle_team_tag_list_options(ack: AsyncAck, payload: dict):
|
||||
tags = await get_team_tags(payload)
|
||||
await ack(options=tags)
|
||||
|
||||
|
||||
@app.options("question-tag-list")
|
||||
async def handle_question_tag_list_options(ack: AsyncAck, payload: dict):
|
||||
tags = await get_question_tags(payload)
|
||||
await ack(options=tags)
|
||||
|
||||
|
||||
|
|
@ -64,7 +75,7 @@ async def app_home_opened_handler(event: dict[str, Any], client: AsyncWebClient)
|
|||
|
||||
@app.action("dashboard")
|
||||
@app.action("assigned-tickets")
|
||||
@app.action("tags")
|
||||
@app.action("team-tags")
|
||||
@app.action("my-stats")
|
||||
async def manage_home_switcher(ack: AsyncAck, body, client: AsyncWebClient):
|
||||
await ack()
|
||||
|
|
@ -84,14 +95,30 @@ async def handle_member_left_channel(event: Dict[str, Any], client: AsyncWebClie
|
|||
await channel_left(ack=AsyncAck(), event=event, client=client)
|
||||
|
||||
|
||||
@app.action("create-tag")
|
||||
async def create_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await create_tag_btn_callback(ack, body, client)
|
||||
@app.action("create-team-tag")
|
||||
async def create_team_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await create_team_tag_btn_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.view("create_tag")
|
||||
async def create_tag_view(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await create_tag_view_callback(ack, body, client)
|
||||
@app.action("create-question-tag")
|
||||
async def create_question_tag(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await create_question_tag_btn_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.view("create_team_tag")
|
||||
async def create_team_tag_view(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await create_team_tag_view_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.view("create_question_tag")
|
||||
async def create_question_tag_view(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await create_question_tag_view_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.action("tag-subscribe")
|
||||
|
|
@ -99,9 +126,17 @@ async def tag_subscribe(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebCli
|
|||
await tag_subscribe_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.action("tag-list")
|
||||
async def assign_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await assign_tag_callback(ack, body, client)
|
||||
@app.action("tag-list") # compat with old backend msgs
|
||||
@app.action("team-tag-list")
|
||||
async def assign_team_tag(ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient):
|
||||
await assign_team_tag_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.action("question-tag-list")
|
||||
async def assign_question_tag(
|
||||
ack: AsyncAck, body: Dict[str, Any], client: AsyncWebClient
|
||||
):
|
||||
await assign_question_tag_callback(ack, body, client)
|
||||
|
||||
|
||||
@app.command("/dm-magic-link")
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ def header_buttons(current_view: str):
|
|||
buttons.append(
|
||||
{
|
||||
"type": "button",
|
||||
"text": {"type": "plain_text", "text": "Tags", "emoji": True},
|
||||
"action_id": "tags",
|
||||
**({"style": "primary"} if current_view != "tags" else {}),
|
||||
"text": {"type": "plain_text", "text": "Team Tags", "emoji": True},
|
||||
"action_id": "team-tags",
|
||||
**({"style": "primary"} if current_view != "team-tags" else {}),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ from nephthys.views.home.components.header import get_header
|
|||
from prisma.models import User
|
||||
|
||||
|
||||
async def get_manage_tags_view(user: User | None) -> dict:
|
||||
header = get_header(user, "tags")
|
||||
async def get_team_tags_view(user: User | None) -> dict:
|
||||
header = get_header(user, "team-tags")
|
||||
is_admin = bool(user and user.admin)
|
||||
is_helper = bool(user and user.helper)
|
||||
tags = await env.db.tag.find_many(include={"userSubscriptions": True})
|
||||
|
|
@ -44,19 +44,21 @@ async def get_manage_tags_view(user: User | None) -> dict:
|
|||
"type": "mrkdwn",
|
||||
"text": f"*{tag.name}* - {''.join(stringified_subs) if stringified_subs else ':rac_nooo: no subscriptions'}",
|
||||
},
|
||||
"accessory": {
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f":rac_cute: {'subscribe' if user.id not in subs else 'unsubscribe'}",
|
||||
"emoji": True,
|
||||
},
|
||||
"action_id": "tag-subscribe",
|
||||
"value": f"{tag.id};{tag.name}",
|
||||
"style": "primary" if user.id not in subs else "danger",
|
||||
}
|
||||
if user and is_helper
|
||||
else {},
|
||||
"accessory": (
|
||||
{
|
||||
"type": "button",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": f":rac_cute: {'subscribe' if user.id not in subs else 'unsubscribe'}",
|
||||
"emoji": True,
|
||||
},
|
||||
"action_id": "tag-subscribe",
|
||||
"value": f"{tag.id};{tag.name}",
|
||||
"style": "primary" if user.id not in subs else "danger",
|
||||
}
|
||||
if user and is_helper
|
||||
else {}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -68,7 +70,7 @@ async def get_manage_tags_view(user: User | None) -> dict:
|
|||
"type": "header",
|
||||
"text": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_info: Manage Tags",
|
||||
"text": ":rac_info: Manage Team Tags",
|
||||
"emoji": True,
|
||||
},
|
||||
},
|
||||
|
|
@ -76,11 +78,15 @@ async def get_manage_tags_view(user: User | None) -> dict:
|
|||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": ":rac_thumbs: here you can manage tags and your subscriptions"
|
||||
if is_admin
|
||||
else ":rac_thumbs: here you can manage your tag subscriptions"
|
||||
if is_helper
|
||||
else ":rac_thumbs: note: you're not a helper, so you can only view tags",
|
||||
"text": (
|
||||
":rac_thumbs: here you can manage tags and your subscriptions"
|
||||
if is_admin
|
||||
else (
|
||||
":rac_thumbs: here you can manage your tag subscriptions"
|
||||
if is_helper
|
||||
else ":rac_thumbs: note: you're not a helper, so you can only view tags"
|
||||
)
|
||||
),
|
||||
},
|
||||
},
|
||||
{"type": "section", "text": {"type": "plain_text", "text": " "}},
|
||||
|
|
@ -100,7 +106,7 @@ async def get_manage_tags_view(user: User | None) -> dict:
|
|||
"text": ":rac_cute: add a tag?",
|
||||
"emoji": True,
|
||||
},
|
||||
"action_id": "create-tag",
|
||||
"action_id": "create-team-tag",
|
||||
"style": "primary",
|
||||
}
|
||||
],
|
||||
43
nephthys/views/modals/create_question_tag.py
Normal file
43
nephthys/views/modals/create_question_tag.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
def get_create_question_tag_modal():
|
||||
return {
|
||||
"type": "modal",
|
||||
"callback_id": "create_question_tag",
|
||||
"title": {
|
||||
"type": "plain_text",
|
||||
"text": "Create question tag",
|
||||
"emoji": True,
|
||||
},
|
||||
"blocks": [
|
||||
{
|
||||
"type": "section",
|
||||
"text": {
|
||||
"type": "mrkdwn",
|
||||
"text": """
|
||||
Question tags are used to keep track of how often specific questions are being asked, so tag names should be brief but specific enough to uniquely identify the question.
|
||||
|
||||
Examples:
|
||||
• Missing CSS on site
|
||||
• What is Flavortown?
|
||||
• "You're not eligible" when trying to ship project
|
||||
""",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "input",
|
||||
"block_id": "tag_label",
|
||||
"label": {
|
||||
"type": "plain_text",
|
||||
"text": "Question label",
|
||||
},
|
||||
"element": {
|
||||
"type": "plain_text_input",
|
||||
"action_id": "tag_label",
|
||||
},
|
||||
},
|
||||
],
|
||||
"submit": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_question: Create!",
|
||||
"emoji": True,
|
||||
},
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
def get_create_tag_modal():
|
||||
def get_create_team_tag_modal():
|
||||
return {
|
||||
"type": "modal",
|
||||
"callback_id": "create_tag",
|
||||
"callback_id": "create_team_tag",
|
||||
"title": {
|
||||
"type": "plain_text",
|
||||
"text": ":rac_info: create a tag!",
|
||||
|
|
@ -30,6 +30,7 @@ model User {
|
|||
openedTickets Ticket[] @relation("OpenedTickets")
|
||||
closedTickets Ticket[] @relation("ClosedTickets")
|
||||
assignedTickets Ticket[] @relation("AssignedTickets")
|
||||
reopenedTickets Ticket[] @relation("ReopenedTickets")
|
||||
|
||||
tagSubscriptions UserTagSubscription[]
|
||||
helper Boolean @default(false)
|
||||
|
|
@ -57,6 +58,9 @@ model Ticket {
|
|||
openedBy User @relation("OpenedTickets", fields: [openedById], references: [id])
|
||||
openedById Int
|
||||
|
||||
reopenedBy User? @relation("ReopenedTickets", fields: [reopenedById], references: [id])
|
||||
reopenedById Int?
|
||||
|
||||
closedBy User? @relation("ClosedTickets", fields: [closedById], references: [id])
|
||||
closedById Int?
|
||||
closedAt DateTime?
|
||||
|
|
@ -66,10 +70,20 @@ model Ticket {
|
|||
assignedAt DateTime?
|
||||
|
||||
tagsOnTickets TagsOnTickets[]
|
||||
questionTag QuestionTag? @relation("QuestionTagTickets", fields: [questionTagId], references: [id])
|
||||
questionTagId Int?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model QuestionTag {
|
||||
id Int @id @unique @default(autoincrement())
|
||||
label String @unique
|
||||
tickets Ticket[] @relation("QuestionTagTickets")
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
// These tags are team tags
|
||||
model Tag {
|
||||
id Int @id @unique @default(autoincrement())
|
||||
name String @unique
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue