]> git.cworth.org Git - turbot/commitdiff
Implement a /round command
authorCarl Worth <cworth@cworth.org>
Sat, 9 Jan 2021 09:19:57 +0000 (01:19 -0800)
committerCarl Worth <cworth@cworth.org>
Sat, 9 Jan 2021 09:40:14 +0000 (01:40 -0800)
This is much like the /hunt command with searching, etc. but with two
differences:

  1. It is limited to display puzzles in the same round(s) as the
     current puzzle

  2. It defaults to display all puzzles rather than unsolved puzzles
     like /hunt does

TODO
turbot/hunt.py
turbot/interaction.py
turbot/round.py

diff --git a/TODO b/TODO
index 8cabbd65e9e196a4d924206bae4c93dc1f709a50..4054e9b84cef4f300ee33c7b8edc2afea123dcb1 100644 (file)
--- a/TODO
+++ b/TODO
@@ -1,11 +1,6 @@
 Ordered punch-list (aiming to complete by 2021-01-08)
 -----------------------------------------------------
 
 Ordered punch-list (aiming to complete by 2021-01-08)
 -----------------------------------------------------
 
-• Add /round as a shortcut for existing /hunt all <round-name> (and
-  slightly different in that it won't actually return a puzzle from
-  some other round that happens to have a name that matches this
-  round's name)
-
 • Add "meta" as a checkbox field on puzzle creation/edit
 
 • Tweak /hunt and /round to treat meta puzzles as special (sort them
 • Add "meta" as a checkbox field on puzzle creation/edit
 
 • Tweak /hunt and /round to treat meta puzzles as special (sort them
index ccab28daa4e004de56229291de50dc7b2b50e926..edc8b87c4c2365394a2abcc5163928ec41bdfde8 100644 (file)
@@ -23,13 +23,14 @@ def find_hunt_for_hunt_id(turb, hunt_id):
     else:
         return None
 
     else:
         return None
 
-def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[]):
+def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[],
+                limit_to_rounds=None):
     """Generate Slack blocks for a hunt
 
     The hunt argument should be a dictionary as returned from the
     database.
 
     """Generate Slack blocks for a hunt
 
     The hunt argument should be a dictionary as returned from the
     database.
 
-    Two optional arguments can be used to filter which puzzles to
+    Three optional arguments can be used to filter which puzzles to
     include in the result:
 
       puzzle_status: If either 'solved' or 'unsolved' only puzzles
     include in the result:
 
       puzzle_status: If either 'solved' or 'unsolved' only puzzles
@@ -44,6 +45,13 @@ def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[]):
                     state or solution string. Terms can include
                     regular expression syntax.
 
                     state or solution string. Terms can include
                     regular expression syntax.
 
+      limit_to_rounds: A list of rounds. If provided only the given
+                       rounds will be included in the output. Note:
+                       an empty list means to display only puzzles
+                       assigned to no rounds, while an argument of
+                       None means to display all puzzles with no
+                       limit on the rounds.
+
     The return value can be used in a Slack command expecting blocks to
     provide all the details of a hunt, (puzzles, their state,
     solution, links to channels and sheets, etc.).
     The return value can be used in a Slack command expecting blocks to
     provide all the details of a hunt, (puzzles, their state,
     solution, links to channels and sheets, etc.).
@@ -85,6 +93,11 @@ def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[]):
         channel_url(channel_id),
         name
     )
         channel_url(channel_id),
         name
     )
+    if limit_to_rounds is not None:
+        hunt_text += " *in round{}: {}*".format(
+            "s" if len(limit_to_rounds) > 1 else "",
+            ", ".join(limit_to_rounds) if limit_to_rounds else "<no round>"
+        )
     if search_terms:
         quoted_terms = ['`{}`'.format(term) for term in search_terms]
         hunt_text += " matching {}".format(" AND ".join(quoted_terms))
     if search_terms:
         quoted_terms = ['`{}`'.format(term) for term in search_terms]
         hunt_text += " matching {}".format(" AND ".join(quoted_terms))
@@ -100,11 +113,23 @@ def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[]):
 
     # Construct blocks for each round
     for round in rounds:
 
     # Construct blocks for each round
     for round in rounds:
-        blocks += round_blocks(round, puzzles)
+        if limit_to_rounds is not None and round not in limit_to_rounds:
+            continue
+        # If we're only displaying one round the round header is redundant
+        if limit_to_rounds and len(limit_to_rounds) == 1:
+            blocks += round_blocks(round, puzzles, omit_header=True)
+        else:
+            blocks += round_blocks(round, puzzles)
 
     # Also blocks for any puzzles not in any round
     stray_puzzles = [puzzle for puzzle in puzzles if 'rounds' not in puzzle]
 
     # Also blocks for any puzzles not in any round
     stray_puzzles = [puzzle for puzzle in puzzles if 'rounds' not in puzzle]
-    if len(stray_puzzles):
+
+    # For this condition, either limit_to_rounds is None which
+    # means we definitely want to display these stray puzzles
+    # (since we are not limiting), _OR_ limit_to_rounds is not
+    # None but is a zero-length array, meaning we are limiting
+    # to rounds but specifically the round of unassigned puzzles
+    if len(stray_puzzles) and not limit_to_rounds:
         stray_text = "*Puzzles with no assigned round*"
         blocks.append(section_block(text_block(stray_text)))
         for puzzle in stray_puzzles:
         stray_text = "*Puzzles with no assigned round*"
         blocks.append(section_block(text_block(stray_text)))
         for puzzle in stray_puzzles:
