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