pserve --reload development.ini #(or production.ini if you've configured that settings file instead)
-To get a Xonotic server configured to use this server, change the CVAR `g_playerstats_uri` to point to the correct host, port, and URL path. By default this is:
+To get a Xonotic server configured to use this server, change the CVAR `g_playerstats_gamereport_uri` to point to the correct host, port, and URL path. By default this is:
http://localhost:6543/stats/submit
...so in the server console (or in your config) you can put:
- set g_playerstats_uri http://localhost:6543/stats/submit
+ set g_playerstats_gamereport_uri http://localhost:6543/stats/submit
If you have any questions or issues please open up a bug report here, or - better yet ! - fork it and send me a pull request.
from xonstat.views import *
from xonstat.security import *
+
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
config.add_view(game_finder_json, route_name="game_index_json", renderer="jsonp")
# SERVER ROUTES
- config.add_route("server_index", "/servers")
- config.add_view(server_index, route_name="server_index", renderer="server_index.mako")
-
- config.add_route("server_index_json", "/servers.json")
- config.add_view(server_index_json, route_name="server_index_json", renderer="jsonp")
-
- config.add_route("server_info", "/server/{id:\d+}")
- config.add_view(server_info, route_name="server_info", renderer="server_info.mako")
-
- config.add_route("server_info_json", "/server/{id:\d+}.json")
- config.add_view(server_info_json, route_name="server_info_json", renderer="jsonp")
+ config.add_route("server_index", "/servers")
+ config.add_view(view=ServerIndex, route_name="server_index", attr="html",
+ renderer="server_index.mako", accept="text/html")
+ config.add_view(view=ServerIndex, route_name="server_index", attr="json", renderer="json",
+ accept="application/json")
+
+ config.add_route("server_top_maps", "/server/{id:\d+}/topmaps")
+ config.add_view(view=ServerTopMaps, route_name="server_top_maps", attr="html",
+ renderer="server_top_maps.mako", accept="text/html")
+ config.add_view(view=ServerTopMaps, route_name="server_top_maps", attr="json", renderer="json",
+ accept="application/json")
+
+ config.add_route("server_top_active", "/server/{id:\d+}/topactive")
+ config.add_view(view=ServerTopPlayers, route_name="server_top_active", attr="html",
+ renderer="server_top_active.mako", accept="text/html")
+ config.add_view(view=ServerTopPlayers, route_name="server_top_active", attr="json",
+ renderer="json", accept="application/json")
+
+ config.add_route("server_top_scorers", "/server/{id:\d+}/topscorers")
+ config.add_view(view=ServerTopScorers, route_name="server_top_scorers", attr="html",
+ renderer="server_top_scorers.mako", accept="text/html")
+ config.add_view(view=ServerTopScorers, route_name="server_top_scorers", attr="json",
+ renderer="json", accept="application/json")
+
+ config.add_route("server_info", "/server/{id:\d+}")
+ config.add_view(view=ServerInfo, route_name="server_info", attr="html",
+ renderer="server_info.mako", accept="text/html")
+ config.add_view(view=ServerInfo, route_name="server_info", attr="json", renderer="json",
+ accept="application/json")
# MAP ROUTES
config.add_route("map_index", "/maps")
#-*- coding: utf-8 -*-
import sys
-from datetime import datetime
-import sqlalchemy as sa
-import sqlalchemy.sql.functions as func
-from sqlalchemy import distinct
-from pyramid.paster import bootstrap
-from xonstat.models import *
-from xonstat.util import datetime_seconds
+from datetime import datetime, timedelta
-from skin import Skin
from playerdata import PlayerData
-
+from pyramid.paster import bootstrap
+from skin import Skin
+from sqlalchemy import distinct
+from xonstat.models import DBSession, PlayerGameStat, Player, PlayerElo
+from xonstat.util import datetime_seconds
# maximal number of query results (for testing, set to None to get all)
NUM_PLAYERS = None
-import sqlalchemy as sa
-import sqlalchemy.sql.functions as func
-from xonstat.models import *
+from xonstat.models import DBSession, Player
from xonstat.views.player import get_games_played, get_overall_stats, get_ranks, get_elos
import datetime
import logging
import math
-import random
-import sys
-from xonstat.models import *
+from xonstat.models import PlayerElo
log = logging.getLogger(__name__)
+++ /dev/null
-import json
-import logging
-import math
-import sqlalchemy
-import sqlalchemy.sql.functions as sfunc
-from calendar import timegm
-from datetime import datetime as dt
-from datetime import timedelta
-from sqlalchemy.orm import mapper
-from sqlalchemy.orm import scoped_session
-from sqlalchemy.orm import sessionmaker
-from sqlalchemy.ext.declarative import declarative_base
-from xonstat.util import qfont_decode, strip_colors, html_colors, pretty_date
-
-log = logging.getLogger(__name__)
-
-DBSession = scoped_session(sessionmaker())
-Base = declarative_base()
-
-# define objects for all tables
-class Player(object):
- def nick_html_colors(self, limit=None):
- if self.nick is None:
- return "Anonymous Player"
- else:
- return html_colors(self.nick, limit)
-
- def nick_strip_colors(self):
- if self.nick is None:
- return "Anonymous Player"
- else:
- return strip_colors(self.nick)
-
- def joined_pretty_date(self):
- return pretty_date(self.create_dt)
-
- def __repr__(self):
- return "<Player(%s, %s)>" % (self.player_id, self.nick.encode('utf-8'))
-
- def to_dict(self):
- return {'player_id':self.player_id, 'nick':self.nick,
- 'joined':self.create_dt.strftime('%Y-%m-%dT%H:%M:%SZ'),
- 'active_ind':self.active_ind, 'location':self.location,
- 'stripped_nick':qfont_decode(self.stripped_nick)}
-
- def epoch(self):
- return timegm(self.create_dt.timetuple())
-
-
-class GameType(object):
- def __repr__(self):
- return "<GameType(%s, %s, %s)>" % (self.game_type_cd, self.descr, self.active_ind)
-
- def to_dict(self):
- return {'game_type_cd':self.game_type_cd, 'name':self.descr, 'active':self.active_ind}
-
-
-class Weapon(object):
- def __repr__(self):
- return "<Weapon(%s, %s, %s)>" % (self.weapon_cd, self.descr, self.active_ind)
-
- def to_dict(self):
- return {'weapon_cd':self.weapon_cd, 'name':self.descr, 'active':self.active_ind}
-
-
-class Server(object):
- def __init__(self, name=None, hashkey=None, ip_addr=None):
- self.name = name
- self.hashkey = hashkey
- self.ip_addr = ip_addr
- self.create_dt = dt.utcnow()
-
- def __repr__(self):
- return "<Server(%s, %s)>" % (self.server_id, self.name.encode('utf-8'))
-
- def to_dict(self):
- return {'server_id':self.server_id, 'name':self.name,
- 'ip_addr':self.ip_addr, 'location':self.location}
-
- 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):
- self.name = name
-
- def __repr__(self):
- return "<Map(%s, %s, %s)>" % (self.map_id, self.name, self.version)
-
- 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,
- server_id=None, map_id=None, winner=None):
- self.game_id = game_id
- self.start_dt = start_dt
- self.game_type_cd = game_type_cd
- self.server_id = server_id
- self.map_id = map_id
- self.winner = winner
-
- def __repr__(self):
- return "<Game(%s, %s, %s, %s)>" % (self.game_id, self.start_dt, self.game_type_cd, self.server_id)
-
- def to_dict(self):
- return {'game_id':self.game_id, 'start':self.start_dt.strftime('%Y-%m-%dT%H:%M:%SZ'),
- 'game_type_cd':self.game_type_cd, 'server_id':self.server_id}
-
- 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):
- self.player_game_stat_id = player_game_stat_id
- self.create_dt = create_dt
-
- def __repr__(self):
- return "<PlayerGameStat(%s, %s, %s)>" % (self.player_id, self.game_id, self.create_dt)
-
- def to_dict(self):
- return {'player_id':self.player_id, 'game_id':self.game_id,
- 'create_dt':self.create_dt.strftime('%Y-%m-%dT%H:%M:%SZ'),
- 'alivetime':self.alivetime, 'rank':self.rank, 'score':self.score, 'team':self.team}
-
- def nick_stripped(self):
- if self.nick is None:
- return "Anonymous Player"
- else:
- return strip_colors(self.nick)
-
- def nick_html_colors(self, limit=None):
- if self.nick is None:
- return "Anonymous Player"
- else:
- return html_colors(self.nick, limit)
-
- def team_html_color(self):
- if self.team == 5:
- return "red"
- if self.team == 14:
- return "blue"
- if self.team == 13:
- return "yellow"
- if self.team == 10:
- return "pink"
-
-
-class Achievement(object):
- def __repr__(self):
- return "<Achievement(%s, %s, %s)>" % (self.achievement_cd, self.descr, self.limit)
-
- def to_dict(self):
- return {'achievement_cd':self.achievement_cd, 'name':self.descr, 'limit':self.limit}
-
-
-class PlayerAchievement(object):
- def __repr__(self):
- return "<PlayerAchievement(%s, %s)>" % (self.player_id, self.achievement_cd)
-
- def to_dict(self):
- return {'player_id':self.player_id, 'achievement_cd':self.achievement_cd}
-
-
-class PlayerWeaponStat(object):
- def __init__(self, player_id=None, game_id=None, weapon_cd=None):
- self.player_id = player_id
- self.game_id = game_id
- self.weapon_cd = weapon_cd
- 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)
-
- def to_dict(self):
- return {
- 'weapon_cd':self.weapon_cd,
- 'player_weapon_stats_id':self.player_weapon_stats_id,
- 'player_id':self.player_id,
- 'game_id':self.game_id,
- 'fired':self.fired,
- 'max':self.max,
- 'hit':self.hit,
- 'actual':self.actual,
- 'frags':self.frags,
- }
-
-
-class Hashkey(object):
- def __init__(self, player_id=None, hashkey=None):
- self.player_id = player_id
- self.hashkey = hashkey
-
- def __repr__(self):
- return "<Hashkey(%s, %s)>" % (self.player_id, self.hashkey)
-
- def to_dict(self):
- return {'player_id':self.player_id, 'hashkey':self.hashkey}
-
-
-class PlayerNick(object):
- def __repr__(self):
- return "<PlayerNick(%s, %s)>" % (self.player_id, qfont_decode(self.stripped_nick))
-
- def to_dict(self):
- return {'player_id':self.player_id, 'name':qfont_decode(self.stripped_nick)}
-
-
-class PlayerElo(object):
- def __init__(self, player_id=None, game_type_cd=None, elo=None):
-
- self.player_id = player_id
- self.game_type_cd = game_type_cd
- self.elo = elo
- self.score = 0
- self.games = 0
-
- def __repr__(self):
- return "<PlayerElo(pid=%s, gametype=%s, elo=%s, games=%s)>" % (self.player_id, self.game_type_cd, self.elo, self.games)
-
- def to_dict(self):
- return {'player_id':self.player_id, 'game_type_cd':self.game_type_cd, 'elo':self.elo, 'games':self.games}
-
-
-class PlayerRank(object):
-
- def nick_html_colors(self, limit=None):
- if self.nick is None:
- return "Anonymous Player"
- else:
- return html_colors(self.nick, limit)
-
- def __repr__(self):
- return "<PlayerRank(pid=%s, gametype=%s, rank=%s)>" % (self.player_id, self.game_type_cd, self.rank)
-
- def to_dict(self):
- return {'player_id':self.player_id, 'game_type_cd':self.game_type_cd, 'rank':self.rank}
-
-
-class PlayerCaptime(object):
- def __init__(self, player_id=None, game_id=None, map_id=None,
- fastest_cap=None, mod=None):
- self.player_id = player_id
- self.game_id = game_id
- self.map_id = map_id
- self.fastest_cap = fastest_cap
- self.mod = mod
-
- def __repr__(self):
- return "<PlayerCaptime(pid=%s, map_id=%s, mod=%s)>" % (self.player_id,
- self.map_id, self.mod)
-
- def fuzzy_date(self):
- return pretty_date(self.create_dt)
-
- def epoch(self):
- return timegm(self.create_dt.timetuple())
-
-
-class SummaryStat(object):
- def __repr__(self):
- return "<SummaryStat(total_players=%s, total_games=%s, total_servers=%s)>" % (self.total_players, self.total_games, self.total_servers)
-
-
-class TeamGameStat(object):
- def __init__(self, team_game_stat_id=None, create_dt=None):
- self.team_game_stat_id = team_game_stat_id
- self.create_dt = create_dt
-
- def __repr__(self):
- return "<TeamGameStat(%s, %s, %s)>" % (self.team_game_stat_id, self.game_id, self.team)
-
- def to_dict(self):
- return {
- 'team_game_stat_id':self.team_game_stat_id,
- 'game_id':self.game_id,
- 'team':self.team,
- 'score':self.score,
- 'rounds':self.rounds,
- 'caps':self.caps,
- 'create_dt':self.create_dt.strftime('%Y-%m-%dT%H:%M:%SZ'),
- }
-
- def team_html_color(self):
- if self.team == 5:
- return "red"
- if self.team == 14:
- return "blue"
- if self.team == 13:
- return "yellow"
- if self.team == 10:
- return "pink"
-
-
-class PlayerGameAnticheat(object):
- def __init__(self, player_id=None, game_id=None, key=None,
- value=None, create_dt=None):
- self.player_id = player_id
- self.game_id = game_id
- self.key = key
- self.value = value
- self.create_dt = create_dt
-
- def __repr__(self):
- return "<PlayerGameAnticheat(%s, %d)>" % (self.key, self.value)
-
-
-class PlayerGroups(object):
- def __init__(self, player_id=None, group_name=None):
- self.player_id = player_id
- self.group_name = group_name
-
- def __repr__(self):
- return "<PlayerGroups(%s, %s)>" % (self.player_id, self.group_name)
-
-
-class MapCapTime(object):
- """Fastest flag capture times per map, assembled from a SQLAlchemy query"""
- def __init__(self, row):
- self.fastest_cap = row.fastest_cap
- self.create_dt = row.create_dt
- self.create_dt_epoch = timegm(row.create_dt.timetuple())
- self.create_dt_fuzzy = pretty_date(row.create_dt)
- self.player_id = row.player_id
- self.player_nick = row.player_nick
- self.player_nick_stripped = strip_colors(row.player_nick)
- self.player_nick_html = html_colors(row.player_nick)
- self.game_id = row.game_id
- self.server_id = row.server_id
- self.server_name = row.server_name
-
- def to_dict(self):
- return {
- "fastest_cap" : self.fastest_cap.total_seconds(),
- "create_dt_epoch" : self.create_dt_epoch,
- "create_dt_fuzzy" : self.create_dt_fuzzy,
- "player_id" : self.player_id,
- "player_nick" : self.player_nick,
- "player_nick_stripped" : self.player_nick_stripped,
- "game_id" : self.game_id,
- "server_id" : self.server_id,
- "server_name" : self.server_name,
- }
-
-
-class PlayerCapTime(object):
- """Fastest flag capture times per player, assembled from a SQLAlchemy query"""
- def __init__(self, row):
- self.fastest_cap = row.fastest_cap
- self.create_dt = row.create_dt
- self.create_dt_epoch = timegm(row.create_dt.timetuple())
- self.create_dt_fuzzy = pretty_date(row.create_dt)
- self.player_id = row.player_id
- self.game_id = row.game_id
- self.map_id = row.map_id
- self.map_name = row.map_name
- self.server_id = row.server_id
- self.server_name = row.server_name
-
- def to_dict(self):
- return {
- "fastest_cap" : self.fastest_cap.total_seconds(),
- "create_dt_epoch": self.create_dt_epoch,
- "create_dt_fuzzy": self.create_dt_fuzzy,
- "game_id":self.game_id,
- "map_id": self.map_id,
- "map_name": self.map_name,
- "server_id": self.server_id,
- "server_name": self.server_name,
- }
-
-
-class ActivePlayer(object):
- def __init__(self, sort_order=None, player_id=None, nick=None,
- alivetime=None):
- self.sort_order = sort_order
- self.player_id = player_id
- self.nick = nick
- self.alivetime = alivetime
-
- def nick_html_colors(self):
- return html_colors(self.nick)
-
- def __repr__(self):
- return "<ActivePlayer(%s, %s)>" % (self.sort_order, self.player_id)
-
-
-class ActiveServer(object):
- def __init__(self, sort_order=None, server_id=None, server_name=None,
- games=None):
- self.sort_order = sort_order
- self.server_id = server_id
- self.server_name = server_name
- self.games = games
-
- def __repr__(self):
- return "<ActiveServer(%s, %s)>" % (self.sort_order, self.server_id)
-
-
-class ActiveMap(object):
- def __init__(self, sort_order=None, map_id=None, map_name=None,
- games=None):
- self.sort_order = sort_order
- self.map_id = map_id
- self.map_name = map_name
- self.games = games
-
- def __repr__(self):
- return "<ActiveMap(%s, %s)>" % (self.sort_order, self.map_id)
-
-
-class PlayerMedal(object):
- def __repr__(self):
- return "<PlayerRank(pid=%s, place=%s, alt=%s)>" % (self.player_id,
- self.place, self.alt)
-
-
-def initialize_db(engine=None):
- DBSession.configure(bind=engine)
- Base.metadata.bind = engine
- Base.metadata.create_all(engine)
- MetaData = sqlalchemy.MetaData(bind=engine)
- MetaData.reflect()
-
- # assign all those tables to an object
- achievements_table = MetaData.tables['achievements']
- cd_achievement_table = MetaData.tables['cd_achievement']
- cd_game_type_table = MetaData.tables['cd_game_type']
- cd_weapon_table = MetaData.tables['cd_weapon']
- db_version_table = MetaData.tables['db_version']
- games_table = MetaData.tables['games']
- hashkeys_table = MetaData.tables['hashkeys']
- maps_table = MetaData.tables['maps']
- player_game_stats_table = MetaData.tables['player_game_stats']
- players_table = MetaData.tables['players']
- player_weapon_stats_table = MetaData.tables['player_weapon_stats']
- servers_table = MetaData.tables['servers']
- player_nicks_table = MetaData.tables['player_nicks']
- player_elos_table = MetaData.tables['player_elos']
- player_ranks_table = MetaData.tables['player_ranks']
- player_captimes_table = MetaData.tables['player_map_captimes']
- summary_stats_table = MetaData.tables['summary_stats']
- team_game_stats_table = MetaData.tables['team_game_stats']
- player_game_anticheats_table = MetaData.tables['player_game_anticheats']
- player_groups_table = MetaData.tables['player_groups']
- active_players_table = MetaData.tables['active_players_mv']
- active_servers_table = MetaData.tables['active_servers_mv']
- active_maps_table = MetaData.tables['active_maps_mv']
- player_medals_table = MetaData.tables['player_medals']
-
- # now map the tables and the objects together
- mapper(PlayerAchievement, achievements_table)
- mapper(Achievement, cd_achievement_table)
- mapper(GameType, cd_game_type_table)
- mapper(Weapon, cd_weapon_table)
- mapper(Game, games_table)
- mapper(Hashkey, hashkeys_table)
- mapper(Map, maps_table)
- mapper(PlayerGameStat, player_game_stats_table)
- mapper(Player, players_table)
- mapper(PlayerWeaponStat, player_weapon_stats_table)
- mapper(Server, servers_table)
- mapper(PlayerNick, player_nicks_table)
- mapper(PlayerElo, player_elos_table)
- mapper(PlayerRank, player_ranks_table)
- mapper(PlayerCaptime, player_captimes_table)
- mapper(SummaryStat, summary_stats_table)
- mapper(TeamGameStat, team_game_stats_table)
- mapper(PlayerGameAnticheat, player_game_anticheats_table)
- mapper(PlayerGroups, player_groups_table)
- mapper(ActivePlayer, active_players_table)
- mapper(ActiveServer, active_servers_table)
- mapper(ActiveMap, active_maps_table)
- mapper(PlayerMedal, player_medals_table)
--- /dev/null
+"""
+Model initialization and mapping.
+"""
+
+from sqlalchemy import MetaData
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import scoped_session, sessionmaker, mapper
+
+from xonstat.models.game import *
+from xonstat.models.main import *
+from xonstat.models.map import *
+from xonstat.models.player import *
+from xonstat.models.server import *
+
+DBSession = scoped_session(sessionmaker())
+Base = declarative_base()
+
+
+def initialize_db(engine=None):
+ """
+ Initialize the database using reflection.
+
+ :param engine: The SQLAlchemy engine instance to bind.
+ :return: None
+ """
+ DBSession.configure(bind=engine)
+
+ Base.metadata.bind = engine
+ Base.metadata.create_all(engine)
+
+ # Since the schema is actually defined elsewhere, we use reflection to determine the
+ # structure of the models instead of specifying them over again in Python.
+ metadata = MetaData(bind=engine)
+ metadata.reflect()
+
+ # Assign all the tables to objects
+ achievements_table = metadata.tables['achievements']
+ cd_achievement_table = metadata.tables['cd_achievement']
+ cd_game_type_table = metadata.tables['cd_game_type']
+ cd_weapon_table = metadata.tables['cd_weapon']
+ db_version_table = metadata.tables['db_version']
+ games_table = metadata.tables['games']
+ hashkeys_table = metadata.tables['hashkeys']
+ maps_table = metadata.tables['maps']
+ player_game_stats_table = metadata.tables['player_game_stats']
+ players_table = metadata.tables['players']
+ player_weapon_stats_table = metadata.tables['player_weapon_stats']
+ servers_table = metadata.tables['servers']
+ player_nicks_table = metadata.tables['player_nicks']
+ player_elos_table = metadata.tables['player_elos']
+ player_ranks_table = metadata.tables['player_ranks']
+ player_captimes_table = metadata.tables['player_map_captimes']
+ summary_stats_table = metadata.tables['summary_stats']
+ team_game_stats_table = metadata.tables['team_game_stats']
+ player_game_anticheats_table = metadata.tables['player_game_anticheats']
+ player_groups_table = metadata.tables['player_groups']
+ active_players_table = metadata.tables['active_players_mv']
+ active_servers_table = metadata.tables['active_servers_mv']
+ active_maps_table = metadata.tables['active_maps_mv']
+ player_medals_table = metadata.tables['player_medals']
+
+ # Map the tables and the objects together
+ mapper(PlayerAchievement, achievements_table)
+ mapper(Achievement, cd_achievement_table)
+ mapper(GameType, cd_game_type_table)
+ mapper(Weapon, cd_weapon_table)
+ mapper(Game, games_table)
+ mapper(Hashkey, hashkeys_table)
+ mapper(Map, maps_table)
+ mapper(PlayerGameStat, player_game_stats_table)
+ mapper(Player, players_table)
+ mapper(PlayerWeaponStat, player_weapon_stats_table)
+ mapper(Server, servers_table)
+ mapper(PlayerNick, player_nicks_table)
+ mapper(PlayerElo, player_elos_table)
+ mapper(PlayerRank, player_ranks_table)
+ mapper(PlayerCaptime, player_captimes_table)
+ mapper(SummaryStat, summary_stats_table)
+ mapper(TeamGameStat, team_game_stats_table)
+ mapper(PlayerGameAnticheat, player_game_anticheats_table)
+ mapper(PlayerGroups, player_groups_table)
+ mapper(ActivePlayer, active_players_table)
+ mapper(ActiveServer, active_servers_table)
+ mapper(ActiveMap, active_maps_table)
+ mapper(PlayerMedal, player_medals_table)
--- /dev/null
+"""
+Models related to games.
+"""
+
+from xonstat.models.mixins import FuzzyDateMixin, EpochMixin
+from xonstat.util import strip_colors, html_colors
+
+
+class Game(FuzzyDateMixin, EpochMixin):
+ """
+ An individual game.
+ """
+
+ def __init__(self, game_id=None, start_dt=None, game_type_cd=None, server_id=None, map_id=None,
+ winner=None):
+ self.game_id = game_id
+ self.start_dt = start_dt
+ self.game_type_cd = game_type_cd
+ self.server_id = server_id
+ self.map_id = map_id
+ self.winner = winner
+
+ def __repr__(self):
+ return "<Game({0.game_id}, {0.start_dt}, {0.game_type_cd}, {0.server_id})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'game_id': self.game_id,
+ 'start': self.start_dt.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'game_type_cd': self.game_type_cd,
+ 'server_id': self.server_id
+ }
+
+
+class PlayerGameStat(object):
+ """
+ The individual statistics a player has gained/lost during a game.
+ """
+
+ def __init__(self, player_game_stat_id=None, create_dt=None):
+ self.player_game_stat_id = player_game_stat_id
+ self.create_dt = create_dt
+
+ def __repr__(self):
+ return "<PlayerGameStat({0.player_id}, {0.game_id}, {0.create_dt})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'player_id': self.player_id,
+ 'game_id': self.game_id,
+ 'create_dt': self.create_dt.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'alivetime': self.alivetime,
+ 'rank': self.rank,
+ 'score': self.score,
+ 'team': self.team
+ }
+
+ def nick_stripped(self):
+ if self.nick is None:
+ return "Anonymous Player"
+ else:
+ return strip_colors(self.nick)
+
+ def nick_html_colors(self, limit=None):
+ if self.nick is None:
+ return "Anonymous Player"
+ else:
+ return html_colors(self.nick, limit)
+
+ def team_html_color(self):
+ if self.team == 5:
+ return "red"
+ if self.team == 14:
+ return "blue"
+ if self.team == 13:
+ return "yellow"
+ if self.team == 10:
+ return "pink"
+
+
+class PlayerWeaponStat(object):
+ """
+ The metrics for a single weapon in a game for a player.
+ """
+
+ def __init__(self, player_id=None, game_id=None, weapon_cd=None):
+ self.player_id = player_id
+ self.game_id = game_id
+ self.weapon_cd = weapon_cd
+ self.fired = 0
+ self.max = 0
+ self.hit = 0
+ self.actual = 0
+ self.frags = 0
+
+ def __repr__(self):
+ return ("<PlayerWeaponStat({0.player_weapon_stats_id}, {0.player_id}, {0.game_id})>"
+ .format(self))
+
+ def to_dict(self):
+ return {
+ 'weapon_cd': self.weapon_cd,
+ 'player_weapon_stats_id': self.player_weapon_stats_id,
+ 'player_id': self.player_id,
+ 'game_id': self.game_id,
+ 'fired': self.fired,
+ 'max': self.max,
+ 'hit': self.hit,
+ 'actual': self.actual,
+ 'frags': self.frags,
+ }
+
+
+class TeamGameStat(object):
+ """
+ Team level metrics.
+ """
+
+ def __init__(self, team_game_stat_id=None, create_dt=None):
+ self.team_game_stat_id = team_game_stat_id
+ self.create_dt = create_dt
+
+ def __repr__(self):
+ return "<TeamGameStat({0.team_game_stat_id}, {0.game_id}, {0.team})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'team_game_stat_id': self.team_game_stat_id,
+ 'game_id': self.game_id,
+ 'team': self.team,
+ 'score': self.score,
+ 'rounds': self.rounds,
+ 'caps': self.caps,
+ 'create_dt': self.create_dt.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ }
+
+ # TODO: move this function to util
+ def team_html_color(self):
+ if self.team == 5:
+ return "red"
+ if self.team == 14:
+ return "blue"
+ if self.team == 13:
+ return "yellow"
+ if self.team == 10:
+ return "pink"
+
+
+class PlayerGameAnticheat(object):
+ """
+ Anticheat metrics sent by the server to identify odd patterns.
+ """
+
+ def __init__(self, player_id=None, game_id=None, key=None, value=None, create_dt=None):
+ self.player_id = player_id
+ self.game_id = game_id
+ self.key = key
+ self.value = value
+ self.create_dt = create_dt
+
+ def __repr__(self):
+ return "<PlayerGameAnticheat({0.key}, {0.value})>".format(self)
+
+
+class GameType(object):
+ """
+ A particular type of game.
+ """
+
+ def __repr__(self):
+ return "<GameType({0.game_type_cd}, {0.descr}, {0.active_ind})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'game_type_cd': self.game_type_cd,
+ 'name': self.descr,
+ 'active': self.active_ind,
+ }
+
+
+class Weapon(object):
+ """
+ A particular type of weapon.
+ """
+
+ def __repr__(self):
+ return "<Weapon({0.weapon_cd}, {0.descr}, {0.active_ind})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'weapon_cd': self.weapon_cd,
+ 'name': self.descr,
+ 'active': self.active_ind,
+ }
\ No newline at end of file
--- /dev/null
+"""
+Models related to the main index page.
+"""
+
+from xonstat.util import html_colors
+
+
+class SummaryStat(object):
+ """
+ The high level summary line that is shown on the main page.
+ """
+
+ def __repr__(self):
+ return ("<SummaryStat(total_players={0.total_players}, total_games={0.total_games}, "
+ "total_servers={0.total_servers})>".format(self))
+
+
+class ActivePlayer(object):
+ """
+ A record in the "Most Active Players" list.
+ """
+
+ def __init__(self, sort_order=None, player_id=None, nick=None, alivetime=None):
+ self.sort_order = sort_order
+ self.player_id = player_id
+ self.nick = nick
+ self.alivetime = alivetime
+
+ def nick_html_colors(self):
+ return html_colors(self.nick)
+
+ def __repr__(self):
+ return "<ActivePlayer({0.sort_order}, {0.player_id})>".format(self)
+
+
+class ActiveServer(object):
+ """
+ A record in the "Most Active Servers" list.
+ """
+
+ def __init__(self, sort_order=None, server_id=None, server_name=None, games=None):
+ self.sort_order = sort_order
+ self.server_id = server_id
+ self.server_name = server_name
+ self.games = games
+
+ def __repr__(self):
+ return "<ActiveServer({0.sort_order}, {0.server_id})>".format(self)
+
+
+class ActiveMap(object):
+ """
+ A record in the "Most Active Maps" list.
+ """
+
+ def __init__(self, sort_order=None, map_id=None, map_name=None, games=None):
+ self.sort_order = sort_order
+ self.map_id = map_id
+ self.map_name = map_name
+ self.games = games
+
+ def __repr__(self):
+ return "<ActiveMap({0.sort_order}, {0.map_id})>".format(self)
--- /dev/null
+"""
+Models related to maps.
+"""
+
+from calendar import timegm
+
+from xonstat.models.mixins import FuzzyDateMixin, EpochMixin
+from xonstat.util import pretty_date, strip_colors, html_colors
+
+
+class Map(FuzzyDateMixin, EpochMixin):
+ """
+ A playable map. Roughly equivalent to a pk3 file, but distinguished by name instead.
+ """
+
+ def __init__(self, name=None):
+ self.name = name
+
+ def __repr__(self):
+ return "<Map({0.map_id}, {0.name}, {0.version})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'map_id': self.map_id,
+ 'name': self.name,
+ 'version': self.version,
+ }
+
+
+# TODO: investigate if this model is truly a model, or really just a query (i.e. namedtuple)
+class MapCapTime(object):
+ """
+ Fastest flag capture times per map.
+ """
+
+ def __init__(self, row):
+ self.fastest_cap = row.fastest_cap
+ self.create_dt = row.create_dt
+ self.create_dt_epoch = timegm(row.create_dt.timetuple())
+ self.create_dt_fuzzy = pretty_date(row.create_dt)
+ self.player_id = row.player_id
+ self.player_nick = row.player_nick
+ self.player_nick_stripped = strip_colors(row.player_nick)
+ self.player_nick_html = html_colors(row.player_nick)
+ self.game_id = row.game_id
+ self.server_id = row.server_id
+ self.server_name = row.server_name
+
+ def to_dict(self):
+ return {
+ "fastest_cap": self.fastest_cap.total_seconds(),
+ "create_dt_epoch": self.create_dt_epoch,
+ "create_dt_fuzzy": self.create_dt_fuzzy,
+ "player_id": self.player_id,
+ "player_nick": self.player_nick,
+ "player_nick_stripped": self.player_nick_stripped,
+ "game_id": self.game_id,
+ "server_id": self.server_id,
+ "server_name": self.server_name,
+ }
--- /dev/null
+"""
+Mixins for methods used by several model classes.
+"""
+from calendar import timegm
+from xonstat.util import pretty_date, html_colors
+
+
+class FuzzyDateMixin(object):
+ """Provides a class with a "create_dt" attribute the ability to return a fuzzy date."""
+
+ def fuzzy_date(self):
+ return pretty_date(self.create_dt)
+
+
+class EpochMixin(object):
+ """Provides a class with a "create_dt" attribute the ability to return the epoch time."""
+
+ def epoch(self):
+ return timegm(self.create_dt.timetuple())
+
+
+class NickColorsMixin(object):
+ """Provides a class with a "nick" attribute the ability to return the nick's HTML colors."""
+
+ def nick_html_colors(self, limit=None):
+ if self.nick is None:
+ return "Anonymous Player"
+ else:
+ return html_colors(self.nick, limit)
--- /dev/null
+"""
+Models related to players.
+"""
+
+from calendar import timegm
+
+from xonstat.models.mixins import FuzzyDateMixin, EpochMixin, NickColorsMixin
+from xonstat.util import strip_colors, pretty_date, qfont_decode
+
+
+class Player(EpochMixin, NickColorsMixin, FuzzyDateMixin):
+ """
+ A player, which can represent either a human or a bot.
+ """
+
+ def nick_strip_colors(self):
+ if self.nick is None:
+ return "Anonymous Player"
+ else:
+ return strip_colors(self.nick)
+
+ def __repr__(self):
+ return "<Player({}, {})>".format(self.player_id, self.nick.encode('utf-8'))
+
+ def to_dict(self):
+ return {
+ 'player_id': self.player_id,
+ 'nick': self.nick,
+ 'joined': self.create_dt.strftime('%Y-%m-%dT%H:%M:%SZ'),
+ 'active_ind': self.active_ind,
+ 'location': self.location,
+ 'stripped_nick': qfont_decode(self.stripped_nick),
+ }
+
+
+class Achievement(object):
+ """
+ A type of achievement. Referenced implicitly in PlayerAchievement.
+ """
+
+ def __repr__(self):
+ return "<Achievement({0.achievement_cd}, {0.descr}, {0.limit})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'achievement_cd': self.achievement_cd,
+ 'name': self.descr,
+ 'limit':self.limit,
+ }
+
+
+class PlayerAchievement(object):
+ """
+ Achievements a player has earned.
+ """
+
+ def __repr__(self):
+ return "<PlayerAchievement({0.player_id}, {0.achievement_cd})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'player_id': self.player_id,
+ 'achievement_cd': self.achievement_cd,
+ }
+
+
+class Hashkey(object):
+ """
+ A player's identifying key from the d0_blind_id library.
+ """
+
+ def __init__(self, player_id=None, hashkey=None):
+ self.player_id = player_id
+ self.hashkey = hashkey
+
+ def __repr__(self):
+ return "<Hashkey({0.player_id}, {0.hashkey})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'player_id': self.player_id,
+ 'hashkey': self.hashkey
+ }
+
+
+class PlayerNick(object):
+ """
+ A single nickname a player has used in a game.
+ """
+
+ def __repr__(self):
+ return "<PlayerNick({0.player_id}, {0.stripped_nick})>".format(self)
+
+ def to_dict(self):
+ return {
+ 'player_id': self.player_id,
+ 'name': qfont_decode(self.stripped_nick),
+ }
+
+
+class PlayerElo(object):
+ """
+ A player's skill for a particular game type, as determined by a modified Elo algorithm.
+ """
+
+ def __init__(self, player_id=None, game_type_cd=None, elo=None):
+ self.player_id = player_id
+ self.game_type_cd = game_type_cd
+ self.elo = elo
+ self.score = 0
+ self.games = 0
+
+ def __repr__(self):
+ return ("<PlayerElo(pid={0.player_id}, gametype={0.game_type_cd}, elo={0.elo}, "
+ "games={0.games})>".format(self))
+
+ def to_dict(self):
+ return {
+ 'player_id': self.player_id,
+ 'game_type_cd': self.game_type_cd,
+ 'elo': self.elo,
+ 'games': self.games,
+ }
+
+
+class PlayerRank(NickColorsMixin):
+ """
+ A player's rank for a given game type.
+ """
+
+ def __repr__(self):
+ return ("<PlayerRank(pid={0.player_id}, gametype={0.game_type_cd}, rank={0.rank})>"
+ .format(self))
+
+ def to_dict(self):
+ return {
+ 'player_id': self.player_id,
+ 'game_type_cd': self.game_type_cd,
+ 'rank': self.rank
+ }
+
+
+class PlayerCaptime(FuzzyDateMixin, EpochMixin):
+ """
+ A flag capture time for a player on a given map.
+ """
+
+ def __init__(self, player_id=None, game_id=None, map_id=None, fastest_cap=None, mod=None):
+ self.player_id = player_id
+ self.game_id = game_id
+ self.map_id = map_id
+ self.fastest_cap = fastest_cap
+ self.mod = mod
+
+ def __repr__(self):
+ return "<PlayerCaptime(pid={0.player_id}, map_id={0.map_id}, mod={0.mod})>".format(self)
+
+
+class PlayerGroups(object):
+ """
+ An authorization group a player belongs to. Used to control access.
+ """
+
+ def __init__(self, player_id=None, group_name=None):
+ self.player_id = player_id
+ self.group_name = group_name
+
+ def __repr__(self):
+ return "<PlayerGroups({0.player_id}, {0.group_name})>".format(self)
+
+
+# TODO: determine if this is a real model (it is very similar to PlayerCaptime from above)
+class PlayerCapTime(object):
+ """
+ Fastest flag capture times per player.
+ """
+
+ def __init__(self, row):
+ self.fastest_cap = row.fastest_cap
+ self.create_dt = row.create_dt
+ self.create_dt_epoch = timegm(row.create_dt.timetuple())
+ self.create_dt_fuzzy = pretty_date(row.create_dt)
+ self.player_id = row.player_id
+ self.game_id = row.game_id
+ self.map_id = row.map_id
+ self.map_name = row.map_name
+ self.server_id = row.server_id
+ self.server_name = row.server_name
+
+ def to_dict(self):
+ return {
+ "fastest_cap" : self.fastest_cap.total_seconds(),
+ "create_dt_epoch": self.create_dt_epoch,
+ "create_dt_fuzzy": self.create_dt_fuzzy,
+ "game_id":self.game_id,
+ "map_id": self.map_id,
+ "map_name": self.map_name,
+ "server_id": self.server_id,
+ "server_name": self.server_name,
+ }
+
+
+class PlayerMedal(object):
+ """
+ A medal a player has earned in a large tournament.
+ """
+
+ def __repr__(self):
+ return "<PlayerMedal(pid={0.player_id}, place={0.place}, alt={0.alt})>".format(self)
--- /dev/null
+"""
+Models related to servers.
+"""
+
+from datetime import datetime as dt
+
+from xonstat.models.mixins import FuzzyDateMixin, EpochMixin
+
+
+class Server(FuzzyDateMixin, EpochMixin):
+ """
+ A Xonotic server, identifiable by name and (when there's a conflict) hashkey.
+ """
+
+ def __init__(self, name=None, hashkey=None, ip_addr=None):
+ self.name = name
+ self.hashkey = hashkey
+ self.ip_addr = ip_addr
+ self.create_dt = dt.utcnow()
+
+ def __repr__(self):
+ return "<Server({}, {})>".format(self.server_id, self.name.encode('utf-8'))
+
+ def to_dict(self):
+ return {
+ 'server_id': self.server_id,
+ 'name': self.name,
+ 'ip_addr': self.ip_addr,
+ 'location': self.location,
+ }
<tr>
<td>${player.player_id}</th>
<td class="no-stretch"><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><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>
+ <td><span class="abstime" data-epoch="${player.epoch()}" title="${player.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${player.fuzzy_date()}</span></th>
<td class="text-center">
<a href="${request.route_url("player_game_index", player_id=player.player_id, page=1)}" title="View recent games by this player">
<i class="fa fa-list"></i>
</h2>
<h5>
- <i><span class="abstime" data-epoch="${player.epoch()}" title="${player.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">Joined ${player.joined_pretty_date()}</span> (player #${player.player_id})</i>
+ <i><span class="abstime" data-epoch="${player.epoch()}" title="${player.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">Joined ${player.fuzzy_date()}</span> (player #${player.player_id})</i>
% if cake_day:
<img src="/static/images/icons/24x24/cake.png" title="Happy cake day!" />
% endif
<tr>
<td>${player.player_id}</th>
<td class="no-stretch"><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><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>
+ <td><span class="abstime" data-epoch="${player.epoch()}" title="${player.create_dt.strftime('%a, %d %b %Y %H:%M:%S UTC')}">${player.fuzzy_date()}</span></th>
<td class="text-center">
<a href="${request.route_url("player_game_index", player_id=player.player_id, page=1)}" title="View recent games by this player">
<i class="fa fa-list"></i>
% endif
</%block>
+<%def name="empty_rows(list, max_empty_rows)">
+ % for i in range(max_empty_rows - len(list)):
+ <tr>
+ <td>-</td>
+ <td>-</td>
+ <td>-</td>
+ </tr>
+ % endfor
+</%def>
+
% if server is None:
<h2>Sorry, that server wasn't found!</h2>
<div class="row">
<div class="small-12 large-4 columns">
- <h5>Top Scoring Players</h5>
+ <h5>Top Scoring Players <a href="${request.route_url('server_top_scorers', id=server.server_id)}" title="See more top scoring players for this server"><i class="fa fa-plus-circle"></i></a></h5>
<table class="table-hover table-condensed">
<thead>
<tr>
</tr>
</thead>
<tbody>
- <% i = 1 %>
- % for (score_player_id, score_nick, score_value) in top_scorers:
+ % for ts in top_scorers:
<tr>
- <td>${i}</td>
- % if score_player_id != '-':
- <td class="no-stretch"><a href="${request.route_url('player_info', id=score_player_id)}" title="Go to the player info page for this player">${score_nick|n}</a></td>
- % else:
- <td class="no-stretch">${score_nick}</td>
- % endif
- <td>${score_value}</td>
+ <td>${ts.rank}</td>
+ <td class="no-stretch"><a href="${request.route_url('player_info', id=ts.player_id)}" title="Go to the player info page for this player">${ts.nick|n}</a></td>
+ <td>${ts.total_score}</td>
</tr>
- <% i = i+1 %>
% endfor
+
+ ${empty_rows(top_scorers, 10)}
+
</tbody>
</table>
</div>
<div class="small-12 large-4 columns">
- <h5>Most Active Players</h5>
+ <h5>Most Active Players <a href="${request.route_url('server_top_active', id=server.server_id)}" title="See more active players for this server"><i class="fa fa-plus-circle"></i></a></h5>
<table class="table-hover table-condensed">
<thead>
<tr>
</tr>
</thead>
<tbody>
- <% i = 1 %>
- % for (player_id, nick, alivetime) in top_players:
+ % for tp in top_players:
<tr>
- <td>${i}</td>
- % if player_id != '-':
- <td class="no-stretch"><a href="${request.route_url('player_info', id=player_id)}" title="Go to the player info page for this player">${nick|n}</a></td>
- % else:
- <td class="no-stretch">${nick}</td>
- % endif
- <td>${alivetime}</td>
+ <td>${tp.rank}</td>
+ <td class="no-stretch"><a href="${request.route_url('player_info', id=tp.player_id)}" title="Go to the player info page for this player">${tp.nick|n}</a></td>
+ <td>${tp.alivetime}</td>
</tr>
- <% i = i+1 %>
% endfor
+
+ ${empty_rows(top_players, 10)}
+
</tbody>
</table>
</div>
<div class="small-12 large-4 columns">
- <h5>Most Active Maps</h5>
+ <h5>Most Active Maps <a href="${request.route_url('server_top_maps', id=server.server_id)}" title="See more top maps for this server"><i class="fa fa-plus-circle"></i></a></h5>
<table class="table-hover table-condensed">
<thead>
<tr>
</tr>
</thead>
<tbody>
- <% i = 1 %>
- % for (map_id, name, count) in top_maps:
+ % for tm in top_maps:
<tr>
- <td>${i}</td>
- % if map_id != '-':
- <td class="no-stretch"><a href="${request.route_url('map_info', id=map_id)}" title="Go to the map info page for ${name}">${name}</a></td>
- % else:
- <td class="no-stretch">${name}</td>
- % endif
- <td>${count}</td>
+ <td>${tm.rank}</td>
+ <td class="no-stretch"><a href="${request.route_url('map_info', id=tm.map_id)}" title="Go to the map info page for ${tm.name}">${tm.name}</a></td>
+ <td>${tm.times_played}</td>
</tr>
- <% i = i+1 %>
% endfor
+
+ ${empty_rows(top_maps, 10)}
+
</tbody>
</table>
</div>
<div class="row">
<div class="small-12 columns">
- <small>*Most active stats are from the past 7 days</small>
+ <small>*Most active stats are from the past ${lifetime} days</small>
</div>
</div>
% if len(recent_games) > 0:
<div class="row">
<div class="small-12 columns">
- <h5>Most Recent Games</h5>
+ <h5>Most Recent Games <a href="${request.route_url('game_index', _query={'server_id':server.server_id})}"><i class="fa fa-plus-circle"></i></a></h5>
<table class="table-hover table-condensed">
<thead>
<tr>
% endfor
</tbody>
</table>
- <p><a href="${request.route_url('game_index', _query={'server_id':server.server_id})}">More...</a></p>
</div>
</div>
% endif
--- /dev/null
+<%inherit file="base.mako"/>
+<%namespace name="nav" file="nav.mako" />
+
+<%block name="navigation">
+ ${nav.nav('servers')}
+</%block>
+
+<%block name="title">
+ Server Active Players Index
+</%block>
+
+% if not top_players and start is not None:
+ <h2 class="text-center">Sorry, no more active players!</h2>
+
+% elif not top_players and start is None:
+ <h2 class="text-center">No active players found. Yikes, get playing!</h2>
+
+% else:
+ ##### ACTIVE PLAYERS #####
+ <div class="row">
+ <div class="small-12 large-6 large-offset-3 columns">
+ <table class="table-hover table-condensed">
+ <thead>
+ <tr>
+ <th class="small-2">#</th>
+ <th class="small-7">Nick</th>
+ <th class="small-3">Play Time</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ % for tp in top_players:
+ <tr>
+ <td>${tp.rank}</td>
+ <td class="no-stretch"><a href="${request.route_url('player_info', id=tp.player_id)}" title="Go to the player info page for this player">${tp.nick|n}</a></td>
+ <td>${tp.alivetime}</td>
+ </tr>
+ % endfor
+ </tbody>
+ </table>
+ <p class="text-center"><small>Note: these figures are from the past ${lifetime} days</small>
+ </div>
+ </div>
+
+ % if len(top_players) == 20:
+ <div class="row">
+ <div class="small-12 large-6 large-offset-3 columns">
+ <ul class="pagination">
+ <li>
+ <a href="${request.route_url('server_top_active', id=server_id, _query=query)}" name="Next Page">Next <i class="fa fa-arrow-right"></i></a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ % endif
+
+% endif
--- /dev/null
+<%inherit file="base.mako"/>
+<%namespace name="nav" file="nav.mako" />
+
+<%block name="navigation">
+ ${nav.nav('servers')}
+</%block>
+
+<%block name="title">
+ Server Top Map Index
+</%block>
+
+% if not top_maps and last is not None:
+ <h2 class="text-center">Sorry, no more maps!</h2>
+
+% elif not top_maps and last is None:
+ <h2 class="text-center">No maps found. Yikes, get playing!</h2>
+
+% else:
+ <div class="row">
+ <div class="small-12 large-6 large-offset-3 columns">
+ <table class="table-hover table-condensed">
+ <thead>
+ <tr>
+ <th class="small-2">#</th>
+ <th class="small-7">Map</th>
+ <th class="small-3">Times Played</th>
+ </tr>
+ </thead>
+ <tbody>
+ % for tm in top_maps:
+ <tr>
+ <td>${tm.rank}</td>
+ <td class="no-stretch"><a href="${request.route_url('map_info', id=tm.map_id)}" title="Go to the map info page for this map">${tm.name|n}</a></td>
+ <td>${tm.times_played}</td>
+ </tr>
+ % endfor
+ </tbody>
+ </table>
+ <p class="text-center"><small>Note: these figures are from the past ${lifetime} days</small>
+ </div>
+ </div>
+
+ % if len(top_maps) == 20:
+ <div class="row">
+ <div class="small-12 large-6 large-offset-3 columns">
+ <ul class="pagination">
+ <li>
+ <a href="${request.route_url('server_top_maps', id=server_id, _query=query)}" name="Next Page">Next <i class="fa fa-arrow-right"></i></a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ % endif
+
+% endif
--- /dev/null
+<%inherit file="base.mako"/>
+<%namespace name="nav" file="nav.mako" />
+
+<%block name="navigation">
+ ${nav.nav('servers')}
+</%block>
+
+<%block name="title">
+ Server Top Scorer Index
+</%block>
+
+% if not top_scorers and last is not None:
+ <h2 class="text-center">Sorry, no more players!</h2>
+
+% elif not top_scorers and last is None:
+ <h2 class="text-center">No players found. Yikes, get playing!</h2>
+
+% else:
+ <div class="row">
+ <div class="small-12 large-6 large-offset-3 columns">
+ <table class="table-hover table-condensed">
+ <thead>
+ <tr>
+ <th class="small-2">#</th>
+ <th class="small-7">Nick</th>
+ <th class="small-3">Score</th>
+ </tr>
+ </thead>
+ <tbody>
+ % for ts in top_scorers:
+ <tr>
+ <td>${ts.rank}</td>
+ <td class="no-stretch"><a href="${request.route_url('player_info', id=ts.player_id)}" title="Go to the player info page for this player">${ts.nick|n}</a></td>
+ <td>${ts.total_score}</td>
+ </tr>
+ % endfor
+ </tbody>
+ </table>
+ <p class="text-center"><small>Note: these figures are from the past ${lifetime} days</small>
+ </div>
+ </div>
+
+ % if len(top_scorers) == 20:
+ <div class="row">
+ <div class="small-12 large-6 large-offset-3 columns">
+ <ul class="pagination">
+ <li>
+ <a href="${request.route_url('server_top_scorers', id=server_id, _query=query)}" name="Next Page">Next <i class="fa fa-arrow-right"></i></a>
+ </li>
+ </ul>
+ </div>
+ </div>
+ % endif
+
+% endif
from xonstat.views.map import map_info_json, map_index_json
from xonstat.views.map import map_captimes, map_captimes_json
-from xonstat.views.server import server_info, server_index
-from xonstat.views.server import server_info_json
-from xonstat.views.server import server_index_json
+from xonstat.views.server import ServerIndex, ServerTopMaps, ServerTopScorers, ServerTopPlayers
+from xonstat.views.server import ServerInfo
from xonstat.views.search import search_q, search
from xonstat.views.search import search_json
-from pyramid.response import Response
-from pyramid.httpexceptions import HTTPForbidden, HTTPFound
-from pyramid.security import remember, forget
+from pyramid.httpexceptions import HTTPFound
+from pyramid.security import remember
from pyramid.session import check_csrf_token
from pyramid_persona.views import verify_login
-from xonstat.models import *
+from xonstat.models import DBSession, Player
+
def forbidden(request):
'''A simple forbidden view. Does nothing more than set the status and then
-import datetime
import logging
-import re
-import time
from collections import OrderedDict
+
from pyramid import httpexceptions
-from pyramid.response import Response
-from sqlalchemy import desc, func, over
from sqlalchemy.orm.exc import *
-from webhelpers.paginate import Page, PageURL
-from xonstat.models import *
+from webhelpers.paginate import Page
+from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, PlayerWeaponStat
+from xonstat.models import TeamGameStat, PlayerRank, GameType, Weapon
from xonstat.util import page_url
from xonstat.views.helpers import RecentGame, recent_games_q
-
log = logging.getLogger(__name__)
raise httpexceptions.HTTPNotFound("Could not find that game!")
except Exception as e:
- raise inst
+ raise e
return {'game':game,
'server':server,
import logging
-import sqlalchemy.sql.expression as expr
+from calendar import timegm
from datetime import datetime
+
+import sqlalchemy.sql.expression as expr
from sqlalchemy.orm import aliased
-from xonstat.models import *
-from xonstat.util import *
+from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, GameType
+from xonstat.util import pretty_date, html_colors
log = logging.getLogger(__name__)
import logging
-import sqlalchemy as sa
-import sqlalchemy.sql.functions as func
-import sqlalchemy.sql.expression as expr
-from beaker.cache import cache_regions, cache_region
-from collections import namedtuple
from datetime import datetime, timedelta
-from pyramid.response import Response
-from xonstat.models import *
-from xonstat.util import *
-from xonstat.views.helpers import RecentGame, recent_games_q
-from webhelpers.paginate import Page
+from beaker.cache import cache_region
+from xonstat.models import DBSession, PlayerRank, ActivePlayer, ActiveServer, ActiveMap
+from xonstat.views.helpers import RecentGame, recent_games_q
log = logging.getLogger(__name__)
)
except Exception as e:
- raise e
stat_line = None
+ raise e
return stat_line
import logging
-import sqlalchemy.sql.functions as func
-import sqlalchemy.sql.expression as expr
from collections import namedtuple
from datetime import datetime, timedelta
+
+import sqlalchemy.sql.expression as expr
+import sqlalchemy.sql.functions as func
+from pyramid import httpexceptions
from webhelpers.paginate import Page
-from xonstat.models import *
+from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, Player, PlayerCaptime
+from xonstat.models.map import MapCapTime
from xonstat.util import page_url, html_colors
from xonstat.views.helpers import RecentGame, recent_games_q
order_by(expr.asc(PlayerCaptime.fastest_cap))
except Exception as e:
- raise pyramid.httpexceptions.HTTPNotFound
+ raise httpexceptions.HTTPNotFound
map_captimes = Page(mct_q, current_page, items_per_page=20, url=page_url)
import datetime
import logging
+from calendar import timegm
+from collections import namedtuple
+from urllib import unquote
+
import pyramid.httpexceptions
import sqlalchemy as sa
-import sqlalchemy.sql.functions as func
import sqlalchemy.sql.expression as expr
-from calendar import timegm
-from collections import namedtuple
+import sqlalchemy.sql.functions as func
from webhelpers.paginate import Page
-from xonstat.models import *
-from xonstat.util import page_url, to_json, pretty_date, datetime_seconds
+from xonstat.models import DBSession, Server, Map, Game, PlayerWeaponStat, Player, Hashkey
+from xonstat.models import PlayerElo, PlayerCaptime, PlayerMedal, GameType
+from xonstat.models.player import PlayerCapTime
from xonstat.util import is_cake_day, verify_request
+from xonstat.util import page_url, to_json, pretty_date, datetime_seconds
from xonstat.views.helpers import RecentGame, recent_games_q
-from urllib import unquote
log = logging.getLogger(__name__)
-import datetime
import logging
-import pyramid.httpexceptions
-import re
-import time
-from pyramid.response import Response
-from sqlalchemy import desc
-from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
+
from sqlalchemy import func
-from xonstat.models import *
-from xonstat.util import strip_colors, qfont_decode
-from xonstat.util import page_url, html_colors
-from webhelpers.paginate import Page, PageURL
+from webhelpers.paginate import Page
+from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, Player
+from xonstat.util import page_url
log = logging.getLogger(__name__)
import logging
import sqlalchemy.sql.functions as func
import sqlalchemy.sql.expression as expr
+from collections import namedtuple
from datetime import datetime, timedelta
+from pyramid.httpexceptions import HTTPNotFound
+from sqlalchemy import func as fg
from webhelpers.paginate import Page
-from xonstat.models import *
+from xonstat.models import DBSession, Player, Server, Map, Game, PlayerGameStat
from xonstat.util import page_url, html_colors
from xonstat.views.helpers import RecentGame, recent_games_q
log = logging.getLogger(__name__)
-def _server_index_data(request):
- if request.params.has_key('page'):
- current_page = request.params['page']
- else:
- current_page = 1
-
- try:
- server_q = DBSession.query(Server).\
- order_by(Server.server_id.desc())
-
- servers = Page(server_q, current_page, items_per_page=25, url=page_url)
-
-
- except Exception as e:
- servers = None
-
- return {'servers':servers, }
-
-
-def server_index(request):
- """
- Provides a list of all the current servers.
- """
- return _server_index_data(request)
-
-
-def server_index_json(request):
- """
- Provides a list of all the current servers. JSON.
- """
- return [{'status':'not implemented'}]
-
-
-def _server_info_data(request):
- server_id = request.matchdict['id']
-
- try:
- leaderboard_lifetime = int(
- request.registry.settings['xonstat.leaderboard_lifetime'])
- except:
- leaderboard_lifetime = 30
-
- leaderboard_count = 10
- recent_games_count = 20
-
- try:
- server = DBSession.query(Server).filter_by(server_id=server_id).one()
-
- # top maps by total times played
- top_maps = DBSession.query(Game.map_id, Map.name,
- func.count()).\
- filter(Map.map_id==Game.map_id).\
- filter(Game.server_id==server.server_id).\
- filter(Game.create_dt >
- (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
- order_by(expr.desc(func.count())).\
- group_by(Game.map_id).\
- group_by(Map.name).all()[0:leaderboard_count]
-
- # top players by score
- top_scorers = DBSession.query(Player.player_id, Player.nick,
- func.sum(PlayerGameStat.score)).\
- filter(Player.player_id == PlayerGameStat.player_id).\
- filter(Game.game_id == PlayerGameStat.game_id).\
- filter(Game.server_id == server.server_id).\
- filter(Player.player_id > 2).\
- filter(PlayerGameStat.create_dt >
- (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
- order_by(expr.desc(func.sum(PlayerGameStat.score))).\
- group_by(Player.nick).\
- group_by(Player.player_id).all()[0:leaderboard_count]
-
- top_scorers = [(player_id, html_colors(nick), score) \
- for (player_id, nick, score) in top_scorers]
-
- # top players by playing time
- top_players = DBSession.query(Player.player_id, Player.nick,
- func.sum(PlayerGameStat.alivetime)).\
- filter(Player.player_id == PlayerGameStat.player_id).\
- filter(Game.game_id == PlayerGameStat.game_id).\
- filter(Game.server_id == server.server_id).\
- filter(Player.player_id > 2).\
- filter(PlayerGameStat.create_dt >
- (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
- order_by(expr.desc(func.sum(PlayerGameStat.alivetime))).\
- group_by(Player.nick).\
- group_by(Player.player_id).all()[0:leaderboard_count]
-
- top_players = [(player_id, html_colors(nick), score) \
- for (player_id, nick, score) in top_players]
-
- # recent games played in descending order
- rgs = recent_games_q(server_id=server_id).limit(recent_games_count).all()
- recent_games = [RecentGame(row) for row in rgs]
-
- except Exception as e:
- server = None
- recent_games = None
- top_players = None
- raise e
- return {'server':server,
- 'recent_games':recent_games,
- 'top_players': top_players,
- 'top_scorers': top_scorers,
- 'top_maps': top_maps,
- }
-
-
-def server_info(request):
- """
- List the stored information about a given server.
- """
- serverinfo_data = _server_info_data(request)
-
- # FIXME: code clone, should get these from _server_info_data
- leaderboard_count = 10
- recent_games_count = 20
-
- for i in range(leaderboard_count-len(serverinfo_data['top_maps'])):
- serverinfo_data['top_maps'].append(('-', '-', '-'))
-
- for i in range(leaderboard_count-len(serverinfo_data['top_scorers'])):
- serverinfo_data['top_scorers'].append(('-', '-', '-'))
-
- for i in range(leaderboard_count-len(serverinfo_data['top_players'])):
- serverinfo_data['top_players'].append(('-', '-', '-'))
-
- return serverinfo_data
-
-
-def server_info_json(request):
- """
- List the stored information about a given server. JSON.
- """
- return [{'status':'not implemented'}]
+
+# Defaults
+LEADERBOARD_LIFETIME = 30
+LEADERBOARD_COUNT = 10
+INDEX_COUNT = 20
+RECENT_GAMES_COUNT = 20
+
+
+class ServerIndex(object):
+ """Returns a list of servers."""
+
+ def __init__(self, request):
+ """Common parameter parsing."""
+ self.request = request
+ self.page = request.params.get("page", 1)
+ self.servers = self.raw()
+
+ def raw(self):
+ """Returns the raw data shared by all renderers."""
+ try:
+ server_q = DBSession.query(Server).order_by(Server.server_id.desc())
+ servers = Page(server_q, self.page, items_per_page=25, url=page_url)
+
+ except Exception as e:
+ log.debug(e)
+ raise HTTPNotFound
+
+ return servers
+
+ def html(self):
+ """For rendering this data using something HTML-based."""
+ return {
+ 'servers': self.servers,
+ }
+
+ def json(self):
+ """For rendering this data using JSON."""
+ return {
+ 'servers': [s.to_dict() for s in self.servers],
+ }
+
+
+class ServerInfoBase(object):
+ """Baseline parameter parsing for Server URLs with a server_id in them."""
+
+ def __init__(self, request, limit=None, last=None):
+ """Common parameter parsing."""
+ self.request = request
+ self.server_id = request.matchdict.get("id", None)
+
+ raw_lifetime = request.registry.settings.get('xonstat.leaderboard_lifetime',
+ LEADERBOARD_LIFETIME)
+ self.lifetime = int(raw_lifetime)
+
+ self.limit = request.params.get("limit", limit)
+ self.last = request.params.get("last", last)
+ self.now = datetime.utcnow()
+
+
+class ServerTopMaps(ServerInfoBase):
+ """Returns the top maps played on a given server."""
+
+ def __init__(self, request, limit=INDEX_COUNT, last=None):
+ """Common parameter parsing."""
+ super(ServerTopMaps, self).__init__(request, limit, last)
+
+ self.top_maps = self.raw()
+
+ def raw(self):
+ """Returns the raw data shared by all renderers."""
+ try:
+ top_maps_q = DBSession.query(
+ fg.row_number().over(order_by=expr.desc(func.count())).label("rank"),
+ Game.map_id, Map.name, func.count().label("times_played"))\
+ .filter(Map.map_id == Game.map_id)\
+ .filter(Game.server_id == self.server_id)\
+ .filter(Game.create_dt > (self.now - timedelta(days=self.lifetime)))\
+ .group_by(Game.map_id)\
+ .group_by(Map.name) \
+ .order_by(expr.desc(func.count()))
+
+ if self.last:
+ top_maps_q = top_maps_q.offset(self.last)
+
+ if self.limit:
+ top_maps_q = top_maps_q.limit(self.limit)
+
+ top_maps = top_maps_q.all()
+ except Exception as e:
+ log.debug(e)
+ raise HTTPNotFound
+
+ return top_maps
+
+ def html(self):
+ """Returns the HTML-ready representation."""
+
+ # build the query string
+ query = {}
+ if len(self.top_maps) > 1:
+ query['last'] = self.top_maps[-1].rank
+
+ return {
+ "server_id": self.server_id,
+ "top_maps": self.top_maps,
+ "lifetime": self.lifetime,
+ "query": query,
+ }
+
+ def json(self):
+ """For rendering this data using JSON."""
+ top_maps = [{
+ "rank": tm.rank,
+ "map_id": tm.map_id,
+ "map_name": tm.name,
+ "times_played": tm.times_played,
+ } for tm in self.top_maps]
+
+ return {
+ "server_id": self.server_id,
+ "top_maps": top_maps,
+ }
+
+
+class ServerTopScorers(ServerInfoBase):
+ """Returns the top scorers on a given server."""
+
+ def __init__(self, request, limit=INDEX_COUNT, last=None):
+ """Common parameter parsing."""
+ super(ServerTopScorers, self).__init__(request, limit, last)
+ self.top_scorers = self.raw()
+
+ def raw(self):
+ """Top scorers on this server by total score."""
+ try:
+ top_scorers_q = DBSession.query(
+ fg.row_number().over(
+ order_by=expr.desc(func.sum(PlayerGameStat.score))).label("rank"),
+ Player.player_id, Player.nick,
+ func.sum(PlayerGameStat.score).label("total_score"))\
+ .filter(Player.player_id == PlayerGameStat.player_id)\
+ .filter(Game.game_id == PlayerGameStat.game_id)\
+ .filter(Game.server_id == self.server_id)\
+ .filter(Player.player_id > 2)\
+ .filter(PlayerGameStat.create_dt >
+ (self.now - timedelta(days=self.lifetime)))\
+ .order_by(expr.desc(func.sum(PlayerGameStat.score)))\
+ .group_by(Player.nick)\
+ .group_by(Player.player_id)
+
+ if self.last:
+ top_scorers_q = top_scorers_q.offset(self.last)
+
+ if self.limit:
+ top_scorers_q = top_scorers_q.limit(self.limit)
+
+ top_scorers = top_scorers_q.all()
+
+ except Exception as e:
+ log.debug(e)
+ raise HTTPNotFound
+
+ return top_scorers
+
+ def html(self):
+ """Returns an HTML-ready representation."""
+ TopScorer = namedtuple("TopScorer", ["rank", "player_id", "nick", "total_score"])
+
+ top_scorers = [TopScorer(ts.rank, ts.player_id, html_colors(ts.nick), ts.total_score)
+ for ts in self.top_scorers]
+
+ # build the query string
+ query = {}
+ if len(top_scorers) > 1:
+ query['last'] = top_scorers[-1].rank
+
+ return {
+ "server_id": self.server_id,
+ "top_scorers": top_scorers,
+ "lifetime": self.lifetime,
+ "query": query,
+ }
+
+ def json(self):
+ """For rendering this data using JSON."""
+ top_scorers = [{
+ "rank": ts.rank,
+ "player_id": ts.player_id,
+ "nick": ts.nick,
+ "score": ts.total_score,
+ } for ts in self.top_scorers]
+
+ return {
+ "server_id": self.server_id,
+ "top_scorers": top_scorers,
+ }
+
+
+class ServerTopPlayers(ServerInfoBase):
+ """Returns the top players by playing time on a given server."""
+
+ def __init__(self, request, limit=INDEX_COUNT, last=None):
+ """Common parameter parsing."""
+ super(ServerTopPlayers, self).__init__(request, limit, last)
+ self.top_players = self.raw()
+
+ def raw(self):
+ """Top players on this server by total playing time."""
+ try:
+ top_players_q = DBSession.query(
+ fg.row_number().over(
+ order_by=expr.desc(func.sum(PlayerGameStat.alivetime))).label("rank"),
+ Player.player_id, Player.nick,
+ func.sum(PlayerGameStat.alivetime).label("alivetime"))\
+ .filter(Player.player_id == PlayerGameStat.player_id)\
+ .filter(Game.game_id == PlayerGameStat.game_id)\
+ .filter(Game.server_id == self.server_id)\
+ .filter(Player.player_id > 2)\
+ .filter(PlayerGameStat.create_dt > (self.now - timedelta(days=self.lifetime)))\
+ .order_by(expr.desc(func.sum(PlayerGameStat.alivetime)))\
+ .group_by(Player.nick)\
+ .group_by(Player.player_id)
+
+ if self.last:
+ top_players_q = top_players_q.offset(self.last)
+
+ if self.limit:
+ top_players_q = top_players_q.limit(self.limit)
+
+ top_players = top_players_q.all()
+
+ except Exception as e:
+ log.debug(e)
+ raise HTTPNotFound
+
+ return top_players
+
+ def html(self):
+ """Returns the HTML-ready representation."""
+ TopPlayer = namedtuple("TopPlayer", ["rank", "player_id", "nick", "alivetime"])
+
+ top_players = [TopPlayer(tp.rank, tp.player_id, html_colors(tp.nick), tp.alivetime)
+ for tp in self.top_players]
+
+ # build the query string
+ query = {}
+ if len(top_players) > 1:
+ query['last'] = top_players[-1].rank
+
+ return {
+ "server_id": self.server_id,
+ "top_players": top_players,
+ "lifetime": self.lifetime,
+ "query": query,
+ }
+
+ def json(self):
+ """For rendering this data using JSON."""
+ top_players = [{
+ "rank": ts.rank,
+ "player_id": ts.player_id,
+ "nick": ts.nick,
+ "time": ts.alivetime.total_seconds(),
+ } for ts in self.top_players]
+
+ return {
+ "server_id": self.server_id,
+ "top_players": top_players,
+ }
+
+
+class ServerInfo(ServerInfoBase):
+ """Returns detailed information about a particular server."""
+
+ def __init__(self, request):
+ """Common data and parameters."""
+ super(ServerInfo, self).__init__(request)
+
+ # this view uses data from other views, so we'll save the data at that level
+ try:
+ self.server = DBSession.query(Server).filter_by(server_id=self.server_id).one()
+ self.top_maps_v = ServerTopMaps(self.request, limit=LEADERBOARD_COUNT)
+ self.top_scorers_v = ServerTopScorers(self.request, limit=LEADERBOARD_COUNT)
+ self.top_players_v = ServerTopPlayers(self.request, limit=LEADERBOARD_COUNT)
+
+ rgs = recent_games_q(server_id=self.server_id).limit(RECENT_GAMES_COUNT).all()
+ self.recent_games = [RecentGame(row) for row in rgs]
+ except:
+ raise HTTPNotFound
+
+ def html(self):
+ """For rendering this data using something HTML-based."""
+ return {
+ 'server': self.server,
+ 'top_players': self.top_players_v.html().get("top_players", None),
+ 'top_scorers': self.top_scorers_v.html().get("top_scorers", None),
+ 'top_maps': self.top_maps_v.html().get("top_maps", None),
+ 'recent_games': self.recent_games,
+ 'lifetime': self.lifetime,
+ }
+
+ def json(self):
+ """For rendering this data using JSON."""
+ return {
+ 'server': self.server.to_dict(),
+ 'top_players': self.top_players_v.json(),
+ 'top_scorers': self.top_scorers_v.json(),
+ 'top_maps': self.top_maps_v.json(),
+ 'recent_games': [rg.to_dict() for rg in self.recent_games],
+ }
import datetime
import logging
-import os
-import pyramid.httpexceptions
import re
-import time
+
+import pyramid.httpexceptions
import sqlalchemy.sql.expression as expr
-from calendar import timegm
-from pyramid.response import Response
from sqlalchemy import Sequence
from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
from xonstat.elo import EloProcessor
-from xonstat.models import *
+from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, PlayerWeaponStat
+from xonstat.models import PlayerRank, PlayerCaptime
+from xonstat.models import TeamGameStat, PlayerGameAnticheat, Player, Hashkey, PlayerNick
from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
-
log = logging.getLogger(__name__)