]> git.cworth.org Git - turbot/commitdiff
Implement a dialog box to edit a puzzle
authorCarl Worth <cworth@cworth.org>
Sat, 9 Jan 2021 01:23:32 +0000 (17:23 -0800)
committerCarl Worth <cworth@cworth.org>
Sat, 9 Jan 2021 05:37:44 +0000 (21:37 -0800)
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
turbot/blocks.py
turbot/interaction.py
turbot/puzzle.py

diff --git a/TODO b/TODO
index 5c551668b13aad5e57e0b321f81d66bb86d3b979..cbc22644936ab4ede2e310a5da337a9f10ebcdab 100644 (file)
--- 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)
 
index dde2cfb913d3bcddd76e1c44da9f465587aeb081..4949420ec5210a4b60d634db1733db6b6f62f254 100644 (file)
@@ -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
index f9fcedee34b174dcc4bc2bd0669e3214c9ea72c9..d14c3d4ad07195aeefc916b9e0649ca664b9e98b 100644 (file)
@@ -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"""
 
index 81e0630f88c08416097ec55aa972ccde18387164..b6e78833098f9ed2c4d5251a82400d01941c9b07 100644 (file)
@@ -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