From: Carl Worth Date: Wed, 12 Jan 2022 07:52:31 +0000 (-0800) Subject: Initial import of turbot-web code X-Git-Url: https://git.cworth.org/git?p=turbot-web;a=commitdiff_plain;h=b8a3ad3918a6367744d4a7bff7cdb27f12155702 Initial import of turbot-web code Thanks to Avram Gottschlich for writing all of this code. This is as it was delivered to Slack as "big board.zip" on 2022-01-09. --- b8a3ad3918a6367744d4a7bff7cdb27f12155702 diff --git a/html_generator.py b/html_generator.py new file mode 100644 index 0000000..1ba3ac5 --- /dev/null +++ b/html_generator.py @@ -0,0 +1,438 @@ +# -*- coding: utf-8 -*- +""" +Created on Thu Jan 6 23:35:23 2022 + +@author: Avram Gottschlich +""" +""" +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 easy to call the puzzle/round functions each time they're updated, +that would be great + +Requires sorttable.js, which should be included +""" +from turbot.channel import channel_url +from boto3.dynamodb.conditions import Key + +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): + """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( + Key={ + 'hunt_id': hunt_id, + 'SK': 'hunt-{}'.format(hunt_id) + }) + + if 'Item' in response: + return response['Item'] + else: + return None + +def hunt_puzzles_for_hunt_id(turb, hunt_id): + """Return all puzzles that belong to the given hunt_id""" + + response = turb.table.query( + KeyConditionExpression=( + Key('hunt_id').eq(hunt_id) & + Key('SK').begins_with('puzzle-') + ) + ) + + return response['Items'] + +def elink(lin, text): + #shortcutting function for creating html links + #opens in a new tab for external links + return '{}'.format(lin, text) + +def link(lin, text): + #internal links, doesn't open new tab + return '{}'.format(lin, text) + +def hunt_info(turb, hunt): + """ + Retrieves list of rounds, puzzles for the given hunt + """ + name = hunt["name"] + hunt_id = hunt["hunt_id"] + channel_id = hunt["channel_id"] + + puzzles = hunt_puzzles_for_hunt_id(turb, hunt_id) + + rounds = set() + for puzzle in puzzles: + if "rounds" not in puzzle: + continue + for rnd in puzzle["rounds"]: + rounds.add(rnd) + rounds = list(rounds) + rounds.sort() + + return puzzles, rounds + +def round_stat(rnd, puzzles): + #Counts puzzles, solved, list of puzzles for a given round + puzzle_count = 0 + solved_count = 0 + solved_puzzles = [] + unsolved_puzzles = [] + metas = [] + meta_solved = 0 + for puzzle in puzzles: + if "rounds" not in puzzle: + continue + if rnd in puzzle["rounds"]: + if puzzle['type'] == 'meta': + metas.append(puzzle) + if puzzle['status'] == 'solved': + meta_solved += 1 + else: + puzzle_count += 1 + if puzzle['status'] == 'solved': + solved_count += 1 + solved_puzzles.append(puzzle) + else: + unsolved_puzzles.append(puzzle) + solved_puzzles = sorted(solved_puzzles, key = lambda i: i['name']) + 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): + #big board, main page. saves as index.html + start = ['\n', + '\n', + '\n', + ' \n', + ' \n', + '\n', + ' \n', + ' \n', + '\n', + ' Hunt Overview\n', + ' \n' + '\n', + '
\n' + ' Hunt Overview' + ' All Puzzles\n' + ' Unsolved\n' + ' Solved\n' + '
\n' + '\n',] + columns = ['
\n'] + expanding = [] + i = 1 + for rnd in rounds: + puzzle_count, solved_count, rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles) + if metas == meta_solved and metas > 0: + status = 'solved' + else: + status = 'unsolved' + columns += ['
\n'.format(status, i), + '

{}

\n'.format(rnd), + '

Puzzles: {}/{}

\n'.format(solved_count, puzzle_count), + '

Metas: {}/{}

