]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/main.py
Use the summary_stats_mv instead of the full table.
[xonotic/xonstat.git] / xonstat / views / main.py
1 import logging
2 import sqlalchemy as sa
3 import sqlalchemy.sql.functions as func
4 import sqlalchemy.sql.expression as expr
5 from beaker.cache import cache_regions, cache_region
6 from collections import namedtuple
7 from datetime import datetime, timedelta
8 from pyramid.response import Response
9 from xonstat.models import *
10 from xonstat.util import *
11 from xonstat.views.helpers import RecentGame, recent_games_q
12 from webhelpers.paginate import Page
13
14
15 log = logging.getLogger(__name__)
16
17
18 @cache_region('hourly_term')
19 def get_summary_stats(scope="all"):
20     """
21     Gets the following aggregate statistics according to the provided scope:
22
23         - the number of active players
24         - the number of games per game type
25
26     Scope can be "all" or "day".
27
28     The fetched information is summarized into a string which is passed
29     directly to the template.
30     """
31     if scope not in ["all", "day"]:
32         scope = "all"
33
34     try:
35         ss = DBSession.query("num_players", "game_type_cd", "num_games").\
36                 from_statement(
37                         "SELECT num_players, game_type_cd, num_games "
38                         "FROM summary_stats_mv "
39                         "WHERE scope = :scope "
40                         "ORDER BY sort_order "
41                 ).params(scope=scope).all()
42
43         i = 1
44         total_games = 0
45         other_games = 0
46         for row in ss:
47             # the number of players is constant in each row
48             total_players = row.num_players
49
50             total_games += row.num_games
51
52             # we can't show all game types on the single summary line, so any
53             # past the fifth one will get bundled in to an "other" count
54             if i > 5:
55                 other_games += row.num_games
56
57             i += 1
58
59         # don't send anything if we don't have any activity
60         if total_games == 0:
61             stat_line = None
62         else:
63         # This is ugly because we're doing template-like stuff within the
64         # view code. The alternative isn't any better, though: we would
65         # have to assemble the string inside the template by using a Python
66         # code block. For now I'll leave it like this since it is the lesser
67         # of two evils IMO.
68         # Also we need to hard-code the URL structure in here to allow caching,
69         # which also sucks.
70             in_paren = "; ".join(["{:2,d} {}".format(
71                 g.num_games,
72                 "<a href='/games?type={0}'>{0}</a>".format(g.game_type_cd)
73             ) for g in ss[:5]])
74
75             if other_games > 0:
76                 in_paren += "; {:2,d} other".format(other_games)
77
78             stat_line = "{:2,d} players and {:2,d} games ({})".format(
79                 total_players,
80                 total_games,
81                 in_paren
82             )
83
84     except Exception as e:
85         raise e
86         stat_line = None
87
88     return stat_line
89
90
91 @cache_region('hourly_term')
92 def get_ranks(game_type_cd):
93     """
94     Gets a set number of the top-ranked people for the specified game_type_cd.
95
96     The game_type_cd parameter is the type to fetch. Currently limited to
97     duel, dm, ctf, and tdm.
98     """
99     # how many ranks we want to fetch
100     leaderboard_count = 10
101
102     # only a few game modes are actually ranked
103     if game_type_cd not in 'duel' 'dm' 'ctf' 'tdm':
104         return None
105
106     ranks = DBSession.query(PlayerRank).\
107             filter(PlayerRank.game_type_cd==game_type_cd).\
108             order_by(PlayerRank.rank).\
109             limit(leaderboard_count).all()
110
111     return ranks
112
113
114 def top_players_by_time_q(cutoff_days):
115     """
116     Query for the top players by the amount of time played during a date range.
117
118     Games older than cutoff_days days old are ignored.
119     """
120
121     # only games played during this range are considered
122     right_now = datetime.utcnow()
123     cutoff_dt = right_now - timedelta(days=cutoff_days)
124
125     top_players_q = DBSession.query(Player.player_id, Player.nick,
126             func.sum(PlayerGameStat.alivetime)).\
127             filter(Player.player_id == PlayerGameStat.player_id).\
128             filter(Player.player_id > 2).\
129             filter(expr.between(PlayerGameStat.create_dt, cutoff_dt, right_now)).\
130             order_by(expr.desc(func.sum(PlayerGameStat.alivetime))).\
131             group_by(Player.nick).\
132             group_by(Player.player_id)
133
134     return top_players_q
135
136
137 @cache_region('hourly_term')
138 def get_top_players_by_time(cutoff_days):
139     """
140     The top players by the amount of time played during a date range.
141
142     Games older than cutoff_days days old are ignored.
143     """
144     # how many to retrieve
145     count = 10
146
147     # only games played during this range are considered
148     right_now = datetime.utcnow()
149     cutoff_dt = right_now - timedelta(days=cutoff_days)
150
151     top_players_q = top_players_by_time_q(cutoff_days)
152
153     top_players = top_players_q.limit(count).all()
154
155     top_players = [(player_id, html_colors(nick), score) \
156             for (player_id, nick, score) in top_players]
157
158     return top_players
159
160
161 def top_servers_by_players_q(cutoff_days):
162     """
163     Query to get the top servers by the amount of players active
164     during a date range.
165
166     Games older than cutoff_days days old are ignored.
167     """
168     # only games played during this range are considered
169     right_now = datetime.utcnow()
170     cutoff_dt = right_now - timedelta(days=cutoff_days)
171
172     top_servers_q = DBSession.query(Server.server_id, Server.name,
173         func.count()).\
174         filter(Game.server_id==Server.server_id).\
175         filter(expr.between(Game.create_dt, cutoff_dt, right_now)).\
176         order_by(expr.desc(func.count(Game.game_id))).\
177         group_by(Server.server_id).\
178         group_by(Server.name)
179
180     return top_servers_q
181
182
183 @cache_region('hourly_term')
184 def get_top_servers_by_players(cutoff_days):
185     """
186     The top servers by the amount of players active during a date range.
187
188     Games older than cutoff_days days old are ignored.
189     """
190     # how many to retrieve
191     count = 10
192
193     top_servers = top_servers_by_players_q(cutoff_days).limit(count).all()
194
195     return top_servers
196
197
198 def top_maps_by_times_played_q(cutoff_days):
199     """
200     Query to retrieve the top maps by the amount of times it was played
201     during a date range.
202
203     Games older than cutoff_days days old are ignored.
204     """
205     # only games played during this range are considered
206     right_now = datetime.utcnow()
207     cutoff_dt = right_now - timedelta(days=cutoff_days)
208
209     top_maps_q = DBSession.query(Game.map_id, Map.name,
210             func.count()).\
211             filter(Map.map_id==Game.map_id).\
212             filter(expr.between(Game.create_dt, cutoff_dt, right_now)).\
213             order_by(expr.desc(func.count())).\
214             group_by(Game.map_id).\
215             group_by(Map.name)
216
217     return top_maps_q
218
219
220 @cache_region('hourly_term')
221 def get_top_maps_by_times_played(cutoff_days):
222     """
223     The top maps by the amount of times it was played during a date range.
224
225     Games older than cutoff_days days old are ignored.
226     """
227     # how many to retrieve
228     count = 10
229
230     top_maps = top_maps_by_times_played_q(cutoff_days).limit(count).all()
231
232     return top_maps
233
234
235 def _main_index_data(request):
236     try:
237         leaderboard_lifetime = int(
238                 request.registry.settings['xonstat.leaderboard_lifetime'])
239     except:
240         leaderboard_lifetime = 30
241
242     leaderboard_count = 10
243     recent_games_count = 20
244
245     # summary statistics for the tagline
246     stat_line = get_summary_stats("all")
247     day_stat_line = get_summary_stats("day")
248
249
250     # the three top ranks tables
251     ranks = []
252     for gtc in ['duel', 'ctf', 'dm', 'tdm']:
253         rank = get_ranks(gtc)
254         if len(rank) != 0:
255             ranks.append(rank)
256
257     right_now = datetime.utcnow()
258     back_then = datetime.utcnow() - timedelta(days=leaderboard_lifetime)
259
260     # top players by playing time
261     top_players = get_top_players_by_time(leaderboard_lifetime)
262
263     # top servers by number of total players played
264     top_servers = get_top_servers_by_players(leaderboard_lifetime)
265
266     # top maps by total times played
267     top_maps = get_top_maps_by_times_played(leaderboard_lifetime)
268
269     # recent games played in descending order
270     rgs = recent_games_q(cutoff=back_then).limit(recent_games_count).all()
271     recent_games = [RecentGame(row) for row in rgs]
272
273     return {'top_players':top_players,
274             'top_servers':top_servers,
275             'top_maps':top_maps,
276             'recent_games':recent_games,
277             'ranks':ranks,
278             'stat_line':stat_line,
279             'day_stat_line':day_stat_line,
280             }
281
282
283 def main_index(request):
284     """
285     Display the main page information.
286     """
287     mainindex_data =  _main_index_data(request)
288
289     # FIXME: code clone, should get these from _main_index_data
290     leaderboard_count = 10
291     recent_games_count = 20
292
293     for i in range(leaderboard_count-len(mainindex_data['top_players'])):
294         mainindex_data['top_players'].append(('-', '-', '-'))
295
296     for i in range(leaderboard_count-len(mainindex_data['top_servers'])):
297         mainindex_data['top_servers'].append(('-', '-', '-'))
298
299     for i in range(leaderboard_count-len(mainindex_data['top_maps'])):
300         mainindex_data['top_maps'].append(('-', '-', '-'))
301
302     return mainindex_data
303
304
305 def main_index_json(request):
306     """
307     JSON output of the main page information.
308     """
309     return [{'status':'not implemented'}]
310
311
312 def top_players_by_time(request):
313     current_page = request.params.get('page', 1)
314
315     cutoff_days = int(request.registry.settings.\
316         get('xonstat.leaderboard_lifetime', 30))
317
318     top_players_q = top_players_by_time_q(cutoff_days)
319
320     top_players = Page(top_players_q, current_page, items_per_page=25, url=page_url)
321
322     top_players.items = [(player_id, html_colors(nick), score) \
323             for (player_id, nick, score) in top_players.items]
324
325     return {'top_players':top_players}
326
327
328 def top_servers_by_players(request):
329     current_page = request.params.get('page', 1)
330
331     cutoff_days = int(request.registry.settings.\
332         get('xonstat.leaderboard_lifetime', 30))
333
334     top_servers_q = top_servers_by_players_q(cutoff_days)
335
336     top_servers = Page(top_servers_q, current_page, items_per_page=25, url=page_url)
337
338     return {'top_servers':top_servers}
339
340
341 def top_maps_by_times_played(request):
342     current_page = request.params.get('page', 1)
343
344     cutoff_days = int(request.registry.settings.\
345         get('xonstat.leaderboard_lifetime', 30))
346
347     top_maps_q = top_maps_by_times_played_q(cutoff_days)
348
349     top_maps = Page(top_maps_q, current_page, items_per_page=25, url=page_url)
350
351     return {'top_maps':top_maps}