]> git.cworth.org Git - turbot-web/blobdiff - html_generator.py
Add code to auto-refresh each HTML page
[turbot-web] / html_generator.py
index 7d18e0a5738a111e9bd4acda97cccbb88d7d8338..f18703211897d73f90fdec216645e24f22a53e33 100644 (file)
@@ -5,7 +5,7 @@ Created on Thu Jan  6 23:35:23 2022
 @author: Avram Gottschlich
 """
 """
 @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
 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
@@ -14,20 +14,46 @@ that would be great
 
 Requires sorttable.js, which should be included
 """
 
 Requires sorttable.js, which should be included
 """
-from turbot.channel import channel_url
+import boto3
 from boto3.dynamodb.conditions import Key
 from boto3.dynamodb.conditions import Key
+import os
+import re
+import sys
 
 
-website = "https://halibut.cworth.org/"
-#change this if we're using AWS or some other subdomain instead
+WEBROOT = "/srv/halibut.cworth.org/www"
 
 
-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 internal_link(hunt, name):
+    """Returns a path for a link on this site."""
+
+    # Just generate a relative link, (which is just the name itself)
+    return "{}".format(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.).
     """
     """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)
         Key={
             'hunt_id': hunt_id,
             'SK': 'hunt-{}'.format(hunt_id)
@@ -38,10 +64,10 @@ def find_hunt_for_hunt_id(turb, hunt_id):
     else:
         return None
 
     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"""
 
     """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-')
         KeyConditionExpression=(
             Key('hunt_id').eq(hunt_id) &
             Key('SK').begins_with('puzzle-')
@@ -59,16 +85,18 @@ def link(lin, text):
     #internal links, doesn't open new tab
     return '<a href="{}">{}</a>'.format(lin, text)
 
     #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"]
     name = hunt["name"]
-    hunt_id = hunt["hunt_id"]
     channel_id = hunt["channel_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 = set()
     for puzzle in puzzles:
         if "rounds" not in puzzle:
@@ -77,8 +105,8 @@ def hunt_info(turb, hunt):
             rounds.add(rnd)
     rounds = list(rounds)
     rounds.sort()
             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
 
 def round_stat(rnd, puzzles):
     #Counts puzzles, solved, list of puzzles for a given round
@@ -90,7 +118,7 @@ def round_stat(rnd, puzzles):
     meta_solved = 0
     for puzzle in puzzles:
         if "rounds" not in puzzle:
     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)
         if rnd in puzzle["rounds"]:
             if puzzle['type'] == 'meta':
                 metas.append(puzzle)
@@ -107,18 +135,19 @@ def round_stat(rnd, puzzles):
     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)
     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',
      '<head>\n',
      '  <meta charset="utf-8">\n',
      '  <meta name="viewport" content="width=device-width, initial-scale=1">\n',
     #big board, main page. saves as index.html
     start = ['<!DOCTYPE html>\n',
      '<html>\n',
      '<head>\n',
      '  <meta charset="utf-8">\n',
      '  <meta name="viewport" content="width=device-width, initial-scale=1">\n',
+     '  <meta http-equiv="refresh" content = "15">\n',
      '\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 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',
@@ -132,14 +161,15 @@ def overview(puzzles, rounds):
      '  </script>\n',
      '\n',
      '  <title>Hunt Overview</title>\n',
      '  </script>\n',
      '\n',
      '  <title>Hunt Overview</title>\n',
-     '  <script src="sorttable.js"></script>\n'
+     '  <script src="/sorttable.js"></script>\n'
      '</head>\n',
      '</head>\n',
-     '    <div class="sidenav">\n'
-     '      <a href="index.html">Hunt Overview</a>'
-     '      <a href="all.html">All Puzzles</a>\n'
-     '      <a href="unsolved.html">Unsolved</a>\n'
-     '      <a href="solved.html">Solved</a>\n'
-     '    </div>\n'
+     '    <div class="sidenav">\n',
+     '      <a href="index.html">Hunt Overview</a>',
+     '      <a href="all.html">All Puzzles</a>\n',
+     '      <a href="unsolved.html">Unsolved</a>\n',
+     '      <a href="solved.html">Solved</a>\n',
+     '      <a href="https://docs.google.com/document/d/14Ww6vWFO4hx1GYz8zDRxP_rI_v4hmRgdgYmN91F-Lqk/edit" target="_blank" rel="noreferrer noopener">Turbot Docs</a>\n'
+     '    </div>\n',
      '<body>\n',]
     columns = ['  <div class="row">\n']
     expanding = []
      '<body>\n',]
     columns = ['  <div class="row">\n']
     expanding = []
