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