Merge branch 'master' of github.com:antzucaro/XonStat
authorAnt Zucaro <azucaro@gmail.com>
Sat, 16 Jun 2012 13:53:08 +0000 (09:53 -0400)
committerAnt Zucaro <azucaro@gmail.com>
Sat, 16 Jun 2012 13:53:08 +0000 (09:53 -0400)
Conflicts:
xonstat/views/__init__.py
xonstat/views/player.py

31 files changed:
xonstat/__init__.py [changed mode: 0755->0644]
xonstat/elo.py [changed mode: 0755->0644]
xonstat/models.py [changed mode: 0755->0644]
xonstat/static/css/colorbox.css
xonstat/static/js/jquery.colorbox-min.js [changed mode: 0755->0644]
xonstat/templates/accuracy.mako [changed mode: 0755->0644]
xonstat/templates/base.mako [changed mode: 0755->0644]
xonstat/templates/game_index.mako [changed mode: 0755->0644]
xonstat/templates/game_info.mako [changed mode: 0755->0644]
xonstat/templates/main_index.mako [changed mode: 0755->0644]
xonstat/templates/map_index.mako [changed mode: 0755->0644]
xonstat/templates/map_info.mako [changed mode: 0755->0644]
xonstat/templates/nav.mako [changed mode: 0755->0644]
xonstat/templates/navlinks.mako [changed mode: 0755->0644]
xonstat/templates/player_game_index.mako [changed mode: 0755->0644]
xonstat/templates/player_index.mako [changed mode: 0755->0644]
xonstat/templates/player_info.mako [changed mode: 0755->0644]
xonstat/templates/rank_index.mako [changed mode: 0755->0644]
xonstat/templates/scoreboard.mako [changed mode: 0755->0644]
xonstat/templates/search.mako [changed mode: 0755->0644]
xonstat/templates/server_index.mako [changed mode: 0755->0644]
xonstat/templates/server_info.mako [changed mode: 0755->0644]
xonstat/util.py [changed mode: 0755->0644]
xonstat/views/__init__.py [changed mode: 0755->0644]
xonstat/views/game.py [changed mode: 0755->0644]
xonstat/views/main.py [changed mode: 0755->0644]
xonstat/views/map.py [changed mode: 0755->0644]
xonstat/views/player.py [changed mode: 0755->0644]
xonstat/views/search.py [changed mode: 0755->0644]
xonstat/views/server.py [changed mode: 0755->0644]
xonstat/views/submission.py [changed mode: 0755->0644]

old mode 100755 (executable)
new mode 100644 (file)
index 79f7f52..3160b75
@@ -2,7 +2,7 @@ import sqlahelper
 from pyramid.config import Configurator
 from sqlalchemy import engine_from_config
 from xonstat.models import initialize_db
