From ac83e42a735ae1098ebcec783b5f816369eb3917 Mon Sep 17 00:00:00 2001 From: Carl Worth Date: Fri, 8 Jan 2021 17:23:32 -0800 Subject: [PATCH] Implement a dialog box to edit a puzzle This is connected to the "pencil" buttons that were recently added to the /hunt output as well as the Turbot home view. So far, this brings up an editable dialog with the puzzle state but doesn't actually do anything with any of the edits made when the user saves the dialog. --- TODO | 3 ++ turbot/blocks.py | 75 +++++++++++++++++++++++++++++------ turbot/interaction.py | 91 +++++++++++++++++++++++++++++++++++++++++-- turbot/puzzle.py | 19 +++++++++ 4 files changed, 174 insertions(+), 14 deletions(-) diff --git a/TODO b/TODO index 5c55166..cbc2264 100644 --- a/TODO +++ b/TODO @@ -23,6 +23,9 @@ Ordered punch-list (aiming to complete by 2021-01-08) • Tweak /hunt and /round to treat meta puzzles as special (sort them first and add some META label) +• Make `/puzzle` for a meta puzzle also display the names/answers of + all puzzles in the round. + • Add /tag to add/remove tags (will force to all caps and store as a prefix as part of the state string for now) diff --git a/turbot/blocks.py b/turbot/blocks.py index dde2cfb..4949420 100644 --- a/turbot/blocks.py +++ b/turbot/blocks.py @@ -29,6 +29,41 @@ def actions_block(*elements): "elements": list(elements) } +def checkbox_block(label, text, name, checked=False): + + element = { + "type": "checkboxes", + "options": [ + { + "value": name, + "text": { + "type": "plain_text", + "text": text + } + } + ] + } + + if checked: + element["initial_options"] = [{ + "value": name, + "text": { + "type": "plain_text", + "text": text + } + }] + + return { + "type": "input", + "block_id": name, + "element": element, + "optional": True, + "label": { + "type": "plain_text", + "text": label + } + } + def button_block(label, name, extra=None): block = { @@ -54,28 +89,35 @@ def accessory_block(main, accessory): } } -def input_block(label, name, placeholder, optional=False): +def input_block(label, name, placeholder, initial_value=None, optional=False): + + element = { + "type": "plain_text_input", + "action_id": name, + "placeholder": { + "type": "plain_text", + "text": placeholder, + } + } + + if initial_value: + element["initial_value"] = initial_value + return { "type": "input", "block_id": name, "optional": optional, - "element": { - "type": "plain_text_input", - "action_id": name, - "placeholder": { - "type": "plain_text", - "text": placeholder, - } - }, + "element": element, "label": { "type": "plain_text", "text": label } } -def multi_select_block(label, name, placeholder, options, default=None): +def multi_select_block(label, name, placeholder, options, + initial_options=None): - multi_select = { + multi_select = { "action_id": name, "type": "multi_static_select", "placeholder": { @@ -93,6 +135,17 @@ def multi_select_block(label, name, placeholder, options, default=None): ] } + if initial_options: + multi_select["initial_options"] = [ + { + "text": { + "type": "plain_text", + "text": option + }, + "value": option + } for option in initial_options + ] + return accessory_block( section_block(text_block("*{}*".format(label)), block_id=name), multi_select diff --git a/turbot/interaction.py b/turbot/interaction.py index f9fcede..d14c3d4 100644 --- a/turbot/interaction.py +++ b/turbot/interaction.py @@ -1,9 +1,9 @@ from slack.errors import SlackApiError from turbot.blocks import ( - input_block, section_block, text_block, multi_select_block + input_block, section_block, text_block, multi_select_block, checkbox_block ) from turbot.hunt import find_hunt_for_hunt_id, hunt_blocks -from turbot.puzzle import find_puzzle_for_url +from turbot.puzzle import find_puzzle_for_url, find_puzzle_for_puzzle_id import turbot.rot import turbot.sheets import turbot.slack @@ -72,12 +72,97 @@ actions['multi_static_select'] = {"*": multi_static_select} def edit_puzzle(turb, payload): """Handler for the action of user pressing an edit_puzzle button""" - print("DEBUG: In edit_puzzle with payload: {}".format(str(payload))) + action_id = payload['actions'][0]['action_id'] + response_url = payload['response_url'] + trigger_id = payload['trigger_id'] + + (hunt_id, puzzle_id) = action_id.split('-', 1) + + puzzle = find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id) + + if not puzzle: + requests.post(response_url, + json = {"text": "Error: Puzzle not found!"}, + headers = {"Content-type": "application/json"}) + return bot_reply("Error: Puzzle not found.") + + round_options = hunt_rounds(turb, hunt_id) + + if len(round_options): + round_options_block = [ + multi_select_block("Round(s)", "rounds", + "Existing round(s) this puzzle belongs to", + round_options, + initial_options=puzzle.get("rounds", None)), + ] + else: + round_options_block = [] + + solved = False + if puzzle.get("status", "unsolved") == solved: + solved = True + + solution_str = None + solution_list = puzzle.get("solution", []) + if solution_list: + solution_str = ", ".join(solution_list) + + view = { + "type": "modal", + "private_metadata": json.dumps({ + "hunt_id": hunt_id, + "SK": puzzle["SK"], + "puzzle_id": puzzle_id, + "channel_id": puzzle["channel_id"], + "channel_url": puzzle["channel_url"], + "sheet_url": puzzle["sheet_url"], + }), + "title": {"type": "plain_text", "text": "Edit Puzzle"}, + "submit": { "type": "plain_text", "text": "Save" }, + "blocks": [ + input_block("Puzzle name", "name", "Name of the puzzle", + initial_value=puzzle["name"]), + input_block("Puzzle URL", "url", "External URL of puzzle", + initial_value=puzzle.get("url", None), + optional=True), + * round_options_block, + input_block("New round(s)", "new_rounds", + "New round(s) this puzzle belongs to " + + "(comma separated)", + optional=True), + input_block("State", "state", + "State of this puzzle (partial progress, next steps)", + initial_value=puzzle.get("state", None), + optional=True), + checkbox_block( + "Puzzle status", "Solved", "solved", + checked=(puzzle.get('status', 'unsolved') == 'solved')), + input_block("Solution", "solution", + "Solutions (comma-separated if multiple", + initial_value=solution_str, + optional=True), + ] + } + + result = turb.slack_client.views_open(trigger_id=trigger_id, + view=view) + + if (result['ok']): + submission_handlers[result['view']['id']] = edit_puzzle_submission return lambda_ok actions['button']['edit_puzzle'] = edit_puzzle +def edit_puzzle_submission(turb, payload, metadata): + """Handler for the user submitting the edit puzzle modal + + This is the modal view presented to the user by the edit_puzzle + function above. + """ + + return lambda_ok + def new_hunt(turb, payload): """Handler for the action of user pressing the new_hunt button""" diff --git a/turbot/puzzle.py b/turbot/puzzle.py index 81e0630..b6e7883 100644 --- a/turbot/puzzle.py +++ b/turbot/puzzle.py @@ -5,6 +5,25 @@ from turbot.channel import channel_url from boto3.dynamodb.conditions import Key import re +def find_puzzle_for_puzzle_id(turb, hunt_id, puzzle_id): + """Given a hunt_id and puzzle_id, return that puzzle + + Returns None if no puzzle with the given hunt_id and puzzle_id + exists in the database, otherwise a dictionary with all fields + from the puzzle's row in the database. + """ + + response = turb.table.get_item( + Key={ + 'hunt_id': hunt_id, + 'SK': 'puzzle-{}'.format(puzzle_id) + }) + + if 'Item' in response: + return response['Item'] + else: + return None + def find_puzzle_for_url(turb, hunt_id, url): """Given a hunt_id and URL, return the puzzle with that URL -- 2.43.0