-from turbot.blocks import section_block, text_block, divider_block
+from turbot.blocks import (
+ section_block, text_block, divider_block, accessory_block, button_block
+)
from turbot.round import round_blocks
-from turbot.puzzle import puzzle_block
+from turbot.puzzle import puzzle_blocks, puzzle_matches_all
from turbot.channel import channel_url
from boto3.dynamodb.conditions import Key
else:
return None
-def hunt_blocks(turb, hunt, puzzle_status='unsolved'):
+def hunt_puzzles_for_hunt_id(turb, hunt_id):
+ """Return all puzzles that belong to the given hunt_id"""
+
+ response = turb.table.query(
+ KeyConditionExpression=(
+ Key('hunt_id').eq(hunt_id) &
+ Key('SK').begins_with('puzzle-')
+ )
+ )
+
+ return response['Items']
+
+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.
- Option 'puzzle_status' indicates which puzzles to include. If
- either 'solved' or 'unsolved' only puzzles with that status from
- the hunt will be included in the result. If any other value, all
- puzzles in the hunt will be included.
+ Three optional arguments can be used to filter which puzzles to
+ include in the result:
+
+ puzzle_status: If either 'solved' or 'unsolved' only puzzles
+ with that status will be included in the
+ result. If any other value, all puzzles in the
+ hunt will be considered.
+
+ search_terms: A list of search terms. Only puzzles that match
+ all of these terms will be included in the
+ result. A match will be considered on any of
+ puzzle title, round title, puzzle URL, puzzle
+ state, puzzle type, tags, 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,
hunt_id = hunt['hunt_id']
channel_id = hunt['channel_id']
- response = turb.table.query(
- KeyConditionExpression=(
- Key('hunt_id').eq(hunt_id) &
- Key('SK').begins_with('puzzle-')
- )
- )
- puzzles = response['Items']
+ puzzles = hunt_puzzles_for_hunt_id(turb, hunt_id)
# Filter the set of puzzles according the the requested puzzle_status
if puzzle_status in ('solved', 'unsolved'):
puzzles = [p for p in puzzles if p['status'] == puzzle_status]
+ else:
+ puzzle_status = 'all'
+
+ if search_terms:
+ puzzles = [puzzle for puzzle in puzzles
+ if puzzle_matches_all(puzzle, search_terms)]
# Compute the set of rounds across all puzzles
rounds = set()
for round in puzzle['rounds']:
rounds.add(round)
- hunt_text = "*<{}|{}>*".format(channel_url(channel_id), name)
+ hunt_text = "*{} puzzles in hunt <{}|{}>*".format(
+ puzzle_status.capitalize(),
+ 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))
blocks = [
- section_block(text_block(hunt_text)),
+ accessory_block(
+ section_block(text_block(hunt_text)),
+ button_block("✏", "edit_hunt", hunt_id)
+ )
]
+ if not len(puzzles):
+ text = "No puzzles found."
+ if puzzle_status != 'all':
+ text += ' (Consider searching for "all" puzzles?)'
+ blocks += [
+ section_block(text_block(text))
+ ]
+
# 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)
+ blocks.append(divider_block())
# 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):
- stray_text = "*Puzzles with no asigned round*"
+
+ # 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:
- blocks.append(puzzle_block(puzzle))
+ blocks += puzzle_blocks(puzzle)
blocks.append(divider_block())