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
18 from boto3.dynamodb.conditions import Key
23 WEBROOT = "/srv/halibut.cworth.org/www"
25 website = "https://halibut.cworth.org/"
26 #change this if we're using AWS or some other subdomain instead
28 def hunt_file(hunt, name):
29 """Return a path file 'name' within the given hunt.
31 This will be withing WEBROOT and in a hunt-specific path."""
33 return "{}/{}/{}".format(WEBROOT, hunt['channel_id'], name)
35 def filename_from_name(name):
36 """Returns a string derived from name, but with all spaces and slashes
37 replaced with underscores, (for making a clean filename)"""
38 return re.sub(r'[ /]', '_', name)
40 def channel_url(channel_id):
41 """Given a channel ID, return the URL for that channel."""
43 return "https://halibutthatbass.slack.com/archives/{}".format(channel_id)
45 def find_hunt_for_hunt_id(table, hunt_id):
46 """Given a hunt ID find the database item for that hunt
48 Returns None if hunt ID is not found, otherwise a
49 dictionary with all fields from the hunt's row in the table,
50 (channel_id, active, hunt_id, name, url, sheet_url, etc.).
52 response = table.get_item(
55 'SK': 'hunt-{}'.format(hunt_id)
58 if 'Item' in response:
59 return response['Item']
63 def hunt_puzzles_for_hunt_id(table, hunt_id):
64 """Return all puzzles that belong to the given hunt_id"""
66 response = table.query(
67 KeyConditionExpression=(
68 Key('hunt_id').eq(hunt_id) &
69 Key('SK').begins_with('puzzle-')
73 return response['Items']
76 #shortcutting function for creating html links
77 #opens in a new tab for external links
78 return '<a href="{}" target="_blank" rel="noreferrer noopener">{}</a>'.format(lin, text)
81 #internal links, doesn't open new tab
82 return '<a href="{}">{}</a>'.format(lin, text)
84 def hunt_info(table, hunt_id):
86 Retrieves list of rounds, puzzles for the given hunt
89 hunt = find_hunt_for_hunt_id(table, hunt_id)
92 channel_id = hunt["channel_id"]
94 puzzles = hunt_puzzles_for_hunt_id(table, hunt_id)
97 for puzzle in puzzles:
98 if "rounds" not in puzzle:
100 for rnd in puzzle["rounds"]:
102 rounds = list(rounds)
105 return hunt, puzzles, rounds
107 def round_stat(rnd, puzzles):
108 #Counts puzzles, solved, list of puzzles for a given round
112 unsolved_puzzles = []
115 for puzzle in puzzles:
116 if "rounds" not in puzzle:
118 if rnd in puzzle["rounds"]:
119 if puzzle['type'] == 'meta':
121 if puzzle['status'] == 'solved':
125 if puzzle['status'] == 'solved':
127 solved_puzzles.append(puzzle)
129 unsolved_puzzles.append(puzzle)
130 solved_puzzles = sorted(solved_puzzles, key = lambda i: i['name'])
131 unsolved_puzzles = sorted(unsolved_puzzles, key = lambda i: i['name'])
132 rnd_puzzles = metas + unsolved_puzzles + solved_puzzles
133 return puzzle_count, solved_count, rnd_puzzles, meta_solved, len(metas)
137 def overview(hunt, puzzles, rounds):
138 #big board, main page. saves as index.html
139 start = ['<!DOCTYPE html>\n',
142 ' <meta charset="utf-8">\n',
143 ' <meta name="viewport" content="width=device-width, initial-scale=1">\n',
145 ' <link rel="stylesheet" href="/overview.css">\n',
146 ' <script type="text/javascript">\n',
147 ' // Hide all elements with class="containerTab", except for the one that matches the clickable grid column\n',
148 ' function openTab(tabName) {\n',
150 ' x = document.getElementsByClassName("containerTab");\n',
151 ' for (i = 0; i < x.length; i++) {\n',
152 ' x[i].style.display = "none";\n',
154 ' document.getElementById(tabName).style.display = "block";\n',
158 ' <title>Hunt Overview</title>\n',
159 ' <script src="/sorttable.js"></script>\n'
161 ' <div class="sidenav">\n'
162 ' <a href="index.html">Hunt Overview</a>'
163 ' <a href="all.html">All Puzzles</a>\n'
164 ' <a href="unsolved.html">Unsolved</a>\n'
165 ' <a href="solved.html">Solved</a>\n'
168 columns = [' <div class="row">\n']
172 puzzle_count, solved_count, rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)
173 if metas == meta_solved and metas > 0:
177 columns += [' <div class="column {}" onclick="openTab(\'b{}\');">\n'.format(status, i),
178 ' <p>{}</p>\n'.format(rnd),
179 ' <p>Puzzles: {}/{}</p>\n'.format(solved_count, puzzle_count),
180 ' <p>Metas: {}/{}</p>\n'.format(meta_solved, metas),
184 ' <div id="b{}" class="containerTab {}" style="display:none;">\n'.format(i, status),
185 ' <span onclick="this.parentElement.style.display=\'none\'" class="closebtn">x</span>\n',
186 ' <h2>{}</h2>\n'.format(link(website + filename_from_name(rnd) + "_round.html", rnd)),
187 ' <table class="sortable">\n',
189 ' <th><u>Puzzle</u></th>\n',
190 ' <th><u>Answer</u></th>\n',
193 for puzzle in rnd_puzzles:
194 if puzzle['type'] == 'meta':
198 if puzzle['status'] == 'solved':
199 expanding += [' <tr class=\'solved\';>\n',
200 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
201 ' <td>{}</td>\n'.format(puzzle['solution']),
204 expanding += [' <tr class=\'unsolved\';>\n',
205 ' <td><b>{}</b></td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
208 expanding.append(' </table>\n')
209 expanding.append(' </div>\n')
211 columns.append(' </div>\n')
212 end = ['</body>\n', '</html>\n']
213 html = start + expanding + columns + end
214 file = hunt_file(hunt, "index.html")
221 def round_overview(hunt, rnd, puzzles):
222 #inputs: round name, puzzles
224 #saves as (round name)_round.html, in case meta/round share names.
225 #underscores replace spaces for links.
226 rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)[2:]
227 if meta_solved == metas and metas > 0:
233 ' <link rel="stylesheet" href="/individual.css">\n',
234 ' <title>Mystery Hunt 2022</title>\n',
235 ' <script src="/sorttable.js"></script>\n',
237 ' <body class="{}">\n'.format(status),
238 ' <h1><b>{}</b></h1>\n'.format(rnd),
239 ' <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
241 ' <table class="center sortable">\n',
244 ' <th>Puzzle Title/Slack</th>\n',
245 ' <th>Puzzle Link</th>\n',
247 ' <th>Overview</th>\n',
248 ' <th>Answer</th>\n',
249 #' <th>Extra Links</th>\n',
255 for puzzle in rnd_puzzles:
256 if puzzle['type'] == 'meta':
260 slack_url = channel_url(puzzle['channel_id'])
262 if puzzle['status'] == 'solved':
263 puzzle_list += [ ' <tr>\n',
264 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
265 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
266 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
267 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
268 ' <td>{}</td>\n'.format(puzzle['solution']),
270 ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
273 puzzle_list += [ ' <tr>\n',
274 ' <td><b>{}</b></td>\n'.format(elink(slack_url, puzzle['name']+meta)),
275 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
276 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
277 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
280 ' <td>{}</td>\n'.format(" ".join(puzzle.get('tags',[]))),
282 end = [' </tbody>\n',
287 html = start + puzzle_list + end
288 file = hunt_file(hunt, "{}_round.html".format(filename_from_name(rnd)))
295 def puzzle_overview(hunt, puzzle):
296 #overview page for individual puzzles. saves as (name of puzzle).html,
297 #with underscores rather than spaces
298 name = puzzle['name']
299 if puzzle['type'] == 'meta':
303 slack_url = channel_url(puzzle['channel_id'])
304 if 'rounds' in puzzle:
305 round_url = [link(website + filename_from_name(rnd) + "_round.html", rnd) for rnd in puzzle['rounds']]
308 if puzzle['status'] == 'solved':
309 solution = puzzle['solution']
314 html = ['<!DOCTYPE html>\n',
317 ' <meta charset="utf-8">\n',
318 ' <meta name="viewport" content="width=device-width, initial-scale=1">\n',
319 ' <link rel="stylesheet" href="/individual.css">\n',
320 ' <title>{}</title>\n'.format(name+meta),
321 ' <p>{}</p>'.format(link(website + 'index.html', 'Hunt Overview')),
324 '<body class="{}">\n'.format(status),
325 ' <h1>{}</h1>\n'.format(name+meta),
327 ' <table class="center">\n',
329 ' <td>{}</td>\n'.format(elink(slack_url, 'Channel')), #slack channel
330 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
331 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
332 ' <td>Additional Resources</td>\n',
335 ' <table class="center">\n',
337 ' <td>Round(s): {}</td>\n'.format(" ".join(round_url)), #round page on our site
338 ' <td>Tags: {}</td>\n'.format(" ".join(puzzle.get('tags',[]))), #add tags
341 ' <td>Answer: {}</td>\n'.format(solution),
342 ' <td>State: {}</td>\n'.format(puzzle['status']),
349 file = hunt_file(hunt, "{}.html".format(filename_from_name(name)))
355 def puzzle_lists(hunt, puzzles, filt):
356 #filt is one of "All", "Solved", "Unsolved" and spits out the appropriate list of puzzles
357 #generates pages for all puzzles, solved puzzles, unsolved puzzles
358 #saves as all/solved/unsolved.html, has a sidebar to link to each other and to the big board
359 solved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] == 'solved']
360 unsolved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] != 'solved']
363 ' <link rel="stylesheet" href="/overview.css">\n',
364 ' <title>Mystery Hunt 2022</title>\n',
365 ' <script src="/sorttable.js"></script>\n',
367 ' <div class="sidenav">\n'
368 ' <a href="index.html">Hunt Overview</a>'
369 ' <a href="all.html">All Puzzles</a>\n'
370 ' <a href="unsolved.html">Unsolved</a>\n'
371 ' <a href="solved.html">Solved</a>\n'
374 ' <h1><b>{}</b></h1>\n'.format('{} Puzzles').format(filt),
375 ' <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
377 ' <table class="center sortable">\n',
380 ' <th>Puzzle Title/Slack</th>\n',
381 ' <th>Puzzle Link</th>\n',
383 ' <th>Overview</th>\n',
384 ' <th>Answer</th>\n',
385 ' <th>Round(s)</th>\n',
392 for puzzle in solved_puzzles:
393 if puzzle['type'] == 'meta':
397 slack_url = channel_url(puzzle['channel_id'])
398 if 'rounds' in puzzle:
399 round_url = link(website + filename_from_name(puzzle['rounds'][0]) + "_round.html", puzzle['rounds'][0])
402 #assuming one round per puzzle for now
404 solved_code += [' <tr>\n',
405 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
406 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
407 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
408 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
409 ' <td>{}</td>\n'.format(puzzle['solution']),
410 ' <td>{}</td>\n'.format(round_url),
411 ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
413 for puzzle in unsolved_puzzles:
414 if puzzle['type'] == 'meta':
418 slack_url = channel_url(puzzle['channel_id'])
419 if 'rounds' in puzzle:
420 round_url = link(website + filename_from_name(puzzle['rounds'][0]) + "_round.html", puzzle['rounds'][0])
423 #assuming one round per puzzle for now
425 unsolved_code += [' <tr>\n',
426 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
427 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
428 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
429 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
431 ' <td>{}</td>\n'.format(round_url),
432 ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
434 end = [' </tbody>\n',
440 file1 = hunt_file(hunt, 'all.html')
442 for line in start + unsolved_code + solved_code + end:
445 elif filt == "Solved":
446 file2 = hunt_file(hunt, 'solved.html')
448 for line in start + solved_code + end:
451 elif filt == "Unsolved":
452 file3 = hunt_file(hunt, 'unsolved.html')
454 for line in start + unsolved_code + end:
459 def generate_for_hunt_id(table, hunt_id):
460 hunt, puzzles, rounds = hunt_info(table, hunt_id)
462 # Create a directory for the hunt in the WEBROOT
463 root = hunt_file(hunt, "")
466 except FileExistsError:
467 # We're happy as a clam if the directory already exists
470 overview(hunt, puzzles, rounds)
472 round_overview(hunt, rnd, puzzles)
473 for puzzle in puzzles:
474 puzzle_overview(hunt, puzzle)
475 puzzle_lists(hunt, puzzles, "All")
476 puzzle_lists(hunt, puzzles, "Solved")
477 puzzle_lists(hunt, puzzles, "Unsolved")
479 # Initialize AWS resources to talk to the database
480 db = boto3.resource('dynamodb')
481 table = db.Table("turbot")
484 print("Usage: {} hunt_id [...]")
486 print("Generates pages (under {}) ".format(WEBROOT))
487 print("for the specified hunt_id(s).")
489 if len(sys.argv) < 2:
493 for hunt_id in sys.argv[1:]:
494 generate_for_hunt_id(table, hunt_id)