\n'.format(meta_solved, metas), + '
\n'] + + expanding += [ + ' \n') + i += 1 + columns.append('
\n') + end = ['\n', '\n'] + html = start + expanding + columns + end + file = "index.html" + f = open(file, "w") + for line in html: + f.write(line) + f.close() + return None + +def round_overview(rnd, puzzles): + #inputs: round name, puzzles + #round overview page + #saves as (round name)_round.html, in case meta/round share names. + #underscores replace spaces for links. + rnd_puzzles, meta_solved, metas = round_stat(rnd, puzzles)[2:] + if meta_solved == metas and metas > 0: + status = 'solved' + else: + status = 'unsolved' + start = ['\n', + ' \n', + ' \n', + ' Mystery Hunt 2022\n', + ' \n', + ' \n', + ' \n'.format(status), + '

{}

\n'.format(rnd), + '

{}

\n'.format(link(website + "index.html", 'Hunt Overview')), + '
\n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + #' \n', + ' \n', + ' \n', + ' \n', + ' \n'] + puzzle_list = [] + for puzzle in rnd_puzzles: + if puzzle['type'] == 'meta': + meta = ' [META]' + else: + meta = '' + slack_url = channel_url(puzzle['channel_id']) + + if puzzle['status'] == 'solved': + puzzle_list += [ ' \n', + ' \n'.format(elink(slack_url, puzzle['name']+meta)), + ' \n'.format(elink(puzzle['url'], 'Puzzle')), + ' \n'.format(elink(puzzle['sheet_url'], 'Sheet')), + ' \n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')), + ' \n'.format(puzzle['solution']), + # ' \n', + ' \n'.format("".join(puzzle['tags'])), + ' \n'] + else: + puzzle_list += [ ' \n', + ' \n'.format(elink(slack_url, puzzle['name']+meta)), + ' \n'.format(elink(puzzle['url'], 'Puzzle')), + ' \n'.format(elink(puzzle['sheet_url'], 'Sheet')), + ' \n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')), + ' \n', + # ' \n', + ' \n'.format(" ".join(puzzle['tags'])), + ' \n'] + end = [' \n', + '
Puzzle Title/SlackPuzzle LinkSheetOverviewAnswerExtra LinksTags
{}{}{}{}{}{}
{}{}{}{}{}
\n', + '
\n', + ' \n', + '\n'] + html = start + puzzle_list + end + file = "{}_round.html".format('_'.join(rnd.split())) + f = open(file, "w") + for line in html: + f.write(line) + f.close() + return None + +def puzzle_overview(puzzle): + #overview page for individual puzzles. saves as (name of puzzle).html, + #with underscores rather than spaces + name = puzzle['name'] + if puzzle['type'] == 'meta': + meta = ' [META]' + 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 puzzle['status'] == 'solved': + solution = puzzle['solution'] + status = 'solved' + else: + solution = "" + status = 'unsolved' + html = ['\n', + '\n', + '\n', + ' \n', + ' \n', + ' \n', + ' {}\n'.format(name+meta), + '

{}

'.format(link(website + 'index.html', 'Hunt Overview')), + '\n', + '\n', + '\n'.format(status), + '

{}

\n'.format(name+meta), + '
\n', + ' \n', + ' \n', + ' \n'.format(elink(slack_url, 'Channel')), #slack channel + ' \n'.format(elink(puzzle['url'], 'Puzzle')), + ' \n'.format(elink(puzzle['sheet_url'], 'Sheet')), + ' \n', + ' \n', + '
{}{}{}Additional Resources
\n', + ' \n', + ' \n', + ' \n'.format(" ".join(round_url)), #round page on our site + ' \n'.format(" ".join(puzzle['tags'])), #add tags + ' \n', + ' \n', + ' \n'.format(solution), + ' \n'.format(puzzle['status']), + ' \n', + '
Round(s): {}Tags: {}
Answer: {}State: {}
\n', + '
\n', + '\n', + '\n', + '\n'] + underscored = "_".join(name.split()) + file = "{}.html".format(underscored) + f = open(file, "w") + for line in html: + f.write(line) + return None + +def puzzle_lists(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 + solved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] == 'solved'] + unsolved_puzzles = [puzzle for puzzle in puzzles if puzzle['status'] != 'solved'] + start = ['\n', + ' \n', + ' \n', + ' Mystery Hunt 2022\n', + ' \n', + ' \n', + '
\n' + ' Hunt Overview' + ' All Puzzles\n' + ' Unsolved\n' + ' Solved\n' + '
\n' + ' \n', + '

