Mercurial > hg > rlgallery-misc
comparison py/rlgalldb.py @ 0:5ba2123d2c20
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/.
| author | John "Elwin" Edwards <elwin@sdf.org> |
|---|---|
| date | Wed, 25 Jul 2012 21:59:42 -0700 |
| parents | |
| children | def7fecbd437 |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:5ba2123d2c20 |
|---|---|
| 1 # rlgalldb.py | |
| 2 # Module for the Roguelike Gallery, using a postgres database | |
| 3 | |
| 4 import os | |
| 5 import sys | |
| 6 import time | |
| 7 import calendar | |
| 8 import re | |
| 9 import psycopg2 | |
| 10 from datetime import datetime, tzinfo, timedelta | |
| 11 | |
| 12 # Configuration | |
| 13 logdir = "/var/dgl/var/games/roguelike/" | |
| 14 webdir = "/var/www/lighttpd/scoring/" | |
| 15 ppagename = webdir + "players/{0}.html" | |
| 16 hpagename = webdir + "highscores.html" | |
| 17 | |
| 18 # HTML fragments for templating | |
| 19 phead = """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> | |
| 20 <html><head> | |
| 21 <title>{0}</title> | |
| 22 <link rel="stylesheet" href="/scoring/scores.css" type="text/css"> | |
| 23 </head> | |
| 24 """ | |
| 25 | |
| 26 ptop = """<body> | |
| 27 <h1>Yendor Guild</h1> | |
| 28 """ | |
| 29 | |
| 30 navtop = '<div class="nav"><a href="/">rlgallery.org</a> -> {0}</div>\n' | |
| 31 navscore = '<div class="nav"><a href="/">rlgallery.org</a> -> \ | |
| 32 <a href="/scoring/">Scores</a> -> {0}</div>\n' | |
| 33 navplayer = '<div class="nav"><a href="/">rlgallery.org</a> -> \ | |
| 34 <a href="/scoring/">Scores</a> -> <a href="/scoring/players/">Players</a> \ | |
| 35 -> {0}</div>' | |
| 36 | |
| 37 pti = '<h2>{0}</h2>\n' | |
| 38 | |
| 39 secthead = '<h3>{0}</h3>\n' | |
| 40 tblhead = '<div class="stable">\n' | |
| 41 rowstart = '<div class="sentry">\n' | |
| 42 rowend = '</div>\n' | |
| 43 cell = ' <span class="sdata">{0}</span>\n' | |
| 44 hcell = ' <span class="shdata">{0}</span>\n' | |
| 45 tblend = '</div>\n' | |
| 46 pend = "</body></html>\n" | |
| 47 | |
| 48 # This would be more useful if we had to do translation | |
| 49 headerbook = {"etime":"End time", "score":"Score", "name":"Name", "xl":"XL", | |
| 50 "fate":"Fate", "rank":"Rank", "game":"Game"} | |
| 51 # Queries for the games table | |
| 52 offselstr = "SELECT offbytes FROM games WHERE gname = %s;" | |
| 53 newoffstr = "UPDATE games SET offbytes = %s WHERE gname = %s;" | |
| 54 | |
| 55 # A representation of the UTC time zone. They say Py3k can better handle | |
| 56 # this madness. | |
| 57 class UTC(tzinfo): | |
| 58 def utcoffset(self, dt): | |
| 59 return timedelta(0) | |
| 60 def dst(self, dt): | |
| 61 return timedelta(0) | |
| 62 def tzname(self, dt): | |
| 63 return "UTC" | |
| 64 utc = UTC() | |
| 65 | |
| 66 def getconn(): | |
| 67 "Returns a database connection, or None if the connection fails." | |
| 68 try: | |
| 69 conn = psycopg2.connect("dbname=rlg") | |
| 70 except psycopg2.OperationalError: | |
| 71 return None | |
| 72 return conn | |
| 73 | |
| 74 def recnameToInt(filename): | |
| 75 recre = r"(\d{4})-(\d{2})-(\d{2})\.(\d{2}):(\d{2}):(\d{2})\.ttyrec$" | |
| 76 match = re.match(recre, filename) | |
| 77 if not match: | |
| 78 return None | |
| 79 return calendar.timegm([int(val) for val in match.groups()]) | |
| 80 | |
| 81 def recnameToTS(filename): | |
| 82 pattern = "%Y-%m-%d.%H:%M:%S.ttyrec" | |
| 83 try: | |
| 84 dt = datetime.strptime(filename, pattern) | |
| 85 dt.replace(tzinfo=utc) | |
| 86 return dt | |
| 87 except ValueError: | |
| 88 return None | |
| 89 | |
| 90 def ttyreclink(text, name, game, gtime): | |
| 91 "Returns a link to the ttyrec archivist" | |
| 92 lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>' | |
| 93 return lstr.format(name, game, gtime, text) | |
| 94 | |
| 95 def playerlink(name): | |
| 96 "Returns a link to a player's page" | |
| 97 lstr = '<a href="/scoring/players/' + name + '.html">' + name + '</a>' | |
| 98 return lstr | |
| 99 | |
| 100 def linktoArchive(entry): | |
| 101 "Takes an entry dict and returns a link to the ttyrec archivist." | |
| 102 lstr = '<a href="/archive.cgi?name={0};game={1};time={2}">{3}</a>' | |
| 103 #linktext = time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(entry["etime"])) | |
| 104 linktext = entry["etstamp"].strftime("%Y/%m/%d %H:%M:%S") | |
| 105 return lstr.format(entry["name"], entry["game"].uname, entry["etime"], | |
| 106 linktext) | |
| 107 | |
| 108 def maketablerow(cells, isheader=None): | |
| 109 "Takes a list of strings and returns a HTML table row with each string \ | |
| 110 in its own cell. isheader will make them header cells, obviously." | |
| 111 if isheader: | |
| 112 celler = hcell | |
| 113 else: | |
| 114 celler = cell | |
| 115 rowstr = rowstart | |
| 116 for entry in cells: | |
| 117 rowstr = rowstr + celler.format(entry) | |
| 118 rowstr = rowstr + rowend | |
| 119 return rowstr | |
| 120 | |
| 121 def printTable(entries, fields, of): | |
| 122 "Takes a list of entry dicts and a list of field strings and writes a \ | |
| 123 HTML table to of." | |
| 124 of.write(tblhead) | |
| 125 clist = [] | |
| 126 for field in fields: | |
| 127 if field in headerbook: | |
| 128 clist.append(headerbook[field]) | |
| 129 else: | |
| 130 clist.append("{0}".format(field)) | |
| 131 of.write(maketablerow(clist, True)) | |
| 132 rnum = 0 | |
| 133 for i, entry in enumerate(entries): | |
| 134 clist = [] | |
| 135 for field in fields: | |
| 136 if field == "rank": | |
| 137 clist.append("{0}".format(i + 1)) | |
| 138 elif field in entry: | |
| 139 thing = entry[field] | |
| 140 if field == "game": | |
| 141 clist.append(thing.name) | |
| 142 elif field == "xl" or field == "score": # Numerics | |
| 143 clist.append(str(thing)) | |
| 144 elif field == "name": | |
| 145 clist.append(playerlink(thing)) | |
| 146 elif field == "fate": | |
| 147 clist.append(thing) | |
| 148 elif field == "etime": | |
| 149 clist.append(linktoArchive(entry)) | |
| 150 else: | |
| 151 clist.append("{0}".format(thing)) | |
| 152 else: | |
| 153 clist.append("N/A") | |
| 154 of.write(maketablerow(clist)) | |
| 155 of.write(tblend) | |
| 156 return | |
| 157 | |
| 158 def readentries(entfile, entlist): | |
| 159 "Reads a list of entries from a file object" | |
| 160 while True: | |
| 161 nextentry = entfile.readline() | |
| 162 if not nextentry: | |
| 163 break | |
| 164 if nextentry[-1] != '\n': # The line is incomplete | |
| 165 break | |
| 166 entlist.append(nextentry.split(None, 4)) | |
| 167 return | |
| 168 | |
| 169 class Game: | |
| 170 pass | |
| 171 | |
| 172 class RogueGame(Game): | |
| 173 def __init__(self, name, uname, suffix): | |
| 174 self.name = name | |
| 175 self.uname = uname | |
| 176 self.scores = logdir + uname + ".log" | |
| 177 self.logspec = ["etime", "score", "name", "xl", "fate"] | |
| 178 self.sqltypes = {"etime": "int", "score": "int", "name": "varchar(20)", | |
| 179 "xl": "int", "fate": "text", "stime": "int", | |
| 180 "ttyrecs": "text ARRAY", "ststamp": "timestamptz", | |
| 181 "etstamp": "timestamptz"} | |
| 182 self.logdelim = " " | |
| 183 # Class variables, used by some methods | |
| 184 fields = ["name", "score", "xl", "fate", "etime"] | |
| 185 rankfields = ["rank", "score", "name", "xl", "fate", "etime"] | |
| 186 pfields = ["score", "xl", "fate", "etime"] | |
| 187 def logtoDict(self, entry): | |
| 188 "Processes a log entry string, returning a dict." | |
| 189 ndict = {"game": self} | |
| 190 entrylist = entry.strip().split(self.logdelim, len(self.logspec) - 1) | |
| 191 for item, value in zip(self.logspec, entrylist): | |
| 192 if self.sqltypes[item] == "int": | |
| 193 ndict[item] = int(value) | |
| 194 else: | |
| 195 ndict[item] = value | |
| 196 return ndict | |
| 197 def getEntryDicts(self, entfile, entlist): | |
| 198 "Reads logfile entries from entfile, interprets them according to the \ | |
| 199 instructions in self.logspec/logdelim, and adds them as dicts to entlist." | |
| 200 while True: | |
| 201 nextentry = entfile.readline() | |
| 202 if not nextentry: | |
| 203 break | |
| 204 if nextentry[-1] != '\n': | |
| 205 break | |
| 206 entlist.append(self.logtoDict(nextentry)) | |
| 207 return | |
| 208 def getRecent(self, n=20): | |
| 209 "Gets the n most recent games out of the database, returning a list \ | |
| 210 of dicts." | |
| 211 try: | |
| 212 n = int(n) | |
| 213 except (ValueError, TypeError): | |
| 214 return [] | |
| 215 if n <= 0: | |
| 216 return [] | |
| 217 dictlist = [] | |
| 218 conn = psycopg2.connect("dbname=rlg") | |
| 219 cur = conn.cursor() | |
| 220 qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM {0} ORDER BY etstamp DESC \ | |
| 221 LIMIT %s;".format(self.uname) | |
| 222 cur.execute(qstr, [n]) | |
| 223 for record in cur: | |
| 224 # This should be less hardcodish | |
| 225 ndict = {"game": self} | |
| 226 ndict["etime"] = record[0] | |
| 227 ndict["score"] = record[1] | |
| 228 ndict["name"] = record[2] | |
| 229 ndict["xl"] = record[3] | |
| 230 ndict["fate"] = record[4] | |
| 231 ndict["ststamp"] = record[5] | |
| 232 ndict["etstamp"] = record[6] | |
| 233 dictlist.append(ndict) | |
| 234 cur.close() | |
| 235 conn.close() | |
| 236 return dictlist | |
| 237 def getHigh(self, n=10, offset=0): | |
| 238 "Gets the n highest scores (starting at offset) from the database, \ | |
| 239 returning a list of dicts." | |
| 240 qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM {0} ORDER BY score DESC\ | |
| 241 ".format(self.uname) | |
| 242 qvals = [] | |
| 243 try: | |
| 244 n = int(n) | |
| 245 except (ValueError, TypeError): | |
| 246 return [] | |
| 247 if n <= 0: | |
| 248 return [] | |
| 249 qstr += " LIMIT %s" | |
| 250 qvals.append(n) | |
| 251 try: | |
| 252 offset = int(offset) | |
| 253 except (ValueError, TypeError): | |
| 254 return [] | |
| 255 if n > 0: | |
| 256 qstr += " OFFSET %s" | |
| 257 qvals.append(offset) | |
| 258 qstr += ";" | |
| 259 conn = psycopg2.connect("dbname=rlg") | |
| 260 cur = conn.cursor() | |
| 261 cur.execute(qstr, qvals) | |
| 262 dictlist = [] | |
| 263 for record in cur: | |
| 264 ndict = {"game": self} | |
| 265 ndict["etime"] = record[0] | |
| 266 ndict["score"] = record[1] | |
| 267 ndict["name"] = record[2] | |
| 268 ndict["xl"] = record[3] | |
| 269 ndict["fate"] = record[4] | |
| 270 ndict["ststamp"] = record[5] | |
| 271 ndict["etstamp"] = record[6] | |
| 272 dictlist.append(ndict) | |
| 273 cur.close() | |
| 274 conn.close() | |
| 275 return dictlist | |
| 276 def getPlayer(self, player): | |
| 277 "Gets all player's games from the database." | |
| 278 qstr = "SELECT etime, score, name, xl, fate, ststamp, etstamp FROM " + self.uname + " WHERE \ | |
| 279 name = %s;" | |
| 280 conn = getconn() | |
| 281 if conn == None: | |
| 282 return [] | |
| 283 cur = conn.cursor() | |
| 284 entries = [] | |
| 285 cur.execute(qstr, [player]) | |
| 286 for record in cur: | |
| 287 ndict = {"game": self} | |
| 288 ndict["etime"] = record[0] | |
| 289 ndict["score"] = record[1] | |
| 290 ndict["name"] = record[2] | |
| 291 ndict["xl"] = record[3] | |
| 292 ndict["fate"] = record[4] | |
| 293 ndict["ststamp"] = record[5] | |
| 294 ndict["etstamp"] = record[6] | |
| 295 entries.append(ndict) | |
| 296 cur.close() | |
| 297 conn.close() | |
| 298 return entries | |
| 299 def putIntoDB(self, dictlist, conn): | |
| 300 "Add the entries in dictlist to the database through connection conn, \ | |
| 301 which needs the INSERT privilege." | |
| 302 # FIXME this monster needs to be procedurally generated | |
| 303 qstr = "INSERT INTO " + self.uname + " (etime, score, name, xl, fate, stime, ttyrecs, ststamp, etstamp) \ | |
| 304 VALUES (%(etime)s, %(score)s, %(name)s, %(xl)s, %(fate)s, %(stime)s, %(ttyrecs)s, %(ststamp)s, %(etstamp)s);" | |
| 305 cur = conn.cursor() | |
| 306 cur.executemany(qstr, [ d for d in dictlist if d["game"] == self ]) | |
| 307 conn.commit() | |
| 308 cur.close() | |
| 309 return | |
| 310 def postprocess(self, gamelist): | |
| 311 names = set([ e["name"] for e in gamelist ]) | |
| 312 conn = getconn() | |
| 313 if conn == None: | |
| 314 return [] | |
| 315 cur = conn.cursor() | |
| 316 for nameF in names: | |
| 317 # Get all this player's games ordered by time | |
| 318 itsEntries = [ entry for entry in gamelist if entry["name"] == nameF ] | |
| 319 itsEntries.sort(key=lambda e: e["etime"]) | |
| 320 # Find the end time of the latest game already in the db | |
| 321 tquery = "SELECT etstamp FROM {0} WHERE name = %s ORDER BY etstamp DESC LIMIT 1;".format(self.uname) | |
| 322 cur.execute(tquery, [nameF]) | |
| 323 result = cur.fetchone() | |
| 324 if result: | |
| 325 prev = calendar.timegm(result[0].utctimetuple()) | |
| 326 else: | |
| 327 prev = 0 | |
| 328 ttyrecdir = "/var/dgl/dgldir/ttyrec/{0}/{1}/".format(nameF, self.uname) | |
| 329 allfilekeys = [ (recnameToInt(f), f) for f in os.listdir(ttyrecdir) ] | |
| 330 vfilekeys = [ e for e in allfilekeys if e[0] > prev ] | |
| 331 vfilekeys.sort(key=lambda e: e[0]) | |
| 332 # Now determine stime and ttyrecs for each game | |
| 333 for i in range(0, len(itsEntries)): | |
| 334 if i == 0: | |
| 335 lowlim = prev | |
| 336 else: | |
| 337 lowlim = itsEntries[i-1]["etime"] | |
| 338 hilim = itsEntries[i]["etime"] | |
| 339 recs = [ k[1] for k in vfilekeys if lowlim <= k[0] < hilim ] | |
| 340 itsEntries[i]["stime"] = recnameToInt(recs[0]) | |
| 341 itsEntries[i]["ttyrecs"] = recs | |
| 342 # While we're at it, replace numeric timestamps with SQL timestamps. | |
| 343 itsEntries[i]["ststamp"] = datetime.fromtimestamp(itsEntries[i]["stime"], utc) | |
| 344 itsEntries[i]["etstamp"] = datetime.fromtimestamp(itsEntries[i]["etime"], utc) | |
| 345 cur.close() | |
| 346 conn.close() | |
| 347 |
