]> git.cworth.org Git - turbot-web/blob - html_generator.py
Replace slashes in puzzle name to create a filename
[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['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['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['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['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     round_url = [link(website + "_".join(rnd.split()) + "_round.html", rnd) for rnd in puzzle['rounds']]
289     if puzzle['status'] == 'solved':
290         solution = puzzle['solution']
291         status = 'solved'
292     else:
293         solution = ""
294         status = 'unsolved'
295     html = ['<!DOCTYPE html>\n',
296      '<html>\n',
297      '<head>\n',
298      '    <meta charset="utf-8">\n',
299      '    <meta name="viewport" content="width=device-width, initial-scale=1">\n',
300      '    <link rel="stylesheet" href="individual.css">\n',
301      '    <title>{}</title>\n'.format(name+meta),
302      '    <p>{}</p>'.format(link(website + 'index.html', 'Hunt Overview')),
303      '\n',
304      '</head>\n',
305      '<body class="{}">\n'.format(status),
306      '    <h1>{}</h1>\n'.format(name+meta),
307      '    <div>\n',
308      '        <table class="center">\n',
309      '            <tr>\n',
310      '                <td>{}</td>\n'.format(elink(slack_url, 'Channel')), #slack channel
311      '                <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
312      '                <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
313      '                <td>Additional Resources</td>\n',
314      '            </tr>\n',
315      '        </table>\n',
316      '        <table class="center">\n',
317      '            <tr>\n',
318      '                <td>Round(s): {}</td>\n'.format(" ".join(round_url)), #round page on our site
319      '                <td>Tags: {}</td>\n'.format(" ".join(puzzle['tags'])), #add tags
320      '            </tr>\n',
321      '            <tr>\n',
322      '                <td>Answer: {}</td>\n'.format(solution),
323      '                <td>State: {}</td>\n'.format(puzzle['status']),
324      '            </tr>\n',
325      '        </table>\n',
326      '    </div>\n',
327      '\n',
328      '</body>\n',
329      '</html>\n']
330     # Replace all spaces and slashes in the name with underscores
331     underscored = re.sub(r'[ /]', '_', name)
332     file = "{}.html".format(underscored)
333     f = open(file, "w")
334     for line in html:
335         f.write(line)
336     return None
337
338 def puzzle_lists(puzzles, filt):
339     #filt is one of "All", "Solved", "Unsolved" and spits out the appropriate list of puzzles
340     #generates pages for all puzzles, solved puzzles, unsolved puzzles
341     #saves as all/solved/unsolved.html, has a sidebar to link to each other and to the big board
342     solved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] == 'solved']
343     unsolved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] != 'solved']
344     start = ['<html>\n',
345      '    <head>\n',
346      '        <link rel="stylesheet" href="overview.css">\n',
347      '        <title>Mystery Hunt 2022</title>\n',
348      '        <script src="sorttable.js"></script>\n',
349      '    </head>\n',
350      '    <div class="sidenav">\n'
351      '      <a href="index.html">Hunt Overview</a>'
352      '      <a href="all.html">All Puzzles</a>\n'
353      '      <a href="unsolved.html">Unsolved</a>\n'
354      '      <a href="solved.html">Solved</a>\n'
355      '    </div>\n'
356      '    <body>\n',
357      '        <h1><b>{}</b></h1>\n'.format('{} Puzzles').format(filt),
358      '        <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
359      '        <div>\n',
360      '            <table class="center sortable">\n',
361      '                <thead>\n',
362      '                    <tr>\n',
363      '                        <th>Puzzle Title/Slack</th>\n',
364      '                        <th>Puzzle Link</th>\n',
365      '                        <th>Sheet</th>\n',
366      '                        <th>Overview</th>\n',
367      '                        <th>Answer</th>\n',
368      '                        <th>Round(s)</th>\n',
369      '                        <th>Tags</th>\n',
370      '                    </tr>\n',
371      '                </thead>\n',
372      '                <tbody>\n']
373     solved_code = []
374     unsolved_code = []
375     for puzzle in solved_puzzles:
376         if puzzle['type'] == 'meta':
377             meta = ' [META]'
378         else:
379             meta = ''
380         slack_url = channel_url(puzzle['channel_id'])
381         round_url = link(website + "_".join(rnd.split()) + "_round.html", puzzle['rounds'][0])
382         #assuming one round per puzzle for now
383
384         solved_code += ['                    <tr>\n',
385          '                        <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
386          '                        <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
387          '                        <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
388          '                        <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
389          '                        <td>{}</td>\n'.format(puzzle['solution']),
390          '                        <td>{}</td>\n'.format(round_url),
391          '                        <td>{}</td>\n'.format("".join(puzzle['tags'])),
392          '                    </tr>\n']
393     for puzzle in unsolved_puzzles:
394         if puzzle['type'] == 'meta':
395             meta = ' [META]'
396         else:
397             meta = ''
398         slack_url = channel_url(puzzle['channel_id'])
399         round_url = link(website + "_".join(rnd.split()) + "_round.html", puzzle['rounds'][0])
400         #assuming one round per puzzle for now
401
402         unsolved_code += ['                    <tr>\n',
403          '                        <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
404          '                        <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
405          '                        <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
406          '                        <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
407          '                        <td></td>\n',
408          '                        <td>{}</td>\n'.format(round_url),
409          '                        <td>{}</td>\n'.format("".join(puzzle['tags'])),
410          '                    </tr>\n']
411     end = ['                </tbody>\n',
412     '            </table>\n',
413     '        </div>\n',
414     '    </body>\n',
415     '</html>\n']
416     if filt == "All":
417         file1 = 'all.html'
418         f = open(file1, "w")
419         for line in start + unsolved_code + solved_code + end:
420             f.write(line)
421         f.close()
422     elif filt == "Solved":
423         file2 = 'solved.html'
424         f = open(file2, 'w')
425         for line in start + solved_code + end:
426             f.write(line)
427         f.close()
428     elif filt == "Unsolved":
429         file3 = 'unsolved.html'
430         f = open(file3, 'w')
431         for line in start + unsolved_code + end:
432             f.write(line)
433         f.close()
434     return None
435
436 # Initialize AWS resources to talk to database
437 db = boto3.resource('dynamodb')
438 table = db.Table("turbot")
439 puzzles, rounds = hunt_info(table, "mh2021")
440
441 overview(puzzles, rounds)
442 for rnd in rounds:
443     round_overview(rnd, puzzles)
444 for puzzle in puzzles:
445     puzzle_overview(puzzle)
446 puzzle_lists(puzzles, "All")
447 puzzle_lists(puzzles, "Solved")
448 puzzle_lists(puzzles, "Unsolved")