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