index 6e353c120e1ba3d2a8a5dbca4584b2aa3033f750..f9640ca3473b3abb8adc8e742b9065112c389158 100644 (file)
@@ -858,15 +858,17 @@ def solved(turb, body, args):
 
 commands["/solved"] = solved
 
 
 commands["/solved"] = solved
 
-
 def hunt(turb, body, args):
     """Implementation of the /hunt command
 
     The (optional) args string can be used to filter which puzzles to
     display. The first word can be one of 'all', 'unsolved', or
     'solved' and can be used to display only puzzles with the given
 def hunt(turb, body, args):
     """Implementation of the /hunt command
 
     The (optional) args string can be used to filter which puzzles to
     display. The first word can be one of 'all', 'unsolved', or
     'solved' and can be used to display only puzzles with the given
-    status. Any remaining text in the args string will be interpreted
-    as search terms. These will be split into separate terms on space
+    status. If this first word is missing, this command will display
+    only unsolved puzzles by default.
+
+    Any remaining text in the args string will be interpreted as
+    search terms. These will be split into separate terms on space
     characters, (though quotation marks can be used to include a space
     character in a term). All terms must match on a puzzle in order
     for that puzzle to be included. But a puzzle will be considered to
     characters, (though quotation marks can be used to include a space
     character in a term). All terms must match on a puzzle in order
     for that puzzle to be included. But a puzzle will be considered to
@@ -916,3 +918,77 @@ def hunt(turb, body, args):
     return lambda_ok
 
 commands["/hunt"] = hunt
     return lambda_ok
 
 commands["/hunt"] = hunt
+
+def round(turb, body, args):
+    """Implementation of the /round command
+
+    Displays puzzles in the same round(s) as the puzzle for the
+    current channel.
+
+    The (optional) args string can be used to filter which puzzles to
+    display. The first word can be one of 'all', 'unsolved', or
+    'solved' and can be used to display only puzzles with the given
+    status. If this first word is missing, this command will display
+    all puzzles in the round by default.
+
+    Any remaining text in the args string will be interpreted as
+    search terms. These will be split into separate terms on space
+    characters, (though quotation marks can be used to include a space
+    character in a term). All terms must match on a puzzle in order
+    for that puzzle to be included. But a puzzle will be considered to
+    match if any of the puzzle title, round title, puzzle URL, puzzle
+    state, or puzzle solution match. Matching will be performed
+    without regard to case sensitivity and the search terms can
+    include regular expression syntax.
+    """
+
+    channel_id = body['channel_id'][0]
+    response_url = body['response_url'][0]
+
+    puzzle = puzzle_for_channel(turb, channel_id)
+    hunt = hunt_for_channel(turb, channel_id)
+
+    if not puzzle:
+        if hunt:
+            return bot_reply(
+                "This is not a puzzle channel, but is a hunt channel. "
+                + "Use /hunt if you want to see all rounds for this hunt.")
+        else:
+            return bot_reply(
+                "Sorry, this channel doesn't appear to be a puzzle channel "
+                + "so the `/round` command cannot work here.")
+
+    terms = None
+    if args:
+        # The first word can be a puzzle status and all remaining word
+        # (if any) are search terms. _But_, if the first word is not a
+        # valid puzzle status ('all', 'unsolved', 'solved'), then all
+        # words are search terms and we default status to 'unsolved'.
+        split_args = args.split(' ', 1)
+        status = split_args[0]
+        if (len(split_args) > 1):
+            terms = split_args[1]
+        if status not in ('unsolved', 'solved', 'all'):
+            terms = args
+            status = 'all'
+    else:
+        status = 'all'
+
+    # Separate search terms on spaces (but allow for quotation marks
+    # to capture spaces in a search term)
+    if terms:
+        terms = shlex.split(terms)
+
+    blocks = hunt_blocks(turb, hunt,
+                         puzzle_status=status, search_terms=terms,
+                         limit_to_rounds=puzzle.get('rounds', [])
+                         )
+
+    requests.post(response_url,
+                  json = { 'blocks': blocks },
+                  headers = {'Content-type': 'application/json'}
+                  )
+
+    return lambda_ok
+
+commands["/round"] = round
index 3a4266faae1483e4d2e68351de9a825f3eb1309f..3e12f5c17eb560c0650b1d8c1c22435a94f2fb1f 100644 (file)
@@ -1,7 +1,7 @@
 from turbot.puzzle import puzzle_blocks
 from turbot.blocks import section_block, text_block
 
 from turbot.puzzle import puzzle_blocks
 from turbot.blocks import section_block, text_block
 
-def round_blocks(round, puzzles):
+def round_blocks(round, puzzles, omit_header=False):
     """Generate Slack blocks for a round
 
     The 'round' argument should be the name of a round as it appears
     """Generate Slack blocks for a round
 
     The 'round' argument should be the name of a round as it appears
@@ -15,11 +15,14 @@ def round_blocks(round, puzzles):
     channels and sheets, etc.).
     """
 
     channels and sheets, etc.).
     """
 
-    round_text = "*Round: {}*".format(round)
+    if omit_header:
+        blocks = []
+    else:
+        round_text = "*Round: {}*".format(round)
 
 
-    blocks = [
-        section_block(text_block(round_text)),
-    ]
+        blocks = [
+            section_block(text_block(round_text)),
+        ]
 
     for puzzle in puzzles:
         if 'rounds' not in puzzle:
 
     for puzzle in puzzles:
         if 'rounds' not in puzzle: