Merge branch 'badges' into approved
authorJan D. Behrens <zykure@web.de>
Sat, 15 Sep 2012 16:45:51 +0000 (18:45 +0200)
committerJan D. Behrens <zykure@web.de>
Sat, 15 Sep 2012 16:45:51 +0000 (18:45 +0200)
Conflicts:
xonstat/batch/badges/gen_badges.py

38 files changed:
.gitignore
xonstat/batch/badges/clean.sh [new file with mode: 0644]
xonstat/batch/badges/css/style.css [deleted file]
xonstat/batch/badges/gen_badges.py
xonstat/batch/badges/img/asfalt.png [new file with mode: 0644]
xonstat/batch/badges/img/background_archer-v1.png [new file with mode: 0644]
xonstat/batch/badges/img/black_linen_v2.png [new file with mode: 0644]
xonstat/batch/badges/img/broken_noise.png [new file with mode: 0644]
xonstat/batch/badges/img/burried.png [new file with mode: 0644]
xonstat/batch/badges/img/dark_leather.png [new file with mode: 0644]
xonstat/batch/badges/img/overlay_classic.png [new file with mode: 0644]
xonstat/batch/badges/img/overlay_classic.xcf [new file with mode: 0644]
xonstat/batch/badges/img/overlay_minimal.png [new file with mode: 0644]
xonstat/batch/badges/img/overlay_minimal.xcf [new file with mode: 0644]
xonstat/batch/badges/img/txture.png [new file with mode: 0644]
xonstat/batch/badges/img/xonotic_logo.svg [new file with mode: 0644]
xonstat/batch/badges/img/xonotic_logo_black-white.svg [new file with mode: 0644]
xonstat/batch/badges/output/_dummy_ [new file with mode: 0644]
xonstat/batch/badges/output/minimal/_dummy_ [new file with mode: 0644]
xonstat/batch/badges/playerdata.py [new file with mode: 0644]
xonstat/batch/badges/skin.py [new file with mode: 0644]
xonstat/batch/badges/templates/badge.html [deleted file]
xonstat/batch/badges/templates/badge.mako [deleted file]
xonstat/models.py
xonstat/static/css/img/web_background_2.jpg [new file with mode: 0644]
xonstat/static/css/style.css
xonstat/templates/base.mako
xonstat/templates/game_info.mako
xonstat/templates/main_index.mako
xonstat/templates/map_index.mako
xonstat/templates/map_info.mako
xonstat/templates/player_index.mako
xonstat/templates/player_info.mako
xonstat/templates/search.mako
xonstat/templates/server_index.mako
xonstat/templates/server_info.mako
xonstat/util.py
xonstat/views/submission.py

index 72723e50a757ee303e6f93d099b636264d5d6171..8e48bc7692b8264bb196983b972be87bab001422 100644 (file)
@@ -1 +1,5 @@
-*pyc
+*.pyc
+*~
+*.bak
+xonstat/batch/badges/output/*.png
+xonstat/batch/badges/output/*/*.png
diff --git a/xonstat/batch/badges/clean.sh b/xonstat/batch/badges/clean.sh
new file mode 100644 (file)
index 0000000..c8fd6aa
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+find output -name "*.png" -exec rm {} \;
+
diff --git a/xonstat/batch/badges/css/style.css b/xonstat/batch/badges/css/style.css
deleted file mode 100644 (file)
index 1d7627e..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-body {
-  color: #ccc;
-  font-family: monospace;
-  font-size: 12px;
-}
-#badge {
-  background: url('../img/dark_wall.png');
-  height: 60px;
-  padding: 5px;
-  position: fixed;
-  width: 550px;
-}
-#nick {
-  font-size: 22px;
-  font-weight: bold;
-  height: 28px;
-  left: 10px;
-  overflow: hidden;
-  position: absolute;
-  width: 285px;
-}
-#games_played {
-  left: 10px;
-  position: absolute;
-  top: 45px;
-}
-#win_percentage, #kill_ratio, #elo {
-  left: 300px;
-  position: absolute;
-}
-#win_percentage {
-  top: 5px;
-}
-#kill_ratio {
-  top: 25px;
-}
-#elo {
-  top: 45px;
-}
index 7a092b2bd9ff9a22372b218ae046feaf17f65c81..0aa3f6014f28ced6956835d349b909925e9e77e6 100644 (file)
 #-*- coding: utf-8 -*-
 
-import cairo as C
+import sys
+from datetime import datetime
 import sqlalchemy as sa
 import sqlalchemy.sql.functions as func
-from datetime import datetime
-from mako.template import Template
-from os import system
+from sqlalchemy import distinct
 from pyramid.paster import bootstrap
 from xonstat.models import *
-from xonstat.views.player import player_info_data
-from xonstat.util import qfont_decode
-from colorsys import rgb_to_hls, hls_to_rgb
-
-
-# similar to html_colors() from util.py
-_contrast_threshold = 0.5
-
-_dec_colors = [ (0.5,0.5,0.5),
-                (1.0,0.0,0.0),
-                (0.2,1.0,0.0),
-                (1.0,1.0,0.0),
-                (0.2,0.4,1.0),
-                (0.2,1.0,1.0),
-                (1.0,0.2,102),
-                (1.0,1.0,1.0),
-                (0.6,0.6,0.6),
-                (0.5,0.5,0.5)
-            ]
-
-
-# parameters to affect the output, could be changed via URL
-params = {
-    'width':        560,
-    'height':        70,
-    'bg':           1,                      # 0 - black, 1 - dark_wall
-    'font':         0,                      # 0 - xolonium, 1 - dejavu sans
-}
-
-
-# maximal number of query results (for testing, set to 0 to get all)
-NUM_PLAYERS = 100
-
-
-def get_data(player):
-    """Return player data as dict.
-    
-    This function is similar to the function in player.py but more optimized
-    for this purpose.
-    """
-
-    # total games
-    # wins/losses
-    # kills/deaths
-    # duel/dm/tdm/ctf elo + rank
-    
-    player_id = player.player_id
-    
-    total_stats = {}
-    
-    games_played = DBSession.query(
-            Game.game_type_cd, func.count(), func.sum(PlayerGameStat.alivetime)).\
-            filter(Game.game_id == PlayerGameStat.game_id).\
-            filter(PlayerGameStat.player_id == player_id).\
-            group_by(Game.game_type_cd).\
-            order_by(func.count().desc()).\
-            limit(3).all()  # limit to 3 gametypes!
-    
-    total_stats['games'] = 0
-    total_stats['games_breakdown'] = {}  # this is a dictionary inside a dictionary .. dictception?
-    total_stats['games_alivetime'] = {}
-    total_stats['gametypes'] = []
-    for (game_type_cd, games, alivetime) in games_played:
-        total_stats['games'] += games
-        total_stats['gametypes'].append(game_type_cd)
-        total_stats['games_breakdown'][game_type_cd] = games
-        total_stats['games_alivetime'][game_type_cd] = alivetime
-    
-    (total_stats['kills'], total_stats['deaths'], total_stats['suicides'],
-     total_stats['alivetime'],) = DBSession.query(
-            func.sum(PlayerGameStat.kills),
-            func.sum(PlayerGameStat.deaths),
-            func.sum(PlayerGameStat.suicides),
-            func.sum(PlayerGameStat.alivetime)).\
-            filter(PlayerGameStat.player_id == player_id).\
-            one()
-    
-    (total_stats['wins'],) = DBSession.query(
-            func.count("*")).\
-            filter(Game.game_id == PlayerGameStat.game_id).\
-            filter(PlayerGameStat.player_id == player_id).\
-            filter(Game.winner == PlayerGameStat.team or PlayerGameStat.rank == 1).\
-            one()
-    
-    ranks = DBSession.query("game_type_cd", "rank", "max_rank").\
-            from_statement(
-                "select pr.game_type_cd, pr.rank, overall.max_rank "
-                "from player_ranks pr,  "
-                   "(select game_type_cd, max(rank) max_rank "
-                    "from player_ranks  "
-                    "group by game_type_cd) overall "
-                "where pr.game_type_cd = overall.game_type_cd  "
-                "and player_id = :player_id "
-                "order by rank").\
-            params(player_id=player_id).all()
-    
-    ranks_dict = {}
-    for gtc,rank,max_rank in ranks:
-        ranks_dict[gtc] = (rank, max_rank)
-
-    elos = DBSession.query(PlayerElo).\
-            filter_by(player_id=player_id).\
-            order_by(PlayerElo.elo.desc()).\
-            all()
-    
-    elos_dict = {}
-    for elo in elos:
-        if elo.games > 32:
-            elos_dict[elo.game_type_cd] = elo.elo
-    
-    data = {
-            'player':player,
-            'total_stats':total_stats,
-            'ranks':ranks_dict,
-            'elos':elos_dict,
-        }
-        
-    #print data
-    return data
-
-
-def render_image(data):
-    """Render an image from the given data fields."""
-    
-    width, height = params['width'], params['height']
-    output = "output/%s.png" % data['player'].player_id
-    
-    font = "Xolonium"
-    if params['font'] == 1:
-        font = "DejaVu Sans"
-
-    total_stats = data['total_stats']
-    total_games = total_stats['games']
-    elos = data["elos"]
-    ranks = data["ranks"]
-
-
-    ## create background
-
-    surf = C.ImageSurface(C.FORMAT_RGB24, width, height)
-    ctx = C.Context(surf)
-    ctx.set_antialias(C.ANTIALIAS_GRAY)
 
