From 9a9b775e570ddf9772c819ff4e84a73eba99d176 Mon Sep 17 00:00:00 2001 From: Rakha Kanz Kautsar Date: Sat, 25 Mar 2017 12:37:37 +0700 Subject: [PATCH] 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 --- CTFd/admin/__init__.py | 7 ++ CTFd/admin/teams.py | 4 +- CTFd/challenges.py | 14 ++- CTFd/models.py | 32 +++++- CTFd/scoreboard.py | 72 +++++++++--- CTFd/templates/admin/config.html | 142 ++++++++++++++++-------- CTFd/templates/original/scoreboard.html | 9 ++ CTFd/templates/original/team.html | 9 ++ CTFd/utils.py | 16 +++ CTFd/views.py | 23 +++- 10 files changed, 250 insertions(+), 78 deletions(-) diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py index d105608..3149f3c 100644 --- a/CTFd/admin/__init__.py +++ b/CTFd/admin/__init__.py @@ -54,10 +54,13 @@ def admin_config(): if request.method == "POST": start = None end = None + freeze = None if request.form.get('start'): start = int(request.form['start']) if request.form.get('end'): end = int(request.form['end']) + if request.form.get('freeze'): + freeze = int(request.form['freeze']) try: 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_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.value = start @@ -136,6 +141,7 @@ def admin_config(): view_after_ctf = utils.get_config('view_after_ctf') start = utils.get_config('start') end = utils.get_config('end') + freeze = utils.get_config('freeze') mail_tls = utils.get_config('mail_tls') mail_ssl = utils.get_config('mail_ssl') @@ -157,6 +163,7 @@ def admin_config(): ctf_theme_config=ctf_theme, start=start, end=end, + freeze=freeze, hide_scores=hide_scores, mail_server=mail_server, mail_port=mail_port, diff --git a/CTFd/admin/teams.py b/CTFd/admin/teams.py index d8d58d9..7642951 100644 --- a/CTFd/admin/teams.py +++ b/CTFd/admin/teams.py @@ -39,8 +39,8 @@ def admin_team(teamid): .order_by(last_seen.desc()).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() - score = user.score() - place = user.place() + score = user.score(admin=True) + place = user.place(admin=True) return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing, place=place, wrong_keys=wrong_keys, awards=awards) elif request.method == 'POST': diff --git a/CTFd/challenges.py b/CTFd/challenges.py index cd153f3..f67a608 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -111,8 +111,18 @@ def solves(teamid=None): else: return redirect(url_for('auth.login', next='solves')) else: - solves = Solves.query.filter_by(teamid=teamid).all() - awards = Awards.query.filter_by(teamid=teamid).all() + solves = Solves.query.filter_by(teamid=teamid) + 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() json = {'solves': []} for solve in solves: diff --git a/CTFd/models.py b/CTFd/models.py index 1aaae96..386d440 100644 --- a/CTFd/models.py +++ b/CTFd/models.py @@ -159,20 +159,42 @@ class Teams(db.Model): def __repr__(self): return '' % self.name - def score(self): + def score(self, admin=False): 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 = 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: return int(team.score or 0) + int(award.award_score or 0) else: return 0 - def place(self): + def place(self, admin=False): score = db.func.sum(Challenges.value).label('score') 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 try: i = teams.index((self.id,)) + 1 diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py index 67141b9..28200cc 100644 --- a/CTFd/scoreboard.py +++ b/CTFd/scoreboard.py @@ -9,23 +9,49 @@ scoreboard = Blueprint('scoreboard', __name__) def get_standings(admin=False, count=None): - score = db.func.sum(Challenges.value).label('score') - date = db.func.max(Solves.date).label('date') - scores = db.session.query(Solves.teamid.label('teamid'), score, date).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) + scores = db.session.query( + Solves.teamid.label('teamid'), + db.func.sum(Challenges.value).label('score'), + db.func.max(Solves.date).label('date') + ).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') - 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: - standings_query = db.session.query(Teams.id.label('teamid'), 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) + standings_query = db.session.query( + Teams.id.label('teamid'), + 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: - standings_query = db.session.query(Teams.id.label('teamid'), Teams.name.label('name'), sumscores.columns.score) \ - .join(sumscores, Teams.id == sumscores.columns.teamid) \ - .filter(Teams.banned == False) \ - .order_by(sumscores.columns.score.desc(), sumscores.columns.date) + standings_query = db.session.query( + Teams.id.label('teamid'), + Teams.name.label('name'), + 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: standings = standings_query.all() else: @@ -41,7 +67,7 @@ def scoreboard_view(): if utils.hide_scores(): return render_template('scoreboard.html', errors=['Scores are currently hidden']) 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') @@ -73,8 +99,20 @@ def topteams(count): standings = get_standings(count=count) for team in standings: - solves = Solves.query.filter_by(teamid=team.teamid).all() - awards = Awards.query.filter_by(teamid=team.teamid).all() + solves = Solves.query.filter_by(teamid=team.teamid) + 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] = [] for x in solves: json['scores'][team.name].append({ diff --git a/CTFd/templates/admin/config.html b/CTFd/templates/admin/config.html index 829e8f8..9eaa4c0 100644 --- a/CTFd/templates/admin/config.html +++ b/CTFd/templates/admin/config.html @@ -179,6 +179,9 @@
  • End Time
  • +
  • + Freeze Time +
  • * All time fields required @@ -307,6 +310,70 @@ +
    +
    +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    +
    +
    @@ -343,57 +410,30 @@ var timestamp = parseInt(timestamp); } var m = moment(timestamp * 1000); - if (place == 'start'){ - console.log('Loading start'); - console.log(timestamp); - console.log(m.toISOString()); - console.log(m.unix()); - var month = $('#start-month').val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/) - var day = $('#start-day').val(m.date()); - var year = $('#start-year').val(m.year()); - var hour = $('#start-hour').val(m.hour()); - var minute = $('#start-minute').val(m.minute()); - 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'); - } + console.log('Loading ' + place); + console.log(timestamp); + console.log(m.toISOString()); + console.log(m.unix()); + var month = $('#' + place + '-month').val(m.month() + 1); // Months are zero indexed (http://momentjs.com/docs/#/get-set/month/) + var day = $('#' + place + '-day').val(m.date()); + var year = $('#' + place + '-year').val(m.year()); + var hour = $('#' + place + '-hour').val(m.hour()); + var minute = $('#' + place + '-minute').val(m.minute()); + load_date_values(place); } function load_date_values(place){ - if (place == "start"){ - var month = $('#start-month').val(); - var day = $('#start-day').val(); - var year = $('#start-year').val(); - var hour = $('#start-hour').val(); - var minute = $('#start-minute').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 month = $('#' + place + '-month').val(); + var day = $('#' + place + '-day').val(); + var year = $('#' + place + '-year').val(); + var hour = $('#' + place + '-hour').val(); + var minute = $('#' + place + '-minute').val(); + var timezone = $('#' + place + '-timezone').val(); + var utc = convert_date_to_moment(month, day, year, hour, minute, timezone); - if (place == "start") { - $('#start').val(utc.unix()); - $('#start-local').val(utc.local().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")); - } + $('#' + place).val(utc.unix()); + $('#' + place + '-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")); } function convert_date_to_moment(month, day, year, hour, minute, timezone){ @@ -431,6 +471,10 @@ load_date_values('end'); }); + $('.freeze-date').change(function () { + load_date_values('freeze'); + }); + $(function () { var hash = window.location.hash; @@ -446,6 +490,7 @@ var start = $('#start').val(); var end = $('#end').val(); + var freeze = $('#freeze').val(); console.log(start); console.log(end); if (start){ @@ -454,6 +499,9 @@ if (end){ load_timestamp('end', end); } + if (freeze) { + load_timestamp('freeze', freeze); + } }); {% endblock %} diff --git a/CTFd/templates/original/scoreboard.html b/CTFd/templates/original/scoreboard.html index 70acffc..71bdae3 100644 --- a/CTFd/templates/original/scoreboard.html +++ b/CTFd/templates/original/scoreboard.html @@ -16,6 +16,15 @@
    {% else %} + + {% if score_frozen %} +
    +
    +

    Scoreboard has been frozen.

    +
    +
    + {% endif %} +

    diff --git a/CTFd/templates/original/team.html b/CTFd/templates/original/team.html index 1e882d1..576ce52 100644 --- a/CTFd/templates/original/team.html +++ b/CTFd/templates/original/team.html @@ -17,6 +17,15 @@ {% endfor %} {% else %} + + {% if score_frozen %} +
    +
    +

    Scoreboard has been frozen.

    +
    +
    + {% endif %} +

    {%if place %} diff --git a/CTFd/utils.py b/CTFd/utils.py index 72932de..5dd74c0 100644 --- a/CTFd/utils.py +++ b/CTFd/utils.py @@ -212,6 +212,18 @@ def 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(): """ Checks whether it's CTF time or not. """ @@ -274,6 +286,10 @@ def unix_time_millis(dt): return unix_time(dt) * 1000 +def unix_time_to_utc(t): + return datetime.datetime.utcfromtimestamp(t) + + def get_ip(): """ 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. diff --git a/CTFd/views.py b/CTFd/views.py index 55882a9..058eb75 100644 --- a/CTFd/views.py +++ b/CTFd/views.py @@ -56,11 +56,12 @@ def setup():

    """.format(request.script_root)) # max attempts per challenge - max_tries = utils.set_config("max_tries", 0) + max_tries = utils.set_config('max_tries', 0) # Start time start = utils.set_config('start', None) end = utils.set_config('end', None) + freeze = utils.set_config('freeze', None) # Challenges cannot be viewed by unregistered users view_challenges_unregistered = utils.set_config('view_challenges_unregistered', None) @@ -102,7 +103,7 @@ def setup(): # Custom CSS handler @views.route('/static/user.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 @@ -139,11 +140,23 @@ def team(teamid): if utils.get_config('view_scoreboard_if_utils.authed') and not utils.authed(): return redirect(url_for('auth.login', next=request.path)) errors = [] + freeze = utils.get_config('freeze') user = Teams.query.filter_by(id=teamid).first_or_404() solves = Solves.query.filter_by(teamid=teamid) - awards = Awards.query.filter_by(teamid=teamid).all() - score = user.score() + awards = Awards.query.filter_by(teamid=teamid) + 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() 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) 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': json = {'solves': []} for x in solves: