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 |