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

Add ability to select which users you want to include for the retro #592

Merged
merged 11 commits into from
Jul 30, 2024
Merged
1 change: 1 addition & 0 deletions app/integrations/google_workspace/google_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def insert_event(start, end, emails, title, incident_document, **kwargs):
body=body,
calendarId="primary",
supportsAttachments=True,
sendUpdates="all",
conferenceDataVersion=1,
)
# Handle the instance differently if the result is a dictionary or a tuple and get the calendar link and start time
Expand Down
34 changes: 34 additions & 0 deletions app/integrations/slack/channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,37 @@ def get_messages_in_time_period(client, channel_id, time_delta):
) # Return only messages from users
else:
return []


def fetch_user_details(client, channel_id):
"""
Fetches user details from a Slack channel, excluding users with the real names 'SRE' and 'SRE Dev'.
Parameters:
client (object): The Slack client used to interact with the Slack API.
channel_id (str): The ID of the Slack channel from which to fetch user details.
Returns:
list: A list of dictionaries containing user details, formatted for Slack modal blocks.
"""
# get all members of the channel
result = client.conversations_members(channel=channel_id)
users = []
# extract the real name of the user and append it to the users list, excluding users with the real names 'SRE' and 'SRE Dev'
for user_id in result["members"]:
user_info = client.users_info(user=user_id)
if (
user_info["user"]["real_name"] != "SRE"
and user_info["user"]["real_name"] != "SRE Dev"
):
users.append(
{
"text": {
"type": "plain_text",
"text": user_info["user"]["real_name"],
"emoji": True,
},
"value": user_info["user"]["id"],
}
)
return users
49 changes: 29 additions & 20 deletions app/modules/incident/incident_helper.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import json
import logging
from integrations import google_drive
Expand Down Expand Up @@ -451,11 +450,6 @@ def schedule_incident_retro(client, body, ack):
)
return

# Get security group members. We want to exclude them from the list of people to schedule the event for
security_group_users = client.usergroups_users_list(
usergroup=os.getenv("SLACK_SECURITY_USER_GROUP_ID")
)["users"]

# get all users in a channel
users = client.conversations_members(channel=channel_id)["members"]

Expand All @@ -464,16 +458,6 @@ def schedule_incident_retro(client, body, ack):
channel_name = "Incident Retro"
logging.warning("Channel topic is empty. Setting it to 'Incident Retro'")

user_emails = []

# get the email addresses of all the users in the channel, except security group members and any apps/bots in the channel, since bots don't have an email address associated with them.
for user in users:
if user not in security_group_users:
response = client.users_info(user=user)["user"]["profile"]
# don't include bots in the list of users
if "bot_id" not in response:
user_emails.append(response["email"])

# get the incident document
# get and update the incident document
document_id = ""
Expand All @@ -493,13 +477,15 @@ def schedule_incident_retro(client, body, ack):
# convert the data to string so that we can send it as private metadata
data_to_send = json.dumps(
{
"emails": user_emails,
"name": channel_name,
"incident_document": document_id,
"channel_id": channel_id,
}
)

# Fetch user details from all members of the channel
users = slack_channels.fetch_user_details(client, channel_id)