-    # draw background (just plain fillcolor)
-    if params['bg'] == 0:
-        ctx.rectangle(0, 0, width, height)
-        ctx.set_source_rgba(0.2, 0.2, 0.2, 1.0)
-        ctx.fill()
-    
-    # draw background image (try to get correct tiling, too)
-    if params['bg'] > 0:
-        bg = None
-        if params['bg'] == 1:
-            bg = C.ImageSurface.create_from_png("img/dark_wall.png")
-        
-        if bg:
-            bg_w, bg_h = bg.get_width(), bg.get_height()
-            bg_xoff = 0
-            while bg_xoff < width:
-                bg_yoff = 0
-                while bg_yoff < height:
-                    ctx.set_source_surface(bg, bg_xoff, bg_yoff)
-                    ctx.paint()
-                    bg_yoff += bg_h
-                bg_xoff += bg_w
-
-
-    ## draw player's nickname with fancy colors
-    
-    # fontsize is reduced if width gets too large
-    nick_xmax = 335
-    ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
-    ctx.set_font_size(20)
-    xoff, yoff, tw, th = ctx.text_extents(player.stripped_nick)[:4]
-    if tw > nick_xmax:
-        ctx.set_font_size(18)
-        xoff, yoff, tw, th = ctx.text_extents(player.stripped_nick)[:4]
-        if tw > nick_xmax:
-            ctx.set_font_size(16)
-            xoff, yoff, tw, th = ctx.text_extents(player.stripped_nick)[:4]
-            if tw > nick_xmax:
-                ctx.set_font_size(14)
-                xoff, yoff, tw, th = ctx.text_extents(player.stripped_nick)[:4]
-                if tw > nick_xmax:
-                    ctx.set_font_size(12)
-    
-    # split up nick into colored segments and draw each of them
-    qstr = qfont_decode(player.nick).replace('^^', '^').replace('\x00', ' ')
-    txt_xoff = 0
-    txt_xpos, txt_ypos = 5,18
-    
-    # split nick into colored segments
-    parts = []
-    pos = 1
-    while True:
-        npos = qstr.find('^', pos)
-        if npos < 0:
-            parts.append(qstr[pos-1:])
-            break;
-        parts.append(qstr[pos-1:npos])
-        pos = npos+1
-    
-    for txt in parts:
-        r,g,b = _dec_colors[7]
-        try:
-            if txt.startswith('^'):
-                txt = txt[1:]
-                if txt.startswith('x'):
-                    r = int(txt[1] * 2, 16) / 255.0
-                    g = int(txt[2] * 2, 16) / 255.0
-                    b = int(txt[3] * 2, 16) / 255.0
-                    hue, light, satur = rgb_to_hls(r, g, b)
-                    if light < _contrast_threshold:
-                        light = _contrast_threshold
-                        r, g, b = hls_to_rgb(hue, light, satur)
-                    txt = txt[4:]
-                else:
-                    r,g,b = _dec_colors[int(txt[0])]
-                    txt = txt[1:]
-        except:
-            r,g,b = _dec_colors[7]
-        
-        if len(txt) < 1:
-            # only colorcode and no real text, skip this
-            continue
-        
-        ctx.set_source_rgb(r, g, b)
-        ctx.move_to(txt_xpos + txt_xoff, txt_ypos)
-        ctx.show_text(txt)
-
-        xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-        if tw == 0:
-            # only whitespaces, use some extra space
-            tw += 5*len(txt)
-        
-        txt_xoff += tw + 1
-
-
-    ## print elos and ranks
-    
-    games_x, games_y = 60,35
-    games_w = 110       # width of each gametype field
-    
-    # show up to three gametypes the player has participated in
-    for gt in total_stats['gametypes'][:3]:
-        ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
-        ctx.set_font_size(10)
-        ctx.set_source_rgb(1.0, 1.0, 1.0)
-        txt = "[ %s ]" % gt.upper()
-        xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-        ctx.move_to(games_x-xoff-tw/2,games_y-yoff-4)
-        ctx.show_text(txt)
-        
-        old_aa = ctx.get_antialias()
-        ctx.set_antialias(C.ANTIALIAS_NONE)
-        ctx.set_source_rgb(0.8, 0.8, 0.8)
-        ctx.set_line_width(1)
-        ctx.move_to(games_x-games_w/2+5, games_y+8)
-        ctx.line_to(games_x+games_w/2-5, games_y+8)
-        ctx.stroke()
-        ctx.move_to(games_x-games_w/2+5, games_y+32)
-        ctx.line_to(games_x+games_w/2-5, games_y+32)
-        ctx.stroke()
-        ctx.set_antialias(old_aa)
-        
-        if not elos.has_key(gt) or not ranks.has_key(gt):
-            ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
-            ctx.set_font_size(12)
-            ctx.set_source_rgb(0.8, 0.2, 0.2)
-            txt = "no stats yet!"
-            xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-            ctx.move_to(games_x-xoff-tw/2,games_y+28-yoff-4)
-            ctx.save()
-            ctx.rotate(math.radians(-10))
-            ctx.show_text(txt)
-            ctx.restore()
+from skin import Skin
+from playerdata import PlayerData
+
+
+# maximal number of query results (for testing, set to None to get all)
+NUM_PLAYERS = None
+
+# we look for players who have activity within the past DELTA hours
+DELTA = 6
+
+
+# classic skin WITHOUT NAME - writes PNGs into "output//###.png"
+skin_classic = Skin( "",
+        bg              = "broken_noise",
+        overlay         = "overlay_classic",
+    )
+
+# more fancy skin [** WIP **]- writes PNGs into "output/archer/###.png"
+skin_archer = Skin( "archer",
+        bg              = "background_archer-v1",
+        overlay         = None,
+    )
+
+# minimal skin - writes PNGs into "output/minimal/###.png"
+skin_minimal = Skin( "minimal",
+        bg              = None,
+        bgcolor         = (0.04, 0.04, 0.04, 1.0),
+        overlay         = "overlay_minimal",
+        width           = 560,
+        height          = 40,
+        nick_fontsize   = 16,
+        nick_pos        = (36,16),
+        num_gametypes   = 3,
+        nick_maxwidth   = 300,
+        gametype_pos    = (70,30),
+        gametype_color  = (0.0, 0.0, 0.0),
+        gametype_text   = "%s:",
+        gametype_width  = 100,
+        gametype_fontsize = 10,
+        gametype_align  = -1,
+        gametype_upper  = False,
+        elo_pos         = (75,30),
+        elo_text        = "Elo %.0f",
+        elo_color       = (0.7, 0.7, 0.7),
+        elo_align       = 1,
+        rank_pos        = None,
+        nostats_pos     = None,
+        #nostats_pos     = (75,30),
+        #nostats_fontsize = 10,
+        #nostats_angle   = 0,
+        #nostats_text    = "no stats yet!",
+        #nostats_color   = (0.7, 0.4, 0.4),
+        kdr_pos         = (392,15),
+        kdr_fontsize    = 10,
+        kdr_colortop    = (0.6, 0.8, 0.6),
+        kdr_colormid    = (0.6, 0.6, 0.6),
+        kdr_colorbot    = (0.8, 0.6, 0.6),
+        kills_pos       = None,
+        deaths_pos      = None,
+        winp_pos        = (508,15),
+        winp_fontsize   = 10,
+        winp_colortop   = (0.6, 0.8, 0.8),
+        winp_colormid   = (0.6, 0.6, 0.6),
+        winp_colorbot   = (0.8, 0.8, 0.6),
+        wins_pos        = None,
+        loss_pos        = None,
+        ptime_pos       = (451,30),
+        ptime_color     = (0.7, 0.7, 0.7),
+    )
+
+
+# parse cmdline parameters (for testing)
+
+skins = []
+for arg in sys.argv[1:]:
+    if arg.startswith("-"):
+        arg = arg[1:]
+        if arg == "force":
+            DELTA = 2**24   # large enough to enforce update, and doesn't result in errors
+        elif arg == "test":
+            NUM_PLAYERS = 100
         else:
-            ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
-            ctx.set_font_size(10)
-            ctx.set_source_rgb(1.0, 1.0, 0.5)
-            txt = "Elo: %.0f" % round(elos[gt], 0)
-            xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-            ctx.move_to(games_x-xoff-tw/2,games_y+15-yoff-4)
-            ctx.show_text(txt)
-            
-            ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
-            ctx.set_font_size(8)
-            ctx.set_source_rgb(0.8, 0.8, 0.8)
-            txt = "Rank %d of %d" % ranks[gt]
-            xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-            ctx.move_to(games_x-xoff-tw/2,games_y+25-yoff-3)
-            ctx.show_text(txt)
-        
-        games_x += games_w
-
-
-    # print win percentage
-    win_x, win_y = 505,11
-    win_w, win_h = 100,14
-    
-    ctx.rectangle(win_x-win_w/2,win_y-win_h/2,win_w,win_h)
-    ctx.set_source_rgba(0.8, 0.8, 0.8, 0.1)
-    ctx.fill();
-    
-    ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
-    ctx.set_font_size(10)
-    ctx.set_source_rgb(0.8, 0.8, 0.8)
-    txt = "Win Percentage"
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(win_x-xoff-tw/2,win_y-yoff-3)
-    ctx.show_text(txt)
-    
-    txt = "???"
-    if total_games > 0 and total_stats['wins'] is not None:
-        ratio = float(total_stats['wins'])/total_games
-        txt = "%.2f%%" % round(ratio * 100, 2)
-    ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
-    ctx.set_font_size(12)
-    if ratio >= 0.90:
-        ctx.set_source_rgb(0.2, 1.0, 1.0)
-    elif ratio >= 0.75:
-        ctx.set_source_rgb(0.5, 1.0, 1.0)
-    elif ratio >= 0.5:
-        ctx.set_source_rgb(0.5, 1.0, 0.8)
-    elif ratio >= 0.25:
-        ctx.set_source_rgb(0.8, 1.0, 0.5)
-    else:
-        ctx.set_source_rgb(1.0, 1.0, 0.5)
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(win_x-xoff-tw/2,win_y+16-yoff-4)
-    ctx.show_text(txt)
-    
-    ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
-    ctx.set_font_size(8)
-    ctx.set_source_rgb(0.6, 0.8, 0.6)
-    txt = "%d wins" % total_stats["wins"]
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(win_x-xoff-tw/2,win_y+28-yoff-3)
-    ctx.show_text(txt)
-    ctx.set_source_rgb(0.8, 0.6, 0.6)
-    txt = "%d losses" % (total_games-total_stats['wins'])
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(win_x-xoff-tw/2,win_y+38-yoff-3)
-    ctx.show_text(txt)
-
-
-    # print kill/death ratio
-    kill_x, kill_y = 395,11
-    kill_w, kill_h = 100,14
-    
-    ctx.rectangle(kill_x-kill_w/2,kill_y-kill_h/2,kill_w,kill_h)
-    ctx.set_source_rgba(0.8, 0.8, 0.8, 0.1)
-    ctx.fill()
-    
-    ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
-    ctx.set_font_size(10)
-    ctx.set_source_rgb(0.8, 0.8, 0.8)
-    txt = "Kill Ratio"
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(kill_x-xoff-tw/2,kill_y-yoff-3)
-    ctx.show_text(txt)
-    
-    txt = "???"
-    if total_stats['deaths'] > 0 and total_stats['kills'] is not None:
-        ratio = float(total_stats['kills'])/total_stats['deaths']
-        txt = "%.3f" % round(ratio, 3)
-    ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_BOLD)
-    ctx.set_font_size(12)
-    if ratio >= 3:
-        ctx.set_source_rgb(0.0, 1.0, 0.0)
-    elif ratio >= 2:
-        ctx.set_source_rgb(0.2, 1.0, 0.2)
-    elif ratio >= 1:
-        ctx.set_source_rgb(0.5, 1.0, 0.5)
-    elif ratio >= 0.5:
-        ctx.set_source_rgb(1.0, 0.5, 0.5)
+            print """Usage:  gen_badges.py [options] [skin list]
+    Options:
+        -force      Force updating all badges (delta = 2^24)
+        -test       Limit number of players to 100 (for testing)
+        -help       Show this help text
+    Skin list:
+        Space-separated list of skins to use when creating badges.
+        Available skins:  classic, minimal, archer
+        If no skins are given, classic and minmal will be used by default.
+        NOTE: Output directories must exists before running the program!
+"""
+            sys.exit(-1)
     else:
