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
21 website = "https://halibut.cworth.org/"
22 #change this if we're using AWS or some other subdomain instead
24 def filename_from_name(name):
25 """Returns a string derived from name, but with all spaces and slashes
26 replaced with underscores, (for making a clean filename)"""
27 return re.sub(r'[ /]', '_', name)
29 def channel_url(channel_id):
30 """Given a channel ID, return the URL for that channel."""
32 return "https://halibutthatbass.slack.com/archives/{}".format(channel_id)
34 def find_hunt_for_hunt_id(table, hunt_id):
35 """Given a hunt ID find the database item for that hunt
37 Returns None if hunt ID is not found, otherwise a
38 dictionary with all fields from the hunt's row in the table,
39 (channel_id, active, hunt_id, name, url, sheet_url, etc.).
41 response = table.get_item(
44 'SK': 'hunt-{}'.format(hunt_id)
47 if 'Item' in response:
48 return response['Item']
52 def hunt_puzzles_for_hunt_id(table, hunt_id):
53 """Return all puzzles that belong to the given hunt_id"""
55 response = table.query(
56 KeyConditionExpression=(
57 Key('hunt_id').eq(hunt_id) &
58 Key('SK').begins_with('puzzle-')
62 return response['Items']
65 #shortcutting function for creating html links
66 #opens in a new tab for external links
67 return '<a href="{}" target="_blank" rel="noreferrer noopener">{}</a>'.format(lin, text)
70 #internal links, doesn't open new tab
71 return '<a href="{}">{}</a>'.format(lin, text)
73 def hunt_info(table, hunt_id):
75 Retrieves list of rounds, puzzles for the given hunt
78 hunt = find_hunt_for_hunt_id(table, hunt_id)
81 channel_id = hunt["channel_id"]
83 puzzles = hunt_puzzles_for_hunt_id(table, hunt_id)
86 for puzzle in puzzles:
87 if "rounds" not in puzzle:
89 for rnd in puzzle["rounds"]:
94 return puzzles, rounds
96 def round_stat(rnd, puzzles):
97 #Counts puzzles, solved, list of puzzles for a given round
101 unsolved_puzzles = []
104 for puzzle in puzzles:
105 if "rounds" not in puzzle:
107 if rnd in puzzle["rounds"]:
108 if puzzle['type'] == 'meta':
110 if puzzle['status'] == 'solved':
114 if puzzle['status'] == 'solved':
116 solved_puzzles.append(puzzle)
118 unsolved_puzzles.append(puzzle)
119 solved_puzzles = sorted(solved_puzzles, key = lambda i: i['name'])
120 unsolved_puzzles = sorted(unsolved_puzzles, key = lambda i: i['name'])
121 rnd_puzzles = metas + unsolved_puzzles + solved_puzzles
122 return puzzle_count, solved_count, rnd_puzzles, meta_solved, len(metas)
126 def overview(puzzles, rounds):
127 #big board, main page. saves as index.html
128 start = ['<!DOCTYPE html>\n',
131 ' <meta charset="utf-8">\n',
132 ' <meta name="viewport" content="width=device-width, initial-scale=1">\n',
134 ' <link rel="stylesheet" href="/overview.css">\n',
135 ' <script type="text/javascript">\n',
136 ' // Hide all elements with class="containerTab", except for the one that matches the clickable grid column\n',
137 ' function openTab(tabName) {\n',
139 ' x = document.getElementsByClassName("containerTab");\n',
140 ' for (i = 0; i < x.length; i++) {\n',
141 ' x[i].style.display = "none";\n',
143 ' document.getElementById(tabName).style.display = "block";\n',
147 ' <title>Hunt Overview</title>\n',
148 ' <script src="/sorttable.js"></script>\n'
150 ' <div class="sidenav">\n'
151 ' <a href="index.html">Hunt Overview</a>'
152 ' <a href="all.html">All Puzzles</a>\n'
153 ' <a href="unsolved.html">Unsolved</a>\n'
154 ' <a href="solved.html">Solved</a>\n'
157 columns = [' <div class="row">\n']
161 puzzle_count, solved_count, rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)
162 if metas == meta_solved and metas > 0:
166 columns += [' <div class="column {}" onclick="openTab(\'b{}\');">\n'.format(status, i),
167 ' <p>{}</p>\n'.format(rnd),
168 ' <p>Puzzles: {}/{}</p>\n'.format(solved_count, puzzle_count),
169 ' <p>Metas: {}/{}</p>\n'.format(meta_solved, metas),
173 ' <div id="b{}" class="containerTab {}" style="display:none;">\n'.format(i, status),
174 ' <span onclick="this.parentElement.style.display=\'none\'" class="closebtn">x</span>\n',
175 ' <h2>{}</h2>\n'.format(link(website + filename_from_name(rnd) + "_round.html", rnd)),
176 ' <table class="sortable">\n',
178 ' <th><u>Puzzle</u></th>\n',
179 ' <th><u>Answer</u></th>\n',
182 for puzzle in rnd_puzzles:
183 if puzzle['type'] == 'meta':
187 if puzzle['status'] == 'solved':
188 expanding += [' <tr class=\'solved\';>\n',
189 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
190 ' <td>{}</td>\n'.format(puzzle['solution']),
193 expanding += [' <tr class=\'unsolved\';>\n',
194 ' <td><b>{}</b></td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
197 expanding.append(' </table>\n')
198 expanding.append(' </div>\n')
200 columns.append(' </div>\n')
201 end = ['</body>\n', '</html>\n']
202 html = start + expanding + columns + end
210 def round_overview(rnd, puzzles):
211 #inputs: round name, puzzles
213 #saves as (round name)_round.html, in case meta/round share names.
214 #underscores replace spaces for links.
215 rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)[2:]
216 if meta_solved == metas and metas > 0:
222 ' <link rel="stylesheet" href="/individual.css">\n',
223 ' <title>Mystery Hunt 2022</title>\n',
224 ' <script src="/sorttable.js"></script>\n',
226 ' <body class="{}">\n'.format(status),
227 ' <h1><b>{}</b></h1>\n'.format(rnd),
228 ' <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
230 ' <table class="center sortable">\n',
233 ' <th>Puzzle Title/Slack</th>\n',
234 ' <th>Puzzle Link</th>\n',
236 ' <th>Overview</th>\n',
237 ' <th>Answer</th>\n',
238 #' <th>Extra Links</th>\n',
244 for puzzle in rnd_puzzles:
245 if puzzle['type'] == 'meta':
249 slack_url = channel_url(puzzle['channel_id'])
251 if puzzle['status'] == 'solved':
252 puzzle_list += [ ' <tr>\n',
253 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
254 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
255 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
256 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
257 ' <td>{}</td>\n'.format(puzzle['solution']),
259 ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
262 puzzle_list += [ ' <tr>\n',
263 ' <td><b>{}</b></td>\n'.format(elink(slack_url, puzzle['name']+meta)),
264 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
265 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
266 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
269 ' <td>{}</td>\n'.format(" ".join(puzzle.get('tags',[]))),
271 end = [' </tbody>\n',
276 html = start + puzzle_list + end
277 file = "{}_round.html".format(filename_from_name(rnd))
284 def puzzle_overview(puzzle):
285 #overview page for individual puzzles. saves as (name of puzzle).html,
286 #with underscores rather than spaces
287 name = puzzle['name']
288 if puzzle['type'] == 'meta':
292 slack_url = channel_url(puzzle['channel_id'])
293 if 'rounds' in puzzle:
294 round_url = [link(website + filename_from_name(rnd) + "_round.html", rnd) for rnd in puzzle['rounds']]
297 if puzzle['status'] == 'solved':
298 solution = puzzle['solution']
303 html = ['<!DOCTYPE html>\n',
306 ' <meta charset="utf-8">\n',
307 ' <meta name="viewport" content="width=device-width, initial-scale=1">\n',
308 ' <link rel="stylesheet" href="/individual.css">\n',
309 ' <title>{}</title>\n'.format(name+meta),
310 ' <p>{}</p>'.format(link(website + 'index.html', 'Hunt Overview')),
313 '<body class="{}">\n'.format(status),
314 ' <h1>{}</h1>\n'.format(name+meta),
316 ' <table class="center">\n',
318 ' <td>{}</td>\n'.format(elink(slack_url, 'Channel')), #slack channel
319 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
320 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
321 ' <td>Additional Resources</td>\n',
324 ' <table class="center">\n',
326 ' <td>Round(s): {}</td>\n'.format(" ".join(round_url)), #round page on our site
327 ' <td>Tags: {}</td>\n'.format(" ".join(puzzle.get('tags',[]))), #add tags
330 ' <td>Answer: {}</td>\n'.format(solution),
331 ' <td>State: {}</td>\n'.format(puzzle['status']),
338 file = "{}.html".format(filename_from_name(name))
344 def puzzle_lists(puzzles, filt):
345 #filt is one of "All", "Solved", "Unsolved" and spits out the appropriate list of puzzles
346 #generates pages for all puzzles, solved puzzles, unsolved puzzles
347 #saves as all/solved/unsolved.html, has a sidebar to link to each other and to the big board
348 solved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] == 'solved']
349 unsolved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] != 'solved']
352 ' <link rel="stylesheet" href="/overview.css">\n',
353 ' <title>Mystery Hunt 2022</title>\n',
354 ' <script src="/sorttable.js"></script>\n',
356 ' <div class="sidenav">\n'
357 ' <a href="index.html">Hunt Overview</a>'
358 ' <a href="all.html">All Puzzles</a>\n'
359 ' <a href="unsolved.html">Unsolved</a>\n'
360 ' <a href="solved.html">Solved</a>\n'
363 ' <h1><b>{}</b></h1>\n'.format('{} Puzzles').format(filt),
364 ' <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
366 ' <table class="center sortable">\n',
369 ' <th>Puzzle Title/Slack</th>\n',
370 ' <th>Puzzle Link</th>\n',
372 ' <th>Overview</th>\n',
373 ' <th>Answer</th>\n',
374 ' <th>Round(s)</th>\n',
381 for puzzle in solved_puzzles:
382 if puzzle['type'] == 'meta':
386 slack_url = channel_url(puzzle['channel_id'])
387 if 'rounds' in puzzle:
388 round_url = link(website + filename_from_name(rnd) + "_round.html", puzzle['rounds'][0])
391 #assuming one round per puzzle for now
393 solved_code += [' <tr>\n',
394 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
395 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
396 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
397 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
398 ' <td>{}</td>\n'.format(puzzle['solution']),
399 ' <td>{}</td>\n'.format(round_url),
400 ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
402 for puzzle in unsolved_puzzles:
403 if puzzle['type'] == 'meta':
407 slack_url = channel_url(puzzle['channel_id'])
408 if 'rounds' in puzzle:
409 round_url = link(website + filename_from_name(rnd) + "_round.html", puzzle['rounds'][0])
412 #assuming one round per puzzle for now
414 unsolved_code += [' <tr>\n',
415 ' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
416 ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
417 ' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
418 ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
420 ' <td>{}</td>\n'.format(round_url),
421 ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
423 end = [' </tbody>\n',
431 for line in start + unsolved_code + solved_code + end:
434 elif filt == "Solved":
435 file2 = 'solved.html'
437 for line in start + solved_code + end:
440 elif filt == "Unsolved":
441 file3 = 'unsolved.html'
443 for line in start + unsolved_code + end:
448 # Initialize AWS resources to talk to database
449 db = boto3.resource('dynamodb')
450 table = db.Table("turbot")
451 puzzles, rounds = hunt_info(table, "mh2021")
453 overview(puzzles, rounds)
455 round_overview(rnd, puzzles)
456 for puzzle in puzzles:
457 puzzle_overview(puzzle)
458 puzzle_lists(puzzles, "All")
459 puzzle_lists(puzzles, "Solved")
460 puzzle_lists(puzzles, "Unsolved")