702d280795a2cceb76b97379efe185f2a9609fcb
[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.page = request.params.get("page", 1)
29         self.last = request.params.get("last", None)
30
31         # all views share this data, so we'll pre-calculate
32         self.maps = self.map_index()
33
34     def map_index(self):
35         """Returns the raw data shared by all renderers."""
36         try:
37             map_q = DBSession.query(Map)
38
39             if self.last:
40                 map_q = map_q.filter(Map.map_id < self.last)
41
42             map_q = map_q.order_by(Map.map_id.desc()).limit(INDEX_COUNT)
43             maps = map_q.all()
44
45         except Exception as e:
46             log.debug(e)
47             raise HTTPNotFound
48
49         return maps
50
51     def html(self):
52         """For rendering this data using something HTML-based."""
53         # build the query string
54         query = {}
55         if len(self.maps) > 1:
56             query['last'] = self.maps[-1].map_id
57
58         return {
59             'maps': self.maps,
60             'query': query,
61         }
62
63     def json(self):
64         """For rendering this data using JSON."""
65         return {
66             'maps': [m.to_dict() for m in self.maps],
67             'last': self.last,
68         }
69
70
71 class MapInfoBase(object):
72     """Base class for all map-based views with a map_id parameter in them."""
73
74     def __init__(self, request, limit=None, last=None):
75         """Common parameter parsing."""
76         self.request = request
77         self.map_id = request.matchdict.get("id", None)
78
79         raw_lifetime = request.registry.settings.get('xonstat.leaderboard_lifetime',
80                                                      LEADERBOARD_LIFETIME)
81         self.lifetime = int(raw_lifetime)
82
83         self.limit = request.params.get("limit", limit)
84         self.last = request.params.get("last", last)
85         self.now = datetime.utcnow()
86
87
88 class MapTopScorers(MapInfoBase):
89     """Returns the top scorers on a given map."""
90
91     def __init__(self, request, limit=INDEX_COUNT, last=None):
92         """Common parameter parsing."""
93         super(MapTopScorers, self).__init__(request, limit, last)
94         self.top_scorers = self.get_top_scorers()
95
96     def get_top_scorers(self):
97         """Top players by score. Shared by all renderers."""
98         cutoff = self.now - timedelta(days=self.lifetime)
99         cutoff = self.now - timedelta(days=120)
100
101         top_scorers_q = DBSession.query(
102             fg.row_number().over(order_by=expr.desc(func.sum(PlayerGameStat.score))).label("rank"),
103             Player.player_id, Player.nick, func.sum(PlayerGameStat.score).label("total_score"))\
104             .filter(Player.player_id == PlayerGameStat.player_id)\
105             .filter(Game.game_id == PlayerGameStat.game_id)\
106             .filter(Game.map_id == self.map_id)\
107             .filter(Player.player_id > 2)\
108             .filter(PlayerGameStat.create_dt > cutoff)\
109             .order_by(expr.desc(func.sum(PlayerGameStat.score)))\
110             .group_by(Player.nick)\
111             .group_by(Player.player_id)
112
113         if self.last:
114             top_scorers_q = top_scorers_q.offset(self.last)
115
116         if self.limit:
117             top_scorers_q = top_scorers_q.limit(self.limit)
118
119         top_scorers = top_scorers_q.all()
120
121         return top_scorers
122
123     def html(self):
124         """Returns an HTML-ready representation."""
125         TopScorer = namedtuple("TopScorer", ["rank", "player_id", "nick", "total_score"])
126
127         top_scorers = [TopScorer(ts.rank, ts.player_id, html_colors(ts.nick), ts.total_score)
128                        for ts in self.top_scorers]
129
130         # build the query string
131         query = {}
132         if len(top_scorers) > 1:
133             query['last'] = top_scorers[-1].rank
134
135         return {
136             "map_id": self.map_id,
137             "top_scorers": top_scorers,
138             "lifetime": self.lifetime,
139             "query": query,
140         }
141
142     def json(self):
143         """For rendering this data using JSON."""
144         top_scorers = [{
145             "rank": ts.rank,
146             "player_id": ts.player_id,
147             "nick": ts.nick,
148             "score": ts.total_score,
149         } for ts in self.top_scorers]
150
151         return {
152             "map_id": self.map_id,
153             "top_scorers": top_scorers,
154         }
155
156
157 def _map_info_data(request):
158     map_id = int(request.matchdict['id'])
159
160     try:
161         leaderboard_lifetime = int(
162                 request.registry.settings['xonstat.leaderboard_lifetime'])
163     except:
164         leaderboard_lifetime = 30
165
166     leaderboard_count = 10
167     recent_games_count = 20
168
169     # captime tuples
170     Captime = namedtuple('Captime', ['player_id', 'nick_html_colors',
171         'fastest_cap', 'game_id'])
172
173     try:
174         gmap = DBSession.query(Map).filter_by(map_id=map_id).one()
175
176         # recent games played in descending order
177         rgs = recent_games_q(map_id=map_id).limit(recent_games_count).all()
178         recent_games = [RecentGame(row) for row in rgs]
179
180         # top players by score
181         top_scorers = DBSession.query(Player.player_id, Player.nick,
182                 func.sum(PlayerGameStat.score)).\
183                 filter(Player.player_id == PlayerGameStat.player_id).\
184                 filter(Game.game_id == PlayerGameStat.game_id).\
185                 filter(Game.map_id == map_id).\
186                 filter(Player.player_id > 2).\
187                 filter(PlayerGameStat.create_dt >
188                         (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
189                 order_by(expr.desc(func.sum(PlayerGameStat.score))).\
190                 group_by(Player.nick).\
191                 group_by(Player.player_id).all()[0:leaderboard_count]
192
193         top_scorers = [(player_id, html_colors(nick), score) \
194                 for (player_id, nick, score) in top_scorers]
195
196         # top players by playing time
197         top_players = DBSession.query(Player.player_id, Player.nick,
198                 func.sum(PlayerGameStat.alivetime)).\
199                 filter(Player.player_id == PlayerGameStat.player_id).\
200                 filter(Game.game_id == PlayerGameStat.game_id).\
201                 filter(Game.map_id == map_id).\
202                 filter(Player.player_id > 2).\
203                 filter(PlayerGameStat.create_dt >
204                         (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
205                 order_by(expr.desc(func.sum(PlayerGameStat.alivetime))).\
206                 group_by(Player.nick).\
207                 group_by(Player.player_id).all()[0:leaderboard_count]
208
209         top_players = [(player_id, html_colors(nick), score) \
210                 for (player_id, nick, score) in top_players]
211
212         # top servers using/playing this map
213         top_servers = DBSession.query(Server.server_id, Server.name,
214                 func.count(Game.game_id)).\
215                 filter(Game.server_id == Server.server_id).\
216                 filter(Game.map_id == map_id).\
217                 filter(Game.create_dt >
218                         (datetime.utcnow() - timedelta(days=leaderboard_lifetime))).\
219                 order_by(expr.desc(func.count(Game.game_id))).\
220                 group_by(Server.name).\
221                 group_by(Server.server_id).all()[0:leaderboard_count]
222
223         # TODO make this a configuration parameter to be set in the settings
224         # top captimes
225         captimes_raw = DBSession.query(Player.player_id, Player.nick,
226             PlayerCaptime.fastest_cap, PlayerCaptime.game_id).\
227                 filter(PlayerCaptime.map_id == map_id).\
228                 filter(Player.player_id == PlayerCaptime.player_id).\
229                 order_by(PlayerCaptime.fastest_cap).\
230                 limit(10).\
231                 all()
232
233         captimes = [Captime(c.player_id, html_colors(c.nick),
234             c.fastest_cap, c.game_id) for c in captimes_raw]
235
236     except Exception as e:
237         gmap = None
238     return {'gmap':gmap,
239             'recent_games':recent_games,
240             'top_scorers':top_scorers,
241             'top_players':top_players,
242             'top_servers':top_servers,
243             'captimes':captimes,
244             }
245
246
247 def map_info(request):
248     """
249     List the information stored about a given map.
250     """
251     mapinfo_data =  _map_info_data(request)
252
253     # FIXME: code clone, should get these from _map_info_data
254     leaderboard_count = 10
255     recent_games_count = 20
256
257     for i in range(leaderboard_count-len(mapinfo_data['top_scorers'])):
258         mapinfo_data['top_scorers'].append(('-', '-', '-'))
259
260     for i in range(leaderboard_count-len(mapinfo_data['top_players'])):
261         mapinfo_data['top_players'].append(('-', '-', '-'))
262
263     for i in range(leaderboard_count-len(mapinfo_data['top_servers'])):
264         mapinfo_data['top_servers'].append(('-', '-', '-'))
265
266     return mapinfo_data
267
268
269 def map_info_json(request):
270     """
271     List the information stored about a given map. JSON.
272     """
273     return [{'status':'not implemented'}]
274
275
276 def map_captimes_data(request):
277     map_id = int(request.matchdict['id'])
278
279     current_page = request.params.get('page', 1)
280
281     try:
282         mmap = DBSession.query(Map).filter_by(map_id=map_id).one()
283
284         mct_q = DBSession.query(PlayerCaptime.fastest_cap, PlayerCaptime.create_dt,
285                 PlayerCaptime.player_id, PlayerCaptime.game_id,
286                 Game.server_id, Server.name.label('server_name'),
287                 PlayerGameStat.nick.label('player_nick')).\
288                 filter(PlayerCaptime.map_id==map_id).\
289                 filter(PlayerCaptime.game_id==Game.game_id).\
290                 filter(PlayerCaptime.map_id==Map.map_id).\
291                 filter(Game.server_id==Server.server_id).\
292                 filter(PlayerCaptime.player_id==PlayerGameStat.player_id).\
293                 filter(PlayerCaptime.game_id==PlayerGameStat.game_id).\
294                 order_by(expr.asc(PlayerCaptime.fastest_cap))
295
296     except Exception as e:
297         raise HTTPNotFound
298
299     map_captimes = Page(mct_q, current_page, items_per_page=20, url=page_url)
300
301     map_captimes.items = [MapCapTime(row) for row in map_captimes.items]
302
303     return {
304             'map_id':map_id,
305             'map':mmap,
306             'captimes':map_captimes,
307         }
308
309 def map_captimes(request):
310     return map_captimes_data(request)
311
312 def map_captimes_json(request):
313     current_page = request.params.get('page', 1)
314     data = map_captimes_data(request)
315
316     return {
317             "map": data["map"].to_dict(),
318             "captimes": [e.to_dict() for e in data["captimes"].items],
319             "page": current_page,
320             }