]> git.cworth.org Git - turbot-web/blob - html_generator.py
Expect all dependencies to be found from /
[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 re
20
21 website = "https://halibut.cworth.org/"
22 #change this if we're using AWS or some other subdomain instead
23
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)
28
29 def channel_url(channel_id):
30     """Given a channel ID, return the URL for that channel."""
31
32     return "https://halibutthatbass.slack.com/archives/{}".format(channel_id)
33
34 def find_hunt_for_hunt_id(table, hunt_id):
35     """Given a hunt ID find the database item for that hunt
36
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.).
40     """
41     response = table.get_item(
42         Key={
43             'hunt_id': hunt_id,
44             'SK': 'hunt-{}'.format(hunt_id)
45             })
46
47     if 'Item' in response:
48         return response['Item']
49     else:
50         return None
51
52 def hunt_puzzles_for_hunt_id(table, hunt_id):
53     """Return all puzzles that belong to the given hunt_id"""
54
55     response = table.query(
56         KeyConditionExpression=(
57             Key('hunt_id').eq(hunt_id) &
58             Key('SK').begins_with('puzzle-')
59             )
60         )
61
62     return response['Items']
63
64 def elink(lin, text):
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)
68
69 def link(lin, text):
70     #internal links, doesn't open new tab
71     return '<a href="{}">{}</a>'.format(lin, text)
72
73 def hunt_info(table, hunt_id):
74     """
75     Retrieves list of rounds, puzzles for the given hunt
76     """
77
78     hunt = find_hunt_for_hunt_id(table, hunt_id)
79
80     name = hunt["name"]
81     channel_id = hunt["channel_id"]
82
83     puzzles = hunt_puzzles_for_hunt_id(table, hunt_id)
84
85     rounds = set()
86     for puzzle in puzzles:
87         if "rounds" not in puzzle:
88             continue
89         for rnd in puzzle["rounds"]:
90             rounds.add(rnd)
91     rounds = list(rounds)
92     rounds.sort()
93
94     return puzzles, rounds
95
96 def round_stat(rnd, puzzles):
97     #Counts puzzles, solved, list of puzzles for a given round
98     puzzle_count = 0
99     solved_count = 0
100     solved_puzzles = []
101     unsolved_puzzles = []
102     metas = []
103     meta_solved = 0
104     for puzzle in puzzles:
105         if "rounds" not in puzzle:
106             continue
107         if rnd in puzzle["rounds"]:
108             if puzzle['type'] == 'meta':
109                 metas.append(puzzle)
110                 if puzzle['status'] == 'solved':
111                     meta_solved += 1
112             else:
113                 puzzle_count += 1
114                 if puzzle['status'] == 'solved':
115                     solved_count += 1
116                     solved_puzzles.append(puzzle)
117                 else:
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)
123
124
125
126 def overview(puzzles, rounds):
127     #big board, main page. saves as index.html
128     start = ['<!DOCTYPE html>\n',
129      '<html>\n',
130      '<head>\n',
131      '  <meta charset="utf-8">\n',
132      '  <meta name="viewport" content="width=device-width, initial-scale=1">\n',
133      '\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',
138      '      var i, x;\n',
139      '      x = document.getElementsByClassName("containerTab");\n',
140      '      for (i = 0; i < x.length; i++) {\n',
141      '        x[i].style.display = "none";\n',
142      '      }\n',
143      '      document.getElementById(tabName).style.display = "block";\n',
144      '    }\n',
145      '  </script>\n',
146      '\n',
147      '  <title>Hunt Overview</title>\n',
148      '  <script src="/sorttable.js"></script>\n'
149      '</head>\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'
155      '    </div>\n'
156      '<body>\n',]
157     columns = ['  <div class="row">\n']
158     expanding = []
159     i = 1
160     for rnd in rounds:
161         puzzle_count, solved_count, rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)
162         if metas == meta_solved and metas > 0:
163             status = 'solved'
164         else:
165             status = 'unsolved'
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),
170         '    </div>\n']
171
172         expanding += [
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',
177         '      <tr>\n',
178         '        <th><u>Puzzle</u></th>\n',
179         '        <th><u>Answer</u></th>\n',
180         '      </tr>\n',]
181
182         for puzzle in rnd_puzzles:
183             if puzzle['type'] == 'meta':
184                 meta = ' [META]'
185             else:
186                 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']),
191                 '      </tr>\n']
192             else:
193                 expanding += ['      <tr class=\'unsolved\';>\n',
194                 '        <td><b>{}</b></td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
195                 '        <td></td>\n',
196                 '      </tr>\n']
197         expanding.append('    </table>\n')
198         expanding.append('  </div>\n')
199         i += 1
200     columns.append('  </div>\n')
201     end = ['</body>\n', '</html>\n']
202     html = start + expanding + columns + end
203     file = "index.html"
204     f = open(file, "w")
205     for line in html:
206         f.write(line)
207     f.close()
208     return None
209
210 def round_overview(rnd, puzzles):
211     #inputs: round name, puzzles
212     #round overview page
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:
217         status = 'solved'
218     else:
219         status = 'unsolved'
220     start = ['<html>\n',
221      '    <head>\n',
222      '        <link rel="stylesheet" href="/individual.css">\n',
223      '        <title>Mystery Hunt 2022</title>\n',
224      '        <script src="/sorttable.js"></script>\n',
225      '    </head>\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')),
229      '        <div>\n',
230      '            <table class="center sortable">\n',
231      '                <thead>\n',
232      '                    <tr>\n',
233      '                        <th>Puzzle Title/Slack</th>\n',
234      '                        <th>Puzzle Link</th>\n',
235      '                        <th>Sheet</th>\n',
236      '                        <th>Overview</th>\n',
237      '                        <th>Answer</th>\n',
238      #'                        <th>Extra Links</th>\n',
239      '                        <th>Tags</th>\n',
240      '                    </tr>\n',
241      '                </thead>\n',
242      '                <tbody>\n']
243     puzzle_list = []
244     for puzzle in rnd_puzzles:
245         if puzzle['type'] == 'meta':
246             meta = ' [META]'
247         else:
248             meta = ''
249         slack_url = channel_url(puzzle['channel_id'])
250
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']),
258             # '                        <td></td>\n',
259              '                        <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
260              '                    </tr>\n']
261         else:
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')),
267              '                        <td></td>\n',
268             # '                        <td></td>\n',
269              '                        <td>{}</td>\n'.format(" ".join(puzzle.get('tags',[]))),
270              '                    </tr>\n']
271     end = ['                </tbody>\n',
272     '            </table>\n',
273     '        </div>\n',
274     '    </body>\n',
275     '</html>\n']
276     html = start + puzzle_list + end
277     file = "{}_round.html".format(filename_from_name(rnd))
278     f = open(file, "w")
279     for line in html:
280         f.write(line)
281     f.close()
282     return None
283
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':
289         meta = ' [META]'
290     else:
291         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']]
295     else:
296         round_url = ''
297     if puzzle['status'] == 'solved':
298         solution = puzzle['solution']
299         status = 'solved'
300     else:
301         solution = ""
302         status = 'unsolved'
303     html = ['<!DOCTYPE html>\n',
304      '<html>\n',
305      '<head>\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')),
311      '\n',
312      '</head>\n',
313      '<body class="{}">\n'.format(status),
314      '    <h1>{}</h1>\n'.format(name+meta),
315      '    <div>\n',
316      '        <table class="center">\n',
317      '            <tr>\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',
322      '            </tr>\n',
323      '        </table>\n',
324      '        <table class="center">\n',
325      '            <tr>\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
328      '            </tr>\n',
329      '            <tr>\n',
330      '                <td>Answer: {}</td>\n'.format(solution),
331      '                <td>State: {}</td>\n'.format(puzzle['status']),
332      '            </tr>\n',
333      '        </table>\n',
334      '    </div>\n',
335      '\n',
336      '</body>\n',
337      '</html>\n']
338     file = "{}.html".format(filename_from_name(name))
339     f = open(file, "w")
340     for line in html:
341         f.write(line)
342     return None
343
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']
350     start = ['<html>\n',
351      '    <head>\n',
352      '        <link rel="stylesheet" href="/overview.css">\n',
353      '        <title>Mystery Hunt 2022</title>\n',
354      '        <script src="/sorttable.js"></script>\n',
355      '    </head>\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'
361      '    </div>\n'
362      '    <body>\n',
363      '        <h1><b>{}</b></h1>\n'.format('{} Puzzles').format(filt),
364      '        <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
365      '        <div>\n',
366      '            <table class="center sortable">\n',
367      '                <thead>\n',
368      '                    <tr>\n',
369      '                        <th>Puzzle Title/Slack</th>\n',
370      '                        <th>Puzzle Link</th>\n',
371      '                        <th>Sheet</th>\n',
372      '                        <th>Overview</th>\n',
373      '                        <th>Answer</th>\n',
374      '                        <th>Round(s)</th>\n',
375      '                        <th>Tags</th>\n',
376      '                    </tr>\n',
377      '                </thead>\n',
378      '                <tbody>\n']
379     solved_code = []
380     unsolved_code = []
381     for puzzle in solved_puzzles:
382         if puzzle['type'] == 'meta':
383             meta = ' [META]'
384         else:
385             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])
389         else:
390             round_url = ''
391         #assuming one round per puzzle for now
392
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',[]))),
401          '                    </tr>\n']
402     for puzzle in unsolved_puzzles:
403         if puzzle['type'] == 'meta':
404             meta = ' [META]'
405         else:
406             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])
410         else:
411             round_url = ''
412         #assuming one round per puzzle for now
413
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')),
419          '                        <td></td>\n',
420          '                        <td>{}</td>\n'.format(round_url),
421          '                        <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
422          '                    </tr>\n']
423     end = ['                </tbody>\n',
424     '            </table>\n',
425     '        </div>\n',
426     '    </body>\n',
427     '</html>\n']
428     if filt == "All":
429         file1 = 'all.html'
430         f = open(file1, "w")
431         for line in start + unsolved_code + solved_code + end:
432             f.write(line)
433         f.close()
434     elif filt == "Solved":
435         file2 = 'solved.html'
436         f = open(file2, 'w')
437         for line in start + solved_code + end:
438             f.write(line)
439         f.close()
440     elif filt == "Unsolved":
441         file3 = 'unsolved.html'
442         f = open(file3, 'w')
443         for line in start + unsolved_code + end:
444             f.write(line)
445         f.close()
446     return None
447
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")
452
453 overview(puzzles, rounds)
454 for rnd in 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")