]> git.cworth.org Git - turbot-web/blobdiff - html_generator.py
Add code to auto-refresh each HTML page
[turbot-web] / html_generator.py
index 3ee751c989348e6c5004ec399fa6dc403f4972be..f18703211897d73f90fdec216645e24f22a53e33 100644 (file)
@@ -16,10 +16,30 @@ Requires sorttable.js, which should be included
 """
 import boto3
 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 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."""
@@ -86,7 +106,7 @@ def hunt_info(table, hunt_id):
     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
@@ -118,15 +138,16 @@ def round_stat(rnd, puzzles):
 
 
 
-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',
+     '  <meta http-equiv="refresh" content = "15">\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',
@@ -140,14 +161,15 @@ def overview(puzzles, rounds):
      '  </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>'
-     '      <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 = []
@@ -167,7 +189,7 @@ def overview(puzzles, rounds):
         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',
@@ -181,12 +203,12 @@ def overview(puzzles, rounds):
                 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',
-                '        <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')
@@ -195,14 +217,15 @@ def overview(puzzles, rounds):
     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()
+    os.rename(file + ".tmp", file)
     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.
@@ -214,13 +237,14 @@ def round_overview(rnd, puzzles):
         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',
+     '        <meta http-equiv="refresh" content = "15">\n',
      '    </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',
@@ -246,22 +270,22 @@ def round_overview(rnd, puzzles):
         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(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'.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(internal_link(hunt, 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',
@@ -269,14 +293,15 @@ def round_overview(rnd, puzzles):
     '    </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()
+    os.rename(file + ".tmp", file)
     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']
@@ -285,9 +310,12 @@ def puzzle_overview(puzzle):
     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':
-        solution = puzzle['solution']
+        solution = ", ".join(puzzle['solution']).upper()
         status = 'solved'
     else:
         solution = ""
@@ -297,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',
-     '    <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),
-     '    <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),
@@ -308,7 +337,7 @@ def puzzle_overview(puzzle):
      '        <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',
@@ -316,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
-     '                <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),
@@ -327,15 +356,15 @@ def puzzle_overview(puzzle):
      '\n',
      '</body>\n',
      '</html>\n']
-    # Replace all spaces and slashes in the name with underscores
-    underscored = re.sub(r'[ /]', '_', name)
-    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)
+    f.close()
+    os.rename(file + ".tmp", file)
     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
@@ -343,19 +372,21 @@ def puzzle_lists(puzzles, filt):
     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',
+     '        <meta http-equiv="refresh" content = "15">\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),
-     '        <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',
@@ -378,17 +409,20 @@ def puzzle_lists(puzzles, filt):
         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
 
         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(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("".join(puzzle['tags'])),
+         '                        <td>{}</td>\n'.format(", ".join(puzzle.get('tags',[]))),
          '                    </tr>\n']
     for puzzle in unsolved_puzzles:
         if puzzle['type'] == 'meta':
@@ -396,17 +430,20 @@ def puzzle_lists(puzzles, filt):
         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
 
         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(internal_link(hunt, 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',
@@ -414,35 +451,62 @@ def puzzle_lists(puzzles, filt):
     '    </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()
+        os.rename(file1 + ".tmp", file1)
     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()
+        os.rename(file2 + ".tmp", file2)
     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()
+        os.rename(file3 + ".tmp", file3)
     return None
 
-# Initialize AWS resources to talk to database
+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")
-puzzles, rounds = hunt_info(table, "mh2021")
-
-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")
+
+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)
+
+for hunt_id in sys.argv[1:]:
+    generate_for_hunt_id(table, hunt_id)