]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/player.py
Merge branch 'master' into approved
[xonotic/xonstat.git] / xonstat / views / player.py
1 import datetime
2 import json
3 import logging
4 import pyramid.httpexceptions
5 import re
6 import sqlalchemy as sa
7 import sqlalchemy.sql.functions as func
8 import time
9 from calendar import timegm
10 from collections import namedtuple
11 from pyramid.url import current_route_url
12 from sqlalchemy import desc, distinct
13 from webhelpers.paginate import Page, PageURL
14 from xonstat.models import *
15 from xonstat.util import page_url, to_json, pretty_date, datetime_seconds
16 from xonstat.views.helpers import RecentGame, recent_games_q
17
18 log = logging.getLogger(__name__)
19
20
21 def player_index_data(request):
22     if request.params.has_key('page'):
23         current_page = request.params['page']
24     else:
25         current_page = 1
26
27     try:
28         player_q = DBSession.query(Player).\
29                 filter(Player.player_id > 2).\
30                 filter(Player.active_ind == True).\
31                 filter(sa.not_(Player.nick.like('Anonymous Player%'))).\
32                 order_by(Player.player_id.desc())
33
34         players = Page(player_q, current_page, items_per_page=10, url=page_url)
35
36     except Exception as e:
37         players = None
38         raise e
39
40     return {'players':players
41            }
42
43
44 def player_index(request):
45     """
46     Provides a list of all the current players.
47     """
48     return player_index_data(request)
49
50
51 def player_index_json(request):
52     """
53     Provides a list of all the current players. JSON.
54     """
55     return [{'status':'not implemented'}]
56
57
58 def get_games_played(player_id):
59     """
60     Provides a breakdown by gametype of the games played by player_id.
61
62     Returns a list of namedtuples with the following members:
63         - game_type_cd
64         - games
65         - wins
66         - losses
67         - win_pct
68
69     The list itself is ordered by the number of games played
70     """
71     GamesPlayed = namedtuple('GamesPlayed', ['game_type_cd', 'games', 'wins',
72         'losses', 'win_pct'])
73
74     raw_games_played = DBSession.query('game_type_cd', 'wins', 'losses').\
75             from_statement(
76                 "SELECT game_type_cd, "
77                        "SUM(win) wins, "
78                        "SUM(loss) losses "
79                 "FROM   (SELECT g.game_id, "
80                                "g.game_type_cd, "
81                                "CASE "
82                                  "WHEN g.winner = pgs.team THEN 1 "
83                                  "WHEN pgs.rank = 1 THEN 1 "
84                                  "ELSE 0 "
85                                "END win, "
86                                "CASE "
87                                  "WHEN g.winner = pgs.team THEN 0 "
88                                  "WHEN pgs.rank = 1 THEN 0 "
89                                  "ELSE 1 "
90                                "END loss "
91                         "FROM   games g, "
92                                "player_game_stats pgs "
93                         "WHERE  g.game_id = pgs.game_id "
94                         "AND pgs.player_id = :player_id) win_loss "
95                 "GROUP  BY game_type_cd "
96             ).params(player_id=player_id).all()
97
98     games_played = []
99     overall_games = 0
100     overall_wins = 0
101     overall_losses = 0
102     for row in raw_games_played:
103         games = row.wins + row.losses
104         overall_games += games
105         overall_wins += row.wins
106         overall_losses += row.losses
107         win_pct = float(row.wins)/games * 100
108
109         games_played.append(GamesPlayed(row.game_type_cd, games, row.wins,
110             row.losses, win_pct))
111
112     try:
113         overall_win_pct = float(overall_wins)/overall_games * 100
114     except:
115         overall_win_pct = 0.0
116
117     games_played.append(GamesPlayed('overall', overall_games, overall_wins,
118         overall_losses, overall_win_pct))
119
120     # sort the resulting list by # of games played
121     games_played = sorted(games_played, key=lambda x:x.games)
122     games_played.reverse()
123     return games_played
124
125
126 def get_overall_stats(player_id):
127     """
128     Provides a breakdown of stats by gametype played by player_id.
129
130     Returns a dictionary of namedtuples with the following members:
131         - total_kills
132         - total_deaths
133         - k_d_ratio
134         - last_played (last time the player played the game type)
135         - last_played_epoch (same as above, but in seconds since epoch)
136         - last_played_fuzzy (same as above, but in relative date)
137         - total_playing_time (total amount of time played the game type)
138         - total_playing_time_secs (same as the above, but in seconds)
139         - total_pickups (ctf only)
140         - total_captures (ctf only)
141         - cap_ratio (ctf only)
142         - total_carrier_frags (ctf only)
143         - game_type_cd
144
145     The key to the dictionary is the game type code. There is also an
146     "overall" game_type_cd which sums the totals and computes the total ratios.
147     """
148     OverallStats = namedtuple('OverallStats', ['total_kills', 'total_deaths',
149         'k_d_ratio', 'last_played', 'last_played_epoch', 'last_played_fuzzy',
150         'total_playing_time', 'total_playing_time_secs', 'total_pickups', 'total_captures', 'cap_ratio',
151         'total_carrier_frags', 'game_type_cd'])
152
153     raw_stats = DBSession.query('game_type_cd', 'total_kills',
154             'total_deaths', 'last_played', 'total_playing_time',
155             'total_pickups', 'total_captures', 'total_carrier_frags').\
156             from_statement(
157                 "SELECT g.game_type_cd, "
158                        "Sum(pgs.kills)         total_kills, "
159                        "Sum(pgs.deaths)        total_deaths, "
160                        "Max(pgs.create_dt)     last_played, "
161                        "Sum(pgs.alivetime)     total_playing_time, "
162                        "Sum(pgs.pickups)       total_pickups, "
163                        "Sum(pgs.captures)      total_captures, "
164                        "Sum(pgs.carrier_frags) total_carrier_frags "
165                 "FROM   games g, "
166                        "player_game_stats pgs "
167                 "WHERE  g.game_id = pgs.game_id "
168                   "AND  pgs.player_id = :player_id "
169                 "GROUP  BY g.game_type_cd "
170                 "UNION "
171                 "SELECT 'overall' game_type_cd, "
172                        "Sum(pgs.kills)         total_kills, "
173                        "Sum(pgs.deaths)        total_deaths, "
174                        "Max(pgs.create_dt)     last_played, "
175                        "Sum(pgs.alivetime)     total_playing_time, "
176                        "Sum(pgs.pickups)       total_pickups, "
177                        "Sum(pgs.captures)      total_captures, "
178                        "Sum(pgs.carrier_frags) total_carrier_frags "
179                 "FROM   games g, "
180                        "player_game_stats pgs "
181                 "WHERE  g.game_id = pgs.game_id "
182                   "AND  pgs.player_id = :player_id "
183             ).params(player_id=player_id).all()
184
185     # to be indexed by game_type_cd
186     overall_stats = {}
187
188     for row in raw_stats:
189         # individual gametype ratio calculations
190         try:
191             k_d_ratio = float(row.total_kills)/row.total_deaths
192         except:
193             k_d_ratio = None
194
195         try:
196             cap_ratio = float(row.total_captures)/row.total_pickups
197         except:
198             cap_ratio = None
199
200         # everything else is untouched or "raw"
201         os = OverallStats(total_kills=row.total_kills,
202                 total_deaths=row.total_deaths,
203                 k_d_ratio=k_d_ratio,
204                 last_played=row.last_played,
205                 last_played_epoch=timegm(row.last_played.timetuple()),
206                 last_played_fuzzy=pretty_date(row.last_played),
207                 total_playing_time=row.total_playing_time,
208                 total_playing_time_secs=int(datetime_seconds(row.total_playing_time)),
209                 total_pickups=row.total_pickups,
210                 total_captures=row.total_captures,
211                 cap_ratio=cap_ratio,
212                 total_carrier_frags=row.total_carrier_frags,
213                 game_type_cd=row.game_type_cd)
214
215         overall_stats[row.game_type_cd] = os
216
217     return overall_stats
218
219
220 def get_fav_maps(player_id, game_type_cd=None):
221     """
222     Provides a breakdown of favorite maps by gametype.
223
224     Returns a dictionary of namedtuples with the following members:
225         - game_type_cd
226         - map_name (map name)
227         - map_id
228         - times_played
229
230     The favorite map is defined as the map you've played the most
231     for the given game_type_cd.
232
233     The key to the dictionary is the game type code. There is also an
234     "overall" game_type_cd which is the overall favorite map. This is
235     defined as the favorite map of the game type you've played the
236     most. The input parameter game_type_cd is for this.
237     """
238     FavMap = namedtuple('FavMap', ['map_name', 'map_id', 'times_played', 'game_type_cd'])
239
240     raw_favs = DBSession.query('game_type_cd', 'map_name',
241             'map_id', 'times_played').\
242             from_statement(
243                 "SELECT game_type_cd, "
244                        "name map_name, "
245                        "map_id, "
246                        "times_played "
247                 "FROM   (SELECT g.game_type_cd, "
248                                "m.name, "
249                                "m.map_id, "
250                                "Count(*) times_played, "
251                                "Row_number() "
252                                  "OVER ( "
253                                    "partition BY g.game_type_cd "
254                                    "ORDER BY Count(*) DESC, m.map_id ASC) rank "
255                         "FROM   games g, "
256                                "player_game_stats pgs, "
257                                "maps m "
258                         "WHERE  g.game_id = pgs.game_id "
259                                "AND g.map_id = m.map_id "
260                                "AND pgs.player_id = :player_id "
261                         "GROUP  BY g.game_type_cd, "
262                                   "m.map_id, "
263                                   "m.name) most_played "
264                 "WHERE  rank = 1 "
265                 "ORDER BY  times_played desc "
266             ).params(player_id=player_id).all()
267
268     fav_maps = {}
269     overall_fav = None
270     for row in raw_favs:
271         fv = FavMap(map_name=row.map_name,
272             map_id=row.map_id,
273             times_played=row.times_played,
274             game_type_cd=row.game_type_cd)
275     
276         # if we aren't given a favorite game_type_cd
277         # then the overall favorite is the one we've
278         # played the most
279         if overall_fav is None:
280             fav_maps['overall'] = fv
281             overall_fav = fv.game_type_cd
282
283         # otherwise it is the favorite map from the
284         # favorite game_type_cd (provided as a param)
285         # and we'll overwrite the first dict entry
286         if game_type_cd == fv.game_type_cd:
287             fav_maps['overall'] = fv
288
289         fav_maps[row.game_type_cd] = fv
290
291     return fav_maps
292
293
294 def get_ranks(player_id):
295     """
296     Provides a breakdown of the player's ranks by game type.
297
298     Returns a dictionary of namedtuples with the following members:
299         - game_type_cd
300         - rank
301         - max_rank
302
303     The key to the dictionary is the game type code. There is also an
304     "overall" game_type_cd which is the overall best rank.
305     """    
306     Rank = namedtuple('Rank', ['rank', 'max_rank', 'percentile', 'game_type_cd'])
307
308     raw_ranks = DBSession.query("game_type_cd", "rank", "max_rank").\
309             from_statement(
310                 "select pr.game_type_cd, pr.rank, overall.max_rank "
311                 "from player_ranks pr,  "
312                    "(select game_type_cd, max(rank) max_rank "
313                     "from player_ranks  "
314                     "group by game_type_cd) overall "
315                 "where pr.game_type_cd = overall.game_type_cd  "
316                 "and player_id = :player_id "
317                 "order by rank").\
318             params(player_id=player_id).all()
319
320     ranks = {}
321     found_top_rank = False
322     for row in raw_ranks:
323         rank = Rank(rank=row.rank,
324             max_rank=row.max_rank,
325             percentile=100 - 100*float(row.rank)/row.max_rank,
326             game_type_cd=row.game_type_cd)
327
328
329         if not found_top_rank:
330             ranks['overall'] = rank
331             found_top_rank = True
332         elif rank.percentile > ranks['overall'].percentile:
333             ranks['overall'] = rank
334
335         ranks[row.game_type_cd] = rank
336
337     return ranks;
338
339
340 def get_elos(player_id):
341     """
342     Provides a breakdown of the player's elos by game type.
343
344     Returns a dictionary of namedtuples with the following members:
345         - player_id
346         - game_type_cd
347         - games
348         - elo
349
350     The key to the dictionary is the game type code. There is also an
351     "overall" game_type_cd which is the overall best rank.
352     """
353     raw_elos = DBSession.query(PlayerElo).filter_by(player_id=player_id).\
354             order_by(PlayerElo.elo.desc()).all()
355
356     elos = {}
357     found_max_elo = False
358     for row in raw_elos:
359         if not found_max_elo:
360             elos['overall'] = row
361             found_max_elo = True
362
363         elos[row.game_type_cd] = row
364
365     return elos
366
367
368 def get_recent_games(player_id):
369     """
370     Provides a list of recent games for a player. Uses the recent_games_q helper.
371     """
372     # recent games played in descending order
373     rgs = recent_games_q(player_id=player_id).limit(10).all()
374     recent_games = [RecentGame(row) for row in rgs]
375
376     return recent_games
377
378
379 def get_recent_weapons(player_id):
380     """
381     Returns the weapons that have been used in the past 90 days
382     and also used in 5 games or more.
383     """
384     cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=90)
385     recent_weapons = []
386     for weapon in DBSession.query(PlayerWeaponStat.weapon_cd, func.count()).\
387             filter(PlayerWeaponStat.player_id == player_id).\
388             filter(PlayerWeaponStat.create_dt > cutoff).\
389             group_by(PlayerWeaponStat.weapon_cd).\
390             having(func.count() > 4).\
391             all():
392                 recent_weapons.append(weapon[0])
393
394     return recent_weapons
395
396
397 def get_accuracy_stats(player_id, weapon_cd, games):
398     """
399     Provides accuracy for weapon_cd by player_id for the past N games.
400     """
401     # Reaching back 90 days should give us an accurate enough average
402     # We then multiply this out for the number of data points (games) to
403     # create parameters for a flot graph
404     try:
405         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.hit),
406                 func.sum(PlayerWeaponStat.fired)).\
407                 filter(PlayerWeaponStat.player_id == player_id).\
408                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
409                 one()
410
411         avg = round(float(raw_avg[0])/raw_avg[1]*100, 2)
412
413         # Determine the raw accuracy (hit, fired) numbers for $games games
414         # This is then enumerated to create parameters for a flot graph
415         raw_accs = DBSession.query(PlayerWeaponStat.game_id, 
416             PlayerWeaponStat.hit, PlayerWeaponStat.fired).\
417                 filter(PlayerWeaponStat.player_id == player_id).\
418                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
419                 order_by(PlayerWeaponStat.game_id.desc()).\
420                 limit(games).\
421                 all()
422
423         # they come out in opposite order, so flip them in the right direction
424         raw_accs.reverse()
425
426         accs = []
427         for i in range(len(raw_accs)):
428             accs.append((raw_accs[i][0], round(float(raw_accs[i][1])/raw_accs[i][2]*100, 2)))
429     except:
430         accs = []
431         avg = 0.0
432
433     return (avg, accs)
434
435
436 def get_damage_stats(player_id, weapon_cd, games):
437     """
438     Provides damage info for weapon_cd by player_id for the past N games.
439     """
440     try:
441         raw_avg = DBSession.query(func.sum(PlayerWeaponStat.actual),
442                 func.sum(PlayerWeaponStat.hit)).\
443                 filter(PlayerWeaponStat.player_id == player_id).\
444                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
445                 one()
446
447         avg = round(float(raw_avg[0])/raw_avg[1], 2)
448
449         # Determine the damage efficiency (hit, fired) numbers for $games games
450         # This is then enumerated to create parameters for a flot graph
451         raw_dmgs = DBSession.query(PlayerWeaponStat.game_id, 
452             PlayerWeaponStat.actual, PlayerWeaponStat.hit).\
453                 filter(PlayerWeaponStat.player_id == player_id).\
454                 filter(PlayerWeaponStat.weapon_cd == weapon_cd).\
455                 order_by(PlayerWeaponStat.game_id.desc()).\
456                 limit(games).\
457                 all()
458
459         # they come out in opposite order, so flip them in the right direction
460         raw_dmgs.reverse()
461
462         dmgs = []
463         for i in range(len(raw_dmgs)):
464             # try to derive, unless we've hit nothing then set to 0!
465             try:
466                 dmg = round(float(raw_dmgs[i][1])/raw_dmgs[i][2], 2)
467             except:
468                 dmg = 0.0
469
470             dmgs.append((raw_dmgs[i][0], dmg))
471     except Exception as e:
472         dmgs = []
473         avg = 0.0
474
475     return (avg, dmgs)
476
477
478 def player_info_data(request):
479     player_id = int(request.matchdict['id'])
480     if player_id <= 2:
481         player_id = -1;
482
483     try:
484         player = DBSession.query(Player).filter_by(player_id=player_id).\
485                 filter(Player.active_ind == True).one()
486
487         games_played   = get_games_played(player_id)
488         overall_stats  = get_overall_stats(player_id)
489         fav_maps       = get_fav_maps(player_id)
490         elos           = get_elos(player_id)
491         ranks          = get_ranks(player_id)
492         recent_games   = get_recent_games(player_id)
493         recent_weapons = get_recent_weapons(player_id)
494
495     except Exception as e:
496         player         = None
497         games_played   = None
498         overall_stats  = None
499         fav_maps       = None
500         elos           = None
501         ranks          = None
502         recent_games   = None
503         recent_weapons = []
504
505     return {'player':player,
506             'games_played':games_played,
507             'overall_stats':overall_stats,
508             'fav_maps':fav_maps,
509             'elos':elos,
510             'ranks':ranks,
511             'recent_games':recent_games,
512             'recent_weapons':recent_weapons
513             }
514
515
516 def player_info(request):
517     """
518     Provides detailed information on a specific player
519     """
520     return player_info_data(request)
521
522
523 def player_info_json(request):
524     """
525     Provides detailed information on a specific player. JSON.
526     """
527
528     # All player_info fields are converted into JSON-formattable dictionaries
529     player_info = player_info_data(request)    
530
531     player = player_info['player'].to_dict()
532
533     games_played = {}
534     for game in player_info['games_played']:
535         games_played[game.game_type_cd] = to_json(game)
536
537     overall_stats = {}
538     for gt,stats in player_info['overall_stats'].items():
539         overall_stats[gt] = to_json(stats)
540
541     elos = {}
542     for gt,elo in player_info['elos'].items():
543         elos[gt] = to_json(elo.to_dict())
544
545     ranks = {}
546     for gt,rank in player_info['ranks'].items():
547         ranks[gt] = to_json(rank)
548
549     fav_maps = {}
550     for gt,mapinfo in player_info['fav_maps'].items():
551         fav_maps[gt] = to_json(mapinfo)
552
553     recent_games = []
554     for game in player_info['recent_games']:
555         recent_games.append(to_json(game))
556
557     #recent_weapons = player_info['recent_weapons']
558
559     return [{
560         'player':           player,
561         'games_played':     games_played,
562         'overall_stats':    overall_stats,
563         'fav_maps':         fav_maps,
564         'elos':             elos,
565         'ranks':            ranks,
566         'recent_games':     recent_games,
567     #    'recent_weapons':   recent_weapons,
568         'recent_weapons':   ['not implemented'],
569     }]
570     #return [{'status':'not implemented'}]
571
572
573 def player_game_index_data(request):
574     player_id = request.matchdict['player_id']
575
576     if request.params.has_key('page'):
577         current_page = request.params['page']
578     else:
579         current_page = 1
580
581     try:
582         player = DBSession.query(Player).filter_by(player_id=player_id).\
583                 filter(Player.active_ind == True).one()
584
585         rgs_q = recent_games_q(player_id=player.player_id)
586
587         games = Page(rgs_q, current_page, items_per_page=10, url=page_url)
588
589         # replace the items in the canned pagination class with more rich ones
590         games.items = [RecentGame(row) for row in games.items]
591
592     except Exception as e:
593         player = None
594         games = None
595
596     return {
597             'player_id':player.player_id,
598             'player':player,
599             'games':games,
600            }
601
602
603 def player_game_index(request):
604     """
605     Provides an index of the games in which a particular
606     player was involved. This is ordered by game_id, with
607     the most recent game_ids first. Paginated.
608     """
609     return player_game_index_data(request)
610
611
612 def player_game_index_json(request):
613     """
614     Provides an index of the games in which a particular
615     player was involved. This is ordered by game_id, with
616     the most recent game_ids first. Paginated. JSON.
617     """
618     return [{'status':'not implemented'}]
619
620
621 def player_accuracy_data(request):
622     player_id = request.matchdict['id']
623     allowed_weapons = ['nex', 'rifle', 'shotgun', 'uzi', 'minstanex']
624     weapon_cd = 'nex'
625     games = 20
626
627     if request.params.has_key('weapon'):
628         if request.params['weapon'] in allowed_weapons:
629             weapon_cd = request.params['weapon']
630
631     if request.params.has_key('games'):
632         try:
633             games = request.params['games']
634
635             if games < 0:
636                 games = 20
637             if games > 50:
638                 games = 50
639         except:
640             games = 20
641
642     (avg, accs) = get_accuracy_stats(player_id, weapon_cd, games)
643
644     # if we don't have enough data for the given weapon
645     if len(accs) < games:
646         games = len(accs)
647
648     return {
649             'player_id':player_id, 
650             'player_url':request.route_url('player_info', id=player_id), 
651             'weapon':weapon_cd, 
652             'games':games, 
653             'avg':avg, 
654             'accs':accs
655             }
656
657
658 def player_accuracy(request):
659     """
660     Provides the accuracy for the given weapon. (JSON only)
661     """
662     return player_accuracy_data(request)
663
664
665 def player_accuracy_json(request):
666     """
667     Provides a JSON response representing the accuracy for the given weapon.
668
669     Parameters:
670        weapon = which weapon to display accuracy for. Valid values are 'nex',
671                 'shotgun', 'uzi', and 'minstanex'.
672        games = over how many games to display accuracy. Can be up to 50.
673     """
674     return player_accuracy_data(request)
675
676
677 def player_damage_data(request):
678     player_id = request.matchdict['id']
679     allowed_weapons = ['grenadelauncher', 'electro', 'crylink', 'hagar',
680             'rocketlauncher', 'laser']
681     weapon_cd = 'rocketlauncher'
682     games = 20
683
684     if request.params.has_key('weapon'):
685         if request.params['weapon'] in allowed_weapons:
686             weapon_cd = request.params['weapon']
687
688     if request.params.has_key('games'):
689         try:
690             games = request.params['games']
691
692             if games < 0:
693                 games = 20
694             if games > 50:
695                 games = 50
696         except:
697             games = 20
698
699     (avg, dmgs) = get_damage_stats(player_id, weapon_cd, games)
700
701     # if we don't have enough data for the given weapon
702     if len(dmgs) < games:
703         games = len(dmgs)
704
705     return {
706             'player_id':player_id, 
707             'player_url':request.route_url('player_info', id=player_id), 
708             'weapon':weapon_cd, 
709             'games':games, 
710             'avg':avg, 
711             'dmgs':dmgs
712             }
713
714
715 def player_damage_json(request):
716     """
717     Provides a JSON response representing the damage for the given weapon.
718
719     Parameters:
720        weapon = which weapon to display damage for. Valid values are
721          'grenadelauncher', 'electro', 'crylink', 'hagar', 'rocketlauncher',
722          'laser'.
723        games = over how many games to display damage. Can be up to 50.
724     """
725     return player_damage_data(request)
726
727
728 def player_hashkey_info_data(request):
729     hashkey = request.matchdict['hashkey']
730     try:
731         player = DBSession.query(Player).\
732                 filter(Player.player_id == Hashkey.player_id).\
733                 filter(Player.active_ind == True).\
734                 filter(Hashkey.hashkey == hashkey).one()
735
736         games_played   = get_games_played(player.player_id)
737         overall_stats  = get_overall_stats(player.player_id)
738         fav_maps       = get_fav_maps(player.player_id)
739         elos           = get_elos(player.player_id)
740         ranks          = get_ranks(player.player_id)
741
742     except Exception as e:
743         raise pyramid.httpexceptions.HTTPNotFound
744
745     return {'player':player,
746             'hashkey':hashkey,
747             'games_played':games_played,
748             'overall_stats':overall_stats,
749             'fav_maps':fav_maps,
750             'elos':elos,
751             'ranks':ranks,
752             }
753
754
755 def player_hashkey_info_json(request):
756     """
757     Provides detailed information on a specific player. JSON.
758     """
759
760     # All player_info fields are converted into JSON-formattable dictionaries
761     player_info = player_hashkey_info_data(request)
762
763     player = player_info['player'].to_dict()
764
765     games_played = {}
766     for game in player_info['games_played']:
767         games_played[game.game_type_cd] = to_json(game)
768
769     overall_stats = {}
770     for gt,stats in player_info['overall_stats'].items():
771         overall_stats[gt] = to_json(stats)
772
773     elos = {}
774     for gt,elo in player_info['elos'].items():
775         elos[gt] = to_json(elo.to_dict())
776
777     ranks = {}
778     for gt,rank in player_info['ranks'].items():
779         ranks[gt] = to_json(rank)
780
781     fav_maps = {}
782     for gt,mapinfo in player_info['fav_maps'].items():
783         fav_maps[gt] = to_json(mapinfo)
784
785     return [{
786         'version':          1,
787         'player':           player,
788         'games_played':     games_played,
789         'overall_stats':    overall_stats,
790         'fav_maps':         fav_maps,
791         'elos':             elos,
792         'ranks':            ranks,
793     }]
794
795
796 def player_hashkey_info_text(request):
797     """
798     Provides detailed information on a specific player. Plain text.
799     """
800     # UTC epoch
801     now = timegm(datetime.datetime.utcnow().timetuple())
802
803     # All player_info fields are converted into JSON-formattable dictionaries
804     player_info = player_hashkey_info_data(request)
805
806     # gather all of the data up into aggregate structures
807     player = player_info['player']
808     games_played = player_info['games_played']
809     overall_stats = player_info['overall_stats']
810     elos = player_info['elos']
811     ranks = player_info['ranks']
812     fav_maps = player_info['fav_maps']
813
814     # one-offs for things needing conversion for text/plain
815     player_joined = timegm(player.create_dt.timetuple())
816     alivetime = int(datetime_seconds(overall_stats['overall'].total_playing_time))
817
818     # this is a plain text response, if we don't do this here then
819     # Pyramid will assume html
820     request.response.content_type = 'text/plain'
821
822     return {
823         'version':          1,
824         'now':              now,
825         'player':           player,
826         'hashkey':          player_info['hashkey'],
827         'player_joined':    player_joined,
828         'games_played':     games_played,
829         'overall_stats':    overall_stats,
830         'alivetime':        alivetime,
831         'fav_maps':         fav_maps,
832         'elos':             elos,
833         'ranks':            ranks,
834     }
835
836
837 def player_elo_info_data(request):
838     """
839     Provides elo information on a specific player. Raw data is returned.
840     """
841     hashkey = request.matchdict['hashkey']
842     try:
843         player = DBSession.query(Player).\
844                 filter(Player.player_id == Hashkey.player_id).\
845                 filter(Player.active_ind == True).\
846                 filter(Hashkey.hashkey == hashkey).one()
847
848         elos = get_elos(player.player_id)
849
850     except Exception as e:
851         log.debug(e)
852         raise pyramid.httpexceptions.HTTPNotFound
853
854     return {'elos':elos}
855
856
857 def player_elo_info_json(request):
858     """
859     Provides elo information on a specific player. JSON.
860     """
861     elo_info = player_elo_info_data(request)
862
863     elos = {}
864     for gt, elo in elo_info['elos'].items():
865         elos[gt] = to_json(elo.to_dict())
866
867     return [{
868         'version':          1,
869         'elos':             elos,
870     }]