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