]> git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views.py
e5fc599fedb28e42195c4f78586e8cc558893076
[xonotic/xonstat.git] / xonstat / views.py
1 import datetime
2 import time
3 import re
4 from pyramid.response import Response
5 from pyramid.view import view_config
6 from webhelpers.paginate import Page, PageURL
7
8 from xonstat.models import *
9 from xonstat.util import page_url
10 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
11 from sqlalchemy import desc
12
13
14 import logging
15 log = logging.getLogger(__name__)
16
17 ##########################################################################
18 # This is the main index - the entry point to the entire site
19 ##########################################################################
20 @view_config(renderer='index.jinja2')
21 def main_index(request):
22     log.debug("testing logging; entered MainHandler.index()")
23     return {'project':'xonstat'}
24
25 ##########################################################################
26 # This is the player views area - only views pertaining to Xonotic players
27 # and their related information goes here
28 ##########################################################################
29 @view_config(renderer='player_index.mako')
30 def player_index(request):
31     players = DBSession.query(Player)
32
33     log.debug("testing logging; entered PlayerHandler.index()")
34     return {'players':players}
35
36 @view_config(renderer='player_info.mako')
37 def player_info(request):
38     player_id = request.matchdict['id']
39     try:
40         player = DBSession.query(Player).filter_by(player_id=player_id).one()
41         recent_games = DBSession.query(PlayerGameStat, Game, Server, Map).\
42                 filter(PlayerGameStat.player_id == player_id).\
43                 filter(PlayerGameStat.game_id == Game.game_id).\
44                 filter(Game.server_id == Server.server_id).\
45                 filter(Game.map_id == Map.map_id).\
46                 order_by(Game.game_id.desc())[0:10]
47
48     except Exception as e:
49         player = None
50         recent_games = None
51     return {'player':player, 
52             'recent_games':recent_games}
53
54
55 def player_game_index(request):
56     player_id = request.matchdict['player_id']
57     current_page = request.matchdict['page']
58
59     try:
60         player = DBSession.query(Player).filter_by(player_id=player_id).one()
61
62         games_q = DBSession.query(PlayerGameStat, Game, Server, Map).\
63                 filter(PlayerGameStat.player_id == player_id).\
64                 filter(PlayerGameStat.game_id == Game.game_id).\
65                 filter(Game.server_id == Server.server_id).\
66                 filter(Game.map_id == Map.map_id).\
67                 order_by(Game.game_id.desc())
68
69         games = Page(games_q, current_page, url=page_url)
70
71         
72     except Exception as e:
73         player = None
74         games = None
75         raise e
76
77     return {'player':player,
78             'games':games}
79
80
81 def player_weapon_stats(request):
82     game_id = request.matchdict['game_id']
83     pgstat_id = request.matchdict['pgstat_id']
84     try:
85         pwstats = DBSession.query(PlayerWeaponStat, Weapon).\
86                 filter(PlayerWeaponStat.weapon_cd==Weapon.weapon_cd).\
87                 filter_by(game_id=game_id).\
88                 filter_by(player_game_stat_id=pgstat_id).\
89                 order_by(Weapon.descr).\
90                 all()
91
92     except Exception as e:
93         pwstats = None
94     return {'pwstats':pwstats}
95
96
97 ##########################################################################
98 # This is the game views area - only views pertaining to Xonotic
99 # games and their related information goes here
100 ##########################################################################
101 def game_index(request):
102     if 'page' in request.matchdict:
103         current_page = request.matchdict['page']
104     else:
105         current_page = 1
106
107     games_q = DBSession.query(Game, Server, Map).\
108             filter(Game.server_id == Server.server_id).\
109             filter(Game.map_id == Map.map_id).\
110             order_by(Game.game_id.desc())
111
112     games = Page(games_q, current_page, url=page_url)
113
114     return {'games':games}
115
116
117 def game_info(request):
118     game_id = request.matchdict['id']
119     try:
120         notfound = False
121
122         (start_dt, game_type_cd, server_id, server_name, map_id, map_name) = \
123         DBSession.query("start_dt", "game_type_cd", "server_id", 
124                 "server_name", "map_id", "map_name").\
125                 from_statement("select g.start_dt, g.game_type_cd, "
126                         "g.server_id, s.name as server_name, g.map_id, "
127                         "m.name as map_name "
128                         "from games g, servers s, maps m "
129                         "where g.game_id = :game_id "
130                         "and g.server_id = s.server_id "
131                         "and g.map_id = m.map_id").\
132                         params(game_id=game_id).one()
133
134         player_game_stats = DBSession.query(PlayerGameStat).\
135                 from_statement("select * from player_game_stats "
136                         "where game_id = :game_id "
137                         "order by score desc").\
138                             params(game_id=game_id).all()
139     except Exception as inst:
140         notfound = True
141         start_dt = None
142         game_type_cd = None
143         server_id = None
144         server_name = None
145         map_id = None
146         map_name = None
147         player_game_stats = None
148
149     return {'notfound':notfound,
150             'start_dt':start_dt,
151             'game_type_cd':game_type_cd,
152             'server_id':server_id,
153             'server_name':server_name,
154             'map_id':map_id,
155             'map_name':map_name,
156             'player_game_stats':player_game_stats}
157
158
159 ##########################################################################
160 # This is the server views area - only views pertaining to Xonotic
161 # servers and their related information goes here
162 ##########################################################################
163 def server_info(request):
164     server_id = request.matchdict['id']
165     try:
166         server = DBSession.query(Server).filter_by(server_id=server_id).one()
167         recent_games = DBSession.query(Game, Server, Map).\
168                 filter(Game.server_id == server_id).\
169                 filter(Game.server_id == Server.server_id).\
170                 filter(Game.map_id == Map.map_id).\
171                 order_by(Game.game_id.desc())[0:10]
172
173     except Exception as e:
174         server = None
175         recent_games = None
176     return {'server':server,
177             'recent_games':recent_games}
178
179
180 def server_game_index(request):
181     server_id = request.matchdict['server_id']
182     current_page = request.matchdict['page']
183
184     try:
185         server = DBSession.query(Server).filter_by(server_id=server_id).one()
186
187         games_q = DBSession.query(Game, Server, Map).\
188                 filter(Game.server_id == server_id).\
189                 filter(Game.server_id == Server.server_id).\
190                 filter(Game.map_id == Map.map_id).\
191                 order_by(Game.game_id.desc())
192
193         games = Page(games_q, current_page, url=page_url)
194     except Exception as e:
195         server = None
196         games = None
197         raise e
198
199     return {'games':games,
200             'server':server}
201
202
203 ##########################################################################
204 # This is the map views area - only views pertaining to Xonotic
205 # maps and their related information goes here
206 ##########################################################################
207 def map_info(request):
208     map_id = request.matchdict['id']
209     try:
210         gmap = DBSession.query(Map).filter_by(map_id=map_id).one()
211     except:
212         gmap = None
213     return {'gmap':gmap}
214
215
216 ##########################################################################
217 # This is the stats views area - only views pertaining to Xonotic
218 # statistics and its related information goes here
219 ##########################################################################
220 def get_or_create_server(session=None, name=None):
221     try:
222         # find one by that name, if it exists
223         server = session.query(Server).filter_by(name=name).one()
224         log.debug("Found server id {0} with name {1}.".format(
225             server.server_id, server.name))
226     except NoResultFound, e:
227         server = Server(name=name)
228         session.add(server)
229         session.flush()
230         log.debug("Created server id {0} with name {1}".format(
231             server.server_id, server.name))
232     except MultipleResultsFound, e:
233         # multiple found, so use the first one but warn
234         log.debug(e)
235         servers = session.query(Server).filter_by(name=name).order_by(
236                 Server.server_id).all()
237         server = servers[0]
238         log.debug("Created server id {0} with name {1} but found \
239                 multiple".format(
240             server.server_id, server.name))
241
242     return server
243
244 def get_or_create_map(session=None, name=None):
245     try:
246         # find one by the name, if it exists
247         gmap = session.query(Map).filter_by(name=name).one()
248         log.debug("Found map id {0} with name {1}.".format(gmap.map_id, 
249             gmap.name))
250     except NoResultFound, e:
251         gmap = Map(name=name)
252         session.add(gmap)
253         session.flush()
254         log.debug("Created map id {0} with name {1}.".format(gmap.map_id,
255             gmap.name))
256     except MultipleResultsFound, e:
257         # multiple found, so use the first one but warn
258         log.debug(e)
259         gmaps = session.query(Map).filter_by(name=name).order_by(
260                 Map.map_id).all()
261         gmap = gmaps[0]
262         log.debug("Found map id {0} with name {1} but found \
263                 multiple.".format(gmap.map_id, gmap.name))
264
265     return gmap
266
267 def create_game(session=None, start_dt=None, game_type_cd=None, 
268         server_id=None, map_id=None, winner=None):
269     game = Game(start_dt=start_dt, game_type_cd=game_type_cd,
270                 server_id=server_id, map_id=map_id, winner=winner)
271     session.add(game)
272     session.flush()
273     log.debug("Created game id {0} on server {1}, map {2} at time \
274             {3} and on map {4}".format(game.game_id, 
275                 server_id, map_id, start_dt, map_id))
276
277     return game
278
279 # search for a player and if found, create a new one (w/ hashkey)
280 def get_or_create_player(session=None, hashkey=None):
281     # if we have a bot
282     if re.search('^bot#\d+$', hashkey):
283         player = session.query(Player).filter_by(player_id=1).one()
284     # if we have an untracked player
285     elif re.search('^player#\d+$', hashkey):
286         player = session.query(Player).filter_by(player_id=2).one()
287     # else it is a tracked player
288     else:
289         # see if the player is already in the database
290         # if not, create one and the hashkey along with it
291         try:
292             hashkey = session.query(Hashkey).filter_by(
293                     hashkey=hashkey).one()
294             player = session.query(Player).filter_by(
295                     player_id=hashkey.player_id).one()
296             log.debug("Found existing player {0} with hashkey {1}.".format(
297                 player.player_id, hashkey.hashkey))
298         except:
299             player = Player()
300             session.add(player)
301             session.flush()
302             hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)
303             session.add(hashkey)
304             log.debug("Created player {0} with hashkey {1}.".format(
305                 player.player_id, hashkey.hashkey))
306
307     return player
308
309 def create_player_game_stat(session=None, player=None, 
310         game=None, player_events=None):
311
312     # in here setup default values (e.g. if game type is CTF then
313     # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
314     # TODO: use game's create date here instead of now()
315     pgstat = PlayerGameStat(create_dt=datetime.datetime.now())
316
317     # set player id from player record
318     pgstat.player_id = player.player_id
319
320     #set game id from game record
321     pgstat.game_id = game.game_id
322
323     # all games have a score
324     pgstat.score = 0
325
326     if game.game_type_cd == 'dm':
327         pgstat.kills = 0
328         pgstat.deaths = 0
329         pgstat.suicides = 0
330     elif game.game_type_cd == 'ctf':
331         pgstat.kills = 0
332         pgstat.captures = 0
333         pgstat.pickups = 0
334         pgstat.drops = 0
335         pgstat.returns = 0
336         pgstat.carrier_frags = 0
337
338     for (key,value) in player_events.items():
339         if key == 'n': pgstat.nick = value
340         if key == 't': pgstat.team = value
341         if key == 'rank': pgstat.rank = value
342         if key == 'alivetime': 
343             pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
344         if key == 'scoreboard-drops': pgstat.drops = value
345         if key == 'scoreboard-returns': pgstat.returns = value
346         if key == 'scoreboard-fckills': pgstat.carrier_frags = value
347         if key == 'scoreboard-pickups': pgstat.pickups = value
348         if key == 'scoreboard-caps': pgstat.captures = value
349         if key == 'scoreboard-score': pgstat.score = value
350         if key == 'scoreboard-deaths': pgstat.deaths = value
351         if key == 'scoreboard-kills': pgstat.kills = value
352         if key == 'scoreboard-suicides': pgstat.suicides = value
353
354     # check to see if we had a name, and if 
355     # not use the name from the player id
356     if pgstat.nick == None:
357         pgstat.nick = player.nick
358
359     session.add(pgstat)
360     session.flush()
361
362     return pgstat
363
364
365 def create_player_weapon_stats(session=None, player=None, 
366         game=None, pgstat=None, player_events=None):
367     pwstats = []
368
369     for (key,value) in player_events.items():
370         matched = re.search("acc-(.*?)-cnt-fired", key)
371         if matched:
372             weapon_cd = matched.group(1)
373             pwstat = PlayerWeaponStat()
374             pwstat.player_id = player.player_id
375             pwstat.game_id = game.game_id
376             pwstat.player_game_stat_id = pgstat.player_game_stat_id
377             pwstat.weapon_cd = weapon_cd
378
379             if 'n' in player_events:
380                 pwstat.nick = player_events['n']
381             else:
382                 pwstat.nick = player_events['P']
383
384             if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
385                 pwstat.fired = int(round(float(
386                         player_events['acc-' + weapon_cd + '-cnt-fired'])))
387             if 'acc-' + weapon_cd + '-fired' in player_events:
388                 pwstat.max = int(round(float(
389                         player_events['acc-' + weapon_cd + '-fired'])))
390             if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
391                 pwstat.hit = int(round(float(
392                         player_events['acc-' + weapon_cd + '-cnt-hit'])))
393             if 'acc-' + weapon_cd + '-hit' in player_events:
394                 pwstat.actual = int(round(float(
395                         player_events['acc-' + weapon_cd + '-hit'])))
396             if 'acc-' + weapon_cd + '-frags' in player_events:
397                 pwstat.frags = int(round(float(
398                         player_events['acc-' + weapon_cd + '-frags'])))
399
400             session.add(pwstat)
401             pwstats.append(pwstat)
402
403     return pwstats
404
405
406 def parse_body(request):
407     # storage vars for the request body
408     game_meta = {}
409     player_events = {}
410     current_team = None
411     players = []
412     
413     log.debug(request.body)
414
415     for line in request.body.split('\n'):
416         try:
417             (key, value) = line.strip().split(' ', 1)
418     
419             if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
420                 game_meta[key] = value
421
422             if key == 'P':
423                 # if we were working on a player record already, append
424                 # it and work on a new one (only set team info)
425                 if len(player_events) != 0:
426                     players.append(player_events)
427                     player_events = {}
428     
429                 player_events[key] = value
430
431             if key == 'e':
432                 (subkey, subvalue) = value.split(' ', 1)
433                 player_events[subkey] = subvalue
434             if key == 'n':
435                 player_events[key] = value
436             if key == 't':
437                 player_events[key] = value
438         except:
439             # no key/value pair - move on to the next line
440             pass
441     
442     # add the last player we were working on
443     if len(player_events) > 0:
444         players.append(player_events)
445
446     return (game_meta, players)
447
448
449 def create_player_stats(session=None, player=None, game=None, 
450         player_events=None):
451     if 'joins' in player_events and 'matches' in player_events\
452             and 'scoreboardvalid' in player_events:
453                 pgstat = create_player_game_stat(session=session, 
454                         player=player, game=game, player_events=player_events)
455                 if not re.search('^bot#\d+$', player_events['P']):
456                         create_player_weapon_stats(session=session, 
457                             player=player, game=game, pgstat=pgstat,
458                             player_events=player_events)
459     
460
461 @view_config(renderer='stats_submit.mako')
462 def stats_submit(request):
463     try:
464         session = DBSession()
465
466         (game_meta, players) = parse_body(request)  
467     
468         # verify required metadata is present
469         if 'T' not in game_meta or\
470             'G' not in game_meta or\
471             'M' not in game_meta or\
472             'S' not in game_meta:
473             log.debug("Required game meta fields (T, G, M, or S) missing. "\
474                     "Can't continue.")
475             raise Exception("Required game meta fields (T, G, M, or S) missing.")
476     
477         has_real_players = False
478         for player_events in players:
479             if not player_events['P'].startswith('bot'):
480                 if 'joins' in player_events and 'matches' in player_events\
481                     and 'scoreboardvalid' in player_events:
482                     has_real_players = True
483
484         if not has_real_players:
485             raise Exception("No real players found. Stats ignored.")
486
487         server = get_or_create_server(session=session, name=game_meta['S'])
488         gmap = get_or_create_map(session=session, name=game_meta['M'])
489
490         if 'W' in game_meta:
491             winner = game_meta['W']
492         else:
493             winner = None
494
495         game = create_game(session=session, 
496                 start_dt=datetime.datetime(
497                     *time.gmtime(float(game_meta['T']))[:6]), 
498                 server_id=server.server_id, game_type_cd=game_meta['G'], 
499                 map_id=gmap.map_id, winner=winner)
500     
501         # find or create a record for each player
502         # and add stats for each if they were present at the end
503         # of the game
504         for player_events in players:
505             player = get_or_create_player(session=session, 
506                     hashkey=player_events['P'])
507             log.debug('Creating stats for %s' % player_events['P'])
508             create_player_stats(session=session, player=player, game=game, 
509                     player_events=player_events)
510     
511         session.commit()
512         log.debug('Success! Stats recorded.')
513         return Response('200 OK')
514     except Exception as e:
515         session.rollback()
516         raise e