1 # -*- coding: utf-8 -*-
3 Created on Thu Jan 6 23:35:23 2022
5 @author: Avram Gottschlich
8 I copied several functions from your code;
9 if it's easier to refer to them rather than copying them that's absolutely fine
11 This rewrites the html each time it is called
12 If it's easy to call the puzzle/round functions each time they're updated,
15 Requires sorttable.js, which should be included
17 from turbot.channel import channel_url
18 from boto3.dynamodb.conditions import Key
20 website = "https://halibut.cworth.org/"
21 #change this if we're using AWS or some other subdomain instead
23 def find_hunt_for_hunt_id(turb, hunt_id):
24 """Given a hunt ID find the database item for that hunt
26 Returns None if hunt ID is not found, otherwise a
27 dictionary with all fields from the hunt's row in the table,
28 (channel_id, active, hunt_id, name, url, sheet_url, etc.).
30 response = turb.table.get_item(
33 'SK': 'hunt-{}'.format(hunt_id)
36 if 'Item' in response:
37 return response['Item']
41 def hunt_puzzles_for_hunt_id(turb, hunt_id):
42 """Return all puzzles that belong to the given hunt_id"""
44 response = turb.table.query(
45 KeyConditionExpression=(
46 Key('hunt_id').eq(hunt_id) &
47 Key('SK').begins_with('puzzle-')
51 return response['Items']
54 #shortcutting function for creating html links
55 #opens in a new tab for external links
56 return '<a href="{}" target="_blank" rel="noreferrer noopener">{}</a>'.format(lin, text)
59 #internal links, doesn't open new tab
60 return '<a href="{}">{}</a>'.format(lin, text)
62 def hunt_info(turb, hunt):
64 Retrieves list of rounds, puzzles for the given hunt
67 hunt_id = hunt["hunt_id"]
68 channel_id = hunt["channel_id"]
70 puzzles = hunt_puzzles_for_hunt_id(turb, hunt_id)
73 for puzzle in puzzles:
74 if "rounds" not in puzzle:
76 for rnd in puzzle["rounds"]:
81 return puzzles, rounds
83 def round_stat(rnd, puzzles):
84 #Counts puzzles, solved, list of puzzles for a given round
91 for puzzle in puzzles:
92 if "rounds" not in puzzle:
94 if rnd in puzzle["rounds"]:
95 if puzzle['type'] == 'meta':
97 if puzzle['status'] == 'solved':
101 if puzzle['status'] == 'solved':
103 solved_puzzles.append(puzzle)
105 unsolved_puzzles.append(puzzle)
106 solved_puzzles = sorted(solved_puzzles, key = lambda i: i['name'])
107 unsolved_puzzles = sorted(unsolved_puzzles, key = lambda i: i['name'])
108 rnd_puzzles = metas + unsolved_puzzles + solved_puzzles
109 return puzzle_count, solved_count, rnd_puzzles, meta_solved, len(metas)
113 def overview(puzzles, rounds):
114 #big board, main page. saves as index.html
115 start = ['<!DOCTYPE html>\n',
118 ' <meta charset="utf-8">\n',
119 ' <meta name="viewport" content="width=device-width, initial-scale=1">\n',
121 ' <link rel="stylesheet" href="overview.css">\n',
122 ' <script type="text/javascript">\n',
123 ' // Hide all elements with class="containerTab", except for the one that matches the clickable grid column\n',
124 ' function openTab(tabName) {\n',
126 ' x = document.getElementsByClassName("containerTab");\n',
127 ' for (i = 0; i < x.length; i++) {\n',
128 ' x[i].style.display = "none";\n',
130 ' document.getElementById(tabName).style.display = "block";\n',
134 ' <title>Hunt Overview</title>\n',
135 ' <script src="sorttable.js"></script>\n'
137 ' <div class="sidenav">\n'
138 ' <a href="index.html">Hunt Overview</a>'
139 ' <a href="all.html">All Puzzles</a>\n'
140 ' <a href="unsolved.html">Unsolved</a>\n'
141 ' <a href="solved.html">Solved</a>\n'
144 columns = [' <div class="row">\n']
148 puzzle_count, solved_count, rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)
149 if metas == meta_solved and metas > 0:
153 columns += [' <div class="column {}" onclick="openTab(\'b{}\');">\n'.format(status, i),
154 ' <p>{}</p>\n'.format(rnd),
155 ' <p>Puzzles: {}/{}</p>\n'.format(solved_count, puzzle_count),
156 ' <p>Metas: {}/{}</p>\n'.format(meta_solved, metas),
160 ' <div id="b{}" class="containerTab {}" style="display:none;">\n'.format(i, status),
161 ' <span onclick="this.parentElement.style.display=\'none\'" class="closebtn">x</span>\n',
162 ' <h2>{}</h2>\n'.format(link(website + "_".join(rnd.split()) + "_round.html", rnd)),
163 ' <table class="sortable">\n',
165 ' <th><u>Puzzle</u></th>\n',
166 ' <th><u>Answer</u></th>\n',
169 for puzzle in rnd_puzzles:
170 if puzzle['type'] == 'meta':
174 if puzzle['status'] == 'solved':
175 expanding += [' <tr class=\'solved\';>\n',
176 ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + ".html", puzzle['name']+meta)),
177 ' <td>{}</td>\n'.format(puzzle['solution']),
180 expanding += [' <tr class=\'unsolved\';>\n',
181 ' <td><b>{}</b></td>\n'.format(link(website + "_".join(puzzle['name'].split()) + ".html", puzzle['name']+meta)),
184 expanding.append(' </table>\n')
185 expanding.append(' </div>\n')
187 columns.append(' </div>\n')
188 end = ['</body>\n', '</html>\n']
189 html = start + expanding + columns + end
197 def round_overview(rnd, puzzles):
198 #inputs: round name, puzzles
200 #saves as (round name)_round.html, in case meta/round share names.
201 #underscores replace spaces for links.
202 rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)[2:]
203 if meta_solved == metas and metas > 0:
209 ' <link rel="stylesheet" href="individual.css">\n',
210 ' <title>Mystery Hunt 2022</title>\n',
211 ' <script src="sorttable.js"></script>\n',
213 ' <body class="{}">\n'.format(status),
214 ' <h1><b>{}</b></h1>\n'.format(rnd),
215 ' <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
217 ' <table class="center sortable">\n',
220 ' <th>Puzzle Title/Slack</th>\n',
221 ' <th>Puzzle Link</th>\n',
223 ' <th>Overview</th>\n',
224 ' <th>Answer</th>\n',
225 #' <th>Extra Links</th>\n',
231 for puzzle in rnd_puzzles:
232 if puzzle['type'] == 'meta':
236 slack_url = channel_url(puzzle['channel_id'])
238 if puzzle['status'] == 'solved':
239 puzzle_list += [ ' <tr>\n',
240 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
241 ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
242 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
243 ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
244 ' <td>{}</td>\n'.format(puzzle['solution']),
246 ' <td>{}</td>\n'.format("".join(puzzle['tags'])),
249 puzzle_list += [ ' <tr>\n',
250 ' <td><b>{}</b></td>\n'.format(elink(slack_url, puzzle['name']+meta)),
251 ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
252 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
253 ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
256 ' <td>{}</td>\n'.format(" ".join(puzzle['tags'])),
258 end = [' </tbody>\n',
263 html = start + puzzle_list + end
264 file = "{}_round.html".format('_'.join(rnd.split()))
271 def puzzle_overview(puzzle):
272 #overview page for individual puzzles. saves as (name of puzzle).html,
273 #with underscores rather than spaces
274 name = puzzle['name']
275 if puzzle['type'] == 'meta':
279 slack_url = channel_url(puzzle['channel_id'])
280 round_url = [link(website + "_".join(rnd.split()) + "_round.html", rnd) for rnd in puzzle['rounds']]
281 if puzzle['status'] == 'solved':
282 solution = puzzle['solution']
287 html = ['<!DOCTYPE html>\n',
290 ' <meta charset="utf-8">\n',
291 ' <meta name="viewport" content="width=device-width, initial-scale=1">\n',
292 ' <link rel="stylesheet" href="individual.css">\n',
293 ' <title>{}</title>\n'.format(name+meta),
294 ' <p>{}</p>'.format(link(website + 'index.html', 'Hunt Overview')),
297 '<body class="{}">\n'.format(status),
298 ' <h1>{}</h1>\n'.format(name+meta),
300 ' <table class="center">\n',
302 ' <td>{}</td>\n'.format(elink(slack_url, 'Channel')), #slack channel
303 ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
304 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
305 ' <td>Additional Resources</td>\n',
308 ' <table class="center">\n',
310 ' <td>Round(s): {}</td>\n'.format(" ".join(round_url)), #round page on our site
311 ' <td>Tags: {}</td>\n'.format(" ".join(puzzle['tags'])), #add tags
314 ' <td>Answer: {}</td>\n'.format(solution),
315 ' <td>State: {}</td>\n'.format(puzzle['status']),
322 underscored = "_".join(name.split())
323 file = "{}.html".format(underscored)
329 def puzzle_lists(puzzles, filt):
330 #filt is one of "All", "Solved", "Unsolved" and spits out the appropriate list of puzzles
331 #generates pages for all puzzles, solved puzzles, unsolved puzzles
332 #saves as all/solved/unsolved.html, has a sidebar to link to each other and to the big board
333 solved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] == 'solved']
334 unsolved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] != 'solved']
337 ' <link rel="stylesheet" href="overview.css">\n',
338 ' <title>Mystery Hunt 2022</title>\n',
339 ' <script src="sorttable.js"></script>\n',
341 ' <div class="sidenav">\n'
342 ' <a href="index.html">Hunt Overview</a>'
343 ' <a href="all.html">All Puzzles</a>\n'
344 ' <a href="unsolved.html">Unsolved</a>\n'
345 ' <a href="solved.html">Solved</a>\n'
348 ' <h1><b>{}</b></h1>\n'.format('{} Puzzles').format(filt),
349 ' <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
351 ' <table class="center sortable">\n',
354 ' <th>Puzzle Title/Slack</th>\n',
355 ' <th>Puzzle Link</th>\n',
357 ' <th>Overview</th>\n',
358 ' <th>Answer</th>\n',
359 ' <th>Round(s)</th>\n',
366 for puzzle in solved_puzzles:
367 if puzzle['type'] == 'meta':
371 slack_url = channel_url(puzzle['channel_id'])
372 round_url = link(website + "_".join(rnd.split()) + "_round.html", puzzle['rounds'][0])
373 #assuming one round per puzzle for now
375 solved_code += [' <tr>\n',
376 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
377 ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
378 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
379 ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
380 ' <td>{}</td>\n'.format(puzzle['solution']),
381 ' <td>{}</td>\n'.format(round_url),
382 ' <td>{}</td>\n'.format("".join(puzzle['tags'])),
384 for puzzle in unsolved_puzzles:
385 if puzzle['type'] == 'meta':
389 slack_url = channel_url(puzzle['channel_id'])
390 round_url = link(website + "_".join(rnd.split()) + "_round.html", puzzle['rounds'][0])
391 #assuming one round per puzzle for now
393 unsolved_code += [' <tr>\n',
394 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
395 ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
396 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
397 ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
399 ' <td>{}</td>\n'.format(round_url),
400 ' <td>{}</td>\n'.format("".join(puzzle['tags'])),
402 end = [' </tbody>\n',
410 for line in start + unsolved_code + solved_code + end:
413 elif filt == "Solved":
414 file2 = 'solved.html'
416 for line in start + solved_code + end:
419 elif filt == "Unsolved":
420 file3 = 'unsolved.html'
422 for line in start + unsolved_code + end:
429 puzzles, rounds = hunt_info(turb, hunt)
430 #I am not sure where these come from
431 overview(puzzles, rounds)
433 round_overview(rnd, puzzles)
434 for puzzle in puzzles:
435 puzzle_overview(puzzle)
436 puzzle_lists(puzzles, "All")
437 puzzle_lists(puzzles, "Solved")
438 puzzle_lists(puzzles, "Unsolved")