Put this project under version control, finally.

Scripts for rlgallery.org, using a PostgreSQL backend.  The recorder
system is in py/, CGI scripts are in web/.
This commit is contained in:
John "Elwin" Edwards 2012-07-25 21:59:42 -07:00
commit ddf0ec25b0
6 changed files with 937 additions and 0 deletions

20
py/cleandb.py Normal file
View file

@ -0,0 +1,20 @@
#!/usr/bin/python
# cleandb.py: empty the database in an orderly fashion
import rlgalldb as rlgall
import psycopg2
dbconn = psycopg2.connect("dbname=rlg")
dbcur = dbconn.cursor()
dbcur.execute("UPDATE games SET offbytes = %s", [0])
for game in rlgall.gamelist:
dbcur.execute("DELETE FROM " + game.uname + ";")
dbcur.execute("DELETE FROM players;")
dbconn.commit()
dbcur.close()
dbconn.close()
exit()

46
py/recorder.py Executable file
View file

@ -0,0 +1,46 @@
#!/usr/bin/python
import os
import time
import calendar
import psycopg2
import rlgalldb as rlgall
# Contains a dir for everyone who registered
everydir = "/var/dgl/dgldir/ttyrec/"
# Contains a page for everyone we know about
#knowndir = rlgall.dbdir + "players/"
# Contact the database
conn = psycopg2.connect("dbname=rlg")
cur = conn.cursor()
# newnames is the list of newly registered players who are not yet in the
# database. updatenames is the set of players whose pages need updating.
cur.execute("SELECT pname FROM players;")
playersInDB = [ row[0] for row in cur.fetchall() ]
playersAll = os.listdir(everydir)
newnames = [ name for name in playersAll if name not in playersInDB ]
updatenames = set(newnames)
# Add the new names to the database
for newplayer in newnames:
cur.execute("INSERT INTO players VALUES (%s);", [newplayer])
conn.commit()
cur.close()
conn.close()
# Update the database for each game.
for game in rlgall.gamelist:
updatenames.update(game.loadnew())
# All the databases have been updated. Now make the pages.
# Currently the high scores for all the games are on the same page. If
# they split up, this will have to change to a Game method.
rlgall.highpage()
for name in updatenames:
rlgall.playerpage(name)
exit()

425
py/rlgalldb.py Normal file
View file

