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