Skip to content

Commit

Permalink
feat: support standup configuration from Slack form
Browse files Browse the repository at this point in the history
This commits adds:
- feature to manage standups from Slack using slash commands & modals.
- New standups, with users, questions and other configs can be added.
- Existing standups can be edited via the same method.
  • Loading branch information
vipul-sharma20 committed Aug 22, 2021
1 parent 4fd2160 commit c20d8da
Show file tree
Hide file tree
Showing 4 changed files with 416 additions and 125 deletions.
106 changes: 106 additions & 0 deletions app/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
{
"type": "actions",
"block_id": "open_standup",
"elements": [
{
"type": "button",
Expand Down Expand Up @@ -102,3 +103,108 @@
}
]
}


CONFIGURE_VIEW = {
"type": "modal",
"callback_id": "configure_standup",
"submit": {
"type": "plain_text",
"text": "Submit",
"emoji": True
},
"close": {
"type": "plain_text",
"text": "Cancel",
"emoji": True
},
"title": {
"type": "plain_text",
"text": "Configure Standup",
"emoji": True
},
"blocks": [
{
"type": "divider"
},
{
"type": "input",
"element": {
"type": "multi_users_select",
"action_id": "multi_users_select-action"
},
"label": {
"type": "plain_text",
"text": "Users",
"emoji": True
}
},
{
"type": "input",
"element": {
"type": "plain_text_input",
"multiline": True,
"action_id": "plain_text_input-action"
},
"label": {
"type": "plain_text",
"text": "Standup questions",
"emoji": True
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Questions are new line separated"
}
]
},
{"type": "divider"},
{
"type": "section",
"block_id": "channels_select",
"text": {
"type": "mrkdwn",
"text": "Pick a channel to publish submissions to from the dropdown list"
},
"accessory": {
"type": "channels_select",
"action_id": "channels_select",
"placeholder": {
"type": "plain_text",
"text": "Select an item"
}
}
},
{"type": "divider"},
{
"type": "section",
"block_id": "timepicker_select",
"text": {
"type": "mrkdwn",
"text": "Pick the time you want submissions to be published"
},
"accessory": {
"type": "timepicker",
"initial_time": "13:00",
"placeholder": {
"type": "plain_text",
"text": "Select time",
"emoji": True
},
"action_id": "timepicker_action"
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Time in UTC"
}
]
}
]
}
273 changes: 273 additions & 0 deletions app/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import json
from datetime import datetime

from slack_sdk.errors import SlackApiError

import app.utils as utils
import app.constants as constants
from app.models import Team, Standup, User, Submission, db
from app import client


# Handler for new/existing standup configuration
def configure_standup_handler(**kwargs):
payload = kwargs.get("data", {})
_, team_name = payload.get("view", {}).get("callback_id", "").split("-")
blocks = payload["view"]["blocks"]
submit_list = []

for block in blocks:
print(block)
block_id = block.get("block_id", "")
action_id = block.get("element", {}).get("action_id", "")

if block_id in ["channels_select", "timepicker_select"]:
action_id = block.get("accessory", {}).get("action_id", "")
print(action_id)

values = payload["view"].get("state", {}).get("values", {})
if action_id == "multi_users_select-action":
submit_list.append(values.get(block_id, {}).get(
action_id, {}).get("selected_users", []))
elif action_id == "channels_select":
submit_list.append(values.get(block_id, {}).get(
action_id, {}).get("selected_channel", []))
elif action_id == "timepicker_action":
submit_list.append(values.get(block_id, {}).get(
action_id, {}).get("selected_time", []))
else:
submit_list.append(values.get(block_id, {}).get(
action_id, {}).get("value", ""))

values = list(filter(lambda x: x != "", submit_list))
user_list, questions, publish_channel, publish_time = values
publish_time = datetime.strptime("13:00", "%H:%M").time()

# Get team
team = Team.query.filter_by(name=team_name).first()
if not team:
team = Team(name=team_name)

db.session.add(team)
db.session.commit()

# Get all active users for this team
users = (
db.session.query(User)
.join(Team.user)
.filter(Team.id == team.id, User.is_active)
)
user_ids = [user.user_id for user in users]

# Users to remove
remove_user_list = set(user_ids) - set(user_list)
for user_id in remove_user_list:
user = User.query.filter_by(user_id=user_id)
teams = []
for team in user.team:
if team.name != team_name:
teams.append(team)
user.team = teams

db.session.add(user)
db.session.commit()

# Users to add
add_user_list = set(user_list) - set(user_ids)
for user_id in add_user_list:
user = User.query.filter_by(user_id=user_id).first()
if not user:
User(user_id=user, is_active=True, team=[team])
else:
if team not in user.team:
user.team.append(team)

db.session.add(user)
db.session.commit()

# Create or update standup
questions = questions.split("\n")
blockkit_form = utils.questions_to_blockkit(questions)
blockkit_form["callback_id"] = f"submit_standup-{team_name}"

standup = Standup.query.filter(Standup.trigger == team_name).first()
if not standup:
standup = Standup(standup_blocks=json.dumps(blockkit_form),
trigger=team_name,
team=team,
publish_time=publish_time,
publish_channel=publish_channel)
team.standup = standup
db.session.add(standup)
db.session.commit()
db.session.add(standup)
db.session.commit()
else:
standup.standup_blocks = json.dumps(blockkit_form)
standup.publish_time = publish_time
standup.publish_channel = publish_channel
db.session.add(standup)
db.session.commit()