@ -0,0 +1,425 @@
# rlgalldb.py
# Module for the Roguelike Gallery, using a postgres database
import os
import sys
import time
import calendar
import re
import psycopg2
from datetime import datetime, tzinfo, timedelta
# Configuration
logdir = "/var/dgl/var/games/roguelike/"
webdir = "/var/www/lighttpd/scoring/"
ppagename = webdir + "players/{0}.html"
hpagename = webdir + "highscores.html"
# HTML fragments for templating
phead = """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html><head>
<title>{0}</title>
<link rel="stylesheet" href="/scoring/scores.css" type="text/css">
</head>
"""
ptop = """<body>
<h1>Yendor Guild</h1>
"""
navtop = '<div class="nav"><a href="/">rlgallery.org</a> -&gt; {0}</div>\n'
navscore = '<div class="nav"><a href="/">rlgallery.org</a> -&gt; \
<a href="/scoring/">Scores</a> -&gt; {0}</div>\n'
navplayer = '<div class="nav"><a href="/">rlgallery.org</a> -&gt; \
<a href="/scoring/">Scores</a> -&gt; <a href="/scoring/players/">Players</a> \
-&gt; {0}</div>'
pti = '<h2>{0}</h2>\n'
secthead = '<h3>{0}</h3>\n'
tblhead = '<div class="stable">\n'
rowstart = '<div class="sentry">\n'
rowend = '</div>\n'
cell = ' <span class="sdata">{0}</span>\n'
hcell = ' <span class="shdata">{0}</span>\n'
tblend = '</div>\n'
pend = "</body></html>\n"
# This would be more useful if we had to do translation
headerbook = {"etime":"End time", "score":"Score", "name":"Name", "xl":"XL",
"fate":"Fate", "rank":"Rank", "game":"Game"}
# Queries for the games table
offselstr = "SELECT offbytes FROM games WHERE gname = %s;"
newoffstr = "UPDATE games SET offbytes = %s WHERE gname = %s;"
# A representation of the UTC time zone. They say Py3k can better handle
# this madness.
class UTC(tzinfo):
def utcoffset(self, dt):
return timedelta(0)
def dst(self, dt):
return timedelta(0)
def tzname(self, dt):
return "UTC"
utc = UTC()
def getconn():
"Returns a database connection, or None if the connection fails."
try:
conn = psycopg2.connect("dbname=rlg")
except psycopg2.OperationalError:
return None
return conn
def recnameToInt(filename):
recre = r"(\d{4})-(\d{2})-(\d{2})\.(\d{2}):(\d{2}):(\d{2})\.ttyrec$"
match = re.match(recre, filename)
if not match:
return None
return calendar.timegm([int(val) for val in match.groups()])
def recnameToTS(filename):
pattern = "%Y-%m-%d.%H:%M:%S.ttyrec"
try:
dt = datetime.strptime(filename, pattern)
dt.replace(tzinfo=utc)
return dt
except ValueError:
return None
def ttyreclink(text, name, game, gtime):
"Returns a link to the ttyrec archivist"
lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>'
return lstr.format(name, game, gtime, text)
def playerlink(name):
"Returns a link to a player's page"
lstr = '<a href="/scoring/players/' + name + '.html">' + name + '</a>'
return lstr
def linktoArchive(entry):
"Takes an entry dict and returns a link to the ttyrec archivist."
lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>'
#linktext = time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(entry["etime"]))
linktext = entry["etstamp"].strftime("%Y/%m/%d %H:%M:%S")
return lstr.format(entry["name"], entry["game"].uname, entry["etime"],
linktext)
def maketablerow(cells, isheader=None):
"Takes a list of strings and returns a HTML table row with each string \
in its own cell. isheader will make them header cells, obviously."
if isheader:
celler = hcell
else:
celler = cell
rowstr = rowstart
for entry in cells:
rowstr = rowstr + celler.format(entry)
rowstr = rowstr + rowend
return rowstr
def printTable(entries, fields, of):
"Takes a list of entry dicts and a list of field strings and writes a \
HTML table to of."
of.write(tblhead)
clist = []
for field in fields:
if field in headerbook:
clist.append(headerbook[field])
else:
clist.append("{0}".format(field))
of.write(maketablerow(clist, True))
rnum = 0
for i, entry in enumerate(entries):
clist = []
for field in fields:
if field == "rank":
clist.append("{0}".format(i + 1))
elif field in entry:
thing = entry[field]
if field == "game":
clist.append(thing.name)
elif field == "xl" or field == "score": # Numerics
clist.append(str(thing))
elif field == "name":
clist.append(playerlink(thing))
elif field == "fate":
clist.append(thing)
elif field == "etime":
clist.append(linktoArchive(entry))
else:
clist.append("{0}".format(thing))
else:
clist.append("N/A")
of.write(maketablerow(clist))
of.write(tblend)
return
def readentries(entfile, entlist):
"Reads a list of entries from a file object"
while True:
nextentry = entfile.readline()
if not nextentry:
break
if nextentry[-1] != '\n': # The line is incomplete
break
entlist.append(nextentry.split(None, 4))
return
class Game:
pass
class RogueGame(Game):
def __init__(self, name, uname, suffix):
self.name = name
self.uname = uname
self.scores = logdir + uname + ".log"
self.logspec = ["etime", "score", "name", "xl", "fate"]
self.sqltypes = {"etime": "int", "score": "int", "name": "varchar(20)",
"xl": "int", "fate": "text", "stime": "int",
"ttyrecs": "text ARRAY", "ststamp": "timestamptz",
"etstamp": "timestamptz"}
self.logdelim = " "
# Class variables, used by some methods
fields = ["name", "score", "xl", "fate", "etime"]
rankfields = ["rank", "score", "name", "xl", "fate", "etime"]
pfields = ["score", "xl", "fate", "etime"]
def logtoDict(self, entry):
"Processes a log entry string, returning a dict."
ndict = {"game": self}
entrylist = entry.strip().split(self.logdelim, len(self.logspec) - 1)
for item, value in zip(self.logspec, entrylist):
if self.sqltypes[item] == "int":
ndict[item] = int(value)
else:
ndict[item] = value
return ndict
def getEntryDicts(self, entfile, entlist):
"Reads logfile entries from entfile, interprets them according to the \
instructions in self.logspec/logdelim, and adds them as dicts to entlist."
while True:
nextentry = entfile.readline()
if not nextentry:
break
if nextentry[-1] != '\n':
break
entlist.append(self.logtoDict(nextentry))
return
def getRecent(self, n=20):
"Gets the n most recent games out of the database, returning a list \
of dicts."
try:
n = int(n)
except (ValueError, TypeError):
return []
if n <= 0:
return []
dictlist = []
conn = psycopg2.connect("dbname=rlg")
cur = conn.cursor()
qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM {0} ORDER BY etstamp DESC \
LIMIT %s;".format(self.uname)
cur.execute(qstr, [n])
for record in cur:
# This should be less hardcodish
ndict = {"game": self}
ndict["etime"] = record[0]
ndict["score"] = record[1]
ndict["name"] = record[2]
ndict["xl"] = record[3]
ndict["fate"] = record[4]
ndict["ststamp"] = record[5]
ndict["etstamp"] = record[6]
dictlist.append(ndict)
cur.close()
conn.close()
return dictlist
def getHigh(self, n=10, offset=0):
"Gets the n highest scores (starting at offset) from the database, \
returning a list of dicts."
qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM {0} ORDER BY score DESC\
".format(self.uname)
qvals = []
try:
n = int(n)
except (ValueError, TypeError):
return []
if n <= 0:
return []
qstr += " LIMIT %s"
qvals.append(n)
try:
offset = int(offset)
except (ValueError, TypeError):
return []
if n > 0:
qstr += " OFFSET %s"
qvals.append(offset)
qstr += ";"
conn = psycopg2.connect("dbname=rlg")
cur = conn.cursor()
cur.execute(qstr, qvals)
dictlist = []
for record in cur:
ndict = {"game": self}
ndict["etime"] = record[0]
ndict["score"] = record[1]
ndict["name"] = record[2]
ndict["xl"] = record[3]
ndict["fate"] = record[4]
ndict["ststamp"] = record[5]
ndict["etstamp"] = record[6]
dictlist.append(ndict)
cur.close()
conn.close()
return dictlist
def getPlayer(self, player):
"Gets all player's games from the database."
qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM " + self.uname + " WHERE \
name = %s;"
conn = getconn()
if conn == None:
return []
cur = conn.cursor()
entries = []
cur.execute(qstr, [player])
for record in cur:
ndict = {"game": self}
ndict["etime"] = record[0]
ndict["score"] = record[1]
ndict["name"] = record[2]
ndict["xl"] = record[3]
ndict["fate"] = record[4]
ndict["ststamp"] = record[5]
ndict["etstamp"] = record[6]
entries.append(ndict)
cur.close()
conn.close()
return entries
def putIntoDB(self, dictlist, conn):
"Add the entries in dictlist to the database through connection conn, \
which needs the INSERT privilege."
# FIXME this monster needs to be procedurally generated
qstr = "INSERT INTO " + self.uname + " (etime, score, name, xl, fate, stime, ttyrecs, ststamp, etstamp) \
VALUES (%(etime)s, %(score)s, %(name)s, %(xl)s, %(fate)s, %(stime)s, %(ttyrecs)s, %(ststamp)s, %(etstamp)s);"
cur = conn.cursor()
cur.executemany(qstr, [ d for d in dictlist if d["game"] == self ])
conn.commit()
cur.close()
return
def postprocess(self, gamelist):
names = set([ e["name"] for e in gamelist ])
conn = getconn()
if conn == None:
return []
cur = conn.cursor()
for nameF in names:
# Get all this player's games ordered by time
itsEntries = [ entry for entry in gamelist if entry["name"] == nameF ]
itsEntries.sort(key=lambda e: e["etime"])
# Find the end time of the latest game already in the db
tquery = "SELECT etstamp FROM {0} WHERE name = %s ORDER BY etstamp DESC LIMIT 1;".format(self.uname)
cur.execute(tquery, [nameF])
result = cur.fetchone()
if result:
prev = calendar.timegm(result[0].utctimetuple())
else:
prev = 0
ttyrecdir = "/var/dgl/dgldir/ttyrec/{0}/{1}/".format(nameF, self.uname)
allfilekeys = [ (recnameToInt(f), f) for f in os.listdir(ttyrecdir) ]
vfilekeys = [ e for e in allfilekeys if e[0] > prev ]
vfilekeys.sort(key=lambda e: e[0])
# Now determine stime and ttyrecs for each game
for i in range(0, len(itsEntries)):
if i == 0:
lowlim = prev
else:
lowlim = itsEntries[i-1]["etime"]
hilim = itsEntries[i]["etime"]
recs = [ k[1] for k in vfilekeys if lowlim <= k[0] < hilim ]
itsEntries[i]["stime"] = recnameToInt(recs[0])
itsEntries[i]["ttyrecs"] = recs
# While we're at it, replace numeric timestamps with SQL timestamps.
itsEntries[i]["ststamp"] = datetime.fromtimestamp(itsEntries[i]["stime"], utc)
itsEntries[i]["etstamp"] = datetime.fromtimestamp(itsEntries[i]["etime"], utc)
cur.close()
conn.close()
def loadnew(self):
conn = getconn()
if conn == None:
return []
cur = conn.cursor()
# Get the previous offset
cur.execute(offselstr, [self.uname])
offset = cur.fetchone()[0]
newlist = []
try:
scr = open(self.scores)
scr.seek(offset)
self.getEntryDicts(scr, newlist)
except IOError:
noffset = offset # Can't read anything, assume no new games
else:
noffset = scr.tell()
scr.close()
cur.execute(newoffstr, [noffset, self.uname])
# The new players must already be added to the players table.
updatenames = set([ e["name"] for e in newlist ])
self.postprocess(newlist)
self.putIntoDB(newlist, conn)
cur.close()
conn.close()
return updatenames
# End RogueGame class definition
rogue3 = RogueGame("Rogue V3", "rogue3", "r3")
rogue4 = RogueGame("Rogue V4", "rogue4", "r4")
rogue5 = RogueGame("Rogue V5", "rogue5", "r5")
srogue = RogueGame("Super-Rogue", "srogue", "sr")
gamelist = [rogue3, rogue4, rogue5, srogue]
def playerpage(pname):
"Generate a player's HTML page"
# Write the beginning of the page
ppagefi = open(ppagename.format(pname), "w")
ppagefi.write(phead.format(pname))
ppagefi.write(ptop)
ppagefi.write(navplayer.format(pname))
ppagefi.write(pti.format("Results for " + pname))
for game in gamelist:
ppagefi.write(secthead.format(game.name))
entries = game.getPlayer(pname)
if not entries:
ppagefi.write("<div>" + pname + " has not yet completed an expedition\
in this dungeon.</div>\n")
else:
printTable(entries, RogueGame.pfields, ppagefi)
scoresum = 0
for entry in entries:
scoresum += int(entry["score"])
avgscr = scoresum / len(entries)
ppagefi.write("<p>Average score: {0}</p>\n".format(avgscr))
# That should settle it. Print the end; then stop.
ppagefi.write(pend)
ppagefi.close()
return
def highpage():
# open the page and print the beginning
highpfi = open(hpagename, "w")
highpfi.write(phead.format("High Scores"))
highpfi.write(ptop)
highpfi.write(navscore.format("High Scores"))
highpfi.write(pti.format("List of High Scores"))
for game in gamelist:
highpfi.write(secthead.format(game.name))
entries = game.getHigh(40)
if not entries:
highpfi.write("<div>No one has braved this dungeon yet.</div>\n")
else:
printTable(entries, game.rankfields, highpfi)
# That should finish it.
highpfi.write(pend)
highpfi.close()
return