@@ -155,11 +185,11 @@ def overview(puzzles, rounds):
         '      <p>Puzzles: {}/{}</p>\n'.format(solved_count, puzzle_count),
         '      <p>Metas: {}/{}</p>\n'.format(meta_solved, metas),
         '    </div>\n']
         '      <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',
         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(internal_link(hunt, filename_from_name(rnd)) + "_round.html", rnd)),
         '    <table class="sortable">\n',
         '      <tr>\n',
         '        <th><u>Puzzle</u></th>\n',
         '    <table class="sortable">\n',
         '      <tr>\n',
         '        <th><u>Puzzle</u></th>\n',
@@ -173,12 +203,12 @@ def overview(puzzles, rounds):
                 meta = ''
             if puzzle['status'] == 'solved':
                 expanding += ['      <tr class=\'solved\';>\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(puzzle['solution']),
+                '        <td>{}</td>\n'.format(link(internal_link(hunt, filename_from_name(puzzle['name'])) + ".html", puzzle['name']+meta)),
+                '        <td>{}</td>\n'.format(", ".join(puzzle['solution']).upper()),
                 '      </tr>\n']
             else:
                 expanding += ['      <tr class=\'unsolved\';>\n',
                 '      </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(internal_link(hunt, filename_from_name(puzzle['name'])) + ".html", puzzle['name']+meta)),
                 '        <td></td>\n',
                 '      </tr>\n']
         expanding.append('    </table>\n')
                 '        <td></td>\n',
                 '      </tr>\n']
         expanding.append('    </table>\n')
@@ -187,14 +217,15 @@ def overview(puzzles, rounds):
     columns.append('  </div>\n')
     end = ['</body>\n', '</html>\n']
     html = start + expanding + columns + end
     columns.append('  </div>\n')
     end = ['</body>\n', '</html>\n']
     html = start + expanding + columns + end
-    file = "index.html"
-    f = open(file, "w")
+    file = hunt_file(hunt, "index.html")
+    f = open(file + ".tmp", "w")
     for line in html:
         f.write(line)
     f.close()
     for line in html:
         f.write(line)
     f.close()
+    os.rename(file + ".tmp", file)
     return None
 
     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.
     #inputs: round name, puzzles
     #round overview page
     #saves as (round name)_round.html, in case meta/round share names.
@@ -206,13 +237,14 @@ def round_overview(rnd, puzzles):
         status = 'unsolved'
     start = ['<html>\n',
      '    <head>\n',
         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',
      '        <title>Mystery Hunt 2022</title>\n',
-     '        <script src="sorttable.js"></script>\n',
+     '        <script src="/sorttable.js"></script>\n',
+     '        <meta http-equiv="refresh" content = "15">\n',
      '    </head>\n',
      '    <body class="{}">\n'.format(status),
      '        <h1><b>{}</b></h1>\n'.format(rnd),
      '    </head>\n',
      '    <body class="{}">\n'.format(status),
      '        <h1><b>{}</b></h1>\n'.format(rnd),
-     '        <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
+     '        <p>{}</p>\n'.format(link(internal_link(hunt, "index") + ".html", 'Hunt Overview')),
      '        <div>\n',
      '            <table class="center sortable">\n',
      '                <thead>\n',
      '        <div>\n',
      '            <table class="center sortable">\n',
      '                <thead>\n',
@@ -234,26 +266,26 @@ def round_overview(rnd, puzzles):
         else:
             meta = ''
         slack_url = channel_url(puzzle['channel_id'])
         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)),
         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(elink(puzzle['sheet_url'], 'Sheet')),
-             '                        <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
-             '                        <td>{}</td>\n'.format(puzzle['solution']),
+             '                        <td>{}</td>\n'.format(link(internal_link(hunt, filename_from_name(puzzle['name'])) + ".html", 'Overview')),
+             '                        <td>{}</td>\n'.format(", ".join(puzzle['solution']).upper()),
             # '                        <td></td>\n',
             # '                        <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)),
              '                    </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(elink(puzzle['sheet_url'], 'Sheet')),
-             '                        <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
+             '                        <td>{}</td>\n'.format(link(internal_link(hunt, filename_from_name(puzzle['name'])) + ".html", 'Overview')),
              '                        <td></td>\n',
             # '                        <td></td>\n',
              '                        <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',
              '                    </tr>\n']
     end = ['                </tbody>\n',
     '            </table>\n',
@@ -261,14 +293,15 @@ def round_overview(rnd, puzzles):
     '    </body>\n',
     '</html>\n']
     html = start + puzzle_list + end
     '    </body>\n',
     '</html>\n']
     html = start + puzzle_list + end