{}

\n'.format('{} Puzzles').format(filt), + '

{}

\n'.format(link(website + "index.html", 'Hunt Overview')), + '
\n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n', + ' \n'] + solved_code = [] + unsolved_code = [] + for puzzle in solved_puzzles: + if puzzle['type'] == 'meta': + meta = ' [META]' + else: + meta = '' + slack_url = channel_url(puzzle['channel_id']) + round_url = link(website + "_".join(rnd.split()) + "_round.html", puzzle['rounds'][0]) + #assuming one round per puzzle for now + + solved_code += [' \n', + ' \n'.format(elink(slack_url, puzzle['name']+meta)), + ' \n'.format(elink(puzzle['url'], 'Puzzle')), + ' \n'.format(elink(puzzle['sheet_url'], 'Sheet')), + ' \n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')), + ' \n'.format(puzzle['solution']), + ' \n'.format(round_url), + ' \n'.format("".join(puzzle['tags'])), + ' \n'] + for puzzle in unsolved_puzzles: + if puzzle['type'] == 'meta': + meta = ' [META]' + else: + meta = '' + slack_url = channel_url(puzzle['channel_id']) + round_url = link(website + "_".join(rnd.split()) + "_round.html", puzzle['rounds'][0]) + #assuming one round per puzzle for now + + unsolved_code += [' \n', + ' \n'.format(elink(slack_url, puzzle['name']+meta)), + ' \n'.format(elink(puzzle['url'], 'Puzzle')), + ' \n'.format(elink(puzzle['sheet_url'], 'Sheet')), + ' \n'.format(link(website + "_".join(puzzle['name'].split()) + '.html', 'Overview')), + ' \n', + ' \n'.format(round_url), + ' \n'.format("".join(puzzle['tags'])), + ' \n'] + end = [' \n', + '
Puzzle Title/SlackPuzzle LinkSheetOverviewAnswerRound(s)Tags
{}{}{}{}{}{}{}
{}{}{}{}{}{}
\n', + '
\n', + ' \n', + '\n'] + if filt == "All": + file1 = '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' + f = open(file2, 'w') + for line in start + solved_code + end: + f.write(line) + f.close() + elif filt == "Unsolved": + file3 = 'unsolved.html' + f = open(file3, 'w') + for line in start + unsolved_code + end: + f.write(line) + f.close() + return None + + + +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") \ No newline at end of file diff --git a/individual.css b/individual.css new file mode 100644 index 0000000..b525932 --- /dev/null +++ b/individual.css @@ -0,0 +1,66 @@ +.solved { + background-color: chartreuse; +} + +.unsolved { + background-color: orange; +} + +table { + border-collapse: collapse; + margin: 30px; + text-align: center; + + +} + +table td { + border: 1px solid black; + padding: 20px; + font-size: 150%; +} + + +h1 { + text-align: center; + font-size: 300%; +} + +.center { + margin-left: auto; + margin-right: auto; +} + +.row { + margin-left: -5px; + margin-right: -5px; +} + +.column{ + float: left; + width: 50%; + padding: 5px; +} + +/* Clearfix (clear floats) */ +.row::after { + content: ""; + clear: both; + display: table; +} + +.grid-container { + display: grid; + grid-template-columns: auto auto auto; + grid-gap: 10px; + background-color: #2196F3; + padding: 10px; +} + +.grid-container > div { + background-color: rgba(255, 255, 255, 0.8); + text-align: center; + padding: 20px 0; + font-size: 30px; +} + diff --git a/overview.css b/overview.css new file mode 100644 index 0000000..b3061da --- /dev/null +++ b/overview.css @@ -0,0 +1,94 @@ +/* The grid: Three equal columns that floats next to each other */ +.column { + float: left; + width: 15%; + padding: 25px; + text-align: center; + font-size: 20px; + cursor: pointer; + color: black; + border-radius: 25px; + margin: 25px; +} + +.containerTab { + padding: 20px; + color: black; + border-radius: 25px; + margin: auto; + width: 20%; + + +} + +/* Clear floats after the columns */ +.row:after { + content: ""; + display: table; + clear: both; +} + +/* Closable button inside the image */ +.closebtn { + float: right; + color: black; + font-size: 35px; + cursor: pointer; +} + +table { + color: black; + padding: 5px; +} + +th, td { + padding: 5px; +} + +.solved { + background-color: chartreuse; +} + +.unsolved { + background-color: orange; +} + +/* The sidebar menu */ +.sidenav { + height: 100%; /* Full-height: remove this if you want "auto" height */ + width: 160px; /* Set the width of the sidebar */ + position: fixed; /* Fixed Sidebar (stay in place on scroll) */ + z-index: 1; /* Stay on top */ + top: 0; /* Stay at the top */ + left: 0; + background-color: #111; /* Black */ + overflow-x: hidden; /* Disable horizontal scroll */ + padding-top: 20px; +} + +/* The navigation menu links */ +.sidenav a { + padding: 6px 8px 6px 16px; + text-decoration: none; + font-size: 25px; + color: #818181; + display: block; +} + +/* When you mouse over the navigation links, change their color */ +.sidenav a:hover { + color: #f1f1f1; +} + +/* Style page content */ +body { + margin-left: 160px; /* Same as the width of the sidebar */ + padding: 0px 10px; + background-color: #BBB; +} + +/* On smaller screens, where height is less than 450px, change the style of the sidebar (less padding and a smaller font size) */ +@media screen and (max-height: 450px) { + .sidenav {padding-top: 15px;} + .sidenav a {font-size: 18px;} +} diff --git a/sorttable.js b/sorttable.js new file mode 100644 index 0000000..d75609f --- /dev/null +++ b/sorttable.js @@ -0,0 +1,494 @@ +/* + SortTable + version 2 + 7th April 2007 + Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/ + + Instructions: + Download this file + Add to your HTML + Add class="sortable" to any table you'd like to make sortable + Click on the headers to sort + + Thanks to many, many people for contributions and suggestions. + Licenced as X11: http://www.kryogenix.org/code/browser/licence.html + This basically means: do what you want with it. +*/ + + +var stIsIE = /*@cc_on!@*/false; + +sorttable = { + init: function() { + // quit if this function has already been called + if (arguments.callee.done) return; + // flag this function so we don't do the same thing twice + arguments.callee.done = true; + // kill the timer + if (_timer) clearInterval(_timer); + + if (!document.createElement || !document.getElementsByTagName) return; + + sorttable.DATE_RE = /^(\d\d?)[\/\.-](\d\d?)[\/\.-]((\d\d)?\d\d)$/; + + forEach(document.getElementsByTagName('table'), function(table) { + if (table.className.search(/\bsortable\b/) != -1) { + sorttable.makeSortable(table); + } + }); + + }, + + makeSortable: function(table) { + if (table.getElementsByTagName('thead').length == 0) { + // table doesn't have a tHead. Since it should have, create one and + // put the first table row in it. + the = document.createElement('thead'); + the.appendChild(table.rows[0]); + table.insertBefore(the,table.firstChild); + } + // Safari doesn't support table.tHead, sigh + if (table.tHead == null) table.tHead = table.getElementsByTagName('thead')[0]; + + if (table.tHead.rows.length != 1) return; // can't cope with two header rows + + // Sorttable v1 put rows with a class of "sortbottom" at the bottom (as + // "total" rows, for example). This is B&R, since what you're supposed + // to do is put them in a tfoot. So, if there are sortbottom rows, + // for backwards compatibility, move them to tfoot (creating it if needed). + sortbottomrows = []; + for (var i=0; i5' : ' ▴'; + this.appendChild(sortrevind); + return; + } + if (this.className.search(/\bsorttable_sorted_reverse\b/) != -1) { + // if we're already sorted by this column in reverse, just + // re-reverse the table, which is quicker + sorttable.reverse(this.sorttable_tbody); + this.className = this.className.replace('sorttable_sorted_reverse', + 'sorttable_sorted'); + this.removeChild(document.getElementById('sorttable_sortrevind')); + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + return; + } + + // remove sorttable_sorted classes + theadrow = this.parentNode; + forEach(theadrow.childNodes, function(cell) { + if (cell.nodeType == 1) { // an element + cell.className = cell.className.replace('sorttable_sorted_reverse',''); + cell.className = cell.className.replace('sorttable_sorted',''); + } + }); + sortfwdind = document.getElementById('sorttable_sortfwdind'); + if (sortfwdind) { sortfwdind.parentNode.removeChild(sortfwdind); } + sortrevind = document.getElementById('sorttable_sortrevind'); + if (sortrevind) { sortrevind.parentNode.removeChild(sortrevind); } + + this.className += ' sorttable_sorted'; + sortfwdind = document.createElement('span'); + sortfwdind.id = "sorttable_sortfwdind"; + sortfwdind.innerHTML = stIsIE ? ' 6' : ' ▾'; + this.appendChild(sortfwdind); + + // build an array to sort. This is a Schwartzian transform thing, + // i.e., we "decorate" each row with the actual sort key, + // sort based on the sort keys, and then put the rows back in order + // which is a lot faster because you only do getInnerText once per row + row_array = []; + col = this.sorttable_columnindex; + rows = this.sorttable_tbody.rows; + for (var j=0; j 12) { + // definitely dd/mm + return sorttable.sort_ddmm; + } else if (second > 12) { + return sorttable.sort_mmdd; + } else { + // looks like a date, but we can't tell which, so assume + // that it's dd/mm (English imperialism!) and keep looking + sortfn = sorttable.sort_ddmm; + } + } + } + } + return sortfn; + }, + + getInnerText: function(node) { + // gets the text we want to use for sorting for a cell. + // strips leading and trailing whitespace. + // this is *not* a generic getInnerText function; it's special to sorttable. + // for example, you can override the cell text with a customkey attribute. + // it also gets .value for fields. + + if (!node) return ""; + + hasInputs = (typeof node.getElementsByTagName == 'function') && + node.getElementsByTagName('input').length; + + if (node.getAttribute("sorttable_customkey") != null) { + return node.getAttribute("sorttable_customkey"); + } + else if (typeof node.textContent != 'undefined' && !hasInputs) { + return node.textContent.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.innerText != 'undefined' && !hasInputs) { + return node.innerText.replace(/^\s+|\s+$/g, ''); + } + else if (typeof node.text != 'undefined' && !hasInputs) { + return node.text.replace(/^\s+|\s+$/g, ''); + } + else { + switch (node.nodeType) { + case 3: + if (node.nodeName.toLowerCase() == 'input') { + return node.value.replace(/^\s+|\s+$/g, ''); + } + case 4: + return node.nodeValue.replace(/^\s+|\s+$/g, ''); + break; + case 1: + case 11: + var innerText = ''; + for (var i = 0; i < node.childNodes.length; i++) { + innerText += sorttable.getInnerText(node.childNodes[i]); + } + return innerText.replace(/^\s+|\s+$/g, ''); + break; + default: + return ''; + } + } + }, + + reverse: function(tbody) { + // reverse the rows in a tbody + newrows = []; + for (var i=0; i=0; i--) { + tbody.appendChild(newrows[i]); + } + delete newrows; + }, + + /* sort functions + each sort function takes two parameters, a and b + you are comparing a[0] and b[0] */ + sort_numeric: function(a,b) { + aa = parseFloat(a[0].replace(/[^0-9.-]/g,'')); + if (isNaN(aa)) aa = 0; + bb = parseFloat(b[0].replace(/[^0-9.-]/g,'')); + if (isNaN(bb)) bb = 0; + return aa-bb; + }, + sort_alpha: function(a,b) { + if (a[0]==b[0]) return 0; + if (a[0] 0 ) { + var q = list[i]; list[i] = list[i+1]; list[i+1] = q; + swap = true; + } + } // for + t--; + + if (!swap) break; + + for(var i = t; i > b; --i) { + if ( comp_func(list[i], list[i-1]) < 0 ) { + var q = list[i]; list[i] = list[i-1]; list[i-1] = q; + swap = true; + } + } // for + b++; + + } // while(swap) + } +} + +/* ****************************************************************** + Supporting functions: bundled here to avoid depending on a library + ****************************************************************** */ + +// Dean Edwards/Matthias Miller/John Resig + +/* for Mozilla/Opera9 */ +if (document.addEventListener) { + document.addEventListener("DOMContentLoaded", sorttable.init, false); +} + +/* for Internet Explorer */ +/*@cc_on @*/ +/*@if (@_win32) + document.write("