changeset 33:25843238434a

Change the Python module's name back to rlgall. It is no longer an experimental variant. Using a database as a backend is a settled feature.
author John "Elwin" Edwards
date Thu, 02 Jan 2014 13:09:48 -0500
parents 05a4afbe6299
children 86b616d88020
files README.txt py/cleandb.py py/recorder.py py/rlgall.py py/rlgalldb.py py/setupdb.py py/stats.py web/archive.cgi web/recent.cgi web/scoring/players/index.cgi
diffstat 10 files changed, 529 insertions(+), 527 deletions(-) [+]
line wrap: on
line diff
--- a/README.txt	Thu Jan 02 11:48:15 2014 -0500
+++ b/README.txt	Thu Jan 02 13:09:48 2014 -0500
@@ -12,7 +12,7 @@
 py/recorder.py processes the log files and stores the data in a PostgreSQL
 database.  It should be run periodically by cron.
 
-py/rlgalldb.py is a module which recorder.py requires.  It should be installed
+py/rlgall.py is a module which recorder.py requires.  It should be installed
 in /lib/python<x.y>/site-packages or the equivalent location.
 
 web/ contains the static parts of the rlgallery.org website.  Note that when 
--- a/py/cleandb.py	Thu Jan 02 11:48:15 2014 -0500
+++ b/py/cleandb.py	Thu Jan 02 13:09:48 2014 -0500
@@ -1,7 +1,7 @@
 #!/usr/bin/python3
 # cleandb.py: empty the database in an orderly fashion
 
-import rlgalldb as rlgall
+import rlgall
 import psycopg2
 
 dbconn = psycopg2.connect("dbname=rlg")
--- a/py/recorder.py	Thu Jan 02 11:48:15 2014 -0500
+++ b/py/recorder.py	Thu Jan 02 13:09:48 2014 -0500
@@ -2,7 +2,7 @@
 
 import os
 import psycopg2
-import rlgalldb as rlgall
+import rlgall
 
 # Contains a dir for everyone who registered
 everydir = "/var/dgl/dgldir/ttyrec/"
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/py/rlgall.py	Thu Jan 02 13:09:48 2014 -0500
@@ -0,0 +1,516 @@
+# rlgall.py
+# Module for the Roguelike Gallery, using a postgres database
+# Requires Python 3.3
+
+import os
+import psycopg2
+from datetime import datetime
+import pytz
+
+# 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'
+rcell = '  <span class="sdatar">{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 = {"endt":"End time", "score":"Score", "name":"Name", "xl":"XL", 
+              "fate":"Fate", "rank":"Rank", "game":"Game", "class": "Class"}
+# Queries for the games table
+offselstr = "SELECT offbytes FROM games WHERE gname = %s;"
+newoffstr = "UPDATE games SET offbytes = %s WHERE gname = %s;"
+
+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 recnameToTS(filename):
+  pattern = "%Y-%m-%d.%H:%M:%S.ttyrec"
+  try:
+    dt = datetime.strptime(filename, pattern).replace(tzinfo=pytz.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 = entry["endt"].strftime("%Y/%m/%d %H:%M:%S")
+  stamp = int(entry["endt"].timestamp())
+  return lstr.format(entry["name"], entry["game"].uname, stamp, 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), rcell))
+      elif field in entry:
+        thing = entry[field]
+        if field == "game":
+          clist.append((thing.name, cell))
+        elif field == "xl" or field == "score": # Numerics
+          clist.append((str(thing), rcell))
+        elif field == "name":
+          clist.append((playerlink(thing), cell))
+        elif field == "fate":
+          clist.append((thing, cell))
+        elif field == "endt":
+          clist.append((linktoArchive(entry), cell))
+        else:
+          clist.append(("{0}".format(thing), cell))
+      else:
+        clist.append(("N/A", cell))
+    rowstr = rowstart + "".join([ t.format(s) for (s, t) in clist ]) + rowend
+    of.write(rowstr)
+  of.write(tblend)
+  return
+
+class Game:
+  def __init__(self, name, uname):
+    pass
+  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)
+      if self.sqltypes[item] == "bool":
+        ndict[item] = bool(int(value))
+      elif self.sqltypes[item] == "timestamptz":
+        ndict[item] = datetime.fromtimestamp(int(value), pytz.utc)
+      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 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, encoding="utf-8")
+      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
+  def postprocess(self, gamelist):
+    "Default postprocessing function: adds start time and ttyrec list"
+    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["endt"])
+      # Find the end time of the latest game already in the db
+      tquery = "SELECT endt FROM {0} WHERE name = %s ORDER BY endt DESC LIMIT 1;".format(self.uname)
+      cur.execute(tquery, [nameF])
+      result = cur.fetchone()