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