add scoreboard freeze (#208)

* add scoreboard freeze

* delete excess div close tag

* filter out scores from team page when scoreboard freezes

* allow teams to see their full score and solves in team page

* fix unset place and score

* change parameter and filter out /solves for graph

* fix utils methods undefined

* add small notice about frozen scoreboard and resolve failing tests

* Update __init__.py

* Update scoreboard.py
selenium-screenshot-testing
Rakha Kanz Kautsar 2017-03-25 12:37:37 +07:00 committed by Kevin Chung
parent a3a7d75ae8
commit 9a9b775e57
10 changed files with 250 additions and 78 deletions

View File

@ -54,10 +54,13 @@ def admin_config():
if request.method == "POST": if request.method == "POST":
start = None start = None
end = None end = None
freeze = None
if request.form.get('start'): if request.form.get('start'):
start = int(request.form['start']) start = int(request.form['start'])
if request.form.get('end'): if request.form.get('end'):
end = int(request.form['end']) end = int(request.form['end'])
if request.form.get('freeze'):
freeze = int(request.form['freeze'])
try: try:
view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None)) view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None))
@ -103,6 +106,8 @@ def admin_config():
mg_base_url = utils.set_config("mg_base_url", request.form.get('mg_base_url', None)) mg_base_url = utils.set_config("mg_base_url", request.form.get('mg_base_url', None))
mg_api_key = utils.set_config("mg_api_key", request.form.get('mg_api_key', None)) mg_api_key = utils.set_config("mg_api_key", request.form.get('mg_api_key', None))
db_freeze = utils.set_config("freeze", freeze)
db_start = Config.query.filter_by(key='start').first() db_start = Config.query.filter_by(key='start').first()
db_start.value = start db_start.value = start
@ -136,6 +141,7 @@ def admin_config():
view_after_ctf = utils.get_config('view_after_ctf') view_after_ctf = utils.get_config('view_after_ctf')
start = utils.get_config('start') start = utils.get_config('start')
end = utils.get_config('end') end = utils.get_config('end')
freeze = utils.get_config('freeze')
mail_tls = utils.get_config('mail_tls') mail_tls = utils.get_config('mail_tls')
mail_ssl = utils.get_config('mail_ssl') mail_ssl = utils.get_config('mail_ssl')
@ -157,6 +163,7 @@ def admin_config():
ctf_theme_config=ctf_theme, ctf_theme_config=ctf_theme,
start=start, start=start,
end=end, end=end,
freeze=freeze,
hide_scores=hide_scores, hide_scores=hide_scores,
mail_server=mail_server, mail_server=mail_server,
mail_port=mail_port, mail_port=mail_port,

View File

@ -39,8 +39,8 @@ def admin_team(teamid):
.order_by(last_seen.desc()).all() .order_by(last_seen.desc()).all()
wrong_keys = WrongKeys.query.filter_by(teamid=teamid).order_by(WrongKeys.date.asc()).all() wrong_keys = WrongKeys.query.filter_by(teamid=teamid).order_by(WrongKeys.date.asc()).all()
awards = Awards.query.filter_by(teamid=teamid).order_by(Awards.date.asc()).all() awards = Awards.query.filter_by(teamid=teamid).order_by(Awards.date.asc()).all()
score = user.score() score = user.score(admin=True)
place = user.place() place = user.place(admin=True)
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing, return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing,
place=place, wrong_keys=wrong_keys, awards=awards) place=place, wrong_keys=wrong_keys, awards=awards)
elif request.method == 'POST': elif request.method == 'POST':

View File

@ -111,8 +111,18 @@ def solves(teamid=None):
else: else:
return redirect(url_for('auth.login', next='solves')) return redirect(url_for('auth.login', next='solves'))
else: else:
solves = Solves.query.filter_by(teamid=teamid).all() solves = Solves.query.filter_by(teamid=teamid)
awards = Awards.query.filter_by(teamid=teamid).all() awards = Awards.query.filter_by(teamid=teamid)
freeze = utils.get_config('freeze')
if freeze:
freeze = utils.unix_time_to_utc(freeze)
if teamid != session.get('id'):
solves = solves.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
solves = solves.all()
awards = awards.all()
db.session.close() db.session.close()
json = {'solves': []} json = {'solves': []}
for solve in solves: for solve in solves:

View File