-    file = "{}_round.html".format('_'.join(rnd.split()))
-    f = open(file, "w")
+    file = hunt_file(hunt, "{}_round.html".format(filename_from_name(rnd)))
+    f = open(file + ".tmp", "w")
     for line in html:
         f.write(line)
     f.close()
     for line in html:
         f.write(line)
     f.close()
+    os.rename(file + ".tmp", file)
     return None
 
     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']
     #overview page for individual puzzles. saves as (name of puzzle).html,
     #with underscores rather than spaces
     name = puzzle['name']
@@ -277,9 +310,12 @@ def puzzle_overview(puzzle):
     else:
         meta = ''
     slack_url = channel_url(puzzle['channel_id'])
     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(internal_link(hunt, filename_from_name(rnd)) + "_round.html", rnd) for rnd in puzzle['rounds']]
+    else:
+        round_url = ''
     if puzzle['status'] == 'solved':
     if puzzle['status'] == 'solved':
-        solution = puzzle['solution']
+        solution = ", ".join(puzzle['solution']).upper()
         status = 'solved'
     else:
         solution = ""
         status = 'solved'
     else:
         solution = ""
@@ -289,9 +325,10 @@ def puzzle_overview(puzzle):
      '<head>\n',
      '    <meta charset="utf-8">\n',
      '    <meta name="viewport" content="width=device-width, initial-scale=1">\n',
      '<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',
+     '    <meta http-equiv="refresh" content = "15">\n',
+     '    <link rel="stylesheet" href="/individual.css">\n',
      '    <title>{}</title>\n'.format(name+meta),
      '    <title>{}</title>\n'.format(name+meta),
-     '    <p>{}</p>'.format(link(website + 'index.html', 'Hunt Overview')),
+     '    <p>{}</p>'.format(link(internal_link(hunt, 'index') + ".html", 'Hunt Overview')),
      '\n',
      '</head>\n',
      '<body class="{}">\n'.format(status),
      '\n',
      '</head>\n',
      '<body class="{}">\n'.format(status),
@@ -300,7 +337,7 @@ def puzzle_overview(puzzle):
      '        <table class="center">\n',
      '            <tr>\n',
      '                <td>{}</td>\n'.format(elink(slack_url, 'Channel')), #slack channel
      '        <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',
      '                <td>{}</td>\n'.format(elink(puzzle['sheet_url'], 'Sheet')),
      '                <td>Additional Resources</td>\n',
      '            </tr>\n',
@@ -308,7 +345,7 @@ def puzzle_overview(puzzle):
      '        <table class="center">\n',
      '            <tr>\n',
      '                <td>Round(s): {}</td>\n'.format(" ".join(round_url)), #round page on our site
      '        <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),
      '            </tr>\n',
      '            <tr>\n',
      '                <td>Answer: {}</td>\n'.format(solution),
@@ -319,14 +356,15 @@ def puzzle_overview(puzzle):
      '\n',
      '</body>\n',
      '</html>\n']
      '\n',
      '</body>\n',
      '</html>\n']
-    underscored = "_".join(name.split())
-    file = "{}.html".format(underscored)
-    f = open(file, "w")
+    file = hunt_file(hunt, "{}.html".format(filename_from_name(name)))
+    f = open(file + ".tmp", "w")
     for line in html:
         f.write(line)
     for line in html:
         f.write(line)
+    f.close()
+    os.rename(file + ".tmp", file)
     return None
 
     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
     #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
