Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

add new history viewer

  • Loading branch information...
commit 13089c3de521ac4ec8bed7964e0aaefe6b5a29a0 1 parent d7e3da3
@artnez authored
View
71 src/faceoff/models/match.py
@@ -4,7 +4,10 @@
"""
import logging
-from time import time
+from time import time, mktime
+from datetime import datetime
+from math import ceil
+from faceoff.debug import debug
from faceoff.db import use_db
from faceoff.debug import debug
from trueskill import TrueSkill
@@ -14,28 +17,66 @@ def find_match(db, **kwargs):
return db.find('match', **kwargs)
@use_db
-def search_matches(db, **kwargs):
- return db.search('match', **kwargs)
-
-@use_db
-def get_match_history(db, league_id, user_id=None, start=0, count=100):
+def search_matches(db, league_id, user_id=None, time_start=None,
+ time_end=None, page=None, per_page=10,
+ sort='date_created', order='desc'):
params = [league_id]
+ fields = """
+ match.*, winner.id AS winner_id, winner.nickname AS winner_nickname,
+ loser.id AS loser_id, loser.nickname AS loser_nickname
+ """
query = """
- SELECT
- match.*, winner.id AS winner_id, winner.nickname AS winner_nickname,
- loser.id AS loser_id, loser.nickname AS loser_nickname
FROM match
INNER JOIN user AS winner ON winner.id = match.winner_id
INNER JOIN user AS loser ON loser.id = match.loser_id
WHERE match.league_id=?
- """
+ """
if user_id is not None:
query += " AND (winner.id=? OR loser.id=?) "
params.extend([user_id, user_id])
- query += ' ORDER BY match.date_created DESC '
- if count is not None:
- query += ' LIMIT %d, %d ' % (start, count)
- return db.select(query, params)
+ if time_start is not None:
+ query += " AND match.date_created >= ? "
+ params.append(time_start)
+ if time_end is not None:
+ query += " AND match.date_created <= ? "
+ params.append(time_end)
+ query += ' ORDER BY match.%s %s ' % (db.clean(sort), db.clean(order))
+ if page is not None and page > 0:
+ return db.paginate(fields, query, params, page, per_page)
+ else:
+ return db.select("SELECT %s %s" % (fields, query), params)
+
+@use_db
+def find_older_match(db, league_id, user_id, timestamp):
+ result = search_matches(
+ db, league_id, user_id=user_id, time_end=timestamp, page=1, per_page=1
+ )
+ if not len(result['row_data']):
+ return None
+ match = result['row_data'][0]
+ match['date'] = datetime.fromtimestamp(match['date_created'])
+ match['dateargs'] = {
+ 'year': match['date'].year,
+ 'month': match['date'].strftime('%b').lower(),
+ 'day': match['date'].day
+ }
+ return match
+
+@use_db
+def find_newer_match(db, league_id, user_id, timestamp):
+ result = search_matches(
+ db, league_id, user_id=user_id, time_start=timestamp, page=1,
+ per_page=1, order='asc')
+ if not len(result['row_data']):
+ return None
+ match = result['row_data'][0]
+ match['date'] = datetime.fromtimestamp(match['date_created'])
+ match['dateargs'] = {
+ 'year': match['date'].year,
+ 'month': match['date'].strftime('%b').lower(),
+ 'day': match['date'].day
+ }
+ return match
@use_db
def create_match(db, league_id, winner_user_id, loser_user_id, match_date=None, norebuild=False):
@@ -86,7 +127,7 @@ def rebuild_rankings(db, league_id):
# generate a local player ranking profile based on user id. all matches will
# be traversed and this object will be populated to build the rankings.
players = {}
- for match in search_matches(db, league_id=league_id):
+ for match in db.search('match', league_id=league_id):
w = match['winner_id']
l = match['loser_id']
View
5 src/faceoff/models/user.py
@@ -18,6 +18,11 @@ def find_user(db, **kwargs):
return db.find('user', **kwargs)
@use_db
+def find_user_id(db, nickname):
+ user = db.find('user', nickname=nickname)
+ return None if user is None else user['id']
+
+@use_db
def search_users(db, **kwargs):
return db.search('user', **kwargs)
View
13 src/faceoff/schema/1.2.sql
@@ -0,0 +1,13 @@
+/**
+ * Adds detailed stats to match history.
+ *
+ * Copyright: (c) 2012 Artem Nezvigin <artem@artnez.com>
+ * License: MIT, see LICENSE for details
+ */
+
+ALTER TABLE match ADD COLUMN draw_prob VARCHAR(64);
+ALTER TABLE match ADD COLUMN winner_mu VARCHAR(64);
+ALTER TABLE match ADD COLUMN winner_sigma VARCHAR(64);
+ALTER TABLE match ADD COLUMN loser_mu VARCHAR(64);
+ALTER TABLE match ADD COLUMN loser_sigma VARCHAR(64);
+UPDATE setting SET value='1.2' WHERE name='schema_version';
View
17 src/faceoff/static/css/faceoff.league.css
@@ -44,19 +44,18 @@ table.match-history thead tr th {
table.match-history thead tr th.winner,
table.match-history tbody tr td.winner {
- width: 150px;
+ width: 135px;
text-align: right;
}
table.match-history thead tr th.versus,
table.match-history tbody tr td.versus {
- width: 20px;
text-align: center;
}
table.match-history thead tr th.loser,
table.match-history tbody tr td.loser {
- width: 150px;
+ width: 135px;
}
table.match-history tbody tr td.winner .rank,
@@ -67,8 +66,16 @@ table.match-history tbody tr td.loser .rank {
color: #999;
}
-table.match-history tbody tr td.date {
- font-size: 12px;
+table.match-history thead tr th.time,
+table.match-history tbody tr td.time {
+ width: 75px;
+ text-align: center;
+}
+
+table.match-history thead tr th.draw,
+table.match-history tbody tr td.draw {
+ width: 75px;
+ text-align: center;
}
table.user-history {}
View
28 src/faceoff/static/css/faceoff.simple.css
@@ -139,6 +139,7 @@ h2 {
}
.section {
+ position: relative;
margin: 0 0 40px 0;
}
@@ -157,6 +158,33 @@ h2 {
padding: 0 12px;
}
+#content .section h2.center a.prev-button,
+#content .section h2.center a.next-button {
+ display: inline-block;
+ width: 20px;
+ height: 20px;
+ background: url('../img/heading-buttons.png') no-repeat 0 0;
+ text-indent: -1000px;
+ overflow: hidden;
+ margin: 0 0 2px 0;
+ padding: 0;
+ vertical-align: middle;
+}
+
+#content .section h2.center a.prev-button.hidden,
+#content .section h2.center a.next-button.hidden {
+ visibility: hidden;
+}
+
+#content .section h2.center a.prev-button {
+ margin-right: -5px;
+ background-position: 0 -20px;
+}
+
+#content .section h2.center a.next-button {
+ margin-left: -5px;
+}
+
.league-list h1 {
margin: 0 0 10px 0;
}
View
BIN  src/faceoff/static/img/heading-buttons.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
12 src/faceoff/static/js/faceoff.js
@@ -4,6 +4,11 @@
*/
jQuery(function($) {
+ // disable certain anchors
+ $('a[href=#]').click(function(e) {
+ e.preventDefault();
+ });
+
// trigger bootstrap opt-in apis
$('a.tip').tooltip();
@@ -20,6 +25,13 @@ jQuery(function($) {
$('#report button[type=submit]').attr('disabled', false);
}, 2000);
+ // history filter
+ $('#history-filter select').change(function() {
+ var sel = $(this),
+ url = sel.parent('form').attr('action');
+ window.location = url + sel.val();
+ });
+
// back buttons
$('a.go-back').click(function() {
history && history.go(-1);
View
26 src/faceoff/templates/history.html
@@ -2,29 +2,43 @@
{% set head_title = 'History' %}
{% block content %}
<div class='section'>
- <h2 class='center'><span>Recent Matches</span></h2>
- {% if match_history | length %}
+ <h2 class='center'>
+ {% if newer_match %}
+ <a class='prev-button' href='{{ url_for('history', nickname=nickname, **newer_match['dateargs']) }}'>Newer</a>
+ {% else %}
+ <a class='prev-button hidden' href='#'>--</a>
+ {% endif %}
+ <span>{{ time_start | full_date }}</span>
+ {% if older_match %}
+ <a class='next-button' href='{{ url_for('history', nickname=nickname, **older_match['dateargs']) }}'>Older</a>
+ {% else %}
+ <a class='next-button hidden' href='#'>--</a>
+ {% endif %}
+ </h2>
+ {% if matches | length %}
<table class='table table-bordered table-striped match-history'>
<thead>
<tr>
<th class='winner'>Won</th>
<th class='versus'>vs.</th>
<th class='loser'>Lost</th>
- <th class='date'>Date Played</th>
+ <th class='time'>Time</th>
+ <th class='draw'>P(draw)</th>
</tr>
</thead>
<tbody>
- {% for match in match_history %}
+ {% for match in matches %}
<tr>
<td class='winner'>{{ match['winner_nickname'] }} <span class='rank'>{{ match['winner_rank']|player_rank }}</span></td><td class='versus'>vs.</td>
<td class='loser'><span class='rank'>{{ match['loser_rank']|player_rank }}</span> {{ match['loser_nickname'] }}</td>
- <td class='date'>{{ match['date_created']|human_date() }}</td>
+ <td class='time'>{{ match['date_created']|date_format('%-I:%M%p') | lower }}</td>
+ <td class='draw'>{{ match['draw_prob'] | default('0') | float | round(1) }}%</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
- <p class='search-empty'>No matches have been played.</p>
+ <p class='search-empty'>No matches played.</p>
{% endif %}
</div>
{% endblock %}
View
24 src/faceoff/tpl.py
@@ -3,8 +3,9 @@
License: MIT, see LICENSE for details
"""
+from faceoff.debug import debug
from datetime import datetime
-from time import localtime, strftime
+from time import localtime, strftime, mktime
_filters = {}
@@ -16,9 +17,12 @@ def template_filter(f):
Marks the decorated function as a template filter.
"""
_filters[f.__name__] = f
+ return f
@template_filter
def date_format(s, f):
+ if isinstance(s, datetime):
+ s = mktime(s.timetuple())
return strftime(f, localtime(int(s)))
@template_filter
@@ -26,7 +30,25 @@ def player_rank(r):
return '---' if r is None else '%03d' % int(r)
@template_filter
+def full_date(s, with_month=True, with_date=True, with_year=True):
+ if isinstance(s, datetime):
+ s = mktime(s.timetuple())
+ d = datetime.fromtimestamp(s)
+ date = ''
+ if with_month:
+ date += ' ' + d.strftime('%b')
+ if with_date:
+ ndate = d.strftime('%d')
+ suffix = num_suffix(int(ndate))
+ date += ' ' + ndate + suffix
+ if with_year:
+ date += ', ' + d.strftime('%Y')
+ return date
+
+@template_filter
def human_date(s):
+ if isinstance(s, datetime):
+ s = mktime(s.timetuple())
d = datetime.fromtimestamp(s)
n = datetime.today()
if d.date() == n.date():
View
56 src/faceoff/views.py
@@ -5,10 +5,11 @@
import os
import logging
-from datetime import datetime
-from time import localtime, strftime
+from datetime import datetime, date
+from time import mktime
from flask import \
- g, request, session, flash, abort, redirect, url_for, send_from_directory
+ g, request, session, flash, abort, redirect, url_for, send_from_directory, \
+ jsonify
from faceoff import app
from faceoff.debug import debug
from faceoff.forms import \
@@ -16,13 +17,14 @@
AdminForm
from faceoff.helpers.decorators import authenticated, templated
from faceoff.models.user import \
- get_active_users, create_user, update_user, auth_login, auth_logout
+ find_user, get_active_users, create_user, update_user, auth_login, \
+ auth_logout, find_user_id
from faceoff.models.league import \
find_league, get_active_leagues, get_inactive_leagues, create_league, \
update_league
from faceoff.models.match import \
- create_match, get_match_history, get_league_ranking, get_user_standing, \
- rebuild_rankings
+ create_match, search_matches, get_league_ranking, get_user_standing, \
+ rebuild_rankings, find_older_match, find_newer_match
from faceoff.models.setting import get_setting, set_access_code
@app.teardown_request
@@ -152,7 +154,7 @@ def dashboard():
report_form = ReportForm(get_active_users()),
current_ranking = get_user_standing(league['id'], user['id']),
ranking=get_league_ranking(league['id']),
- history=get_match_history(league['id'], user_id=user['id'])
+ history=search_matches(league['id'], user_id=user['id'])
)
@app.route('/<league>/report', methods=('POST',))
@@ -177,11 +179,45 @@ def report():
def standings():
return dict(ranking=get_league_ranking(g.current_league['id']))
-@app.route('/<league>/history/')
+@app.route('/<league>/history/', defaults={'nickname': None, 'year': None, 'month': None, 'day': None})
+@app.route('/<league>/history/<int:year>/', defaults={'nickname': None, 'month': None, 'day': None})
+@app.route('/<league>/history/<int:year>/<month>/', defaults={'nickname': None, 'day': None})
+@app.route('/<league>/history/<int:year>/<month>/<int:day>', defaults={'nickname': None})
+@app.route('/<league>/history/<nickname>/', defaults={'year': None, 'month': None, 'day': None})
+@app.route('/<league>/history/<nickname>/<int:year>/', defaults={'month': None, 'day': None})
+@app.route('/<league>/history/<nickname>/<int:year>/<month>/', defaults={'day': None})
+@app.route('/<league>/history/<nickname>/<int:year>/<month>/<int:day>')
@templated()
@authenticated
-def history():
- return dict(match_history=get_match_history(g.current_league['id']))
+def history(nickname, year, month, day):
+ league_id = g.current_league['id']
+ user_id = find_user_id(nickname) if nickname is not None else None
+ today = date.today()
+ refresh = False
+ if year is None:
+ year = today.year
+ refresh = True
+ if month is None:
+ month = today.strftime('%b').lower()
+ refresh = True
+ if day is None:
+ day = today.day if month == today.strftime('%b').lower() else 1
+ refresh = True
+ if refresh:
+ kwargs = {'nickname': nickname, 'year': year, 'month': month, 'day': day}
+ return redirect(url_for('history', **kwargs))
+ try:
+ date_start = datetime.strptime('%s-%s-%s' % (year, month, day), '%Y-%b-%d')
+ date_end = date_start.replace(hour=23, minute=59, second=59)
+ time_start = mktime(date_start.timetuple())
+ time_end = mktime(date_end.timetuple())
+ except ValueError:
+ abort(404)
+ matches = search_matches(league_id, user_id, time_start, time_end)
+ newer = find_newer_match(league_id, user_id, time_end)
+ older = find_older_match(league_id, user_id, time_start)
+ return dict(matches=matches, time_start=time_start, time_end=time_end,
+ newer_match=newer, older_match=older, nickname=nickname)
@app.route('/<league>/settings/', methods=('GET', 'POST'))
@templated()
Please sign in to comment.
Something went wrong with that request. Please try again.