@ -159,20 +159,42 @@ class Teams(db.Model):
def __repr__(self): def __repr__(self):
return '<team %r>' % self.name return '<team %r>' % self.name
def score(self): def score(self, admin=False):
score = db.func.sum(Challenges.value).label('score') score = db.func.sum(Challenges.value).label('score')
team = db.session.query(Solves.teamid, score).join(Teams).join(Challenges).filter(Teams.banned == False, Teams.id == self.id).group_by(Solves.teamid).first() team = db.session.query(Solves.teamid, score).join(Teams).join(Challenges).filter(Teams.banned == False, Teams.id == self.id)
award_score = db.func.sum(Awards.value).label('award_score') award_score = db.func.sum(Awards.value).label('award_score')
award = db.session.query(award_score).filter_by(teamid=self.id).first() award = db.session.query(award_score).filter_by(teamid=self.id)
if not admin:
freeze = Config.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
team = team.filter(Solves.date < freeze)
award = award.filter(Awards.date < freeze)
team = team.group_by(Solves.teamid).first()
award = award.first()
if team: if team:
return int(team.score or 0) + int(award.award_score or 0) return int(team.score or 0) + int(award.award_score or 0)
else: else:
return 0 return 0
def place(self): def place(self, admin=False):
score = db.func.sum(Challenges.value).label('score') score = db.func.sum(Challenges.value).label('score')
quickest = db.func.max(Solves.date).label('quickest') quickest = db.func.max(Solves.date).label('quickest')
teams = db.session.query(Solves.teamid).join(Teams).join(Challenges).filter(Teams.banned == False).group_by(Solves.teamid).order_by(score.desc(), quickest).all() teams = db.session.query(Solves.teamid).join(Teams).join(Challenges).filter(Teams.banned == False)
if not admin:
freeze = Config.query.filter_by(key='freeze').first()
if freeze and freeze.value:
freeze = int(freeze.value)
freeze = datetime.datetime.utcfromtimestamp(freeze)
teams = teams.filter(Solves.date < freeze)
teams = teams.group_by(Solves.teamid).order_by(score.desc(), quickest).all()
# http://codegolf.stackexchange.com/a/4712 # http://codegolf.stackexchange.com/a/4712
try: try:
i = teams.index((self.id,)) + 1 i = teams.index((self.id,)) + 1

View File