37
py/setupdb.py Normal file
View file

@ -0,0 +1,37 @@
#!/usr/bin/python
# setupdb.py: initializes the database tables used by the rlg system.
import rlgalldb as rlgall
import psycopg2
webuser = "webserver"
allowquery = "GRANT SELECT ON {0} TO " + webuser + ";"
dbconn = psycopg2.connect("dbname=rlg")
dbcur = dbconn.cursor()
dbcur.execute("CREATE TABLE games ( gname varchar(20), offbytes int );")
dbcur.execute("CREATE TABLE players (pname varchar(20) PRIMARY KEY);")
dbconn.commit()
for game in rlgall.gamelist:
dbcur.execute("INSERT INTO games VALUES (%s, %s);", (game.uname, 0))
createquery = "CREATE TABLE " + game.uname + " ( "
for i, field in enumerate(game.sqltypes.keys()):
createquery += "{0} {1}".format(field, game.sqltypes[field])
if field == "name":
createquery += " REFERENCES players(pname)"
if i == len(game.sqltypes) - 1:
createquery += " )"
else:
createquery += ", "
createquery += ";"
#print createquery
dbcur.execute(createquery)
dbcur.execute(allowquery.format(game.uname))
dbconn.commit()
dbcur.close()
dbconn.close()
exit()

