You've already forked q3stats
Q3Stats is now open source! :)
This commit is contained in:
3
q3stats/__init__.py
Normal file
3
q3stats/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = '1.0'
|
||||
67
q3stats/database.py
Normal file
67
q3stats/database.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.database
|
||||
================
|
||||
|
||||
This module contains database utilities.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
||||
#: Base for declarative models
|
||||
Base = declarative_base()
|
||||
|
||||
#: Current SQLAlchemy engine
|
||||
db_engine = None
|
||||
|
||||
#: Current SQLAlchemy session
|
||||
db_session = None
|
||||
|
||||
|
||||
def create_engine(config):
|
||||
"""Creates and initializes engine according to *config* dictionary.
|
||||
|
||||
Returns the initialized engine."""
|
||||
engine = sa.create_engine(
|
||||
config['SQLALCHEMY_URL'], **config.get('SQLALCHEMY_OPTS', {})
|
||||
)
|
||||
|
||||
return engine
|
||||
|
||||
|
||||
def create_session(engine, config, scoped=True):
|
||||
"""Creates a session factory bound to *engine* according to *config*
|
||||
dictionary. If *scoped* is ``True`` then the session will be scoped.
|
||||
Otherwise, it'll be a standard ``sessionmaker``.
|
||||
|
||||
Returns the initialized session factory."""
|
||||
Session = sa.orm.sessionmaker(
|
||||
bind=engine, **config.get('SQLALCHEMY_SESSION_OPTS', {})
|
||||
)
|
||||
|
||||
if scoped:
|
||||
return sa.orm.scoped_session(Session)
|
||||
|
||||
return Session
|
||||
0
q3stats/lib/__init__.py
Normal file
0
q3stats/lib/__init__.py
Normal file
2
q3stats/lib/charts/__init__.py
Normal file
2
q3stats/lib/charts/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .day import get_day_chart
|
||||
from .player import get_player_wins_chart, get_player_avg_accuracy_chart
|
||||
64
q3stats/lib/charts/day.py
Normal file
64
q3stats/lib/charts/day.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.lib.charts.day
|
||||
======================
|
||||
|
||||
This module contains functions for genering day chart.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from q3stats.models import Game, Score
|
||||
|
||||
|
||||
def get_day_chart(day, session):
|
||||
"""Returns chart data for *day*."""
|
||||
games = session.query(Game).\
|
||||
filter_by(date=day).\
|
||||
order_by(Game.time).\
|
||||
all()
|
||||
|
||||
maps = []
|
||||
game_ids = []
|
||||
player_scores = defaultdict(dict)
|
||||
|
||||
for game in games:
|
||||
maps.append(game.map)
|
||||
game_ids.append(game.id)
|
||||
|
||||
for score in game.scores:
|
||||
player_scores[score.player][game.id] = score.score
|
||||
|
||||
scores = []
|
||||
players = sorted(player_scores.keys(), key=lambda x: x.lower())
|
||||
|
||||
for player in players:
|
||||
scores.append({
|
||||
'name': player,
|
||||
'data': [
|
||||
player_scores[player].get(game_id, 0) for game_id in game_ids
|
||||
]
|
||||
})
|
||||
|
||||
return maps, scores
|
||||
125
q3stats/lib/charts/player.py
Normal file
125
q3stats/lib/charts/player.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.lib.charts.player
|
||||
=========================
|
||||
|
||||
This module contains functions for genering player charts.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
import six
|
||||
|
||||
from q3stats.lib import defs, queries
|
||||
from q3stats.models import Game, Score
|
||||
|
||||
|
||||
def get_player_wins_chart(session, player, agg_by='date'):
|
||||
"""Returns wins chart data for *player*.
|
||||
|
||||
Use the *agg_by* kwargs to specify aggregation level. Valid values are
|
||||
``date`` and ``map``."""
|
||||
assert agg_by in ('date', 'map')
|
||||
|
||||
categories = []
|
||||
wins_serie = []
|
||||
losses_serie = []
|
||||
|
||||
player_sessions = queries.get_player_sessions(session, player)
|
||||
if player_sessions:
|
||||
scores = session.query(Score).\
|
||||
join(Score.game).\
|
||||
filter(Game.date.in_(player_sessions)).\
|
||||
filter(Score.player == player).\
|
||||
all()
|
||||
|
||||
if scores:
|
||||
intermediary = defaultdict(lambda: [0, 0])
|
||||
for score in scores:
|
||||
agg = getattr(score.game, agg_by)
|
||||
if score.score == score.game.fraglimit:
|
||||
intermediary[agg][0] += 1
|
||||
else:
|
||||
intermediary[agg][1] += 1
|
||||
|
||||
categories = sorted(intermediary.keys())
|
||||
for key in categories:
|
||||
wins_serie.append(intermediary[key][0])
|
||||
losses_serie.append(intermediary[key][1])
|
||||
|
||||
return categories, wins_serie, losses_serie
|
||||
|
||||
|
||||
def get_player_avg_accuracy_chart(session, player, agg_by='date'):
|
||||
"""Returns avg accuracy chart data for *player*.
|
||||
|
||||
Use the *agg_by* kwargs to specify aggregation level. Valid values are
|
||||
``date`` and ``map``."""
|
||||
assert agg_by in ('date', 'map')
|
||||
|
||||
categories = []
|
||||
series = []
|
||||
|
||||
player_sessions = queries.get_player_sessions(session, player)
|
||||
if player_sessions:
|
||||
scores = session.query(Score).\
|
||||
join(Score.game).\
|
||||
filter(Game.date.in_(player_sessions)).\
|
||||
filter(Score.player == player).\
|
||||
all()
|
||||
|
||||
if scores:
|
||||
intermediary = defaultdict(lambda: defaultdict(list))
|
||||
for score in scores:
|
||||
for weapon, weapon_stats in six.iteritems(score.weapons):
|
||||
if weapon_stats['shots'] > 0:
|
||||
agg = getattr(score.game, agg_by)
|
||||
intermediary[agg][weapon].append(
|
||||
weapon_stats['hits'] / float(weapon_stats['shots'])
|
||||
)
|
||||
|
||||
categories = sorted(intermediary.keys())
|
||||
|
||||
weapons = list(defs.WEAPON_NAMES.keys())
|
||||
weapons.remove('G')
|
||||
weapons.sort()
|
||||
|
||||
for weapon in weapons:
|
||||
serie = {
|
||||
'name': defs.WEAPON_NAMES[weapon],
|
||||
'data': []
|
||||
}
|
||||
|
||||
for key in categories:
|
||||
accuracies = intermediary[key][weapon]
|
||||
if not accuracies:
|
||||
serie['data'].append(None)
|
||||
else:
|
||||
serie['data'].append(
|
||||
round(sum(accuracies) / len(accuracies), 4) * 100
|
||||
)
|
||||
|
||||
series.append(serie)
|
||||
|
||||
return categories, series
|
||||
62
q3stats/lib/config.py
Normal file
62
q3stats/lib/config.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.lib.config
|
||||
==================
|
||||
|
||||
This module contains configuration handling functions.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from flask import Config
|
||||
|
||||
#: Default configuration file name.
|
||||
DEFAULT_CONFIG_FILE = 'development.cfg'
|
||||
|
||||
|
||||
def get_config_path(config_path=None):
|
||||
"""Returns an absolute path to a config file."""
|
||||
if config_path is None:
|
||||
config_path = os.environ.get(
|
||||
'Q3STATS_CONFIG_FILE', DEFAULT_CONFIG_FILE
|
||||
)
|
||||
|
||||
if not os.path.isabs(config_path):
|
||||
config_path = os.path.abspath(os.path.join(
|
||||
os.getcwd(), config_path
|
||||
))
|
||||
|
||||
return config_path
|
||||
|
||||
|
||||
def get_config(config_path=None):
|
||||
"""Returns an instance of ``flask.Config`` class with config loaded from
|
||||
*config_path*."""
|
||||
config_path = get_config_path(config_path)
|
||||
dirname, basename = os.path.split(config_path)
|
||||
|
||||
config = Config(dirname)
|
||||
config.from_pyfile(basename)
|
||||
|
||||
return config
|
||||
61
q3stats/lib/defs.py
Normal file
61
q3stats/lib/defs.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.lib.defs
|
||||
================
|
||||
|
||||
This module contains constants used across the project.
|
||||
"""
|
||||
|
||||
#: Standard date format.
|
||||
DATE_FORMAT = '%Y-%m-%d'
|
||||
|
||||
#: Standard time format.
|
||||
TIME_FORMAT = '%H:%M:%S'
|
||||
|
||||
#: Dictionary mapping Q3A weapon symbols to human readable names.
|
||||
WEAPON_NAMES = {
|
||||
'BFG': 'BFG10k',
|
||||
'G': 'Gauntlet',
|
||||
'GL': 'Grenade Launcher',
|
||||
'LG': 'Lightning Gun',
|
||||
'MG': 'Machine Gun',
|
||||
'PG': 'Plasma Gun',
|
||||
'RG': 'Railgun',
|
||||
'RL': 'Rocket Launcher',
|
||||
'SG': 'Shotgun',
|
||||
}
|
||||
|
||||
#: Dictionary mapping Q3A item symbols to human readable names.
|
||||
ITEM_NAMES = {
|
||||
'GA': 'Green Armor',
|
||||
'MH': 'Mega Health',
|
||||
'RA': 'Red Armor',
|
||||
'Regen': 'Regeneration',
|
||||
'YA': 'Yellow Armor',
|
||||
}
|
||||
|
||||
#: Dictionary mapping Q3A powerup symbols to human readable names.
|
||||
POWERUP_NAMES = {
|
||||
'Quad': 'Quad Damage'
|
||||
}
|
||||
105
q3stats/lib/queries.py
Normal file
105
q3stats/lib/queries.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.lib.queries
|
||||
===================
|
||||
|
||||
This module contains functions for querying the database.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from q3stats.models import Game, Score
|
||||
|
||||
SQL_TOP_PLAYERS = """SELECT
|
||||
scores.player,
|
||||
sum(scores.%s) as sum_frags
|
||||
FROM scores
|
||||
LEFT JOIN games ON games.id = scores.game_id
|
||||
WHERE
|
||||
games.date >= :date_start
|
||||
AND games.date < :date_end
|
||||
GROUP BY scores.player
|
||||
ORDER BY sum_frags DESC
|
||||
LIMIT :limit"""
|
||||
|
||||
|
||||
def get_game_dates(db_session):
|
||||
"""Returns a list of unique game dates."""
|
||||
dates_query = sa.sql.expression.select([Game.__table__.c.date]).\
|
||||
distinct(Game.__table__.c.date).\
|
||||
order_by(Game.__table__.c.date.desc())
|
||||
|
||||
dates = [x.date for x in db_session.execute(dates_query)]
|
||||
return dates
|
||||
|
||||
|
||||
def get_player_sessions(db_session, player, limit=7):
|
||||
"""Returns a list of unique sessions for the *player*."""
|
||||
player_sessions_query = sa.sql.expression.\
|
||||
select([Game.__table__.c.date]).\
|
||||
distinct(Game.__table__.c.date).\
|
||||
select_from(Game.__table__.join(
|
||||
Score.__table__, Score.__table__.c.game_id == Game.__table__.c.id
|
||||
)).\
|
||||
where(Score.__table__.c.player == player).\
|
||||
order_by(Game.__table__.c.date.desc()).\
|
||||
limit(limit)
|
||||
|
||||
player_sessions = [
|
||||
x.date for x in db_session.execute(player_sessions_query)
|
||||
]
|
||||
|
||||
return player_sessions
|
||||
|
||||
|
||||
def get_top_players(session, agg_by='kills', limit=3):
|
||||
"""Returns a list of ``(player, score)`` tuples identifying top players
|
||||
in the current month.
|
||||
|
||||
Use the *agg_by* kwargs to specify aggregation level. Valid values are
|
||||
``kills`` and ``suicides``."""
|
||||
assert agg_by in ('kills', 'suicides')
|
||||
|
||||
date_start = datetime.datetime.now().replace(day=1).date()
|
||||
date_end = None
|
||||
|
||||
if date_start.month == 12:
|
||||
date_end = datetime.date(
|
||||
date_start.year + 1, 1, 1
|
||||
)
|
||||
else:
|
||||
date_end = datetime.date(
|
||||
date_start.year, date_start.month + 1, 1
|
||||
)
|
||||
|
||||
query = SQL_TOP_PLAYERS % agg_by
|
||||
params = {
|
||||
'date_start': date_start, 'date_end': date_end, 'limit': limit
|
||||
}
|
||||
|
||||
return [
|
||||
(x.player, x.sum_frags) for x in session.execute(query, params)
|
||||
]
|
||||
0
q3stats/lib/scripts/__init__.py
Normal file
0
q3stats/lib/scripts/__init__.py
Normal file
228
q3stats/lib/scripts/import_games.py
Normal file
228
q3stats/lib/scripts/import_games.py
Normal file
@@ -0,0 +1,228 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.lib.scripts.import_games
|
||||
================================
|
||||
|
||||
This module contains the Q3A game importer.
|
||||
"""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
import datetime
|
||||
import codecs
|
||||
import logging
|
||||
import os
|
||||
|
||||
import lxml.etree
|
||||
|
||||
from q3stats.lib.config import get_config
|
||||
from q3stats.lib.scripts import utils
|
||||
from q3stats.models import Game, Score
|
||||
|
||||
__all__ = ['script_main']
|
||||
|
||||
XML_STAT_TO_MODEL_FIELD = {
|
||||
'Score': 'score',
|
||||
'Kills': 'kills',
|
||||
'Deaths': 'deaths',
|
||||
'Suicides': 'suicides',
|
||||
'Net': 'net',
|
||||
'DamageGiven': 'damage_given',
|
||||
'DamageTaken': 'damage_taken',
|
||||
'HealthTotal': 'total_health',
|
||||
'ArmorTotal': 'total_armor',
|
||||
}
|
||||
|
||||
XML_STAT_POWERUP_NAMES = ['Quad']
|
||||
|
||||
|
||||
def _find_stats_files(stats_path):
|
||||
"""Finds Q3A stats files in *stats_path*."""
|
||||
stats_files = []
|
||||
for root, dirs, files in os.walk(stats_path):
|
||||
for filename in files:
|
||||
_, ext = os.path.splitext(filename)
|
||||
if ext.lower() == '.xml':
|
||||
stats_files.append(os.path.join(root, filename))
|
||||
|
||||
return stats_files
|
||||
|
||||
|
||||
def _read_match(file_path):
|
||||
"""Reads a Q3A match file from *file_path*."""
|
||||
with codecs.open(file_path, 'r', 'utf-8') as stats_f:
|
||||
stats_xml = lxml.etree.parse(stats_f)
|
||||
|
||||
xmlroot = stats_xml.getroot()
|
||||
if xmlroot.tag != 'match':
|
||||
return None
|
||||
|
||||
return xmlroot
|
||||
|
||||
|
||||
def _read_game(match):
|
||||
"""Creates and returns an instance of :py:class:`q3stats.models.game.Game`
|
||||
filled with game data from *match*."""
|
||||
ts = datetime.datetime.strptime(
|
||||
match.get('datetime'), '%Y/%m/%d %H:%M:%S'
|
||||
)
|
||||
|
||||
is_team = False
|
||||
if match.get('isTeamGame') == 'true':
|
||||
is_team = True
|
||||
|
||||
game = Game(
|
||||
map=match.get('map').upper(),
|
||||
date=ts.date(),
|
||||
time=ts.time(),
|
||||
attrs={
|
||||
'type': match.get('type'),
|
||||
'team_game': is_team,
|
||||
'duration': int(match.get('duration')),
|
||||
}
|
||||
)
|
||||
|
||||
game.update_uuid()
|
||||
|
||||
return game
|
||||
|
||||
|
||||
def _read_scores(match):
|
||||
"""Reads game data from *match* and extracts player scores.
|
||||
|
||||
Returns an array of :py:class:`q3stats.models.score.Score` objects and
|
||||
the actual fraglimit (aka the highest score)."""
|
||||
scores = []
|
||||
|
||||
current_score = None
|
||||
fraglimit = 0
|
||||
for element in match.iter():
|
||||
if element.tag == 'player':
|
||||
if current_score is not None:
|
||||
if current_score.score > fraglimit:
|
||||
fraglimit = current_score.score
|
||||
|
||||
scores.append(current_score)
|
||||
|
||||
current_score = Score(player=element.get('name'))
|
||||
current_score.weapons = {}
|
||||
current_score.items = {}
|
||||
current_score.powerups = {}
|
||||
elif element.tag == 'stat':
|
||||
name = element.get('name')
|
||||
if name in XML_STAT_TO_MODEL_FIELD:
|
||||
setattr(
|
||||
current_score, XML_STAT_TO_MODEL_FIELD[name],
|
||||
int(element.get('value'))
|
||||
)
|
||||
elif element.tag == 'weapon':
|
||||
current_score.weapons[element.get('name')] = {
|
||||
'hits': int(element.get('hits')),
|
||||
'shots': int(element.get('shots')),
|
||||
'kills': int(element.get('kills')),
|
||||
}
|
||||
elif element.tag == 'item':
|
||||
name = element.get('name')
|
||||
if name in XML_STAT_POWERUP_NAMES:
|
||||
current_score.powerups[name] = [
|
||||
int(element.get('pickups')), int(element.get('time'))
|
||||
]
|
||||
else:
|
||||
current_score.items[name] = int(element.get('pickups'))
|
||||
|
||||
if current_score is not None:
|
||||
if current_score.score > fraglimit:
|
||||
fraglimit = current_score.score
|
||||
|
||||
scores.append(current_score)
|
||||
|
||||
return scores, fraglimit
|
||||
|
||||
|
||||
def _read_stats_file(file_path):
|
||||
"""Reads stats file from *file_path*.
|
||||
|
||||
Returns an instance of :py:class:`q3stats.models.game.Game` with
|
||||
associated instances of :py:class:`q3stats.models.score.Score`."""
|
||||
match = _read_match(file_path)
|
||||
if match is None:
|
||||
raise RuntimeError('Invalid stats file: %s' % file_path)
|
||||
|
||||
game = _read_game(match)
|
||||
|
||||
scores, fraglimit = _read_scores(match)
|
||||
game.scores.extend(scores)
|
||||
game.fraglimit = fraglimit
|
||||
|
||||
return game
|
||||
|
||||
|
||||
def _get_argument_parser():
|
||||
"""Creates and returns an instance of ``ArgumentParser`` configured for
|
||||
the script."""
|
||||
parser = ArgumentParser(description='Import Q3A stats')
|
||||
parser.add_argument('stats_path', help='path to stats directory')
|
||||
parser.add_argument(
|
||||
'-v', '--verbose', action='store_true', default=False,
|
||||
help='be verbose'
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def script_main(argv, config=None):
|
||||
"""Script entry point."""
|
||||
result = utils.RET_OK
|
||||
|
||||
parser = _get_argument_parser()
|
||||
args = parser.parse_args(args=argv)
|
||||
|
||||
logger = utils.get_logger('import_games', args)
|
||||
|
||||
try:
|
||||
if config is None:
|
||||
config = get_config()
|
||||
|
||||
stats_files = _find_stats_files(args.stats_path)
|
||||
logger.debug('Found %d stat files.' % len(stats_files))
|
||||
|
||||
with utils.db_session(config) as session:
|
||||
for stats_file in stats_files:
|
||||
logger.debug('Processing file: %s' % stats_file)
|
||||
game = _read_stats_file(stats_file)
|
||||
|
||||
current_game = session.query(Game).\
|
||||
filter_by(uuid=game.uuid).\
|
||||
first()
|
||||
|
||||
if current_game is not None:
|
||||
logger.debug('Skipping!')
|
||||
else:
|
||||
session.add(game)
|
||||
|
||||
session.commit()
|
||||
except Exception as exc:
|
||||
logger.error('Unhandled exception', exc_info=True)
|
||||
result = utils.RET_ERROR
|
||||
finally:
|
||||
return result
|
||||
100
q3stats/lib/scripts/utils.py
Normal file
100
q3stats/lib/scripts/utils.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.lib.scripts.utils
|
||||
=========================
|
||||
|
||||
This module contains utility functions used by scripts.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
|
||||
import q3stats.database
|
||||
|
||||
RET_OK = 0
|
||||
RET_ERROR = 99
|
||||
|
||||
|
||||
def pprint(value, stream=None):
|
||||
"""JSON-based pretty print. *stream* defaults to ``sys.stdout``."""
|
||||
if stream is None:
|
||||
stream = sys.stdout
|
||||
|
||||
stream.write(json.dumps(value, indent=4, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def db_session(config):
|
||||
"""Context manager for getting the DB session configured for the current
|
||||
environment.
|
||||
|
||||
On exception, the session will be rolled back. The session is always
|
||||
properly disposed of, regardless of any errors.
|
||||
|
||||
**Example**:
|
||||
|
||||
.. sourcecode:: python
|
||||
|
||||
def some_function():
|
||||
with db_session() as session:
|
||||
# Do something with the session.
|
||||
session.commit()
|
||||
"""
|
||||
if q3stats.database.db_engine is None:
|
||||
q3stats.database.db_engine = q3stats.database.create_engine(config)
|
||||
q3stats.database.db_session = q3stats.database.create_session(
|
||||
q3stats.database.db_engine, config
|
||||
)
|
||||
|
||||
session = q3stats.database.db_session
|
||||
try:
|
||||
yield session
|
||||
except:
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.remove()
|
||||
|
||||
|
||||
def get_logger(name, args):
|
||||
"""Sets up a ``logging.Logger`` instance for script named *name*."""
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
handler = logging.StreamHandler()
|
||||
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s ' + name + ': %(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
if args.verbose is True:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.WARNING)
|
||||
|
||||
return logger
|
||||
53
q3stats/lib/stats.py
Normal file
53
q3stats/lib/stats.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.lib.stats
|
||||
=================
|
||||
|
||||
This module contains functions for dealing with stats.
|
||||
"""
|
||||
|
||||
|
||||
def weapon_accuracy(weapon, weapon_stats):
|
||||
"""Returns a string representation of weapon accuracy."""
|
||||
if weapon == 'G':
|
||||
return '---'
|
||||
|
||||
if weapon_stats['shots'] == 0:
|
||||
return 'NaN'
|
||||
|
||||
accuracy = float(weapon_stats['hits']) / float(weapon_stats['shots'])
|
||||
return '{:.2%}'.format(accuracy)
|
||||
|
||||
|
||||
def powerup_time(seconds):
|
||||
"""Returns a string representation of powerup time."""
|
||||
if not seconds:
|
||||
return 'NaN'
|
||||
|
||||
result = str(seconds)
|
||||
if len(result) < 4:
|
||||
result = result.zfill(4)
|
||||
|
||||
result = result[0:-3] + '.' + result[-3:]
|
||||
return result
|
||||
2
q3stats/models/__init__.py
Normal file
2
q3stats/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from .game import Game
|
||||
from .score import Score
|
||||
103
q3stats/models/game.py
Normal file
103
q3stats/models/game.py
Normal file
@@ -0,0 +1,103 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.models.game
|
||||
===================
|
||||
|
||||
This module contains the ``Game`` model.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.dialects.postgresql as pg
|
||||
|
||||
from q3stats.database import Base
|
||||
from q3stats.lib import defs
|
||||
|
||||
|
||||
class Game(Base):
|
||||
"""The ``Game`` model."""
|
||||
__tablename__ = 'games'
|
||||
__table_args__ = (
|
||||
sa.UniqueConstraint('uuid'),
|
||||
)
|
||||
|
||||
#: Format string for UUID URL string
|
||||
UUID_UID_FORMAT = 'pl.bthlabs.q3stats.{date}.{time}.{map}'
|
||||
|
||||
#: ``id INT PRIMARY KEY``
|
||||
id = sa.Column(sa.Integer(), primary_key=True)
|
||||
|
||||
#: ``uuid VARCHAR(36)``
|
||||
uuid = sa.Column(sa.String(255))
|
||||
|
||||
#: ``map VARCHAR(255)``
|
||||
map = sa.Column(sa.String(255), index=True)
|
||||
|
||||
#: ``date DATE``
|
||||
date = sa.Column(sa.Date(), index=True)
|
||||
|
||||
#: ``time TIME``
|
||||
time = sa.Column(sa.Time())
|
||||
|
||||
#: ``fraglimit INT``
|
||||
fraglimit = sa.Column(sa.Integer())
|
||||
|
||||
#: ``attrs JSON``
|
||||
attrs = sa.Column(pg.JSON())
|
||||
|
||||
#: List of :py:class:`q3stats.models.score.Score` objects
|
||||
scores = sa.orm.relationship(
|
||||
'Score', backref=sa.orm.backref('game'), order_by='Score.score.desc()'
|
||||
)
|
||||
|
||||
def update_uuid(self):
|
||||
self.uuid = str(uuid.uuid3(
|
||||
uuid.NAMESPACE_OID,
|
||||
self.UUID_UID_FORMAT.format(
|
||||
date=self.date.strftime(defs.DATE_FORMAT),
|
||||
time=self.time.strftime(defs.TIME_FORMAT),
|
||||
map=self.map
|
||||
)
|
||||
))
|
||||
|
||||
def to_json(self, recursive=False):
|
||||
"""Returns dictionary representation suitable for serializing to JSON.
|
||||
|
||||
If *recursive* is ``True`` then the associated score objects will be
|
||||
included."""
|
||||
result = {
|
||||
'id': self.id,
|
||||
'uuid': self.uuid,
|
||||
'map': self.map,
|
||||
'date': self.date.strftime(defs.DATE_FORMAT),
|
||||
'time': self.time.strftime(defs.TIME_FORMAT),
|
||||
'attrs': dict(self.attrs),
|
||||
'scores': None
|
||||
}
|
||||
|
||||
if recursive:
|
||||
result['scores'] = [x.to_json() for x in self.scores]
|
||||
|
||||
return result
|
||||
104
q3stats/models/score.py
Normal file
104
q3stats/models/score.py
Normal file
@@ -0,0 +1,104 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.models.score
|
||||
====================
|
||||
|
||||
This module contains the ``Score`` model.
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
import sqlalchemy.dialects.postgresql as pg
|
||||
|
||||
from q3stats.database import Base
|
||||
|
||||
|
||||
class Score(Base):
|
||||
"""The ``Score`` model."""
|
||||
__tablename__ = 'scores'
|
||||
|
||||
#: ``id INT PRIMARY KEY``
|
||||
id = sa.Column(sa.Integer(), primary_key=True)
|
||||
|
||||
#: ``game_id INT REFERENCES game.id``
|
||||
game_id = sa.Column(sa.Integer(), sa.ForeignKey('games.id'), index=True)
|
||||
|
||||
#: ``player VARCHAR(255)``
|
||||
player = sa.Column(sa.String(255), index=True)
|
||||
|
||||
#: ``score INT``
|
||||
score = sa.Column(sa.Integer())
|
||||
|
||||
#: ``kills INT``
|
||||
kills = sa.Column(sa.Integer())
|
||||
|
||||
#: ``deaths INT``
|
||||
deaths = sa.Column(sa.Integer())
|
||||
|
||||
#: ``sucides INT``
|
||||
suicides = sa.Column(sa.Integer())
|
||||
|
||||
#: ``net INT``
|
||||
net = sa.Column(sa.Integer())
|
||||
|
||||
#: ``damage_taken INT``
|
||||
damage_taken = sa.Column(sa.Integer())
|
||||
|
||||
#: ``damage_given INT``
|
||||
damage_given = sa.Column(sa.Integer())
|
||||
|
||||
#: ``total_health INT``
|
||||
total_health = sa.Column(sa.Integer())
|
||||
|
||||
#: ``total_armor INT``
|
||||
total_armor = sa.Column(sa.Integer())
|
||||
|
||||
#: ``weapons JSON``
|
||||
weapons = sa.Column(pg.JSON())
|
||||
|
||||
#: ``items JSON``
|
||||
items = sa.Column(pg.JSON())
|
||||
|
||||
#: ``powerups JSON``
|
||||
powerups = sa.Column(pg.JSON())
|
||||
|
||||
def to_json(self):
|
||||
"""Returns dictionary representation suitable for serializing to
|
||||
JSON."""
|
||||
return {
|
||||
'id': self.id,
|
||||
'game_id': self.game_id,
|
||||
'player': self.player,
|
||||
'score': self.score,
|
||||
'kills': self.kills,
|
||||
'deaths': self.deaths,
|
||||
'suicides': self.suicides,
|
||||
'net': self.net,
|
||||
'damage_taken': self.damage_taken,
|
||||
'damage_given': self.damage_given,
|
||||
'total_health': self.total_health,
|
||||
'total_armor': self.total_armor,
|
||||
'weapons': dict(self.weapons),
|
||||
'items': dict(self.items),
|
||||
'powerups': dict(self.powerups),
|
||||
}
|
||||
0
q3stats/scripts/__init__.py
Normal file
0
q3stats/scripts/__init__.py
Normal file
39
q3stats/scripts/import_games.py
Normal file
39
q3stats/scripts/import_games.py
Normal file
@@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.scripts.import_games
|
||||
============================
|
||||
|
||||
Imports games from a given directory.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(
|
||||
os.path.dirname(__file__), '..', '..'
|
||||
)))
|
||||
|
||||
from q3stats.lib.scripts import import_games
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(import_games.script_main(sys.argv[1:]))
|
||||
34
q3stats/scripts/wsgi_app.py
Normal file
34
q3stats/scripts/wsgi_app.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.scripts.wsgi_app
|
||||
========================
|
||||
|
||||
WSGI app entry point.
|
||||
"""
|
||||
|
||||
from q3stats.lib.config import get_config_path
|
||||
from q3stats.web_app.application import create_app
|
||||
|
||||
config_path = get_config_path()
|
||||
app = create_app(config_path)
|
||||
68
q3stats/skel/alembic.ini
Normal file
68
q3stats/skel/alembic.ini
Normal file
@@ -0,0 +1,68 @@
|
||||
# A generic, single database configuration.
|
||||
|
||||
[alembic]
|
||||
# path to migration scripts
|
||||
script_location = alembic
|
||||
|
||||
# template used to generate migration files
|
||||
# file_template = %%(rev)s_%%(slug)s
|
||||
|
||||
# max length of characters to apply to the
|
||||
# "slug" field
|
||||
#truncate_slug_length = 40
|
||||
|
||||
# set to 'true' to run the environment during
|
||||
# the 'revision' command, regardless of autogenerate
|
||||
# revision_environment = false
|
||||
|
||||
# set to 'true' to allow .pyc and .pyo files without
|
||||
# a source .py file to be detected as revisions in the
|
||||
# versions/ directory
|
||||
# sourceless = false
|
||||
|
||||
# version location specification; this defaults
|
||||
# to alembic/versions. When using multiple version
|
||||
# directories, initial revisions must be specified with --version-path
|
||||
# version_locations = %(here)s/bar %(here)s/bat alembic/versions
|
||||
|
||||
# the output encoding used when revision files
|
||||
# are written from script.py.mako
|
||||
# output_encoding = utf-8
|
||||
|
||||
sqlalchemy.url = postgres://user:pass@localhost/dbname
|
||||
|
||||
|
||||
# Logging configuration
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
||||
4
q3stats/skel/example.cfg
Normal file
4
q3stats/skel/example.cfg
Normal file
@@ -0,0 +1,4 @@
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
|
||||
SQLALCHEMY_URL = 'postgres://user:pass@localhost/dbname'
|
||||
90
q3stats/testing.py
Normal file
90
q3stats/testing.py
Normal file
@@ -0,0 +1,90 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.testing
|
||||
===============
|
||||
|
||||
This module contains testing utilites.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from flask import Response
|
||||
|
||||
from q3stats.lib.config import get_config
|
||||
import q3stats.database
|
||||
import q3stats.models
|
||||
from q3stats.web_app.application import create_app
|
||||
|
||||
|
||||
class ResponseWrapper(Response):
|
||||
@property
|
||||
def json(self):
|
||||
if not hasattr(self, '_json_data'):
|
||||
self._json_data = json.loads(self.get_data().decode('utf-8'))
|
||||
|
||||
return self._json_data
|
||||
|
||||
|
||||
class BaseQ3StatsTestCase(object):
|
||||
"""Base class for Q3Stats test cases."""
|
||||
|
||||
CONFIG_PATH = os.path.join(os.getcwd(), 'testing.cfg')
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls._config = get_config(cls.CONFIG_PATH)
|
||||
|
||||
q3stats.database.db_engine = q3stats.database.create_engine(
|
||||
cls._config
|
||||
)
|
||||
|
||||
q3stats.database.db_session = q3stats.database.create_session(
|
||||
q3stats.database.db_engine, cls._config
|
||||
)
|
||||
|
||||
q3stats.database.Base.metadata.drop_all(
|
||||
bind=q3stats.database.db_engine
|
||||
)
|
||||
|
||||
q3stats.database.Base.metadata.create_all(
|
||||
bind=q3stats.database.db_engine
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
|
||||
class BaseQ3StatsWebAppTestCase(BaseQ3StatsTestCase):
|
||||
"""Base class for Q3Stats Web App test cases."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(BaseQ3StatsWebAppTestCase, cls).setUpClass()
|
||||
|
||||
cls.app = create_app(cls.CONFIG_PATH)
|
||||
cls.app.response_class = ResponseWrapper
|
||||
|
||||
cls.client = cls.app.test_client()
|
||||
2
q3stats/web_app/.gitignore
vendored
Normal file
2
q3stats/web_app/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
static/
|
||||
templates/*.html
|
||||
0
q3stats/web_app/__init__.py
Normal file
0
q3stats/web_app/__init__.py
Normal file
100
q3stats/web_app/application.py
Normal file
100
q3stats/web_app/application.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.web_app.application
|
||||
===========================
|
||||
|
||||
This module contains the Web application core.
|
||||
"""
|
||||
|
||||
import flask
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from q3stats import __version__ as app_version
|
||||
import q3stats.database
|
||||
from q3stats.lib.config import get_config_path
|
||||
|
||||
|
||||
class Q3StatsApplication(flask.Flask):
|
||||
"""Customized ``flask.Flask`` subclass."""
|
||||
|
||||
def init_db(self):
|
||||
"""Creates the SQLAlchemy engine and session for this instance."""
|
||||
if q3stats.database.db_engine is None:
|
||||
q3stats.database.db_engine = q3stats.database.create_engine(
|
||||
self.config
|
||||
)
|
||||
|
||||
if q3stats.database.db_session is None:
|
||||
q3stats.database.db_session = q3stats.database.create_session(
|
||||
q3stats.database.db_engine, self.config
|
||||
)
|
||||
|
||||
@self.teardown_request
|
||||
def shutdown_session(exception=None):
|
||||
q3stats.database.db_session.remove()
|
||||
|
||||
@property
|
||||
def db_session(self):
|
||||
"""The SQLAlchemy session for the current context."""
|
||||
return q3stats.database.db_session
|
||||
|
||||
|
||||
def create_app(config_path=None):
|
||||
"""The application factory.
|
||||
|
||||
Creates an instance of :py:class:`Q3StatsApplication` and sets its config
|
||||
from file located at *config_path*."""
|
||||
app = Q3StatsApplication(__name__)
|
||||
|
||||
if config_path is None:
|
||||
config_path = get_config_path()
|
||||
|
||||
app.config.from_pyfile(config_path)
|
||||
app.init_db()
|
||||
|
||||
@app.context_processor
|
||||
def app_context_processor():
|
||||
return {
|
||||
'version': app_version
|
||||
}
|
||||
|
||||
@app.errorhandler(400)
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(500)
|
||||
def app_handle_error(error):
|
||||
error_code = 500
|
||||
if isinstance(error, HTTPException):
|
||||
error_code = error.code
|
||||
|
||||
return flask.render_template(
|
||||
'error.html', error_code=error_code
|
||||
), error_code
|
||||
|
||||
from .blueprints.frontend import frontend_blueprint
|
||||
app.register_blueprint(frontend_blueprint)
|
||||
|
||||
from .blueprints.api_v1 import api_v1_blueprint
|
||||
app.register_blueprint(api_v1_blueprint)
|
||||
|
||||
return app
|
||||
0
q3stats/web_app/blueprints/__init__.py
Normal file
0
q3stats/web_app/blueprints/__init__.py
Normal file
7
q3stats/web_app/blueprints/api_v1/__init__.py
Normal file
7
q3stats/web_app/blueprints/api_v1/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
api_v1_blueprint = Blueprint('api_v1', __name__, url_prefix='/api/v1')
|
||||
|
||||
from .views import *
|
||||
4
q3stats/web_app/blueprints/api_v1/views/__init__.py
Normal file
4
q3stats/web_app/blueprints/api_v1/views/__init__.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from . import charts
|
||||
from . import dashboard
|
||||
from . import players
|
||||
from . import sessions
|
||||
145
q3stats/web_app/blueprints/api_v1/views/charts.py
Normal file
145
q3stats/web_app/blueprints/api_v1/views/charts.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.web_app.blueprints.api_v1.views.charts
|
||||
==============================================
|
||||
|
||||
This module contains the APIv1 chart views.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
|
||||
from flask import abort, current_app, jsonify
|
||||
import sqlalchemy as sa
|
||||
|
||||
from q3stats.web_app.blueprints.api_v1 import api_v1_blueprint
|
||||
from q3stats.lib import charts, defs
|
||||
from q3stats.models import Score, Game
|
||||
from q3stats.lib import queries
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/charts/day/<day>', methods=['GET'])
|
||||
def get_api_v1_charts_day(day):
|
||||
"""Returns day chart data for *day*."""
|
||||
try:
|
||||
date = datetime.datetime.strptime(day, defs.DATE_FORMAT)
|
||||
except (TypeError, ValueError):
|
||||
abort(400)
|
||||
|
||||
maps, scores = charts.get_day_chart(date, current_app.db_session)
|
||||
return jsonify({
|
||||
'day': date.strftime(defs.DATE_FORMAT), 'maps': maps, 'scores': scores
|
||||
})
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/charts/player/<player>/game/<game_uuid>',
|
||||
methods=['GET'])
|
||||
def get_api_v1_charts_player_game(player, game_uuid):
|
||||
"""Returns player charts data for *player* and *game_uuid*."""
|
||||
score = current_app.db_session.query(Score).\
|
||||
join(Score.game).\
|
||||
filter(Game.uuid == game_uuid).\
|
||||
filter(Score.player == player).\
|
||||
first()
|
||||
|
||||
if not score:
|
||||
abort(404)
|
||||
|
||||
result = {
|
||||
'score': [
|
||||
['Frags', score.kills],
|
||||
['Deaths', score.deaths],
|
||||
['Suicides', score.suicides],
|
||||
],
|
||||
'damage': [
|
||||
['Taken', score.damage_taken],
|
||||
['Given', score.damage_given],
|
||||
],
|
||||
'totals': [
|
||||
['Armor', score.total_armor],
|
||||
['Health', score.total_health],
|
||||
]
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/charts/player/<player>/wins/session',
|
||||
methods=['GET'])
|
||||
def get_api_v1_charts_player_wins_session(player):
|
||||
"""Returns wins chart data aggregated by session for *player*."""
|
||||
categories, wins_serie, losses_serie = charts.get_player_wins_chart(
|
||||
current_app.db_session, player, agg_by='date'
|
||||
)
|
||||
|
||||
dates = [
|
||||
x.strftime(defs.DATE_FORMAT) for x in categories
|
||||
]
|
||||
|
||||
return jsonify({
|
||||
'dates': dates,
|
||||
'wins': wins_serie,
|
||||
'losses': losses_serie
|
||||
})
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/charts/player/<player>/wins/map',
|
||||
methods=['GET'])
|
||||
def get_api_v1_charts_player_wins_map(player):
|
||||
"""Returns wins chart data aggregated by map for *player*."""
|
||||
categories, wins_serie, losses_serie = charts.get_player_wins_chart(
|
||||
current_app.db_session, player, agg_by='map'
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'maps': categories,
|
||||
'wins': wins_serie,
|
||||
'losses': losses_serie
|
||||
})
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/charts/player/<player>/accuracy/session',
|
||||
methods=['GET'])
|
||||
def get_api_v1_charts_player_accuracy_session(player):
|
||||
"""Returns avg accuracy chart data aggregated by session for *player*."""
|
||||
categories, series = charts.get_player_avg_accuracy_chart(
|
||||
current_app.db_session, player, agg_by='date'
|
||||
)
|
||||
|
||||
dates = [
|
||||
x.strftime(defs.DATE_FORMAT) for x in categories
|
||||
]
|
||||
|
||||
return jsonify({'dates': dates, 'series': series})
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/charts/player/<player>/accuracy/map',
|
||||
methods=['GET'])
|
||||
def get_api_v1_charts_player_accuracy_map(player):
|
||||
"""Returns avg accuracy chart data aggregated by map for *player*."""
|
||||
categories, series = charts.get_player_avg_accuracy_chart(
|
||||
current_app.db_session, player, agg_by='map'
|
||||
)
|
||||
|
||||
return jsonify({'maps': categories, 'series': series})
|
||||
47
q3stats/web_app/blueprints/api_v1/views/dashboard.py
Normal file
47
q3stats/web_app/blueprints/api_v1/views/dashboard.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.web_app.blueprints.api_v1.views.dashboard
|
||||
=================================================
|
||||
|
||||
This module contains the APIv1 dashboard views.
|
||||
"""
|
||||
|
||||
from flask import abort, current_app, jsonify
|
||||
|
||||
from q3stats.web_app.blueprints.api_v1 import api_v1_blueprint
|
||||
from q3stats.lib import defs, queries
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/dashboard')
|
||||
def get_api_v1_dashboard():
|
||||
"""Returns data for the Dashboard view."""
|
||||
dates = queries.get_game_dates(current_app.db_session)
|
||||
fotm = queries.get_top_players(current_app.db_session)
|
||||
eotm = queries.get_top_players(current_app.db_session, agg_by='suicides')
|
||||
|
||||
return jsonify({
|
||||
'day': dates[0].strftime(defs.DATE_FORMAT),
|
||||
'fotm': fotm,
|
||||
'eotm': eotm
|
||||
})
|
||||
101
q3stats/web_app/blueprints/api_v1/views/players.py
Normal file
101
q3stats/web_app/blueprints/api_v1/views/players.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.web_app.blueprints.api_v1.views.players
|
||||
===============================================
|
||||
|
||||
This module contains the APIv1 players views.
|
||||
"""
|
||||
|
||||
from flask import abort, current_app, jsonify
|
||||
import sqlalchemy as sa
|
||||
import six
|
||||
|
||||
from q3stats.web_app.blueprints.api_v1 import api_v1_blueprint
|
||||
from q3stats.lib import queries, stats
|
||||
from q3stats.models import Game, Score
|
||||
|
||||
|
||||
def _process_weapons(weapons):
|
||||
"""Returns processed weapon stats."""
|
||||
result = {}
|
||||
|
||||
for weapon, weapon_stats in six.iteritems(weapons):
|
||||
result[weapon] = dict(weapon_stats)
|
||||
result[weapon]['accuracy'] = stats.weapon_accuracy(
|
||||
weapon, weapon_stats
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _process_powerups(powerups):
|
||||
"""Returns processed powerup stats."""
|
||||
result = {}
|
||||
|
||||
for powerup, powerup_stats in six.iteritems(powerups):
|
||||
result[powerup] = [
|
||||
powerup_stats[0], stats.powerup_time(powerup_stats[1])
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/players/')
|
||||
def get_api_v1_players():
|
||||
"""Returns list of known players."""
|
||||
players_query = sa.sql.expression.select([Score.__table__.c.player]).\
|
||||
distinct(Score.__table__.c.player).\
|
||||
order_by(Score.__table__.c.player.asc())
|
||||
|
||||
players = [x.player for x in current_app.db_session.execute(players_query)]
|
||||
|
||||
return jsonify({"players": players})
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/players/<player>/game/<game_uuid>')
|
||||
def get_api_v1_player_game(player, game_uuid):
|
||||
"""Returns data for the Player Game view."""
|
||||
game = current_app.db_session.query(Game).\
|
||||
filter_by(uuid=game_uuid).\
|
||||
first()
|
||||
|
||||
if not game:
|
||||
abort(404)
|
||||
|
||||
score = current_app.db_session.query(Score).\
|
||||
filter_by(game_id=game.id).\
|
||||
filter_by(player=player).\
|
||||
first()
|
||||
|
||||
if not score:
|
||||
abort(404)
|
||||
|
||||
result = {
|
||||
"map": game.map,
|
||||
"items": score.items,
|
||||
"weapons": _process_weapons(score.weapons),
|
||||
"powerups": _process_powerups(score.powerups)
|
||||
}
|
||||
|
||||
return jsonify(result)
|
||||
146
q3stats/web_app/blueprints/api_v1/views/sessions.py
Normal file
146
q3stats/web_app/blueprints/api_v1/views/sessions.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.web_app.blueprints.api_v1.views.sessions
|
||||
================================================
|
||||
|
||||
This module contains the APIv1 sessions views.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
import datetime
|
||||
|
||||
from flask import abort, current_app, jsonify
|
||||
import six
|
||||
|
||||
from q3stats.web_app.blueprints.api_v1 import api_v1_blueprint
|
||||
from q3stats.lib import defs, queries
|
||||
from q3stats.models import Game
|
||||
|
||||
|
||||
def _get_quad_freaks(games):
|
||||
"""Returns quad freaks data for *games*."""
|
||||
intermediate = defaultdict(lambda: defaultdict(int))
|
||||
|
||||
for game in games:
|
||||
for score in game.scores:
|
||||
if 'Quad' in score.powerups:
|
||||
intermediate[game.uuid][score.player] +=\
|
||||
score.powerups['Quad'][0]
|
||||
|
||||
result = {}
|
||||
for game_uuid, players in six.iteritems(intermediate):
|
||||
sorted_players = sorted(
|
||||
list(six.iteritems(players)), key=lambda x: x[1], reverse=True
|
||||
)
|
||||
|
||||
result[game_uuid] = sorted_players[0]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@api_v1_blueprint.route('/sessions/')
|
||||
@api_v1_blueprint.route('/sessions/<day>')
|
||||
def get_api_v1_sessions(day=None):
|
||||
"""Returns data for the Sessions view.
|
||||
|
||||
If *day* is omitted, the latest session is used."""
|
||||
date = None
|
||||
|
||||
if day is not None:
|
||||
try:
|
||||
date = datetime.datetime.strptime(day, defs.DATE_FORMAT).date()
|
||||
except (TypeError, ValueError):
|
||||
abort(400)
|
||||
|
||||
dates = queries.get_game_dates(current_app.db_session)
|
||||
if date is None:
|
||||
date = dates[0]
|
||||
elif date not in dates:
|
||||
abort(404)
|
||||
|
||||
current_date_idx = dates.index(date)
|
||||
|
||||
try:
|
||||
prev_date = dates[current_date_idx + 1].strftime(defs.DATE_FORMAT)
|
||||
except IndexError:
|
||||
prev_date = None
|
||||
|
||||
next_date = None
|
||||
if current_date_idx > 0:
|
||||
try:
|
||||
next_date = dates[current_date_idx - 1].strftime(defs.DATE_FORMAT)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
games = current_app.db_session.query(Game).filter_by(date=date).\
|
||||
order_by(Game.time)
|
||||
|
||||
quad_freaks = _get_quad_freaks(games)
|
||||
|
||||
result = {
|
||||
"day": date.strftime(defs.DATE_FORMAT),
|
||||
"previous_day": prev_date,
|
||||
"next_day": next_date,
|
||||
"games": []
|
||||
}
|
||||
|
||||
for game in games:
|
||||
game_data = {
|
||||
"uuid": game.uuid,
|
||||
"map": game.map,
|
||||
"scores": []
|
||||
}
|
||||
|
||||
game_quad_freaks = quad_freaks.get(game.uuid, [])
|
||||
|
||||
for score in game.scores:
|
||||
weapons = sorted(
|
||||
score.weapons.keys(), key=lambda x: score.weapons[x]['kills'],
|
||||
reverse=True
|
||||
)
|
||||
|
||||
try:
|
||||
favourite_weapon = weapons[0]
|
||||
except IndexError:
|
||||
favourite_weapon = None
|
||||
|
||||
is_quad_freak = score.player in game_quad_freaks
|
||||
quad_pickups = 0
|
||||
if is_quad_freak:
|
||||
quad_pickups = game_quad_freaks[1]
|
||||
|
||||
game_data['scores'].append({
|
||||
"player": score.player,
|
||||
"score": score.score,
|
||||
"kills": score.kills,
|
||||
"deaths": score.deaths,
|
||||
"suicides": score.suicides,
|
||||
"favourite_weapon": favourite_weapon,
|
||||
"quad_freak": is_quad_freak,
|
||||
"quad_pickups": quad_pickups
|
||||
})
|
||||
|
||||
result['games'].append(game_data)
|
||||
|
||||
return jsonify(result)
|
||||
7
q3stats/web_app/blueprints/frontend/__init__.py
Normal file
7
q3stats/web_app/blueprints/frontend/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from flask import Blueprint
|
||||
|
||||
frontend_blueprint = Blueprint('frontend', __name__, url_prefix=None)
|
||||
|
||||
from .views import *
|
||||
88
q3stats/web_app/blueprints/frontend/views.py
Normal file
88
q3stats/web_app/blueprints/frontend/views.py
Normal file
@@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2017 Tomek Wójcik <tomek@bthlabs.pl>
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
#
|
||||
|
||||
"""
|
||||
q3stats.web_app.blueprints.frontend.views
|
||||
=========================================
|
||||
|
||||
This module contains the frontend views.
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
from flask import current_app, abort, redirect, render_template, request,\
|
||||
url_for
|
||||
|
||||
from q3stats.lib.scripts.import_games import script_main as importer
|
||||
from q3stats.web_app.blueprints.frontend import frontend_blueprint
|
||||
|
||||
|
||||
@frontend_blueprint.route('/', methods=['GET'])
|
||||
def get_frontend_index():
|
||||
"""Renders the application."""
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
@frontend_blueprint.route('/sessions/', methods=['GET'])
|
||||
@frontend_blueprint.route('/sessions/<day>', methods=['GET'])
|
||||
def get_frontend_sessions(day=None):
|
||||
"""Redirects UA to Sessions view."""
|
||||
return redirect("/#/sessions/%s" % (day or "",))
|
||||
|
||||
|
||||
@frontend_blueprint.route(
|
||||
'/players/<player>/game/<game_uuid>', methods=['GET']
|
||||
)
|
||||
def get_frontend_player_game(player, game_uuid):
|
||||
"""Redirects UA to Player Game view."""
|
||||
return redirect("/#/players/%s/game/%s" % (player, game_uuid))
|
||||
|
||||
|
||||
@frontend_blueprint.route('/players/', methods=['GET'])
|
||||
@frontend_blueprint.route('/players/<player>', methods=['GET'])
|
||||
def get_frontend_players(player=None):
|
||||
"""Redirects UA to Players view."""
|
||||
return redirect("/#/players/%s" % (player or "",))
|
||||
|
||||
|
||||
@frontend_blueprint.route('/receivestats', methods=['POST'])
|
||||
def post_frontend_receivestats():
|
||||
"""Receives a tarball with stats, extracts it and imports the games."""
|
||||
if 'stats' not in request.files:
|
||||
abort(400)
|
||||
|
||||
pack = request.files['stats']
|
||||
|
||||
work_dir = tempfile.mkdtemp()
|
||||
|
||||
try:
|
||||
with tarfile.open('stats.tar', 'r', pack.stream) as pack_f:
|
||||
pack_f.extractall(work_dir)
|
||||
|
||||
importer([work_dir], current_app.config) # h4x0r
|
||||
finally:
|
||||
shutil.rmtree(work_dir)
|
||||
|
||||
return 'OK', 200
|
||||
0
q3stats/web_app/templates/.placeholder
Normal file
0
q3stats/web_app/templates/.placeholder
Normal file
Reference in New Issue
Block a user