@@ -334,19 +372,21 @@ def puzzle_lists(puzzles, filt):
     unsolved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] != 'solved']
     start = ['<html>\n',
      '    <head>\n',
     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',
      '        <title>Mystery Hunt 2022</title>\n',
-     '        <script src="sorttable.js"></script>\n',
+     '        <script src="/sorttable.js"></script>\n',
+     '        <meta http-equiv="refresh" content = "15">\n',
      '    </head>\n',
      '    </head>\n',
-     '    <div class="sidenav">\n'
-     '      <a href="index.html">Hunt Overview</a>'
-     '      <a href="all.html">All Puzzles</a>\n'
-     '      <a href="unsolved.html">Unsolved</a>\n'
-     '      <a href="solved.html">Solved</a>\n'
-     '    </div>\n'
+     '    <div class="sidenav">\n',
+     '      <a href="index.html">Hunt Overview</a>',
+     '      <a href="all.html">All Puzzles</a>\n',
+     '      <a href="unsolved.html">Unsolved</a>\n',
+     '      <a href="solved.html">Solved</a>\n',
+     '      <a href="https://docs.google.com/document/d/14Ww6vWFO4hx1GYz8zDRxP_rI_v4hmRgdgYmN91F-Lqk/edit" target="_blank" rel="noreferrer noopener">Turbot Docs</a>\n'
+     '    </div>\n',
      '    <body>\n',
      '        <h1><b>{}</b></h1>\n'.format('{} Puzzles').format(filt),
      '    <body>\n',
      '        <h1><b>{}</b></h1>\n'.format('{} Puzzles').format(filt),
-     '        <p>{}</p>\n'.format(link(website + "index.html", 'Hunt Overview')),
+     '        <p>{}</p>\n'.format(link(internal_link(hunt, 'index') + ".html", 'Hunt Overview')),
      '        <div>\n',
      '            <table class="center sortable">\n',
      '                <thead>\n',
      '        <div>\n',
      '            <table class="center sortable">\n',
      '                <thead>\n',
@@ -369,17 +409,20 @@ def puzzle_lists(puzzles, filt):
         else:
             meta = ''
         slack_url = channel_url(puzzle['channel_id'])
         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(internal_link(hunt, filename_from_name(puzzle['rounds'][0])) + "_round.html", puzzle['rounds'][0])
+        else:
+            round_url = ''
         #assuming one round per puzzle for now
         #assuming one round per puzzle for now
-        
+
         solved_code += ['                    <tr>\n',
          '                        <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
         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(elink(puzzle['sheet_url'], 'Sheet')),
-         '                        <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
-         '                        <td>{}</td>\n'.format(puzzle['solution']),
+         '                        <td>{}</td>\n'.format(link(internal_link(hunt, filename_from_name(puzzle['name'])) + ".html", 'Overview')),
+         '                        <td>{}</td>\n'.format(", ".join(puzzle['solution']).upper()),
          '                        <td>{}</td>\n'.format(round_url),
          '                        <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':
          '                    </tr>\n']
     for puzzle in unsolved_puzzles:
         if puzzle['type'] == 'meta':
@@ -387,17 +430,20 @@ def puzzle_lists(puzzles, filt):
         else:
             meta = ''
         slack_url = channel_url(puzzle['channel_id'])
         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(internal_link(hunt, filename_from_name(puzzle['rounds'][0])) + "_round.html", puzzle['rounds'][0])
+        else:
+            round_url = ''
         #assuming one round per puzzle for now
         #assuming one round per puzzle for now
-        
+
         unsolved_code += ['                    <tr>\n',
          '                        <td>{}</td>\n'.format(elink(slack_url, puzzle['name']+meta)),
         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(elink(puzzle['sheet_url'], 'Sheet')),
-         '                        <td>{}</td>\n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')),
+         '                        <td>{}</td>\n'.format(link(internal_link(hunt, filename_from_name(puzzle['name'])) + ".html", 'Overview')),
          '                        <td></td>\n',
          '                        <td>{}</td>\n'.format(round_url),
          '                        <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',
          '                    </tr>\n']
     end = ['                </tbody>\n',
     '            </table>\n',
@@ -405,34 +451,62 @@ def puzzle_lists(puzzles, filt):
     '    </body>\n',
     '</html>\n']
     if filt == "All":
     '    </body>\n',
     '</html>\n']
     if filt == "All":
-        file1 = 'all.html'
-        f = open(file1, "w")
+        file1 = hunt_file(hunt, 'all.html')
+        f = open(file1 + ".tmp", "w")
         for line in start + unsolved_code + solved_code + end:
             f.write(line)
         f.close()
         for line in start + unsolved_code + solved_code + end:
             f.write(line)
         f.close()
+        os.rename(file1 + ".tmp", file1)
     elif filt == "Solved":
     elif filt == "Solved":
-        file2 = 'solved.html'
-        f = open(file2, 'w')
+        file2 = hunt_file(hunt, 'solved.html')
+        f = open(file2 + ".tmp", 'w')
         for line in start + solved_code + end:
             f.write(line)
         f.close()
         for line in start + solved_code + end:
             f.write(line)
         f.close()
+        os.rename(file2 + ".tmp", file2)
     elif filt == "Unsolved":
     elif filt == "Unsolved":
-        file3 = 'unsolved.html'
-        f = open(file3, 'w')
+        file3 = hunt_file(hunt, 'unsolved.html')
+        f = open(file3 + ".tmp", 'w')
         for line in start + unsolved_code + end:
             f.write(line)
         f.close()
         for line in start + unsolved_code + end:
             f.write(line)
         f.close()
+        os.rename(file3 + ".tmp", file3)
     return None
 
     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)