Sorry, but the different encodings between files keep messing up my diffs.
Let's stick to one encoding here.
-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)
-/*\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;}
-// 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
-<%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
+
-<%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 »</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 »</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 -->
-<%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 -->
-<%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 -->
-<%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
-<%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 -->
-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\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
+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
-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)
-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
-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 _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
+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 _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)
-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)
-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