#!/usr/bin/env python
import time, sys, os, string, cgi, stat, fcntl, Template, copy, feedwriter
import cPickle as pickle
datadir = os.getenv("DATADIR")
tmpdir = os.getenv("STATSTEMPDIR")
auctionurl = "https://www.cs.kent.ac.uk/systems/auction/cgi-bin/"
graph_maxage = 60
data_maxage = 120
# Current as of 2003-10-10 from Tescos.
#doughnut_price = 11
# Current as of 2004-03-12 from Tescos.
doughnut_price = 10
def readfile(n):
try:
f = open(n)
except IOError:
return None
ls = map(string.strip, f.readlines())
f.close()
return ls
def datefile(n):
return os.stat(n).st_mtime
class Bid:
def __init__(self, row):
cols = row.split(" ")
self.time = int(cols[0])
self.amount = int(cols[2])*100
self.bidder = cols[1].lower()
class Item:
def __init__(self, itemname):
dn = datadir + "/" + itemname + "/"
self.lot = int(itemname)
self.closing_time = datefile(dn + "close")
ls = readfile(dn + "description")
self.description = ls[0]
self.details = "\n".join(ls[2:])
ls = readfile(dn + "bids")
if ls is None:
self.bids = []
else:
self.bids = map(Bid, ls)
def isotime(secs):
return time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(secs))
def format_time(secs):
return "" + isotime(secs) + ""
def format_money(n):
return "£" + str(n / 100)
def format_sysadmin(n):
return "%d doughnuts" % (int(n / doughnut_price))
format_cash = format_money
def lotlink(lot):
lot = str(lot)
return '' + lot + ''
def bidderlink(bidder):
if bidder is None:
return '-'
else:
return '' + bidder + ''
def html_start(title):
sys.stdout.write("Content-type: text/html\n\n")
Template.print_header("Auction: " + title)
print """
Back to the auction | Statistics index | RSS feed of bids
""" def html_end(): Template.print_footer() def die(s): html_start("Statistics error") print "" + s + "
" html_end() sys.exit(0) def log(*s): print >>sys.stderr, os.getpid(), time.ctime(), " ".join(map(str, s)) def open_aging_file(name, maxage, updater): try: f = open(name, "r") mtime = os.fstat(f.fileno()).st_mtime except IOError: log("file", name, "does not yet exist") f = None now = time.time() if f is None or (now - mtime) > maxage: if f is not None: f.close() lock = open(name + ".lock", "w+") fcntl.lockf(lock.fileno(), fcntl.LOCK_EX) # At this point, another process that had locked the file might # have already updated it, so we need to check again. # (I'm not *entirely* convinced about the logic...) try: f = open(name, "r") mtime = os.fstat(f.fileno()).st_mtime except IOError: f = None if f is None or (now - mtime) > maxage: newname = name + ".new-" + str(os.getpid()) updater(newname) if f is not None: f.close() f = open(newname, "r") os.rename(newname, name) log("file", name, "updated") else: log("file", name, "updated by other process") lock.close() else: log("file", name, "is up-to-date") return f if __name__ == "__main__": form = cgi.FieldStorage() log("startup: " + " ".join([x + "=" + form[x].value for x in form.keys()])) def update_data(name): items = [] for itemname in os.listdir(datadir): if stat.S_ISDIR(os.stat(datadir + "/" + itemname).st_mode): try: items.append(Item(itemname)) except: print >>sys.stderr, "Bad item " + itemname log("processing bids") lots = {} bids = [] bidders = {} for item in items: lots[item.lot] = item for bid in item.bids: bids.append((bid.time, item.lot, bid.bidder, bid.amount)) bidders[bid.bidder] = 1 bids.sort() log("processing history") # The history at each point is a tuple: # (time, count, lot, from, to, amount, income, bidderinfo, lotinfo) # where bidderinfo is a dict from bidder to (count, committed, lotsheld, lotsbidupon) # lotinfo is a dict from lot to (owner, value) history = [] owners = {} values = {} bidupon = {} seenbidder = {} counts = {} bidderinfo = {} for lot in lots.keys(): owners[lot] = None values[lot] = 0 for bidder in bidders.keys(): bidupon[bidder] = {} counts[bidder] = 0 bidderinfo[bidder] = (0, 0, 0, 0) count = 0 for (btime, lot, bidder, amount) in bids: count += 1 counts[bidder] += 1 oldowner = owners[lot] owners[lot] = bidder if oldowner is not None: obi = bidderinfo[oldowner] nbi = (obi[0], obi[1] - values[lot], obi[2] - 1, obi[3]) bidderinfo[oldowner] = nbi values[lot] = amount seenbidder[bidder] = 1 obi = bidderinfo[bidder] if not bidupon[bidder].has_key(lot): n = 1 else: n = 0 nbi = (obi[0] + 1, obi[1] + amount, obi[2] + 1, obi[3] + n) bidderinfo[bidder] = nbi bidupon[bidder][lot] = 1 numbidders = len(seenbidder.keys()) numlots = 0 lotinfo = {} income = 0 for l in lots.keys(): income += values[l] if owners[l] is not None: numlots += 1 lotinfo[l] = (owners[l], values[l]) history.append((btime, count, lot, oldowner, bidder, amount, income, numbidders, numlots, copy.copy(bidderinfo), lotinfo)) log("done history") f = open(name, "w") pickle.dump((items, bids, history, bidders, lots), f, 1) f.close() f = open_aging_file(tmpdir + "/data", data_maxage, update_data) (items, bids, history, bidders, lots) = pickle.load(f) f.close() if len(history) == 0: die("Nobody's bid yet, so no statistics.
") if form.has_key("sysadmin"): format_cash = format_sysadmin if form.has_key("rss"): c = feedwriter.Channel(title = "CS Auction bids", link = auctionurl + "index.pl", description = "Bids on the UKC CS auction") bids.reverse() for (btime, lot, bidder, amount) in bids[:100]: c.add_item(title ='#' + str(lot) + ' ' + lots[lot].description + U': \u00A3' + str(amount / 100) + ' ' + bidder, link = auctionurl + 'info.pl?item=' + str(lot), pubDate = btime) sys.stdout.write("Content-type: application/rss+xml\n\n" + c.rss2()) elif form.has_key("graph1"): def update_graph1(newname): fn = tmpdir + "/graph1.data" f = open(fn, "w") for (btime, count, lot, oldowner, bidder, amount, income, numbidders, numlots, bidderinfo, lotinfo) in history: date = isotime(btime) f.write("%s %d %d %d %d\n" % (date, count, income / 100.0, numlots, numbidders)) f.close() f = os.popen("gnuplot", "w") f.write('''set terminal png set size 1,0.6 set output "''' + newname + '''" set title "UKC CS Auction" # needed to get it to leave enough space for the x axis... set xlabel " " set xdata time set timefmt "%Y-%m-%dT%H:%M:%S" set ylabel "Pounds" set y2label "Number" set ytics nomirror set y2tics nomirror set data style lines set key top left plot \ "''' + fn + '''" using 1:3 title "Income", \ "''' + fn + '''" using 1:2 axes x1y2 title "Number of bids", \ "''' + fn + '''" using 1:5 axes x1y2 title "Number of bidders", \ "''' + fn + '''" using 1:4 axes x1y2 title "Lots bid upon" ''') f.close() f = open_aging_file(tmpdir + "/graph1.png", graph_maxage, update_graph1) sys.stdout.write("Content-type: image/png\n\n") sys.stdout.write(f.read()) f.close() elif form.has_key("biddergraph"): wanted = form["biddergraph"].value if not bidders.has_key(wanted): die("No such bidder") def update_biddergraph(newname): fn = tmpdir + "/bidder-" + wanted f = open(fn, "w") maxtotal = 0 for (btime, count, lot, oldowner, bidder, amount, income, numbidders, numlots, bidderinfo, lotinfo) in history: (rcount, rcommitted, rlotsheld, rlotsbidupon) = bidderinfo[wanted] if rcommitted > maxtotal: maxtotal = rcommitted date = isotime(btime) f.write("%s %d %d %d %d\n" % (date, rcommitted / 100, rcount, rlotsheld, rlotsbidupon)) f.close() f = os.popen("gnuplot", "w") title = '"Bidder ' + wanted + ' (max committed GBP' + str(maxtotal / 100) + ')"' f.write('''set terminal png set size 1,0.4 set output "''' + newname + '''" set title ''' + title + ''' # needed to get it to leave enough space for the x axis... set xlabel " " set xdata time set timefmt "%Y-%m-%dT%H:%M:%S" set ylabel "Pounds" set y2label "Number" set ytics nomirror set y2tics nomirror set data style lines set key top left plot \ "''' + fn + '''" using 1:2 title "Amount committed", \ "''' + fn + '''" using 1:3 axes x1y2 title "Number of bids", \ "''' + fn + '''" using 1:4 axes x1y2 title "Items held", \ "''' + fn + '''" using 1:5 axes x1y2 title "Lots bid upon" ''') f.close() f = open_aging_file(tmpdir + "/biddergraph-" + wanted + ".png", graph_maxage, update_biddergraph) sys.stdout.write("Content-type: image/png\n\n") sys.stdout.write(f.read()) f.close() elif form.has_key("bidder"): wanted = form["bidder"].value if not bidders.has_key(wanted): die("No such bidder") (btime, count, lot, oldowner, bidder, amount, income, numbidders, numlots, bidderinfo, lotinfo) = history[-1] (rcount, rcommitted, rlotsheld, rlotsbidupon) = bidderinfo[wanted] html_start("Statistics for bidder " + wanted) print """Number of bids: """ + str(rcount) + """
Total committed: """ + format_cash(rcommitted) + """
Number of lots held: """ + str(rlotsheld) + """
Number of lots bid upon: """ + str(rlotsbidupon) + """
Current fraction of total income: """ + ("%2.1f%%" % (100.0 * rcommitted / income,)) + """
| Lot | Description | Amount bid |
|---|---|---|
| """ + lotlink(l) + """ | """ + lots[l].description + """ | """ + format_cash(value) + """ |
| Time | Lot | Description | Action | Amount |
|---|---|---|---|---|
| """ + format_time(btime) + """ | """ + lotlink(lot) + """ | """ + lots[lot].description + """ | """ + action + """ | """ + format_cash(amount) + """ |
Current auction time: """ + format_time(time.time()) + """
Current total income: """ + format_cash(income) + """
Number of bids: """ + str(count) + """
Number of bidders: """ + str(numbidders) + """
Number of lots bid upon: """ + str(numlots) + """
| Bidder | Number of bids | Lots held | Lots bid upon | Committed |
|---|---|---|---|---|
| """ + bidderlink(b) + """ | """ + str(rcount) + """ | """ + str(rlotsheld) + warning + """ | """ + str(rlotsbidupon) + """ | """ + format_cash(rcommitted) + """ |
| Lot | Description | Number of bids | Held by | Bid amount |
|---|---|---|---|---|
| """ + lotlink(l) + """ | """ + lots[l].description + """ | """ + str(len(lots[l].bids)) + """ | """ + bidderlink(owner) + """ | """ + format_cash(value) + """ |