Return actual data in the /map/<map id> JSON views.
[xonotic/xonstat.git] / xonstat / views / map.py
1 import logging
2 from collections import namedtuple
3 from datetime import datetime, timedelta
4
5 import sqlalchemy.sql.expression as expr
6 import sqlalchemy.sql.functions as func
7 from pyramid.httpexceptions import HTTPNotFound
8 from sqlalchemy import func as fg
9 from webhelpers.paginate import Page
10 from xonstat.models import DBSession, Server, Map, Game, PlayerGameStat, Player, PlayerCaptime
11 from xonstat.models.map import MapCapTime
12 from xonstat.util import page_url, html_colors
13 from xonstat.views.helpers import RecentGame, recent_games_q
14
15 log = logging.getLogger(__name__)
16
17 # Defaults
18 INDEX_COUNT = 20
19 LEADERBOARD_LIFETIME = 30
20
21
22 class MapIndex(object):
23     """Returns a list of maps."""
24
25     def __init__(self, request):
26         """Common parameter parsing."""
27         self.request = request
28         self.last = request.params.get("last", None)
29
30         # all views share this data, so we'll pre-calculate
31         self.maps = self.map_index()
32
33     def map_index(self):
34         """Returns the raw data shared by all renderers."""
35         try:
36             map_q = DBSession.query(Map)
37
38             if self.last:
39                 map_q = map_q.filter(Map.map_id < self.last)
40
41             map_q = map_q.order_by(Map.map_id.desc()).limit(INDEX_COUNT)
42             maps = map_q.all()
43
44         except Exception as e:
45             log.debug(e)
46             raise HTTPNotFound
47
48         return maps
49
50     def html(self):
51         """For rendering this data using something HTML-based."""
52         # build the query string
53         query = {}
54         if len(self.maps) > 1:
55             query['last'] = self.maps[-1].map_id
56
57         return {
58             'maps': self.maps,
59             'query': query,
60         }
61
62     def json(self):
63         """For rendering this data using JSON."""
64         return {
65             'maps': [m.to_dict() for m in self.maps],
66             'last': self.last,
67         }
68
69
70 class MapInfoBase(object):
71     """Base class for all map-based views with a map_id parameter in them."""
72
73     def __init__(self, request, limit=None, last=None):
74         """Common parameter parsing."""
75         self.request = request
76         self.map_id = request.matchdict.get("id", None)
77
78         raw_lifetime = request.registry.settings.get('xonstat.leaderboard_lifetime',
79                                                      LEADERBOARD_LIFETIME)
80         self.lifetime = int(raw_lifetime)
81
82         self.limit = request.params.get("limit", limit)
83         self.last = request.params.get("last", last)
84         self.now = datetime.utcnow()
85
86
87 class MapTopScorers(MapInfoBase):
88     """Returns the top scorers on a given map."""
89
90     def __init__(self, request, limit=INDEX_COUNT, last=None):
91         """Common parameter parsing."""
92         super(MapTopScorers, self).__init__(request, limit, last)
93         self.top_scorers = self.get_top_scorers()
94
95     def get_top_scorers(self):
96         """Top players by score. Shared by all renderers."""
97         cutoff = self.now - timedelta(days=self.lifetime)
98
99         try:
100             top_scorers_q = DBSession.query(
101                 fg.row_number().over(order_by=expr.desc(func.sum(PlayerGameStat.score))).label("rank"),
102                 Player.player_id, Player.nick, func.sum(PlayerGameStat.score).label("total_score"))\
103                 .filter(Player.player_id == PlayerGameStat.player_id)\
104                 .filter(Game.game_id == PlayerGameStat.game_id)\
105                 .filter(Game.map_id == self.map_id)\
106                 .filter(Player.player_id > 2)\
107                 .filter(PlayerGameStat.create_dt > cutoff)\
108                 .order_by(expr.desc(func.sum(PlayerGameStat.score)))\
109                 .group_by(Player.nick)\
110                 .group_by(Player.player_id)
111
112             if self.last:
113                 top_scorers_q = top_scorers_q.offset(self.last)
114
115             if self.limit:
116                 top_scorers_q = top_scorers_q.limit(self.limit)
117
118             top_scorers = top_scorers_q.all()
119
120             return top_scorers
121         except Exception as e:
122             log.debug(e)
123             raise HTTPNotFound
124
125     def html(self):
126         """Returns an HTML-ready representation."""
127         TopScorer = namedtuple("TopScorer", ["rank", "player_id", "nick", "total_score"])
128
129         top_scorers = [TopScorer(ts.rank, ts.player_id, html_colors(ts.nick), ts.total_score)
130                        for ts in self.top_scorers]
131
132         # build the query string
133         query = {}
134         if len(top_scorers) > 1:
135             query['last'] = top_scorers[-1].rank
136
137         return {
138             "map_id": self.map_id,
139             "top_scorers": top_scorers,
140             "lifetime": self.lifetime,
141             "query": query,
142         }
143
144     def json(self):
145         """For rendering this data using JSON."""
146         top_scorers = [{
147             "rank": ts.rank,
148             "player_id": ts.player_id,
149             "nick": ts.nick,
150             "score": ts.total_score,
151         } for ts in self.top_scorers]
152
153         return {
154             "map_id": self.map_id,
155             "top_scorers": top_scorers,
156         }
157
158
159 class MapTopPlayers(MapInfoBase):
160     """Returns the top players by time on a given map."""
161
162     def __init__(self, request, limit=INDEX_COUNT, last=None):
163         """Common parameter parsing."""
164         super(MapTopPlayers, self).__init__(request, limit, last)
165         self.top_players = self.get_top_players()
166
167     def get_top_players(self):
168         """Top players by score. Shared by all renderers."""
169         cutoff = self.now - timedelta(days=self.lifetime)
170
171         try:
172             top_players_q = DBSession.query(
173                 fg.row_number().over(order_by=expr.desc(func.sum(PlayerGameStat.alivetime))).label("rank"),
174                 Player.player_id, Player.nick, func.sum(PlayerGameStat.alivetime).label("alivetime"))\
175                 .filter(Player.player_id == PlayerGameStat.player_id)\
176                 .filter(Game.game_id == PlayerGameStat.game_id)\
177                 .filter(Game.map_id == self.map_id)\
178                 .filter(Player.player_id > 2)\
179                 .filter(PlayerGameStat.create_dt > cutoff)\
180                 .order_by(expr.desc(func.sum(PlayerGameStat.alivetime)))\
181                 .group_by(Player.nick)\
182                 .group_by(Player.player_id)
183
184             if self.last:
185                 top_players_q = top_players_q.offset(self.last)
186
187             if self.limit:
188                 top_players_q = top_players_q.limit(self.limit)
189
190             top_players = top_players_q.all()
191
192             return top_players
193         except Exception as e:
194             log.debug(e)
195             raise HTTPNotFound
196
197     def html(self):
198         """Returns the HTML-ready representation."""
199         TopPlayer = namedtuple("TopPlayer", ["rank", "player_id", "nick", "alivetime"])
200
201         top_players = [TopPlayer(tp.rank, tp.player_id, html_colors(tp.nick), tp.alivetime)
202                        for tp in self.top_players]
203
204         # build the query string
205         query = {}
206         if len(top_players) > 1:
207             query['last'] = top_players[-1].rank
208
209         return {
210             "map_id": self.map_id,
211             "top_players": top_players,
212             "lifetime": self.lifetime,
213             "last": query.get("last", None),
214             "query": query,
215         }
216
217     def json(self):
218         """For rendering this data using JSON."""
219         top_players = [{
220             "rank": ts.rank,
221             "player_id": ts.player_id,
222             "nick": ts.nick,
223             "time": ts.alivetime.total_seconds(),
224         } for ts in self.top_players]
225
226         return {
227             "map_id": self.map_id,
228             "top_players": top_players,
229         }
230
231
232 class MapTopServers(MapInfoBase):
233     """Returns the top servers by the number of times they've played a given map."""
234
235     def __init__(self, request, limit=INDEX_COUNT, last=None):
236         """Common parameter parsing."""
237         super(MapTopServers, self).__init__(request, limit, last)
238         self.top_servers = self.get_top_servers()
239
240     def get_top_servers(self):
241         """Top servers by the number of times they have played the map. Shared by all renderers."""
242         cutoff = self.now - timedelta(days=self.lifetime)
243
244         try:
245             top_servers_q = DBSession.query(
246                 fg.row_number().over(order_by=expr.desc(func.count(Game.game_id))).label("rank"),
247                 Server.server_id, Server.name, func.count(Game.game_id).label("games"))\
248                 .filter(Game.server_id == Server.server_id)\
249                 .filter(Game.map_id == self.map_id)\
250                 .filter(Game.create_dt > cutoff)\
251                 .order_by(expr.desc(func.count(Game.game_id)))\
252                 .group_by(Server.name)\
253                 .group_by(Server.server_id)
254
255             if self.last:
256                 top_servers_q = top_servers_q.offset(self.last)
257
258             if self.limit:
259                 top_servers_q = top_servers_q.limit(self.limit)
260
261             top_servers = top_servers_q.all()
262
263             return top_servers
264         except Exception as e:
265             log.debug(e)
266             raise HTTPNotFound
267
268     def html(self):
269         """Returns the HTML-ready representation."""
270         TopServer = namedtuple("TopServer", ["rank", "server_id", "server_name", "games"])
271
272         top_servers = [TopServer(ts.rank, ts.server_id, ts.name, ts.games)
273                        for ts in self.top_servers]
274
275         # build the query string
276         query = {}
277         if len(top_servers) > 1:
278             query['last'] = top_servers[-1].rank
279
280         return {
281             "map_id": self.map_id,
282             "top_servers": top_servers,
283             "lifetime": self.lifetime,
284             "last": query.get("last", None),
285             "query": query,
286         }
287
288     def json(self):
289         """For rendering this data using JSON."""
290         top_servers = [{
291             "rank": ts.rank,
292             "server_id": ts.server_id,
293             "server_name": ts.server_name,
294             "games": ts.games,
295         } for ts in self.top_servers]
296
297         return {
298             "map_id": self.map_id,
299             "top_servers": top_servers,
300         }
301
302
303 def _map_info_data(request):
304     map_id = int(request.matchdict['id'])
305
306     try:
307         leaderboard_lifetime = int(
308                 request.registry.settings['xonstat.leaderboard_lifetime'])
309     except:
310         leaderboard_lifetime = 30
311
312     leaderboard_count = 10
313     recent_games_count = 20
314
315     # captime tuples
316     Captime = namedtuple('Captime', ['player_id', 'nick_html_colors',
317         'fastest_cap', 'game_id'])
318
319     try:
320         gmap = DBSession.query(Map).filter_by(map_id=map_id).one()
321
322         # recent games played in descending order
323         rgs = recent_games_q(map_id=map_id).limit(recent_games_count).all()
324         recent_games = [RecentGame(row) for row in rgs]
325
326         # top players by score
327         top_scorers = DBSession.query(Player.player_id, Player.nick,
328                 func.sum(PlayerGameStat.score)).\
329                 filter(Player.player_id == PlayerGameStat.player_id).\
330                 filter(Game.game_id == PlayerGameStat.game_id).\
331                 filter(Game.map_id == map_id).\
332                 filter(Player.player_id > 2).\
333                 filter(PlayerGameStat.create_dt >
334                         (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
335                 order_by(expr.desc(func.sum(PlayerGameStat.score))).\
336                 group_by(Player.nick).\
337                 group_by(Player.player_id).all()[0:leaderboard_count]
338
339         top_scorers = [(player_id, html_colors(nick), score, nick)
340                        for (player_id, nick, score) in top_scorers]
341
342         # top players by playing time
343         top_players = DBSession.query(Player.player_id, Player.nick,
344                 func.sum(PlayerGameStat.alivetime)).\
345                 filter(Player.player_id == PlayerGameStat.player_id).\
346                 filter(Game.game_id == PlayerGameStat.game_id).\
347                 filter(Game.map_id == map_id).\
348                 filter(Player.player_id > 2).\
349                 filter(PlayerGameStat.create_dt >
350                         (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
351                 order_by(expr.desc(func.sum(PlayerGameStat.alivetime))).\
352                 group_by(Player.nick).\
353                 group_by(Player.player_id).all()[0:leaderboard_count]
354
355         top_players = [(player_id, html_colors(nick), score, nick)
356                        for (player_id, nick, score) in top_players]
357
358         # top servers using/playing this map
359         top_servers = DBSession.query(Server.server_id, Server.name,
360                 func.count(Game.game_id)).\
361                 filter(Game.server_id == Server.server_id).\
362                 filter(Game.map_id == map_id).\
363                 filter(Game.create_dt >
364                         (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
365                 order_by(expr.desc(func.count(Game.game_id))).\
366                 group_by(Server.name).\
367                 group_by(Server.server_id).all()[0:leaderboard_count]
368
369         # TODO make this a configuration parameter to be set in the settings
370         # top captimes
371         captimes_raw = DBSession.query(Player.player_id, Player.nick,
372             PlayerCaptime.fastest_cap, PlayerCaptime.game_id).\
373                 filter(PlayerCaptime.map_id == map_id).\
374                 filter(Player.player_id == PlayerCaptime.player_id).\
375                 order_by(PlayerCaptime.fastest_cap).\
376                 limit(10).\
377                 all()
378
379         captimes = [Captime(c.player_id, html_colors(c.nick),
380             c.fastest_cap, c.game_id) for c in captimes_raw]
381
382     except Exception as e:
383         gmap = None
384     return {'gmap':gmap,
385             'recent_games':recent_games,
386             'top_scorers':top_scorers,
387             'top_players':top_players,
388             'top_servers':top_servers,
389             'captimes':captimes,
390             }
391
392
393 def map_info(request):
394     """
395     List the information stored about a given map.
396     """
397     mapinfo_data =  _map_info_data(request)
398
399     # FIXME: code clone, should get these from _map_info_data
400     leaderboard_count = 10
401     recent_games_count = 20
402
403     for i in range(leaderboard_count-len(mapinfo_data['top_scorers'])):
404         mapinfo_data['top_scorers'].append(('-', '-', '-'))
405
406     for i in range(leaderboard_count-len(mapinfo_data['top_players'])):
407         mapinfo_data['top_players'].append(('-', '-', '-'))
408
409     for i in range(leaderboard_count-len(mapinfo_data['top_servers'])):
410         mapinfo_data['top_servers'].append(('-', '-', '-'))
411
412     return mapinfo_data
413
414
415 def map_info_json(request):
416     """
417     List the information stored about a given map. JSON.
418     """
419     data = _map_info_data(request)
420
421     # convert the top scorers to a dict
422     def top_scorers_dict(tp):
423         return {
424             "player_id": tp[0],
425             "nick": tp[3],
426             "score": tp[2],
427         }
428
429     top_scorers = [top_scorers_dict(ts) for ts in data["top_scorers"]]
430
431     # convert the top players to a dict
432     def top_players_dict(tp):
433         return {
434             "player_id": tp[0],
435             "nick": tp[3],
436             "alivetime": tp[2].total_seconds(),
437         }
438
439     top_players = [top_players_dict(tp) for tp in data["top_players"]]
440
441     def top_servers_dict(ts):
442         return {
443             "server_id": ts[0],
444             "server_name": ts[1],
445             "games": ts[2],
446         }
447
448     top_servers = [top_servers_dict(ts) for ts in data["top_servers"]]
449
450     return {
451         'map': data["gmap"].to_dict(),
452         'recent_games': [rg.to_dict() for rg in data["recent_games"]],
453         'top_scorers': top_scorers,
454         'top_players': top_players,
455         'top_servers':top_servers,
456         # 'captimes':captimes,
457     }
458
459
460 def map_captimes_data(request):
461     map_id = int(request.matchdict['id'])
462
463     current_page = request.params.get('page', 1)
464
465     try:
466         mmap = DBSession.query(Map).filter_by(map_id=map_id).one()
467
468         mct_q = DBSession.query(PlayerCaptime.fastest_cap, PlayerCaptime.create_dt,
469                 PlayerCaptime.player_id, PlayerCaptime.game_id,
470                 Game.server_id, Server.name.label('server_name'),
471                 PlayerGameStat.nick.label('player_nick')).\
472                 filter(PlayerCaptime.map_id==map_id).\
473                 filter(PlayerCaptime.game_id==Game.game_id).\
474                 filter(PlayerCaptime.map_id==Map.map_id).\
475                 filter(Game.server_id==Server.server_id).\
476                 filter(PlayerCaptime.player_id==PlayerGameStat.player_id).\
477                 filter(PlayerCaptime.game_id==PlayerGameStat.game_id).\
478                 order_by(expr.asc(PlayerCaptime.fastest_cap))
479
480     except Exception as e:
481         raise HTTPNotFound
482
483     map_captimes = Page(mct_q, current_page, items_per_page=20, url=page_url)
484
485     map_captimes.items = [MapCapTime(row) for row in map_captimes.items]
486
487     return {
488             'map_id':map_id,
489             'map':mmap,
490             'captimes':map_captimes,
491         }
492
493 def map_captimes(request):
494     return map_captimes_data(request)
495
496 def map_captimes_json(request):
497     current_page = request.params.get('page', 1)
498     data = map_captimes_data(request)
499
500     return {
501             "map": data["map"].to_dict(),
502             "captimes": [e.to_dict() for e in data["captimes"].items],
503             "page": current_page,
504             }