# Handler for new standup submission
def submit_standup_handler(**kwargs):
payload = kwargs.get("data")
standup_submission = json.dumps(payload.get("view"))

if payload and utils.is_submission_eligible(payload):
user_payload = payload.get("user", {})
_, team_name = payload.get("view", {}).get("callback_id", "").split("-")

user = User.query.filter_by(user_id=user_payload.get("id")).first()
standup = Standup.query.filter(Standup.trigger == team_name).first()

todays_datetime = datetime(
datetime.today().year, datetime.today().month, datetime.today().day
)

is_edit = False
if submission := utils.submission_exists(user, standup):
client.chat_postMessage(channel=user.user_id,
text=constants.SUBMISSION_UPDATED_MESSAGE)
submission.standup_submission = standup_submission
is_edit = True
else:
submission = Submission(user_id=user.id,
standup_submission=standup_submission,
standup_id=standup.id,
standup=standup)

db.session.add(submission)
db.session.commit()

utils.after_submission(submission, is_edit)


# Open view to configure standup
def open_configure_view(**kwargs):
data = kwargs.get("data")
config_blocks: List = dict(constants.CONFIGURE_VIEW)

try:
command, team_name = data.get("text").split(" ")
except ValueError:
return make_response(
f"Slash command format is `/standup configure <team-name>`.",
200,
)

team = Team.query.filter_by(name=team_name).first()

if team:
# Prepare standup question list to put in textfield
standup_json = json.loads(team.standup.standup_blocks)

blocks = standup_json.get("blocks", [])
questions = filter(lambda block: block["type"] == "input", blocks)
questions = map(lambda block: block["label"]["text"], questions)

# Get all active users for this team
users = (
db.session.query(User)
.join(Team.user)
.filter(Team.id == team.id, User.is_active)
).all()

users_list = [user.user_id for user in users]

# Add initial values
users_input_block = config_blocks["blocks"][1]
standup_input_block = config_blocks["blocks"][2]
channel_block = config_blocks["blocks"][5]
publish_time_block = config_blocks["blocks"][7]

users_input_block["element"]["initial_users"] = users_list
standup_input_block["element"]["initial_value"] = "\n".join(questions)
channel_block["accessory"]["initial_channel"] = team.standup.publish_channel
publish_time_block["accessory"]["initial_time"] = time.strftime(team.standup.publish_time, "%H:%M")

config_blocks["callback_id"] = f"configure_standup-{team_name}"

return client.views_open(
trigger_id=data.get("trigger_id"),
view=config_blocks
)


# Open standup view for a user
def open_standup_view(**kwargs):
user_id = kwargs.get("user_id")
data = kwargs.get("data", None)
trigger_type = kwargs.get("trigger_type", constants.BUTTON_TRIGGER)

try:
user = User.query.filter_by(user_id=user_id).first()
if trigger_type == constants.BUTTON_TRIGGER:
team = (
db.session.query(Team)
.join(User.team)
.filter(User.id == user.id)
.first()
)
else:
team_name = data.get("text")
print(team_name)
if not team_name:
return make_response(
f"Slash command format is `/standup <team-name>`.\nYour commands: {', '.join(utils.get_user_slash_commands(user))}",
200,
)
team = Team.query.filter_by(name=team_name).first()

# TODO: Check if this user it allowed in this team's standup especially
# in the case of slash command trigger.
standup = team.standup

if submission := utils.submission_exists(user, standup):
client.views_open(
trigger_id=data.get("trigger_id"),
view=open_edit_view(standup, submission)
)

client.views_open(
trigger_id=data.get("trigger_id"),
view=utils.get_standup_view(standup)
)
return make_response("", 200)
except SlackApiError as e:
code = e.response["error"]
return make_response(f"Failed to open a modal due to {code}", 200)
except AttributeError:
return make_response(
f"No user details or standup exists for this request.\n{NO_USER_ERROR_MESSAGE}",
200,
)


# Create block kit filled with existing responses for standup
def open_edit_view(standup: Standup, submission: Submission) -> str:
standup_json = json.loads(submission.standup_submission)
submission_text_list: List = []

# Create list of existing responses
blocks = standup_json.get("blocks", [])
for block in blocks:
block_id = block.get("block_id", "")
action_id = block.get("element", {}).get("action_id", "")

values = standup_json.get("state", {}).get("values", {})
submission_text_list.append(values.get(
block_id, {}).get(action_id, {}).get("value", ""))

# Create edit view filled with responses
standup_blocks = json.loads(standup.standup_blocks)
filled_blocks: List = []
for idx, block in enumerate(standup_blocks.get("blocks", [])):
if block["type"] == "input":
block["element"]["initial_value"] = submission_text_list[idx]
filled_blocks.append(block)
standup_blocks["blocks"] = filled_blocks
standup_blocks["callback_id"] = f"submit_standup-{standup.trigger}"

return json.dumps(standup_blocks)
Loading

0 comments on commit c20d8da

Please sign in to comment.