]> git.cworth.org Git - turbot/blob - turbot/hunt.py
Add state string to list of puzzle attributes matched in searching
[turbot] / turbot / hunt.py
1 from turbot.blocks import section_block, text_block, divider_block
2 from turbot.round import round_blocks
3 from turbot.puzzle import puzzle_block, puzzle_matches_all
4 from turbot.channel import channel_url
5 from boto3.dynamodb.conditions import Key
6
7 def find_hunt_for_hunt_id(turb, hunt_id):
8     """Given a hunt ID find the database item for that hunt
9
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.).
13     """
14
15     response = turb.table.get_item(
16         Key={
17             'hunt_id': hunt_id,
18             'SK': 'hunt-{}'.format(hunt_id)
19         })
20
21     if 'Item' in response:
22         return response['Item']
23     else:
24         return None
25
26 def quote_if_has_space(term):
27     if ' ' in term:
28         return '"{}"'.format(term)
29     else:
30         return term
31
32 def hunt_blocks(turb, hunt, puzzle_status='unsolved', search_terms=[]):
33     """Generate Slack blocks for a hunt
34
35     The hunt argument should be a dictionary as returned from the
36     database.
37
38     Two optional arguments can be used to filter which puzzles to
39     include in the result:
40
41       puzzle_status: If either 'solved' or 'unsolved' only puzzles
42                      with that status will be included in the
43                      result. If any other value, all puzzles in the
44                      hunt will be considered.
45
46       search_terms: A list of search terms. Only puzzles that match
47                     all of these terms will be included in the
48                     result. A match will be considered on any of
49                     puzzle title, round title, puzzle URL, puzzle
50                     state or solution string. Terms can include
51                     regular expression syntax.
52
53     The return value can be used in a Slack command expecting blocks to
54     provide all the details of a hunt, (puzzles, their state,
55     solution, links to channels and sheets, etc.).
56
57     """
58
59     name = hunt['name']
60     hunt_id = hunt['hunt_id']
61     channel_id = hunt['channel_id']
62
63     response = turb.table.query(
64         KeyConditionExpression=(
65             Key('hunt_id').eq(hunt_id) &
66             Key('SK').begins_with('puzzle-')
67         )
68     )
69     puzzles = response['Items']
70
71     # Filter the set of puzzles according the the requested puzzle_status
72     if puzzle_status in ('solved', 'unsolved'):
73         puzzles = [p for p in puzzles if p['status'] == puzzle_status]
74     else:
75         puzzle_status = 'all'
76
77     if search_terms:
78         puzzles = [puzzle for puzzle in puzzles
79                    if puzzle_matches_all(puzzle, search_terms)]
80
81     # Compute the set of rounds across all puzzles
82     rounds = set()
83     for puzzle in puzzles:
84         if 'rounds' not in puzzle:
85             continue
86         for round in puzzle['rounds']:
87             rounds.add(round)
88
89     hunt_text = "*{} puzzles in hunt <{}|{}>".format(
90         puzzle_status.capitalize(),
91         channel_url(channel_id),
92         name
93     )
94     if search_terms:
95         quoted_terms = [quote_if_has_space(term) for term in search_terms]
96         hunt_text += " matching {}".format(" AND ".join(quoted_terms))
97     hunt_text += "*"
98
99     blocks = [
100         section_block(text_block(hunt_text)),
101     ]
102
103     if not len(puzzles):
104         blocks += [
105             section_block(text_block("No puzzles found."))
106         ]
107
108     # Construct blocks for each round
109     for round in rounds:
110         blocks += round_blocks(round, puzzles)
111
112     # Also blocks for any puzzles not in any round
113     stray_puzzles = [puzzle for puzzle in puzzles if 'rounds' not in puzzle]
114     if len(stray_puzzles):
115         stray_text = "*Puzzles with no assigned round*"
116         blocks.append(section_block(text_block(stray_text)))
117         for puzzle in stray_puzzles:
118             blocks.append(puzzle_block(puzzle))
119
120     blocks.append(divider_block())
121
122     return blocks