blocks = {
"type": "modal",
"callback_id": "view_save_event",
Expand All @@ -523,6 +509,20 @@ def schedule_incident_retro(client, body, ack):
"text": "How many days from now should I start checking the calendar for availability?",
},
},
{
"type": "input",
"block_id": "user_select_block",
"label": {
"type": "plain_text",
"text": "Select everyone you want to include in the retro calendar invite",
"emoji": True,
sylviamclaughlin marked this conversation as resolved.
Show resolved Hide resolved
},
"element": {
"type": "multi_static_select",
"action_id": "user_select_action",
"options": users,
},
},
{"type": "divider"},
{
"type": "section",
Expand All @@ -542,7 +542,7 @@ def schedule_incident_retro(client, body, ack):
"type": "section",
"text": {
"type": "mrkdwn",
"text": "2. A proposed event will be added to everyone's calendar that is part of this channel (except Security team).",
"text": "2. A proposed event will be added to everyone's calendar that is selected.",
},
},
{
Expand Down Expand Up @@ -577,9 +577,18 @@ def save_incident_retro(client, ack, body, view):
# get the number of days data from the view and convert to an integer
days = int(view["state"]["values"]["number_of_days"]["number_of_days"]["value"])

# pass the data using the view["private_metadata"] to the schedule_event function
result = schedule_retro.schedule_event(view["private_metadata"], days)
# get all the users selected in the multi select block
users = view["state"]["values"]["user_select_block"]["user_select_action"][
"selected_options"
]
user_emails = []
for user in users:
user_id = user["value"].strip()
user_email = client.users_info(user=user_id)["user"]["profile"]["email"]
user_emails.append(user_email)

# pass the data using the view["private_metadata"] to the schedule_event function
result = schedule_retro.schedule_event(view["private_metadata"], days, user_emails)
# if we could not schedule the event, display a message to the user that the event could not be scheduled
if result is None:
blocks = {
Expand Down
9 changes: 4 additions & 5 deletions app/modules/incident/schedule_retro.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


# Schedule a calendar event by finding the first available slot in the next 60 days that all participants are free in and book the event
def schedule_event(event_details, days):
def schedule_event(event_details, days, user_emails):
# Define the time range for the query
now = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0)
# time_min is the current time + days and time_max is the current time + 60 days + days
Expand All @@ -20,11 +20,10 @@ def schedule_event(event_details, days):

# Construct the items array
items = []
emails = json.loads(event_details).get("emails")
incident_name = json.loads(event_details).get("name")
for email in emails:
for email in user_emails:
email = email.strip()
items.append({"id": email})
incident_name = json.loads(event_details).get("name")

# get the incident document link
incident_document = json.loads(event_details).get("incident_document")
Expand Down Expand Up @@ -63,7 +62,7 @@ def schedule_event(event_details, days):
result = insert_event(
first_available_start.isoformat(),
first_available_end.isoformat(),
emails,
user_emails,
"Retro " + incident_name,
incident_document,
**event_config,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ def test_insert_event_no_kwargs_no_delegated_email(
},
calendarId="primary",
supportsAttachments=True,
sendUpdates="all",
conferenceDataVersion=1,
)
assert not mock_convert_string_to_camel_case.called
Expand Down Expand Up @@ -297,6 +298,7 @@ def test_insert_event_with_kwargs(
},
calendarId="primary",
supportsAttachments=True,
sendUpdates="all",
conferenceDataVersion=1,
)
for key in kwargs:
Expand Down Expand Up @@ -372,6 +374,7 @@ def test_insert_event_with_no_document(
},
calendarId="primary",
supportsAttachments=True,
sendUpdates="all",
conferenceDataVersion=1,
)
for key in kwargs:
Expand Down Expand Up @@ -442,6 +445,7 @@ def test_insert_event_google_hangout_link_created(
},
calendarId="primary",
supportsAttachments=True,
sendUpdates="all",
conferenceDataVersion=1,
)
assert mock_unique_id.called
Expand Down
58 changes: 58 additions & 0 deletions app/tests/integrations/slack/test_channels.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,61 @@ def test_get_messages_in_time_period_with_error():
channels.get_messages_in_time_period(client, "channel_id", timedelta(days=1))
== []
)


def test_fetch_user_details():
client_mock = MagicMock()
client_mock.conversations_members.return_value = {
"members": ["U123", "U456", "U789"]
}
client_mock.users_info.side_effect = [
{"user": {"real_name": "Alice", "id": "U123"}},
{"user": {"real_name": "SRE", "id": "U456"}},
{"user": {"real_name": "Bob", "id": "U789"}},
]

expected_users = [
{
"text": {
"type": "plain_text",
"text": "Alice",
"emoji": True,
},
"value": "U123",
},
{
"text": {
"type": "plain_text",
"text": "Bob",
"emoji": True,
},
"value": "U789",
},
]

result = channels.fetch_user_details(client_mock, "C123456")
assert result == expected_users


def test_fetch_user_details_no_users():
client_mock = MagicMock()
client_mock.conversations_members.return_value = {"members": []}

expected_users = []

result = channels.fetch_user_details(client_mock, "C123456")
assert result == expected_users


def test_fetch_user_details_all_sre():
client_mock = MagicMock()
client_mock.conversations_members.return_value = {"members": ["U123", "U456"]}
client_mock.users_info.side_effect = [
{"user": {"real_name": "SRE", "id": "U123"}},
{"user": {"real_name": "SRE Dev", "id": "U456"}},
]

expected_users = []

result = channels.fetch_user_details(client_mock, "C123456")
assert result == expected_users
Loading
Loading