1 from turbot.blocks import section_block, text_block, divider_block
2 from turbot.round import round_blocks
3 from turbot.puzzle import puzzle_blocks, puzzle_matches_all
4 from turbot.channel import channel_url
5 from boto3.dynamodb.conditions import Key
7 def find_hunt_for_hunt_id(turb, hunt_id):
8 """Given a hunt ID find the database item for that hunt
10 Returns None if hunt ID is not found, otherwise a
11 dictionary with all fields from the hunt's row in the table,
12 (channel_id, active, hunt_id, name, url, sheet_url, etc.).
15 response = turb.table.get_item(
18 'SK': 'hunt-{}'.format(hunt_id)
21 if 'Item' in response:
22 return response['Item']
26 def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[],
27 limit_to_rounds=None):
28 """Generate Slack blocks for a hunt
30 The hunt argument should be a dictionary as returned from the
33 Three optional arguments can be used to filter which puzzles to
34 include in the result:
36 puzzle_status: If either 'solved' or 'unsolved' only puzzles
37 with that status will be included in the
38 result. If any other value, all puzzles in the
39 hunt will be considered.
41 search_terms: A list of search terms. Only puzzles that match
42 all of these terms will be included in the
43 result. A match will be considered on any of
44 puzzle title, round title, puzzle URL, puzzle
45 state or solution string. Terms can include
46 regular expression syntax.
48 limit_to_rounds: A list of rounds. If provided only the given
49 rounds will be included in the output. Note:
50 an empty list means to display only puzzles
51 assigned to no rounds, while an argument of
52 None means to display all puzzles with no
55 The return value can be used in a Slack command expecting blocks to
56 provide all the details of a hunt, (puzzles, their state,
57 solution, links to channels and sheets, etc.).
62 hunt_id = hunt['hunt_id']
63 channel_id = hunt['channel_id']
65 response = turb.table.query(
66 KeyConditionExpression=(
67 Key('hunt_id').eq(hunt_id) &
68 Key('SK').begins_with('puzzle-')
71 puzzles = response['Items']
73 # Filter the set of puzzles according the the requested puzzle_status
74 if puzzle_status in ('solved', 'unsolved'):
75 puzzles = [p for p in puzzles if p['status'] == puzzle_status]
80 puzzles = [puzzle for puzzle in puzzles
81 if puzzle_matches_all(puzzle, search_terms)]
83 # Compute the set of rounds across all puzzles
85 for puzzle in puzzles:
86 if 'rounds' not in puzzle:
88 for round in puzzle['rounds']:
91 hunt_text = "*{} puzzles in hunt <{}|{}>*".format(
92 puzzle_status.capitalize(),
93 channel_url(channel_id),
96 if limit_to_rounds is not None:
97 hunt_text += " *in round{}: {}*".format(
98 "s" if len(limit_to_rounds) > 1 else "",
99 ", ".join(limit_to_rounds) if limit_to_rounds else "<no round>"
102 quoted_terms = ['`{}`'.format(term) for term in search_terms]
103 hunt_text += " matching {}".format(" AND ".join(quoted_terms))
106 section_block(text_block(hunt_text)),
111 section_block(text_block("No puzzles found."))
114 # Construct blocks for each round
116 if limit_to_rounds is not None and round not in limit_to_rounds:
118 # If we're only displaying one round the round header is redundant
119 if limit_to_rounds and len(limit_to_rounds) == 1:
120 blocks += round_blocks(round, puzzles, omit_header=True)
122 blocks += round_blocks(round, puzzles)
123 blocks.append(divider_block())
125 # Also blocks for any puzzles not in any round
126 stray_puzzles = [puzzle for puzzle in puzzles if 'rounds' not in puzzle]
128 # For this condition, either limit_to_rounds is None which
129 # means we definitely want to display these stray puzzles
130 # (since we are not limiting), _OR_ limit_to_rounds is not
131 # None but is a zero-length array, meaning we are limiting
132 # to rounds but specifically the round of unassigned puzzles
133 if len(stray_puzzles) and not limit_to_rounds:
134 stray_text = "*Puzzles with no assigned round*"
135 blocks.append(section_block(text_block(stray_text)))
136 for puzzle in stray_puzzles:
137 blocks += puzzle_blocks(puzzle)
139 blocks.append(divider_block())