]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/main.py
Add a separate JSON view for summary stats.
[xonotic/xonstat.git] / xonstat / views / main.py
1 import logging
2 from datetime import datetime, timedelta
3
4 from beaker.cache import cache_region
5 from sqlalchemy import text
6 from xonstat.models import DBSession, PlayerRank, ActivePlayer, ActiveServer, ActiveMap
7 from xonstat.views.helpers import RecentGame, recent_games_q
8
9 log = logging.getLogger(__name__)
10
11
12 @cache_region('hourly_term')
13 def summary_stats_data(scope="all"):
14     """
15     Gets the summary stats (number of active players, the game type, and the number of games)
16     for a given scope.
17
18     :param scope: The scope to fetch from the table. May be "all" or "day".
19     :return: list[tuple]
20     """
21     if scope not in ["all", "day"]:
22         scope = "all"
23
24     sql = text("SELECT num_players, game_type_cd, num_games, create_dt refresh_dt "
25                "FROM summary_stats_mv "
26                "WHERE scope = :scope "
27                "ORDER BY sort_order ")
28
29     try:
30         ss = DBSession.query("num_players", "game_type_cd", "num_games", "refresh_dt").\
31                 from_statement(sql).params(scope=scope).all()
32
33         return ss
34     except Exception as e:
35         log.error(e)
36         return []
37
38
39 def summary_stats_json(request):
40     ss = summary_stats_data(request.params.get("scope", "all"))
41     return [
42         {
43             "players": r.num_players,
44             "game_type_cd": r.game_type_cd,
45             "games": r.num_games,
46             "refresh_dt": r.refresh_dt.isoformat(),
47         }
48         for r in ss]
49
50
51 @cache_region('hourly_term')
52 def summary_stats_string(scope="all"):
53     """
54     Assembles the summary stats data into a readable line for direct inclusion in templates.
55     """
56     try:
57         ss = summary_stats_data(scope)
58
59         i = 1
60         total_games = 0
61         other_games = 0
62         for row in ss:
63             # the number of players is constant in each row
64             total_players = row.num_players
65
66             total_games += row.num_games
67
68             # we can't show all game types on the single summary line, so any
69             # past the fifth one will get bundled in to an "other" count
70             if i > 5:
71                 other_games += row.num_games
72
73             i += 1
74
75         # don't send anything if we don't have any activity
76         if total_games == 0:
77             stat_line = None
78         else:
79         # This is ugly because we're doing template-like stuff within the
80         # view code. The alternative isn't any better, though: we would
81         # have to assemble the string inside the template by using a Python
82         # code block. For now I'll leave it like this since it is the lesser
83         # of two evils IMO.
84         # Also we need to hard-code the URL structure in here to allow caching,
85         # which also sucks.
86             in_paren = "; ".join(["{:2,d} {}".format(
87                 g.num_games,
88                 "<a href='/games?type={0}'>{0}</a>".format(g.game_type_cd)
89             ) for g in ss[:5]])
90
91             if other_games > 0:
92                 in_paren += "; {:2,d} other".format(other_games)
93
94             stat_line = "{:2,d} players and {:2,d} games ({})".format(
95                 total_players,
96                 total_games,
97                 in_paren
98             )
99
100     except Exception as e:
101         stat_line = None
102         raise e
103
104     return stat_line
105
106
107 @cache_region('hourly_term')
108 def get_ranks(game_type_cd):
109     """
110     Gets a set number of the top-ranked people for the specified game_type_cd.
111
112     The game_type_cd parameter is the type to fetch. Currently limited to
113     duel, dm, ctf, and tdm.
114     """
115     # how many ranks we want to fetch
116     leaderboard_count = 10
117
118     # only a few game modes are actually ranked
119     if game_type_cd not in 'duel' 'dm' 'ctf' 'tdm':
120         return None
121
122     ranks = DBSession.query(PlayerRank).\
123             filter(PlayerRank.game_type_cd==game_type_cd).\
124             order_by(PlayerRank.rank).\
125             limit(leaderboard_count).all()
126
127     return ranks
128
129
130 @cache_region('hourly_term')
131 def get_top_players_by_time(limit=None, start=None):
132     """
133     The top players by the amount of time played during a date range.
134     """
135     q = DBSession.query(ActivePlayer)
136
137     if start is not None:
138         q = q.filter(ActivePlayer.sort_order >= start)
139
140     q = q.order_by(ActivePlayer.sort_order)
141
142     if limit is not None:
143         q = q.limit(limit)
144
145     return q.all()
146
147
148 @cache_region('hourly_term')
149 def get_top_servers_by_play_time(limit=None, start=None):
150     """
151     The top servers by the cumulative amount of time played on them during a given interval.
152     """
153     q = DBSession.query(ActiveServer)
154
155     if start is not None:
156         q = q.filter(ActiveServer.sort_order >= start)
157
158     q = q.order_by(ActiveServer.sort_order)
159
160     if limit is not None:
161         q = q.limit(limit)
162
163     return q.all()
164
165
166 @cache_region('hourly_term')
167 def get_top_maps_by_games(limit=None, start=None):
168     """
169     The top maps by the number of games played during a date range.
170     """
171     q = DBSession.query(ActiveMap)
172
173     if start is not None:
174         q = q.filter(ActiveMap.sort_order >= start)
175
176     q = q.order_by(ActiveMap.sort_order)
177
178     if limit is not None:
179         q = q.limit(limit)
180
181     return q.all()
182
183
184 def _main_index_data(request):
185     try:
186         leaderboard_lifetime = int(
187                 request.registry.settings['xonstat.leaderboard_lifetime'])
188     except:
189         leaderboard_lifetime = 30
190
191     leaderboard_count = 10
192     recent_games_count = 20
193
194     # summary statistics for the tagline
195     stat_line = summary_stats_string("all")
196     day_stat_line = summary_stats_string("day")
197
198
199     # the three top ranks tables
200     ranks = []
201     for gtc in ['duel', 'ctf', 'dm', 'tdm']:
202         rank = get_ranks(gtc)
203         if len(rank) != 0:
204             ranks.append(rank)
205
206     right_now = datetime.utcnow()
207     back_then = datetime.utcnow() - timedelta(days=leaderboard_lifetime)
208
209     # top players by playing time
210     top_players = get_top_players_by_time(10)
211
212     # top servers by number of games
213     top_servers = get_top_servers_by_play_time(10)
214
215     # top maps by total times played
216     top_maps = get_top_maps_by_games(10)
217
218     # recent games played in descending order
219     rgs = recent_games_q(cutoff=back_then).limit(recent_games_count).all()
220     recent_games = [RecentGame(row) for row in rgs]
221
222     return {'top_players':top_players,
223             'top_servers':top_servers,
224             'top_maps':top_maps,
225             'recent_games':recent_games,
226             'ranks':ranks,
227             'stat_line':stat_line,
228             'day_stat_line':day_stat_line,
229             }
230
231
232 def main_index(request):
233     """
234     Display the main page information.
235     """
236     return _main_index_data(request)
237
238
239 def main_index_json(request):
240     """
241     JSON output of the main page information.
242     """
243     return [{'status':'not implemented'}]
244
245
246 def top_players_index(request):
247     try:
248         start = int(request.params.get('start', None))
249     except:
250         start = None
251
252     top_players = get_top_players_by_time(20, start)
253
254     # building a query string
255     query = {}
256     if len(top_players) > 1:
257         query['start'] = top_players[-1].sort_order + 1
258
259     return {
260             'top_players':top_players,
261             'query':query,
262             'start':start,
263             }
264
265
266 def top_servers_index(request):
267     try:
268         start = int(request.params.get('start', None))
269     except:
270         start = None
271
272     top_servers = get_top_servers_by_play_time(20, start)
273
274     # building a query string
275     query = {}
276     if len(top_servers) > 1:
277         query['start'] = top_servers[-1].sort_order + 1
278
279     return {
280             'top_servers':top_servers,
281             'query':query,
282             'start':start,
283             }
284
285
286 def top_maps_index(request):
287     try:
288         start = int(request.params.get('start', None))
289     except:
290         start = None
291
292     top_maps = get_top_maps_by_games(20, start)
293
294     # building a query string
295     query = {}
296     if len(top_maps) > 1:
297         query['start'] = top_maps[-1].sort_order + 1
298
299     return {
300             'top_maps':top_maps,
301             'query':query,
302             'start':start,
303             }