]> git.cworth.org Git - turbot-web/blob - html_generator.py
Generate all files in a hunt-specific directory under a target WEBROOT
[turbot-web] / html_generator.py
1 # -*- coding: utf-8 -*-
2 """
3 Created on Thu Jan  6 23:35:23 2022
4
5 @author: Avram Gottschlich
6 """
7 """
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
10
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,
13 that would be great
14
15 Requires sorttable.js, which should be included
16 """
17 import boto3
18 from boto3.dynamodb.conditions import Key
19 import os
20 import re
21
22 WEBROOT = "/srv/halibut.cworth.org/www"
23
24 website = "https://halibut.cworth.org/"
25 #change this if we're using AWS or some other subdomain instead
26
27 def hunt_file(hunt, name):
28     """Return a path file 'name' within the given hunt.
29
30     This will be withing WEBROOT and in a hunt-specific path."""
31
32     return "{}/{}/{}".format(WEBROOT, hunt['channel_id'], name)
33
34 def filename_from_name(name):
35     """Returns a string derived from name, but with all spaces and slashes
36        replaced with underscores, (for making a clean filename)"""
37     return re.sub(r'[ /]', '_', name)
38
39 def channel_url(channel_id):
40     """Given a channel ID, return the URL for that channel."""
41
42     return "https://halibutthatbass.slack.com/archives/{}".format(channel_id)
43
44 def find_hunt_for_hunt_id(table, hunt_id):
45     """Given a hunt ID find the database item for that hunt
46
47     Returns None if hunt ID is not found, otherwise a
48     dictionary with all fields from the hunt's row in the table,
49     (channel_id, active, hunt_id, name, url, sheet_url, etc.).
50     """
51     response = table.get_item(
52         Key={
53             'hunt_id': hunt_id,
54             'SK': 'hunt-{}'.format(hunt_id)
55             })
56
57     if 'Item' in response:
58         return response['Item']
59     else:
60         return None
61
62 def hunt_puzzles_for_hunt_id(table, hunt_id):
63     """Return all puzzles that belong to the given hunt_id"""
64
65     response = table.query(
66         KeyConditionExpression=(
67             Key('hunt_id').eq(hunt_id) &
68             Key('SK').begins_with('puzzle-')
69             )
70         )
71
72     return response['Items']
73
74 def elink(lin, text):
75     #shortcutting function for creating html links
76     #opens in a new tab for external links
77     return '<a href="{}" target="_blank" rel="noreferrer noopener">{}</a>'.format(lin, text)
78
79 def link(lin, text):
80     #internal links, doesn't open new tab
81     return '<a href="{}">{}</a>'.format(lin, text)
82
83 def hunt_info(table, hunt_id):
84     """
85     Retrieves list of rounds, puzzles for the given hunt
86     """
87
88     hunt = find_hunt_for_hunt_id(table, hunt_id)
89
90     name = hunt["name"]
91     channel_id = hunt["channel_id"]
92
93     puzzles = hunt_puzzles_for_hunt_id(table, hunt_id)
94
95     rounds = set()
96     for puzzle in puzzles:
97         if "rounds" not in puzzle:
98             continue
99         for rnd in puzzle["rounds"]:
100             rounds.add(rnd)
101     rounds = list(rounds)
102     rounds.sort()
103
104     return hunt, puzzles, rounds
105
106 def round_stat(rnd, puzzles):
107     #Counts puzzles, solved, list of puzzles for a given round
108     puzzle_count = 0
109     solved_count = 0
110     solved_puzzles = []
111     unsolved_puzzles = []
112     metas = []
113     meta_solved = 0
114     for puzzle in puzzles:
115         if "rounds" not in puzzle:
116             continue
117         if rnd in puzzle["rounds"]:
118             if puzzle['type'] == 'meta':
119                 metas.append(puzzle)
120                 if puzzle['status'] == 'solved':
121                     meta_solved += 1
122             else:
123                 puzzle_count += 1
124                 if puzzle['status'] == 'solved':
125                     solved_count += 1
126                     solved_puzzles.append(puzzle)
127                 else:
128                     unsolved_puzzles.append(puzzle)
129     solved_puzzles = sorted(solved_puzzles, key = lambda i: i['name'])
130     unsolved_puzzles = sorted(unsolved_puzzles, key = lambda i: i['name'])
131     rnd_puzzles = metas + unsolved_puzzles + solved_puzzles
132     return puzzle_count, solved_count, rnd_puzzles, meta_solved, len(metas)
133
134
135
136 def overview(hunt, puzzles, rounds):
137     #big board, main page. saves as index.html
138     start = ['<!DOCTYPE html>\n',
139      '<html>\n',
140      '<head>\n',
141      '  <meta charset="utf-8">\n',
142      '  <meta name="viewport" content="width=device-width, initial-scale=1">\n',
143      '\n',
144      '  <link rel="stylesheet" href="/overview.css">\n',
145      '  <script type="text/javascript">\n',
146      '    // Hide all elements with class="containerTab", except for the one that matches the clickable grid column\n',
147      '    function openTab(tabName) {\n',
148      '      var i, x;\n',
149      '      x = document.getElementsByClassName("containerTab");\n',
150      '      for (i = 0; i < x.length; i++) {\n',
151      '        x[i].style.display = "none";\n',
152      '      }\n',
153      '      document.getElementById(tabName).style.display = "block";\n',
154      '    }\n',
155      '  </script>\n',
156      '\n',
157      '  <title>Hunt Overview</title>\n',
158      '  <script src="/sorttable.js"></script>\n'
159      '</head>\n',
160      '    <div class="sidenav">\n'
161      '      <a href="index.html">Hunt Overview</a>'
162      '      <a href="all.html">All Puzzles</a>\n'
163      '      <a href="unsolved.html">Unsolved</a>\n'
164      '      <a href="solved.html">Solved</a>\n'
165      '    </div>\n'
166      '<body>\n',]
167     columns = ['  <div class="row">\n']
168     expanding = []
169     i = 1
170     for rnd in rounds:
171         puzzle_count, solved_count, rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)
172         if metas == meta_solved and metas > 0:
173             status = 'solved'
174         else:
175             status = 'unsolved'
176         columns += ['    <div class="column {}" onclick="openTab(\'b{}\');">\n'.format(status, i),
177         '      <p>{}</p>\n'.format(rnd),
178         '      <p>Puzzles: {}/{}</p>\n'.format(solved_count, puzzle_count),
179         '      <p>Metas: {}/{}</p>\n'.format(meta_solved, metas),
180         '    </div>\n']
181
182         expanding += [
183         '  <div id="b{}" class="containerTab {}" style="display:none;">\n'.format(i, status),
184         '    <span onclick="this.parentElement.style.display=\'none\'" class="closebtn">x</span>\n',
185         '    <h2>{}</h2>\n'.format(link(website + filename_from_name(rnd) + "_round.html", rnd)),
186         '    <table class="sortable">\n',
187         '      <tr>\n',
188         '        <th><u>Puzzle</u></th>\n',
189         '        <th><u>Answer</u></th>\n',
190         '      </tr>\n',]
191
192         for puzzle in rnd_puzzles:
193             if puzzle['type'] == 'meta':
194                 meta = ' [META]'
195             else:
196                 meta = ''
197             if puzzle['status'] == 'solved':
198                 expanding += ['      <tr class=\'solved\';>\n',
199                 '        <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
200                 '        <td>{}</td>\n'.format(puzzle['solution']),
201                 '      </tr>\n']
202             else:
203                 expanding += ['      <tr class=\'unsolved\';>\n',
204                 '        <td><b>{}</b></td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
205                 '        <td></td>\n',
206                 '      </tr>\n']
207         expanding.append('    </table>\n')
208         expanding.append('  </div>\n')
209         i += 1
210     columns.append('  </div>\n')
211     end = ['</body>\n', '</html>\n']
212     html = start + expanding + columns + end
213     file = hunt_file(hunt, "index.html")
214     f = open(file, "w")
215     for line in html:
216         f.write(line)
217     f.close()
218     return None
219
220 def round_overview(hunt, rnd, puzzles):
221     #inputs: round name, puzzles
222     #round overview page
223     #saves as (round name)_round.html, in case meta/round share names.
224     #underscores replace spaces for links.
225     rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)[2:]
226     if meta_solved == metas and metas > 0:
227         status = 'solved'
228     else:
229         status = 'unsolved'
230     start = ['<html>\n',
231      '    <head>\n',
232      '        <link rel="stylesheet" href="/individual.css">\n',
233      '        <title>Mystery Hunt 2022</title>\n',
234      '        <script src="/sorttable.js"></script>\n',
235      '    </head>\n',
236      '    <body class="{}">\n'.format(status),
237      '        <h1><b>{}</b></h1>\n'.format(rnd),
238      '        <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
239      '        <div>\n',
240      '            <table class="center sortable">\n',
241      '                <thead>\n',
242      '                    <tr>\n',
243      '                        <th>Puzzle Title/Slack</th>\n',
244      '                        <th>Puzzle Link</th>\n',
245      '                        <th>Sheet</th>\n',
246      '                        <th>Overview</th>\n',
247      '                        <th>Answer</th>\n',
248      #'                        <th>Extra Links</th>\n',
249      '                        <th>Tags</th>\n',
250      '                    </tr>\n',
251      '                </thead>\n',
252      '                <tbody>\n']
253     puzzle_list = []
254     for puzzle in rnd_puzzles:
255         if puzzle['type'] == 'meta':
256             meta = ' [META]'
257         else:
258             meta = ''
259         slack_url = channel_url(puzzle['channel_id'])
260
261         if puzzle['status'] == 'solved':
262             puzzle_list += [ '                    <tr>\n',
263              '                        <td>{}</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')),
267              '                        <td>{}</td>\n'.format(puzzle['solution']),
268             # '                        <td></td>\n',
269              '                        <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
270              '                    </tr>\n']
271         else:
272             puzzle_list += [ '                    <tr>\n',
273              '                        <td><b>{}</b></td>\n'.format(elink(slack_url, puzzle['name']+meta)),
274              '                        <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
275              '                        <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
276              '                        <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
277              '                        <td></td>\n',
278             # '                        <td></td>\n',
279              '                        <td>{}</td>\n'.format(" ".join(puzzle.get('tags',[]))),
280              '                    </tr>\n']
281     end = ['                </tbody>\n',
282     '            </table>\n',
283     '        </div>\n',
284     '    </body>\n',
285     '</html>\n']
286     html = start + puzzle_list + end
287     file = hunt_file(hunt, "{}_round.html".format(filename_from_name(rnd)))
288     f = open(file, "w")
289     for line in html:
290         f.write(line)
291     f.close()
292     return None
293
294 def puzzle_overview(hunt, puzzle):
295     #overview page for individual puzzles. saves as (name of puzzle).html,
296     #with underscores rather than spaces
297     name = puzzle['name']
298     if puzzle['type'] == 'meta':
299         meta = ' [META]'
300     else:
301         meta = ''
302     slack_url = channel_url(puzzle['channel_id'])
303     if 'rounds' in puzzle:
304         round_url = [link(website + filename_from_name(rnd) + "_round.html", rnd) for rnd in puzzle['rounds']]
305     else:
306         round_url = ''
307     if puzzle['status'] == 'solved':
308         solution = puzzle['solution']
309         status = 'solved'
310     else:
311         solution = ""
312         status = 'unsolved'
313     html = ['<!DOCTYPE html>\n',
314      '<html>\n',
315      '<head>\n',
316      '    <meta charset="utf-8">\n',
317      '    <meta name="viewport" content="width=device-width, initial-scale=1">\n',
318      '    <link rel="stylesheet" href="/individual.css">\n',
319      '    <title>{}</title>\n'.format(name+meta),
320      '    <p>{}</p>'.format(link(website + 'index.html', 'Hunt Overview')),
321      '\n',
322      '</head>\n',
323      '<body class="{}">\n'.format(status),
324      '    <h1>{}</h1>\n'.format(name+meta),
325      '    <div>\n',
326      '        <table class="center">\n',
327      '            <tr>\n',
328      '                <td>{}</td>\n'.format(elink(slack_url, 'Channel')), #slack channel
329      '                <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
330      '                <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
331      '                <td>Additional Resources</td>\n',
332      '            </tr>\n',
333      '        </table>\n',
334      '        <table class="center">\n',
335      '            <tr>\n',
336      '                <td>Round(s): {}</td>\n'.format(" ".join(round_url)), #round page on our site
337      '                <td>Tags: {}</td>\n'.format(" ".join(puzzle.get('tags',[]))), #add tags
338      '            </tr>\n',
339      '            <tr>\n',
340      '                <td>Answer: {}</td>\n'.format(solution),
341      '                <td>State: {}</td>\n'.format(puzzle['status']),
342      '            </tr>\n',
343      '        </table>\n',
344      '    </div>\n',
345      '\n',
346      '</body>\n',
347      '</html>\n']
348     file = hunt_file(hunt, "{}.html".format(filename_from_name(name)))
349     f = open(file, "w")
350     for line in html:
351         f.write(line)
352     return None
353
354 def puzzle_lists(hunt, puzzles, filt):
355     #filt is one of "All", "Solved", "Unsolved" and spits out the appropriate list of puzzles
356     #generates pages for all puzzles, solved puzzles, unsolved puzzles
357     #saves as all/solved/unsolved.html, has a sidebar to link to each other and to the big board
358     solved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] == 'solved']
359     unsolved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] != 'solved']
360     start = ['<html>\n',
361      '    <head>\n',
362      '        <link rel="stylesheet" href="/overview.css">\n',
363      '        <title>Mystery Hunt 2022</title>\n',
364      '        <script src="/sorttable.js"></script>\n',
365      '    </head>\n',
366      '    <div class="sidenav">\n'
367      '      <a href="index.html">Hunt Overview</a>'
368      '      <a href="all.html">All Puzzles</a>\n'
369      '      <a href="unsolved.html">Unsolved</a>\n'
370      '      <a href="solved.html">Solved</a>\n'
371      '    </div>\n'
372      '    <body>\n',
373      '        <h1><b>{}</b></h1>\n'.format('{} Puzzles').format(filt),
374      '        <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
375      '        <div>\n',
376      '            <table class="center sortable">\n',
377      '                <thead>\n',
378      '                    <tr>\n',
379      '                        <th>Puzzle Title/Slack</th>\n',
380      '                        <th>Puzzle Link</th>\n',
381      '                        <th>Sheet</th>\n',
382      '                        <th>Overview</th>\n',
383      '                        <th>Answer</th>\n',
384      '                        <th>Round(s)</th>\n',
385      '                        <th>Tags</th>\n',
386      '                    </tr>\n',
387      '                </thead>\n',
388      '                <tbody>\n']
389     solved_code = []
390     unsolved_code = []
391     for puzzle in solved_puzzles:
392         if puzzle['type'] == 'meta':
393             meta = ' [META]'
394         else:
395             meta = ''
396         slack_url = channel_url(puzzle['channel_id'])
397         if 'rounds' in puzzle:
398             round_url = link(website + filename_from_name(rnd) + "_round.html", puzzle['rounds'][0])
399         else:
400             round_url = ''
401         #assuming one round per puzzle for now
402
403         solved_code += ['                    <tr>\n',
404          '                        <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
405          '                        <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
406          '                        <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
407          '                        <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
408          '                        <td>{}</td>\n'.format(puzzle['solution']),
409          '                        <td>{}</td>\n'.format(round_url),
410          '                        <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
411          '                    </tr>\n']
412     for puzzle in unsolved_puzzles:
413         if puzzle['type'] == 'meta':
414             meta = ' [META]'
415         else:
416             meta = ''
417         slack_url = channel_url(puzzle['channel_id'])
418         if 'rounds' in puzzle:
419             round_url = link(website + filename_from_name(rnd) + "_round.html", puzzle['rounds'][0])
420         else:
421             round_url = ''
422         #assuming one round per puzzle for now
423
424         unsolved_code += ['                    <tr>\n',
425          '                        <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
426          '                        <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
427          '                        <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
428          '                        <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
429          '                        <td></td>\n',
430          '                        <td>{}</td>\n'.format(round_url),
431          '                        <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
432          '                    </tr>\n']
433     end = ['                </tbody>\n',
434     '            </table>\n',
435     '        </div>\n',
436     '    </body>\n',
437     '</html>\n']
438     if filt == "All":
439         file1 = hunt_file(hunt, 'all.html')
440         f = open(file1, "w")
441         for line in start + unsolved_code + solved_code + end:
442             f.write(line)
443         f.close()
444     elif filt == "Solved":
445         file2 = hunt_file(hunt, 'solved.html')
446         f = open(file2, 'w')
447         for line in start + solved_code + end:
448             f.write(line)
449         f.close()
450     elif filt == "Unsolved":
451         file3 = hunt_file(hunt, 'unsolved.html')
452         f = open(file3, 'w')
453         for line in start + unsolved_code + end:
454             f.write(line)
455         f.close()
456     return None
457
458 # Initialize AWS resources to talk to database
459 db = boto3.resource('dynamodb')
460 table = db.Table("turbot")
461 hunt, puzzles, rounds = hunt_info(table, "mh2021")
462
463 # Create a directory for the hunt in the WEBROOT
464 root = hunt_file(hunt, "")
465 try:
466     os.mkdir(root)
467 except FileExistsError:
468     #  We're happy as a clam if the directory already exists
469     pass
470
471 overview(hunt, puzzles, rounds)
472 for rnd in rounds:
473     round_overview(hunt, rnd, puzzles)
474 for puzzle in puzzles:
475     puzzle_overview(hunt, puzzle)
476 puzzle_lists(hunt, puzzles, "All")
477 puzzle_lists(hunt, puzzles, "Solved")
478 puzzle_lists(hunt, puzzles, "Unsolved")