-        ctx.set_source_rgb(1.0, 0.2, 0.2)
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(kill_x-xoff-tw/2,kill_y+16-yoff-4)
-    ctx.show_text(txt)
-
-    ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
-    ctx.set_font_size(8)
-    ctx.set_source_rgb(0.6, 0.8, 0.6)
-    txt = "%d kills" % total_stats["kills"]
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(kill_x-xoff-tw/2,kill_y+28-yoff-3)
-    ctx.show_text(txt)
-    ctx.set_source_rgb(0.8, 0.6, 0.6)
-    txt = "%d deaths" % total_stats['deaths']
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(kill_x-xoff-tw/2,kill_y+38-yoff-3)
-    ctx.show_text(txt)
-
-
-    # print playing time
-    time_x, time_y = 450,64
-    time_w, time_h = 210,10
-    
-    ctx.rectangle(time_x-time_w/2,time_y-time_h/2-1,time_w,time_y+time_h/2-1)
-    ctx.set_source_rgba(0.8, 0.8, 0.8, 0.6)
-    ctx.fill();
-    
-    ctx.select_font_face(font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
-    ctx.set_font_size(10)
-    ctx.set_source_rgb(0.1, 0.1, 0.1)
-    txt = "Playing time: %s" % str(total_stats['alivetime'])
-    xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
-    ctx.move_to(time_x-xoff-tw/2,time_y-yoff-4)
-    ctx.show_text(txt)
-
-
-    # save to PNG
-    surf.write_to_png(output)
+        if arg == "classic":
+            skins.append(skin_classic)
+        elif arg == "minimal":
+            skins.append(skin_minimal)
+        elif arg == "archer":
+            skins.append(skin_archer)
 
+if len(skins) == 0:
+    skins = [ skin_classic, skin_minimal ]
 
 # environment setup
 env = bootstrap('../../../development.ini')
@@ -431,37 +119,64 @@ req = env['request']
 req.matchdict = {'id':3}
 
 print "Requesting player data from db ..."
+cutoff_dt = datetime.utcnow() - timedelta(hours=DELTA)
 start = datetime.now()
-players = DBSession.query(Player).\
-        filter(Player.player_id == PlayerElo.player_id).\
-        filter(Player.nick != None).\
-        filter(Player.player_id > 2).\
-        filter(Player.active_ind == True).\
-        limit(NUM_PLAYERS).all()
-stop = datetime.now()
-print "Query took %.2f seconds" % (stop-start).total_seconds()
-
-print "Creating badges for %d players ..." % len(players)
-start = datetime.now()
-data_time, render_time = 0,0
-for player in players:
-    req.matchdict['id'] = player.player_id
-    
-    sstart = datetime.now()
-    #data = player_info_data(req)
-    data = get_data(player)
-    sstop = datetime.now()
-    data_time += (sstop-sstart).total_seconds()
-    
-    print "\t#%d (%s)" % (player.player_id, player.stripped_nick)
-
-    sstart = datetime.now()
-    render_image(data)
-    sstop = datetime.now()
-    render_time += (sstop-sstart).total_seconds()
+players = []
+if NUM_PLAYERS:
+    players = DBSession.query(distinct(Player.player_id)).\
+            filter(Player.player_id == PlayerElo.player_id).\
+            filter(Player.player_id == PlayerGameStat.player_id).\
+            filter(PlayerGameStat.create_dt > cutoff_dt).\
+            filter(Player.nick != None).\
+            filter(Player.player_id > 2).\
+            filter(Player.active_ind == True).\
+            limit(NUM_PLAYERS).all()
+else:
+    players = DBSession.query(distinct(Player.player_id)).\
+            filter(Player.player_id == PlayerElo.player_id).\
+            filter(Player.player_id == PlayerGameStat.player_id).\
+            filter(PlayerGameStat.create_dt > cutoff_dt).\
+            filter(Player.nick != None).\
+            filter(Player.player_id > 2).\
+            filter(Player.active_ind == True).\
+            all()
 
-stop = datetime.now()
-print "Creating the badges took %.2f seconds (%.2f s per player)" % ((stop-start).total_seconds(), (stop-start).total_seconds()/float(len(players)))
-print "Total time for getting data: %.2f s" % data_time
-print "Total time for redering images: %.2f s" % render_time
+playerdata = PlayerData()
+
+if len(players) > 0:
+    stop = datetime.now()
+    td = stop-start
+    total_seconds = (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
+    print "Query took %.2f seconds" % (total_seconds)
+
+    print "Creating badges for %d players ..." % len(players)
+    start = datetime.now()
+    data_time, render_time = 0,0
+    for player_id in players:
+        req.matchdict['id'] = player_id
+
+        sstart = datetime.now()
+        playerdata.get_data(player_id)
+        sstop = datetime.now()
+        td = sstop-sstart
+        total_seconds = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
+        data_time += total_seconds
+
+        sstart = datetime.now()
+        for sk in skins:
+            sk.render_image(playerdata, "output/%s/%d.png" % (str(sk), player_id[0]))
+        sstop = datetime.now()
+        td = sstop-sstart
+        total_seconds = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
+        render_time += total_seconds
+
+    stop = datetime.now()
+    td = stop-start
+    total_seconds = float(td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
+    print "Creating the badges took %.1f seconds (%.3f s per player)" % (total_seconds, total_seconds/float(len(players)))
+    print "Total time for rendering images: %.3f s" % render_time
+    print "Total time for getting data: %.3f s" % data_time
+
+else:
+    print "No active players found!"
 
diff --git a/xonstat/batch/badges/img/asfalt.png b/xonstat/batch/badges/img/asfalt.png
new file mode 100644 (file)
index 0000000..1fb261d
Binary files /dev/null and b/xonstat/batch/badges/img/asfalt.png differ
diff --git a/xonstat/batch/badges/img/background_archer-v1.png b/xonstat/batch/badges/img/background_archer-v1.png
new file mode 100644 (file)
index 0000000..c577321
Binary files /dev/null and b/xonstat/batch/badges/img/background_archer-v1.png differ
diff --git a/xonstat/batch/badges/img/black_linen_v2.png b/xonstat/batch/badges/img/black_linen_v2.png
new file mode 100644 (file)
index 0000000..d125b4b
Binary files /dev/null and b/xonstat/batch/badges/img/black_linen_v2.png differ
diff --git a/xonstat/batch/badges/img/broken_noise.png b/xonstat/batch/badges/img/broken_noise.png
new file mode 100644 (file)
index 0000000..fe5857f
Binary files /dev/null and b/xonstat/batch/badges/img/broken_noise.png differ
diff --git a/xonstat/batch/badges/img/burried.png b/xonstat/batch/badges/img/burried.png
new file mode 100644 (file)
index 0000000..6c1e0da
Binary files /dev/null and b/xonstat/batch/badges/img/burried.png differ
diff --git a/xonstat/batch/badges/img/dark_leather.png b/xonstat/batch/badges/img/dark_leather.png
new file mode 100644 (file)
index 0000000..3ce4b73
Binary files /dev/null and b/xonstat/batch/badges/img/dark_leather.png differ
diff --git a/xonstat/batch/badges/img/overlay_classic.png b/xonstat/batch/badges/img/overlay_classic.png
new file mode 100644 (file)
index 0000000..c7a2347
Binary files /dev/null and b/xonstat/batch/badges/img/overlay_classic.png differ
diff --git a/xonstat/batch/badges/img/overlay_classic.xcf b/xonstat/batch/badges/img/overlay_classic.xcf
new file mode 100644 (file)
index 0000000..aafc152
Binary files /dev/null and b/xonstat/batch/badges/img/overlay_classic.xcf differ
diff --git a/xonstat/batch/badges/img/overlay_minimal.png b/xonstat/batch/badges/img/overlay_minimal.png
new file mode 100644 (file)
index 0000000..184af23
Binary files /dev/null and b/xonstat/batch/badges/img/overlay_minimal.png differ
diff --git a/xonstat/batch/badges/img/overlay_minimal.xcf b/xonstat/batch/badges/img/overlay_minimal.xcf
new file mode 100644 (file)
index 0000000..047278e
Binary files /dev/null and b/xonstat/batch/badges/img/overlay_minimal.xcf differ
diff --git a/xonstat/batch/badges/img/txture.png b/xonstat/batch/badges/img/txture.png
new file mode 100644 (file)
index 0000000..ff3ec14
Binary files /dev/null and b/xonstat/batch/badges/img/txture.png differ
diff --git a/xonstat/batch/badges/img/xonotic_logo.svg b/xonstat/batch/badges/img/xonotic_logo.svg
new file mode 100644 (file)
index 0000000..b8bc1b2
--- /dev/null
@@ -0,0 +1,346 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="289.6001"
+   height="277.31879"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.48.2 r9819"
+   version="1.0"
+   sodipodi:docname="xonotic_logo.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   inkscape:export-xdpi="45"
+   inkscape:export-ydpi="45">
+  <defs
+     id="defs4">
+    <linearGradient
+       id="linearGradient3700">
+      <stop
+         style="stop-color:#000000;stop-opacity:0;"
+         offset="0"
+         id="stop3705" />
+      <stop
+         id="stop3707"
+         offset="0.31"
+         style="stop-color:#000000;stop-opacity:0;" />
+      <stop
+         style="stop-color:#000000;stop-opacity:0.18431373;"
+         offset="0.44"
+         id="stop3709" />
+      <stop
+         id="stop3711"
+         offset="0.85000002"
+         style="stop-color:#000000;stop-opacity:0;" />
+      <stop
+         id="stop3713"
+         offset="1"
+         style="stop-color:#000000;stop-opacity:0;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3681">
+      <stop
+         id="stop3691"
+         offset="0"
+         style="stop-color:#1f7fff;stop-opacity:0;" />
+      <stop
+         style="stop-color:#1f7fff;stop-opacity:0;"
+         offset="0.31"
+         id="stop3695" />
+      <stop
+         id="stop3689"
+         offset="0.44"
+         style="stop-color:#1f7fff;stop-opacity:0.18431373;" />
+      <stop
+         style="stop-color:#1f7fff;stop-opacity:0;"
+         offset="0.85000002"
+         id="stop3704" />
+      <stop
+         style="stop-color:#1f7fff;stop-opacity:0;"
+         offset="1"
+         id="stop3685" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3699">
+      <stop
+         id="stop3701"
+         offset="0"
+         style="stop-color:#3f0f00;stop-opacity:0.56078434;" />
+      <stop
+         style="stop-color:#ff7f2f;stop-opacity:0.56078434;"
+         offset="0.75"
+         id="stop3673" />
+      <stop
+         id="stop3703"
+         offset="1"
+         style="stop-color:#ff9f5f;stop-opacity:0.56078434;" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3698">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop3700" />
+      <stop
+         id="stop3706"
+         offset="0.5"
+         style="stop-color:#0f3f6f;stop-opacity:1;" />
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="1"
+         id="stop3702" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3674">
+      <stop
+         style="stop-color:#000000;stop-opacity:1;"
+         offset="0"
+         id="stop3676" />
+      <stop
+         style="stop-color:#3f0b00;stop-opacity:1;"
+         offset="1"
+         id="stop3678" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3666">
+      <stop
+         style="stop-color:#3f0f00;stop-opacity:1;"
+         offset="0"
+         id="stop3668" />
+      <stop
+         id="stop3671"
+         offset="0.75"
+         style="stop-color:#ff7f2f;stop-opacity:1;" />
+      <stop
+         style="stop-color:#ff9f5f;stop-opacity:1;"
+         offset="1"
+         id="stop3670" />
+    </linearGradient>
+    <linearGradient
+       id="linearGradient3606">
+      <stop
+         style="stop-color:#bfdfff;stop-opacity:1;"
+         offset="0"
+         id="stop3608" />
+      <stop
+         id="stop3616"
+         offset="0.2"
+         style="stop-color:#5fafff;stop-opacity:1;" />
+      <stop
+         id="stop3614"
+         offset="0.80000001"
+         style="stop-color:#5fafff;stop-opacity:1;" />
+      <stop
+         style="stop-color:#bfdfff;stop-opacity:1;"
+         offset="1"
+         id="stop3610" />
+    </linearGradient>
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3674"
+       id="linearGradient4196"
+       x1="1024"
+       y1="640"
+       x2="1024"
+       y2="440"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3666"
+       id="linearGradient4204"
+       x1="1024"
+       y1="640"
+       x2="1024"
+       y2="440"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3699"
+       id="linearGradient4214"
+       x1="1024"
+       y1="640"
+       x2="1024"
+       y2="440"
+       gradientUnits="userSpaceOnUse" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3698"
+       id="linearGradient3686"
+       gradientUnits="userSpaceOnUse"
+       x1="1024"
+       y1="640"
+       x2="1024"
+       y2="384" />
+    <linearGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3606"
+       id="linearGradient3689"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="translate(0,-1024)"
+       x1="1024"
+       y1="1664"
+       x2="1024"
+       y2="1480" />
+    <filter
+       inkscape:collect="always"
+       id="filter3806"
+       x="-0.075428568"
+       width="1.1508571"
+       y="-0.132"
+       height="1.2640001"
+       inkscape:label="BlurShadow"
+       color-interpolation-filters="sRGB">
+      <feGaussianBlur
+         inkscape:collect="always"
+         stdDeviation="6"
+         id="feGaussianBlur3808" />
+    </filter>
+    <filter
+       inkscape:collect="always"
+       id="filter3852"
+       inkscape:label="BlurGlow"
+       color-interpolation-filters="sRGB">
+      <feGaussianBlur
+         inkscape:collect="always"
+         stdDeviation="7"
+         id="feGaussianBlur3854" />
+    </filter>
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#000000"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="1"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1"
+     inkscape:cx="72.936179"
+     inkscape:cy="118.80348"
+     inkscape:document-units="px"
+     inkscape:current-layer="XonoticLogo"
+     showgrid="false"
+     inkscape:showpageshadow="false"
+     gridtolerance="10"
+     showguides="true"
+     inkscape:guide-bbox="true"
+     inkscape:snap-to-guides="true"
+     inkscape:snap-global="true"
+     borderlayer="true"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     units="pt"
+     inkscape:window-width="1366"
+     inkscape:window-height="744"
+     inkscape:window-x="0"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2383"
+       visible="true"
+       enabled="true"
+       empspacing="4"
+       snapvisiblegridlinesonly="true" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+        <dc:date>2010</dc:date>
+        <dc:rights>
+          <cc:Agent>
+            <dc:title>Xonotic Community</dc:title>
+          </cc:Agent>
+        </dc:rights>
+        <dc:publisher>
+          <cc:Agent>
+            <dc:title />
+          </cc:Agent>
+        </dc:publisher>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Xonotic Community</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <cc:license
+           rdf:resource="Dual-licensed under the &quot;GNU LGPL v2.1, or any later version&quot; (http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) and &quot;CC-BY v3.0&quot; (http://creativecommons.org/licenses/by/3.0/)" />
+        <dc:contributor>
+          <cc:Agent>
+            <dc:title />
+          </cc:Agent>
+        </dc:contributor>
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>Xonotic</rdf:li>
+            <rdf:li>Phoenix</rdf:li>
+            <rdf:li>Logo</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <dc:language />
+        <dc:description>The logo of the Xonotic project.</dc:description>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     id="XonoticLogo"
+     transform="translate(-879.2,-379.4812)">
+    <g
+       id="PhoenixHead">
+      <path
+         sodipodi:nodetypes="cccccccsccccccccccc"
+         id="PhoenixHeadShadow"
+         d="m 1057.0938,439.71875 c -34.0014,5.48439 -77.51225,47.9452 -119.9063,82.0625 l 52.125,-24.0625 -30.125,32.03125 26.03125,-12.1875 c -8.54487,16.84277 -7.62992,45.97673 -35.75,51.71875 14.64181,14.49069 33.4581,24.78052 54.50005,28.8125 C 1014,600 1021,628 1024,628 c 3,0 10,-28 20.0312,-29.90625 21.8482,-4.18648 41.2906,-15.12031 56.1563,-30.5 -74.3924,0.92961 -69.6917,-74.54814 -3.9687,-91.0625 -5.069,-4.72305 -21.9395,-3.83806 -35.75,-1.65625 2.4067,-16.09443 25.2286,-23.80249 42.0937,-21.71875 -9.6384,-9.77813 -21.3735,-16.84255 -40.8437,-8.71875 l -4.625,-4.71875 z m -9.3438,13.5625 5.0625,5 c -6.6098,5.59761 -12.4827,5.96968 -18.0625,0.96875 l 13,-5.96875 z"
+         style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:8;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;filter:url(#filter3806)"
+         inkscape:connector-curvature="0" />
+      <path
+         style="fill:url(#linearGradient4214);fill-opacity:1;fill-rule:evenodd;stroke:none;filter:url(#filter3852)"
+         d="m 1057.0938,439.71875 c -34.0014,5.48439 -77.51225,47.9452 -119.9063,82.0625 l 52.125,-24.0625 -30.125,32.03125 26.03125,-12.1875 c -8.54487,16.84277 -7.62992,45.97673 -35.75,51.71875 14.64181,14.49069 33.4581,24.78052 54.50005,28.8125 C 1014,600 1021,628 1024,628 c 3,0 10,-28 20.0312,-29.90625 21.8482,-4.18648 41.2906,-15.12031 56.1563,-30.5 -74.3924,0.92961 -69.6917,-74.54814 -3.9687,-91.0625 -5.069,-4.72305 -21.9395,-3.83806 -35.75,-1.65625 2.4067,-16.09443 25.2286,-23.80249 42.0937,-21.71875 -9.6384,-9.77813 -21.3735,-16.84255 -40.8437,-8.71875 l -4.625,-4.71875 z m -9.3438,13.5625 5.0625,5 c -6.6098,5.59761 -12.4827,5.96968 -18.0625,0.96875 l 13,-5.96875 z"
+         id="PhoenixHeadGlow"
+         sodipodi:nodetypes="cccccccsccccccccccc"
+         inkscape:connector-curvature="0" />
+      <path
+         sodipodi:nodetypes="cccccccsccccccccccc"
+         id="PhoenixHeadFill"
+         d="m 1057.0938,439.71875 c -34.0014,5.48439 -77.51225,47.9452 -119.9063,82.0625 l 52.125,-24.0625 -30.125,32.03125 26.03125,-12.1875 c -8.54487,16.84277 -7.62992,45.97673 -35.75,51.71875 14.64181,14.49069 33.4581,24.78052 54.50005,28.8125 C 1014,600 1021,628 1024,628 c 3,0 10,-28 20.0312,-29.90625 21.8482,-4.18648 41.2906,-15.12031 56.1563,-30.5 -74.3924,0.92961 -69.6917,-74.54814 -3.9687,-91.0625 -5.069,-4.72305 -21.9395,-3.83806 -35.75,-1.65625 2.4067,-16.09443 25.2286,-23.80249 42.0937,-21.71875 -9.6384,-9.77813 -21.3735,-16.84255 -40.8437,-8.71875 l -4.625,-4.71875 z m -9.3438,13.5625 5.0625,5 c -6.6098,5.59761 -12.4827,5.96968 -18.0625,0.96875 l 13,-5.96875 z"
+         style="fill:url(#linearGradient4196);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient4204);stroke-width:2;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         inkscape:connector-curvature="0" />
+    </g>
+    <g
+       id="Ring"
+       inkscape:export-xdpi="45"
+       inkscape:export-ydpi="45">
+      <path
+         style="fill:#1f7fff;fill-opacity:1;stroke:none;filter:url(#filter3852)"
+         d="M 969.28125,396.2812 C 925.981,416.8108 896,460.9225 896,512 c 0,70.656 57.344,128 128.0001,128 70.656,0 128,-57.344 128,-128 0,-51.0775 -29.9809,-95.1892 -73.2812,-115.7188 34.1753,19.1854 57.2812,55.7626 57.2812,97.7188 0,54.5462 -39.0631,100.0173 -90.7188,109.9688 -6.8922,1.3277 -15.2812,32.0312 -21.2812,32.0312 -6,0 -14.3892,-30.7034 -21.2814,-32.0312 C 951.06315,594.0172 912,548.5462 912,494 c 0,-41.9562 23.1059,-78.5334 57.28125,-97.7188 z"
+         id="RingGlow"
+         inkscape:connector-curvature="0" />
+      <path
+         id="RingBorder"
+         d="M 969.28125,396.2812 C 925.981,416.8108 896,460.9225 896,512 c 0,70.656 57.344,128 128.0001,128 70.656,0 128,-57.344 128,-128 0,-51.0775 -29.9809,-95.1892 -73.2812,-115.7188 34.1753,19.1854 57.2812,55.7626 57.2812,97.7188 0,54.5462 -39.0631,100.0173 -90.7188,109.9688 -6.8922,1.3277 -15.2812,32.0312 -21.2812,32.0312 -6,0 -14.3892,-30.7034 -21.2814,-32.0312 C 951.06315,594.0172 912,548.5462 912,494 c 0,-41.9562 23.1059,-78.5334 57.28125,-97.7188 z"
+         style="fill:url(#linearGradient3689);fill-opacity:1;stroke:none"
+         inkscape:connector-curvature="0" />
+      <path
+         sodipodi:nodetypes="csccsccsccsc"
+         id="RingFill"
+         d="M 924.4375,436.40625 C 908.47534,457.39804 899,483.594 899,512 c 0,67.6951 53.81855,122.8847 121,125 -8,-7 -14,-29 -18,-30 -53.03387,-10.23 -93,-56.9795 -93,-113 0,-20.98082 5.62297,-40.66002 15.4375,-57.59375 z m 199.125,0 C 1133.377,453.33998 1139,473.01918 1139,494 c 0,56.0205 -39.9661,102.77 -93,113 -4,1 -10,23 -18,30 67.1815,-2.1153 121,-57.3049 121,-125 0,-28.406 -9.4753,-54.60196 -25.4375,-75.59375 z"
+         style="fill:url(#linearGradient3686);fill-opacity:1;stroke:#00001f;stroke-width:1.5;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
+         inkscape:connector-curvature="0" />
+    </g>
+  </g>
+</svg>
diff --git a/xonstat/batch/badges/img/xonotic_logo_black-white.svg b/xonstat/batch/badges/img/xonotic_logo_black-white.svg
new file mode 100644 (file)
index 0000000..e0c8dbb
--- /dev/null
@@ -0,0 +1,120 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="256"
+   height="243.72501"
+   id="svg2"
+   sodipodi:version="0.32"
+   inkscape:version="0.48.2 r9819"
+   version="1.0"
+   sodipodi:docname="xonotic_logo_black-white.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   inkscape:export-xdpi="14.765625"
+   inkscape:export-ydpi="14.765625"
+   inkscape:export-filename="/home/zykure/XonStat/xonstat/batch/badges/img/xonotic_logo_black-white.png">
+  <defs
+     id="defs4" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="1"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.4142136"
+     inkscape:cx="219.9015"
+     inkscape:cy="151.47868"
+     inkscape:document-units="px"
+     inkscape:current-layer="XonoticLogo"
+     showgrid="false"
+     inkscape:showpageshadow="false"
+     gridtolerance="10"
+     showguides="false"
+     inkscape:guide-bbox="true"
+     inkscape:snap-to-guides="true"
+     inkscape:snap-global="true"
+     borderlayer="true"
+     inkscape:window-width="1366"
+     inkscape:window-height="744"
+     inkscape:window-x="0"
+     inkscape:window-y="24"
+     inkscape:window-maximized="1"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     units="pt">
+    <inkscape:grid
+       type="xygrid"
+       id="grid2383"
+       visible="true"
+       enabled="true"
+       empspacing="4"
+       snapvisiblegridlinesonly="true" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata7">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+        <dc:date>2010</dc:date>
+        <dc:rights>
+          <cc:Agent>
+            <dc:title>Xonotic Community</dc:title>
+          </cc:Agent>
+        </dc:rights>
+        <dc:publisher>
+          <cc:Agent>
+            <dc:title />
+          </cc:Agent>
+        </dc:publisher>
+        <dc:creator>
+          <cc:Agent>
+            <dc:title>Xonotic Community</dc:title>
+          </cc:Agent>
+        </dc:creator>
+        <cc:license
+           rdf:resource="Dual-licensed under the &quot;GNU LGPL v2.1, or any later version&quot; (http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html) and &quot;CC-BY v3.0&quot; (http://creativecommons.org/licenses/by/3.0/)" />
+        <dc:subject>
+          <rdf:Bag>
+            <rdf:li>Xonotic</rdf:li>
+            <rdf:li>Phoenix</rdf:li>
+            <rdf:li>Logo</rdf:li>
+          </rdf:Bag>
+        </dc:subject>
+        <dc:description>A print version of the Xonotic logo. Completely black.</dc:description>
+        <dc:relation>Xonotic Logo</dc:relation>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:groupmode="layer"
+     id="XonoticLogo"
+     transform="translate(-896,-396.275)">
+    <path
+       style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none"
+       d="m 1057.0938,439.71875 c -34.0014,5.48439 -77.51225,47.9452 -119.9063,82.0625 l 52.125,-24.0625 -30.125,32.03125 26.03125,-12.1875 c -8.54487,16.84277 -7.62992,45.97673 -35.75,51.71875 14.64181,14.49069 33.4581,24.78052 54.50005,28.8125 C 1014,600 1021,628 1024,628 c 3,0 10,-28 20.0312,-29.90625 21.8482,-4.18648 41.2906,-15.12031 56.1563,-30.5 -74.3924,0.92961 -69.6917,-74.54814 -3.9687,-91.0625 -5.069,-4.72305 -21.9395,-3.83806 -35.75,-1.65625 2.4067,-16.09443 25.2286,-23.80249 42.0937,-21.71875 -9.6384,-9.77813 -21.3735,-16.84255 -40.8437,-8.71875 l -4.625,-4.71875 z m -9.3438,13.5625 5.0625,5 c -6.6098,5.59761 -12.4827,5.96968 -18.0625,0.96875 l 13,-5.96875 z"
+       id="PhoenixHead"
+       sodipodi:nodetypes="cccccccsccccccccccc"
+       inkscape:connector-curvature="0" />
+    <path
+       style="fill:#000000;fill-opacity:1;stroke:none"
+       d="M 969.28125,396.2812 C 925.981,416.8108 896,460.9225 896,512 c 0,70.656 57.344,128 128.0001,128 70.656,0 128,-57.344 128,-128 0,-51.0775 -29.9809,-95.1892 -73.2812,-115.7188 34.1753,19.1854 57.2812,55.7626 57.2812,97.7188 0,54.5462 -39.0631,100.0173 -90.7188,109.9688 -6.8922,1.3277 -15.2812,32.0312 -21.2812,32.0312 -6,0 -14.3892,-30.7034 -21.2814,-32.0312 C 951.06315,594.0172 912,548.5462 912,494 c 0,-41.9562 23.1059,-78.5334 57.28125,-97.7188 z"
+       id="RingBorder"
+       inkscape:connector-curvature="0"
+       inkscape:export-xdpi="14.765619"
+       inkscape:export-ydpi="14.765619" />
+  </g>
+</svg>
diff --git a/xonstat/batch/badges/output/_dummy_ b/xonstat/batch/badges/output/_dummy_
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/xonstat/batch/badges/output/minimal/_dummy_ b/xonstat/batch/badges/output/minimal/_dummy_
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/xonstat/batch/badges/playerdata.py b/xonstat/batch/badges/playerdata.py
new file mode 100644 (file)
index 0000000..0990208
--- /dev/null
@@ -0,0 +1,111 @@
+import sqlalchemy as sa
+import sqlalchemy.sql.functions as func
+from xonstat.models import *
+
+
+class PlayerData:
+
+    # player data, will be filled by get_data()
+    data = {}
+
+    def __init__(self):
+        self.data = {}
+
+    def __getattr__(self, key):
+        if self.data.has_key(key):
+            return self.data[key]
+        return None
+
+    def get_data(self, player_id):
+        """Return player data as dict.
+
+        This function is similar to the function in player.py but more optimized
+        for this purpose.
+        """
+        # total games
+        # wins/losses
+        # kills/deaths
+        # duel/dm/tdm/ctf elo + rank
+
+        player = DBSession.query(Player).filter(Player.player_id == player_id).one()
+
+        games_played = DBSession.query(
+                Game.game_type_cd, func.count(), func.sum(PlayerGameStat.alivetime)).\
+                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_stats = {}
+        total_stats['games'] = 0
+        total_stats['games_breakdown'] = {}  # this is a dictionary inside a dictionary .. dictception?
+        total_stats['games_alivetime'] = {}
+        total_stats['gametypes'] = []
+        for (game_type_cd, games, alivetime) in games_played:
+            total_stats['games'] += games
+            total_stats['gametypes'].append(game_type_cd)
+            total_stats['games_breakdown'][game_type_cd] = games
+            total_stats['games_alivetime'][game_type_cd] = alivetime
+
+        (total_stats['kills'], total_stats['deaths'], total_stats['alivetime'],) = DBSession.query(
+                func.sum(PlayerGameStat.kills),
+                func.sum(PlayerGameStat.deaths),
+                func.sum(PlayerGameStat.alivetime)).\
+                filter(PlayerGameStat.player_id == player_id).\
+                one()
+
+        (total_stats['wins'], total_stats['losses']) = DBSession.\
+                query("wins", "losses").\
+                from_statement(
+                    "SELECT SUM(win) wins, SUM(loss) losses "
+                    "FROM   (SELECT  g.game_id, "
+                    "                CASE "
+                    "                  WHEN g.winner = pgs.team THEN 1 "
+                    "                  WHEN pgs.rank = 1 THEN 1 "
+                    "                  ELSE 0 "
+                    "                END win, "
+                    "                CASE "
+                    "                  WHEN g.winner = pgs.team THEN 0 "
+                    "                  WHEN pgs.rank = 1 THEN 0 "
+                    "                  ELSE 1 "
+                    "                END loss "
+                    "        FROM    games g, "
+                    "                player_game_stats pgs "
+                    "        WHERE   g.game_id = pgs.game_id "
+                    "                AND pgs.player_id = :player_id) win_loss").\
+                params(player_id=player_id).one()
+
+        ranks = DBSession.query("game_type_cd", "rank", "max_rank").\
+                from_statement(
+                    "SELECT  pr.game_type_cd, pr.rank, overall.max_rank "
+                    "FROM    player_ranks pr, "
+                    "        (SELECT  game_type_cd, max(rank) max_rank "
+                    "        FROM     player_ranks "
+                    "        GROUP BY game_type_cd) overall "
+                    "WHERE   pr.game_type_cd = overall.game_type_cd  "
+                    "        AND player_id = :player_id "
+                    "ORDER BY rank").\
+                params(player_id=player_id).all()
+
+        ranks_dict = {}
+        for gtc,rank,max_rank in ranks:
+            ranks_dict[gtc] = (rank, max_rank)
+
+        elos = DBSession.query(PlayerElo).\
+                filter_by(player_id=player_id).\
+                order_by(PlayerElo.elo.desc()).\
+                all()
+
+        elos_dict = {}
+        for elo in elos:
+            if elo.games >= 32:
+                elos_dict[elo.game_type_cd] = elo.elo
+
+        self.data = {
+                'player':player,
+                'total_stats':total_stats,
+                'ranks':ranks_dict,
+                'elos':elos_dict,
+            }
+
diff --git a/xonstat/batch/badges/skin.py b/xonstat/batch/badges/skin.py
new file mode 100644 (file)
index 0000000..399bb79
--- /dev/null
@@ -0,0 +1,481 @@
+import math
+import re
+import zlib, struct
+import cairo as C
+from colorsys import rgb_to_hls, hls_to_rgb
+from xonstat.util import strip_colors, qfont_decode, _all_colors
+
+# similar to html_colors() from util.py
+_contrast_threshold = 0.5
+
+# standard colorset (^0 ... ^9)
+_dec_colors = [ (0.5,0.5,0.5),
+                (1.0,0.0,0.0),
+                (0.2,1.0,0.0),
+                (1.0,1.0,0.0),
+                (0.2,0.4,1.0),
+                (0.2,1.0,1.0),
+                (1.0,0.2,0.4),
+                (1.0,1.0,1.0),
+                (0.6,0.6,0.6),
+                (0.5,0.5,0.5)
+            ]
+
+
+# function to write compressed PNG (using zlib)
+def write_png(filename, buf, width, height):
+    width_byte_4 = width * 4
+    # fix color ordering (BGRA -> RGBA)
+    for byte in xrange(width*height):
+        pos = byte * 4
+        buf[pos:pos+4] = buf[pos+2] + buf[pos+1] + buf[pos+0] + buf[pos+3]
+    raw_data = b"".join(b'\x00' + buf[span:span + width_byte_4] for span in range(0, (height-1) * width * 4 + 1, width_byte_4))
+    def png_pack(png_tag, data):
+        chunk_head = png_tag + data
+        return struct.pack("!I", len(data)) + chunk_head + struct.pack("!I", 0xFFFFFFFF & zlib.crc32(chunk_head))
+    data = b"".join([
+        b'\x89PNG\r\n\x1a\n',
+        png_pack(b'IHDR', struct.pack("!2I5B", width, height, 8, 6, 0, 0, 0)),
+        png_pack(b'IDAT', zlib.compress(raw_data, 9)),
+        png_pack(b'IEND', b'')])
+    f = open(filename, "wb")
+    try:
+        f.write(data)
+    finally:
+        f.close()
+
+
+class Skin:
+
+    # skin parameters, can be overriden by init
+    params = {}
+
+    # skin name
+    name = ""
+
+    # render context
+    ctx = None
+
+    def __init__(self, name, **params):
+        # default parameters
+        self.name = name
+        self.params = {
+            'bg':               None,           # None - plain; otherwise use given texture
+            'bgcolor':          None,           # transparent bg when bgcolor==None
+            'overlay':          None,           # add overlay graphic on top of bg
+            'font':             "Xolonium",
+            'width':            560,
+            'height':           70,
+            'nick_fontsize':    20,
+            'nick_pos':         (56,18),
+            'nick_maxwidth':    280,
+            'gametype_fontsize':10,
+            'gametype_pos':     (101,33),
+            'gametype_width':   94,
+            'gametype_height':  0,
+            'gametype_color':   (0.9, 0.9, 0.9),
+            'gametype_text':    "%s",
+            'gametype_align':   0,
+            'gametype_upper':   True,
+            'num_gametypes':    3,
+            'nostats_fontsize': 12,
+            'nostats_pos':      (101,59),
+            'nostats_color':    (0.8, 0.2, 0.1),
+            'nostats_angle':    -10,
+            'nostats_text':     "no stats yet!",
+            'nostats_align':    0,
+            'elo_pos':          (101,47),
+            'elo_fontsize':     10,
+            'elo_color':        (1.0, 1.0, 0.5),
+            'elo_text':         "Elo %.0f",
+            'elo_align':        0,
+            'rank_fontsize':    8,
+            'rank_pos':         (101,58),
+            'rank_color':       (0.8, 0.8, 1.0),
+            'rank_text':        "Rank %d of %d",
+            'rank_align':       0,
+            'wintext_fontsize': 10,
+            'wintext_pos':      (508,3),
+            'wintext_color':    (0.8, 0.8, 0.8),
+            'wintext_text':     "Win Percentage",
+            'wintext_align':    0,
+            'winp_fontsize':    12,
+            'winp_pos':         (508,19),
+            'winp_colortop':    (0.2, 1.0, 1.0),
+            'winp_colormid':    (0.4, 0.8, 0.4),
+            'winp_colorbot':    (1.0, 1.0, 0.2),
+            'winp_align':       0,
+            'wins_fontsize':    8,
+            'wins_pos':         (508,33),
+            'wins_color':       (0.6, 0.8, 0.8),
+            'wins_align':       0,
+            'loss_fontsize':    8,
+            'loss_pos':         (508,43),
+            'loss_color':       (0.8, 0.8, 0.6),
+            'loss_align':       0,
+            'kdtext_fontsize':  10,
+            'kdtext_pos':       (390,3),
+            'kdtext_width':     102,
+            'kdtext_color':     (0.8, 0.8, 0.8),
+            'kdtext_bg':        (0.8, 0.8, 0.8, 0.1),
+            'kdtext_text':      "Kill Ratio",
+            'kdtext_align':     0,
+            'kdr_fontsize':     12,
+            'kdr_pos':          (392,19),
+            'kdr_colortop':     (0.2, 1.0, 0.2),
+            'kdr_colormid':     (0.8, 0.8, 0.4),
+            'kdr_colorbot':     (1.0, 0.2, 0.2),
+            'kdr_align':        0,
+            'kills_fontsize':   8,
+            'kills_pos':        (392,33),
+            'kills_color':      (0.6, 0.8, 0.6),
+            'kills_align':      0,
+            'deaths_fontsize':  8,
+            'deaths_pos':       (392,43),
+            'deaths_color':     (0.8, 0.6, 0.6),
+            'deaths_align':     0,
+            'ptime_fontsize':   10,
+            'ptime_pos':        (451,60),
+            'ptime_color':      (0.1, 0.1, 0.1),
+            'ptime_text':       "Playing Time: %s",
+            'ptime_align':      0,
+        }
+        
+        for k,v in params.items():
+            if self.params.has_key(k):
+                self.params[k] = v
+
+    def __str__(self):
+        return self.name
+
+    def __getattr__(self, key):
+        if self.params.has_key(key):
+            return self.params[key]
+        return None
+
+    def show_text(self, txt, pos, align=0, angle=None, offset=(0,0)):
+        ctx = self.ctx
+
+        xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
+        if align > 0:
+            ctx.move_to(pos[0]+offset[0]-xoff,      pos[1]+offset[1]-yoff)
+        elif align < 0:
+            ctx.move_to(pos[0]+offset[0]-xoff-tw,   pos[1]+offset[1]-yoff)
+        else:
+            ctx.move_to(pos[0]+offset[0]-xoff-tw/2, pos[1]+offset[1]-yoff)
+        ctx.save()
+        if angle:
+            ctx.rotate(math.radians(angle))
+        ctx.show_text(txt)
+        ctx.restore()
+
+    def set_font(self, fontsize, color, bold=False, italic=False):
+        ctx    = self.ctx
+        font   = self.font
+        slant  = C.FONT_SLANT_ITALIC if italic else C.FONT_SLANT_NORMAL
+        weight = C.FONT_WEIGHT_BOLD  if bold   else C.FONT_WEIGHT_NORMAL
+
+        ctx.select_font_face(font, slant, weight)
+        ctx.set_font_size(fontsize)
+        if len(color) == 1:
+            ctx.set_source_rgb(color[0], color[0], color[0])
+        elif len(color) == 3:
+            ctx.set_source_rgb(color[0], color[1], color[2])
+        elif len(color) == 4:
+            ctx.set_source_rgba(color[0], color[1], color[2], color[3])
+        else:
+            ctx.set_source_rgb(1, 1, 1)
+
+    def render_image(self, data, output_filename):
+        """Render an image for the given player id."""
+
+        # setup variables
+
+        player          = data.player
+        elos            = data.elos
+        ranks           = data.ranks
+        #games           = data.total_stats['games']
+        wins, losses    = data.total_stats['wins'], data.total_stats['losses']
+        games           = wins + losses
+        kills, deaths   = data.total_stats['kills'], data.total_stats['deaths']
+        alivetime       = data.total_stats['alivetime']
+
+
+        # build image
+
+        surf = C.ImageSurface(C.FORMAT_ARGB32, self.width, self.height)
+        ctx = C.Context(surf)
+        self.ctx = ctx
+        ctx.set_antialias(C.ANTIALIAS_GRAY)
+        
+        # draw background
+        if self.bg == None:
+            if self.bgcolor != None:
+                # plain fillcolor, full transparency possible with (1,1,1,0)
+                ctx.save()
+                ctx.set_operator(C.OPERATOR_SOURCE)
+                ctx.rectangle(0, 0, self.width, self.height)
+                ctx.set_source_rgba(self.bgcolor[0], self.bgcolor[1], self.bgcolor[2], self.bgcolor[3])
+                ctx.fill()
+                ctx.restore()
+        else:
+            try:
+                # background texture
+                bg = C.ImageSurface.create_from_png("img/%s.png" % self.bg)
+                
+                # tile image
+                if bg:
+                    bg_w, bg_h = bg.get_width(), bg.get_height()
+                    bg_xoff = 0
+                    while bg_xoff < self.width:
+                        bg_yoff = 0
+                        while bg_yoff < self.height:
+                            ctx.set_source_surface(bg, bg_xoff, bg_yoff)
+                            #ctx.mask_surface(bg)
+                            ctx.paint()
+                            bg_yoff += bg_h
+                        bg_xoff += bg_w
+            except:
+                #print "Error: Can't load background texture: %s" % self.bg
+                pass
+
+        # draw overlay graphic
+        if self.overlay != None:
+            try:
+                overlay = C.ImageSurface.create_from_png("img/%s.png" % self.overlay)
+                ctx.set_source_surface(overlay, 0, 0)
+                #ctx.mask_surface(overlay)
+                ctx.paint()
+            except:
+                #print "Error: Can't load overlay texture: %s" % self.overlay
+                pass
+
+
+        ## draw player's nickname with fancy colors
+        
+        # deocde nick, strip all weird-looking characters
+        qstr = qfont_decode(player.nick).replace('^^', '^').replace(u'\x00', '')
+        chars = []
+        for c in qstr:
+            # replace weird characters that make problems - TODO
+            if ord(c) < 128:
+                chars.append(c)
+        qstr = ''.join(chars)
+        stripped_nick = strip_colors(qstr.replace(' ', '_'))
+        
+        # fontsize is reduced if width gets too large
+        ctx.select_font_face(self.font, C.FONT_SLANT_NORMAL, C.FONT_WEIGHT_NORMAL)
+        shrinknick = 0
+        while shrinknick < 0.6*fontsize:
+            ctx.set_font_size(self.nick_fontsize - shrinknick)
+            xoff, yoff, tw, th = ctx.text_extents(stripped_nick)[:4]
+            if tw > self.nick_maxwidth:
+                shrinknick += 1
+                continue
+            break
+
+        # determine width of single whitespace for later use
+        xoff, yoff, tw, th = ctx.text_extents("_")[:4]
+        space_w = tw
+
+        # split nick into colored segments
+        xoffset = 0
+        _all_colors = re.compile(r'(\^\d|\^x[\dA-Fa-f]{3})')
+        parts = _all_colors.split(qstr)
+        while len(parts) > 0:
+            tag = None
+            txt = parts[0]
+            if _all_colors.match(txt):
+                tag = txt[1:]  # strip leading '^'
+                if len(parts) < 2:
+                    break
+                txt = parts[1]
+                del parts[1]
+            del parts[0]
+                
+            if not txt or len(txt) == 0:
+                # only colorcode and no real text, skip this
+                continue
+            
+            if tag:
+                if tag.startswith('x'):
+                    r = int(tag[1] * 2, 16) / 255.0
+                    g = int(tag[2] * 2, 16) / 255.0
+                    b = int(tag[3] * 2, 16) / 255.0
+                    hue, light, satur = rgb_to_hls(r, g, b)
+                    if light < _contrast_threshold:
+                        light = _contrast_threshold
+                        r, g, b = hls_to_rgb(hue, light, satur)
+                else:
+                    r,g,b = _dec_colors[int(tag[0])]
+            else:
+                r,g,b = _dec_colors[7]
+
+            xoff, yoff, tw, th = ctx.text_extents(txt)[:4]
+            ctx.set_source_rgb(r, g, b)
+            ctx.move_to(self.nick_pos[0] + xoffset - xoff, self.nick_pos[1])
+            ctx.show_text(txt)
+
+            tw += (len(txt)-len(txt.strip())) * space_w  # account for lost whitespaces
+            xoffset += tw + 2
+
+        ## print elos and ranks
+        
+        xoffset, yoffset = 0, 0
+        count = 0
+        for gt in data.total_stats['gametypes'][:self.num_gametypes]:
+            if not elos.has_key(gt) or not ranks.has_key(gt):
+                continue
+            count += 1
+        
+        # re-align segments if less than max. gametypes are shown
+        if count > 0:
+            if count < self.num_gametypes:
+                diff = self.num_gametypes - count
+                if diff % 2 == 0:
+                    xoffset += (diff-1) * self.gametype_width
+                    yoffset += (diff-1) * self.gametype_height
+                else:
+                    xoffset += 0.5 * diff * self.gametype_width
+                    yoffset += 0.5 * diff * self.gametype_height
+        
+            # show a number gametypes the player has participated in
+            for gt in data.total_stats['gametypes'][:self.num_gametypes]:
+                if not elos.has_key(gt) or not ranks.has_key(gt):
+                    continue
+
+                offset = (xoffset, yoffset)
+                if self.gametype_pos:
+                    if self.gametype_upper:
+                        txt = self.gametype_text % gt.upper()
+                    else:
+                        txt = self.gametype_text % gt.lower()
+                    self.set_font(self.gametype_fontsize, self.gametype_color, bold=True)
+                    self.show_text(txt, self.gametype_pos, self.gametype_align, offset=offset)
+
+                if self.elo_pos:
+                    txt = self.elo_text % round(elos[gt], 0)
+                    self.set_font(self.elo_fontsize, self.elo_color)
+                    self.show_text(txt, self.elo_pos, self.elo_align, offset=offset)
+                if  self.rank_pos:
+                    txt = self.rank_text % ranks[gt]
+                    self.set_font(self.rank_fontsize, self.rank_color)
+                    self.show_text(txt, self.rank_pos, self.rank_align, offset=offset)
+
+                xoffset += self.gametype_width
+                yoffset += self.gametype_height
+        else:
+            if self.nostats_pos:
+                xoffset += (self.num_gametypes-2) * self.gametype_width
+                yoffset += (self.num_gametypes-2) * self.gametype_height
+                offset = (xoffset, yoffset)
+
+                txt = self.nostats_text
+                self.set_font(self.nostats_fontsize, self.nostats_color, bold=True)
+                self.show_text(txt, self.nostats_pos, self.nostats_align, angle=self.nostats_angle, offset=offset)
+
+
+        # print win percentage
+
+        if self.wintext_pos:
+            txt = self.wintext_text
+            self.set_font(self.wintext_fontsize, self.wintext_color)
+            self.show_text(txt, self.wintext_pos, self.wintext_align)
+
+        txt = "???"
+        try:
+            ratio = float(wins)/games
+            txt = "%.2f%%" % round(ratio * 100, 2)
+        except:
+            ratio = 0
+        
+        if self.winp_pos:
+            if ratio >= 0.5:
+                nr = 2*(ratio-0.5)
+                r = nr*self.winp_colortop[0] + (1-nr)*self.winp_colormid[0]
+                g = nr*self.winp_colortop[1] + (1-nr)*self.winp_colormid[1]
+                b = nr*self.winp_colortop[2] + (1-nr)*self.winp_colormid[2]
+            else:
+                nr = 2*ratio
+                r = nr*self.winp_colormid[0] + (1-nr)*self.winp_colorbot[0]
+                g = nr*self.winp_colormid[1] + (1-nr)*self.winp_colorbot[1]
+                b = nr*self.winp_colormid[2] + (1-nr)*self.winp_colorbot[2]
+            self.set_font(self.winp_fontsize, (r,g,b), bold=True)
+            self.show_text(txt, self.winp_pos, self.winp_align)
+
+        if self.wins_pos:
+            txt = "%d win" % wins
+            if wins != 1:
+                txt += "s"
+            self.set_font(self.wins_fontsize, self.wins_color)
+            self.show_text(txt, self.wins_pos, self.wins_align)
+
+        if self.loss_pos:
+            txt = "%d loss" % losses
+            if losses != 1:
+                txt += "es"
+            self.set_font(self.loss_fontsize, self.loss_color)
+            self.show_text(txt, self.loss_pos, self.loss_align)
+
+
+        # print kill/death ratio
+
+        if self.kdtext_pos:
+            txt = self.kdtext_text
+            self.set_font(self.kdtext_fontsize, self.kdtext_color)
+            self.show_text(txt, self.kdtext_pos, self.kdtext_align)
+        
+        txt = "???"
+        try:
+            ratio = float(kills)/deaths
+            txt = "%.3f" % round(ratio, 3)
+        except:
+            ratio = 0
+
+        if self.kdr_pos:
+            if ratio >= 1.0:
+                nr = ratio-1.0
+                if nr > 1:
+                    nr = 1
+                r = nr*self.kdr_colortop[0] + (1-nr)*self.kdr_colormid[0]
+                g = nr*self.kdr_colortop[1] + (1-nr)*self.kdr_colormid[1]
+                b = nr*self.kdr_colortop[2] + (1-nr)*self.kdr_colormid[2]
+            else:
+                nr = ratio
+                r = nr*self.kdr_colormid[0] + (1-nr)*self.kdr_colorbot[0]
+                g = nr*self.kdr_colormid[1] + (1-nr)*self.kdr_colorbot[1]
+                b = nr*self.kdr_colormid[2] + (1-nr)*self.kdr_colorbot[2]
+            self.set_font(self.kdr_fontsize, (r,g,b), bold=True)
+            self.show_text(txt, self.kdr_pos, self.kdr_align)
+
+        if self.kills_pos:
+            txt = "%d kill" % kills
+            if kills != 1:
+                txt += "s"
+            self.set_font(self.kills_fontsize, self.kills_color)
+            self.show_text(txt, self.kills_pos, self.kills_align)
+
+        if self.deaths_pos:
+            txt = ""
+            if deaths is not None:
+                txt = "%d death" % deaths
+                if deaths != 1:
+                    txt += "s"
+            self.set_font(self.deaths_fontsize, self.deaths_color)
+            self.show_text(txt, self.deaths_pos, self.deaths_align)
+
+
+        # print playing time
+
+        if self.ptime_pos:
+            txt = self.ptime_text % str(alivetime)
+            self.set_font(self.ptime_fontsize, self.ptime_color)
+            self.show_text(txt, self.ptime_pos, self.ptime_align)
+
+
+        # save to PNG
+        #surf.write_to_png(output_filename)
+        surf.flush()
+        imgdata = surf.get_data()
+        write_png(output_filename, imgdata, self.width, self.height)
+
diff --git a/xonstat/batch/badges/templates/badge.html b/xonstat/batch/badges/templates/badge.html
deleted file mode 100644 (file)
index 9c6e11f..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-<html>
-  <head>
-    <link href="../css/style.css" rel="stylesheet">
-  </head>
-  <body>
-    <div id="badge">
-      <div id="nick">
-        <b>
-        <span style="color:rgb(0,255,0)">print('<span style="color:rgb(255,0,0)">Anti<span style="color:rgb(127,127,127)">body<span style="color:rgb(0,255,0)">')<span style='color:rgb(255,255,255)'></span></span></span></span></span>
-      </b>
-      </div>
-
-      <div id="games_played">
-        218 games (144 duel, 41 ctf, 31 dm, 2 tdm)
-      </div>
-
-      <div id="win_percentage">
-        111 wins, 107 losses (50.92%)
-      </div>
-
-      <div id="kill_ratio">
-        3084 kills, 2519 deaths (1.224)
-      </div>
-
-      <div id="elo">
-        Elo: 375.091 (ctf), 358.604 (dm)
-      </div>
-    </div>
-
-  </body>
-</html>
diff --git a/xonstat/batch/badges/templates/badge.mako b/xonstat/batch/badges/templates/badge.mako
deleted file mode 100644 (file)
index e43089e..0000000
+++ /dev/null
@@ -1,39 +0,0 @@
-<html>
-  <head>
-    <link href="../css/style.css" rel="stylesheet">
-  </head>
-  <body>
-    <div id="badge">
-      <div id="nick">
-        <b>
-          ${player.nick_html_colors()|n}
-        </b>
-      </div>
-
-      <div id="games_played">
-        <% games_breakdown_str = ', '.join(["{0} {1}".format(ng, gt) for (gt, ng) in games_breakdown]) %>
-        ${total_games} (${games_breakdown_str})
-      </div>
-
-      <div id="win_percentage">
-        % if total_games > 0 and total_stats['wins'] is not None:
-          ${total_stats['wins']} wins, ${total_games - total_stats['wins']} losses (${round(float(total_stats['wins'])/total_games * 100, 2)}%)
-        % endif
-      </div>
-
-      <div id="kill_ratio">
-        % if total_stats['kills'] > 0 and total_stats['deaths'] > 0:
-          ${total_stats['kills']} kills, ${total_stats['deaths']} deaths (${round(float(total_stats['kills'])/total_stats['deaths'], 3)})
-        % endif
-      </div>
-
-      <div id="elo">
-        % if elos_display is not None and len(elos_display) > 0:
-         Elo: ${', '.join(elos_display[0:2])}
-       % endif
-      </div>
-
-    </div>
-
-  </body>
-</html>
index 84a64ea9a665fc6c9d49e52f12c041d8d48bca17..bbf8bf416009a1bedea531fcb3fbd0690a65cf1c 100644 (file)
@@ -3,6 +3,7 @@ import logging
 import math
 import sqlalchemy
 import sqlalchemy.sql.functions as sfunc
+from calendar import timegm
 from datetime import timedelta
 from sqlalchemy.orm import mapper
 from sqlalchemy.orm import scoped_session
@@ -38,6 +39,9 @@ class Player(object):
     def to_dict(self):
         return {'player_id':self.player_id, 'name':self.nick.encode('utf-8')}
 
+    def epoch(self):
+        return timegm(self.create_dt.timetuple())
+
 
 class GameType(object):
     def __repr__(self):
@@ -67,6 +71,12 @@ class Server(object):
     def to_dict(self):
         return {'server_id':self.server_id, 'name':self.name.encode('utf-8')}
 
+    def fuzzy_date(self):
+        return pretty_date(self.create_dt)
+
+    def epoch(self):
+        return timegm(self.create_dt.timetuple())
+
 
 class Map(object):
     def __init__(self, name=None):
@@ -78,6 +88,12 @@ class Map(object):
     def to_dict(self):
         return {'map_id':self.map_id, 'name':self.name, 'version':self.version}
 
+    def fuzzy_date(self):
+        return pretty_date(self.create_dt)
+
+    def epoch(self):
+        return timegm(self.create_dt.timetuple())
+
 
 class Game(object):
     def __init__(self, game_id=None, start_dt=None, game_type_cd=None, 
@@ -98,6 +114,9 @@ class Game(object):
     def fuzzy_date(self):
         return pretty_date(self.start_dt)
 
+    def epoch(self):
+        return timegm(self.start_dt.timetuple())
+
 
 class PlayerGameStat(object):
     def __init__(self, player_game_stat_id=None, create_dt=None):
@@ -152,6 +171,13 @@ class PlayerAchievement(object):
 
 
 class PlayerWeaponStat(object):
+    def __init__(self):
+        self.fired = 0
+        self.max = 0
+        self.hit = 0
+        self.actual = 0
+        self.frags = 0
+
     def __repr__(self):
         return "<PlayerWeaponStat(%s, %s, %s)>" % (self.player_weapon_stats_id, self.player_id, self.game_id)
 
diff --git a/xonstat/static/css/img/web_background_2.jpg b/xonstat/static/css/img/web_background_2.jpg
new file mode 100644 (file)
index 0000000..870773f
Binary files /dev/null and b/xonstat/static/css/img/web_background_2.jpg differ
index 0e7ff885bd283acd31a2451f6ebd3aa2b1b7374d..d87b5bf5b43d0309645839d5bb4b0462706dde4f 100755 (executable)
@@ -107,7 +107,7 @@ textarea {
 }
 body {
   background-color: #000000;
-  background:#000 url('img/web_background.png') 0 0 no-repeat;
+  background:#000 url('img/web_background_2.jpg') 0 0 no-repeat;
   background-size: 100%;
   color: #d0d0d0;
   font-family: "XoloniumNormal", "Helvetica Neue", Helvetica, Arial, sans-serif;
@@ -3356,7 +3356,7 @@ a.thumbnail:hover {
   color: #ffffff;
 }
 .hero-unit {
-  margin-bottom: 30px;
+  margin-top: -15px;
   -webkit-border-radius: 6px;
   -moz-border-radius: 6px;
   border-radius: 6px;
@@ -3395,21 +3395,20 @@ a.thumbnail:hover {
   top: -25px;
 }
 #xonborder {
-  //border-width: 56px 56px 56px;
-  -moz-border-image: url(img/web_border.png) 85 85 85 stretch;
-  -webkit-border-image: url(img/web_border.png) 72 85 85 stretch;
-  -o-border-image: url(img/web_border.png) 96 96 96 stretch;
-  background-color: #000000;
-  border-image: url(img/web_border.png) 96 96 96 stretch;
-  border-width: 40px;
-  left: -40px;
-  position: relative;
+  background: rgb(0, 0, 0); /* IE Fallback */
+  background: none repeat scroll 0 0 rgba(0, 0, 0, 0.4);
+  border-radius: 6px 6px 6px 6px;
+  margin-bottom: 30px;
+  padding: 20px;
 }
 #title {
-    font-size: 16px;
+    color: #0088CC;
+    font-size: 30px;
+    font-style: italic;
+    margin-bottom: 25px;
     position: relative;
-    top: -35px;
     text-align: center;
+    text-shadow: 2px 2px 3px #333;
 }
 #navsearch {
   float: right;
index 53526091cafe7ac0c7ef3494e5a8e3189d66a217..3947174e39183162305ea6e070d6377563167c99 100644 (file)
@@ -39,7 +39,7 @@
 
       <div class="row">
         <div class="span12" id="xonborder">
-          <div id="title"><%block name="title"></%block></div>
+          <div id="title"><%block name="title"></%block>&nbsp;</div>
             ${self.body()}
         </div> <!-- /xonborder -->
       </div> <!-- /main row -->
       </%block>
 
       <%block name="js">
+      <script src="/static/js/jquery-1.7.1.min.js"></script>
       </%block>
 
+      <!-- RELATIVE TIME CONVERSION -->
+      <script type="text/javascript">
+      $('.abstime').each(function(i,e){
+        var epoch = e.getAttribute('data-epoch');
+        var d = new Date(0);
+        d.setUTCSeconds(epoch);
+        e.setAttribute('title', d.toDateString() + ' ' + d.toTimeString());  
+      });
+      </script>
+
+      <!-- GOOGLE ANALYTICS -->
       <script type="text/javascript">
       var _gaq = _gaq || [];
       _gaq.push(['_setAccount', 'UA-30391685-1']);
index 67446bc079a9705d6179aa92bc754d0b7e6e905c..141f3a9c65826cd435ff4068094a5f3e42b22fe3 100644 (file)
@@ -31,7 +31,7 @@ Game Information
   <div class="span6">
     <h2>Game Detail</h2>
     <p>
-      Played on: ${game.start_dt.strftime('%m/%d/%Y at %I:%M %p')}<br />
+      Played: <span class="abstime" data-epoch="${game.epoch()}" title="${game.start_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${game.fuzzy_date()}</span><br />
       Game Type: ${game.game_type_cd}<br />
       Server: <a href="${request.route_url("server_info", id=server.server_id)}" name="Server info page for ${server.name}">${server.name}</a><br />
       Map: <a href="${request.route_url("map_info", id=map.map_id)}" name="Map info page for ${map.name}">${map.name}</a><br />
index c731a0eecdef44d7e7f8e52a2cc85fb7e3159400..ca165e46c6fd3c80243831ad02411b649a27a293 100644 (file)
@@ -9,7 +9,6 @@ Leaderboard
         <img src="/static/css/img/web_background_l2.png" />
         #####<p id="statline">Tracking <a href="#">12345</a> players, <a href="#">12345</a> games (<a href="#">123</a> duels, <a href="#">123</a> ctfs, <a href="#">123</a> dms), <a href="#">12345</a> servers, and <a href="#">12345</a> maps since November 2011.</p>
         <p id="statline">Tracking Xonotic statistics since October 2011.</p>
-        <p><a class="btn btn-primary btn-large" href="http://www.xonotic.org/download" title="Download Xonotic">Get the game &raquo;</a></p>
       </div>
 </%block>
 
@@ -226,7 +225,7 @@ Leaderboard
           <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><span class="abstime" data-epoch="${game.epoch()}" title="${game.start_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${game.fuzzy_date()}</span></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>
index bb297ee6a20f3f0462e4e5642a2f1d949d9ae553..ba5f5945cf393e1d0d333c7a031b0349a7977c52 100644 (file)
@@ -29,7 +29,7 @@ Map Index
     % 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><span class="abstime" data-epoch="${map.epoch()}" title="${map.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${map.fuzzy_date()}</span></td>
     </td>
       </tr>
     % endfor
index 7e975c152221b0b9baa0094b1fec5296dc634560..a16e92087385e8192371d9e18ab1e9d6f009ff0a 100644 (file)
@@ -21,7 +21,7 @@ ${parent.title()}
 % else:
 <h2>${gmap.name}</h2>
 <p>
-  Added on ${gmap.create_dt.strftime('%m/%d/%Y at %H:%M')}
+  Added <span class="abstime" data-epoch="${gmap.epoch()}" title="${gmap.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${gmap.fuzzy_date()}</span>
 </p>
 <div class="row">
   <div class="span4">
@@ -126,7 +126,7 @@ ${parent.title()}
         <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>${game.start_dt.strftime('%m/%d/%Y %H:%M')}</td>
+          <td><span class="abstime" data-epoch="${game.epoch()}" title="${game.start_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${game.fuzzy_date()}</span></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>
index 2ef7d5c481c4a164588be1e1cdf7689d7c7de02a..320437b8e3bbb8af3a3c1f9aae501173a0457960 100644 (file)
@@ -29,7 +29,7 @@ Player Index
     % 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>
+        <td><span class="abstime" data-epoch="${player.epoch()}" title="${player.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${player.joined_pretty_date()}</span></th>
       </tr>
     % endfor
     </table>
index e7b04058691832d2ddf84c352f3b0d8eb635ae33..c88c96c27c924cf3511fbc72ec9b5a7fc6abd033 100644 (file)
@@ -191,7 +191,7 @@ Player Information
     <p>
       Member Since: <small>${player.create_dt.strftime('%m/%d/%Y at %I:%M %p')} </small><br />
 
-      Last Seen: <small>${recent_games[0][1].fuzzy_date()} </small><br />
+      Last Seen: <small><span class="abstime" data-epoch="${recent_games[0][1].epoch()}" title="${recent_games[0][1].create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${recent_games[0][1].fuzzy_date()}</span> </small><br />
 
       Playing Time: <small>${total_stats['alivetime']}
       % if total_stats['alivetime_month'] and total_stats['alivetime'] > total_stats['alivetime_month']:
@@ -589,7 +589,7 @@ Player Information
             % endif
           % endif
            </td>
-           <td>${game.fuzzy_date()}</td>
+           <td><span class="abstime" data-epoch="${game.epoch()}" title="${game.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${game.fuzzy_date()}</span></td>
         </tr>
       % endfor
       </tbody>
index a7d54547f9947b5dba33df6d3fe7033f0018b8f8..2b9fb0c9d92150fc19fd4e1d05e0e27380082029 100644 (file)
@@ -47,7 +47,7 @@
     % for player in results:
     <tr>
         <td><a href="${request.route_url("player_info", id=player.player_id)}" name="Player info page for player #${player.player_id}">${player.nick_html_colors()|n}</a></td>
-        <td>${player.joined_pretty_date()}</td>
+        <td><span class="abstime" data-epoch="${player.epoch()}" title="${player.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${player.joined_pretty_date()}</span></td>
     </tr>
     % endfor
 </table>
@@ -63,7 +63,7 @@
     % for server in results:
     <tr>
         <td><a href="${request.route_url("server_info", id=server.server_id)}" name="Server info page for server #${server.server_id}">${server.name}</a></td>
-        <td>${server.create_dt.strftime('%m/%d/%Y at %I:%M %p')}</td>
+        <td><span class="abstime" data-epoch="${server.epoch()}" title="${server.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${server.fuzzy_date()}</span></td>
     </tr>
     % endfor
 </table>
@@ -79,7 +79,7 @@
     % for map in results:
     <tr>
         <td><a href="${request.route_url("map_info", id=map.map_id)}" name="Map info page for map #${map.map_id}">${map.name}</a></td>
-        <td>${map.create_dt.strftime('%m/%d/%Y at %I:%M %p')}</td>
+        <td><span class="abstime" data-epoch="${map.epoch()}" title="${map.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${map.fuzzy_date()}</span></td>
     </tr>
     % endfor
 </table>
         <th></th>
         <th>Map</th>
         <th>Server</th>
-        <th>Played On</th>
+        <th>Time</th>
     </tr>
     % for (game, server, gmap) in results:
     <tr>
         <td><a class="btn btn-primary btn-small" href="${request.route_url("game_info", id=game.game_id)}" name="Game info page for game #${game.game_id}">View</a></td>
         <td><a href="${request.route_url("map_info", id=gmap.map_id)}" name="Map info page for map #${gmap.map_id}">${gmap.name}</a></td>
         <td><a href="${request.route_url("server_info", id=server.server_id)}" name="Server info page for server #${server.server_id}">${server.name}</a></td>
-        <td>${game.create_dt.strftime('%m/%d/%Y at %I:%M %p')}</td>
+        <td><span class="abstime" data-epoch="${game.epoch()}" title="${game.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${game.fuzzy_date()}</span></td>
     </tr>
     % endfor
 </table>
index bc2b37843fdfdb91f73bb758f951b2aebfeb38d9..3a64f2f3d8fb72e84fa9da2e3b36d4096f34a2ba 100644 (file)
@@ -29,7 +29,7 @@ Server Index
     % 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>
+        <td><span class="abstime" data-epoch="${server.epoch()}" title="${server.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${server.fuzzy_date()}</span></td>
       </tr>
     % endfor
     </table>
index 2c8d4963b505bed9e0fefb5c2824b6ca6380ed15..04bf6b455d37065478b019c560e0a286ad698a60 100644 (file)
@@ -22,7 +22,7 @@ Server Information
     <p>
       IP Address: ${server.ip_addr} <br />
       Revision: ${server.revision} <br />
-      Added on ${server.create_dt.strftime('%m/%d/%Y at %I:%M %p')} <br />
+      Added <span class="abstime" data-epoch="${server.epoch()}" title="${server.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${server.fuzzy_date()}</span> <br />
     </p>
   </div>
 </div>
@@ -139,7 +139,7 @@ Server Information
           <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('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><span class="abstime" data-epoch="${game.epoch()}" title="${game.start_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${game.fuzzy_date()}</span></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>
index 1bea47c94d8100e3047464dc6601657d010d9de3..f9aa1d5c07f96135f47cfdc71d2972bb3b693ec1 100644 (file)
@@ -1,7 +1,7 @@
 import re
 from colorsys import rgb_to_hls, hls_to_rgb
 from cgi import escape as html_escape
-from datetime import datetime
+from datetime import datetime, timedelta
 
 # Map of special chars to ascii from Darkplace's console.c.
 _qfont_table = [
@@ -113,53 +113,41 @@ def page_url(page):
 
 
 def pretty_date(time=False):
-    """
-    Get a datetime object or a int() Epoch timestamp and return a
-    pretty string like 'an hour ago', 'Yesterday', '3 months ago',
-    'just now', etc
-    """
+    '''Returns a human-readable relative date.'''
     now = datetime.utcnow()
     if type(time) is int:
         diff = now - datetime.fromtimestamp(time)
     elif isinstance(time,datetime):
-        diff = now - time 
+        diff = now - time
     elif not time:
+        print "not a time value"
         diff = now - now
-    second_diff = diff.seconds
-    day_diff = diff.days
-
-    if day_diff < 0:
-        return ''
-
-    if day_diff == 0:
-        if second_diff < 10:
-            return "just now"
-        if second_diff < 60:
-            return str(second_diff) + " seconds ago"
-        if second_diff < 120:
-            return  "a minute ago"
-        if second_diff < 3600:
-            return str( second_diff / 60 ) + " minutes ago"
-        if second_diff < 7200:
-            return "an hour ago"
-        if second_diff < 86400:
-            return str( second_diff / 3600 ) + " hours ago"
-    if day_diff == 1:
-        return "Yesterday"
-    if day_diff < 7:
-        return str(day_diff) + " days ago"
-    if day_diff < 31:
-        if day_diff/7 == 1:
-            return "a week ago"
-        else:
-            return str(day_diff/7) + " weeks ago"
-    if day_diff < 365:
-        if day_diff/30 == 1:
-            return "a month ago"
-        else:
-            return str(day_diff/30) + " months ago"
+
+    dim = round(diff.seconds/60.0 + diff.days*1440.0)
+
+    if dim == 0:
+        return "less than a minute ago"
+    elif dim == 1:
+        return "1 minute ago"
+    elif dim >= 2 and dim <= 44:
+        return "{0} minutes ago".format(int(dim))
+    elif dim >= 45 and dim <= 89:
+        return "about 1 hour ago"
+    elif dim >= 90 and dim <= 1439:
+        return "about {0} hours ago".format(int(round(dim/60.0)))
+    elif dim >= 1440 and dim <= 2519:
+        return "1 day ago"
+    elif dim >= 2520 and dim <= 43199:
+        return "{0} days ago".format(int(round(dim/1440.0)))
+    elif dim >= 43200 and dim <= 86399:
+        return "about 1 month ago"
+    elif dim >= 86400 and dim <= 525599:
+        return "{0} months ago".format(int(round(dim/43200.0)))
+    elif dim >= 525600 and dim <= 655199:
+        return "about 1 year ago"
+    elif dim >= 655200 and dim <= 914399:
+        return "over 1 year ago"
+    elif dim >= 914400 and dim <= 1051199:
+        return "almost 2 years ago"
     else:
-        if day_diff/365 == 1:
-            return "a year ago"
-        else:
-            return str(day_diff/365) + " years ago"
+        return "about {0} years ago".format(int(round(dim/525600.0)))
index 78f02c94512301948da85053458c4d563f1726a8..95c83738df4384b438dbbca87532aec5a922d7d0 100644 (file)
@@ -101,7 +101,6 @@ def has_minimum_real_players(settings, player_events):
 
     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
 
@@ -156,7 +155,7 @@ def register_new_nick(session, player, new_nick):
     new_nick - the new nickname
     """
     # see if that nick already exists
-    stripped_nick = strip_colors(player.nick)
+    stripped_nick = strip_colors(qfont_decode(player.nick))
     try:
         player_nick = session.query(PlayerNick).filter_by(
             player_id=player.player_id, stripped_nick=stripped_nick).one()
@@ -172,7 +171,7 @@ def register_new_nick(session, player, new_nick):
 
     # We change to the new nick regardless
     player.nick = new_nick
-    player.stripped_nick = strip_colors(new_nick)
+    player.stripped_nick = strip_colors(qfont_decode(new_nick))
     session.add(player)
 
 
@@ -325,7 +324,7 @@ def get_or_create_player(session=None, hashkey=None, nick=None):
             # with a suffix added for uniqueness.
             if nick:
                 player.nick = nick[:128]
-                player.stripped_nick = strip_colors(nick[:128])
+                player.stripped_nick = strip_colors(qfont_decode(nick[:128]))
             else:
                 player.nick = "Anonymous Player #{0}".format(player.player_id)
                 player.stripped_nick = player.nick
@@ -362,8 +361,9 @@ def create_player_game_stat(session=None, player=None,
     #set game id from game record
     pgstat.game_id = game.game_id
 
-    # all games have a score
+    # all games have a score and every player has an alivetime
     pgstat.score = 0
+    pgstat.alivetime = datetime.timedelta(seconds=0)
 
     if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel':
         pgstat.kills = 0
@@ -378,7 +378,9 @@ def create_player_game_stat(session=None, player=None,
         pgstat.carrier_frags = 0
 
     for (key,value) in player_events.items():
-        if key == 'n': pgstat.nick = value[:128]
+        if key == 'n': 
+            pgstat.nick = value[:128]
+            pgstat.stripped_nick = strip_colors(qfont_decode(pgstat.nick))
         if key == 't': pgstat.team = int(value)
         if key == 'rank': pgstat.rank = int(value)
         if key == 'alivetime': 
@@ -416,7 +418,7 @@ def create_player_game_stat(session=None, player=None,
 
 
 def create_player_weapon_stats(session=None, player=None, 
-        game=None, pgstat=None, player_events=None):
+        game=None, pgstat=None, player_events=None, game_meta=None):
     """
     Creates accuracy records for each weapon used by a given player in a
     given game. Parameters:
@@ -427,9 +429,23 @@ def create_player_weapon_stats(session=None, player=None,
     pgstat - Corresponding PlayerGameStat record for these weapon stats
     player_events - dictionary containing the raw weapon values that need to be
         transformed
+    game_meta - dictionary of game metadata (only used for stats version info)
     """
     pwstats = []
 
+    # Version 1 of stats submissions doubled the data sent.
+    # To counteract this we divide the data by 2 only for
+    # POSTs coming from version 1.
+    try:
+        version = int(game_meta['V'])
+        if version == 1:
+            is_doubled = True
+            log.debug('NOTICE: found a version 1 request, halving the weapon stats...')
+        else:
+            is_doubled = False
+    except:
+        is_doubled = False
+
     for (key,value) in player_events.items():
         matched = re.search("acc-(.*?)-cnt-fired", key)
         if matched:
@@ -464,6 +480,13 @@ def create_player_weapon_stats(session=None, player=None,
                 pwstat.frags = int(round(float(
                         player_events['acc-' + weapon_cd + '-frags'])))
 
+            if is_doubled:
+                pwstat.fired = pwstat.fired/2
+                pwstat.max = pwstat.max/2
+                pwstat.hit = pwstat.hit/2
+                pwstat.actual = pwstat.actual/2
+                pwstat.frags = pwstat.frags/2
+
             session.add(pwstat)
             pwstats.append(pwstat)
 
@@ -520,7 +543,7 @@ def parse_body(request):
 
 
 def create_player_stats(session=None, player=None, game=None, 
-        player_events=None):
+        player_events=None, game_meta=None):
     """
     Creates player game and weapon stats according to what type of player
     """
@@ -531,7 +554,7 @@ def create_player_stats(session=None, player=None, game=None,
     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)
+            player_events=player_events, game_meta=game_meta)
 
 
 def stats_submit(request):
@@ -539,7 +562,8 @@ def stats_submit(request):
     Entry handler for POST stats submissions.
     """
     try:
-        session = DBSession()
+        # placeholder for the actual session
+        session = None
 
         log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
                 "----- END REQUEST BODY -----\n\n")
@@ -549,7 +573,7 @@ def stats_submit(request):
             log.debug("ERROR: Unverified request")
             raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
 
-        (game_meta, players) = parse_body(request)  
+        (game_meta, players) = parse_body(request)
 
         if not has_required_metadata(game_meta):
             log.debug("ERROR: Required game meta missing")
@@ -579,6 +603,12 @@ def stats_submit(request):
         except:
             revision = "unknown"
 
+        #----------------------------------------------------------------------
+        # This ends the "precondition" section of sanity checks. All
+        # functions not requiring a database connection go ABOVE HERE.
+        #----------------------------------------------------------------------
+        session = DBSession()
+
         server = get_or_create_server(session=session, hashkey=idfp, 
                 name=game_meta['S'], revision=revision,
                 ip_addr=get_remote_addr(request))
@@ -609,7 +639,7 @@ def stats_submit(request):
                     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)
+                        player_events=player_events, game_meta=game_meta)
 
         # update elos
         try:
@@ -621,5 +651,6 @@ def stats_submit(request):
         log.debug('Success! Stats recorded.')
         return Response('200 OK')
     except Exception as e:
-        session.rollback()
+        if session:
+            session.rollback()
         return e