-from xonstat.views import * 
+from xonstat.views import *
 
 def main(global_config, **settings):
     """ This function returns a Pyramid WSGI application.
@@ -20,30 +20,26 @@ def main(global_config, **settings):
 
     # ROOT ROUTE
     config.add_route("main_index", "/")
-    config.add_view(main_index, route_name="main_index",
-        renderer="main_index.mako")
+    config.add_view(main_index, route_name="main_index", renderer="main_index.mako")
 
     # MAIN SUBMISSION ROUTE
     config.add_route("stats_submit", "stats/submit")
     config.add_view(stats_submit, route_name="stats_submit")
 
     # PLAYER ROUTES
-    config.add_route("player_game_index",
-            "/player/{player_id:\d+}/games")
-    config.add_view(player_game_index, route_name="player_game_index",
-        renderer="player_game_index.mako")
+    config.add_route("player_game_index", "/player/{player_id:\d+}/games")
+    config.add_view(player_game_index, route_name="player_game_index", renderer="player_game_index.mako")
 
     config.add_route("player_index", "/players")
-    config.add_view(player_index, route_name="player_index",
-        renderer="player_index.mako")
+    config.add_view(player_index, route_name="player_index", renderer="player_index.mako")
 
     config.add_route("player_info", "/player/{id:\d+}")
-    config.add_view(player_info, route_name="player_info",
-        renderer="player_info.mako")
+    config.add_view(player_info, route_name="player_info", renderer="player_info.mako")
 
-    config.add_route("player_accuracy", "/player/{id:\d+}/accuracy")
-    config.add_view(player_accuracy_json, route_name="player_accuracy",
-        renderer="json")
+    config.add_route("player_accuracy",      "/player/{id:\d+}/accuracy")
+    config.add_route("player_accuracy_json", "/player/{id:\d+}/accuracy.json")
+    config.add_view(player_accuracy_json, route_name="player_accuracy",      renderer="json")
+    config.add_view(player_accuracy_json, route_name="player_accuracy_json", renderer="json")
 
     config.add_route("player_damage", "/player/{id:\d+}/damage")
     config.add_view(player_damage_json, route_name="player_damage",
@@ -51,47 +47,35 @@ def main(global_config, **settings):
 
     # GAME ROUTES
     config.add_route("game_index", "/games")
-    config.add_view(game_index, route_name="game_index",
-        renderer="game_index.mako")
+    config.add_view(game_index, route_name="game_index", renderer="game_index.mako")
 
     config.add_route("game_info", "/game/{id:\d+}")
-    config.add_view(game_info, route_name="game_info",
-        renderer="game_info.mako")
+    config.add_view(game_info, route_name="game_info", renderer="game_info.mako")
 
     config.add_route("rank_index", "/ranks/{game_type_cd:ctf|dm|tdm|duel}")
-    config.add_view(rank_index, route_name="rank_index",
-        renderer="rank_index.mako")
+    config.add_view(rank_index, route_name="rank_index", renderer="rank_index.mako")
 
     # SERVER ROUTES
     config.add_route("server_index", "/servers")
-    config.add_view(server_index, route_name="server_index",
-        renderer="server_index.mako")
+    config.add_view(server_index, route_name="server_index", renderer="server_index.mako")
 
-    config.add_route("server_game_index",
-        "/server/{server_id:\d+}/games/page/{page:\d+}")
-    config.add_view(server_game_index, route_name="server_game_index",
-        renderer="server_game_index.mako")
+    config.add_route("server_game_index", "/server/{server_id:\d+}/games/page/{page:\d+}")
+    config.add_view(server_game_index, route_name="server_game_index", renderer="server_game_index.mako")
 
     config.add_route("server_info", "/server/{id:\d+}")
-    config.add_view(server_info, route_name="server_info",
-        renderer="server_info.mako")
+    config.add_view(server_info, route_name="server_info", renderer="server_info.mako")
 
     # MAP ROUTES
+    config.add_route("map_index",      "/maps")
     config.add_route("map_index_json", "/maps.json")
-    config.add_view(map_index_json, route_name="map_index_json",
-        renderer="json")
-
-    config.add_route("map_index", "/maps")
-    config.add_view(map_index, route_name="map_index",
-        renderer="map_index.mako")
+    config.add_view(map_index,      route_name="map_index",      renderer="map_index.mako")
+    config.add_view(map_index_json, route_name="map_index_json", renderer="json")
 
     config.add_route("map_info", "/map/{id:\d+}")
-    config.add_view(map_info, route_name="map_info",
-        renderer="map_info.mako")
+    config.add_view(map_info, route_name="map_info", renderer="map_info.mako")
 
     # SEARCH ROUTES
     config.add_route("search", "search")
-    config.add_view(search, route_name="search",
-        renderer="search.mako")
+    config.add_view(search, route_name="search", renderer="search.mako")
 
     return config.make_wsgi_app()
old mode 100755 (executable)
new mode 100644 (file)
index 9d1d467..19bcfba
@@ -1,46 +1,46 @@
-import sys\r
-import math\r
-import random\r
-\r
-class EloParms:\r
-    def __init__(self, global_K = 15, initial = 100, floor = 100, logdistancefactor = math.log(10)/float(400), maxlogdistance = math.log(10)):\r
-        self.global_K = global_K\r
-        self.initial = initial\r
-        self.floor = floor\r
-        self.logdistancefactor = logdistancefactor\r
-        self.maxlogdistance = maxlogdistance\r
-\r
-\r
-class KReduction:\r
-    def __init__(self, fulltime, mintime, minratio, games_min, games_max, games_factor):\r
-        self.fulltime = fulltime\r
-        self.mintime = mintime\r
-        self.minratio = minratio\r
-        self.games_min = games_min\r
-        self.games_max = games_max\r
-        self.games_factor = games_factor\r
-\r
-    def eval(self, mygames, mytime, matchtime):\r
-        if mytime < self.mintime:\r
-            return 0\r
-        if mytime < self.minratio * matchtime:\r
-            return 0\r
-        if mytime < self.fulltime:\r
-            k = mytime / float(self.fulltime)\r
-        else:\r
-            k = 1.0\r
-        if mygames >= self.games_max:\r
-            k *= self.games_factor\r
-        elif mygames > self.games_min:\r
-            k *= 1.0 - (1.0 - self.games_factor) * (mygames - self.games_min) / float(self.games_max - self.games_min)\r
-        return k\r
-\r
-\r
-# parameters for K reduction\r
-# this may be touched even if the DB already exists\r
-KREDUCTION = KReduction(600, 120, 0.5, 0, 32, 0.2)\r
-\r
-# parameters for chess elo\r
-# only global_K may be touched even if the DB already exists\r
-# we start at K=200, and fall to K=40 over the first 20 games\r
-ELOPARMS = EloParms(global_K = 200)\r
+import sys
+import math
+import random
+
+class EloParms:
+    def __init__(self, global_K = 15, initial = 100, floor = 100, logdistancefactor = math.log(10)/float(400), maxlogdistance = math.log(10)):
+        self.global_K = global_K
+        self.initial = initial
+        self.floor = floor
+        self.logdistancefactor = logdistancefactor
+        self.maxlogdistance = maxlogdistance
+
+
+class KReduction:
+    def __init__(self, fulltime, mintime, minratio, games_min, games_max, games_factor):
+        self.fulltime = fulltime
+        self.mintime = mintime
+        self.minratio = minratio
+        self.games_min = games_min
+        self.games_max = games_max
+        self.games_factor = games_factor
+
+    def eval(self, mygames, mytime, matchtime):
+        if mytime < self.mintime:
+            return 0
+        if mytime < self.minratio * matchtime:
+            return 0
+        if mytime < self.fulltime:
+            k = mytime / float(self.fulltime)
+        else:
+            k = 1.0
+        if mygames >= self.games_max:
+            k *= self.games_factor
+        elif mygames > self.games_min:
+            k *= 1.0 - (1.0 - self.games_factor) * (mygames - self.games_min) / float(self.games_max - self.games_min)
+        return k
+
+
+# parameters for K reduction
+# this may be touched even if the DB already exists
+KREDUCTION = KReduction(600, 120, 0.5, 0, 32, 0.2)
+
+# parameters for chess elo
+# only global_K may be touched even if the DB already exists
+# we start at K=200, and fall to K=40 over the first 20 games
+ELOPARMS = EloParms(global_K = 200)
old mode 100755 (executable)
new mode 100644 (file)
index d5cd885..949f90c
@@ -34,20 +34,17 @@ class Player(object):
         return pretty_date(self.create_dt)
 
     def __repr__(self):
-        return "<Player(%s, %s)>" % (self.player_id, 
-                self.nick.encode('utf-8'))
+        return "<Player(%s, %s)>" % (self.player_id, self.nick.encode('utf-8'))
 
 
 class GameType(object):
     def __repr__(self):
-        return "<GameType(%s, %s, %s)>" % (self.game_type_cd, self.descr, 
-                self.active_ind)
+        return "<GameType(%s, %s, %s)>" % (self.game_type_cd, self.descr, self.active_ind)
 
 
 class Weapon(object):
     def __repr__(self):
-        return "<Weapon(%s, %s, %s)>" % (self.weapon_cd, self.descr, 
-                self.active_ind)
+        return "<Weapon(%s, %s, %s)>" % (self.weapon_cd, self.descr, self.active_ind)
 
 
 class Server(object):
@@ -82,8 +79,7 @@ class Game(object):
         self.winner = winner
 
     def __repr__(self):
-        return "<Game(%s, %s, %s, %s)>" % (self.game_id, self.start_dt, 
-                self.game_type_cd, self.server_id)
+        return "<Game(%s, %s, %s, %s)>" % (self.game_id, self.start_dt, self.game_type_cd, self.server_id)
 
     def fuzzy_date(self):
         return pretty_date(self.start_dt)
@@ -201,8 +197,7 @@ class PlayerGameStat(object):
         self.create_dt = create_dt
 
     def __repr__(self):
-        return "<PlayerGameStat(%s, %s, %s)>" \
-        % (self.player_id, self.game_id, self.create_dt)
+        return "<PlayerGameStat(%s, %s, %s)>" % (self.player_id, self.game_id, self.create_dt)
 
     def nick_stripped(self):
         if self.nick is None:
@@ -231,8 +226,7 @@ class PlayerGameStat(object):
 
 class Achievement(object):
     def __repr__(self):
-        return "<Achievement(%s, %s, %s)>" % (self.achievement_cd, self.descr,
-                self.limit)
+        return "<Achievement(%s, %s, %s)>" % (self.achievement_cd, self.descr, self.limit)
 
 
 class PlayerAchievement(object):
@@ -242,8 +236,7 @@ class PlayerAchievement(object):
 
 class PlayerWeaponStat(object):
     def __repr__(self):
-        return "<PlayerWeaponStat(%s, %s, %s)>" % (self.player_weapon_stats_id,
-                self.player_id, self.game_id)
+        return "<PlayerWeaponStat(%s, %s, %s)>" % (self.player_weapon_stats_id, self.player_id, self.game_id)
 
 
 class Hashkey(object):
@@ -270,8 +263,7 @@ class PlayerElo(object):
         self.elo = ELOPARMS.initial
 
     def __repr__(self):
-        return "<PlayerElo(pid=%s, gametype=%s, elo=%s)>" % \
-                (self.player_id, self.game_type_cd, self.elo)
+        return "<PlayerElo(pid=%s, gametype=%s, elo=%s)>" % (self.player_id, self.game_type_cd, self.elo)
 
 
 class PlayerRank(object):
@@ -284,8 +276,7 @@ class PlayerRank(object):
 
 
     def __repr__(self):
-        return "<PlayerRank(pid=%s, gametype=%s, rank=%s)>" % \
-                (self.player_id, self.game_type_cd, self.rank)
+        return "<PlayerRank(pid=%s, gametype=%s, rank=%s)>" % (self.player_id, self.game_type_cd, self.rank)
 
 
 def initialize_db(engine=None):
index e0ac646387490b53e5de710272afdb52a6a3508b..8358ac5321d56dae7198fd41ed6855060917cbb8 100755 (executable)
@@ -1,49 +1,49 @@
-/*\r
-    ColorBox Core Style:\r
-    The following CSS is consistent between example themes and should not be altered.\r
-*/\r
-#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}\r
-#cboxOverlay{position:fixed; width:100%; height:100%;}\r
-#cboxMiddleLeft, #cboxBottomLeft{clear:left;}\r
-#cboxContent{position:relative;}\r
-#cboxLoadedContent{overflow:auto;}\r
-#cboxTitle{margin:0;}\r
-#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%;}\r
-#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}\r
-.cboxPhoto{float:left; margin:auto; border:0; display:block;}\r
-.cboxIframe{width:100%; height:100%; display:block; border:0;}\r
-\r
-/* \r
-    User Style:\r
-    Change the following styles to modify the appearance of ColorBox.  They are\r
-    ordered & tabbed in a way that represents the nesting of the generated HTML.\r
-*/\r
-#cboxOverlay{background:#000;}\r
-#colorbox{}\r
-    #cboxTopLeft{width:14px; height:14px; background:url(/static/images/controls.png) no-repeat 0 0;}\r
-    #cboxTopCenter{height:14px; background:url(/static/images/border.png) repeat-x top left;}\r
-    #cboxTopRight{width:14px; height:14px; background:url(/static/images/controls.png) no-repeat -36px 0;}\r
-    #cboxBottomLeft{width:14px; height:43px; background:url(/static/images/controls.png) no-repeat 0 -32px;}\r
-    #cboxBottomCenter{height:43px; background:url(/static/images/border.png) repeat-x bottom left;}\r
-    #cboxBottomRight{width:14px; height:43px; background:url(/static/images/controls.png) no-repeat -36px -32px;}\r
-    #cboxMiddleLeft{width:14px; background:url(/static/images/controls.png) repeat-y -175px 0;}\r
-    #cboxMiddleRight{width:14px; background:url(/static/images/controls.png) repeat-y -211px 0;}\r
-    #cboxContent{background:#fff; overflow:visible;}\r
-        #cboxLoadedContent{margin-bottom:5px;}\r
-        #cboxLoadingOverlay{background:url(/static/images/loading_background.png) no-repeat center center;}\r
-        #cboxLoadingGraphic{background:url(/static/images/loading.gif) no-repeat center center;}\r
-        #cboxTitle{position:absolute; bottom:-25px; left:0; text-align:center; width:100%; font-weight:bold; color:#7C7C7C;}\r
-        #cboxCurrent{position:absolute; bottom:-25px; left:58px; font-weight:bold; color:#7C7C7C;}\r
-        \r
-        #cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{position:absolute; bottom:-29px; background:url(/static/images/controls.png) no-repeat 0px 0px; width:23px; height:23px; text-indent:-9999px;}\r
-        #cboxPrevious{left:0px; background-position: -51px -25px;}\r
-        #cboxPrevious.hover{background-position:-51px 0px;}\r
-        #cboxNext{left:27px; background-position:-75px -25px;}\r
-        #cboxNext.hover{background-position:-75px 0px;}\r
-        #cboxClose{right:0; background-position:-100px -25px;}\r
-        #cboxClose.hover{background-position:-100px 0px;}\r
-        \r
-        .cboxSlideshow_on #cboxSlideshow{background-position:-125px 0px; right:27px;}\r
-        .cboxSlideshow_on #cboxSlideshow.hover{background-position:-150px 0px;}\r
-        .cboxSlideshow_off #cboxSlideshow{background-position:-150px -25px; right:27px;}\r
-        .cboxSlideshow_off #cboxSlideshow.hover{background-position:-125px 0px;}\r
+/*
+    ColorBox Core Style:
+    The following CSS is consistent between example themes and should not be altered.
+*/
+#colorbox, #cboxOverlay, #cboxWrapper{position:absolute; top:0; left:0; z-index:9999; overflow:hidden;}
+#cboxOverlay{position:fixed; width:100%; height:100%;}
+#cboxMiddleLeft, #cboxBottomLeft{clear:left;}
+#cboxContent{position:relative;}
+#cboxLoadedContent{overflow:auto;}
+#cboxTitle{margin:0;}
+#cboxLoadingOverlay, #cboxLoadingGraphic{position:absolute; top:0; left:0; width:100%;}
+#cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{cursor:pointer;}
+.cboxPhoto{float:left; margin:auto; border:0; display:block;}
+.cboxIframe{width:100%; height:100%; display:block; border:0;}
+
+/* 
+    User Style:
+    Change the following styles to modify the appearance of ColorBox.  They are
+    ordered & tabbed in a way that represents the nesting of the generated HTML.
+*/
+#cboxOverlay{background:#000;}
+#colorbox{}
+    #cboxTopLeft{width:14px; height:14px; background:url(/static/images/controls.png) no-repeat 0 0;}
+    #cboxTopCenter{height:14px; background:url(/static/images/border.png) repeat-x top left;}
+    #cboxTopRight{width:14px; height:14px; background:url(/static/images/controls.png) no-repeat -36px 0;}
+    #cboxBottomLeft{width:14px; height:43px; background:url(/static/images/controls.png) no-repeat 0 -32px;}
+    #cboxBottomCenter{height:43px; background:url(/static/images/border.png) repeat-x bottom left;}
+    #cboxBottomRight{width:14px; height:43px; background:url(/static/images/controls.png) no-repeat -36px -32px;}
+    #cboxMiddleLeft{width:14px; background:url(/static/images/controls.png) repeat-y -175px 0;}
+    #cboxMiddleRight{width:14px; background:url(/static/images/controls.png) repeat-y -211px 0;}
+    #cboxContent{background:#fff; overflow:visible;}
+        #cboxLoadedContent{margin-bottom:5px;}
+        #cboxLoadingOverlay{background:url(/static/images/loading_background.png) no-repeat center center;}
+        #cboxLoadingGraphic{background:url(/static/images/loading.gif) no-repeat center center;}
+        #cboxTitle{position:absolute; bottom:-25px; left:0; text-align:center; width:100%; font-weight:bold; color:#7C7C7C;}
+        #cboxCurrent{position:absolute; bottom:-25px; left:58px; font-weight:bold; color:#7C7C7C;}
+        
+        #cboxPrevious, #cboxNext, #cboxClose, #cboxSlideshow{position:absolute; bottom:-29px; background:url(/static/images/controls.png) no-repeat 0px 0px; width:23px; height:23px; text-indent:-9999px;}
+        #cboxPrevious{left:0px; background-position: -51px -25px;}
+        #cboxPrevious.hover{background-position:-51px 0px;}
+        #cboxNext{left:27px; background-position:-75px -25px;}
+        #cboxNext.hover{background-position:-75px 0px;}
+        #cboxClose{right:0; background-position:-100px -25px;}
+        #cboxClose.hover{background-position:-100px 0px;}
+        
+        .cboxSlideshow_on #cboxSlideshow{background-position:-125px 0px; right:27px;}
+        .cboxSlideshow_on #cboxSlideshow.hover{background-position:-150px 0px;}
+        .cboxSlideshow_off #cboxSlideshow{background-position:-150px -25px; right:27px;}
+        .cboxSlideshow_off #cboxSlideshow.hover{background-position:-125px 0px;}
old mode 100755 (executable)
new mode 100644 (file)
index b5f6e83..689a007
@@ -1,4 +1,4 @@
-// ColorBox v1.3.17.1 - a full featured, light-weight, customizable lightbox based on jQuery 1.3+\r
-// Copyright (c) 2011 Jack Moore - jack@colorpowered.com\r
-// Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php\r
+// ColorBox v1.3.17.1 - a full featured, light-weight, customizable lightbox based on jQuery 1.3+
+// Copyright (c) 2011 Jack Moore - jack@colorpowered.com
+// Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
 (function(a,b,c){function bc(b){if(!T){O=b,_(a.extend(J,a.data(O,e))),x=a(O),P=0,J.rel!=="nofollow"&&(x=a("."+X).filter(function(){var b=a.data(this,e).rel||this.rel;return b===J.rel}),P=x.index(O),P===-1&&(x=x.add(O),P=x.length-1));if(!R){R=S=!0,q.show();if(J.returnFocus)try{O.blur(),a(O).one(k,function(){try{this.focus()}catch(a){}})}catch(c){}p.css({opacity:+J.opacity,cursor:J.overlayClose?"pointer":"auto"}).show(),J.w=Z(J.initialWidth,"x"),J.h=Z(J.initialHeight,"y"),W.position(0),n&&y.bind("resize."+o+" scroll."+o,function(){p.css({width:y.width(),height:y.height(),top:y.scrollTop(),left:y.scrollLeft()})}).trigger("resize."+o),ba(g,J.onOpen),I.add(C).hide(),H.html(J.close).show()}W.load(!0)}}function bb(){var a,b=f+"Slideshow_",c="click."+f,d,e,g;J.slideshow&&x[1]?(d=function(){E.text(J.slideshowStop).unbind(c).bind(i,function(){if(P<x.length-1||J.loop)a=setTimeout(W.next,J.slideshowSpeed)}).bind(h,function(){clearTimeout(a)}).one(c+" "+j,e),q.removeClass(b+"off").addClass(b+"on"),a=setTimeout(W.next,J.slideshowSpeed)},e=function(){clearTimeout(a),E.text(J.slideshowStart).unbind([i,h,j,c].join(" ")).one(c,d),q.removeClass(b+"on").addClass(b+"off")},J.slideshowAuto?d():e()):q.removeClass(b+"off "+b+"on")}function ba(b,c){c&&c.call(O),a.event.trigger(b)}function _(b){for(var c in b)a.isFunction(b[c])&&c.substring(0,2)!=="on"&&(b[c]=b[c].call(O));b.rel=b.rel||O.rel||"nofollow",b.href=b.href||a(O).attr("href"),b.title=b.title||O.title,typeof b.href=="string"&&(b.href=a.trim(b.href))}function $(a){return J.photo||/\.(gif|png|jpg|jpeg|bmp)(?:\?([^#]*))?(?:#(\.*))?$/i.test(a)}function Z(a,b){b=b==="x"?y.width():y.height();return typeof a=="string"?Math.round(/%/.test(a)?b/100*parseInt(a,10):parseInt(a,10)):a}function Y(c,d){var e=b.createElement("div");c&&(e.id=f+c),e.style.cssText=d||"";return a(e)}var d={transition:"elastic",speed:300,width:!1,initialWidth:"600",innerWidth:!1,maxWidth:!1,height:!1,initialHeight:"450",innerHeight:!1,maxHeight:!1,scalePhotos:!0,scrolling:!0,inline:!1,html:!1,iframe:!1,fastIframe:!0,photo:!1,href:!1,title:!1,rel:!1,opacity:.9,preloading:!0,current:"image {current} of {total}",previous:"previous",next:"next",close:"close",open:!1,returnFocus:!0,loop:!0,slideshow:!1,slideshowAuto:!0,slideshowSpeed:2500,slideshowStart:"start slideshow",slideshowStop:"stop slideshow",onOpen:!1,onLoad:!1,onComplete:!1,onCleanup:!1,onClosed:!1,overlayClose:!0,escKey:!0,arrowKey:!0,top:!1,bottom:!1,left:!1,right:!1,fixed:!1,data:!1},e="colorbox",f="cbox",g=f+"_open",h=f+"_load",i=f+"_complete",j=f+"_cleanup",k=f+"_closed",l=f+"_purge",m=a.browser.msie&&!a.support.opacity,n=m&&a.browser.version<7,o=f+"_IE6",p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J={},K,L,M,N,O,P,Q,R,S,T,U,V,W,X=f+"Element";W=a.fn[e]=a[e]=function(b,c){var f=this,g;if(!f[0]&&f.selector)return f;b=b||{},c&&(b.onComplete=c);if(!f[0]||f.selector===undefined)f=a("<a/>"),b.open=!0;f.each(function(){a.data(this,e,a.extend({},a.data(this,e)||d,b)),a(this).addClass(X)}),g=b.open,a.isFunction(g)&&(g=g.call(f)),g&&bc(f[0]);return f},W.init=function(){y=a(c),q=Y().attr({id:e,"class":m?f+(n?"IE6":"IE"):""}),p=Y("Overlay",n?"position:absolute":"").hide(),r=Y("Wrapper"),s=Y("Content").append(z=Y("LoadedContent","width:0; height:0; overflow:hidden"),B=Y("LoadingOverlay").add(Y("LoadingGraphic")),C=Y("Title"),D=Y("Current"),F=Y("Next"),G=Y("Previous"),E=Y("Slideshow").bind(g,bb),H=Y("Close")),r.append(Y().append(Y("TopLeft"),t=Y("TopCenter"),Y("TopRight")),Y(!1,"clear:left").append(u=Y("MiddleLeft"),s,v=Y("MiddleRight")),Y(!1,"clear:left").append(Y("BottomLeft"),w=Y("BottomCenter"),Y("BottomRight"))).children().children().css({"float":"left"}),A=Y(!1,"position:absolute; width:9999px; visibility:hidden; display:none"),a("body").prepend(p,q.append(r,A)),s.children().hover(function(){a(this).addClass("hover")},function(){a(this).removeClass("hover")}).addClass("hover"),K=t.height()+w.height()+s.outerHeight(!0)-s.height(),L=u.width()+v.width()+s.outerWidth(!0)-s.width(),M=z.outerHeight(!0),N=z.outerWidth(!0),q.css({"padding-bottom":K,"padding-right":L}).hide(),F.click(function(){W.next()}),G.click(function(){W.prev()}),H.click(function(){W.close()}),I=F.add(G).add(D).add(E),s.children().removeClass("hover"),p.click(function(){J.overlayClose&&W.close()}),a(b).bind("keydown."+f,function(a){var b=a.keyCode;R&&J.escKey&&b===27&&(a.preventDefault(),W.close()),R&&J.arrowKey&&x[1]&&(b===37?(a.preventDefault(),G.click()):b===39&&(a.preventDefault(),F.click()))})},W.remove=function(){q.add(p).remove(),a("."+X).removeData(e).removeClass(X)},W.position=function(a,c){function g(a){t[0].style.width=w[0].style.width=s[0].style.width=a.style.width,B[0].style.height=B[1].style.height=s[0].style.height=u[0].style.height=v[0].style.height=a.style.height}var d,e=0,f=0;q.hide(),J.fixed&&!n?q.css({position:"fixed"}):(e=y.scrollTop(),f=y.scrollLeft(),q.css({position:"absolute"})),J.right!==!1?f+=Math.max(y.width()-J.w-N-L-Z(J.right,"x"),0):J.left!==!1?f+=Z(J.left,"x"):f+=Math.max(y.width()-J.w-N-L,0)/2,J.bottom!==!1?e+=Math.max(b.documentElement.clientHeight-J.h-M-K-Z(J.bottom,"y"),0):J.top!==!1?e+=Z(J.top,"y"):e+=Math.max(b.documentElement.clientHeight-J.h-M-K,0)/2,q.show(),d=q.width()===J.w+N&&q.height()===J.h+M?0:a,r[0].style.width=r[0].style.height="9999px",q.dequeue().animate({width:J.w+N,height:J.h+M,top:e,left:f},{duration:d,complete:function(){g(this),S=!1,r[0].style.width=J.w+N+L+"px",r[0].style.height=J.h+M+K+"px",c&&c()},step:function(){g(this)}})},W.resize=function(a){if(R){a=a||{},a.width&&(J.w=Z(a.width,"x")-N-L),a.innerWidth&&(J.w=Z(a.innerWidth,"x")),z.css({width:J.w}),a.height&&(J.h=Z(a.height,"y")-M-K),a.innerHeight&&(J.h=Z(a.innerHeight,"y"));if(!a.innerHeight&&!a.height){var b=z.wrapInner("<div style='overflow:auto'></div>").children();J.h=b.height(),b.replaceWith(b.children())}z.css({height:J.h}),W.position(J.transition==="none"?0:J.speed)}},W.prep=function(b){function h(b){W.position(b,function(){function o(){m&&q[0].style.removeAttribute("filter")}var b,d,g,h,j=x.length,k,n;!R||(n=function(){clearTimeout(V),B.hide(),ba(i,J.onComplete)},m&&Q&&z.fadeIn(100),C.html(J.title).add(z).show(),j>1?(typeof J.current=="string"&&D.html(J.current.replace(/\{current\}/,P+1).replace(/\{total\}/,j)).show(),F[J.loop||P<j-1?"show":"hide"]().html(J.next),G[J.loop||P?"show":"hide"]().html(J.previous),b=P?x[P-1]:x[j-1],g=P<j-1?x[P+1]:x[0],J.slideshow&&E.show(),J.preloading&&(h=a.data(g,e).href||g.href,d=a.data(b,e).href||b.href,h=a.isFunction(h)?h.call(g):h,d=a.isFunction(d)?d.call(b):d,$(h)&&(a("<img/>")[0].src=h),$(d)&&(a("<img/>")[0].src=d))):I.hide(),J.iframe?(k=a("<iframe/>").addClass(f+"Iframe")[0],J.fastIframe?n():a(k).one("load",n),k.name=f+ +(new Date),k.src=J.href,J.scrolling||(k.scrolling="no"),m&&(k.frameBorder=0,k.allowTransparency="true"),a(k).appendTo(z).one(l,function(){k.src="//about:blank"})):n(),J.transition==="fade"?q.fadeTo(c,1,o):o(),y.bind("resize."+f,function(){W.position(0)}))})}function g(){J.h=J.h||z.height(),J.h=J.mh&&J.mh<J.h?J.mh:J.h;return J.h}function d(){J.w=J.w||z.width(),J.w=J.mw&&J.mw<J.w?J.mw:J.w;return J.w}if(!!R){var c=J.transition==="none"?0:J.speed;y.unbind("resize."+f),z.remove(),z=Y("LoadedContent").html(b),z.hide().appendTo(A.show()).css({width:d(),overflow:J.scrolling?"auto":"hidden"}).css({height:g()}).prependTo(s),A.hide(),a(Q).css({"float":"none"}),n&&a("select").not(q.find("select")).filter(function(){return this.style.visibility!=="hidden"}).css({visibility:"hidden"}).one(j,function(){this.style.visibility="inherit"}),J.transition==="fade"?q.fadeTo(c,0,function(){h(0)}):h(c)}},W.load=function(b){var c,d,g=W.prep;S=!0,Q=!1,O=x[P],b||_(a.extend(J,a.data(O,e))),ba(l),ba(h,J.onLoad),J.h=J.height?Z(J.height,"y")-M-K:J.innerHeight&&Z(J.innerHeight,"y"),J.w=J.width?Z(J.width,"x")-N-L:J.innerWidth&&Z(J.innerWidth,"x"),J.mw=J.w,J.mh=J.h,J.maxWidth&&(J.mw=Z(J.maxWidth,"x")-N-L,J.mw=J.w&&J.w<J.mw?J.w:J.mw),J.maxHeight&&(J.mh=Z(J.maxHeight,"y")-M-K,J.mh=J.h&&J.h<J.mh?J.h:J.mh),c=J.href,V=setTimeout(function(){B.show()},100),J.inline?(Y().hide().insertBefore(a(c)[0]).one(l,function(){a(this).replaceWith(z.children())}),g(a(c))):J.iframe?g(" "):J.html?g(J.html):$(c)?(a(Q=new Image).addClass(f+"Photo").error(function(){J.title=!1,g(Y("Error").text("This image could not be loaded"))}).load(function(){var a;Q.onload=null,J.scalePhotos&&(d=function(){Q.height-=Q.height*a,Q.width-=Q.width*a},J.mw&&Q.width>J.mw&&(a=(Q.width-J.mw)/Q.width,d()),J.mh&&Q.height>J.mh&&(a=(Q.height-J.mh)/Q.height,d())),J.h&&(Q.style.marginTop=Math.max(J.h-Q.height,0)/2+"px"),x[1]&&(P<x.length-1||J.loop)&&(Q.style.cursor="pointer",Q.onclick=function(){W.next()}),m&&(Q.style.msInterpolationMode="bicubic"),setTimeout(function(){g(Q)},1)}),setTimeout(function(){Q.src=c},1)):c&&A.load(c,J.data,function(b,c,d){g(c==="error"?Y("Error").text("Request unsuccessful: "+d.statusText):a(this).contents())})},W.next=function(){!S&&x[1]&&(P<x.length-1||J.loop)&&(P=P<x.length-1?P+1:0,W.load())},W.prev=function(){!S&&x[1]&&(P||J.loop)&&(P=P?P-1:x.length-1,W.load())},W.close=function(){R&&!T&&(T=!0,R=!1,ba(j,J.onCleanup),y.unbind("."+f+" ."+o),p.fadeTo(200,0),q.stop().fadeTo(300,0,function(){q.add(p).css({opacity:1,cursor:"auto"}).hide(),ba(l),z.remove(),setTimeout(function(){T=!1,ba(k,J.onClosed)},1)}))},W.element=function(){return a(O)},W.settings=d,U=function(a){a.button!==0&&typeof a.button!="undefined"||a.ctrlKey||a.shiftKey||a.altKey||(a.preventDefault(),bc(this))},a.fn.delegate?a(b).delegate("."+X,"click",U):a("."+X).live("click",U),a(W.init)})(jQuery,document,this)
\ No newline at end of file
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index ecff992..a8f1723
@@ -1,33 +1,33 @@
-<%inherit file="base.mako"/>\r
-<%namespace name="nav" file="nav.mako" />\r
-<%namespace file="scoreboard.mako" import="scoreboard" />\r
-<%namespace file="navlinks.mako" import="navlinks" />\r
-\r
-<%block name="navigation">\r
-${nav.nav('games')}\r
-</%block>\r
-\r
-<%block name="title">\r
-Game Index\r
-</%block>\r
-\r
-% if not games:\r
-<h2>Sorry, no games yet. Get playing!</h2>\r
-\r
-% else:\r
-<div class="row">\r
-  <div class="span12">\r
-    <h2>Recent Games</h2>\r
-    % for (game, server, map) in games:\r
-    <div class="game">\r
-      <h4><img src="/static/images/icons/48x48/${game.game_type_cd}.png" width="30" height="30" /><a href="${request.route_url("map_info", id=map.map_id)}" name="Map info page for ${map.name}">${map.name}</a> on <a href="${request.route_url("server_info", id=server.server_id)}" name="Server info page for ${server.name}">${server.name}</a> <span class="permalink">(<a href="${request.route_url('game_info', id=game.game_id)}" name="Permalink for game #${game.game_id}">permalink</a>)</span></h4>\r
-      ${scoreboard(game.game_type_cd, pgstats[game.game_id])}\r
-    </div>\r
-    % endfor\r
-  </div>\r
-</div>\r
-\r
-<!-- navigation links -->\r
-${navlinks("game_index", games.page, games.last_page)}\r
-% endif\r
-\r
+<%inherit file="base.mako"/>
+<%namespace name="nav" file="nav.mako" />
+<%namespace file="scoreboard.mako" import="scoreboard" />
+<%namespace file="navlinks.mako" import="navlinks" />
+
+<%block name="navigation">
+${nav.nav('games')}
+</%block>
+
+<%block name="title">
+Game Index
+</%block>
+
+% if not games:
+<h2>Sorry, no games yet. Get playing!</h2>
+
+% else:
+<div class="row">
+  <div class="span12">
+    <h2>Recent Games</h2>
+    % for (game, server, map) in games:
+    <div class="game">
+      <h4><img src="/static/images/icons/48x48/${game.game_type_cd}.png" width="30" height="30" /><a href="${request.route_url("map_info", id=map.map_id)}" name="Map info page for ${map.name}">${map.name}</a> on <a href="${request.route_url("server_info", id=server.server_id)}" name="Server info page for ${server.name}">${server.name}</a> <span class="permalink">(<a href="${request.route_url('game_info', id=game.game_id)}" name="Permalink for game #${game.game_id}">permalink</a>)</span></h4>
+      ${scoreboard(game.game_type_cd, pgstats[game.game_id])}
+    </div>
+    % endfor
+  </div>
+</div>
+
+<!-- navigation links -->
+${navlinks("game_index", games.page, games.last_page)}
+% endif
+
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index f5b8abd..c731a0e
-<%inherit file="base.mako"/>\r
-\r
-<%block name="title">\r
-Leaderboard\r
-</%block>\r
-\r
-<%block name="hero_unit">\r
-      <div class="hero-unit">\r
-        <img src="/static/css/img/web_background_l2.png" />\r
-        #####<p id="statline">Tracking <a href="#">12345</a> players, <a href="#">12345</a> games (<a href="#">123</a> duels, <a href="#">123</a> ctfs, <a href="#">123</a> dms), <a href="#">12345</a> servers, and <a href="#">12345</a> maps since November 2011.</p>\r
-        <p id="statline">Tracking Xonotic statistics since October 2011.</p>\r
-        <p><a class="btn btn-primary btn-large" href="http://www.xonotic.org/download" title="Download Xonotic">Get the game &raquo;</a></p>\r
-      </div>\r
-</%block>\r
-\r
-<div class="row">\r
-  <div class="span4">\r
-    ##### DUEL RANKS #####\r
-    <h3>Duel Ranks</h3>\r
-    <table class="table table-bordered table-condensed">\r
-      <thead>\r
-        <tr>\r
-          <th>#</th>\r
-          <th>Nick</th>\r
-          <th>Elo</th>\r
-        </tr>\r
-      </thead>\r
-      <tbody>\r
-      <% i = 1 %>\r
-      % for (player_id, nick, elo) in duel_ranks:\r
-        <tr>\r
-          <td>${i}</td>\r
-          % if player_id != '-':\r
-          <td><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>\r
-          % else:\r
-          <td>${nick|n}</td>\r
-          % endif\r
-          % if elo != '-':\r
-          <td>${round(elo, 3)}</td>\r
-          % else:\r
-          <td>${elo}</td>\r
-          % endif\r
-        </tr>\r
-        <% i = i+1 %>\r
-      % endfor\r
-      </tbody>\r
-    </table>\r
-    <p class="note"><a href="${request.route_url('rank_index', page=1, game_type_cd='duel')}" title="See more duel rankings">More...</a></p>\r
-  </div> <!-- /span4 -->\r
-\r
-  <div class="span4">\r
-    ##### CTF RANKS #####\r
-    <h3>CTF Ranks</h3>\r
-    <table class="table table-bordered table-condensed">\r
-      <thead>\r
-        <tr>\r
-          <th>#</th>\r
-          <th>Nick</th>\r
-          <th>Elo</th>\r
-        </tr>\r
-      </thead>\r
-      <tbody>\r
-      <% i = 1 %>\r
-      % for (player_id, nick, elo) in ctf_ranks:\r
-        <tr>\r
-          <td>${i}</td>\r
-          % if player_id != '-':\r
-          <td><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>\r
-          % else:\r
-          <td>${nick|n}</td>\r
-          % endif\r
-          % if elo != '-':\r
-          <td>${round(elo, 3)}</td>\r
-          % else:\r
-          <td>${elo}</td>\r
-          % endif\r
-        </tr>\r
-        <% i = i+1 %>\r
-      % endfor\r
-      </tbody>\r
-    </table>\r
-    <p class="note"><a href="${request.route_url('rank_index', page=1, game_type_cd='ctf')}" title="See more CTF rankings">More...</a></p>\r
-  </div> <!-- /span4 -->\r
-\r
-  <div class="span4">\r
-    ##### DM RANKS #####\r
-    <h3>DM Ranks</h3>\r
-    <table class="table table-bordered table-condensed">\r
-      <thead>\r
-        <tr>\r
-          <th>#</th>\r
-          <th>Nick</th>\r
-          <th>Elo</th>\r
-        </tr>\r
-      </thead>\r
-      <tbody>\r
-      <% i = 1 %>\r
-      % for (player_id, nick, elo) in dm_ranks:\r
-        <tr>\r
-          <td>${i}</td>\r
-          % if player_id != '-':\r
-          <td><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>\r
-          % else:\r
-          <td>${nick|n}</td>\r
-          % endif\r
-          % if elo != '-':\r
-          <td>${round(elo, 3)}</td>\r
-          % else:\r
-          <td>${elo}</td>\r
-          % endif\r
-        </tr>\r
-        <% i = i+1 %>\r
-      % endfor\r
-    </tbody>\r
-  </table>\r
-  <p class="note"><a href="${request.route_url('rank_index', page=1, game_type_cd='dm')}" title="See more deathmatch rankings">More...</a></p>\r
-  </div> <!-- /span4 -->\r
-</div> <!-- /row -->\r
-\r
-<div class="row">\r
-  <div class="span4">\r
-    <h3>Most Active Players</h3>\r
-    <table class="table table-bordered table-condensed">\r
-      <thead>\r
-        <tr>\r
-          <th>#</th>\r
-          <th>Nick</th>\r
-          <th class="play-time">Play Time</th>\r
-        </tr>\r
-      </thead>\r
-      <tbody>\r
-      <% i = 1 %>\r
-      % for (player_id, nick, alivetime) in top_players:\r
-        <tr>\r
-          <td>${i}</td>\r
-          % if player_id != '-':\r
-          <td><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>\r
-          % else:\r
-          <td>${nick|n}</td>\r
-          % endif\r
-          <td class="play-time">${alivetime}</td>\r
-        </tr>\r
-        <% i = i+1 %>\r
-      % endfor\r
-      </tbody>\r
-    </table>\r
-    <p class="note">*Most active stats are from the past 7 days</p>\r
-  </div> <!-- /span4 -->\r
-\r
-  <div class="span4">\r
-    <h3>Most Active Servers</h3>\r
-    <table class="table table-bordered table-condensed">\r
-      <thead>\r
-        <tr>\r
-          <th>#</th>\r
-          <th>Server</th>\r
-          <th>Games</th>\r
-        </tr>\r
-      </thead>\r
-      <tbody>\r
-      <% i = 1 %>\r
-      % for (server_id, name, count) in top_servers:\r
-        <tr>\r
-          <td>${i}</td>\r
-          % if server_id != '-':\r
-          <td><a href="${request.route_url('server_info', id=server_id)}" title="Go to the server info page for ${name}">${name}</a></td>\r
-          % else:\r
-          <td>${name}</td>\r
-          % endif\r
-          <td>${count}</td>\r
-        </tr>\r
-        <% i = i+1 %>\r
-      % endfor\r
-      </tbody>\r
-    </table>\r
-  </div> <!-- /span4 -->\r
-\r
-  <div class="span4">\r
-    <h3>Most Active Maps</h3>\r
-    <table class="table table-bordered table-condensed">\r
-      <thead>\r
-        <tr>\r
-          <th>#</th>\r
-          <th>Map</th>\r
-          <th>Games</th>\r
-        </tr>\r
-      </thead>\r
-      <tbody>\r
-      <% i = 1 %>\r
-      % for (map_id, name, count) in top_maps:\r
-        <tr>\r
-          <td>${i}</td>\r
-          % if map_id != '-':\r
-          <td><a href="${request.route_url('map_info', id=map_id)}" title="Go to the map info page for ${name}">${name}</a></td>\r
-          % else:\r
-          <td>${name}</td>\r
-          % endif\r
-          <td>${count}</td>\r
-        </tr>\r
-        <% i = i+1 %>\r
-      % endfor\r
-      </tbody>\r
-    </table>\r
-  </div> <!-- /span4 -->\r
-</div> <!-- /row -->\r
-\r
-<div class="row">\r
-  <div class="span12">\r
-    <h3>Recent Games</h3>\r
-    <table class="table table-bordered table-condensed">\r
-      <thead>\r
-        <tr>\r
-          <th></th>\r
-          <th>Type</th>\r
-          <th>Server</th>\r
-          <th>Map</th>\r
-          <th>Time</th>\r
-          <th>Winner</th>\r
-        </tr>\r
-      </thead>\r
-      <tbody>\r
-      % for (game, server, map, pgstat) in recent_games:\r
-        % if game != '-':\r
-        <tr>\r
-          <td><a class="btn btn-primary btn-small" href="${request.route_url('game_info', id=game.game_id)}" title="View detailed information about this game">view</a></td>\r
-          <td class="gt_icon"><img title="${game.game_type_cd}" src="/static/images/icons/24x24/${game.game_type_cd}.png" alt="${game.game_type_cd}" /></td>\r
-          <td><a href="${request.route_url('server_info', id=server.server_id)}" title="Go to the detail page for this server">${server.name}</a></td>\r
-          <td><a href="${request.route_url('map_info', id=map.map_id)}" title="Go to the map detail page for this map">${map.name}</a></td>\r
-          <td>${game.start_dt.strftime('%m/%d/%Y %H:%M')}</td>\r
-          <td>\r
-            % if pgstat.player_id > 2:\r
-            <a href="${request.route_url('player_info', id=pgstat.player_id)}" title="Go to the player info page for this player">${pgstat.nick_html_colors()|n}</a></td>\r
-            % else:\r
-            ${pgstat.nick_html_colors()|n}</td>\r
-            % endif\r
-        </tr>\r
-        % else:\r
-        <tr>\r
-          <td>-</td>\r
-          <td>-</td>\r
-          <td>-</td>\r
-          <td>-</td>\r
-          <td>-</td>\r
-          <td>-</td>\r
-        </tr>\r
-        % endif\r
-        % endfor\r
-        </tbody>\r
-    </table>\r
-  </div> <!-- /span12 -->\r
-</div> <!-- /row -->\r
+<%inherit file="base.mako"/>
+
+<%block name="title">
+Leaderboard
+</%block>
+
+<%block name="hero_unit">
+      <div class="hero-unit">
+        <img src="/static/css/img/web_background_l2.png" />
+        #####<p id="statline">Tracking <a href="#">12345</a> players, <a href="#">12345</a> games (<a href="#">123</a> duels, <a href="#">123</a> ctfs, <a href="#">123</a> dms), <a href="#">12345</a> servers, and <a href="#">12345</a> maps since November 2011.</p>
+        <p id="statline">Tracking Xonotic statistics since October 2011.</p>
+        <p><a class="btn btn-primary btn-large" href="http://www.xonotic.org/download" title="Download Xonotic">Get the game &raquo;</a></p>
+      </div>
+</%block>
+
+<div class="row">
+  <div class="span4">
+    ##### DUEL RANKS #####
+    <h3>Duel Ranks</h3>
+    <table class="table table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th>#</th>
+          <th>Nick</th>
+          <th>Elo</th>
+        </tr>
+      </thead>
+      <tbody>
+      <% i = 1 %>
+      % for (player_id, nick, elo) in duel_ranks:
+        <tr>
+          <td>${i}</td>
+          % if player_id != '-':
+          <td><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>
+          % else:
+          <td>${nick|n}</td>
+          % endif
+          % if elo != '-':
+          <td>${round(elo, 3)}</td>
+          % else:
+          <td>${elo}</td>
+          % endif
+        </tr>
+        <% i = i+1 %>
+      % endfor
+      </tbody>
+    </table>
+    <p class="note"><a href="${request.route_url('rank_index', page=1, game_type_cd='duel')}" title="See more duel rankings">More...</a></p>
+  </div> <!-- /span4 -->
+
+  <div class="span4">
+    ##### CTF RANKS #####
+    <h3>CTF Ranks</h3>
+    <table class="table table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th>#</th>
+          <th>Nick</th>
+          <th>Elo</th>
+        </tr>
+      </thead>
+      <tbody>
+      <% i = 1 %>
+      % for (player_id, nick, elo) in ctf_ranks:
+        <tr>
+          <td>${i}</td>
+          % if player_id != '-':
+          <td><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>
+          % else:
+          <td>${nick|n}</td>
+          % endif
+          % if elo != '-':
+          <td>${round(elo, 3)}</td>
+          % else:
+          <td>${elo}</td>
+          % endif
+        </tr>
+        <% i = i+1 %>
+      % endfor
+      </tbody>
+    </table>
+    <p class="note"><a href="${request.route_url('rank_index', page=1, game_type_cd='ctf')}" title="See more CTF rankings">More...</a></p>
+  </div> <!-- /span4 -->
+
+  <div class="span4">
+    ##### DM RANKS #####
+    <h3>DM Ranks</h3>
+    <table class="table table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th>#</th>
+          <th>Nick</th>
+          <th>Elo</th>
+        </tr>
+      </thead>
+      <tbody>
+      <% i = 1 %>
+      % for (player_id, nick, elo) in dm_ranks:
+        <tr>
+          <td>${i}</td>
+          % if player_id != '-':
+          <td><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>
+          % else:
+          <td>${nick|n}</td>
+          % endif
+          % if elo != '-':
+          <td>${round(elo, 3)}</td>
+          % else:
+          <td>${elo}</td>
+          % endif
+        </tr>
+        <% i = i+1 %>
+      % endfor
+    </tbody>
+  </table>
+  <p class="note"><a href="${request.route_url('rank_index', page=1, game_type_cd='dm')}" title="See more deathmatch rankings">More...</a></p>
+  </div> <!-- /span4 -->
+</div> <!-- /row -->
+
+<div class="row">
+  <div class="span4">
+    <h3>Most Active Players</h3>
+    <table class="table table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th>#</th>
+          <th>Nick</th>
+          <th class="play-time">Play Time</th>
+        </tr>
+      </thead>
+      <tbody>
+      <% i = 1 %>
+      % for (player_id, nick, alivetime) in top_players:
+        <tr>
+          <td>${i}</td>
+          % if player_id != '-':
+          <td><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>
+          % else:
+          <td>${nick|n}</td>
+          % endif
+          <td class="play-time">${alivetime}</td>
+        </tr>
+        <% i = i+1 %>
+      % endfor
+      </tbody>
+    </table>
+    <p class="note">*Most active stats are from the past 7 days</p>
+  </div> <!-- /span4 -->
+
+  <div class="span4">
+    <h3>Most Active Servers</h3>
+    <table class="table table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th>#</th>
+          <th>Server</th>
+          <th>Games</th>
+        </tr>
+      </thead>
+      <tbody>
+      <% i = 1 %>
+      % for (server_id, name, count) in top_servers:
+        <tr>
+          <td>${i}</td>
+          % if server_id != '-':
+          <td><a href="${request.route_url('server_info', id=server_id)}" title="Go to the server info page for ${name}">${name}</a></td>
+          % else:
+          <td>${name}</td>
+          % endif
+          <td>${count}</td>
+        </tr>
+        <% i = i+1 %>
+      % endfor
+      </tbody>
+    </table>
+  </div> <!-- /span4 -->
+
+  <div class="span4">
+    <h3>Most Active Maps</h3>
+    <table class="table table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th>#</th>
+          <th>Map</th>
+          <th>Games</th>
+        </tr>
+      </thead>
+      <tbody>
+      <% i = 1 %>
+      % for (map_id, name, count) in top_maps:
+        <tr>
+          <td>${i}</td>
+          % if map_id != '-':
+          <td><a href="${request.route_url('map_info', id=map_id)}" title="Go to the map info page for ${name}">${name}</a></td>
+          % else:
+          <td>${name}</td>
+          % endif
+          <td>${count}</td>
+        </tr>
+        <% i = i+1 %>
+      % endfor
+      </tbody>
+    </table>
+  </div> <!-- /span4 -->
+</div> <!-- /row -->
+
+<div class="row">
+  <div class="span12">
+    <h3>Recent Games</h3>
+    <table class="table table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th></th>
+          <th>Type</th>
+          <th>Server</th>
+          <th>Map</th>
+          <th>Time</th>
+          <th>Winner</th>
+        </tr>
+      </thead>
+      <tbody>
+      % for (game, server, map, pgstat) in recent_games:
+        % if game != '-':
+        <tr>
+          <td><a class="btn btn-primary btn-small" href="${request.route_url('game_info', id=game.game_id)}" title="View detailed information about this game">view</a></td>
+          <td class="gt_icon"><img title="${game.game_type_cd}" src="/static/images/icons/24x24/${game.game_type_cd}.png" alt="${game.game_type_cd}" /></td>
+          <td><a href="${request.route_url('server_info', id=server.server_id)}" title="Go to the detail page for this server">${server.name}</a></td>
+          <td><a href="${request.route_url('map_info', id=map.map_id)}" title="Go to the map detail page for this map">${map.name}</a></td>
+          <td>${game.start_dt.strftime('%m/%d/%Y %H:%M')}</td>
+          <td>
+            % if pgstat.player_id > 2:
+            <a href="${request.route_url('player_info', id=pgstat.player_id)}" title="Go to the player info page for this player">${pgstat.nick_html_colors()|n}</a></td>
+            % else:
+            ${pgstat.nick_html_colors()|n}</td>
+            % endif
+        </tr>
+        % else:
+        <tr>
+          <td>-</td>
+          <td>-</td>
+          <td>-</td>
+          <td>-</td>
+          <td>-</td>
+          <td>-</td>
+        </tr>
+        % endif
+        % endfor
+        </tbody>
+    </table>
+  </div> <!-- /span12 -->
+</div> <!-- /row -->
old mode 100755 (executable)
new mode 100644 (file)
index fe34956..bb297ee
@@ -1,42 +1,42 @@
-<%inherit file="base.mako"/>\r
-<%namespace name="nav" file="nav.mako" />\r
-<%namespace file="navlinks.mako" import="navlinks" />\r
-\r
-<%block name="navigation">\r
-${nav.nav('maps')}\r
-</%block>\r
-\r
-<%block name="title">\r
-Map Index\r
-</%block>\r
-\r
-% if not maps:\r
-<h2>Sorry, no maps yet. Get playing!</h2>\r
-\r
-% else:\r
-<div class="row">\r
-  <div class="span6">\r
-    <form method="get" action="${request.route_url('search')}">\r
-      <input type="hidden" name="fs" />\r
-      <input type="text" name="map_name" />\r
-      <input type="submit" value="search" />\r
-    </form>\r
-    <table class="table table-bordered table-condensed">\r
-      <tr>\r
-        <th>Name</th>\r
-        <th>Added</th>\r
-      </tr>\r
-    % for map in maps:\r
-      <tr>\r
-        <td><a href="${request.route_url("map_info", id=map.map_id)}" title="Go to this map's info page">${map.name}</a></th>\r
-        <td>${map.create_dt.strftime('%m/%d/%Y at %H:%M')}</td>\r
-    </td>\r
-      </tr>\r
-    % endfor\r
-    </table>\r
-    % endif\r
-\r
-    <!-- navigation links -->\r
-    ${navlinks("map_index", maps.page, maps.last_page)}\r
-  </div> <!-- /span4 -->\r
-</div> <!-- /row -->\r
+<%inherit file="base.mako"/>
+<%namespace name="nav" file="nav.mako" />
+<%namespace file="navlinks.mako" import="navlinks" />
+
+<%block name="navigation">
+${nav.nav('maps')}
+</%block>
+
+<%block name="title">
+Map Index
+</%block>
+
+% if not maps:
+<h2>Sorry, no maps yet. Get playing!</h2>
+
+% else:
+<div class="row">
+  <div class="span6">
+    <form method="get" action="${request.route_url('search')}">
+      <input type="hidden" name="fs" />
+      <input type="text" name="map_name" />
+      <input type="submit" value="search" />
+    </form>
+    <table class="table table-bordered table-condensed">
+      <tr>
+        <th>Name</th>
+        <th>Added</th>
+      </tr>
+    % for map in maps:
+      <tr>
+        <td><a href="${request.route_url("map_info", id=map.map_id)}" title="Go to this map's info page">${map.name}</a></th>
+        <td>${map.create_dt.strftime('%m/%d/%Y at %H:%M')}</td>
+    </td>
+      </tr>
+    % endfor
+    </table>
+    % endif
+
+    <!-- navigation links -->
+    ${navlinks("map_index", maps.page, maps.last_page)}
+  </div> <!-- /span4 -->
+</div> <!-- /row -->
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 6bfdc7d..2ef7d5c
@@ -1,40 +1,40 @@
-<%inherit file="base.mako"/>\r
-<%namespace name="nav" file="nav.mako" />\r
-<%namespace file="navlinks.mako" import="navlinks" />\r
-\r
-<%block name="navigation">\r
-${nav.nav('players')}\r
-</%block>\r
-\r
-<%block name="title">\r
-Player Index\r
-</%block>\r
-\r
-% if not players:\r
-<h2>Sorry, no players yet. Get playing!</h2>\r
-\r
-% else:\r
-<div class="row">\r
-  <div class="span6">\r
-    <form method="get" action="${request.route_url('search')}">\r
-      <input type="hidden" name="fs" />\r
-      <input type="text" name="nick" />\r
-      <input type="submit" value="search" />\r
-    </form>\r
-    <table class="table table-bordered table-condensed">\r
-      <tr>\r
-        <th>Nick</th>\r
-        <th class="create-dt">Joined</th>\r
-      </tr>\r
-    % for player in players:\r
-      <tr>\r
-        <td><a href="${request.route_url("player_info", id=player.player_id)}" title="Go to this player's info page">${player.nick_html_colors()|n}</a></th>\r
-        <td>${player.joined_pretty_date()}</th>\r
-      </tr>\r
-    % endfor\r
-    </table>\r
-% endif\r
-\r
-    ${navlinks("player_index", players.page, players.last_page)}\r
-  </div> <!-- /span4 -->\r
-</div> <!-- /row -->\r
+<%inherit file="base.mako"/>
+<%namespace name="nav" file="nav.mako" />
+<%namespace file="navlinks.mako" import="navlinks" />
+
+<%block name="navigation">
+${nav.nav('players')}
+</%block>
+
+<%block name="title">
+Player Index
+</%block>
+
+% if not players:
+<h2>Sorry, no players yet. Get playing!</h2>
+
+% else:
+<div class="row">
+  <div class="span6">
+    <form method="get" action="${request.route_url('search')}">
+      <input type="hidden" name="fs" />
+      <input type="text" name="nick" />
+      <input type="submit" value="search" />
+    </form>
+    <table class="table table-bordered table-condensed">
+      <tr>
+        <th>Nick</th>
+        <th class="create-dt">Joined</th>
+      </tr>
+    % for player in players:
+      <tr>
+        <td><a href="${request.route_url("player_info", id=player.player_id)}" title="Go to this player's info page">${player.nick_html_colors()|n}</a></th>
+        <td>${player.joined_pretty_date()}</th>
+      </tr>
+    % endfor
+    </table>
+% endif
+
+    ${navlinks("player_index", players.page, players.last_page)}
+  </div> <!-- /span4 -->
+</div> <!-- /row -->
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index a3ce68f..47a2d06
@@ -1,43 +1,43 @@
-<%inherit file="base.mako"/>\r
-<%namespace file="navlinks.mako" import="navlinks" />\r
-\r
-<%block name="title">\r
-Rank Index - ${parent.title()}\r
-</%block>\r
-\r
-% if not ranks:\r
-<h2>Sorry, no ranks yet. Get some buddies together and start playing!</h2>\r
-\r
-% else:\r
-<h2>\r
-% if game_type_cd == 'dm':\r
-Deathmatch \r
-% elif game_type_cd == 'duel':\r
-Duel \r
-% elif game_type_cd == 'tdm':\r
-Team Deathmatch \r
-% elif game_type_cd == 'ctf':\r
-Capture The Flag \r
-% endif\r
-\r
-Rank Index</h2>\r
-<table id="rank-index-table" border="1">\r
-  <tr>\r
-    <th>Rank</th>\r
-    <th>Nick</th>\r
-    <th>Elo</th>\r
-  </tr>\r
-<% i = 1 %>\r
-% for rank in ranks:\r
-  <tr>\r
-    <td>${rank.rank}</td>\r
-    <td><a href="${request.route_url("player_info", id=rank.player_id)}" title="Go to this player's info page">${rank.nick_html_colors()|n}</a></th>\r
-    <td>${round(rank.elo, 3)}</th>\r
-  </tr>\r
-<% i += 1 %>\r
-% endfor\r
-</table>\r
-\r
-<!-- navigation links -->\r
-${navlinks("rank_index", ranks.page, ranks.last_page, game_type_cd=game_type_cd)}\r
-% endif\r
+<%inherit file="base.mako"/>
+<%namespace file="navlinks.mako" import="navlinks" />
+
+<%block name="title">
+Rank Index - ${parent.title()}
+</%block>
+
+% if not ranks:
+<h2>Sorry, no ranks yet. Get some buddies together and start playing!</h2>
+
+% else:
+<h2>
+% if game_type_cd == 'dm':
+Deathmatch 
+% elif game_type_cd == 'duel':
+Duel 
+% elif game_type_cd == 'tdm':
+Team Deathmatch 
+% elif game_type_cd == 'ctf':
+Capture The Flag 
+% endif
+
+Rank Index</h2>
+<table id="rank-index-table" border="1">
+  <tr>
+    <th>Rank</th>
+    <th>Nick</th>
+    <th>Elo</th>
+  </tr>
+<% i = 1 %>
+% for rank in ranks:
+  <tr>
+    <td>${rank.rank}</td>
+    <td><a href="${request.route_url("player_info", id=rank.player_id)}" title="Go to this player's info page">${rank.nick_html_colors()|n}</a></th>
+    <td>${round(rank.elo, 3)}</th>
+  </tr>
+<% i += 1 %>
+% endfor
+</table>
+
+<!-- navigation links -->
+${navlinks("rank_index", ranks.page, ranks.last_page, game_type_cd=game_type_cd)}
+% endif
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index d6893cd..bc2b378
@@ -1,40 +1,40 @@
-<%inherit file="base.mako"/>\r
-<%namespace name="nav" file="nav.mako" />\r
-<%namespace file="navlinks.mako" import="navlinks" />\r
-\r
-<%block name="navigation">\r
-${nav.nav('servers')}\r
-</%block>\r
-\r
-<%block name="title">\r
-Server Index\r
-</%block>\r
-\r
-% if not servers:\r
-<h2>Sorry, no servers yet. Get playing!</h2>\r
-\r
-% else:\r
-<div class="row">\r
-  <div class="span6">\r
-    <form method="get" action="${request.route_url('search')}">\r
-      <input type="hidden" name="fs" />\r
-      <input type="text" name="server_name" />\r
-      <input type="submit" value="search" />\r
-    </form>\r
-    <table class="table table-bordered table-condensed">\r
-      <tr>\r
-        <th>Name</th>\r
-        <th class="create-dt">Added</th>\r
-      </tr>\r
-    % for server in servers:\r
-      <tr>\r
-        <td><a href="${request.route_url("server_info", id=server.server_id)}" title="Go to this server's info page">${server.name}</a></th>\r
-        <td>${server.create_dt.strftime('%m/%d/%Y at %H:%M')}</td>\r
-      </tr>\r
-    % endfor\r
-    </table>\r
-    % endif\r
-\r
-    ${navlinks("server_index", servers.page, servers.last_page)}\r
-  </div> <!-- /span4 -->\r
-</div> <!-- /row -->\r
+<%inherit file="base.mako"/>
+<%namespace name="nav" file="nav.mako" />
+<%namespace file="navlinks.mako" import="navlinks" />
+
+<%block name="navigation">
+${nav.nav('servers')}
+</%block>
+
+<%block name="title">
+Server Index
+</%block>
+
+% if not servers:
+<h2>Sorry, no servers yet. Get playing!</h2>
+
+% else:
+<div class="row">
+  <div class="span6">
+    <form method="get" action="${request.route_url('search')}">
+      <input type="hidden" name="fs" />
+      <input type="text" name="server_name" />
+      <input type="submit" value="search" />
+    </form>
+    <table class="table table-bordered table-condensed">
+      <tr>
+        <th>Name</th>
+        <th class="create-dt">Added</th>
+      </tr>
+    % for server in servers:
+      <tr>
+        <td><a href="${request.route_url("server_info", id=server.server_id)}" title="Go to this server's info page">${server.name}</a></th>
+        <td>${server.create_dt.strftime('%m/%d/%Y at %H:%M')}</td>
+      </tr>
+    % endfor
+    </table>
+    % endif
+
+    ${navlinks("server_index", servers.page, servers.last_page)}
+  </div> <!-- /span4 -->
+</div> <!-- /row -->
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 45a91b5..525d2f9
@@ -1,8 +1,8 @@
-from xonstat.views.submission import stats_submit\r
-from xonstat.views.player import player_index, player_info, player_game_index\r
-from xonstat.views.player import player_accuracy_json, player_damage_json\r
-from xonstat.views.game import game_index, game_info, rank_index\r
-from xonstat.views.map import map_info, map_index, map_index_json\r
-from xonstat.views.server import server_info, server_game_index, server_index\r
-from xonstat.views.search import search_q, search\r
-from xonstat.views.main import main_index\r
+from xonstat.views.submission import stats_submit
+from xonstat.views.player import player_index, player_info, player_game_index
+from xonstat.views.player import player_accuracy_json, player_damage_json
+from xonstat.views.game import game_index, game_info, rank_index
+from xonstat.views.map import map_info, map_index, map_index_json
+from xonstat.views.server import server_info, server_game_index, server_index
+from xonstat.views.search import search_q, search
+from xonstat.views.main import main_index
old mode 100755 (executable)
new mode 100644 (file)
index 44e929e..70bbd58
-import datetime\r
-import logging\r
-import re\r
-import time\r
-from pyramid.response import Response\r
-from sqlalchemy import desc, func, over\r
-from webhelpers.paginate import Page, PageURL\r
-from xonstat.models import *\r
-from xonstat.util import page_url\r
-\r
-log = logging.getLogger(__name__)\r
-\r
-\r
-def _game_index_data(request):\r
-    if request.params.has_key('page'):\r
-        current_page = request.params['page']\r
-    else:\r
-        current_page = 1\r
-\r
-    games_q = DBSession.query(Game, Server, Map).\\r
-            filter(Game.server_id == Server.server_id).\\r
-            filter(Game.map_id == Map.map_id).\\r
-            order_by(Game.game_id.desc())\r
-\r
-    games = Page(games_q, current_page, items_per_page=10, url=page_url)\r
-\r
-    pgstats = {}\r
-    for (game, server, map) in games:\r
-        pgstats[game.game_id] = DBSession.query(PlayerGameStat).\\r
-                filter(PlayerGameStat.game_id == game.game_id).\\r
-                order_by(PlayerGameStat.rank).\\r
-                order_by(PlayerGameStat.score).all()\r
-\r
-    return {'games':games, \r
-            'pgstats':pgstats}\r
-\r
-\r
-def game_index(request):\r
-    """\r
-    Provides a list of current games, with the associated game stats.\r
-    These games are ordered by game_id, with the most current ones first.\r
-    Paginated.\r
-    """\r
-    return _game_index_data(request)\r
-\r
-\r
-def _game_info_data(request):\r
-    game_id = request.matchdict['id']\r
-    try:\r
-        notfound = False\r
-\r
-        (game, server, map) = DBSession.query(Game, Server, Map).\\r
-                filter(Game.game_id == game_id).\\r
-                filter(Game.server_id == Server.server_id).\\r
-                filter(Game.map_id == Map.map_id).one()\r
-\r
-        pgstats = DBSession.query(PlayerGameStat).\\r
-                filter(PlayerGameStat.game_id == game_id).\\r
-                order_by(PlayerGameStat.rank).\\r
-                order_by(PlayerGameStat.score).\\r
-                all()\r
-\r
-        pwstats = {}\r
-        for (pwstat, pgstat, weapon) in DBSession.query(PlayerWeaponStat, PlayerGameStat, Weapon).\\r
-                filter(PlayerWeaponStat.game_id == game_id).\\r
-                filter(PlayerWeaponStat.weapon_cd == Weapon.weapon_cd).\\r
-                filter(PlayerWeaponStat.player_game_stat_id == \\r
-                    PlayerGameStat.player_game_stat_id).\\r
-                order_by(PlayerGameStat.rank).\\r
-                order_by(PlayerGameStat.score).\\r
-                order_by(Weapon.descr).\\r
-                all():\r
-                    if pgstat.player_game_stat_id not in pwstats:\r
-                        pwstats[pgstat.player_game_stat_id] = []\r
-\r
-                    # NOTE adding pgstat to position 6 in order to display nick.\r
-                    # You have to use a slice [0:5] to pass to the accuracy \r
-                    # template\r
-                    pwstats[pgstat.player_game_stat_id].append((weapon.descr, \r
-                        weapon.weapon_cd, pwstat.actual, pwstat.max, \r
-                        pwstat.hit, pwstat.fired, pgstat))\r
-\r
-    except Exception as inst:\r
-        game = None\r
-        server = None\r
-        map = None\r
-        pgstats = None\r
-        pwstats = None\r
-        raise inst\r
-\r
-    return {'game':game,\r
-            'server':server,\r
-            'map':map,\r
-            'pgstats':pgstats,\r
-            'pwstats':pwstats,\r
-            }\r
-\r
-\r
-def game_info(request):\r
-    """\r
-    List the game stats (scoreboard) for a particular game. Paginated.\r
-    """\r
-    return _game_info_data(request)\r
-\r
-\r
-def _rank_index_data(request):\r
-    if request.params.has_key('page'):\r
-        current_page = request.params['page']\r
-    else:\r
-        current_page = 1\r
-\r
-    game_type_cd = request.matchdict['game_type_cd']\r
-\r
-    ranks_q = DBSession.query(PlayerRank).\\r
-            filter(PlayerRank.game_type_cd==game_type_cd).\\r
-            order_by(PlayerRank.rank)\r
-\r
-    ranks = Page(ranks_q, current_page, url=page_url)\r
-\r
-    if len(ranks) == 0:\r
-        ranks = None\r
-\r
-    return {\r
-            'ranks':ranks,\r
-            'game_type_cd':game_type_cd,\r
-           }\r
-\r
-\r
-def rank_index(request):\r
-    """\r
-    Provide a list of gametype ranks, paginated.\r
-    """\r
-    return _rank_index_data(request)\r
+import datetime
+import logging
+import re
+import time
+from pyramid.response import Response
+from sqlalchemy import desc, func, over
+from webhelpers.paginate import Page, PageURL
+from xonstat.models import *
+from xonstat.util import page_url
+
+log = logging.getLogger(__name__)
+
+
+def _game_index_data(request):
+    if request.params.has_key('page'):
+        current_page = request.params['page']
+    else:
+        current_page = 1
+
+    games_q = DBSession.query(Game, Server, Map).\
+            filter(Game.server_id == Server.server_id).\
+            filter(Game.map_id == Map.map_id).\
+            order_by(Game.game_id.desc())
+
+    games = Page(games_q, current_page, items_per_page=10, url=page_url)
+
+    pgstats = {}
+    for (game, server, map) in games:
+        pgstats[game.game_id] = DBSession.query(PlayerGameStat).\
+                filter(PlayerGameStat.game_id == game.game_id).\
+                order_by(PlayerGameStat.rank).\
+                order_by(PlayerGameStat.score).all()
+
+    return {'games':games, 
+            'pgstats':pgstats}
+
+
+def game_index(request):
+    """
+    Provides a list of current games, with the associated game stats.
+    These games are ordered by game_id, with the most current ones first.
+    Paginated.
+    """
+    return _game_index_data(request)
+
+
+def _game_info_data(request):
+    game_id = request.matchdict['id']
+    try:
+        notfound = False
+
+        (game, server, map) = DBSession.query(Game, Server, Map).\
+                filter(Game.game_id == game_id).\
+                filter(Game.server_id == Server.server_id).\
+                filter(Game.map_id == Map.map_id).one()
+
+        pgstats = DBSession.query(PlayerGameStat).\
+                filter(PlayerGameStat.game_id == game_id).\
+                order_by(PlayerGameStat.rank).\
+                order_by(PlayerGameStat.score).\
+                all()
+
+        pwstats = {}
+        for (pwstat, pgstat, weapon) in DBSession.query(PlayerWeaponStat, PlayerGameStat, Weapon).\
+                filter(PlayerWeaponStat.game_id == game_id).\
+                filter(PlayerWeaponStat.weapon_cd == Weapon.weapon_cd).\
+                filter(PlayerWeaponStat.player_game_stat_id == \
+                    PlayerGameStat.player_game_stat_id).\
+                order_by(PlayerGameStat.rank).\
+                order_by(PlayerGameStat.score).\
+                order_by(Weapon.descr).\
+                all():
+                    if pgstat.player_game_stat_id not in pwstats:
+                        pwstats[pgstat.player_game_stat_id] = []
+
+                    # NOTE adding pgstat to position 6 in order to display nick.
+                    # You have to use a slice [0:5] to pass to the accuracy 
+                    # template
+                    pwstats[pgstat.player_game_stat_id].append((weapon.descr, 
+                        weapon.weapon_cd, pwstat.actual, pwstat.max, 
+                        pwstat.hit, pwstat.fired, pgstat))
+
+    except Exception as inst:
+        game = None
+        server = None
+        map = None
+        pgstats = None
+        pwstats = None
+        raise inst
+
+    return {'game':game,
+            'server':server,
+            'map':map,
+            'pgstats':pgstats,
+            'pwstats':pwstats,
+            }
+
+
+def game_info(request):
+    """
+    List the game stats (scoreboard) for a particular game. Paginated.
+    """
+    return _game_info_data(request)
+
+
+def _rank_index_data(request):
+    if request.params.has_key('page'):
+        current_page = request.params['page']
+    else:
+        current_page = 1
+
+    game_type_cd = request.matchdict['game_type_cd']
+
+    ranks_q = DBSession.query(PlayerRank).\
+            filter(PlayerRank.game_type_cd==game_type_cd).\
+            order_by(PlayerRank.rank)
+
+    ranks = Page(ranks_q, current_page, url=page_url)
+
+    if len(ranks) == 0:
+        ranks = None
+
+    return {
+            'ranks':ranks,
+            'game_type_cd':game_type_cd,
+           }
+
+
+def rank_index(request):
+    """
+    Provide a list of gametype ranks, paginated.
+    """
+    return _rank_index_data(request)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 86e70ad..df64122
-import logging\r
-import sqlalchemy.sql.functions as func\r
-import sqlalchemy.sql.expression as expr\r
-from datetime import datetime, timedelta\r
-from pyramid.response import Response\r
-from sqlalchemy import desc\r
-from webhelpers.paginate import Page, PageURL\r
-from xonstat.models import *\r
-from xonstat.util import page_url\r
-\r
-log = logging.getLogger(__name__)\r
-\r
-def _map_index_data(request):\r
-    if request.params.has_key('page'):\r
-        current_page = request.params['page']\r
-    else:\r
-        current_page = 1\r
-\r
-    try:\r
-        map_q = DBSession.query(Map).\\r
-                order_by(Map.map_id.desc())\r
-\r
-        maps = Page(map_q, current_page, items_per_page=10, url=page_url)\r
-\r
-    except Exception as e:\r
-        maps = None\r
-\r
-    return {'maps':maps, }\r
-\r
-\r
-def map_index(request):\r
-    """\r
-    Provides a list of all the current maps. \r
-    """\r
-    return _map_index_data(request)\r
-\r
-\r
-def map_index_json(request):\r
-    """\r
-    Provides a JSON-serialized list of all the current maps. \r
-    """\r
-    view_data =  _map_index_data(request)\r
-\r
-    maps = [m.to_dict() for m in view_data['maps']]\r
-\r
-    return maps\r
-\r
-\r
-def _map_info_data(request):\r
-    map_id = request.matchdict['id']\r
-\r
-    try: \r
-        leaderboard_lifetime = int(\r
-                request.registry.settings['xonstat.leaderboard_lifetime'])\r
-    except:\r
-        leaderboard_lifetime = 30\r
-\r
-    leaderboard_count = 10\r
-    recent_games_count = 20\r
-\r
-    try:\r
-        gmap = DBSession.query(Map).filter_by(map_id=map_id).one()\r
-\r
-        # recent games on this map\r
-        recent_games = DBSession.query(Game, Server, Map, PlayerGameStat).\\r
-            filter(Game.server_id==Server.server_id).\\r
-            filter(Game.map_id==Map.map_id).\\r
-            filter(Game.map_id==map_id).\\r
-            filter(PlayerGameStat.game_id==Game.game_id).\\r
-            filter(PlayerGameStat.rank==1).\\r
-            order_by(expr.desc(Game.start_dt)).all()[0:recent_games_count]\r
-\r
-        # top players by score\r
-        top_scorers = DBSession.query(Player.player_id, Player.nick,\r
-                func.sum(PlayerGameStat.score)).\\r
-                filter(Player.player_id == PlayerGameStat.player_id).\\r
-                filter(Game.game_id == PlayerGameStat.game_id).\\r
-                filter(Game.map_id == map_id).\\r
-                filter(Player.player_id > 2).\\r
-                filter(PlayerGameStat.create_dt > \r
-                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\\r
-                order_by(expr.desc(func.sum(PlayerGameStat.score))).\\r
-                group_by(Player.nick).\\r
-                group_by(Player.player_id).all()[0:leaderboard_count]\r
-\r
-        top_scorers = [(player_id, html_colors(nick), score) \\r
-                for (player_id, nick, score) in top_scorers]\r
-\r
-        # top players by playing time\r
-        top_players = DBSession.query(Player.player_id, Player.nick, \r
-                func.sum(PlayerGameStat.alivetime)).\\r
-                filter(Player.player_id == PlayerGameStat.player_id).\\r
-                filter(Game.game_id == PlayerGameStat.game_id).\\r
-                filter(Game.map_id == map_id).\\r
-                filter(Player.player_id > 2).\\r
-                filter(PlayerGameStat.create_dt > \r
-                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\\r
-                order_by(expr.desc(func.sum(PlayerGameStat.alivetime))).\\r
-                group_by(Player.nick).\\r
-                group_by(Player.player_id).all()[0:leaderboard_count]\r
-\r
-        top_players = [(player_id, html_colors(nick), score) \\r
-                for (player_id, nick, score) in top_players]\r
-\r
-        # top servers using/playing this map\r
-        top_servers = DBSession.query(Server.server_id, Server.name, \r
-                func.count(Game.game_id)).\\r
-                filter(Game.server_id == Server.server_id).\\r
-                filter(Game.map_id == map_id).\\r
-                filter(Game.create_dt > \r
-                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\\r
-                order_by(expr.desc(func.count(Game.game_id))).\\r
-                group_by(Server.name).\\r
-                group_by(Server.server_id).all()[0:leaderboard_count]\r
-\r
-    except Exception as e:\r
-        gmap = None\r
-    return {'gmap':gmap,\r
-            'recent_games':recent_games,\r
-            'top_scorers':top_scorers,\r
-            'top_players':top_players,\r
-            'top_servers':top_servers,\r
-            }\r
-\r
-\r
-def map_info(request):\r
-    """\r
-    List the information stored about a given map.\r
-    """\r
-    mapinfo_data =  _map_info_data(request)\r
-\r
-    # FIXME: code clone, should get these from _map_info_data\r
-    leaderboard_count = 10\r
-    recent_games_count = 20\r
-\r
-    for i in range(recent_games_count-len(mapinfo_data['recent_games'])):\r
-        mapinfo_data['recent_games'].append(('-', '-', '-', '-'))\r
-\r
-    for i in range(leaderboard_count-len(mapinfo_data['top_scorers'])):\r
-        mapinfo_data['top_scorers'].append(('-', '-', '-'))\r
-\r
-    for i in range(leaderboard_count-len(mapinfo_data['top_players'])):\r
-        mapinfo_data['top_players'].append(('-', '-', '-'))\r
-\r
-    for i in range(leaderboard_count-len(mapinfo_data['top_servers'])):\r
-        mapinfo_data['top_servers'].append(('-', '-', '-'))\r
-\r
-    return mapinfo_data\r
+import logging
+import sqlalchemy.sql.functions as func
+import sqlalchemy.sql.expression as expr
+from datetime import datetime, timedelta
+from pyramid.response import Response
+from sqlalchemy import desc
+from webhelpers.paginate import Page, PageURL
+from xonstat.models import *
+from xonstat.util import page_url
+
+log = logging.getLogger(__name__)
+
+def _map_index_data(request):
+    if request.params.has_key('page'):
+        current_page = request.params['page']
+    else:
+        current_page = 1
+
+    try:
+        map_q = DBSession.query(Map).\
+                order_by(Map.map_id.desc())
+
+        maps = Page(map_q, current_page, items_per_page=10, url=page_url)
+
+    except Exception as e:
+        maps = None
+
+    return {'maps':maps, }
+
+
+def map_index(request):
+    """
+    Provides a list of all the current maps. 
+    """
+    return _map_index_data(request)
+
+
+def map_index_json(request):
+    """
+    Provides a JSON-serialized list of all the current maps. 
+    """
+    view_data =  _map_index_data(request)
+
+    maps = [m.to_dict() for m in view_data['maps']]
+
+    return maps
+
+
+def _map_info_data(request):
+    map_id = request.matchdict['id']
+
+    try: 
+        leaderboard_lifetime = int(
+                request.registry.settings['xonstat.leaderboard_lifetime'])
+    except:
+        leaderboard_lifetime = 30
+
+    leaderboard_count = 10
+    recent_games_count = 20
+
+    try:
+        gmap = DBSession.query(Map).filter_by(map_id=map_id).one()
+
+        # recent games on this map
+        recent_games = DBSession.query(Game, Server, Map, PlayerGameStat).\
+            filter(Game.server_id==Server.server_id).\
+            filter(Game.map_id==Map.map_id).\
+            filter(Game.map_id==map_id).\
+            filter(PlayerGameStat.game_id==Game.game_id).\
+            filter(PlayerGameStat.rank==1).\
+            order_by(expr.desc(Game.start_dt)).all()[0:recent_games_count]
+
+        # top players by score
+        top_scorers = DBSession.query(Player.player_id, Player.nick,
+                func.sum(PlayerGameStat.score)).\
+                filter(Player.player_id == PlayerGameStat.player_id).\
+                filter(Game.game_id == PlayerGameStat.game_id).\
+                filter(Game.map_id == map_id).\
+                filter(Player.player_id > 2).\
+                filter(PlayerGameStat.create_dt > 
+                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
+                order_by(expr.desc(func.sum(PlayerGameStat.score))).\
+                group_by(Player.nick).\
+                group_by(Player.player_id).all()[0:leaderboard_count]
+
+        top_scorers = [(player_id, html_colors(nick), score) \
+                for (player_id, nick, score) in top_scorers]
+
+        # top players by playing time
+        top_players = DBSession.query(Player.player_id, Player.nick, 
+                func.sum(PlayerGameStat.alivetime)).\
+                filter(Player.player_id == PlayerGameStat.player_id).\
+                filter(Game.game_id == PlayerGameStat.game_id).\
+                filter(Game.map_id == map_id).\
+                filter(Player.player_id > 2).\
+                filter(PlayerGameStat.create_dt > 
+                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
+                order_by(expr.desc(func.sum(PlayerGameStat.alivetime))).\
+                group_by(Player.nick).\
+                group_by(Player.player_id).all()[0:leaderboard_count]
+
+        top_players = [(player_id, html_colors(nick), score) \
+                for (player_id, nick, score) in top_players]
+
+        # top servers using/playing this map
+        top_servers = DBSession.query(Server.server_id, Server.name, 
+                func.count(Game.game_id)).\
+                filter(Game.server_id == Server.server_id).\
+                filter(Game.map_id == map_id).\
+                filter(Game.create_dt > 
+                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
+                order_by(expr.desc(func.count(Game.game_id))).\
+                group_by(Server.name).\
+                group_by(Server.server_id).all()[0:leaderboard_count]
+
+    except Exception as e:
+        gmap = None
+    return {'gmap':gmap,
+            'recent_games':recent_games,
+            'top_scorers':top_scorers,
+            'top_players':top_players,
+            'top_servers':top_servers,
+            }
+
+
+def map_info(request):
+    """
+    List the information stored about a given map.
+    """
+    mapinfo_data =  _map_info_data(request)
+
+    # FIXME: code clone, should get these from _map_info_data
+    leaderboard_count = 10
+    recent_games_count = 20
+
+    for i in range(recent_games_count-len(mapinfo_data['recent_games'])):
+        mapinfo_data['recent_games'].append(('-', '-', '-', '-'))
+
+    for i in range(leaderboard_count-len(mapinfo_data['top_scorers'])):
+        mapinfo_data['top_scorers'].append(('-', '-', '-'))
+
+    for i in range(leaderboard_count-len(mapinfo_data['top_players'])):
+        mapinfo_data['top_players'].append(('-', '-', '-'))
+
+    for i in range(leaderboard_count-len(mapinfo_data['top_servers'])):
+        mapinfo_data['top_servers'].append(('-', '-', '-'))
+
+    return mapinfo_data
old mode 100755 (executable)
new mode 100644 (file)
index 82acf16..fe322a7
-import datetime\r
-import json\r
-import logging\r
-import re\r
-import sqlalchemy as sa\r
-import sqlalchemy.sql.functions as func\r
-import time\r
-from pyramid.response import Response\r
-from pyramid.url import current_route_url\r
-from sqlalchemy import desc, distinct\r
-from webhelpers.paginate import Page, PageURL\r
-from xonstat.models import *\r
-from xonstat.util import page_url\r
-\r
-log = logging.getLogger(__name__)\r
-\r
-\r
-def _player_index_data(request):\r
-    if request.params.has_key('page'):\r
-        current_page = request.params['page']\r
-    else:\r
-        current_page = 1\r
-\r
-    try:\r
-        player_q = DBSession.query(Player).\\r
-                filter(Player.player_id > 2).\\r
-                filter(Player.active_ind == True).\\r
-                filter(sa.not_(Player.nick.like('Anonymous Player%'))).\\r
-                order_by(Player.player_id.desc())\r
-\r
-        players = Page(player_q, current_page, items_per_page=10, url=page_url)\r
-\r
-    except Exception as e:\r
-        players = None\r
-        raise e\r
-\r
-    return {'players':players\r
-           }\r
-\r
-\r
-def player_index(request):\r
-    """\r
-    Provides a list of all the current players.\r
-    """\r
-    return _player_index_data(request)\r
-\r
-\r
-def _get_games_played(player_id):\r
-    """\r
-    Provides a breakdown by gametype of the games played by player_id.\r
-\r
-    Returns a tuple containing (total_games, games_breakdown), where\r
-    total_games is the absolute number of games played by player_id\r
-    and games_breakdown is an array containing (game_type_cd, # games)\r
-    """\r
-    games_played = DBSession.query(Game.game_type_cd, func.count()).\\r
-            filter(Game.game_id == PlayerGameStat.game_id).\\r
-            filter(PlayerGameStat.player_id == player_id).\\r
-            group_by(Game.game_type_cd).\\r
-            order_by(func.count().desc()).all()\r
-\r
-    total = 0\r
-    for (game_type_cd, games) in games_played:\r
-        total += games\r
-\r
-    return (total, games_played)\r
-\r
-\r
-# TODO: should probably factor the above function into this one such that\r
-# total_stats['ctf_games'] is the count of CTF games and so on...\r
-def _get_total_stats(player_id):\r
-    """\r
-    Provides aggregated stats by player_id.\r
-\r
-    Returns a dict with the keys 'kills', 'deaths', 'alivetime'.\r
-\r
-    kills = how many kills a player has over all games\r
-    deaths = how many deaths a player has over all games\r
-    alivetime = how long a player has played over all games\r
-\r
-    If any of the above are None, they are set to 0.\r
-    """\r
-    total_stats = {}\r
-    (total_stats['kills'], total_stats['deaths'], total_stats['alivetime']) = DBSession.\\r
-            query("total_kills", "total_deaths", "total_alivetime").\\r
-            from_statement(\r
-                "select sum(kills) total_kills, "\r
-                "sum(deaths) total_deaths, "\r
-                "sum(alivetime) total_alivetime "\r
-                "from player_game_stats "\r
-                "where player_id=:player_id"\r
-            ).params(player_id=player_id).one()\r
-\r
-    (total_stats['wins'],) = DBSession.\\r
-            query("total_wins").\\r
-            from_statement(\r
-                "select count(*) total_wins "\r
-                "from games g, player_game_stats pgs "\r
-                "where g.game_id = pgs.game_id "\r
-                "and player_id=:player_id "\r
-                "and (g.winner = pgs.team or pgs.rank = 1)"\r
-            ).params(player_id=player_id).one()\r
-\r
-    for (key,value) in total_stats.items():\r
-        if value == None:\r
-            total_stats[key] = 0\r
-\r
-    return total_stats\r
-\r
-\r
-def get_accuracy_stats(player_id, weapon_cd, games):\r
-    """\r
-    Provides accuracy for weapon_cd by player_id for the past N games.\r
-    """\r
-    # Reaching back 90 days should give us an accurate enough average\r
-    # We then multiply this out for the number of data points (games) to\r
-    # create parameters for a flot graph\r
-    try:\r
-        raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),\r
-                func.sum(PlayerWeaponStat.fired)).\\r
-                filter(PlayerWeaponStat.player_id == player_id).\\r
-                filter(PlayerWeaponStat.weapon_cd == weapon_cd).\\r
-                one()\r
-\r
-        avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)\r
-\r
-        # Determine the raw accuracy (hit, fired) numbers for $games games\r
-        # This is then enumerated to create parameters for a flot graph\r
-        raw_accs = DBSession.query(PlayerWeaponStat.game_id, \r
-            PlayerWeaponStat.hit, PlayerWeaponStat.fired).\\r
-                filter(PlayerWeaponStat.player_id == player_id).\\r
-                filter(PlayerWeaponStat.weapon_cd == weapon_cd).\\r
-                order_by(PlayerWeaponStat.game_id.desc()).\\r
-                limit(games).\\r
-                all()\r
-\r
-        # they come out in opposite order, so flip them in the right direction\r
-        raw_accs.reverse()\r
-\r
-        accs = []\r
-        for i in range(len(raw_accs)):\r
-            accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))\r
-    except:\r
-        accs = []\r
-        avg = 0.0\r
-\r
-    return (avg, accs)\r
-\r
-\r
-def get_damage_stats(player_id, weapon_cd, games):\r
-    """\r
-    Provides damage info for weapon_cd by player_id for the past N games.\r
-    """\r
-    try:\r
-        raw_avg = DBSession.query(func.sum(PlayerWeaponStat.actual),\r
-                func.sum(PlayerWeaponStat.hit)).\\r
-                filter(PlayerWeaponStat.player_id == player_id).\\r
-                filter(PlayerWeaponStat.weapon_cd == weapon_cd).\\r
-                one()\r
-\r
-        avg = round(float(raw_avg[0])/raw_avg[1], 2)\r
-\r
-        # Determine the damage efficiency (hit, fired) numbers for $games games\r
-        # This is then enumerated to create parameters for a flot graph\r
-        raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, \r
-            PlayerWeaponStat.actual, PlayerWeaponStat.hit).\\r
-                filter(PlayerWeaponStat.player_id == player_id).\\r
-                filter(PlayerWeaponStat.weapon_cd == weapon_cd).\\r
-                order_by(PlayerWeaponStat.game_id.desc()).\\r
-                limit(games).\\r
-                all()\r
-\r
-        # they come out in opposite order, so flip them in the right direction\r
-        raw_dmgs.reverse()\r
-\r
-        dmgs = []\r
-        for i in range(len(raw_dmgs)):\r
-            # try to derive, unless we've hit nothing then set to 0!\r
-            try:\r
-                dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)\r
-            except:\r
-                dmg = 0.0\r
-\r
-            dmgs.append((raw_dmgs[i][0], dmg))\r
-    except Exception as e:\r
-        dmgs = []\r
-        avg = 0.0\r
-\r
-    return (avg, dmgs)\r
-\r
-\r
-def _player_info_data(request):\r
-    player_id = int(request.matchdict['id'])\r
-    if player_id <= 2:\r
-        player_id = -1;\r
-\r
-    try:\r
-        player = DBSession.query(Player).filter_by(player_id=player_id).\\r
-                filter(Player.active_ind == True).one()\r
-\r
-        # games played, alivetime, wins, kills, deaths\r
-        total_stats = _get_total_stats(player.player_id)\r
-\r
-        # games breakdown - N games played (X ctf, Y dm) etc\r
-        (total_games, games_breakdown) = _get_games_played(player.player_id)\r
-\r
-\r
-        # friendly display of elo information and preliminary status\r
-        elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\\r
-                filter(PlayerElo.game_type_cd.in_(['ctf','duel','dm'])).\\r
-                order_by(PlayerElo.elo.desc()).all()\r
-\r
-        elos_display = []\r
-        for elo in elos:\r
-            if elo.games > 32:\r
-                str = "{0} ({1})"\r
-            else:\r
-                str = "{0}* ({1})"\r
-\r
-            elos_display.append(str.format(round(elo.elo, 3),\r
-                elo.game_type_cd))\r
-\r
-        # which weapons have been used in the past 90 days\r
-        # and also, used in 5 games or more?\r
-        back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)\r
-        recent_weapons = []\r
-        for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\\r
-                filter(PlayerWeaponStat.player_id == player_id).\\r
-                filter(PlayerWeaponStat.create_dt > back_then).\\r
-                group_by(PlayerWeaponStat.weapon_cd).\\r
-                having(func.count() > 4).\\r
-                all():\r
-                    recent_weapons.append(weapon[0])\r
-\r
-        # recent games table, all data\r
-        recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\\r
-                filter(PlayerGameStat.player_id == player_id).\\r
-                filter(PlayerGameStat.game_id == Game.game_id).\\r
-                filter(Game.server_id == Server.server_id).\\r
-                filter(Game.map_id == Map.map_id).\\r
-                order_by(Game.game_id.desc())[0:10]\r
-\r
-    except Exception as e:\r
-        player = None\r
-        elos_display = None\r
-        total_stats = None\r
-        recent_games = None\r
-        total_games = None\r
-        games_breakdown = None\r
-        recent_weapons = []\r
-\r
-    return {'player':player,\r
-            'elos_display':elos_display,\r
-            'recent_games':recent_games,\r
-            'total_stats':total_stats,\r
-            'total_games':total_games,\r
-            'games_breakdown':games_breakdown,\r
-            'recent_weapons':recent_weapons,\r
-            }\r
-\r
-\r
-def player_info(request):\r
-    """\r
-    Provides detailed information on a specific player\r
-    """\r
-    return _player_info_data(request)\r
-\r
-\r
-def _player_game_index_data(request):\r
-    player_id = request.matchdict['player_id']\r
-\r
-    if request.params.has_key('page'):\r
-        current_page = request.params['page']\r
-    else:\r
-        current_page = 1\r
-\r
-    try:\r
-        games_q = DBSession.query(Game, Server, Map).\\r
-            filter(PlayerGameStat.game_id == Game.game_id).\\r
-            filter(PlayerGameStat.player_id == player_id).\\r
-            filter(Game.server_id == Server.server_id).\\r
-            filter(Game.map_id == Map.map_id).\\r
-            order_by(Game.game_id.desc())\r
-\r
-        games = Page(games_q, current_page, items_per_page=10, url=page_url)\r
-\r
-        pgstats = {}\r
-        for (game, server, map) in games:\r
-            pgstats[game.game_id] = DBSession.query(PlayerGameStat).\\r
-                    filter(PlayerGameStat.game_id == game.game_id).\\r
-                    order_by(PlayerGameStat.rank).\\r
-                    order_by(PlayerGameStat.score).all()\r
-\r
-    except Exception as e:\r
-        player = None\r
-        games = None\r
-\r
-    return {'player_id':player_id,\r
-            'games':games,\r
-            'pgstats':pgstats}\r
-\r
-\r
-def player_game_index(request):\r
-    """\r
-    Provides an index of the games in which a particular\r
-    player was involved. This is ordered by game_id, with\r
-    the most recent game_ids first. Paginated.\r
-    """\r
-    return _player_game_index_data(request)\r
-\r
-\r
-def _player_accuracy_data(request):\r
-    player_id = request.matchdict['id']\r
-    allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']\r
-    weapon_cd = 'nex'\r
-    games = 20\r
-\r
-    if request.params.has_key('weapon'):\r
-        if request.params['weapon'] in allowed_weapons:\r
-            weapon_cd = request.params['weapon']\r
-\r
-    if request.params.has_key('games'):\r
-        try:\r
-            games = request.params['games']\r
-\r
-            if games < 0:\r
-                games = 20\r
-            if games > 50:\r
-                games = 50\r
-        except:\r
-            games = 20\r
-\r
-    (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)\r
-\r
-    # if we don't have enough data for the given weapon\r
-    if len(accs) < games:\r
-        games = len(accs)\r
-\r
-    return {\r
-            'player_id':player_id, \r
-            'player_url':request.route_url('player_info', id=player_id), \r
-            'weapon':weapon_cd, \r
-            'games':games, \r
-            'avg':avg, \r
-            'accs':accs\r
-            }\r
-\r
-\r
-def player_accuracy_json(request):\r
-    """\r
-    Provides a JSON response representing the accuracy for the given weapon.\r
-\r
-    Parameters:\r
-       weapon = which weapon to display accuracy for. Valid values are 'nex',\r
-                'shotgun', 'uzi', and 'minstanex'.\r
-       games = over how many games to display accuracy. Can be up to 50.\r
-    """\r
-    return _player_accuracy_data(request)\r
-\r
-\r
-def _player_damage_data(request):\r
-    player_id = request.matchdict['id']\r
-    allowed_weapons = ['grenadelauncher', 'electro', 'crylink', 'hagar',\r
-            'rocketlauncher', 'laser']\r
-    weapon_cd = 'laser'\r
-    games = 20\r
-\r
-    if request.params.has_key('weapon'):\r
-        if request.params['weapon'] in allowed_weapons:\r
-            weapon_cd = request.params['weapon']\r
-\r
-    if request.params.has_key('games'):\r
-        try:\r
-            games = request.params['games']\r
-\r
-            if games < 0:\r
-                games = 20\r
-            if games > 50:\r
-                games = 50\r
-        except:\r
-            games = 20\r
-\r
-    (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)\r
-\r
-    # if we don't have enough data for the given weapon\r
-    if len(dmgs) < games:\r
-        games = len(dmgs)\r
-\r
-    return {\r
-            'player_id':player_id, \r
-            'player_url':request.route_url('player_info', id=player_id), \r
-            'weapon':weapon_cd, \r
-            'games':games, \r
-            'avg':avg, \r
-            'dmgs':dmgs\r
-            }\r
-\r
-\r
-def player_damage_json(request):\r
-    """\r
-    Provides a JSON response representing the damage for the given weapon.\r
-\r
-    Parameters:\r
-       weapon = which weapon to display damage for. Valid values are\r
-         'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',\r
-         'laser'.\r
-       games = over how many games to display damage. Can be up to 50.\r
-    """\r
-    return _player_damage_data(request)\r
+import datetime
+import json
+import logging
+import re
+import sqlalchemy as sa
+import sqlalchemy.sql.functions as func
+import time
+from pyramid.response import Response
+from pyramid.url import current_route_url
+from sqlalchemy import desc, distinct
+from webhelpers.paginate import Page, PageURL
+from xonstat.models import *
+from xonstat.util import page_url
+
+log = logging.getLogger(__name__)
+
+
+def _player_index_data(request):
+    if request.params.has_key('page'):
+        current_page = request.params['page']
+    else:
+        current_page = 1
+
+    try:
+        player_q = DBSession.query(Player).\
+                filter(Player.player_id > 2).\
+                filter(Player.active_ind == True).\
+                filter(sa.not_(Player.nick.like('Anonymous Player%'))).\
+                order_by(Player.player_id.desc())
+
+        players = Page(player_q, current_page, items_per_page=10, url=page_url)
+
+    except Exception as e:
+        players = None
+        raise e
+
+    return {'players':players
+           }
+
+
+def player_index(request):
+    """
+    Provides a list of all the current players.
+    """
+    return _player_index_data(request)
+
+
+def _get_games_played(player_id):
+    """
+    Provides a breakdown by gametype of the games played by player_id.
+
+    Returns a tuple containing (total_games, games_breakdown), where
+    total_games is the absolute number of games played by player_id
+    and games_breakdown is an array containing (game_type_cd, # games)
+    """
+    games_played = DBSession.query(Game.game_type_cd, func.count()).\
+            filter(Game.game_id == PlayerGameStat.game_id).\
+            filter(PlayerGameStat.player_id == player_id).\
+            group_by(Game.game_type_cd).\
+            order_by(func.count().desc()).all()
+
+    total = 0
+    for (game_type_cd, games) in games_played:
+        total += games
+
+    return (total, games_played)
+
+
+# TODO: should probably factor the above function into this one such that
+# total_stats['ctf_games'] is the count of CTF games and so on...
+def _get_total_stats(player_id):
+    """
+    Provides aggregated stats by player_id.
+
+    Returns a dict with the keys 'kills', 'deaths', 'alivetime'.
+
+    kills = how many kills a player has over all games
+    deaths = how many deaths a player has over all games
+    alivetime = how long a player has played over all games
+
+    If any of the above are None, they are set to 0.
+    """
+    total_stats = {}
+    (total_stats['kills'], total_stats['deaths'], total_stats['alivetime']) = DBSession.\
+            query("total_kills", "total_deaths", "total_alivetime").\
+            from_statement(
+                "select sum(kills) total_kills, "
+                "sum(deaths) total_deaths, "
+                "sum(alivetime) total_alivetime "
+                "from player_game_stats "
+                "where player_id=:player_id"
+            ).params(player_id=player_id).one()
+
+    (total_stats['wins'],) = DBSession.\
+            query("total_wins").\
+            from_statement(
+                "select count(*) total_wins "
+                "from games g, player_game_stats pgs "
+                "where g.game_id = pgs.game_id "
+                "and player_id=:player_id "
+                "and (g.winner = pgs.team or pgs.rank = 1)"
+            ).params(player_id=player_id).one()
+
+    for (key,value) in total_stats.items():
+        if value == None:
+            total_stats[key] = 0
+
+    return total_stats
+
+
+def get_accuracy_stats(player_id, weapon_cd, games):
+    """
+    Provides accuracy for weapon_cd by player_id for the past N games.
+    """
+    # Reaching back 90 days should give us an accurate enough average
+    # We then multiply this out for the number of data points (games) to
+    # create parameters for a flot graph
+    try:
+        raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),
+                func.sum(PlayerWeaponStat.fired)).\
+                filter(PlayerWeaponStat.player_id == player_id).\
+                filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
+                one()
+
+        avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)
+
+        # Determine the raw accuracy (hit, fired) numbers for $games games
+        # This is then enumerated to create parameters for a flot graph
+        raw_accs = DBSession.query(PlayerWeaponStat.game_id, 
+            PlayerWeaponStat.hit, PlayerWeaponStat.fired).\
+                filter(PlayerWeaponStat.player_id == player_id).\
+                filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
+                order_by(PlayerWeaponStat.game_id.desc()).\
+                limit(games).\
+                all()
+
+        # they come out in opposite order, so flip them in the right direction
+        raw_accs.reverse()
+
+        accs = []
+        for i in range(len(raw_accs)):
+            accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))
+    except:
+        accs = []
+        avg = 0.0
+
+    return (avg, accs)
+
+
+def get_damage_stats(player_id, weapon_cd, games):
+    """
+    Provides damage info for weapon_cd by player_id for the past N games.
+    """
+    try:
+        raw_avg = DBSession.query(func.sum(PlayerWeaponStat.actual),
+                func.sum(PlayerWeaponStat.hit)).\
+                filter(PlayerWeaponStat.player_id == player_id).\
+                filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
+                one()
+
+        avg = round(float(raw_avg[0])/raw_avg[1], 2)
+
+        # Determine the damage efficiency (hit, fired) numbers for $games games
+        # This is then enumerated to create parameters for a flot graph
+        raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, 
+            PlayerWeaponStat.actual, PlayerWeaponStat.hit).\
+                filter(PlayerWeaponStat.player_id == player_id).\
+                filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
+                order_by(PlayerWeaponStat.game_id.desc()).\
+                limit(games).\
+                all()
+
+        # they come out in opposite order, so flip them in the right direction
+        raw_dmgs.reverse()
+
+        dmgs = []
+        for i in range(len(raw_dmgs)):
+            # try to derive, unless we've hit nothing then set to 0!
+            try:
+                dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)
+            except:
+                dmg = 0.0
+
+            dmgs.append((raw_dmgs[i][0], dmg))
+    except Exception as e:
+        dmgs = []
+        avg = 0.0
+
+    return (avg, dmgs)
+
+
+def _player_info_data(request):
+    player_id = int(request.matchdict['id'])
+    if player_id <= 2:
+        player_id = -1;
+
+    try:
+        player = DBSession.query(Player).filter_by(player_id=player_id).\
+                filter(Player.active_ind == True).one()
+
+        # games played, alivetime, wins, kills, deaths
+        total_stats = _get_total_stats(player.player_id)
+
+        # games breakdown - N games played (X ctf, Y dm) etc
+        (total_games, games_breakdown) = _get_games_played(player.player_id)
+
+
+        # friendly display of elo information and preliminary status
+        elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\
+                filter(PlayerElo.game_type_cd.in_(['ctf','duel','dm'])).\
+                order_by(PlayerElo.elo.desc()).all()
+
+        elos_display = []
+        for elo in elos:
+            if elo.games > 32:
+                str = "{0} ({1})"
+            else:
+                str = "{0}* ({1})"
+
+            elos_display.append(str.format(round(elo.elo, 3),
+                elo.game_type_cd))
+
+        # which weapons have been used in the past 90 days
+        # and also, used in 5 games or more?
+        back_then = datetime.datetime.utcnow() - datetime.timedelta(days=90)
+        recent_weapons = []
+        for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\
+                filter(PlayerWeaponStat.player_id == player_id).\
+                filter(PlayerWeaponStat.create_dt > back_then).\
+                group_by(PlayerWeaponStat.weapon_cd).\
+                having(func.count() > 4).\
+                all():
+                    recent_weapons.append(weapon[0])
+
+        # recent games table, all data
+        recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\
+                filter(PlayerGameStat.player_id == player_id).\
+                filter(PlayerGameStat.game_id == Game.game_id).\
+                filter(Game.server_id == Server.server_id).\
+                filter(Game.map_id == Map.map_id).\
+                order_by(Game.game_id.desc())[0:10]
+
+    except Exception as e:
+        player = None
+        elos_display = None
+        total_stats = None
+        recent_games = None
+        total_games = None
+        games_breakdown = None
+        recent_weapons = []
+
+    return {'player':player,
+            'elos_display':elos_display,
+            'recent_games':recent_games,
+            'total_stats':total_stats,
+            'total_games':total_games,
+            'games_breakdown':games_breakdown,
+            'recent_weapons':recent_weapons,
+            }
+
+
+def player_info(request):
+    """
+    Provides detailed information on a specific player
+    """
+    return _player_info_data(request)
+
+
+def _player_game_index_data(request):
+    player_id = request.matchdict['player_id']
+
+    if request.params.has_key('page'):
+        current_page = request.params['page']
+    else:
+        current_page = 1
+
+    try:
+        games_q = DBSession.query(Game, Server, Map).\
+            filter(PlayerGameStat.game_id == Game.game_id).\
+            filter(PlayerGameStat.player_id == player_id).\
+            filter(Game.server_id == Server.server_id).\
+            filter(Game.map_id == Map.map_id).\
+            order_by(Game.game_id.desc())
+
+        games = Page(games_q, current_page, items_per_page=10, url=page_url)
+
+        pgstats = {}
+        for (game, server, map) in games:
+            pgstats[game.game_id] = DBSession.query(PlayerGameStat).\
+                    filter(PlayerGameStat.game_id == game.game_id).\
+                    order_by(PlayerGameStat.rank).\
+                    order_by(PlayerGameStat.score).all()
+
+    except Exception as e:
+        player = None
+        games = None
+
+    return {'player_id':player_id,
+            'games':games,
+            'pgstats':pgstats}
+
+
+def player_game_index(request):
+    """
+    Provides an index of the games in which a particular
+    player was involved. This is ordered by game_id, with
+    the most recent game_ids first. Paginated.
+    """
+    return _player_game_index_data(request)
+
+
+def _player_accuracy_data(request):
+    player_id = request.matchdict['id']
+    allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']
+    weapon_cd = 'nex'
+    games = 20
+
+    if request.params.has_key('weapon'):
+        if request.params['weapon'] in allowed_weapons:
+            weapon_cd = request.params['weapon']
+
+    if request.params.has_key('games'):
+        try:
+            games = request.params['games']
+
+            if games < 0:
+                games = 20
+            if games > 50:
+                games = 50
+        except:
+            games = 20
+
+    (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)
+
+    # if we don't have enough data for the given weapon
+    if len(accs) < games:
+        games = len(accs)
+
+    return {
+            'player_id':player_id, 
+            'player_url':request.route_url('player_info', id=player_id), 
+            'weapon':weapon_cd, 
+            'games':games, 
+            'avg':avg, 
+            'accs':accs
+            }
+
+
+def player_accuracy_json(request):
+    """
+    Provides a JSON response representing the accuracy for the given weapon.
+
+    Parameters:
+       weapon = which weapon to display accuracy for. Valid values are 'nex',
+                'shotgun', 'uzi', and 'minstanex'.
+       games = over how many games to display accuracy. Can be up to 50.
+    """
+    return _player_accuracy_data(request)
+
+
+def _player_damage_data(request):
+    player_id = request.matchdict['id']
+    allowed_weapons = ['grenadelauncher', 'electro', 'crylink', 'hagar',
+            'rocketlauncher', 'laser']
+    weapon_cd = 'laser'
+    games = 20
+
+    if request.params.has_key('weapon'):
+        if request.params['weapon'] in allowed_weapons:
+            weapon_cd = request.params['weapon']
+
+    if request.params.has_key('games'):
+        try:
+            games = request.params['games']
+
+            if games < 0:
+                games = 20
+            if games > 50:
+                games = 50
+        except:
+            games = 20
+
+    (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)
+
+    # if we don't have enough data for the given weapon
+    if len(dmgs) < games:
+        games = len(dmgs)
+
+    return {
+            'player_id':player_id, 
+            'player_url':request.route_url('player_info', id=player_id), 
+            'weapon':weapon_cd, 
+            'games':games, 
+            'avg':avg, 
+            'dmgs':dmgs
+            }
+
+
+def player_damage_json(request):
+    """
+    Provides a JSON response representing the damage for the given weapon.
+
+    Parameters:
+       weapon = which weapon to display damage for. Valid values are
+         'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',
+         'laser'.
+       games = over how many games to display damage. Can be up to 50.
+    """
+    return _player_damage_data(request)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 575d1b0..93e5fec
-import logging\r
-import sqlalchemy.sql.functions as func\r
-import sqlalchemy.sql.expression as expr\r
-import time\r
-from datetime import datetime, timedelta\r
-from pyramid.response import Response\r
-from sqlalchemy import desc\r
-from webhelpers.paginate import Page, PageURL\r
-from xonstat.models import *\r
-from xonstat.util import page_url, html_colors\r
-\r
-log = logging.getLogger(__name__)\r
-\r
-def _server_index_data(request):\r
-    if request.params.has_key('page'):\r
-        current_page = request.params['page']\r
-    else:\r
-        current_page = 1\r
-\r
-    try:\r
-        server_q = DBSession.query(Server).\\r
-                order_by(Server.server_id.desc())\r
-\r
-        servers = Page(server_q, current_page, items_per_page=10, url=page_url)\r
-\r
-        \r
-    except Exception as e:\r
-        servers = None\r
-\r
-    return {'servers':servers, }\r
-\r
-\r
-def server_index(request):\r
-    """\r
-    Provides a list of all the current servers.\r
-    """\r
-    return _server_index_data(request)\r
-\r
-\r
-def _server_info_data(request):\r
-    server_id = request.matchdict['id']\r
-\r
-    try: \r
-        leaderboard_lifetime = int(\r
-                request.registry.settings['xonstat.leaderboard_lifetime'])\r
-    except:\r
-        leaderboard_lifetime = 30\r
-\r
-    leaderboard_count = 10\r
-    recent_games_count = 20\r
-\r
-    try:\r
-        server = DBSession.query(Server).filter_by(server_id=server_id).one()\r
-\r
-        # top maps by total times played\r
-        top_maps = DBSession.query(Game.map_id, Map.name, \r
-                func.count()).\\r
-                filter(Map.map_id==Game.map_id).\\r
-                filter(Game.server_id==server.server_id).\\r
-                filter(Game.create_dt > \r
-                    (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\\r
-                order_by(expr.desc(func.count())).\\r
-                group_by(Game.map_id).\\r
-                group_by(Map.name).all()[0:leaderboard_count]\r
-\r
-        # top players by score\r
-        top_scorers = DBSession.query(Player.player_id, Player.nick, \r
-                func.sum(PlayerGameStat.score)).\\r
-                filter(Player.player_id == PlayerGameStat.player_id).\\r
-                filter(Game.game_id == PlayerGameStat.game_id).\\r
-                filter(Game.server_id == server.server_id).\\r
-                filter(Player.player_id > 2).\\r
-                filter(PlayerGameStat.create_dt > \r
-                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\\r
-                order_by(expr.desc(func.sum(PlayerGameStat.score))).\\r
-                group_by(Player.nick).\\r
-                group_by(Player.player_id).all()[0:leaderboard_count]\r
-\r
-        top_scorers = [(player_id, html_colors(nick), score) \\r
-                for (player_id, nick, score) in top_scorers]\r
-\r
-        # top players by playing time\r
-        top_players = DBSession.query(Player.player_id, Player.nick, \r
-                func.sum(PlayerGameStat.alivetime)).\\r
-                filter(Player.player_id == PlayerGameStat.player_id).\\r
-                filter(Game.game_id == PlayerGameStat.game_id).\\r
-                filter(Game.server_id == server.server_id).\\r
-                filter(Player.player_id > 2).\\r
-                filter(PlayerGameStat.create_dt > \r
-                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\\r
-                order_by(expr.desc(func.sum(PlayerGameStat.alivetime))).\\r
-                group_by(Player.nick).\\r
-                group_by(Player.player_id).all()[0:leaderboard_count]\r
-\r
-        top_players = [(player_id, html_colors(nick), score) \\r
-                for (player_id, nick, score) in top_players]\r
-\r
-        # recent games played in descending order\r
-        recent_games = DBSession.query(Game, Server, Map, PlayerGameStat).\\r
-            filter(Game.server_id==Server.server_id).\\r
-            filter(Game.map_id==Map.map_id).\\r
-            filter(PlayerGameStat.game_id==Game.game_id).\\r
-            filter(PlayerGameStat.rank==1).\\r
-            filter(Server.server_id==server.server_id).\\r
-            order_by(expr.desc(Game.start_dt)).all()[0:recent_games_count]\r
-\r
-    except Exception as e:\r
-        server = None\r
-        recent_games = None\r
-        top_players = None\r
-        raise e\r
-    return {'server':server,\r
-            'recent_games':recent_games,\r
-            'top_players': top_players,\r
-            'top_scorers': top_scorers,\r
-            'top_maps': top_maps,\r
-            }\r
-\r
-\r
-def server_info(request):\r
-    """\r
-    List the stored information about a given server.\r
-    """\r
-    serverinfo_data =  _server_info_data(request)\r
-\r
-    # FIXME: code clone, should get these from _server_info_data\r
-    leaderboard_count = 10\r
-    recent_games_count = 20\r
-\r
-    for i in range(leaderboard_count-len(serverinfo_data['top_maps'])):\r
-        serverinfo_data['top_maps'].append(('-', '-', '-'))\r
-\r
-    for i in range(leaderboard_count-len(serverinfo_data['top_scorers'])):\r
-        serverinfo_data['top_scorers'].append(('-', '-', '-'))\r
-\r
-    for i in range(leaderboard_count-len(serverinfo_data['top_players'])):\r
-        serverinfo_data['top_players'].append(('-', '-', '-'))\r
-\r
-    for i in range(recent_games_count-len(serverinfo_data['recent_games'])):\r
-        serverinfo_data['recent_games'].append(('-', '-', '-', '-'))\r
-\r
-    return serverinfo_data\r
-\r
-\r
-def _server_game_index_data(request):\r
-    server_id = request.matchdict['server_id']\r
-    current_page = request.matchdict['page']\r
-\r
-    try:\r
-        server = DBSession.query(Server).filter_by(server_id=server_id).one()\r
-\r
-        games_q = DBSession.query(Game, Server, Map).\\r
-                filter(Game.server_id == server_id).\\r
-                filter(Game.server_id == Server.server_id).\\r
-                filter(Game.map_id == Map.map_id).\\r
-                order_by(Game.game_id.desc())\r
-\r
-        games = Page(games_q, current_page, url=page_url)\r
-    except Exception as e:\r
-        server = None\r
-        games = None\r
-        raise e\r
-\r
-    return {'games':games,\r
-            'server':server}\r
-\r
-\r
-def server_game_index(request):\r
-    """\r
-    List the games played on a given server. Paginated.\r
-    """\r
-    return _server_game_index_data(request)\r
+import logging
+import sqlalchemy.sql.functions as func
+import sqlalchemy.sql.expression as expr
+import time
+from datetime import datetime, timedelta
+from pyramid.response import Response
+from sqlalchemy import desc
+from webhelpers.paginate import Page, PageURL
+from xonstat.models import *
+from xonstat.util import page_url, html_colors
+
+log = logging.getLogger(__name__)
+
+def _server_index_data(request):
+    if request.params.has_key('page'):
+        current_page = request.params['page']
+    else:
+        current_page = 1
+
+    try:
+        server_q = DBSession.query(Server).\
+                order_by(Server.server_id.desc())
+
+        servers = Page(server_q, current_page, items_per_page=10, url=page_url)
+
+        
+    except Exception as e:
+        servers = None
+
+    return {'servers':servers, }
+
+
+def server_index(request):
+    """
+    Provides a list of all the current servers.
+    """
+    return _server_index_data(request)
+
+
+def _server_info_data(request):
+    server_id = request.matchdict['id']
+
+    try: 
+        leaderboard_lifetime = int(
+                request.registry.settings['xonstat.leaderboard_lifetime'])
+    except:
+        leaderboard_lifetime = 30
+
+    leaderboard_count = 10
+    recent_games_count = 20
+
+    try:
+        server = DBSession.query(Server).filter_by(server_id=server_id).one()
+
+        # top maps by total times played
+        top_maps = DBSession.query(Game.map_id, Map.name, 
+                func.count()).\
+                filter(Map.map_id==Game.map_id).\
+                filter(Game.server_id==server.server_id).\
+                filter(Game.create_dt > 
+                    (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
+                order_by(expr.desc(func.count())).\
+                group_by(Game.map_id).\
+                group_by(Map.name).all()[0:leaderboard_count]
+
+        # top players by score
+        top_scorers = DBSession.query(Player.player_id, Player.nick, 
+                func.sum(PlayerGameStat.score)).\
+                filter(Player.player_id == PlayerGameStat.player_id).\
+                filter(Game.game_id == PlayerGameStat.game_id).\
+                filter(Game.server_id == server.server_id).\
+                filter(Player.player_id > 2).\
+                filter(PlayerGameStat.create_dt > 
+                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
+                order_by(expr.desc(func.sum(PlayerGameStat.score))).\
+                group_by(Player.nick).\
+                group_by(Player.player_id).all()[0:leaderboard_count]
+
+        top_scorers = [(player_id, html_colors(nick), score) \
+                for (player_id, nick, score) in top_scorers]
+
+        # top players by playing time
+        top_players = DBSession.query(Player.player_id, Player.nick, 
+                func.sum(PlayerGameStat.alivetime)).\
+                filter(Player.player_id == PlayerGameStat.player_id).\
+                filter(Game.game_id == PlayerGameStat.game_id).\
+                filter(Game.server_id == server.server_id).\
+                filter(Player.player_id > 2).\
+                filter(PlayerGameStat.create_dt > 
+                        (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
+                order_by(expr.desc(func.sum(PlayerGameStat.alivetime))).\
+                group_by(Player.nick).\
+                group_by(Player.player_id).all()[0:leaderboard_count]
+
+        top_players = [(player_id, html_colors(nick), score) \
+                for (player_id, nick, score) in top_players]
+
+        # recent games played in descending order
+        recent_games = DBSession.query(Game, Server, Map, PlayerGameStat).\
+            filter(Game.server_id==Server.server_id).\
+            filter(Game.map_id==Map.map_id).\
+            filter(PlayerGameStat.game_id==Game.game_id).\
+            filter(PlayerGameStat.rank==1).\
+            filter(Server.server_id==server.server_id).\
+            order_by(expr.desc(Game.start_dt)).all()[0:recent_games_count]
+
+    except Exception as e:
+        server = None
+        recent_games = None
+        top_players = None
+        raise e
+    return {'server':server,
+            'recent_games':recent_games,
+            'top_players': top_players,
+            'top_scorers': top_scorers,
+            'top_maps': top_maps,
+            }
+
+
+def server_info(request):
+    """
+    List the stored information about a given server.
+    """
+    serverinfo_data =  _server_info_data(request)
+
+    # FIXME: code clone, should get these from _server_info_data
+    leaderboard_count = 10
+    recent_games_count = 20
+
+    for i in range(leaderboard_count-len(serverinfo_data['top_maps'])):
+        serverinfo_data['top_maps'].append(('-', '-', '-'))
+
+    for i in range(leaderboard_count-len(serverinfo_data['top_scorers'])):
+        serverinfo_data['top_scorers'].append(('-', '-', '-'))
+
+    for i in range(leaderboard_count-len(serverinfo_data['top_players'])):
+        serverinfo_data['top_players'].append(('-', '-', '-'))
+
+    for i in range(recent_games_count-len(serverinfo_data['recent_games'])):
+        serverinfo_data['recent_games'].append(('-', '-', '-', '-'))
+
+    return serverinfo_data
+
+
+def _server_game_index_data(request):
+    server_id = request.matchdict['server_id']
+    current_page = request.matchdict['page']
+
+    try:
+        server = DBSession.query(Server).filter_by(server_id=server_id).one()
+
+        games_q = DBSession.query(Game, Server, Map).\
+                filter(Game.server_id == server_id).\
+                filter(Game.server_id == Server.server_id).\
+                filter(Game.map_id == Map.map_id).\
+                order_by(Game.game_id.desc())
+
+        games = Page(games_q, current_page, url=page_url)
+    except Exception as e:
+        server = None
+        games = None
+        raise e
+
+    return {'games':games,
+            'server':server}
+
+
+def server_game_index(request):
+    """
+    List the games played on a given server. Paginated.
+    """
+    return _server_game_index_data(request)
old mode 100755 (executable)
new mode 100644 (file)
index e841e0d..68610cf
-import datetime\r
-import logging\r
-import os\r
-import pyramid.httpexceptions\r
-import re\r
-import time\r
-from pyramid.response import Response\r
-from sqlalchemy import Sequence\r
-from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\r
-from xonstat.d0_blind_id import d0_blind_id_verify\r
-from xonstat.models import *\r
-from xonstat.util import strip_colors, qfont_decode\r
-\r
-log = logging.getLogger(__name__)\r
-\r
-\r
-def is_blank_game(players):\r
-    """Determine if this is a blank game or not. A blank game is either:\r
-\r
-    1) a match that ended in the warmup stage, where accuracy events are not\r
-    present\r
-\r
-    2) a match in which no player made a positive or negative score AND was\r
-    on the scoreboard\r
-    """\r
-    r = re.compile(r'acc-.*-cnt-fired')\r
-    flg_nonzero_score = False\r
-    flg_acc_events = False\r
-\r
-    for events in players:\r
-        if is_real_player(events):\r
-            for (key,value) in events.items():\r
-                if key == 'scoreboard-score' and value != '0':\r
-                    flg_nonzero_score = True\r
-                if r.search(key):\r
-                    flg_acc_events = True\r
-\r
-    return not (flg_nonzero_score and flg_acc_events)\r
-\r
-def get_remote_addr(request):\r
-    """Get the Xonotic server's IP address"""\r
-    if 'X-Forwarded-For' in request.headers:\r
-        return request.headers['X-Forwarded-For']\r
-    else:\r
-        return request.remote_addr\r
-\r
-\r
-def is_supported_gametype(gametype):\r
-    """Whether a gametype is supported or not"""\r
-    flg_supported = True\r
-\r
-    if gametype == 'cts' or gametype == 'lms':\r
-        flg_supported = False\r
-\r
-    return flg_supported\r
-\r
-\r
-def verify_request(request):\r
-    try:\r
-        (idfp, status) = d0_blind_id_verify(\r
-                sig=request.headers['X-D0-Blind-Id-Detached-Signature'],\r
-                querystring='',\r
-                postdata=request.body)\r
-\r
-        log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))\r
-    except: \r
-        idfp = None\r
-        status = None\r
-\r
-    return (idfp, status)\r
-\r
-\r
-def num_real_players(player_events, count_bots=False):\r
-    """\r
-    Returns the number of real players (those who played \r
-    and are on the scoreboard).\r
-    """\r
-    real_players = 0\r
-\r
-    for events in player_events:\r
-        if is_real_player(events, count_bots):\r
-            real_players += 1\r
-\r
-    return real_players\r
-\r
-\r
-def has_minimum_real_players(settings, player_events):\r
-    """\r
-    Determines if the collection of player events has enough "real" players\r
-    to store in the database. The minimum setting comes from the config file\r
-    under the setting xonstat.minimum_real_players.\r
-    """\r
-    flg_has_min_real_players = True\r
-\r
-    try:\r
-        minimum_required_players = int(\r
-                settings['xonstat.minimum_required_players'])\r
-    except:\r
-        minimum_required_players = 2\r
-\r
-    real_players = num_real_players(player_events)\r
-\r
-    #TODO: put this into a config setting in the ini file?\r
-    if real_players < minimum_required_players:\r
-        flg_has_min_real_players = False\r
-\r
-    return flg_has_min_real_players\r
-\r
-\r
-def has_required_metadata(metadata):\r
-    """\r
-    Determines if a give set of metadata has enough data to create a game,\r
-    server, and map with.\r
-    """\r
-    flg_has_req_metadata = True\r
-\r
-    if 'T' not in metadata or\\r
-        'G' not in metadata or\\r
-        'M' not in metadata or\\r
-        'I' not in metadata or\\r
-        'S' not in metadata:\r
-            flg_has_req_metadata = False\r
-\r
-    return flg_has_req_metadata\r
-\r
-\r
-def is_real_player(events, count_bots=False):\r
-    """\r
-    Determines if a given set of player events correspond with a player who\r
-\r
-    1) is not a bot (P event does not look like a bot)\r
-    2) played in the game (matches 1)\r
-    3) was present at the end of the game (scoreboardvalid 1)\r
-\r
-    Returns True if the player meets the above conditions, and false otherwise.\r
-    """\r
-    flg_is_real = False\r
-\r
-    # removing 'joins' here due to bug, but it should be here\r
-    if 'matches' in events and 'scoreboardvalid' in events:\r
-        if (events['P'].startswith('bot') and count_bots) or \\r
-            not events['P'].startswith('bot'):\r
-            flg_is_real = True\r
-\r
-    return flg_is_real\r
-\r
-\r
-def register_new_nick(session, player, new_nick):\r
-    """\r
-    Change the player record's nick to the newly found nick. Store the old\r
-    nick in the player_nicks table for that player.\r
-\r
-    session - SQLAlchemy database session factory\r
-    player - player record whose nick is changing\r
-    new_nick - the new nickname\r
-    """\r
-    # see if that nick already exists\r
-    stripped_nick = strip_colors(player.nick)\r
-    try:\r
-        player_nick = session.query(PlayerNick).filter_by(\r
-            player_id=player.player_id, stripped_nick=stripped_nick).one()\r
-    except NoResultFound, e:\r
-        # player_id/stripped_nick not found, create one\r
-        # but we don't store "Anonymous Player #N"\r
-        if not re.search('^Anonymous Player #\d+$', player.nick):\r
-            player_nick = PlayerNick()\r
-            player_nick.player_id = player.player_id\r
-            player_nick.stripped_nick = player.stripped_nick\r
-            player_nick.nick = player.nick\r
-            session.add(player_nick)\r
-\r
-    # We change to the new nick regardless\r
-    player.nick = new_nick\r
-    player.stripped_nick = strip_colors(new_nick)\r
-    session.add(player)\r
-\r
-\r
-def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,\r
-        revision=None):\r
-    """\r
-    Find a server by name or create one if not found. Parameters:\r
-\r
-    session - SQLAlchemy database session factory\r
-    name - server name of the server to be found or created\r
-    hashkey - server hashkey\r
-    """\r
-    try:\r
-        # find one by that name, if it exists\r
-        server = session.query(Server).filter_by(name=name).one()\r
-\r
-        # store new hashkey\r
-        if server.hashkey != hashkey:\r
-            server.hashkey = hashkey\r
-            session.add(server)\r
-\r
-        # store new IP address\r
-        if server.ip_addr != ip_addr:\r
-            server.ip_addr = ip_addr\r
-            session.add(server)\r
-\r
-        # store new revision\r
-        if server.revision != revision:\r
-            server.revision = revision\r
-            session.add(server)\r
-\r
-        log.debug("Found existing server {0}".format(server.server_id))\r
-\r
-    except MultipleResultsFound, e:\r
-        # multiple found, so also filter by hashkey\r
-        server = session.query(Server).filter_by(name=name).\\r
-                filter_by(hashkey=hashkey).one()\r
-        log.debug("Found existing server {0}".format(server.server_id))\r
-\r
-    except NoResultFound, e:\r
-        # not found, create one\r
-        server = Server(name=name, hashkey=hashkey)\r
-        session.add(server)\r
-        session.flush()\r
-        log.debug("Created server {0} with hashkey {1}".format(\r
-            server.server_id, server.hashkey))\r
-\r
-    return server\r
-\r
-\r
-def get_or_create_map(session=None, name=None):\r
-    """\r
-    Find a map by name or create one if not found. Parameters:\r
-\r
-    session - SQLAlchemy database session factory\r
-    name - map name of the map to be found or created\r
-    """\r
-    try:\r
-        # find one by the name, if it exists\r
-        gmap = session.query(Map).filter_by(name=name).one()\r
-        log.debug("Found map id {0}: {1}".format(gmap.map_id, \r
-            gmap.name))\r
-    except NoResultFound, e:\r
-        gmap = Map(name=name)\r
-        session.add(gmap)\r
-        session.flush()\r
-        log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
-            gmap.name))\r
-    except MultipleResultsFound, e:\r
-        # multiple found, so use the first one but warn\r
-        log.debug(e)\r
-        gmaps = session.query(Map).filter_by(name=name).order_by(\r
-                Map.map_id).all()\r
-        gmap = gmaps[0]\r
-        log.debug("Found map id {0}: {1} but found \\r
-                multiple".format(gmap.map_id, gmap.name))\r
-\r
-    return gmap\r
-\r
-\r
-def create_game(session=None, start_dt=None, game_type_cd=None, \r
-        server_id=None, map_id=None, winner=None, match_id=None):\r
-    """\r
-    Creates a game. Parameters:\r
-\r
-    session - SQLAlchemy database session factory\r
-    start_dt - when the game started (datetime object)\r
-    game_type_cd - the game type of the game being played\r
-    server_id - server identifier of the server hosting the game\r
-    map_id - map on which the game was played\r
-    winner - the team id of the team that won\r
-    """\r
-    seq = Sequence('games_game_id_seq')\r
-    game_id = session.execute(seq)\r
-    game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,\r
-                server_id=server_id, map_id=map_id, winner=winner)\r
-    game.match_id = match_id\r
-\r
-    try:\r
-        session.query(Game).filter(Game.server_id==server_id).\\r
-                filter(Game.match_id==match_id).one()\r
-        # if a game under the same server and match_id found, \r
-        # this is a duplicate game and can be ignored\r
-        raise pyramid.httpexceptions.HTTPOk('OK')\r
-    except NoResultFound, e:\r
-        # server_id/match_id combination not found. game is ok to insert\r
-        session.add(game)\r
-        log.debug("Created game id {0} on server {1}, map {2} at \\r
-                {3}".format(game.game_id, \r
-                    server_id, map_id, start_dt))\r
-\r
-    return game\r
-\r
-\r
-def get_or_create_player(session=None, hashkey=None, nick=None):\r
-    """\r
-    Finds a player by hashkey or creates a new one (along with a\r
-    corresponding hashkey entry. Parameters:\r
-\r
-    session - SQLAlchemy database session factory\r
-    hashkey - hashkey of the player to be found or created\r
-    nick - nick of the player (in case of a first time create)\r
-    """\r
-    # if we have a bot\r
-    if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):\r
-        player = session.query(Player).filter_by(player_id=1).one()\r
-    # if we have an untracked player\r
-    elif re.search('^player#\d+$', hashkey):\r
-        player = session.query(Player).filter_by(player_id=2).one()\r
-    # else it is a tracked player\r
-    else:\r
-        # see if the player is already in the database\r
-        # if not, create one and the hashkey along with it\r
-        try:\r
-            hk = session.query(Hashkey).filter_by(\r
-                    hashkey=hashkey).one()\r
-            player = session.query(Player).filter_by(\r
-                    player_id=hk.player_id).one()\r
-            log.debug("Found existing player {0} with hashkey {1}".format(\r
-                player.player_id, hashkey))\r
-        except:\r
-            player = Player()\r
-            session.add(player)\r
-            session.flush()\r
-\r
-            # if nick is given to us, use it. If not, use "Anonymous Player"\r
-            # with a suffix added for uniqueness.\r
-            if nick:\r
-                player.nick = nick[:128]\r
-                player.stripped_nick = strip_colors(nick[:128])\r
-            else:\r
-                player.nick = "Anonymous Player #{0}".format(player.player_id)\r
-                player.stripped_nick = player.nick\r
-\r
-            hk = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
-            session.add(hk)\r
-            log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
-                player.player_id, hashkey, player.nick.encode('utf-8')))\r
-\r
-    return player\r
-\r
-def create_player_game_stat(session=None, player=None, \r
-        game=None, player_events=None):\r
-    """\r
-    Creates game statistics for a given player in a given game. Parameters:\r
-\r
-    session - SQLAlchemy session factory\r
-    player - Player record of the player who owns the stats\r
-    game - Game record for the game to which the stats pertain\r
-    player_events - dictionary for the actual stats that need to be transformed\r
-    """\r
-\r
-    # in here setup default values (e.g. if game type is CTF then\r
-    # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc\r
-    # TODO: use game's create date here instead of now()\r
-    seq = Sequence('player_game_stats_player_game_stat_id_seq')\r
-    pgstat_id = session.execute(seq)\r
-    pgstat = PlayerGameStat(player_game_stat_id=pgstat_id, \r
-            create_dt=datetime.datetime.utcnow())\r
-\r
-    # set player id from player record\r
-    pgstat.player_id = player.player_id\r
-\r
-    #set game id from game record\r
-    pgstat.game_id = game.game_id\r
-\r
-    # all games have a score\r
-    pgstat.score = 0\r
-\r
-    if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel':\r
-        pgstat.kills = 0\r
-        pgstat.deaths = 0\r
-        pgstat.suicides = 0\r
-    elif game.game_type_cd == 'ctf':\r
-        pgstat.kills = 0\r
-        pgstat.captures = 0\r
-        pgstat.pickups = 0\r
-        pgstat.drops = 0\r
-        pgstat.returns = 0\r
-        pgstat.carrier_frags = 0\r
-\r
-    for (key,value) in player_events.items():\r
-        if key == 'n': pgstat.nick = value[:128]\r
-        if key == 't': pgstat.team = value\r
-        if key == 'rank': pgstat.rank = value\r
-        if key == 'alivetime': \r
-            pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))\r
-        if key == 'scoreboard-drops': pgstat.drops = value\r
-        if key == 'scoreboard-returns': pgstat.returns = value\r
-        if key == 'scoreboard-fckills': pgstat.carrier_frags = value\r
-        if key == 'scoreboard-pickups': pgstat.pickups = value\r
-        if key == 'scoreboard-caps': pgstat.captures = value\r
-        if key == 'scoreboard-score': pgstat.score = value\r
-        if key == 'scoreboard-deaths': pgstat.deaths = value\r
-        if key == 'scoreboard-kills': pgstat.kills = value\r
-        if key == 'scoreboard-suicides': pgstat.suicides = value\r
-\r
-    # check to see if we had a name, and if\r
-    # not use an anonymous handle\r
-    if pgstat.nick == None:\r
-        pgstat.nick = "Anonymous Player"\r
-        pgstat.stripped_nick = "Anonymous Player"\r
-\r
-    # otherwise process a nick change\r
-    elif pgstat.nick != player.nick and player.player_id > 2:\r
-        register_new_nick(session, player, pgstat.nick)\r
-\r
-    # if the player is ranked #1 and it is a team game, set the game's winner\r
-    # to be the team of that player\r
-    # FIXME: this is a hack, should be using the 'W' field (not present)\r
-    if pgstat.rank == '1' and pgstat.team:\r
-        game.winner = pgstat.team\r
-        session.add(game)\r
-\r
-    session.add(pgstat)\r
-\r
-    return pgstat\r
-\r
-\r
-def create_player_weapon_stats(session=None, player=None, \r
-        game=None, pgstat=None, player_events=None):\r
-    """\r
-    Creates accuracy records for each weapon used by a given player in a\r
-    given game. Parameters:\r
-\r
-    session - SQLAlchemy session factory object\r
-    player - Player record who owns the weapon stats\r
-    game - Game record in which the stats were created\r
-    pgstat - Corresponding PlayerGameStat record for these weapon stats\r
-    player_events - dictionary containing the raw weapon values that need to be\r
-        transformed\r
-    """\r
-    pwstats = []\r
-\r
-    for (key,value) in player_events.items():\r
-        matched = re.search("acc-(.*?)-cnt-fired", key)\r
-        if matched:\r
-            weapon_cd = matched.group(1)\r
-            seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
-            pwstat_id = session.execute(seq)\r
-            pwstat = PlayerWeaponStat()\r
-            pwstat.player_weapon_stats_id = pwstat_id\r
-            pwstat.player_id = player.player_id\r
-            pwstat.game_id = game.game_id\r
-            pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
-            pwstat.weapon_cd = weapon_cd\r
-\r
-            if 'n' in player_events:\r
-                pwstat.nick = player_events['n']\r
-            else:\r
-                pwstat.nick = player_events['P']\r
-\r
-            if 'acc-' + weapon_cd + '-cnt-fired' in player_events:\r
-                pwstat.fired = int(round(float(\r
-                        player_events['acc-' + weapon_cd + '-cnt-fired'])))\r
-            if 'acc-' + weapon_cd + '-fired' in player_events:\r
-                pwstat.max = int(round(float(\r
-                        player_events['acc-' + weapon_cd + '-fired'])))\r
-            if 'acc-' + weapon_cd + '-cnt-hit' in player_events:\r
-                pwstat.hit = int(round(float(\r
-                        player_events['acc-' + weapon_cd + '-cnt-hit'])))\r
-            if 'acc-' + weapon_cd + '-hit' in player_events:\r
-                pwstat.actual = int(round(float(\r
-                        player_events['acc-' + weapon_cd + '-hit'])))\r
-            if 'acc-' + weapon_cd + '-frags' in player_events:\r
-                pwstat.frags = int(round(float(\r
-                        player_events['acc-' + weapon_cd + '-frags'])))\r
-\r
-            session.add(pwstat)\r
-            pwstats.append(pwstat)\r
-\r
-    return pwstats\r
-\r
-\r
-def parse_body(request):\r
-    """\r
-    Parses the POST request body for a stats submission\r
-    """\r
-    # storage vars for the request body\r
-    game_meta = {}\r
-    player_events = {}\r
-    current_team = None\r
-    players = []\r
-\r
-    for line in request.body.split('\n'):\r
-        try:\r
-            (key, value) = line.strip().split(' ', 1)\r
-\r
-            # Server (S) and Nick (n) fields can have international characters.\r
-            # We convert to UTF-8.\r
-            if key in 'S' 'n':\r
-                value = unicode(value, 'utf-8')\r
-\r
-            if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I':\r
-                game_meta[key] = value\r
-\r
-            if key == 'P':\r
-                # if we were working on a player record already, append\r
-                # it and work on a new one (only set team info)\r
-                if len(player_events) != 0:\r
-                    players.append(player_events)\r
-                    player_events = {}\r
-\r
-                player_events[key] = value\r
-\r
-            if key == 'e':\r
-                (subkey, subvalue) = value.split(' ', 1)\r
-                player_events[subkey] = subvalue\r
-            if key == 'n':\r
-                player_events[key] = value\r
-            if key == 't':\r
-                player_events[key] = value\r
-        except:\r
-            # no key/value pair - move on to the next line\r
-            pass\r
-\r
-    # add the last player we were working on\r
-    if len(player_events) > 0:\r
-        players.append(player_events)\r
-\r
-    return (game_meta, players)\r
-\r
-\r
-def create_player_stats(session=None, player=None, game=None, \r
-        player_events=None):\r
-    """\r
-    Creates player game and weapon stats according to what type of player\r
-    """\r
-    pgstat = create_player_game_stat(session=session, \r
-        player=player, game=game, player_events=player_events)\r
-\r
-    #TODO: put this into a config setting in the ini file?\r
-    if not re.search('^bot#\d+$', player_events['P']):\r
-        create_player_weapon_stats(session=session, \r
-            player=player, game=game, pgstat=pgstat,\r
-            player_events=player_events)\r
-\r
-\r
-def stats_submit(request):\r
-    """\r
-    Entry handler for POST stats submissions.\r
-    """\r
-    try:\r
-        session = DBSession()\r
-\r
-        log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
-                "----- END REQUEST BODY -----\n\n")\r
-\r
-        (idfp, status) = verify_request(request)\r
-        if not idfp:\r
-            log.debug("ERROR: Unverified request")\r
-            raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")\r
-\r
-        (game_meta, players) = parse_body(request)  \r
-\r
-        if not has_required_metadata(game_meta):\r
-            log.debug("ERROR: Required game meta missing")\r
-            raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")\r
-\r
-        if not is_supported_gametype(game_meta['G']):\r
-            log.debug("ERROR: Unsupported gametype")\r
-            raise pyramid.httpexceptions.HTTPOk("OK")\r
-\r
-        if not has_minimum_real_players(request.registry.settings, players):\r
-            log.debug("ERROR: Not enough real players")\r
-            raise pyramid.httpexceptions.HTTPOk("OK")\r
-\r
-        if is_blank_game(players):\r
-            log.debug("ERROR: Blank game")\r
-            raise pyramid.httpexceptions.HTTPOk("OK")\r
-\r
-        # the "duel" gametype is fake\r
-        if num_real_players(players, count_bots=True) == 2 and \\r
-                game_meta['G'] == 'dm':\r
-            game_meta['G'] = 'duel'\r
-\r
-\r
-        # fix for DTG, who didn't #ifdef WATERMARK to set the revision info\r
-        try:\r
-            revision = game_meta['R']\r
-        except:\r
-            revision = "unknown"\r
-\r
-        server = get_or_create_server(session=session, hashkey=idfp, \r
-                name=game_meta['S'], revision=revision,\r
-                ip_addr=get_remote_addr(request))\r
-\r
-        gmap = get_or_create_map(session=session, name=game_meta['M'])\r
-\r
-        # FIXME: use the gmtime instead of utcnow() when the timezone bug is\r
-        # fixed\r
-        game = create_game(session=session, \r
-                start_dt=datetime.datetime.utcnow(),\r
-                #start_dt=datetime.datetime(\r
-                    #*time.gmtime(float(game_meta['T']))[:6]), \r
-                server_id=server.server_id, game_type_cd=game_meta['G'], \r
-                   map_id=gmap.map_id, match_id=game_meta['I'])\r
-\r
-        # find or create a record for each player\r
-        # and add stats for each if they were present at the end\r
-        # of the game\r
-        for player_events in players:\r
-            if 'n' in player_events:\r
-                nick = player_events['n']\r
-            else:\r
-                nick = None\r
-\r
-            if 'matches' in player_events and 'scoreboardvalid' \\r
-                in player_events:\r
-                player = get_or_create_player(session=session, \r
-                    hashkey=player_events['P'], nick=nick)\r
-                log.debug('Creating stats for %s' % player_events['P'])\r
-                create_player_stats(session=session, player=player, game=game, \r
-                        player_events=player_events)\r
-\r
-        # update elos\r
-        try:\r
-            game.process_elos(session)\r
-        except Exception as e:\r
-            log.debug('Error (non-fatal): elo processing failed.')\r
-\r
-        session.commit()\r
-        log.debug('Success! Stats recorded.')\r
-        return Response('200 OK')\r
-    except Exception as e:\r
-        session.rollback()\r
-        return e\r
+import datetime
+import logging
+import os
+import pyramid.httpexceptions
+import re
+import time
+from pyramid.response import Response
+from sqlalchemy import Sequence
+from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
+from xonstat.d0_blind_id import d0_blind_id_verify
+from xonstat.models import *
+from xonstat.util import strip_colors, qfont_decode
+
+log = logging.getLogger(__name__)
+
+
+def is_blank_game(players):
+    """Determine if this is a blank game or not. A blank game is either:
+
+    1) a match that ended in the warmup stage, where accuracy events are not
+    present
+
+    2) a match in which no player made a positive or negative score AND was
+    on the scoreboard
+    """
+    r = re.compile(r'acc-.*-cnt-fired')
+    flg_nonzero_score = False
+    flg_acc_events = False
+
+    for events in players:
+        if is_real_player(events):
+            for (key,value) in events.items():
+                if key == 'scoreboard-score' and value != '0':
+                    flg_nonzero_score = True
+                if r.search(key):
+                    flg_acc_events = True
+
+    return not (flg_nonzero_score and flg_acc_events)
+
+def get_remote_addr(request):
+    """Get the Xonotic server's IP address"""
+    if 'X-Forwarded-For' in request.headers:
+        return request.headers['X-Forwarded-For']
+    else:
+        return request.remote_addr
+
+
+def is_supported_gametype(gametype):
+    """Whether a gametype is supported or not"""
+    flg_supported = True
+
+    if gametype == 'cts' or gametype == 'lms':
+        flg_supported = False
+
+    return flg_supported
+
+
+def verify_request(request):
+    try:
+        (idfp, status) = d0_blind_id_verify(
+                sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
+                querystring='',
+                postdata=request.body)
+
+        log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
+    except: 
+        idfp = None
+        status = None
+
+    return (idfp, status)
+
+
+def num_real_players(player_events, count_bots=False):
+    """
+    Returns the number of real players (those who played 
+    and are on the scoreboard).
+    """
+    real_players = 0
+
+    for events in player_events:
+        if is_real_player(events, count_bots):
+            real_players += 1
+
+    return real_players
+
+
+def has_minimum_real_players(settings, player_events):
+    """
+    Determines if the collection of player events has enough "real" players
+    to store in the database. The minimum setting comes from the config file
+    under the setting xonstat.minimum_real_players.
+    """
+    flg_has_min_real_players = True
+
+    try:
+        minimum_required_players = int(
+                settings['xonstat.minimum_required_players'])
+    except:
+        minimum_required_players = 2
+
+    real_players = num_real_players(player_events)
+
+    #TODO: put this into a config setting in the ini file?
+    if real_players < minimum_required_players:
+        flg_has_min_real_players = False
+
+    return flg_has_min_real_players
+
+
+def has_required_metadata(metadata):
+    """
+    Determines if a give set of metadata has enough data to create a game,
+    server, and map with.
+    """
+    flg_has_req_metadata = True
+
+    if 'T' not in metadata or\
+        'G' not in metadata or\
+        'M' not in metadata or\
+        'I' not in metadata or\
+        'S' not in metadata:
+            flg_has_req_metadata = False
+
+    return flg_has_req_metadata
+
+
+def is_real_player(events, count_bots=False):
+    """
+    Determines if a given set of player events correspond with a player who
+
+    1) is not a bot (P event does not look like a bot)
+    2) played in the game (matches 1)
+    3) was present at the end of the game (scoreboardvalid 1)
+
+    Returns True if the player meets the above conditions, and false otherwise.
+    """
+    flg_is_real = False
+
+    # removing 'joins' here due to bug, but it should be here
+    if 'matches' in events and 'scoreboardvalid' in events:
+        if (events['P'].startswith('bot') and count_bots) or \
+            not events['P'].startswith('bot'):
+            flg_is_real = True
+
+    return flg_is_real
+
+
+def register_new_nick(session, player, new_nick):
+    """
+    Change the player record's nick to the newly found nick. Store the old
+    nick in the player_nicks table for that player.
+
+    session - SQLAlchemy database session factory
+    player - player record whose nick is changing
+    new_nick - the new nickname
+    """
+    # see if that nick already exists
+    stripped_nick = strip_colors(player.nick)
+    try:
+        player_nick = session.query(PlayerNick).filter_by(
+            player_id=player.player_id, stripped_nick=stripped_nick).one()
+    except NoResultFound, e:
+        # player_id/stripped_nick not found, create one
+        # but we don't store "Anonymous Player #N"
+        if not re.search('^Anonymous Player #\d+$', player.nick):
+            player_nick = PlayerNick()
+            player_nick.player_id = player.player_id
+            player_nick.stripped_nick = player.stripped_nick
+            player_nick.nick = player.nick
+            session.add(player_nick)
+
+    # We change to the new nick regardless
+    player.nick = new_nick
+    player.stripped_nick = strip_colors(new_nick)
+    session.add(player)
+
+
+def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,
+        revision=None):
+    """
+    Find a server by name or create one if not found. Parameters:
+
+    session - SQLAlchemy database session factory
+    name - server name of the server to be found or created
+    hashkey - server hashkey
+    """
+    try:
+        # find one by that name, if it exists
+        server = session.query(Server).filter_by(name=name).one()
+
+        # store new hashkey
+        if server.hashkey != hashkey:
+            server.hashkey = hashkey
+            session.add(server)
+
+        # store new IP address
+        if server.ip_addr != ip_addr:
+            server.ip_addr = ip_addr
+            session.add(server)
+
+        # store new revision
+        if server.revision != revision:
+            server.revision = revision
+            session.add(server)
+
+        log.debug("Found existing server {0}".format(server.server_id))
+
+    except MultipleResultsFound, e:
+        # multiple found, so also filter by hashkey
+        server = session.query(Server).filter_by(name=name).\
+                filter_by(hashkey=hashkey).one()
+        log.debug("Found existing server {0}".format(server.server_id))
+
+    except NoResultFound, e:
+        # not found, create one
+        server = Server(name=name, hashkey=hashkey)
+        session.add(server)
+        session.flush()
+        log.debug("Created server {0} with hashkey {1}".format(
+            server.server_id, server.hashkey))
+
+    return server
+
+
+def get_or_create_map(session=None, name=None):
+    """
+    Find a map by name or create one if not found. Parameters:
+
+    session - SQLAlchemy database session factory
+    name - map name of the map to be found or created
+    """
+    try:
+        # find one by the name, if it exists
+        gmap = session.query(Map).filter_by(name=name).one()
+        log.debug("Found map id {0}: {1}".format(gmap.map_id, 
+            gmap.name))
+    except NoResultFound, e:
+        gmap = Map(name=name)
+        session.add(gmap)
+        session.flush()
+        log.debug("Created map id {0}: {1}".format(gmap.map_id,
+            gmap.name))
+    except MultipleResultsFound, e:
+        # multiple found, so use the first one but warn
+        log.debug(e)
+        gmaps = session.query(Map).filter_by(name=name).order_by(
+                Map.map_id).all()
+        gmap = gmaps[0]
+        log.debug("Found map id {0}: {1} but found \
+                multiple".format(gmap.map_id, gmap.name))
+
+    return gmap
+
+
+def create_game(session=None, start_dt=None, game_type_cd=None, 
+        server_id=None, map_id=None, winner=None, match_id=None):
+    """
+    Creates a game. Parameters:
+
+    session - SQLAlchemy database session factory
+    start_dt - when the game started (datetime object)
+    game_type_cd - the game type of the game being played
+    server_id - server identifier of the server hosting the game
+    map_id - map on which the game was played
+    winner - the team id of the team that won
+    """
+    seq = Sequence('games_game_id_seq')
+    game_id = session.execute(seq)
+    game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
+                server_id=server_id, map_id=map_id, winner=winner)
+    game.match_id = match_id
+
+    try:
+        session.query(Game).filter(Game.server_id==server_id).\
+                filter(Game.match_id==match_id).one()
+        # if a game under the same server and match_id found, 
+        # this is a duplicate game and can be ignored
+        raise pyramid.httpexceptions.HTTPOk('OK')
+    except NoResultFound, e:
+        # server_id/match_id combination not found. game is ok to insert
+        session.add(game)
+        log.debug("Created game id {0} on server {1}, map {2} at \
+                {3}".format(game.game_id, 
+                    server_id, map_id, start_dt))
+
+    return game
+
+
+def get_or_create_player(session=None, hashkey=None, nick=None):
+    """
+    Finds a player by hashkey or creates a new one (along with a
+    corresponding hashkey entry. Parameters:
+
+    session - SQLAlchemy database session factory
+    hashkey - hashkey of the player to be found or created
+    nick - nick of the player (in case of a first time create)
+    """
+    # if we have a bot
+    if re.search('^bot#\d+$', hashkey) or re.search('^bot#\d+#', hashkey):
+        player = session.query(Player).filter_by(player_id=1).one()
+    # if we have an untracked player
+    elif re.search('^player#\d+$', hashkey):
+        player = session.query(Player).filter_by(player_id=2).one()
+    # else it is a tracked player
+    else:
+        # see if the player is already in the database
+        # if not, create one and the hashkey along with it
+        try:
+            hk = session.query(Hashkey).filter_by(
+                    hashkey=hashkey).one()
+            player = session.query(Player).filter_by(
+                    player_id=hk.player_id).one()
+            log.debug("Found existing player {0} with hashkey {1}".format(
+                player.player_id, hashkey))
+        except:
+            player = Player()
+            session.add(player)
+            session.flush()
+
+            # if nick is given to us, use it. If not, use "Anonymous Player"
+            # with a suffix added for uniqueness.
+            if nick:
+                player.nick = nick[:128]
+                player.stripped_nick = strip_colors(nick[:128])
+            else:
+                player.nick = "Anonymous Player #{0}".format(player.player_id)
+                player.stripped_nick = player.nick
+
+            hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
+            session.add(hk)
+            log.debug("Created player {0} ({2}) with hashkey {1}".format(
+                player.player_id, hashkey, player.nick.encode('utf-8')))
+
+    return player
+
+def create_player_game_stat(session=None, player=None, 
+        game=None, player_events=None):
+    """
+    Creates game statistics for a given player in a given game. Parameters:
+
+    session - SQLAlchemy session factory
+    player - Player record of the player who owns the stats
+    game - Game record for the game to which the stats pertain
+    player_events - dictionary for the actual stats that need to be transformed
+    """
+
+    # in here setup default values (e.g. if game type is CTF then
+    # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
+    # TODO: use game's create date here instead of now()
+    seq = Sequence('player_game_stats_player_game_stat_id_seq')
+    pgstat_id = session.execute(seq)
+    pgstat = PlayerGameStat(player_game_stat_id=pgstat_id, 
+            create_dt=datetime.datetime.utcnow())
+
+    # set player id from player record
+    pgstat.player_id = player.player_id
+
+    #set game id from game record
+    pgstat.game_id = game.game_id
+
+    # all games have a score
+    pgstat.score = 0
+
+    if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel':
+        pgstat.kills = 0
+        pgstat.deaths = 0
+        pgstat.suicides = 0
+    elif game.game_type_cd == 'ctf':
+        pgstat.kills = 0
+        pgstat.captures = 0
+        pgstat.pickups = 0
+        pgstat.drops = 0
+        pgstat.returns = 0
+        pgstat.carrier_frags = 0
+
+    for (key,value) in player_events.items():
+        if key == 'n': pgstat.nick = value[:128]
+        if key == 't': pgstat.team = value
+        if key == 'rank': pgstat.rank = value
+        if key == 'alivetime': 
+            pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
+        if key == 'scoreboard-drops': pgstat.drops = value
+        if key == 'scoreboard-returns': pgstat.returns = value
+        if key == 'scoreboard-fckills': pgstat.carrier_frags = value
+        if key == 'scoreboard-pickups': pgstat.pickups = value
+        if key == 'scoreboard-caps': pgstat.captures = value
+        if key == 'scoreboard-score': pgstat.score = value
+        if key == 'scoreboard-deaths': pgstat.deaths = value
+        if key == 'scoreboard-kills': pgstat.kills = value
+        if key == 'scoreboard-suicides': pgstat.suicides = value
+
+    # check to see if we had a name, and if
+    # not use an anonymous handle
+    if pgstat.nick == None:
+        pgstat.nick = "Anonymous Player"
+        pgstat.stripped_nick = "Anonymous Player"
+
+    # otherwise process a nick change
+    elif pgstat.nick != player.nick and player.player_id > 2:
+        register_new_nick(session, player, pgstat.nick)
+
+    # if the player is ranked #1 and it is a team game, set the game's winner
+    # to be the team of that player
+    # FIXME: this is a hack, should be using the 'W' field (not present)
+    if pgstat.rank == '1' and pgstat.team:
+        game.winner = pgstat.team
+        session.add(game)
+
+    session.add(pgstat)
+
+    return pgstat
+
+
+def create_player_weapon_stats(session=None, player=None, 
+        game=None, pgstat=None, player_events=None):
+    """
+    Creates accuracy records for each weapon used by a given player in a
+    given game. Parameters:
+
+    session - SQLAlchemy session factory object
+    player - Player record who owns the weapon stats
+    game - Game record in which the stats were created
+    pgstat - Corresponding PlayerGameStat record for these weapon stats
+    player_events - dictionary containing the raw weapon values that need to be
+        transformed
+    """
+    pwstats = []
+
+    for (key,value) in player_events.items():
+        matched = re.search("acc-(.*?)-cnt-fired", key)
+        if matched:
+            weapon_cd = matched.group(1)
+            seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
+            pwstat_id = session.execute(seq)
+            pwstat = PlayerWeaponStat()
+            pwstat.player_weapon_stats_id = pwstat_id
+            pwstat.player_id = player.player_id
+            pwstat.game_id = game.game_id
+            pwstat.player_game_stat_id = pgstat.player_game_stat_id
+            pwstat.weapon_cd = weapon_cd
+
+            if 'n' in player_events:
+                pwstat.nick = player_events['n']
+            else:
+                pwstat.nick = player_events['P']
+
+            if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
+                pwstat.fired = int(round(float(
+                        player_events['acc-' + weapon_cd + '-cnt-fired'])))
+            if 'acc-' + weapon_cd + '-fired' in player_events:
+                pwstat.max = int(round(float(
+                        player_events['acc-' + weapon_cd + '-fired'])))
+            if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
+                pwstat.hit = int(round(float(
+                        player_events['acc-' + weapon_cd + '-cnt-hit'])))
+            if 'acc-' + weapon_cd + '-hit' in player_events:
+                pwstat.actual = int(round(float(
+                        player_events['acc-' + weapon_cd + '-hit'])))
+            if 'acc-' + weapon_cd + '-frags' in player_events:
+                pwstat.frags = int(round(float(
+                        player_events['acc-' + weapon_cd + '-frags'])))
+
+            session.add(pwstat)
+            pwstats.append(pwstat)
+
+    return pwstats
+
+
+def parse_body(request):
+    """
+    Parses the POST request body for a stats submission
+    """
+    # storage vars for the request body
+    game_meta = {}
+    player_events = {}
+    current_team = None
+    players = []
+
+    for line in request.body.split('\n'):
+        try:
+            (key, value) = line.strip().split(' ', 1)
+
+            # Server (S) and Nick (n) fields can have international characters.
+            # We convert to UTF-8.
+            if key in 'S' 'n':
+                value = unicode(value, 'utf-8')
+
+            if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I':
+                game_meta[key] = value
+
+            if key == 'P':
+                # if we were working on a player record already, append
+                # it and work on a new one (only set team info)
+                if len(player_events) != 0:
+                    players.append(player_events)
+                    player_events = {}
+
+                player_events[key] = value
+
+            if key == 'e':
+                (subkey, subvalue) = value.split(' ', 1)
+                player_events[subkey] = subvalue
+            if key == 'n':
+                player_events[key] = value
+            if key == 't':
+                player_events[key] = value
+        except:
+            # no key/value pair - move on to the next line
+            pass
+
+    # add the last player we were working on
+    if len(player_events) > 0:
+        players.append(player_events)
+
+    return (game_meta, players)
+
+
+def create_player_stats(session=None, player=None, game=None, 
+        player_events=None):
+    """
+    Creates player game and weapon stats according to what type of player
+    """
+    pgstat = create_player_game_stat(session=session, 
+        player=player, game=game, player_events=player_events)
+
+    #TODO: put this into a config setting in the ini file?
+    if not re.search('^bot#\d+$', player_events['P']):
+        create_player_weapon_stats(session=session, 
+            player=player, game=game, pgstat=pgstat,
+            player_events=player_events)
+
+
+def stats_submit(request):
+    """
+    Entry handler for POST stats submissions.
+    """
+    try:
+        session = DBSession()
+
+        log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
+                "----- END REQUEST BODY -----\n\n")
+
+        (idfp, status) = verify_request(request)
+        if not idfp:
+            log.debug("ERROR: Unverified request")
+            raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
+
+        (game_meta, players) = parse_body(request)  
+
+        if not has_required_metadata(game_meta):
+            log.debug("ERROR: Required game meta missing")
+            raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
+
+        if not is_supported_gametype(game_meta['G']):
+            log.debug("ERROR: Unsupported gametype")
+            raise pyramid.httpexceptions.HTTPOk("OK")
+
+        if not has_minimum_real_players(request.registry.settings, players):
+            log.debug("ERROR: Not enough real players")
+            raise pyramid.httpexceptions.HTTPOk("OK")
+
+        if is_blank_game(players):
+            log.debug("ERROR: Blank game")
+            raise pyramid.httpexceptions.HTTPOk("OK")
+
+        # the "duel" gametype is fake
+        if num_real_players(players, count_bots=True) == 2 and \
+                game_meta['G'] == 'dm':
+            game_meta['G'] = 'duel'
+
+
+        # fix for DTG, who didn't #ifdef WATERMARK to set the revision info
+        try:
+            revision = game_meta['R']
+        except:
+            revision = "unknown"
+
+        server = get_or_create_server(session=session, hashkey=idfp, 
+                name=game_meta['S'], revision=revision,
+                ip_addr=get_remote_addr(request))
+
+        gmap = get_or_create_map(session=session, name=game_meta['M'])
+
+        # FIXME: use the gmtime instead of utcnow() when the timezone bug is
+        # fixed
+        game = create_game(session=session, 
+                start_dt=datetime.datetime.utcnow(),
+                #start_dt=datetime.datetime(
+                    #*time.gmtime(float(game_meta['T']))[:6]), 
+                server_id=server.server_id, game_type_cd=game_meta['G'], 
+                   map_id=gmap.map_id, match_id=game_meta['I'])
+
+        # find or create a record for each player
+        # and add stats for each if they were present at the end
+        # of the game
+        for player_events in players:
+            if 'n' in player_events:
+                nick = player_events['n']
+            else:
+                nick = None
+
+            if 'matches' in player_events and 'scoreboardvalid' \
+                in player_events:
+                player = get_or_create_player(session=session, 
+                    hashkey=player_events['P'], nick=nick)
+                log.debug('Creating stats for %s' % player_events['P'])
+                create_player_stats(session=session, player=player, game=game, 
+                        player_events=player_events)
+
+        # update elos
+        try:
+            game.process_elos(session)
+        except Exception as e:
+            log.debug('Error (non-fatal): elo processing failed.')
+
+        session.commit()
+        log.debug('Success! Stats recorded.')
+        return Response('200 OK')
+    except Exception as e:
+        session.rollback()
+        return e