mv periodic back to cron, add winrate graphs
This commit is contained in:
parent
5d81c4fbe4
commit
73f7f9e567
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@ venv/
|
|||||||
*.db
|
*.db
|
||||||
*.swp
|
*.swp
|
||||||
clientlist.txt
|
clientlist.txt
|
||||||
|
*.wsgi
|
||||||
|
24
app.py
24
app.py
@ -1,24 +0,0 @@
|
|||||||
#!venv/bin/python
|
|
||||||
from flask import Flask
|
|
||||||
from flask.ext.script import Manager, Server
|
|
||||||
from flask.ext.migrate import Migrate, MigrateCommand
|
|
||||||
|
|
||||||
from app import *
|
|
||||||
|
|
||||||
SQLALCHEMY_DATABASE_URI = 'mysql://root:$perwePP@localhost/dotanoobs'
|
|
||||||
|
|
||||||
migrate = Migrate(app, db)
|
|
||||||
manager = Manager(app)
|
|
||||||
manager.add_command('db', MigrateCommand)
|
|
||||||
|
|
||||||
|
|
||||||
@manager.command
|
|
||||||
def admin(name):
|
|
||||||
u = models.User.query.filter_by(nickname=name).first()
|
|
||||||
if u and not u.admin:
|
|
||||||
u.admin = True
|
|
||||||
db.session.commit()
|
|
||||||
print "User {} has been granted admin access.".format(name)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
manager.run()
|
|
@ -10,29 +10,4 @@ db = SQLAlchemy(app)
|
|||||||
oid = OpenID(app)
|
oid = OpenID(app)
|
||||||
cache = Cache(app, config={'CACHE_TYPE': app.config['CACHE_TYPE']})
|
cache = Cache(app, config={'CACHE_TYPE': app.config['CACHE_TYPE']})
|
||||||
|
|
||||||
import ts3
|
from app import views
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
|
||||||
from teamspeak import idle_mover, store_active_data, \
|
|
||||||
process_ts3_events, award_idle_ts3_points
|
|
||||||
|
|
||||||
def set_voice_server():
|
|
||||||
ts3Server = ts3.TS3Server(app.config['TS3_HOST'], app.config['TS3_PORT'])
|
|
||||||
ts3Server.login(app.config['TS3_USERNAME'], app.config['TS3_PASSWORD'])
|
|
||||||
ts3Server.use(1)
|
|
||||||
return ts3Server
|
|
||||||
|
|
||||||
voice = set_voice_server()
|
|
||||||
|
|
||||||
def refresh_voice_server():
|
|
||||||
app.logger.info("Refreshing TS3 connection...")
|
|
||||||
voice = set_voice_server()
|
|
||||||
|
|
||||||
scheduler = BackgroundScheduler(logger=app.logger)
|
|
||||||
scheduler.add_job(refresh_voice_server, 'interval', hours=6)
|
|
||||||
scheduler.add_job(idle_mover, 'interval', [voice], minutes=30)
|
|
||||||
scheduler.add_job(store_active_data, 'interval', [voice], minutes=30)
|
|
||||||
scheduler.add_job(award_idle_ts3_points, 'interval', [voice], minutes=30)
|
|
||||||
scheduler.add_job(process_ts3_events, 'interval', [voice], hours=1)
|
|
||||||
scheduler.start()
|
|
||||||
|
|
||||||
from app import views, models
|
|
||||||
|
76
app/analytics.py
Normal file
76
app/analytics.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import requests
|
||||||
|
from time import sleep, mktime
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app import app, db, models
|
||||||
|
|
||||||
|
MODES_TO_SKIP = ['Ability Draft', 'Greeviling', 'Diretide']
|
||||||
|
|
||||||
|
def collect_match_results(dotabuff_id, num_matches):
|
||||||
|
results = []
|
||||||
|
page = 0
|
||||||
|
while True:
|
||||||
|
page += 1
|
||||||
|
url = "http://dotabuff.com/players/{}/matches/?page={}".format(dotabuff_id, page)
|
||||||
|
data = requests.get(url).text
|
||||||
|
soup = BeautifulSoup(data).article.table.tbody
|
||||||
|
# Catch last page
|
||||||
|
if 'sorry' in soup.tr.td.text.lower():
|
||||||
|
break
|
||||||
|
# Parse the matches on current page
|
||||||
|
for row in soup.find_all('tr'):
|
||||||
|
# Pass over bot matches and other 'inactive' games
|
||||||
|
if 'inactive' in row.get('class', ''): continue
|
||||||
|
cells = row.find_all('td')
|
||||||
|
result_cell = cells[2]
|
||||||
|
match_cell = cells[3]
|
||||||
|
match_id = int(result_cell.a['href'].split('/')[-1])
|
||||||
|
match_type = match_cell.div.text
|
||||||
|
if match_type in MODES_TO_SKIP: continue
|
||||||
|
result = True if 'won' in result_cell.a['class'] else False
|
||||||
|
dt = datetime.strptime(result_cell.time['datetime'], '%Y-%m-%dT%H:%M:%S+00:00')
|
||||||
|
results.append({'match_id':match_id, 'win':result, 'datetime':dt, 'game_mode':match_type})
|
||||||
|
if len(results) > num_matches:
|
||||||
|
break
|
||||||
|
if len(results) > num_matches:
|
||||||
|
break
|
||||||
|
sleep(60)
|
||||||
|
results.reverse()
|
||||||
|
return results
|
||||||
|
|
||||||
|
def apply_window(results, window_size=50):
|
||||||
|
windows = []
|
||||||
|
# Compute the initial window
|
||||||
|
win_rate = 0.00
|
||||||
|
for idx in range(0, window_size):
|
||||||
|
win_rate += 1 if results[idx]['win'] else 0
|
||||||
|
win_rate /= window_size
|
||||||
|
windows.append(win_rate)
|
||||||
|
# From here on, modify based on leave/enter data points
|
||||||
|
fractional_change = 1. / window_size
|
||||||
|
for idx in range(window_size, len(results)):
|
||||||
|
if results[idx-window_size]['win'] == results[idx]['win']:
|
||||||
|
pass
|
||||||
|
elif results[idx]['win']:
|
||||||
|
win_rate += fractional_change
|
||||||
|
else:
|
||||||
|
win_rate -= fractional_change
|
||||||
|
windows.append(win_rate)
|
||||||
|
return windows
|
||||||
|
|
||||||
|
def calculate_winrates():
|
||||||
|
users_analyzed = 0
|
||||||
|
for user in models.User.query.all():
|
||||||
|
db_id = requests.get("http://dotabuff.com/search?q="+user.steam_id).url.split("/")[-1]
|
||||||
|
result = collect_match_results(db_id, app.config['ANALYTICS_WINRATE_NUM_MATCHES'])
|
||||||
|
windowed = apply_window(result, app.config['ANALYTICS_WINRATE_WINDOW'])
|
||||||
|
date_nums = map(lambda x: mktime(x['datetime'].timetuple()),\
|
||||||
|
result[app.config['ANALYTICS_WINRATE_WINDOW']-1:])
|
||||||
|
winrate = {'total_games': len(result), 'data': zip(date_nums, windowed) }
|
||||||
|
user.winrate_data = winrate
|
||||||
|
db.session.commit()
|
||||||
|
users_analyzed += 1
|
||||||
|
sleep(60)
|
||||||
|
app.logger.info("Calculated win rate numbers for {} doobs.".format(users_analyzed))
|
||||||
|
return users_analyzed
|
@ -81,11 +81,14 @@ class User(db.Model):
|
|||||||
points_from_events = db.Column(db.Integer)
|
points_from_events = db.Column(db.Integer)
|
||||||
points_from_ts3 = db.Column(db.Integer)
|
points_from_ts3 = db.Column(db.Integer)
|
||||||
points_from_forum = db.Column(db.Integer)
|
points_from_forum = db.Column(db.Integer)
|
||||||
|
|
||||||
ts3_starttime = db.Column(db.DateTime)
|
ts3_starttime = db.Column(db.DateTime)
|
||||||
ts3_endtime = db.Column(db.DateTime)
|
ts3_endtime = db.Column(db.DateTime)
|
||||||
ts3_rewardtime = db.Column(db.DateTime)
|
ts3_rewardtime = db.Column(db.DateTime)
|
||||||
ts3_connections = db.Column(MutableDict.as_mutable(Json))
|
ts3_connections = db.Column(MutableDict.as_mutable(Json))
|
||||||
|
|
||||||
last_post_reward = db.Column(db.Integer)
|
last_post_reward = db.Column(db.Integer)
|
||||||
|
winrate_data = db.Column(MutableDict.as_mutable(Json))
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -96,6 +99,7 @@ class User(db.Model):
|
|||||||
self.steam_id = steam_id
|
self.steam_id = steam_id
|
||||||
self.random_heroes = {'current':None, 'completed':[]}
|
self.random_heroes = {'current':None, 'completed':[]}
|
||||||
self.az_completions = 0
|
self.az_completions = 0
|
||||||
|
self.ts3_connections = {'list':[]}
|
||||||
self.created = datetime.utcnow()
|
self.created = datetime.utcnow()
|
||||||
self.last_seen = datetime.utcnow()
|
self.last_seen = datetime.utcnow()
|
||||||
self.bio_text = None
|
self.bio_text = None
|
||||||
@ -161,7 +165,7 @@ class User(db.Model):
|
|||||||
db.session.commit();
|
db.session.commit();
|
||||||
|
|
||||||
def finalize_connection(self):
|
def finalize_connection(self):
|
||||||
self.ts3_connections.append({'starttime': self.ts3_starttime, 'endtime': self.ts3_endtime})
|
self.ts3_connections['list'].append({'starttime': self.ts3_starttime, 'endtime': self.ts3_endtime})
|
||||||
self.ts3_startime = None
|
self.ts3_startime = None
|
||||||
self.ts3_endtime = None
|
self.ts3_endtime = None
|
||||||
db.session.commit();
|
db.session.commit();
|
||||||
|
@ -95,6 +95,15 @@ footer {
|
|||||||
float:right;
|
float:right;
|
||||||
}
|
}
|
||||||
|
|
||||||
#profile_links > a {
|
#winrate_graph {
|
||||||
font-size:14px;
|
margin: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipemenu > li {
|
||||||
|
display: inline-block;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pipemenu > li + li::before {
|
||||||
|
content: " | ";
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,12 @@ from datetime import datetime, timedelta
|
|||||||
from xml.etree import ElementTree
|
from xml.etree import ElementTree
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
import models
|
||||||
from app import app, db
|
from app import app, db
|
||||||
|
|
||||||
def getTeamspeakWindow(window=timedelta(weeks=1)):
|
def getTeamspeakWindow(window=timedelta(weeks=1)):
|
||||||
current_time = datetime.utcnow()
|
current_time = datetime.utcnow()
|
||||||
from models import TeamspeakData
|
return models.TeamspeakData.query.filter(models.TeamspeakData.time < current_time, models.TeamspeakData.time > current_time-window).order_by(models.TeamspeakData.time).all()
|
||||||
return TeamspeakData.query.filter(TeamspeakData.time < current_time, TeamspeakData.time > current_time-window).order_by(TeamspeakData.time).all()
|
|
||||||
|
|
||||||
def registerUserTeamspeakId(user, tsid):
|
def registerUserTeamspeakId(user, tsid):
|
||||||
server = ts3.TS3Server(app.config['TS3_HOST'], app.config['TS3_PORT'])
|
server = ts3.TS3Server(app.config['TS3_HOST'], app.config['TS3_PORT'])
|
||||||
@ -173,7 +173,7 @@ def create_teamspeak_viewer():
|
|||||||
return "error: %s" % inst
|
return "error: %s" % inst
|
||||||
|
|
||||||
def get_ISO3166_mapping():
|
def get_ISO3166_mapping():
|
||||||
with open('app/static/country_codes.xml', mode='r') as d:
|
with open(path.join(path.dirname(__file__), 'static/country_codes.xml'), mode='r') as d:
|
||||||
data = d.read()
|
data = d.read()
|
||||||
xml = ElementTree.fromstring(data)
|
xml = ElementTree.fromstring(data)
|
||||||
d = dict()
|
d = dict()
|
||||||
@ -187,10 +187,10 @@ ISO3166_MAPPING = get_ISO3166_mapping()
|
|||||||
# Scheduled functions for TeamspeakServer
|
# Scheduled functions for TeamspeakServer
|
||||||
#
|
#
|
||||||
|
|
||||||
def idle_mover(voice):
|
def idle_mover(server):
|
||||||
""" Checks connected clients idle_time, moving to AFK if over TS3_MAX_IDLETIME. """
|
""" Checks connected clients idle_time, moving to AFK if over TS3_MAX_IDLETIME. """
|
||||||
|
|
||||||
app.logger.info("Running TS3 AFK mover...")
|
app.logger.debug("Running TS3 AFK mover...")
|
||||||
exempt_cids = []
|
exempt_cids = []
|
||||||
permid_response = server.send_command('permidgetbyname', keys={'permsid': 'i_channel_needed_join_power'})
|
permid_response = server.send_command('permidgetbyname', keys={'permsid': 'i_channel_needed_join_power'})
|
||||||
if permid_response.is_successful:
|
if permid_response.is_successful:
|
||||||
@ -220,10 +220,8 @@ def idle_mover(voice):
|
|||||||
clientlist = server.send_command('clientlist', opts=['times']).data
|
clientlist = server.send_command('clientlist', opts=['times']).data
|
||||||
for client in clientlist:
|
for client in clientlist:
|
||||||
clientinfo = server.send_command('clientinfo', {'clid':client['clid']})
|
clientinfo = server.send_command('clientinfo', {'clid':client['clid']})
|
||||||
if clientinfo.is_successful:
|
#if clientinfo.is_successful:
|
||||||
client['client_unique_identifier'] = clientinfo.data[0]['client_unique_identifier']
|
#client['client_unique_identifier'] = clientinfo.data[0]['client_unique_identifier']
|
||||||
else:
|
|
||||||
raise UserWarning('Could not find the clientinfo for %s' % client['clid'])
|
|
||||||
|
|
||||||
# move idlers to afk channel
|
# move idlers to afk channel
|
||||||
for client in clientlist:
|
for client in clientlist:
|
||||||
@ -232,10 +230,10 @@ def idle_mover(voice):
|
|||||||
# Have TeamSpeak move AFK user to appropriate channel
|
# Have TeamSpeak move AFK user to appropriate channel
|
||||||
server.send_command('clientmove', keys={'clid': client['clid'], 'cid': afk_channel['cid']})
|
server.send_command('clientmove', keys={'clid': client['clid'], 'cid': afk_channel['cid']})
|
||||||
|
|
||||||
def store_active_data(voice):
|
def store_active_data(server):
|
||||||
""" Take a snapshot of Teamspeak (clients, countries, etc) to feed the ts3_stats page """
|
""" Take a snapshot of Teamspeak (clients, countries, etc) to feed the ts3_stats page """
|
||||||
|
|
||||||
app.logger.info("Taking Teamspeak snapshot...")
|
app.logger.debug("Taking Teamspeak snapshot...")
|
||||||
# Get exempt channels (AFK, passworded, join powers)
|
# Get exempt channels (AFK, passworded, join powers)
|
||||||
exempt_cids = []
|
exempt_cids = []
|
||||||
permid_response = server.send_command('permidgetbyname', keys={'permsid': 'i_channel_needed_join_power'})
|
permid_response = server.send_command('permidgetbyname', keys={'permsid': 'i_channel_needed_join_power'})
|
||||||
@ -276,18 +274,16 @@ def store_active_data(voice):
|
|||||||
db.session.add(tsdata)
|
db.session.add(tsdata)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
def process_ts3_events(voice):
|
def process_ts3_events(server):
|
||||||
""" Create Teamspeak channels for upcoming events, delete empty event channels that have expired """
|
""" Create Teamspeak channels for upcoming events, delete empty event channels that have expired """
|
||||||
|
|
||||||
app.logger.info("Processing Teamspeak events...")
|
app.logger.debug("Processing Teamspeak events...")
|
||||||
# Get list of clients
|
# Get list of clients
|
||||||
clientlist = server.clientlist()
|
clientlist = server.clientlist()
|
||||||
for clid, client in clientlist.iteritems():
|
for clid, client in clientlist.iteritems():
|
||||||
clientinfo = server.send_command('clientinfo', {'clid':clid})
|
clientinfo = server.send_command('clientinfo', {'clid':clid})
|
||||||
if clientinfo.is_successful:
|
if clientinfo.is_successful:
|
||||||
client['client_unique_identifier'] = clientinfo.data[0]['client_unique_identifier']
|
client['client_unique_identifier'] = clientinfo.data[0]['client_unique_identifier']
|
||||||
else:
|
|
||||||
raise UserWarning('Could not find clientinfo for %s' % clid)
|
|
||||||
|
|
||||||
# Process any active events
|
# Process any active events
|
||||||
for clid, client in clientlist.iteritems():
|
for clid, client in clientlist.iteritems():
|
||||||
@ -327,10 +323,10 @@ def process_ts3_events(voice):
|
|||||||
server.clientpoke(client['clid'], message)
|
server.clientpoke(client['clid'], message)
|
||||||
|
|
||||||
|
|
||||||
def award_idle_ts3_points(voice):
|
def award_idle_ts3_points(server):
|
||||||
""" Award points for active time spent in the Teamspeak server. """
|
""" Award points for active time spent in the Teamspeak server. """
|
||||||
|
|
||||||
app.logger.info("Awarding Teamspeak idle points")
|
app.logger.debug("Awarding Teamspeak idle points")
|
||||||
# Get exempt channels (AFK, passwords, join power)
|
# Get exempt channels (AFK, passwords, join power)
|
||||||
exempt_cids = []
|
exempt_cids = []
|
||||||
permid_response = server.send_command('permidgetbyname', keys={'permsid': 'i_channel_needed_join_power'})
|
permid_response = server.send_command('permidgetbyname', keys={'permsid': 'i_channel_needed_join_power'})
|
||||||
@ -358,14 +354,15 @@ def award_idle_ts3_points(voice):
|
|||||||
clientinfo = server.send_command('clientinfo', {'clid': clid})
|
clientinfo = server.send_command('clientinfo', {'clid': clid})
|
||||||
if clientinfo.is_successful:
|
if clientinfo.is_successful:
|
||||||
client['client_unique_identifier'] = clientinfo.data[0]['client_unique_identifier']
|
client['client_unique_identifier'] = clientinfo.data[0]['client_unique_identifier']
|
||||||
else:
|
|
||||||
raise UserWarning('Could not find the clientinfo for %s' % clid)
|
|
||||||
|
|
||||||
# Update the data
|
# Update the data
|
||||||
active_users = set()
|
active_users = set()
|
||||||
for client in clientlist.values():
|
for client in clientlist.values():
|
||||||
if client['cid'] not in exempt_cids:
|
if client['cid'] not in exempt_cids:
|
||||||
doob = models.User.query.filter_by(teamspeak_id=client['client_unique_identifier']).first()
|
try:
|
||||||
|
doob = models.User.query.filter_by(teamspeak_id=client['client_unique_identifier']).first()
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
if doob:
|
if doob:
|
||||||
doob.update_connection()
|
doob.update_connection()
|
||||||
active_users.add(doob)
|
active_users.add(doob)
|
||||||
|
@ -64,6 +64,16 @@
|
|||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/uikit/2.4.0/addons/timepicker/timepicker.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/uikit/2.4.0/addons/timepicker/timepicker.min.js"></script>
|
||||||
<link rel=stylesheet href="//cdnjs.cloudflare.com/ajax/libs/uikit/2.4.0/addons/datepicker/datepicker.gradient.css"></link>
|
<link rel=stylesheet href="//cdnjs.cloudflare.com/ajax/libs/uikit/2.4.0/addons/datepicker/datepicker.gradient.css"></link>
|
||||||
<script>
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
var s = $("#start_time").val().split(" ");
|
||||||
|
var e = $("#end_time").val().split(" ");
|
||||||
|
|
||||||
|
$("#start_d").val(s[0]);
|
||||||
|
$("#start_t").val(s[1]);
|
||||||
|
$("#end_d").val(e[0]);
|
||||||
|
$("#end_t").val(e[1]);
|
||||||
|
});
|
||||||
|
|
||||||
$(".uk-form").submit(function(event) {
|
$(".uk-form").submit(function(event) {
|
||||||
var s = moment($("#start_d").val() + ' ' + $("#start_t").val(), "DD.MM.YYYY HH:mm");
|
var s = moment($("#start_d").val() + ' ' + $("#start_t").val(), "DD.MM.YYYY HH:mm");
|
||||||
var e = moment($("#end_d").val() + ' ' + $("#end_t").val(), "DD.MM.YYYY HH:mm");
|
var e = moment($("#end_d").val() + ' ' + $("#end_t").val(), "DD.MM.YYYY HH:mm");
|
||||||
|
@ -10,19 +10,21 @@
|
|||||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
|
||||||
<!-- CSS includes -->
|
<!-- CSS includes -->
|
||||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/uikit/2.4.0/css/uikit.gradient.min.css" />
|
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/uikit/2.8.0/css/uikit.gradient.min.css" />
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}" />
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}" />
|
||||||
|
|
||||||
<!-- Javascript includes -->
|
<!-- Javascript includes -->
|
||||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/uikit/2.4.0/js/uikit.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/uikit/2.8.0/js/uikit.min.js"></script>
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.5.1/moment.min.js"></script>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Navigation bar -->
|
<!-- Navigation bar -->
|
||||||
<nav class="uk-navbar uk-navbar-attached" data-uk-navbar>
|
<nav class="uk-navbar uk-navbar-attached" data-uk-navbar>
|
||||||
<a href="#offcanvas" class="uk-navbar-brand uk-hidden-large" data-uk-offcanvas><img src="{{ url_for('static', filename='img/navlogo.png') }}" /><i class="uk-icon-double-angle-down"></i></a>
|
<a href="#offcanvas" class="uk-navbar-brand uk-hidden-large" data-uk-offcanvas>
|
||||||
|
<img src="{{ url_for('static', filename='img/navlogo.png') }}" class="uk-visible-large" /><i class="uk-icon-navicon uk-hidden-large"></i>
|
||||||
|
</a>
|
||||||
<a href="" class="uk-navbar-brand uk-visible-large" data-uk-offcanvas><img src="{{ url_for('static', filename='img/navlogo.png') }}" /></a>
|
<a href="" class="uk-navbar-brand uk-visible-large" data-uk-offcanvas><img src="{{ url_for('static', filename='img/navlogo.png') }}" /></a>
|
||||||
<!-- Check if user is logged in -->
|
<!-- Check if user is logged in -->
|
||||||
{% if g.user %}
|
{% if g.user %}
|
||||||
|
@ -7,14 +7,16 @@
|
|||||||
<div class="uk-width-2-3">
|
<div class="uk-width-2-3">
|
||||||
<h2 class="uk-float-left"><img class="" src="{{ user.avatar }}" /> {{ user.nickname }}</h2>
|
<h2 class="uk-float-left"><img class="" src="{{ user.avatar }}" /> {{ user.nickname }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="profile_links" class="uk-width-1-3 uk-text-center">
|
<div id="profile_links" class="uk-width-1-3 uk-hidden-small uk-text-center">
|
||||||
<div class="uk-panel">
|
{% if user.public %}
|
||||||
{% if user.public %}
|
<div class="uk-button-group">
|
||||||
<a href="http://steamcommunity.com/profiles/{{ user.steam_id | safe }}">Steam</a> |
|
<a class="uk-button" href="http://steamcommunity.com/profiles/{{ user.steam_id | safe }}">Steam</a>
|
||||||
<a href="http://board.dotanoobs.com/?page=profile&id={{ user.id | safe }}">Forum Profile</a> |
|
{% if user.forum_id %}
|
||||||
<a href="http://dotabuff.com/search?q={{ user.steam_id }}">Dotabuff</a>
|
<a class="uk-button" href="http://board.dotanoobs.com/?page=profile&id={{ user.forum_id | safe }}">Forum Profile</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a class="uk-button" href="http://dotabuff.com/search?q={{ user.steam_id }}">Dotabuff</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<!--Main content area -->
|
<!--Main content area -->
|
||||||
<div class="uk-width-large-2-3 uk-width-medium-1-1 uk-panel">
|
<div class="uk-width-large-2-3 uk-width-medium-1-1 uk-panel">
|
||||||
@ -29,24 +31,104 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if user.id == g.user.id %} <a href="{{ url_for('user_settings')}}"><i class="uk-icon-edit"></i></a>{% endif %}
|
{% if user.id == g.user.id %} <a href="{{ url_for('user_settings')}}"><i class="uk-icon-edit"></i></a>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="profile_links" class="uk-width-1-3 uk-visible-small uk-text-center">
|
||||||
|
{% if user.public %}
|
||||||
|
<div class="uk-button-group">
|
||||||
|
<a class="uk-button" href="http://steamcommunity.com/profiles/{{ user.steam_id | safe }}">Steam</a>
|
||||||
|
{% if user.forum_id %}
|
||||||
|
<a class="uk-button" href="http://board.dotanoobs.com/?page=profile&id={{ user.forum_id | safe }}">Forum Profile</a>
|
||||||
|
{% endif %}
|
||||||
|
<a class="uk-button" href="http://dotabuff.com/search?q={{ user.steam_id }}">Dotabuff</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<!-- Side bar -->
|
<!-- Side bar -->
|
||||||
<div class="uk-width-large-1-3 uk-width-medium-1-1 uk-panel uk-panel-box uk-panel-box-secondary">
|
<div class="uk-width-large-1-3 uk-width-medium-1-1 uk-panel">
|
||||||
{% if user.public %}
|
{% if user.public %}
|
||||||
<div class="uk-container-center uk-text-center">
|
<div class="uk-container-center uk-text-center">
|
||||||
<span class="uk-text-bold">Current Hero</span><br/>
|
<span class="uk-text-bold">Current Hero</span><br/>
|
||||||
<span>{{ user.random_hero['localized_name'] }}</span><br/>
|
<span class="uk-text-success uk-text-bold">
|
||||||
|
{{ user.random_hero['localized_name'] }}
|
||||||
|
({{ user.random_heroes.completed | length + 1 }}
|
||||||
|
/
|
||||||
|
{{ total_hero_pool() - user.random_heroes.completed|length }})
|
||||||
|
</span><br/>
|
||||||
<a href={{ url_for('user_random_hero', userid=user.id) }}>
|
<a href={{ url_for('user_random_hero', userid=user.id) }}>
|
||||||
<img src="{{ url_for('static', filename=hero_image_large(user.random_hero)) }}" class="dn-hero-icon" /><br/>
|
<img src="{{ url_for('static', filename=hero_image_large(user.random_hero)) }}" class="dn-hero-icon" /><br/>
|
||||||
<span>View A-Z Progress</span>
|
<span>View A-Z Progress</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<ul class="uk-list uk-list-space uk-list-striped uk-text-center uk-text-small">
|
<table class="uk-table uk-table-hover uk-table-condensed">
|
||||||
<li>Completed <span id='rands'>{{ user.random_heroes.completed | length }}</span> heroes in A-Z</li>
|
<caption>{{ user.nickname }}</caption>
|
||||||
<li>Has <span id='points_total'>0</span> doobs points</li>
|
<tbody class="uk-text-small">
|
||||||
<li>Last seen at <span class='date'> {{ user.last_seen | js_datetime }}</span></li>
|
<tr>
|
||||||
<li>Doob since <span class='date'> {{ user.created | js_datetime }}</span></li>
|
<td class="uk-width-4-10">TS Points</td>
|
||||||
</ul>
|
<td class="uk-width-6-10 uk-text-right">0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Events Points</td>
|
||||||
|
<td class="uk-text-right">0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Forum Points</td>
|
||||||
|
<td class="uk-text-right">0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Last Seen</td>
|
||||||
|
<td class="date uk-text-right">{{ user.last_seen | js_datetime }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Member Since</td>
|
||||||
|
<td class="date uk-text-right">{{ user.created | js_datetime }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button class="uk-button uk-button-success uk-align-center" data-uk-modal="{target: '#winrate_modal'}">View Winrate</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div id="winrate_modal" class="uk-modal">
|
||||||
|
<div class="uk-modal-dialog uk-modal-dialog-frameless uk-modal-dialog-large">
|
||||||
|
<a class="uk-modal-close uk-close uk-close-alt"></a>
|
||||||
|
</div>
|
||||||
|
<div id="winrate_graph" class="uk-overflow-container"></div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pagescripts %}
|
||||||
|
<script src="http://code.highcharts.com/highcharts.js"></script>
|
||||||
|
{% cache 60*700 %}
|
||||||
|
<script>
|
||||||
|
$('#winrate_modal').on({
|
||||||
|
'uk.modal.show': function(){
|
||||||
|
Highcharts.charts[0].reflow();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
$(function () {
|
||||||
|
$('#winrate_graph').highcharts({
|
||||||
|
chart: { reflow: true },
|
||||||
|
title: { text: "Win rate for {{ user.nickname }}" },
|
||||||
|
subtitle: { text: "Over last {{ user.winrate_data['total_games'] }} games" },
|
||||||
|
xAxis: {type: 'datetime', dateTimeLabelFormats:{
|
||||||
|
month:'%m'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yAxis: {min: 0.35, max: 0.90, plotLines: [{value:0, width:2, color:'#808080'}]},
|
||||||
|
legend: {enabled: false},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '{{ user.nickname }}',
|
||||||
|
data: [
|
||||||
|
{% for date_nums, windowed in user.winrate_data['data'] %}
|
||||||
|
[({{ date_nums }} * 1000), parseFloat({{ windowed }}.toFixed(3))],
|
||||||
|
{% endfor %}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endcache %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<a href="#">Stream Stats</a>
|
<a href="#">Stream Stats</a>
|
||||||
<ul class="uk-nav-sub">
|
<ul class="uk-nav-sub">
|
||||||
<li><a href="http://potatr.dotanoobs.com">Potato_Bot</a></li>
|
<li><a href="http://potatr.dotanoobs.com">Potato_Bot</a></li>
|
||||||
<li><a href="http://potatr.dotanoobs.com">Cider_Bot</a></li>
|
<li><a href="http://cidr.dotanoobs.com">Cider_Bot</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li class="uk-nav-divider"></li>
|
<li class="uk-nav-divider"></li>
|
||||||
|
@ -36,12 +36,12 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block pagescripts %}
|
{% block pagescripts %}
|
||||||
{% cache 60*5 %}
|
|
||||||
{% set teamspeak_data = get_teamspeak_window() %}
|
{% set teamspeak_data = get_teamspeak_window() %}
|
||||||
<script src="//cdnjs.cloudflare.com/ajax/libs/highcharts/3.0.7/highcharts.js"></script>
|
<script src="//cdnjs.cloudflare.com/ajax/libs/highcharts/3.0.7/highcharts.js"></script>
|
||||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/highcharts/3.0.7/modules/map.src.js"></script>
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/highcharts/3.0.7/modules/map.src.js"></script>
|
||||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/highcharts/3.0.7/modules/data.src.js"></script>
|
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/highcharts/3.0.7/modules/data.src.js"></script>
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/world-map-shapes.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/world-map-shapes.js') }}"></script>
|
||||||
|
{% cache 60*5 %}
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('#unique_clients').html("{{ ts3_active_clients(teamspeak_data) }}");
|
$('#unique_clients').html("{{ ts3_active_clients(teamspeak_data) }}");
|
||||||
|
26
migrations/versions/1c90e0fd276a_.py
Normal file
26
migrations/versions/1c90e0fd276a_.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 1c90e0fd276a
|
||||||
|
Revises: a6f7dd522b7
|
||||||
|
Create Date: 2014-06-24 19:01:39.358682
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '1c90e0fd276a'
|
||||||
|
down_revision = 'a6f7dd522b7'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('user', sa.Column('winrate_data', sa.Json(), nullable=True))
|
||||||
|
### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('user', 'winrate_data')
|
||||||
|
### end Alembic commands ###
|
102
run.py
Executable file
102
run.py
Executable file
@ -0,0 +1,102 @@
|
|||||||
|
#!venv/bin/python
|
||||||
|
from flask import Flask
|
||||||
|
from flask.ext.script import Manager, Server
|
||||||
|
from flask.ext.migrate import Migrate, MigrateCommand
|
||||||
|
|
||||||
|
from app import app, db, models
|
||||||
|
|
||||||
|
#SQLALCHEMY_DATABASE_URI = 'mysql://root:$perwePP@localhost/dotanoobs'
|
||||||
|
|
||||||
|
migrate = Migrate(app, db)
|
||||||
|
manager = Manager(app)
|
||||||
|
manager.add_command('db', MigrateCommand)
|
||||||
|
|
||||||
|
def createTeamspeakInstance():
|
||||||
|
import ts3
|
||||||
|
s = ts3.TS3Server(app.config['TS3_HOST'], app.config['TS3_PORT'])
|
||||||
|
s.login(app.config['TS3_USERNAME'], app.config['TS3_PASSWORD'])
|
||||||
|
s.use(1)
|
||||||
|
return s
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def install_cronjobs():
|
||||||
|
from os import path
|
||||||
|
from crontab import CronTab
|
||||||
|
cron = CronTab(user=True)
|
||||||
|
|
||||||
|
# Clear out existing jobs
|
||||||
|
cron.remove_all(comment='DOOBSAUTO')
|
||||||
|
|
||||||
|
def make_job(job):
|
||||||
|
p = path.realpath(__file__)
|
||||||
|
c = cron.new(command='{}/venv/bin/python {} {}'.format(path.split(p)[0],\
|
||||||
|
p, job), comment='DOOBSAUTO')
|
||||||
|
return c
|
||||||
|
|
||||||
|
# Create the jobs
|
||||||
|
winrate = make_job('calc_winrates')
|
||||||
|
ts3_move_afk = make_job('ts3_move_afk')
|
||||||
|
ts3_snapshot = make_job('ts3_snapshot')
|
||||||
|
ts3_award_points = make_job('ts3_award_points')
|
||||||
|
ts3_process_events = make_job('ts3_process_events')
|
||||||
|
|
||||||
|
# Set their frequency to run
|
||||||
|
winrate.every(1).day()
|
||||||
|
ts3_move_afk.every(app.config['MOVE_AFK_FREQUENCY']).minute()
|
||||||
|
ts3_snapshot.every(app.config['SNAPSHOT_FREQUENCY']).hour()
|
||||||
|
ts3_award_points.every(app.config['AWARD_POINTS_FREQUENCY']).minute()
|
||||||
|
ts3_process_events.every(app.config['PROCESS_EVENTS_FREQUENCY']).hour()
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert True == winrate.is_valid()
|
||||||
|
assert True == ts3_move_afk.is_valid()
|
||||||
|
assert True == ts3_snapshot.is_valid()
|
||||||
|
assert True == ts3_award_points.is_valid()
|
||||||
|
assert True == ts3_process_events.is_valid()
|
||||||
|
except AssertionError as e:
|
||||||
|
print "Problem installing cronjobs: {}".format(e)
|
||||||
|
else:
|
||||||
|
cron.write()
|
||||||
|
print "Cron jobs written succesfully"
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def admin(name):
|
||||||
|
u = models.User.query.filter_by(nickname=name).first()
|
||||||
|
if u and not u.admin:
|
||||||
|
u.admin = True
|
||||||
|
db.session.commit()
|
||||||
|
print "User {} has been granted admin access.".format(name)
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def calc_winrates():
|
||||||
|
from app.analytics import calculate_winrates
|
||||||
|
tsServer = createTeamspeakInstance()
|
||||||
|
calculate_winrates()
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def ts3_move_afk():
|
||||||
|
from app.teamspeak import idle_mover
|
||||||
|
tsServer = createTeamspeakInstance()
|
||||||
|
idle_mover(tsServer)
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def ts3_snapshot():
|
||||||
|
from app.teamspeak import store_active_data
|
||||||
|
tsServer = createTeamspeakInstance()
|
||||||
|
store_active_data(tsServer)
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def ts3_award_points():
|
||||||
|
from app.teamspeak import award_idle_ts3_points
|
||||||
|
tsServer = createTeamspeakInstance()
|
||||||
|
award_idle_ts3_points(tsServer)
|
||||||
|
|
||||||
|
@manager.command
|
||||||
|
def ts3_process_events():
|
||||||
|
from app.teamspeak import process_ts3_events
|
||||||
|
tsServer = createTeamspeakInstance()
|
||||||
|
process_ts3_events(tsServer)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
manager.run()
|
Loading…
x
Reference in New Issue
Block a user