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