From: Ant Zucaro Date: Fri, 23 Dec 2016 18:42:02 +0000 (-0500) Subject: Add separate HTML and JSON responses for a map's top scorers. X-Git-Url: https://git.xonotic.org/?p=xonotic%2Fxonstat.git;a=commitdiff_plain;h=7fe3ae838800023c9943948f565752e96d28acdc Add separate HTML and JSON responses for a map's top scorers. --- diff --git a/xonstat/__init__.py b/xonstat/__init__.py index 1d2e075..75c4d2a 100644 --- a/xonstat/__init__.py +++ b/xonstat/__init__.py @@ -171,6 +171,12 @@ def main(global_config, **settings): config.add_view(view=MapIndex, route_name="map_index", attr="json", renderer="json", accept="application/json") + config.add_route("map_top_scorers", "/map/{id:\d+}/topscorers") + config.add_view(view=MapTopScorers, route_name="map_top_scorers", attr="html", + renderer="map_top_scorers.mako", accept="text/html") + config.add_view(view=MapTopScorers, route_name="map_top_scorers", attr="json", + renderer="json", accept="application/json") + config.add_route("map_info", "/map/{id:\d+}") config.add_view(map_info, route_name="map_info", renderer="map_info.mako") diff --git a/xonstat/templates/map_top_scorers.mako b/xonstat/templates/map_top_scorers.mako new file mode 100644 index 0000000..09d273c --- /dev/null +++ b/xonstat/templates/map_top_scorers.mako @@ -0,0 +1,55 @@ +<%inherit file="base.mako"/> +<%namespace name="nav" file="nav.mako" /> + +<%block name="navigation"> + ${nav.nav('maps')} + + +<%block name="title"> + Map Top Scorer Index + + +% if not top_scorers and last is not None: +

Sorry, no more maps!

+ +% elif not top_scorers and last is None: +

No maps found. Yikes, get playing!

+ +% else: +
+
+ + + + + + + + + + % for ts in top_scorers: + + + + + + % endfor + +
#NickScore
${ts.rank}${ts.nick|n}${ts.total_score}
+

Note: these figures are from the past ${lifetime} days

+
+
+ + % if len(top_scorers) == 20: +
+
+ +
+
+ % endif + +% endif diff --git a/xonstat/views/__init__.py b/xonstat/views/__init__.py index 5d7997b..e947cb0 100644 --- a/xonstat/views/__init__.py +++ b/xonstat/views/__init__.py @@ -13,7 +13,7 @@ from xonstat.views.game import game_info, rank_index from xonstat.views.game import game_info_json, rank_index_json from xonstat.views.game import game_finder, game_finder_json -from xonstat.views.map import MapIndex +from xonstat.views.map import MapIndex, MapTopScorers from xonstat.views.map import map_info, map_info_json from xonstat.views.map import map_captimes, map_captimes_json diff --git a/xonstat/views/map.py b/xonstat/views/map.py index ff375e2..702d280 100644 --- a/xonstat/views/map.py +++ b/xonstat/views/map.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta import sqlalchemy.sql.expression as expr import sqlalchemy.sql.functions as func from pyramid.httpexceptions import HTTPNotFound +from sqlalchemy import func as fg from webhelpers.paginate import Page from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, Player, PlayerCaptime from xonstat.models.map import MapCapTime @@ -15,6 +16,7 @@ log = logging.getLogger(__name__) # Defaults INDEX_COUNT = 20 +LEADERBOARD_LIFETIME = 30 class MapIndex(object): @@ -66,6 +68,92 @@ class MapIndex(object): } +class MapInfoBase(object): + """Base class for all map-based views with a map_id parameter in them.""" + + def __init__(self, request, limit=None, last=None): + """Common parameter parsing.""" + self.request = request + self.map_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 MapTopScorers(MapInfoBase): + """Returns the top scorers on a given map.""" + + def __init__(self, request, limit=INDEX_COUNT, last=None): + """Common parameter parsing.""" + super(MapTopScorers, self).__init__(request, limit, last) + self.top_scorers = self.get_top_scorers() + + def get_top_scorers(self): + """Top players by score. Shared by all renderers.""" + cutoff = self.now - timedelta(days=self.lifetime) + cutoff = self.now - timedelta(days=120) + + 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.map_id == self.map_id)\ + .filter(Player.player_id > 2)\ + .filter(PlayerGameStat.create_dt > cutoff)\ + .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() + + 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 { + "map_id": self.map_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 { + "map_id": self.map_id, + "top_scorers": top_scorers, + } + + def _map_info_data(request): map_id = int(request.matchdict['id'])