35
py/stats.py Normal file
View file

@ -0,0 +1,35 @@
import math
import psycopg2
import rlgalldb as rlgall
def makeExpPDF(lbd):
def lpdf(x):
return lbd * math.exp(-lbd * x)
return lpdf
def makeExpCDF(lbd):
def lcdf(x):
return 1 - math.exp(-lbd * x)
return lcdf
conn = psycopg2.connect("dbname=rlg")
cur = conn.cursor()
for game in rlgall.gamelist:
query = "SELECT score FROM {0};".format(game.uname)
cur.execute(query)
scores = [ r[0] for r in cur.fetchall() ]
count = len(scores)
total = sum(scores)
lbd = float(count) / total
cdf = makeExpCDF(lbd)
print "{0}: {1} games, average {2}".format(game.name, count, int(1/lbd))
for i in range(0, 10000, 1000):
actual = len([ s for s in scores if i <= s < i + 1000 ])
predicted = (cdf(i + 1000) - cdf(i)) * count
print "{0:5}: {1:4} {2}".format(i, actual, predicted)
high = max(scores)
print "Max: {0} {1}\n".format(high, 1 - cdf(high))
cur.close()
conn.close()
exit()

374
web/archive.cgi Executable file
View file

@ -0,0 +1,374 @@
#!/usr/bin/python
import cgi
import os
import sys
import re
import time
import calendar
import rlgalldb as rlgall
import cgitb
cgitb.enable()
infop = """<p>Looking for ttyrec files? Don't like digging through Web directory listings, converting time zones in your head, or guessing how many files
the game was spread across? The Archivist can find them all for you.</p>
"""
ttyrecbase = "/var/dgl/dgldir/ttyrec/"
recre = r"(\d{4,4})-(\d{2,2})-(\d{2,2})\.(\d{2,2}):(\d{2,2}):(\d{2,2})\.ttyrec"
recrec = re.compile(recre)
def name_in_time(filename, timerange):
"Checks whether filename is a ttyrec with time between Unix times \
timerange[0] and timerange[1]."
nmatch = recrec.match(filename)
if not nmatch:
return False
ntime = calendar.timegm([int(val) for val in nmatch.groups()])
if ntime > timerange[0] and ntime <= timerange[1]:
return True
else:
return False
def input_game(outf, selected=None):
"Prints the form components for selecting a game."
selstr = '<option label="{0}" value="{1}" selected="selected">{0}</option>\n'
unselstr = '<option label="{0}" value="{1}">{0}</option>\n'
outf.write('<div>Dungeon name: <select name="game">\n')
for game in rlgall.gamelist:
# This will cause trouble if someone puts games with identical names
# into rlgall.
if selected and selected == game.uname:
outf.write(selstr.format(game.name, game.uname))
else:
outf.write(unselstr.format(game.name, game.uname))
outf.write('</select></div>\n')
return
def input_name(outf, defaultval=None):
defstr = '<div>Adventurer\'s name: <input type="text" name="name" value="{0}"></div>\n'
if defaultval:
outf.write(defstr.format(defaultval))
else:
outf.write('<div>Adventurer\'s Name: <input type="text" name="name"></div>\n')
return
def input_time(outf, defaultval=None):
defstr = '<div>Time: <input type="text" name="time" value="{0}"></div>\n'
if defaultval:
outf.write(defstr.format(defaultval))
else:
outf.write('<div>Time: <input type="text" name="time"></div>\n')
return
def input_datetime(outf, dvals=[None, None, None, None, None, None]):
optstr = '<option value="{0}" label="{1}">{1}</option>\n'
soptstr = '<option value="{0}" label="{1}" selected="selected">{1}</option>\n'
tf = '<input type="text" size="2" maxlength="2" name="{0}" value="{1:02}">'
emptf = '<input type="text" size="2" maxlength="2" name="{0}">'
sstr = '<div>Date: <select name="year">\n'
# Default to today
now = time.gmtime()
for dindex in range(3):
if not dvals[dindex]:
dvals[dindex] = now[dindex]
for year in range(2010, now[0] + 1):
if year == dvals[0]:
sstr += soptstr.format(year, year)
else:
sstr += optstr.format(year, year)
sstr += '</select>\n'
outf.write(sstr)
sstr = '<select name="month">\n'
for month in range(1, 13):
if month == dvals[1]:
sstr += soptstr.format(month, calendar.month_name[month])
else:
sstr += optstr.format(month, calendar.month_name[month])
sstr += '</select>\n'
outf.write(sstr)
sstr = '<select name="day">\n'
for day in range(1, 32):
if day == dvals[2]:
sstr += soptstr.format(day, day)
else:
sstr += optstr.format(day, day)
sstr += '</select></div>\n'
outf.write(sstr)
sstr = '<div>Approximate time: '
if dvals[3] != None:
sstr += tf.format("hour", dvals[3])
else:
sstr += emptf.format("hour")
sstr += " : "
if dvals[4] != None:
sstr += tf.format("minute", dvals[4])
else:
sstr += emptf.format("minute")
sstr += " : "
if dvals[5] != None:
sstr += tf.format("second", dvals[5])
else:
sstr += emptf.format("second")
sstr += ' (optional)</div>\n'
outf.write(sstr)
return
def checkempty(cgidata):
exkeys = set(cgidata.keys())
wanted = set(["name", "game", "time", "year", "month", "day", "hour",
"minute", "second"])
return wanted.isdisjoint(exkeys)
def processname(fdata, errlist):
"Takes a CGI data object, extracts the player name, checks its validity, \
and returns it. If errors are encountered, they are appended to errlist."
cantfind = "I have no record of an adventurer called {0}."
if "name" not in fdata:
errlist.append("You didn\'t tell me the adventurer\'s name.")
return None
# Just in case someone supplies something nasty, make sure they can't go
# digging all over the filesystem.
formname = fdata.getfirst("name").rpartition("/")[2]
try:
os.stat(ttyrecbase + formname)
except OSError:
errlist.append(cantfind.format(cgi.escape(formname)))
return None
return formname
def processgame(fdata, errlist):
"Takes a CGI data object and returns the game from rlgall.gamelist that \
it asks for. Errors appended to errlist."
cantfind = "I've never heard of a dungeon called {0}."
if "game" not in fdata:
errlist.append('You didn\'t choose a dungeon.')
return None
formgame = fdata.getfirst("game")
for agame in rlgall.gamelist:
if agame.uname == formgame:
return agame
errlist.append(cantfind.format(cgi.escape(formgame)))
return None
def processtime(fdata, errlist, hlist):
"Takes a CGI data object and converts to a Unix timestamp by finding fields \
called year, month, etc. Any errors get appended to errlist. hlist \
should contain 6 components, for ymd-hms fields."
# Timestamp overrides human-readable, even if it's invalid.
badtime = 'The time field is for Unix clocks, which never say anything \
like {0}. Try the form instead.'
utime = None
formtime = fdata.getfirst("time")
if formtime:
try:
utime = int(formtime)
except ValueError:
errlist.append(badtime.format(cgi.escape(formtime)))
return None
else:
if utime < 0:
utime = 0
if utime != None:
chtime = time.gmtime(utime)
for i in range(6):
hlist[i] = chtime[i]
return utime
# Now try to get a human-readable specification.
lerrors = []
year = month = day = hour = minute = second = None
fyear = fdata.getfirst("year")
if not fyear:
lerrors.append("No year was provided.")
else:
try:
year = int(fyear)
except ValueError:
lerrors.append("Invalid year.")
else:
if year < 2010:
lerrors.append("The Gallery has only existed since 2010.")
elif year > time.gmtime()[0]:
lerrors.append("I can't see into the future.")
else:
hlist[0] = year # It is valid
fmonth = fdata.getfirst("month")
if not fmonth:
lerrors.append("No month was provided.")
else:
try:
month = int(fmonth)
except ValueError:
lerrors.append("Invalid month.")
else:
if month < 1 or month > 12:
lerrors.append("Invalid month.")
else:
hlist[1] = month
fday = fdata.getfirst("day")
if not fday:
lerrors.append("No day was provided.")
else:
try:
day = int(fday)
except ValueError:
lerrors.append("Invalid day.")
else:
if day < 1 or day > 31:
lerrors.append("Invalid day.")
elif not lerrors:
if day > calendar.monthrange(year, month)[1]:
lerrors.append("Invalid day.")
else:
hlist[2] = day
fhour = fdata.getfirst("hour")
if not fhour:
hour = 0 # Assume things.
hlist[3] = 0
else:
try:
hour = int(fhour)
except ValueError:
lerrors.append("Invalid hour.")
else:
if hour < 0 or hour > 23:
lerrors.append("Invalid hour.")
else:
hlist[3] = hour
fminute = fdata.getfirst("minute")
if not fminute:
minute = 0
hlist[4] = 0
else:
try:
minute = int(fminute)
except ValueError:
lerrors.append("Invalid minute.")
else:
if minute < 0 or minute > 59:
lerrors.append("Invalid minute.")
else:
hlist[4] = minute
fsecond = fdata.getfirst("second")
if not fsecond:
second = 0
hlist[5] = 0
else:
try:
second = int(fsecond)
except ValueError:
lerrors.append("Invalid second.")
else:
if second < 0 or second > 59:
lerrors.append("Invalid second.")
if second == 60 or second == 61:
lerrors.append("Leap seconds are worse than purple worms.")
else:
hlist[5] = second
if lerrors:
errlist.extend(lerrors)
return None
return calendar.timegm([year, month, day, hour, minute, second, 0, 0, 0])
# Begin processing
fdata = cgi.FieldStorage()
# If this is true, the user didn't supply any search criteria, so assume it
# doesn't want a search. Otherwise, every error or omission must be printed.
isnotsearch = checkempty(fdata)
errors = []
formname = dungeon = utime = None
timepieces = [None, None, None, None, None, None]
if not isnotsearch:
formname = processname(fdata, errors)
dungeon = processgame(fdata, errors)
utime = processtime(fdata, errors, timepieces)
dosearch = formname != None and dungeon != None and utime != None
# Find the actual files, and put them in a list called gamefiles.
gtimes = [0, int(time.time())]
relgame = None
gamefiles = []
if dosearch:
ttyrecdir = "{0}/{1}/{2}/".format(ttyrecbase, formname, dungeon.uname)
query1 = "SELECT ttyrecs FROM {0} WHERE name = %s AND stime <= %s AND etime >= %s;".format(dungeon.uname)
query2 = "SELECT ttyrecs FROM {0} WHERE name = %s AND etime >= %s ORDER BY etime LIMIT 1;".format(dungeon.uname)
query3 = "SELECT ttyrecs FROM {0} WHERE name = %s AND stime <= %s ORDER BY stime DESC LIMIT 1;".format(dungeon.uname)
conn = rlgall.getconn()
cur = conn.cursor()
cur.execute(query1, [formname, utime, utime])
result = cur.fetchone()
if result:
gamefiles = result[0]
else:
cur.execute(query2, [formname, utime])
result = cur.fetchone()
if result:
gamefiles = result[0]
else:
cur.execute(query3, [formname, utime])
result = cur.fetchone()
if result:
gamefiles = result[0]
cur.close()
conn.close()
# Now we are ready to print the page.
sys.stdout.write("Content-type: text/html\r\n\r\n")
sys.stdout.write(rlgall.phead.format("Archive"))
sys.stdout.write(rlgall.ptop)
sys.stdout.write(rlgall.navtop.format("Archive"))
sys.stdout.write('<div class="content">\n')
sys.stdout.write(rlgall.pti.format("Guild Archives"))
if dosearch:
sys.stdout.write("<p>Expedition by {0} to {1} about {2}:</p>\n".format(
rlgall.playerlink(formname), dungeon.name,
time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(utime))))
if not gamefiles:
sys.stdout.write("<p>No record found.</p>\n")
elif len(gamefiles) == 1:
sys.stdout.write('<p><a href="/ttyrecs/{0}/{1}/{2}">1 ttyrec found.</a>\
</p>\n'.format(formname, dungeon.uname, gamefiles[0]))
else:
sys.stdout.write('<p>{0}-part ttyrec found.</p>\n'.format(len(gamefiles)))
sys.stdout.write('<ul>\n')
for i, afile in enumerate(gamefiles):
sys.stdout.write('<li><a href="/ttyrecs/{0}/{1}/{2}">Section {3}</a>\
</li>\n'.format(formname, dungeon.uname, afile, i + 1))
sys.stdout.write('</ul>\n')
if isnotsearch:
sys.stdout.write(infop)
else:
# There was information, but not good enough, i.e. errors.
sys.stdout.write('<p>')
for errmsg in errors:
sys.stdout.write(errmsg + " ")
sys.stdout.write('</p>\n')
# Print out a search form, whether a search was done or not.
sys.stdout.write('<form action="/archivedb.cgi" method="get">\n')
if dungeon != None:
input_game(sys.stdout, dungeon.uname)
else:
input_game(sys.stdout)
if formname != None:
input_name(sys.stdout, formname)
else:
input_name(sys.stdout)
input_datetime(sys.stdout, timepieces)
if dosearch:
sys.stdout.write('<div><input type="submit" value="Search Again"></div></form>')
else:
sys.stdout.write('<div><input type="submit" value="Search the Archive"></div></form>')
sys.stdout.write('<p>Or <a href="/ttyrecs/">browse all ttyrecs</a>.</p>')
sys.stdout.write('<p><a href="/about/ttyrec.html">What are ttyrecs?</a></p>')
sys.stdout.write("</div>\n</body>\n</html>\n")
exit()