@author: Avram Gottschlich
"""
"""
-I copied several functions from your code;
+I copied several functions from your code;
if it's easier to refer to them rather than copying them that's absolutely fine
This rewrites the html each time it is called
Requires sorttable.js, which should be included
"""
-from turbot.channel import channel_url
+import boto3
from boto3.dynamodb.conditions import Key
+import os
+import re
+import sys
+
+WEBROOT = "/srv/halibut.cworth.org/www"
website = "https://halibut.cworth.org/"
#change this if we're using AWS or some other subdomain instead
-def find_hunt_for_hunt_id(turb, hunt_id):
+def hunt_file(hunt, name):
+ """Return a path file 'name' within the given hunt.
+
+ This will be withing WEBROOT and in a hunt-specific path."""
+
+ return "{}/{}/{}".format(WEBROOT, hunt['channel_id'], name)
+
+def filename_from_name(name):
+ """Returns a string derived from name, but with all spaces and slashes
+ replaced with underscores, (for making a clean filename)"""
+ return re.sub(r'[ /]', '_', name)
+
+def channel_url(channel_id):
+ """Given a channel ID, return the URL for that channel."""
+
+ return "https://halibutthatbass.slack.com/archives/{}".format(channel_id)
+
+def find_hunt_for_hunt_id(table, hunt_id):
"""Given a hunt ID find the database item for that hunt
Returns None if hunt ID is not found, otherwise a
dictionary with all fields from the hunt's row in the table,
(channel_id, active, hunt_id, name, url, sheet_url, etc.).
"""
- response = turb.table.get_item(
+ response = table.get_item(
Key={
'hunt_id': hunt_id,
'SK': 'hunt-{}'.format(hunt_id)
else:
return None
-def hunt_puzzles_for_hunt_id(turb, hunt_id):
+def hunt_puzzles_for_hunt_id(table, hunt_id):
"""Return all puzzles that belong to the given hunt_id"""
- response = turb.table.query(
+ response = table.query(
KeyConditionExpression=(
Key('hunt_id').eq(hunt_id) &
Key('SK').begins_with('puzzle-')
#internal links, doesn't open new tab
return '<a href="{}">{}</a>'.format(lin, text)
-def hunt_info(turb, hunt):
+def hunt_info(table, hunt_id):
"""
- Retrieves list of rounds, puzzles for the given hunt
+ Retrieves list of rounds, puzzles for the given hunt
"""
+
+ hunt = find_hunt_for_hunt_id(table, hunt_id)
+
name = hunt["name"]
- hunt_id = hunt["hunt_id"]
channel_id = hunt["channel_id"]
-
- puzzles = hunt_puzzles_for_hunt_id(turb, hunt_id)
-
+
+ puzzles = hunt_puzzles_for_hunt_id(table, hunt_id)
+
rounds = set()
for puzzle in puzzles:
if "rounds" not in puzzle:
rounds.add(rnd)
rounds = list(rounds)
rounds.sort()
-
- return puzzles, rounds
+
+ return hunt, puzzles, rounds
def round_stat(rnd, puzzles):
#Counts puzzles, solved, list of puzzles for a given round
meta_solved = 0
for puzzle in puzzles:
if "rounds" not in puzzle:
- continue
+ continue
if rnd in puzzle["rounds"]:
if puzzle['type'] == 'meta':
metas.append(puzzle)
unsolved_puzzles = sorted(unsolved_puzzles, key = lambda i: i['name'])
rnd_puzzles = metas + unsolved_puzzles + solved_puzzles
return puzzle_count, solved_count, rnd_puzzles, meta_solved, len(metas)
-
-
-
-def overview(puzzles, rounds):
+
+
+
+def overview(hunt, puzzles, rounds):
#big board, main page. saves as index.html
start = ['<!DOCTYPE html>\n',
'<html>\n',
' <meta charset="utf-8">\n',
' <meta name="viewport" content="width=device-width, initial-scale=1">\n',
'\n',
- ' <link rel="stylesheet" href="overview.css">\n',
+ ' <link rel="stylesheet" href="/overview.css">\n',
' <script type="text/javascript">\n',
' // Hide all elements with class="containerTab", except for the one that matches the clickable grid column\n',
' function openTab(tabName) {\n',
' </script>\n',
'\n',
' <title>Hunt Overview</title>\n',
- ' <script src="sorttable.js"></script>\n'
+ ' <script src="/sorttable.js"></script>\n'
'</head>\n',
' <div class="sidenav">\n'
' <a href="index.html">Hunt Overview</a>'
' <p>Puzzles: {}/{}</p>\n'.format(solved_count, puzzle_count),
' <p>Metas: {}/{}</p>\n'.format(meta_solved, metas),
' </div>\n']
-
+
expanding += [
' <div id="b{}" class="containerTab {}" style="display:none;">\n'.format(i, status),
' <span onclick="this.parentElement.style.display=\'none\'" class="closebtn">x</span>\n',
- ' <h2>{}</h2>\n'.format(link(website + "_".join(rnd.split()) + "_round.html", rnd)),
+ ' <h2>{}</h2>\n'.format(link(website + filename_from_name(rnd) + "_round.html", rnd)),
' <table class="sortable">\n',
' <tr>\n',
' <th><u>Puzzle</u></th>\n',
meta = ''
if puzzle['status'] == 'solved':
expanding += [' <tr class=\'solved\';>\n',
- ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + ".html", puzzle['name']+meta)),
+ ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
' <td>{}</td>\n'.format(puzzle['solution']),
' </tr>\n']
else:
expanding += [' <tr class=\'unsolved\';>\n',
- ' <td><b>{}</b></td>\n'.format(link(website + "_".join(puzzle['name'].split()) + ".html", puzzle['name']+meta)),
+ ' <td><b>{}</b></td>\n'.format(link(website + filename_from_name(puzzle['name']) + ".html", puzzle['name']+meta)),
' <td></td>\n',
' </tr>\n']
expanding.append(' </table>\n')
columns.append(' </div>\n')
end = ['</body>\n', '</html>\n']
html = start + expanding + columns + end
- file = "index.html"
+ file = hunt_file(hunt, "index.html")
f = open(file, "w")
for line in html:
f.write(line)
f.close()
return None
-def round_overview(rnd, puzzles):
+def round_overview(hunt, rnd, puzzles):
#inputs: round name, puzzles
#round overview page
#saves as (round name)_round.html, in case meta/round share names.
status = 'unsolved'
start = ['<html>\n',
' <head>\n',
- ' <link rel="stylesheet" href="individual.css">\n',
+ ' <link rel="stylesheet" href="/individual.css">\n',
' <title>Mystery Hunt 2022</title>\n',
- ' <script src="sorttable.js"></script>\n',
+ ' <script src="/sorttable.js"></script>\n',
' </head>\n',
' <body class="{}">\n'.format(status),
' <h1><b>{}</b></h1>\n'.format(rnd),
else:
meta = ''
slack_url = channel_url(puzzle['channel_id'])
-
+
if puzzle['status'] == 'solved':
puzzle_list += [ ' <tr>\n',
' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
- ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
+ ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
- ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
+ ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
' <td>{}</td>\n'.format(puzzle['solution']),
# ' <td></td>\n',
- ' <td>{}</td>\n'.format("".join(puzzle['tags'])),
+ ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
' </tr>\n']
else:
puzzle_list += [ ' <tr>\n',
' <td><b>{}</b></td>\n'.format(elink(slack_url, puzzle['name']+meta)),
- ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
+ ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
- ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
+ ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
' <td></td>\n',
# ' <td></td>\n',
- ' <td>{}</td>\n'.format(" ".join(puzzle['tags'])),
+ ' <td>{}</td>\n'.format(" ".join(puzzle.get('tags',[]))),
' </tr>\n']
end = [' </tbody>\n',
' </table>\n',
' </body>\n',
'</html>\n']
html = start + puzzle_list + end
- file = "{}_round.html".format('_'.join(rnd.split()))
+ file = hunt_file(hunt, "{}_round.html".format(filename_from_name(rnd)))
f = open(file, "w")
for line in html:
f.write(line)
f.close()
return None
-def puzzle_overview(puzzle):
+def puzzle_overview(hunt, puzzle):
#overview page for individual puzzles. saves as (name of puzzle).html,
#with underscores rather than spaces
name = puzzle['name']
else:
meta = ''
slack_url = channel_url(puzzle['channel_id'])
- round_url = [link(website + "_".join(rnd.split()) + "_round.html", rnd) for rnd in puzzle['rounds']]
+ if 'rounds' in puzzle:
+ round_url = [link(website + filename_from_name(rnd) + "_round.html", rnd) for rnd in puzzle['rounds']]
+ else:
+ round_url = ''
if puzzle['status'] == 'solved':
solution = puzzle['solution']
status = 'solved'
'<head>\n',
' <meta charset="utf-8">\n',
' <meta name="viewport" content="width=device-width, initial-scale=1">\n',
- ' <link rel="stylesheet" href="individual.css">\n',
+ ' <link rel="stylesheet" href="/individual.css">\n',
' <title>{}</title>\n'.format(name+meta),
' <p>{}</p>'.format(link(website + 'index.html', 'Hunt Overview')),
'\n',
' <table class="center">\n',
' <tr>\n',
' <td>{}</td>\n'.format(elink(slack_url, 'Channel')), #slack channel
- ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
+ ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
' <td>Additional Resources</td>\n',
' </tr>\n',
' <table class="center">\n',
' <tr>\n',
' <td>Round(s): {}</td>\n'.format(" ".join(round_url)), #round page on our site
- ' <td>Tags: {}</td>\n'.format(" ".join(puzzle['tags'])), #add tags
+ ' <td>Tags: {}</td>\n'.format(" ".join(puzzle.get('tags',[]))), #add tags
' </tr>\n',
' <tr>\n',
' <td>Answer: {}</td>\n'.format(solution),
'\n',
'</body>\n',
'</html>\n']
- underscored = "_".join(name.split())
- file = "{}.html".format(underscored)
+ file = hunt_file(hunt, "{}.html".format(filename_from_name(name)))
f = open(file, "w")
for line in html:
f.write(line)
return None
-def puzzle_lists(puzzles, filt):
+def puzzle_lists(hunt, puzzles, filt):
#filt is one of "All", "Solved", "Unsolved" and spits out the appropriate list of puzzles
#generates pages for all puzzles, solved puzzles, unsolved puzzles
#saves as all/solved/unsolved.html, has a sidebar to link to each other and to the big board
unsolved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] != 'solved']
start = ['<html>\n',
' <head>\n',
- ' <link rel="stylesheet" href="overview.css">\n',
+ ' <link rel="stylesheet" href="/overview.css">\n',
' <title>Mystery Hunt 2022</title>\n',
- ' <script src="sorttable.js"></script>\n',
+ ' <script src="/sorttable.js"></script>\n',
' </head>\n',
' <div class="sidenav">\n'
' <a href="index.html">Hunt Overview</a>'
else:
meta = ''
slack_url = channel_url(puzzle['channel_id'])
- round_url = link(website + "_".join(rnd.split()) + "_round.html", puzzle['rounds'][0])
+ if 'rounds' in puzzle:
+ round_url = link(website + filename_from_name(puzzle['rounds'][0]) + "_round.html", puzzle['rounds'][0])
+ else:
+ round_url = ''
#assuming one round per puzzle for now
-
+
solved_code += [' <tr>\n',
' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
- ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
+ ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
- ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
+ ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
' <td>{}</td>\n'.format(puzzle['solution']),
' <td>{}</td>\n'.format(round_url),
- ' <td>{}</td>\n'.format("".join(puzzle['tags'])),
+ ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
' </tr>\n']
for puzzle in unsolved_puzzles:
if puzzle['type'] == 'meta':
else:
meta = ''
slack_url = channel_url(puzzle['channel_id'])
- round_url = link(website + "_".join(rnd.split()) + "_round.html", puzzle['rounds'][0])
+ if 'rounds' in puzzle:
+ round_url = link(website + filename_from_name(puzzle['rounds'][0]) + "_round.html", puzzle['rounds'][0])
+ else:
+ round_url = ''
#assuming one round per puzzle for now
-
+
unsolved_code += [' <tr>\n',
' <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
- ' <td>{}</td>\n'.format(elink(puzzle['url'], 'Puzzle')),
+ ' <td>{}</td>\n'.format(elink(puzzle.get('url',''), 'Puzzle')),
' <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
- ' <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
+ ' <td>{}</td>\n'.format(link(website + filename_from_name(puzzle['name']) + '.html', 'Overview')),
' <td></td>\n',
' <td>{}</td>\n'.format(round_url),
- ' <td>{}</td>\n'.format("".join(puzzle['tags'])),
+ ' <td>{}</td>\n'.format("".join(puzzle.get('tags',[]))),
' </tr>\n']
end = [' </tbody>\n',
' </table>\n',
' </body>\n',
'</html>\n']
if filt == "All":
- file1 = 'all.html'
+ file1 = hunt_file(hunt, 'all.html')
f = open(file1, "w")
for line in start + unsolved_code + solved_code + end:
f.write(line)
f.close()
elif filt == "Solved":
- file2 = 'solved.html'
+ file2 = hunt_file(hunt, 'solved.html')
f = open(file2, 'w')
for line in start + solved_code + end:
f.write(line)
f.close()
elif filt == "Unsolved":
- file3 = 'unsolved.html'
+ file3 = hunt_file(hunt, 'unsolved.html')
f = open(file3, 'w')
for line in start + unsolved_code + end:
f.write(line)
f.close()
return None
+def generate_for_hunt_id(table, hunt_id):
+ hunt, puzzles, rounds = hunt_info(table, hunt_id)
+
+ # Create a directory for the hunt in the WEBROOT
+ root = hunt_file(hunt, "")
+ try:
+ os.mkdir(root)
+ except FileExistsError:
+ # We're happy as a clam if the directory already exists
+ pass
+
+ overview(hunt, puzzles, rounds)
+ for rnd in rounds:
+ round_overview(hunt, rnd, puzzles)
+ for puzzle in puzzles:
+ puzzle_overview(hunt, puzzle)
+ puzzle_lists(hunt, puzzles, "All")
+ puzzle_lists(hunt, puzzles, "Solved")
+ puzzle_lists(hunt, puzzles, "Unsolved")
+
+# Initialize AWS resources to talk to the database
+db = boto3.resource('dynamodb')
+table = db.Table("turbot")
+
+def usage():
+ print("Usage: {} hunt_id [...]")
+ print("")
+ print("Generates pages (under {}) ".format(WEBROOT))
+ print("for the specified hunt_id(s).")
+if len(sys.argv) < 2:
+ usage()
+ sys.exit(1)
-puzzles, rounds = hunt_info(turb, hunt)
-#I am not sure where these come from
-overview(puzzles, rounds)
-for rnd in rounds:
- round_overview(rnd, puzzles)
-for puzzle in puzzles:
- puzzle_overview(puzzle)
-puzzle_lists(puzzles, "All")
-puzzle_lists(puzzles, "Solved")
-puzzle_lists(puzzles, "Unsolved")
+for hunt_id in sys.argv[1:]:
+ generate_for_hunt_id(table, hunt_id)