@ -9,23 +9,49 @@ scoreboard = Blueprint('scoreboard', __name__)
def get_standings(admin=False, count=None): def get_standings(admin=False, count=None):
score = db.func.sum(Challenges.value).label('score') scores = db.session.query(
date = db.func.max(Solves.date).label('date') Solves.teamid.label('teamid'),
scores = db.session.query(Solves.teamid.label('teamid'), score, date).join(Challenges).group_by(Solves.teamid) db.func.sum(Challenges.value).label('score'),
awards = db.session.query(Awards.teamid.label('teamid'), db.func.sum(Awards.value).label('score'), db.func.max(Awards.date).label('date')) \ db.func.max(Solves.date).label('date')
.group_by(Awards.teamid) ).join(Challenges).group_by(Solves.teamid)
awards = db.session.query(
Awards.teamid.label('teamid'),
db.func.sum(Awards.value).label('score'),
db.func.max(Awards.date).label('date')
).group_by(Awards.teamid)
freeze = utils.get_config('freeze')
if not admin and freeze:
scores = scores.filter(Solves.date < utils.unix_time_to_utc(freeze))
awards = awards.filter(Awards.date < utils.unix_time_to_utc(freeze))
results = union_all(scores, awards).alias('results') results = union_all(scores, awards).alias('results')
sumscores = db.session.query(results.columns.teamid, db.func.sum(results.columns.score).label('score'), db.func.max(results.columns.date).label('date')) \
.group_by(results.columns.teamid).subquery() sumscores = db.session.query(
results.columns.teamid,
db.func.sum(results.columns.score).label('score'),
db.func.max(results.columns.date).label('date')
).group_by(results.columns.teamid).subquery()
if admin: if admin:
standings_query = db.session.query(Teams.id.label('teamid'), Teams.name.label('name'), Teams.banned, sumscores.columns.score) \ standings_query = db.session.query(
.join(sumscores, Teams.id == sumscores.columns.teamid) \ Teams.id.label('teamid'),
.order_by(sumscores.columns.score.desc(), sumscores.columns.date) Teams.name.label('name'),
Teams.banned, sumscores.columns.score
)\
.join(sumscores, Teams.id == sumscores.columns.teamid) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.date)
else: else:
standings_query = db.session.query(Teams.id.label('teamid'), Teams.name.label('name'), sumscores.columns.score) \ standings_query = db.session.query(
.join(sumscores, Teams.id == sumscores.columns.teamid) \ Teams.id.label('teamid'),
.filter(Teams.banned == False) \ Teams.name.label('name'),
.order_by(sumscores.columns.score.desc(), sumscores.columns.date) sumscores.columns.score
)\
.join(sumscores, Teams.id == sumscores.columns.teamid) \
.filter(Teams.banned == False) \
.order_by(sumscores.columns.score.desc(), sumscores.columns.date)
if count is None: if count is None:
standings = standings_query.all() standings = standings_query.all()
else: else:
@ -41,7 +67,7 @@ def scoreboard_view():
if utils.hide_scores(): if utils.hide_scores():
return render_template('scoreboard.html', errors=['Scores are currently hidden']) return render_template('scoreboard.html', errors=['Scores are currently hidden'])
standings = get_standings() standings = get_standings()
return render_template('scoreboard.html', teams=standings) return render_template('scoreboard.html', teams=standings, score_frozen=utils.is_scoreboard_frozen())
@scoreboard.route('/scores') @scoreboard.route('/scores')
@ -73,8 +99,20 @@ def topteams(count):
standings = get_standings(count=count) standings = get_standings(count=count)
for team in standings: for team in standings:
solves = Solves.query.filter_by(teamid=team.teamid).all() solves = Solves.query.filter_by(teamid=team.teamid)
awards = Awards.query.filter_by(teamid=team.teamid).all() awards = Awards.query.filter_by(teamid=team.teamid)
freeze = utils.get_config('freeze')
if freeze:
solves = solves.filter(Solves.date < utils.unix_time_to_utc(freeze))
awards = awards.filter(Awards.date < utils.unix_time_to_utc(freeze))
solves = solves.all()
awards = awards.all()
json['scores'][team.name] = [] json['scores'][team.name] = []
for x in solves: for x in solves:
json['scores'][team.name].append({ json['scores'][team.name].append({

View File

@ -179,6 +179,9 @@
<li role="presentation"> <li role="presentation">
<a href="#end-date" aria-controls="end-date" role="tab" data-toggle="tab">End Time</a> <a href="#end-date" aria-controls="end-date" role="tab" data-toggle="tab">End Time</a>
</li> </li>
<li role="presentation">
<a href="#freeze-date" aria-controls="freeze-date" role="tab" data-toggle="tab">Freeze Time</a>
</li>
<sub style="float:right;">* All time fields required</sub> <sub style="float:right;">* All time fields required</sub>
</ul> </ul>
@ -307,6 +310,70 @@
</div> </div>
</div> </div>
</div> </div>
<div role="tabpanel" class="tab-pane" id="freeze-date">
<div class="row" id="freeze-date">
<div class="form-group col-xs-2">
<label for="freeze-month">Month:</label>
<input class="form-control freeze-date" id='freeze-month' name='freeze-month' min="1" max="12"
type='number'>
</div>
<div class="form-group col-xs-2">
<label for="freeze-day">Day:</label>
<input class="form-control freeze-date" id='freeze-day' name='freeze-day' min="1" max="31"
type='number'>
</div>
<div class="form-group col-xs-2">
<label for="freeze-year">Year:</label>
<input class="form-control freeze-date" id='freeze-year' name='freeze-year' type='number'>
</div>
<div class="form-group col-xs-2">
<label for="freeze-hour">Hour:</label>
<input class="form-control freeze-date" id='freeze-hour' name='freeze-hour' min="0" max="23"
type='number'>
</div>
<div class="form-group col-xs-2">
<label for="freeze-minute">Minute:</label>
<input class="form-control freeze-date" id='freeze-minute' name='freeze-minute' min="0"
max="59" type='number'>
</div>
<div class="form-group col-xs-4">
<label for="freeze-timezone">Timezone:</label>
<select class="form-control freeze-date" id="freeze-timezone">
<script>
document.write('<option>' + moment.tz.guess() + '</option>');
var tz_names = moment.tz.names();
for (var i = 0; i < tz_names.length; i++) {
document.write('<option>' + tz_names[i] + '</option>');
}
</script>
</select>
</div>
<div class="form-group col-xs-12">
<label for="freeze-local">Local Time:</label>
<input class="form-control" id='freeze-local' type='text'
placeholder="Freeze Date (Local Time)" readonly>
</div>
<div class="form-group col-xs-12">
<label for="freeze-zonetime">Timezone Time:</label>
<input class="form-control" id='freeze-zonetime' type='text'
placeholder="Freeze Date (Timezone Time)" readonly>
</div>
<div class="form-group col-xs-12">
<label for="freeze">UTC Timestamp:</label>
<input class="form-control" id='freeze' name='freeze' type='text'
placeholder="Freeze Date (UTC timestamp)"
{% if freeze is defined and freeze != None %}value="{{ freeze }}"{% endif %} readonly>
</div>
</div>
</div>
</div> </div>
<div class="checkbox"> <div class="checkbox">
@ -343,57 +410,30 @@
var timestamp = parseInt(timestamp); var timestamp = parseInt(timestamp);
} }
var m = moment(timestamp * 1000); var m = moment(timestamp * 1000);
if (place == 'start'){ console.log('Loading ' + place);
console.log('Loading start'); console.log(timestamp);
console.log(timestamp); console.log(m.toISOString());
console.log(m.toISOString()); console.log(m.unix());
console.log(m.unix()); var month = $('#' + place + '-month').val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/)
var month = $('#start-month').val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/) var day = $('#' + place + '-day').val(m.date());
var day = $('#start-day').val(m.date()); var year = $('#' + place + '-year').val(m.year());
var year = $('#start-year').val(m.year()); var hour = $('#' + place + '-hour').val(m.hour());
var hour = $('#start-hour').val(m.hour()); var minute = $('#' + place + '-minute').val(m.minute());
var minute = $('#start-minute').val(m.minute()); load_date_values(place);
load_date_values('start');
} else if (place == 'end'){
console.log('Loading end');
console.log(timestamp);
console.log(m.toISOString());
console.log(m.unix());
var month = $('#end-month').val(m.month() + 1);
var day = $('#end-day').val(m.date());
var year = $('#end-year').val(m.year());
var hour = $('#end-hour').val(m.hour());
var minute = $('#end-minute').val(m.minute());
load_date_values('end');
}
} }
function load_date_values(place){ function load_date_values(place){
if (place == "start"){ var month = $('#' + place + '-month').val();
var month = $('#start-month').val(); var day = $('#' + place + '-day').val();
var day = $('#start-day').val(); var year = $('#' + place + '-year').val();
var year = $('#start-year').val(); var hour = $('#' + place + '-hour').val();
var hour = $('#start-hour').val(); var minute = $('#' + place + '-minute').val();
var minute = $('#start-minute').val(); var timezone = $('#' + place + '-timezone').val();
var timezone = $('#start-timezone').val();
} else if (place == "end") {
var month = $('#end-month').val();
var day = $('#end-day').val();
var year = $('#end-year').val();
var hour = $('#end-hour').val();
var minute = $('#end-minute').val();
var timezone = $('#end-timezone').val();
}
var utc = convert_date_to_moment(month, day, year, hour, minute, timezone); var utc = convert_date_to_moment(month, day, year, hour, minute, timezone);
if (place == "start") { $('#' + place).val(utc.unix());
$('#start').val(utc.unix()); $('#' + place + '-local').val(utc.local().format("dddd, MMMM Do YYYY, h:mm:ss a zz"));
$('#start-local').val(utc.local().format("dddd, MMMM Do YYYY, h:mm:ss a zz")); $('#' + place + '-zonetime').val(utc.tz(timezone).format("dddd, MMMM Do YYYY, h:mm:ss a zz"));
$('#start-zonetime').val(utc.tz(timezone).format("dddd, MMMM Do YYYY, h:mm:ss a zz"));
} else if (place == "end") {
$('#end').val(utc.unix());
$('#end-local').val(utc.local().format("dddd, MMMM Do YYYY, h:mm:ss a zz"));
$('#end-zonetime').val(utc.tz(timezone).format("dddd, MMMM Do YYYY, h:mm:ss a zz"));
}
} }
function convert_date_to_moment(month, day, year, hour, minute, timezone){ function convert_date_to_moment(month, day, year, hour, minute, timezone){
@ -431,6 +471,10 @@
load_date_values('end'); load_date_values('end');
}); });
$('.freeze-date').change(function () {
load_date_values('freeze');
});
$(function () { $(function () {
var hash = window.location.hash; var hash = window.location.hash;
@ -446,6 +490,7 @@
var start = $('#start').val(); var start = $('#start').val();
var end = $('#end').val(); var end = $('#end').val();
var freeze = $('#freeze').val();
console.log(start); console.log(start);
console.log(end); console.log(end);
if (start){ if (start){
@ -454,6 +499,9 @@
if (end){ if (end){
load_timestamp('end', end); load_timestamp('end', end);
} }
if (freeze) {
load_timestamp('freeze', freeze);
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@ -16,6 +16,15 @@
</div> </div>
</div> </div>
{% else %} {% else %}
{% if score_frozen %}
<div class="container main-container">
<div class="row">
<h1>Scoreboard has been frozen.</h1>
</div>
</div>
{% endif %}
<div id="score-graph"></div> <div id="score-graph"></div>
<br> <br>

View File

@ -17,6 +17,15 @@
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
{% if score_frozen %}
<div class="container main-container">
<div class="row">
<h1>Scoreboard has been frozen.</h1>
</div>
</div>
{% endif %}
<div class="team-info"> <div class="team-info">
<h2 id="team-place" class="text-center"> <h2 id="team-place" class="text-center">
{%if place %} {%if place %}

View File

@ -212,6 +212,18 @@ def view_after_ctf():
return bool(get_config('view_after_ctf')) return bool(get_config('view_after_ctf'))
def is_scoreboard_frozen():
freeze = get_config('freeze')
if freeze:
freeze = int(freeze)
if freeze < time.time():
return True
return False
def ctftime(): def ctftime():
""" Checks whether it's CTF time or not. """ """ Checks whether it's CTF time or not. """
@ -274,6 +286,10 @@ def unix_time_millis(dt):
return unix_time(dt) * 1000 return unix_time(dt) * 1000
def unix_time_to_utc(t):
return datetime.datetime.utcfromtimestamp(t)
def get_ip(): def get_ip():
""" Returns the IP address of the currently in scope request. The approach is to define a list of trusted proxies """ Returns the IP address of the currently in scope request. The approach is to define a list of trusted proxies
(in this case the local network), and only trust the most recently defined untrusted IP address. (in this case the local network), and only trust the most recently defined untrusted IP address.

View File

@ -56,11 +56,12 @@ def setup():
</div>""".format(request.script_root)) </div>""".format(request.script_root))
# max attempts per challenge # max attempts per challenge
max_tries = utils.set_config("max_tries", 0) max_tries = utils.set_config('max_tries', 0)
# Start time # Start time
start = utils.set_config('start', None) start = utils.set_config('start', None)
end = utils.set_config('end', None) end = utils.set_config('end', None)
freeze = utils.set_config('freeze', None)
# Challenges cannot be viewed by unregistered users # Challenges cannot be viewed by unregistered users
view_challenges_unregistered = utils.set_config('view_challenges_unregistered', None) view_challenges_unregistered = utils.set_config('view_challenges_unregistered', None)
@ -102,7 +103,7 @@ def setup():
# Custom CSS handler # Custom CSS handler
@views.route('/static/user.css') @views.route('/static/user.css')
def custom_css(): def custom_css():
return Response(utils.get_config("css"), mimetype='text/css') return Response(utils.get_config('css'), mimetype='text/css')
# Static HTML files # Static HTML files
@ -139,11 +140,23 @@ def team(teamid):
if utils.get_config('view_scoreboard_if_utils.authed') and not utils.authed(): if utils.get_config('view_scoreboard_if_utils.authed') and not utils.authed():
return redirect(url_for('auth.login', next=request.path)) return redirect(url_for('auth.login', next=request.path))
errors = [] errors = []
freeze = utils.get_config('freeze')
user = Teams.query.filter_by(id=teamid).first_or_404() user = Teams.query.filter_by(id=teamid).first_or_404()
solves = Solves.query.filter_by(teamid=teamid) solves = Solves.query.filter_by(teamid=teamid)
awards = Awards.query.filter_by(teamid=teamid).all() awards = Awards.query.filter_by(teamid=teamid)
score = user.score()
place = user.place() place = user.place()
score = user.score()
if freeze:
freeze = utils.unix_time_to_utc(freeze)
if teamid != session.get('id'):
solves = solves.filter(Solves.date < freeze)
awards = awards.filter(Awards.date < freeze)
solves = solves.all()
awards = awards.all()
db.session.close() db.session.close()
if utils.hide_scores() and teamid != session.get('id'): if utils.hide_scores() and teamid != session.get('id'):
@ -153,7 +166,7 @@ def team(teamid):
return render_template('team.html', team=user, errors=errors) return render_template('team.html', team=user, errors=errors)
if request.method == 'GET': if request.method == 'GET':
return render_template('team.html', solves=solves, awards=awards, team=user, score=score, place=place) return render_template('team.html', solves=solves, awards=awards, team=user, score=score, place=place, score_frozen=utils.is_scoreboard_frozen())
elif request.method == 'POST': elif request.method == 'POST':
json = {'solves': []} json = {'solves': []}
for x in solves: for x in solves: