mirror of https://github.com/JohnHammond/CTFd.git
Version 1.1 CTFd (#514)
* Bootstrap v4 (#490) * Upgrading original theme to use Bootstrap v4 and overall improve use of utility classes * Fixing graph issues. Colors per team & cleaner hover * The solves tab now shows relative time instead of absolute time * Redesign admin theme * Updating modals and changing form name from desc to description * Moving CSS config from Pages to Config page * Adding IP address count to statistics * Move control of certain modals (files, flags, tags, hints) to challenges page * Expanding size of config page * Combining statistics and graphs pages * Moving percentage solved to the statistics page instead of the admin challenges page * Rename Keys.key_type to Keys.type (#459) (#478) * Rename keys.key_type to keys.type (#459) * Fixing previous migration to not be worried about key_type v type * Fixing loading of challenge type plugins * Switching from Handlebars to Nunjucks (#491) * Switching from Handlebars to Nunjucks * Allow admins to unlock hints before CTF begins and test that this is not allowed for regular users * Authed only (#492) * Adding authed_only decorator and adding next to url_for * Adding a basic preview to hints (#494) * Hints have a preview now for creating and updating hints. HTML and markdown are still allowed. * Ezq (#495) * Adding ezq as a simple wrapper around bootstrap modals * Use tabs not spaces and remove gray background on inputs * Adding title & draft to Pages. Making page preview open a new tab (#497) * Adding title & draft to Pages. * Making page preview open a new tab instead of render in the existing tab * Draft pages cannot be seen without a preview * Update check (#499) * Add update_check function * Notify user that a CTFd update is available in the admin panel * Adding update_check tests * Ratelimit (#500) * Implementing a ratelimit function * Fix error page formatting * Add rate limiting tests * Rate limit authentication functions and rate limit admin send email function * Load user solves before we load challenges to avoid unstyled buttons (#502) * Add a challenge preview (#503) * Adding a challenge preview to the admin panel * Change /admin/chals/<int:chalid> to /admin/chal/<int:chalid> * Adding codecov (#504) * Test coverage at https://codecov.io/gh/CTFd/CTFd * Sendmail improvements (#505) * Add get_smtp timeout, add sendmail error messages * Adding more error handling to sendmail * Adding Flask-Script (#507) * Pause ctf (#508) * Implement CTF pausing * Test CTF pausing * Fix loading challenges for users (#510) * Fix loading challenges for users * Temporarily switch themes in test * Pause help text (#509) * Adding pause help text * Pages authed (#511) * Adding authentication options to pages * Adding tests for accessing pages while draft & auth_required * Merging master into 1.1 (#513) * Name the core theme and remove the original themeselenium-screenshot-testing
parent
4c0ae9f3b5
commit
3af98b17d5
|
@ -16,3 +16,5 @@ before_script:
|
|||
script:
|
||||
- pep8 --ignore E501,E712 CTFd/ tests/
|
||||
- nosetests -d
|
||||
after_success:
|
||||
- codecov
|
||||
|
|
40
CHANGELOG.md
40
CHANGELOG.md
|
@ -1,3 +1,43 @@
|
|||
1.1.0 / TBD
|
||||
==================
|
||||
|
||||
**Themes**
|
||||
|
||||
* The original theme has been replaced by the core theme. The core theme is written in Bootstrap v4.0.0-beta.2 and significantly reduces the amount of custom styles/classes used.
|
||||
* Challenges can now be previewed from the admin panel.
|
||||
* The modals to modify files, flags, tags, and hints are no longer controlled by Challenge Type Plugins and are defined in CTFd itself.
|
||||
* The admin graphs and admin statistics pages have been combined.
|
||||
* Percentage solved for challenges has been moved to the new statistics page.
|
||||
* The scoregraph on the scoreboard has been cleaned up to better fit the page width.
|
||||
* Score graphs now use user-specific colors.
|
||||
* Hints can now be previewed from the admin panel.
|
||||
* Various confirmation modals have been replaced with `ezq.js`, a simple Bootstrap modal wrapper.
|
||||
* Fixed a bug where challenge buttons on the challenge board would load before being styled as solved.
|
||||
|
||||
**Database**
|
||||
|
||||
* `Keys.key_type` has been renamed to `Keys.type`.
|
||||
* Pages Improvements:
|
||||
* Page previews are now independent of the editor page.
|
||||
* Pages now have a title which refer to the link's name on the navbar.
|
||||
* Pages can now be drafts which cannot be seen by regular users.
|
||||
* Pages can now require authentication to view.
|
||||
* CSS editing has been moved to the config panel.
|
||||
|
||||
**Challenge Type Plugins**
|
||||
|
||||
* Handlebars has been replaced with Nunjucks which means Challenge Type Plugins using Handlebars must be updated to work with 1.1.0
|
||||
|
||||
**General**
|
||||
|
||||
* CTFs can now be paused to prevent solves.
|
||||
* A new authed_only decorator is available to restrict pages to logged-in users.
|
||||
* CTFd will now check for updates on start against `versioning.ctfd.io`. Admins will see in the admin panel that CTFd can be updated.
|
||||
* A ratelimit function has been implemented. Authentication and email related functions are now ratelimited.
|
||||
* Code coverage from codecov.
|
||||
* Admins can now see the reason why an email to a team failed to send.
|
||||
|
||||
|
||||
1.0.5 / 2017-10-25
|
||||
==================
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ from sqlalchemy_utils import database_exists, create_database
|
|||
from sqlalchemy_utils.functions import get_tables
|
||||
from six.moves import input
|
||||
|
||||
from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp
|
||||
from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp, update_check
|
||||
from CTFd import utils
|
||||
|
||||
# Hack to support Unicode in Python 2 properly
|
||||
|
@ -18,7 +18,7 @@ if sys.version_info[0] < 3:
|
|||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '1.0.5'
|
||||
__version__ = '1.1.0a1'
|
||||
|
||||
|
||||
class ThemeLoader(FileSystemLoader):
|
||||
|
@ -33,7 +33,7 @@ class ThemeLoader(FileSystemLoader):
|
|||
|
||||
# Check if the template requested is for the admin panel
|
||||
if template.startswith('admin/'):
|
||||
template = template.lstrip('admin/')
|
||||
template = template[6:] # Strip out admin/
|
||||
template = "/".join(['admin', 'templates', template])
|
||||
return super(ThemeLoader, self).get_source(environment, template)
|
||||
|
||||
|
@ -109,10 +109,13 @@ def create_app(config='CTFd.config.Config'):
|
|||
exit()
|
||||
|
||||
app.db = db
|
||||
app.VERSION = __version__
|
||||
|
||||
cache.init_app(app)
|
||||
app.cache = cache
|
||||
|
||||
update_check()
|
||||
|
||||
version = utils.get_config('ctf_version')
|
||||
|
||||
# Upgrading from an older version of CTFd
|
||||
|
@ -123,7 +126,7 @@ def create_app(config='CTFd.config.Config'):
|
|||
exit()
|
||||
|
||||
if not utils.get_config('ctf_theme'):
|
||||
utils.set_config('ctf_theme', 'original')
|
||||
utils.set_config('ctf_theme', 'core')
|
||||
|
||||
from CTFd.views import views
|
||||
from CTFd.challenges import challenges
|
||||
|
|
|
@ -10,7 +10,8 @@ from sqlalchemy.sql import not_
|
|||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, \
|
||||
DatabaseError
|
||||
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
|
||||
|
||||
from CTFd.admin.statistics import admin_statistics
|
||||
|
@ -22,14 +23,13 @@ from CTFd.admin.teams import admin_teams
|
|||
|
||||
from CTFd import utils
|
||||
|
||||
|
||||
admin = Blueprint('admin', __name__)
|
||||
|
||||
|
||||
@admin.route('/admin', methods=['GET'])
|
||||
def admin_view():
|
||||
if is_admin():
|
||||
return redirect(url_for('admin_statistics.admin_graphs'))
|
||||
return redirect(url_for('admin_statistics.admin_stats'))
|
||||
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
@ -107,63 +107,55 @@ def admin_config():
|
|||
freeze = int(request.form['freeze'])
|
||||
|
||||
try:
|
||||
view_challenges_unregistered = bool(request.form.get('view_challenges_unregistered', None))
|
||||
view_scoreboard_if_authed = bool(request.form.get('view_scoreboard_if_authed', None))
|
||||
hide_scores = bool(request.form.get('hide_scores', None))
|
||||
prevent_registration = bool(request.form.get('prevent_registration', None))
|
||||
prevent_name_change = bool(request.form.get('prevent_name_change', None))
|
||||
view_after_ctf = bool(request.form.get('view_after_ctf', None))
|
||||
verify_emails = bool(request.form.get('verify_emails', None))
|
||||
mail_tls = bool(request.form.get('mail_tls', None))
|
||||
mail_ssl = bool(request.form.get('mail_ssl', None))
|
||||
mail_useauth = bool(request.form.get('mail_useauth', None))
|
||||
workshop_mode = bool(request.form.get('workshop_mode', None))
|
||||
except (ValueError, TypeError):
|
||||
view_challenges_unregistered = None
|
||||
view_scoreboard_if_authed = None
|
||||
hide_scores = None
|
||||
prevent_registration = None
|
||||
prevent_name_change = None
|
||||
view_after_ctf = None
|
||||
verify_emails = None
|
||||
mail_tls = None
|
||||
mail_ssl = None
|
||||
mail_useauth = None
|
||||
workshop_mode = None
|
||||
# Set checkbox config values
|
||||
view_challenges_unregistered = 'view_challenges_unregistered' in request.form
|
||||
view_scoreboard_if_authed = 'view_scoreboard_if_authed' in request.form
|
||||
hide_scores = 'hide_scores' in request.form
|
||||
prevent_registration = 'prevent_registration' in request.form
|
||||
prevent_name_change = 'prevent_name_change' in request.form
|
||||
view_after_ctf = 'view_after_ctf' in request.form
|
||||
verify_emails = 'verify_emails' in request.form
|
||||
mail_tls = 'mail_tls' in request.form
|
||||
mail_ssl = 'mail_ssl' in request.form
|
||||
mail_useauth = 'mail_useauth' in request.form
|
||||
workshop_mode = 'workshop_mode' in request.form
|
||||
paused = 'paused' in request.form
|
||||
finally:
|
||||
view_challenges_unregistered = utils.set_config('view_challenges_unregistered', view_challenges_unregistered)
|
||||
view_scoreboard_if_authed = utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
|
||||
hide_scores = utils.set_config('hide_scores', hide_scores)
|
||||
prevent_registration = utils.set_config('prevent_registration', prevent_registration)
|
||||
prevent_name_change = utils.set_config('prevent_name_change', prevent_name_change)
|
||||
view_after_ctf = utils.set_config('view_after_ctf', view_after_ctf)
|
||||
verify_emails = utils.set_config('verify_emails', verify_emails)
|
||||
mail_tls = utils.set_config('mail_tls', mail_tls)
|
||||
mail_ssl = utils.set_config('mail_ssl', mail_ssl)
|
||||
mail_useauth = utils.set_config('mail_useauth', mail_useauth)
|
||||
workshop_mode = utils.set_config('workshop_mode', workshop_mode)
|
||||
utils.set_config('view_challenges_unregistered', view_challenges_unregistered)
|
||||
utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
|
||||
utils.set_config('hide_scores', hide_scores)
|
||||
utils.set_config('prevent_registration', prevent_registration)
|
||||
utils.set_config('prevent_name_change', prevent_name_change)
|
||||
utils.set_config('view_after_ctf', view_after_ctf)
|
||||
utils.set_config('verify_emails', verify_emails)
|
||||
utils.set_config('mail_tls', mail_tls)
|
||||
utils.set_config('mail_ssl', mail_ssl)
|
||||
utils.set_config('mail_useauth', mail_useauth)
|
||||
utils.set_config('workshop_mode', workshop_mode)
|
||||
utils.set_config('paused', paused)
|
||||
|
||||
mail_server = utils.set_config("mail_server", request.form.get('mail_server', None))
|
||||
mail_port = utils.set_config("mail_port", request.form.get('mail_port', None))
|
||||
utils.set_config("mail_server", request.form.get('mail_server', None))
|
||||
utils.set_config("mail_port", request.form.get('mail_port', None))
|
||||
|
||||
if request.form.get('mail_useauth', None) and (request.form.get('mail_u', None) or request.form.get('mail_p', None)):
|
||||
if len(request.form.get('mail_u')) > 0:
|
||||
mail_username = utils.set_config("mail_username", request.form.get('mail_u', None))
|
||||
utils.set_config("mail_username", request.form.get('mail_u', None))
|
||||
if len(request.form.get('mail_p')) > 0:
|
||||
mail_password = utils.set_config("mail_password", request.form.get('mail_p', None))
|
||||
utils.set_config("mail_password", request.form.get('mail_p', None))
|
||||
|
||||
elif request.form.get('mail_useauth', None) is None:
|
||||
utils.set_config("mail_username", None)
|
||||
utils.set_config("mail_password", None)
|
||||
|
||||
ctf_name = utils.set_config("ctf_name", request.form.get('ctf_name', None))
|
||||
ctf_theme = utils.set_config("ctf_theme", request.form.get('ctf_theme', None))
|
||||
utils.set_config("ctf_name", request.form.get('ctf_name', None))
|
||||
utils.set_config("ctf_theme", request.form.get('ctf_theme', None))
|
||||
utils.set_config('css', request.form.get('css', None))
|
||||
|
||||
mailfrom_addr = utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', 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))
|
||||
utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None))
|
||||
utils.set_config("mg_base_url", request.form.get('mg_base_url', None))
|
||||
utils.set_config("mg_api_key", request.form.get('mg_api_key', None))
|
||||
|
||||
db_freeze = utils.set_config("freeze", freeze)
|
||||
utils.set_config("freeze", freeze)
|
||||
|
||||
db_start = Config.query.filter_by(key='start').first()
|
||||
db_start.value = start
|
||||
|
@ -180,11 +172,13 @@ def admin_config():
|
|||
cache.clear()
|
||||
return redirect(url_for('admin.admin_config'))
|
||||
|
||||
with app.app_context():
|
||||
# Clear the cache so that we don't get stale values
|
||||
cache.clear()
|
||||
|
||||
ctf_name = utils.get_config('ctf_name')
|
||||
ctf_theme = utils.get_config('ctf_theme')
|
||||
hide_scores = utils.get_config('hide_scores')
|
||||
css = utils.get_config('css')
|
||||
|
||||
mail_server = utils.get_config('mail_server')
|
||||
mail_port = utils.get_config('mail_port')
|
||||
|
@ -211,6 +205,7 @@ def admin_config():
|
|||
verify_emails = utils.get_config('verify_emails')
|
||||
|
||||
workshop_mode = utils.get_config('workshop_mode')
|
||||
paused = utils.get_config('paused')
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
@ -218,9 +213,11 @@ def admin_config():
|
|||
themes = utils.get_themes()
|
||||
themes.remove(ctf_theme)
|
||||
|
||||
return render_template('admin/config.html',
|
||||
return render_template(
|
||||
'admin/config.html',
|
||||
ctf_name=ctf_name,
|
||||
ctf_theme_config=ctf_theme,
|
||||
css=css,
|
||||
start=start,
|
||||
end=end,
|
||||
freeze=freeze,
|
||||
|
@ -242,4 +239,6 @@ def admin_config():
|
|||
verify_emails=verify_emails,
|
||||
view_after_ctf=view_after_ctf,
|
||||
themes=themes,
|
||||
workshop_mode=workshop_mode)
|
||||
workshop_mode=workshop_mode,
|
||||
paused=paused
|
||||
)
|
||||
|
|
|
@ -33,18 +33,8 @@ def admin_chals():
|
|||
if request.method == 'POST':
|
||||
chals = Challenges.query.add_columns('id', 'type', 'name', 'value', 'description', 'category', 'hidden', 'max_attempts').order_by(Challenges.value).all()
|
||||
|
||||
teams_with_points = db.session.query(Solves.teamid).join(Teams).filter(
|
||||
Teams.banned == False).group_by(Solves.teamid).count()
|
||||
|
||||
json_data = {'game': []}
|
||||
for x in chals:
|
||||
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(
|
||||
Solves.chalid == x[1], Teams.banned == False).count()
|
||||
if teams_with_points > 0:
|
||||
percentage = (float(solve_count) / float(teams_with_points))
|
||||
else:
|
||||
percentage = 0.0
|
||||
|
||||
type_class = CHALLENGE_CLASSES.get(x.type)
|
||||
type_name = type_class.name if type_class else None
|
||||
|
||||
|
@ -58,7 +48,6 @@ def admin_chals():
|
|||
'max_attempts': x.max_attempts,
|
||||
'type': x.type,
|
||||
'type_name': type_name,
|
||||
'percentage_solved': percentage,
|
||||
'type_data': {
|
||||
'id': type_class.id,
|
||||
'name': type_class.name,
|
||||
|
@ -70,17 +59,23 @@ def admin_chals():
|
|||
db.session.close()
|
||||
return jsonify(json_data)
|
||||
else:
|
||||
return render_template('admin/chals.html')
|
||||
challenges = Challenges.query.all()
|
||||
return render_template('admin/challenges.html', challenges=challenges)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chals/<int:chalid>', methods=['GET', 'POST'])
|
||||
@admin_challenges.route('/admin/chal/<int:chalid>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_chal_detail(chalid):
|
||||
if request.method == 'POST':
|
||||
pass
|
||||
elif request.method == 'GET':
|
||||
chal = Challenges.query.filter_by(id=chalid).first_or_404()
|
||||
chal_class = get_chal_class(chal.type)
|
||||
|
||||
if request.method == 'POST':
|
||||
status, message = chal_class.attempt(chal, request)
|
||||
if status:
|
||||
return jsonify({'status': 1, 'message': message})
|
||||
else:
|
||||
return jsonify({'status': 0, 'message': message})
|
||||
elif request.method == 'GET':
|
||||
obj, data = chal_class.read(chal)
|
||||
return jsonify(data)
|
||||
|
||||
|
@ -211,11 +206,11 @@ def admin_get_values(chalid, prop):
|
|||
chal_keys = Keys.query.filter_by(chal=challenge.id).all()
|
||||
json_data = {'keys': []}
|
||||
for x in chal_keys:
|
||||
key_class = get_key_class(x.key_type)
|
||||
key_class = get_key_class(x.type)
|
||||
json_data['keys'].append({
|
||||
'id': x.id,
|
||||
'key': x.flag,
|
||||
'type': x.key_type,
|
||||
'type': x.type,
|
||||
'type_name': key_class.name,
|
||||
'templates': key_class.templates,
|
||||
})
|
||||
|
|
|
@ -35,13 +35,13 @@ def admin_keys_view(keyid):
|
|||
if request.method == 'GET':
|
||||
if keyid:
|
||||
saved_key = Keys.query.filter_by(id=keyid).first_or_404()
|
||||
key_class = get_key_class(saved_key.key_type)
|
||||
key_class = get_key_class(saved_key.type)
|
||||
json_data = {
|
||||
'id': saved_key.id,
|
||||
'key': saved_key.flag,
|
||||
'data': saved_key.data,
|
||||
'chal': saved_key.chal,
|
||||
'type': saved_key.key_type,
|
||||
'type': saved_key.type,
|
||||
'type_name': key_class.name,
|
||||
'templates': key_class.templates,
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ def admin_keys_view(keyid):
|
|||
k = Keys.query.filter_by(id=keyid).first()
|
||||
k.flag = flag
|
||||
k.data = data
|
||||
k.key_type = key_type
|
||||
k.type = key_type
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
|
||||
from CTFd.utils import admins_only, is_admin, cache
|
||||
from CTFd.utils import admins_only, is_admin, cache, markdown
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
|
||||
from CTFd import utils
|
||||
|
@ -7,65 +7,88 @@ from CTFd import utils
|
|||
admin_pages = Blueprint('admin_pages', __name__)
|
||||
|
||||
|
||||
@admin_pages.route('/admin/css', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_css():
|
||||
if request.method == 'POST':
|
||||
css = request.form['css']
|
||||
css = utils.set_config('css', css)
|
||||
with app.app_context():
|
||||
cache.clear()
|
||||
return '1'
|
||||
return '0'
|
||||
|
||||
|
||||
@admin_pages.route('/admin/pages', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_pages_view():
|
||||
route = request.args.get('route')
|
||||
page_id = request.args.get('id')
|
||||
page_op = request.args.get('operation')
|
||||
|
||||
if request.method == 'GET' and request.args.get('mode') == 'create':
|
||||
if request.method == 'GET' and page_op == 'preview':
|
||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||
return render_template('page.html', content=markdown(page.html))
|
||||
|
||||
if request.method == 'GET' and page_op == 'create':
|
||||
return render_template('admin/editor.html')
|
||||
|
||||
if route and request.method == 'GET':
|
||||
page = Pages.query.filter_by(route=route).first()
|
||||
if page_id and request.method == 'GET':
|
||||
page = Pages.query.filter_by(id=page_id).first()
|
||||
return render_template('admin/editor.html', page=page)
|
||||
|
||||
if request.method == 'POST':
|
||||
page_form_id = request.form.get('id')
|
||||
title = request.form['title']
|
||||
html = request.form['html']
|
||||
route = request.form['route'].lstrip('/')
|
||||
page = Pages.query.filter_by(route=route).first()
|
||||
auth_required = 'auth_required' in request.form
|
||||
|
||||
if page_op == 'preview':
|
||||
page = Pages(title, route, html, draft=False)
|
||||
return render_template('page.html', content=markdown(page.html))
|
||||
|
||||
page = Pages.query.filter_by(id=page_form_id).first()
|
||||
|
||||
errors = []
|
||||
if not route:
|
||||
errors.append('Missing URL route')
|
||||
|
||||
if errors:
|
||||
page = Pages(html, route)
|
||||
page = Pages(title, html, route)
|
||||
return render_template('/admin/editor.html', page=page)
|
||||
|
||||
if page:
|
||||
page.title = title
|
||||
page.route = route
|
||||
page.html = html
|
||||
page.auth_required = auth_required
|
||||
|
||||
if page_op == 'publish':
|
||||
page.draft = False
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
with app.app_context():
|
||||
|
||||
cache.clear()
|
||||
return redirect(url_for('admin_pages.admin_pages_view'))
|
||||
page = Pages(route, html)
|
||||
|
||||
return jsonify({
|
||||
'result': 'success',
|
||||
'operation': page_op
|
||||
})
|
||||
|
||||
if page_op == 'publish':
|
||||
page = Pages(title, route, html, draft=False, auth_required=auth_required)
|
||||
elif page_op == 'save':
|
||||
page = Pages(title, route, html, auth_required=auth_required)
|
||||
|
||||
db.session.add(page)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
with app.app_context():
|
||||
|
||||
cache.clear()
|
||||
return redirect(url_for('admin_pages.admin_pages_view'))
|
||||
|
||||
return jsonify({
|
||||
'result': 'success',
|
||||
'operation': page_op
|
||||
})
|
||||
|
||||
pages = Pages.query.all()
|
||||
return render_template('admin/pages.html', routes=pages, css=utils.get_config('css'))
|
||||
return render_template('admin/pages.html', pages=pages)
|
||||
|
||||
|
||||
@admin_pages.route('/admin/pages/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def delete_page():
|
||||
route = request.form['route']
|
||||
page = Pages.query.filter_by(route=route).first_or_404()
|
||||
id = request.form['id']
|
||||
page = Pages.query.filter_by(id=id).first_or_404()
|
||||
db.session.delete(page)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
|
|
@ -20,7 +20,9 @@ def admin_scoreboard_view():
|
|||
def admin_scores():
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
quickest = db.func.max(Solves.date).label('quickest')
|
||||
teams = db.session.query(Solves.teamid, Teams.name, score).join(Teams).join(Challenges).filter(Teams.banned == False).group_by(Solves.teamid).order_by(score.desc(), quickest)
|
||||
teams = db.session.query(Solves.teamid, Teams.name, score)\
|
||||
.join(Teams).join(Challenges).filter(Teams.banned == False)\
|
||||
.group_by(Solves.teamid).order_by(score.desc(), quickest)
|
||||
db.session.close()
|
||||
json_data = {'teams': []}
|
||||
for i, x in enumerate(teams):
|
||||
|
|
|
@ -7,12 +7,6 @@ from CTFd import utils
|
|||
admin_statistics = Blueprint('admin_statistics', __name__)
|
||||
|
||||
|
||||
@admin_statistics.route('/admin/graphs')
|
||||
@admins_only
|
||||
def admin_graphs():
|
||||
return render_template('admin/graphs.html')
|
||||
|
||||
|
||||
@admin_statistics.route('/admin/graphs/<graph_type>')
|
||||
@admins_only
|
||||
def admin_graph(graph_type):
|
||||
|
@ -32,15 +26,47 @@ def admin_graph(graph_type):
|
|||
for chal, count, name in solves:
|
||||
json_data[name] = count
|
||||
return jsonify(json_data)
|
||||
elif graph_type == "solve-percentages":
|
||||
chals = Challenges.query.add_columns('id', 'name', 'hidden', 'max_attempts').order_by(Challenges.value).all()
|
||||
|
||||
teams_with_points = db.session.query(Solves.teamid)\
|
||||
.join(Teams)\
|
||||
.filter(Teams.banned == False)\
|
||||
.group_by(Solves.teamid)\
|
||||
.count()
|
||||
|
||||
percentage_data = []
|
||||
for x in chals:
|
||||
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id)\
|
||||
.filter(Solves.chalid == x[1], Teams.banned == False)\
|
||||
.count()
|
||||
|
||||
if teams_with_points > 0:
|
||||
percentage = (float(solve_count) / float(teams_with_points))
|
||||
else:
|
||||
percentage = 0.0
|
||||
|
||||
percentage_data.append({
|
||||
'id': x.id,
|
||||
'name': x.name,
|
||||
'percentage': percentage,
|
||||
})
|
||||
|
||||
percentage_data = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True)
|
||||
json_data = {'percentages': percentage_data}
|
||||
return jsonify(json_data)
|
||||
|
||||
|
||||
@admin_statistics.route('/admin/statistics', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_stats():
|
||||
teams_registered = db.session.query(db.func.count(Teams.id)).first()[0]
|
||||
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
|
||||
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
|
||||
|
||||
wrong_count = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count()
|
||||
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
|
||||
|
||||
challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0]
|
||||
ip_count = db.session.query(db.func.count(Tracking.ip.distinct())).first()[0]
|
||||
|
||||
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
|
||||
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
|
||||
|
@ -61,13 +87,17 @@ def admin_stats():
|
|||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return render_template('admin/statistics.html', team_count=teams_registered,
|
||||
return render_template(
|
||||
'admin/statistics.html',
|
||||
team_count=teams_registered,
|
||||
ip_count=ip_count,
|
||||
wrong_count=wrong_count,
|
||||
solve_count=solve_count,
|
||||
challenge_count=challenge_count,
|
||||
solve_data=solve_data,
|
||||
most_solved=most_solved,
|
||||
least_solved=least_solved)
|
||||
least_solved=least_solved
|
||||
)
|
||||
|
||||
|
||||
@admin_statistics.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET'])
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
|
||||
from CTFd.utils import admins_only, is_admin, cache
|
||||
from CTFd.utils import admins_only, is_admin, cache, ratelimit
|
||||
from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from sqlalchemy.sql import not_
|
||||
|
@ -184,13 +184,21 @@ def admin_team(teamid):
|
|||
|
||||
@admin_teams.route('/admin/team/<int:teamid>/mail', methods=['POST'])
|
||||
@admins_only
|
||||
@ratelimit(method="POST", limit=10, interval=60)
|
||||
def email_user(teamid):
|
||||
message = request.form.get('msg', None)
|
||||
team = Teams.query.filter(Teams.id == teamid).first()
|
||||
if message and team:
|
||||
if utils.sendmail(team.email, message):
|
||||
return '1'
|
||||
return '0'
|
||||
msg = request.form.get('msg', None)
|
||||
team = Teams.query.filter(Teams.id == teamid).first_or_404()
|
||||
if msg and team:
|
||||
result, response = utils.sendmail(team.email, msg)
|
||||
return jsonify({
|
||||
'result': result,
|
||||
'message': response
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'result': False,
|
||||
'message': "Missing information"
|
||||
})
|
||||
|
||||
|
||||
@admin_teams.route('/admin/team/<int:teamid>/ban', methods=['POST'])
|
||||
|
@ -236,6 +244,7 @@ def delete_team(teamid):
|
|||
def admin_solves(teamid="all"):
|
||||
if teamid == "all":
|
||||
solves = Solves.query.all()
|
||||
awards = []
|
||||
else:
|
||||
solves = Solves.query.filter_by(teamid=teamid).all()
|
||||
awards = Awards.query.filter_by(teamid=teamid).all()
|
||||
|
|
15
CTFd/auth.py
15
CTFd/auth.py
|
@ -9,12 +9,14 @@ from passlib.hash import bcrypt_sha256
|
|||
|
||||
from CTFd.models import db, Teams
|
||||
from CTFd import utils
|
||||
from CTFd.utils import ratelimit
|
||||
|
||||
auth = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
@auth.route('/confirm', methods=['POST', 'GET'])
|
||||
@auth.route('/confirm/<data>', methods=['GET'])
|
||||
@ratelimit(method="POST", limit=10, interval=60)
|
||||
def confirm_user(data=None):
|
||||
if not utils.get_config('verify_emails'):
|
||||
# If the CTF doesn't care about confirming email addresses then redierct to challenges
|
||||
|
@ -75,6 +77,7 @@ def confirm_user(data=None):
|
|||
|
||||
@auth.route('/reset_password', methods=['POST', 'GET'])
|
||||
@auth.route('/reset_password/<data>', methods=['POST', 'GET'])
|
||||
@ratelimit(method="POST", limit=10, interval=60)
|
||||
def reset_password(data=None):
|
||||
logger = logging.getLogger('logins')
|
||||
if data is not None and request.method == "GET":
|
||||
|
@ -115,16 +118,8 @@ def reset_password(data=None):
|
|||
'reset_password.html',
|
||||
errors=['If that account exists you will receive an email, please check your inbox']
|
||||
)
|
||||
s = TimedSerializer(app.config['SECRET_KEY'])
|
||||
token = s.dumps(team.name)
|
||||
text = """
|
||||
Did you initiate a password reset?
|
||||
|
||||
{0}/{1}
|
||||
|
||||
""".format(url_for('auth.reset_password', _external=True), utils.base64encode(token, urlencode=True))
|
||||
|
||||
utils.sendmail(email, text)
|
||||
utils.forgot_password(email, team.name)
|
||||
|
||||
return render_template(
|
||||
'reset_password.html',
|
||||
|
@ -134,6 +129,7 @@ Did you initiate a password reset?
|
|||
|
||||
|
||||
@auth.route('/register', methods=['POST', 'GET'])
|
||||
@ratelimit(method="POST", limit=10, interval=5)
|
||||
def register():
|
||||
logger = logging.getLogger('regs')
|
||||
if not utils.can_register():
|
||||
|
@ -209,6 +205,7 @@ def register():
|
|||
|
||||
|
||||
@auth.route('/login', methods=['POST', 'GET'])
|
||||
@ratelimit(method="POST", limit=10, interval=5)
|
||||
def login():
|
||||
logger = logging.getLogger('logins')
|
||||
if request.method == 'POST':
|
||||
|
|
|
@ -18,7 +18,8 @@ challenges = Blueprint('challenges', __name__)
|
|||
|
||||
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
|
||||
def hints_view(hintid):
|
||||
if not utils.ctf_started():
|
||||
if utils.ctf_started() is False:
|
||||
if utils.is_admin() is False:
|
||||
abort(403)
|
||||
hint = Hints.query.filter_by(id=hintid).first_or_404()
|
||||
chal = Challenges.query.filter_by(id=hint.chal).first()
|
||||
|
@ -37,7 +38,7 @@ def hints_view(hintid):
|
|||
})
|
||||
elif request.method == 'POST':
|
||||
if unlock is None: # The user does not have an unlock.
|
||||
if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()):
|
||||
if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()) or utils.is_admin() is True:
|
||||
# It's ctftime or the CTF has ended (but we allow views after)
|
||||
team = Teams.query.filter_by(id=session['id']).first()
|
||||
if team.score() < hint.cost:
|
||||
|
@ -68,9 +69,12 @@ def hints_view(hintid):
|
|||
|
||||
@challenges.route('/challenges', methods=['GET'])
|
||||
def challenges_view():
|
||||
infos = []
|
||||
errors = []
|
||||
start = utils.get_config('start') or 0
|
||||
end = utils.get_config('end') or 0
|
||||
if utils.ctf_paused():
|
||||
infos.append('{} is paused'.format(utils.ctf_name()))
|
||||
if not utils.is_admin(): # User is not an admin
|
||||
if not utils.ctftime():
|
||||
# It is not CTF time
|
||||
|
@ -81,7 +85,7 @@ def challenges_view():
|
|||
errors.append('{} has not started yet'.format(utils.ctf_name()))
|
||||
if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf():
|
||||
errors.append('{} has ended'.format(utils.ctf_name()))
|
||||
return render_template('chals.html', errors=errors, start=int(start), end=int(end))
|
||||
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
||||
|
||||
if utils.get_config('verify_emails'):
|
||||
if utils.authed():
|
||||
|
@ -93,7 +97,7 @@ def challenges_view():
|
|||
errors.append('{} has not started yet'.format(utils.ctf_name()))
|
||||
if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf():
|
||||
errors.append('{} has ended'.format(utils.ctf_name()))
|
||||
return render_template('chals.html', errors=errors, start=int(start), end=int(end))
|
||||
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
||||
else:
|
||||
return redirect(url_for('auth.login', next='challenges'))
|
||||
|
||||
|
@ -311,6 +315,11 @@ def who_solved(chalid):
|
|||
|
||||
@challenges.route('/chal/<int:chalid>', methods=['POST'])
|
||||
def chal(chalid):
|
||||
if utils.ctf_paused():
|
||||
return jsonify({
|
||||
'status': 3,
|
||||
'message': '{} is paused'.format(utils.ctf_name())
|
||||
})
|
||||
if utils.ctf_ended() and not utils.view_after_ctf():
|
||||
abort(403)
|
||||
if not utils.user_can_view_challenges():
|
||||
|
|
|
@ -121,6 +121,11 @@ class Config(object):
|
|||
else:
|
||||
CACHE_TYPE = 'simple'
|
||||
|
||||
'''
|
||||
UPDATE_CHECK specifies whether or not CTFd will check whether or not there is a new version of CTFd
|
||||
'''
|
||||
UPDATE_CHECK = True
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
SECRET_KEY = 'AAAAAAAAAAAAAAAAAAAA'
|
||||
|
@ -129,3 +134,6 @@ class TestingConfig(Config):
|
|||
DEBUG = True
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://'
|
||||
SERVER_NAME = 'localhost'
|
||||
UPDATE_CHECK = False
|
||||
CACHE_REDIS_URL = None
|
||||
CACHE_TYPE = 'simple'
|
||||
|
|
|
@ -30,12 +30,18 @@ db = SQLAlchemy()
|
|||
|
||||
class Pages(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
route = db.Column(db.String(80), unique=True)
|
||||
auth_required = db.Column(db.Boolean)
|
||||
title = db.Column(db.String(80))
|
||||
route = db.Column(db.Text, unique=True)
|
||||
html = db.Column(db.Text)
|
||||
draft = db.Column(db.Boolean)
|
||||
|
||||
def __init__(self, route, html):
|
||||
def __init__(self, title, route, html, draft=True, auth_required=False):
|
||||
self.title = title
|
||||
self.route = route
|
||||
self.html = html
|
||||
self.draft = draft
|
||||
self.auth_required = auth_required
|
||||
|
||||
def __repr__(self):
|
||||
return "<Pages route {0}>".format(self.route)
|
||||
|
@ -131,14 +137,14 @@ class Files(db.Model):
|
|||
class Keys(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
key_type = db.Column(db.String(80))
|
||||
type = db.Column(db.String(80))
|
||||
flag = db.Column(db.Text)
|
||||
data = db.Column(db.Text)
|
||||
|
||||
def __init__(self, chal, flag, key_type):
|
||||
def __init__(self, chal, flag, type):
|
||||
self.chal = chal
|
||||
self.flag = flag
|
||||
self.key_type = key_type
|
||||
self.type = type
|
||||
|
||||
def __repr__(self):
|
||||
return "<Flag {0} for challenge {1}>".format(self.flag, self.chal)
|
||||
|
|
|
@ -9,11 +9,12 @@ from CTFd.utils import (
|
|||
admins_only as admins_only_wrapper,
|
||||
override_template as utils_override_template,
|
||||
register_plugin_script as utils_register_plugin_script,
|
||||
register_plugin_stylesheet as utils_register_plugin_stylesheet
|
||||
register_plugin_stylesheet as utils_register_plugin_stylesheet,
|
||||
pages as db_pages
|
||||
)
|
||||
|
||||
|
||||
Menu = namedtuple('Menu', ['name', 'route'])
|
||||
Menu = namedtuple('Menu', ['title', 'route'])
|
||||
ADMIN_PLUGIN_MENU_BAR = []
|
||||
USER_PAGE_MENU_BAR = []
|
||||
|
||||
|
@ -82,7 +83,7 @@ def register_plugin_stylesheet(*args, **kwargs):
|
|||
utils_register_plugin_stylesheet(*args, **kwargs)
|
||||
|
||||
|
||||
def register_admin_plugin_menu_bar(name, route):
|
||||
def register_admin_plugin_menu_bar(title, route):
|
||||
"""
|
||||
Registers links on the Admin Panel menubar/navbar
|
||||
|
||||
|
@ -90,7 +91,7 @@ def register_admin_plugin_menu_bar(name, route):
|
|||
:param route: A string that is the href used by the link
|
||||
:return:
|
||||
"""
|
||||
am = Menu(name=name, route=route)
|
||||
am = Menu(title=title, route=route)
|
||||
ADMIN_PLUGIN_MENU_BAR.append(am)
|
||||
|
||||
|
||||
|
@ -103,7 +104,7 @@ def get_admin_plugin_menu_bar():
|
|||
return ADMIN_PLUGIN_MENU_BAR
|
||||
|
||||
|
||||
def register_user_page_menu_bar(name, route):
|
||||
def register_user_page_menu_bar(title, route):
|
||||
"""
|
||||
Registers links on the User side menubar/navbar
|
||||
|
||||
|
@ -111,7 +112,7 @@ def register_user_page_menu_bar(name, route):
|
|||
:param route: A string that is the href used by the link
|
||||
:return:
|
||||
"""
|
||||
p = Menu(name=name, route=route)
|
||||
p = Menu(title=title, route=route)
|
||||
USER_PAGE_MENU_BAR.append(p)
|
||||
|
||||
|
||||
|
@ -121,7 +122,7 @@ def get_user_page_menu_bar():
|
|||
|
||||
:return: Returns a list of Menu namedtuples. They have name, and route attributes.
|
||||
"""
|
||||
return USER_PAGE_MENU_BAR
|
||||
return db_pages() + USER_PAGE_MENU_BAR
|
||||
|
||||
|
||||
def init_plugins(app):
|
||||
|
|
|
@ -14,10 +14,10 @@ class BaseChallenge(object):
|
|||
class CTFdStandardChallenge(BaseChallenge):
|
||||
id = "standard" # Unique identifier used to register challenges
|
||||
name = "standard" # Name of a challenge type
|
||||
templates = { # Handlebars templates used for each aspect of challenge editing & viewing
|
||||
'create': '/plugins/challenges/assets/standard-challenge-create.hbs',
|
||||
'update': '/plugins/challenges/assets/standard-challenge-update.hbs',
|
||||
'modal': '/plugins/challenges/assets/standard-challenge-modal.hbs',
|
||||
templates = { # Nunjucks templates used for each aspect of challenge editing & viewing
|
||||
'create': '/plugins/challenges/assets/standard-challenge-create.njk',
|
||||
'update': '/plugins/challenges/assets/standard-challenge-update.njk',
|
||||
'modal': '/plugins/challenges/assets/standard-challenge-modal.njk',
|
||||
}
|
||||
scripts = { # Scripts that are loaded when a template is loaded
|
||||
'create': '/plugins/challenges/assets/standard-challenge-create.js',
|
||||
|
@ -33,12 +33,10 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
:param request:
|
||||
:return:
|
||||
"""
|
||||
files = request.files.getlist('files[]')
|
||||
|
||||
# Create challenge
|
||||
chal = Challenges(
|
||||
name=request.form['name'],
|
||||
description=request.form['desc'],
|
||||
description=request.form['description'],
|
||||
value=request.form['value'],
|
||||
category=request.form['category'],
|
||||
type=request.form['chaltype']
|
||||
|
@ -63,6 +61,7 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
|
||||
db.session.commit()
|
||||
|
||||
files = request.files.getlist('files[]')
|
||||
for f in files:
|
||||
utils.upload_file(file=f, chalid=chal.id)
|
||||
|
||||
|
@ -105,7 +104,7 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
:return:
|
||||
"""
|
||||
challenge.name = request.form['name']
|
||||
challenge.description = request.form['desc']
|
||||
challenge.description = request.form['description']
|
||||
challenge.value = int(request.form.get('value', 0)) if request.form.get('value', 0) else 0
|
||||
challenge.max_attempts = int(request.form.get('max_attempts', 0)) if request.form.get('max_attempts', 0) else 0
|
||||
challenge.category = request.form['category']
|
||||
|
@ -146,7 +145,7 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
provided_key = request.form['key'].strip()
|
||||
chal_keys = Keys.query.filter_by(chal=chal.id).all()
|
||||
for chal_key in chal_keys:
|
||||
if get_key_class(chal_key.key_type).compare(chal_key.flag, provided_key):
|
||||
if get_key_class(chal_key.type).compare(chal_key.flag, provided_key):
|
||||
return True, 'Correct'
|
||||
return False, 'Incorrect'
|
||||
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
<div class="col-md-6 col-md-offset-3">
|
||||
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="name">Name
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Category
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
|
||||
<li role="presentation" class="active"><a href="#new-desc-write" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
|
||||
<li role="presentation"><a href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
|
||||
<div class="form-group">
|
||||
<label for="message-text" class="control-label">Message:
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
|
||||
</label>
|
||||
<textarea id="new-desc-editor" class="form-control" name="desc" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Value
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="This is how many points are rewarded for solving this challenge."></i>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="col-md-8">
|
||||
<label for="key">Key
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="This is the flag/solution for the challenge."></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="key" placeholder="Enter key">
|
||||
</div>
|
||||
<div class="form-vertical">
|
||||
<div class="col-md-2">
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="key_type[0]" value="static" checked>
|
||||
Static
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="key_type[0]" value="regex">
|
||||
Regex
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="col-md-9">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input name="hidden" type="checkbox">
|
||||
Hide challenge on creation
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="solve-attempts-checkbox">
|
||||
Limit amount of solve attempts
|
||||
</label>
|
||||
</div>
|
||||
<div id="solve-attempts-input" style="display: none;">
|
||||
<label for="max_attempts">Maximum Attempts
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="How many attempts should a user have for this challenge? For unlimited attempts, use the value 0"></i>
|
||||
</label>
|
||||
<input class="form-control" id='max_attempts' name='max_attempts' type='number' placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="col-md-9">
|
||||
<label>Upload Challenge Files
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="Challenges files are provided to users for download alongside your challenge description"></i>
|
||||
</label>
|
||||
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
|
||||
<input type="file" name="files[]" multiple="multiple">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<input type="hidden" value="standard" name="chaltype" id="chaltype">
|
||||
<div style="text-align:center">
|
||||
<button class="btn btn-theme btn-outlined create-challenge-submit" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,101 @@
|
|||
<div class="col-md-12">
|
||||
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="name">Name
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Category
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab" data-toggle="tab">Write</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
|
||||
<div class="form-group">
|
||||
<label for="message-text" class="control-label">Message:
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
|
||||
</label>
|
||||
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Value
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="This is how many points are rewarded for solving this challenge."></i>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Flag
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="This is the flag or solution for your challenge. You can choose whether your flag is a static string or a regular expression."></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="key" placeholder="Enter flag">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<select class="custom-select" name="key_type[0]">
|
||||
<option value="static">Static</option>
|
||||
<option value="regex">Regex</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<div class="checkbox">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" name="hidden" type="checkbox">
|
||||
Hide challenge on creation
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<div class="checkbox">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" id="solve-attempts-checkbox">
|
||||
Limit amount of solve attempts
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="solve-attempts-input" style="display: none;">
|
||||
<label for="max_attempts">Maximum Attempts
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="How many attempts should a user have for this challenge? For unlimited attempts, use the value 0"></i>
|
||||
</label>
|
||||
<input class="form-control" id='max_attempts' name='max_attempts' type='number' placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Upload Challenge Files
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="Challenges files are provided to users for download alongside your challenge description"></i>
|
||||
</label>
|
||||
<input class="form-control-file" type="file" name="files[]" multiple="multiple">
|
||||
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<input type="hidden" value="standard" name="chaltype" id="chaltype">
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -1,94 +0,0 @@
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content content">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#challenge" aria-controls="challenge" role="tab">Challenge</a></li>
|
||||
{{#if_eq solves '-1 Solves'}}
|
||||
{{else}}
|
||||
<li role="presentation"><a href="#solves" aria-controls="solves" class="chal-solves" role="tab">{{solves}}</a></li>
|
||||
{{/if_eq}}
|
||||
</ul>
|
||||
<div class="modal-body">
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade in active" id="challenge">
|
||||
<h3 class='chal-name text-center'>{{ name }}</h3>
|
||||
<h4 class="chal-value text-center">{{ value }}</h4>
|
||||
<div class="chal-tags text-center">
|
||||
{{#each tags }}
|
||||
<span class='label label-primary chal-tag'>{{this}}</span>
|
||||
{{/each}}
|
||||
</div>
|
||||
<span class="chal-desc">{{ desc }}</span>
|
||||
<div class="chal-hints file-row row">
|
||||
{{#each hints}}
|
||||
<div class='col-md-12 file-button-wrapper text-center'>
|
||||
<a onclick="javascript:loadhint({{this.id}})">
|
||||
{{#if this.hint }}
|
||||
<label class='challenge-wrapper file-wrapper hide-text'>
|
||||
View Hint
|
||||
</label>
|
||||
{{else}}
|
||||
{{#if this.cost}}
|
||||
<label class='challenge-wrapper file-wrapper hide-text'>
|
||||
Unlock Hint for {{this.cost}} points
|
||||
</label>
|
||||
{{else}}
|
||||
<label class='challenge-wrapper file-wrapper hide-text'>
|
||||
View Hint
|
||||
</label>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</a>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
<div class="chal-files file-row row text-center">
|
||||
{{#each files}}
|
||||
<div class='col-md-3 file-button-wrapper'>
|
||||
<a class='file-button' href='files/{{this}}'>
|
||||
<label class='challenge-wrapper file-wrapper hide-text'>
|
||||
{{splitSlash this}}
|
||||
</label>
|
||||
</a>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<div class="row submit-row">
|
||||
<div class="col-md-9" style="padding-right:0px;padding-left:10px;">
|
||||
<span class="input">
|
||||
<input class="input-field" type="text" name="answer" id="answer-input" placeholder="Key" />
|
||||
</span>
|
||||
|
||||
<input id="chal-id" type="hidden" value="{{id}}">
|
||||
</div>
|
||||
<div class="col-md-3 key-submit">
|
||||
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-theme btn-outlined pull-right" style="height:46.375px">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row notification-row">
|
||||
<div id="result-notification" class="alert alert-dismissable text-center" role="alert" style="display: none;">
|
||||
<strong id="result-message"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="solves">
|
||||
<table class="table table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Name</b>
|
||||
</td>
|
||||
<td><b>Date</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chal-solves-names">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,103 @@
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#challenge">Challenge</a>
|
||||
</li>
|
||||
{% if solves == '-1 Solves' %}
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link chal-solves" href="#solves">{{solves}}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
|
||||
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
|
||||
<h3 class="chal-value text-center">{{ value }}</h3>
|
||||
<div class="chal-tags text-center">
|
||||
{% for tag in tags %}
|
||||
<span class='badge badge-info chal-tag'>{{tag}}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="chal-desc">{{ desc }}</span>
|
||||
<div class="chal-hints hint-row row">
|
||||
{% for hint in hints %}
|
||||
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
|
||||
<a class="btn btn-info btn-hint" href="javascript:;" onclick="javascript:loadhint({{hint.id}})">
|
||||
{% if hint.hint %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% else %}
|
||||
{% if hint.cost %}
|
||||
<small>
|
||||
Unlock Hint for {{hint.cost}} points
|
||||
</small>
|
||||
{% else %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row chal-files text-center pb-3">
|
||||
{% for file in files %}
|
||||
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
|
||||
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate' href='files/{{file}}'>
|
||||
<small>
|
||||
{{ file.split('/')[1] }}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row submit-row">
|
||||
<div class="col-md-9 form-group">
|
||||
<input class="form-control" type="text" name="answer" id="answer-input" placeholder="Flag" />
|
||||
<input id="chal-id" type="hidden" value="{{id}}">
|
||||
</div>
|
||||
<div class="col-md-3 form-group key-submit">
|
||||
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-outline-secondary float-right">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row notification-row">
|
||||
<div class="col-md-12">
|
||||
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert" style="display: none;">
|
||||
<strong id="result-message"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="solves">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Name</b>
|
||||
</td>
|
||||
<td><b>Date</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chal-solves-names">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,267 +0,0 @@
|
|||
<div id="update-challenge" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3 class="chal-title text-center"></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ request.script_root }}/admin/chal/update">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Category
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
|
||||
<li role="presentation" class="active"><a href="#desc-write" id="desc-write-link" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
|
||||
<li role="presentation"><a href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="desc-write">
|
||||
<div class="form-group">
|
||||
<label for="message-text" class="control-label">Message:
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
|
||||
</label>
|
||||
<textarea id="desc-editor" class="form-control chal-desc" name="desc" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Value
|
||||
<i class="fa fa-question-circle gray-text" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" required>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox">
|
||||
Limit challenge attempts
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="chal-attempts-group" style="display:none;">
|
||||
<label for="value">Max Attempts</label>
|
||||
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value">
|
||||
</div>
|
||||
<input class="chal-id" type='hidden' name='id' placeholder='ID'>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="chal-hidden" name="hidden" type="checkbox">
|
||||
Hidden
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<a href="#" data-toggle="modal" data-target="#update-tags" class="btn btn-primary">Tags</a>
|
||||
<a href="#" data-toggle="modal" data-target="#update-files" class="btn btn-primary">Files</a>
|
||||
<a href="#" data-toggle="modal" data-target="#update-hints" class="btn btn-primary">Hints</a>
|
||||
<a href="#" data-toggle="modal" data-target="#update-keys" class="btn btn-primary">Keys</a>
|
||||
<a href="#" data-toggle="modal" data-target="#delete-chal" class="btn btn-danger">Delete</a>
|
||||
</div>
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<div style="text-align:center">
|
||||
<button class="btn btn-theme btn-outlined update-challenge-submit" type="submit">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="update-tags" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3>Tags</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="tag-insert">Value</label>
|
||||
<input max-length="80" type="text" class="form-control tag-insert" name="tag-insert" placeholder="Type tag and press Enter">
|
||||
</div>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="tags-chal" name='chal' type='hidden'>
|
||||
|
||||
<div id="current-tags">
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
<div id="chal-tags">
|
||||
</div>
|
||||
<div class="row" style="text-align:center;margin-top:20px">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button class="btn btn-theme btn-outlined" id="submit-tags">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="update-files" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3>Files</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ request.script_root }}/admin/files/" enctype="multipart/form-data">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="files-chal" name='chal' type='hidden'>
|
||||
<input name='method' type='hidden' value='upload'>
|
||||
|
||||
<div id="current-files"></div>
|
||||
<input type="hidden" name="method" value="upload">
|
||||
<input type="file" name="files[]" multiple="multiple">
|
||||
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
|
||||
<div class="row" style="text-align:center;margin-top:20px">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button class="btn btn-theme btn-outlined" id="submit-files">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="update-hints" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3>Hints</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row" style="text-align:center">
|
||||
<a href="#" id="create-hint" class="btn btn-primary" style="margin-bottom:15px;">New Hint</a>
|
||||
</div>
|
||||
<div class='current-hints'>
|
||||
<table id="hintsboard" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Hint</b></td>
|
||||
<td class="text-center"><b>Cost</b></td>
|
||||
<td class="text-center"><b>Settings</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="text-center">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="update-keys" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3>Keys</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ request.script_root }}/admin/keys" style="text-align:center">
|
||||
<a href="#" id="create-key" class="btn btn-primary" style="margin-bottom:15px;">New Key</a>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="keys-chal" name='chal' type='hidden'>
|
||||
<div id="current-keys" class="row"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="delete-chal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3>Delete Challenge</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ request.script_root }}/admin/chal/delete">
|
||||
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<input type="hidden" name="id" class="chal-id">
|
||||
<div class="small-6 small-centered text-center columns">
|
||||
<p>Are you sure you want to delete this challenge?</p>
|
||||
<p>Solves, wrong keys, files, tags will all be deleted.</p>
|
||||
<a onclick="$('#delete-chal').modal('hide')" class="btn btn-primary">No</a>
|
||||
<button class="btn btn-danger" id="delete-user" type="submit">Delete</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="create-keys" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<input type="hidden" class="chal-id" name="chal-id">
|
||||
<div class="modal-header text-center">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3>Create Key</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="create-keys-select-div">
|
||||
<label for="create-keys-select" class="control-label">Choose Key Type</label>
|
||||
<select class="form-control" id="create-keys-select">
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<div id="create-keys-entry-div">
|
||||
</div>
|
||||
<br>
|
||||
<div style="text-align:center;display:none;" id="create-keys-button-div">
|
||||
<button id="create-keys-submit" class="btn btn-theme btn-outlined">Create Key</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="edit-keys" class="modal fade" tabindex="-1">
|
||||
</div>
|
||||
|
||||
<div id="hint-modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3 id="hint-modal-title"></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="hint-modal-submit" method='POST'>
|
||||
<input type="hidden" class="chal-id" name="chal">
|
||||
<div class="form-group">
|
||||
<textarea id="hint-modal-hint" type="text" class="form-control" name="hint" placeholder="Hint"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input id="hint-modal-cost" type="number" class="form-control" name="cost" placeholder="Cost">
|
||||
</div>
|
||||
<div class="row" style="text-align:center;margin-top:20px">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button class="btn btn-theme btn-outlined" id="hint-modal-button">Add Hint</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,232 +1,13 @@
|
|||
//http://stackoverflow.com/a/2648463 - wizardry!
|
||||
String.prototype.format = String.prototype.f = function() {
|
||||
var s = this,
|
||||
i = arguments.length;
|
||||
|
||||
while (i--) {
|
||||
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
function load_hint_modal(method, hintid){
|
||||
$('#hint-modal-hint').val('');
|
||||
$('#hint-modal-cost').val('');
|
||||
if (method == 'create'){
|
||||
$('#hint-modal-submit').attr('action', '/admin/hints');
|
||||
$('#hint-modal-title').text('Create Hint');
|
||||
$("#hint-modal").modal();
|
||||
} else if (method == 'update'){
|
||||
$.get(script_root + '/admin/hints/' + hintid, function(data){
|
||||
$('#hint-modal-submit').attr('action', '/admin/hints/' + hintid);
|
||||
$('#hint-modal-hint').val(data.hint);
|
||||
$('#hint-modal-cost').val(data.cost);
|
||||
$('#hint-modal-title').text('Update Hint');
|
||||
$("#hint-modal-button").text('Update Hint');
|
||||
$("#hint-modal").modal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function submitkey(chal, key) {
|
||||
$.post(script_root + "/admin/chal/" + chal, {
|
||||
key: key,
|
||||
nonce: $('#nonce').val()
|
||||
}, function (data) {
|
||||
alert(data)
|
||||
})
|
||||
}
|
||||
|
||||
function create_key(chal, key, key_type) {
|
||||
$.post(script_root + "/admin/keys", {
|
||||
chal: chal,
|
||||
key: key,
|
||||
key_type: key_type,
|
||||
nonce: $('#nonce').val()
|
||||
}, function (data) {
|
||||
if (data == "1"){
|
||||
loadkeys(chal);
|
||||
$("#create-keys").modal('toggle');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadkeys(chal){
|
||||
$.get(script_root + '/admin/chal/' + chal + '/keys', function(data){
|
||||
$('#keys-chal').val(chal);
|
||||
keys = $.parseJSON(JSON.stringify(data));
|
||||
keys = keys['keys'];
|
||||
$('#current-keys').empty();
|
||||
$.get(script_root + "/themes/admin/static/js/templates/admin-keys-table.hbs", function(data){
|
||||
var template = Handlebars.compile(data);
|
||||
var wrapper = {keys: keys, script_root: script_root};
|
||||
$('#current-keys').append(template(wrapper));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updatekeys(){
|
||||
keys = [];
|
||||
vals = [];
|
||||
chal = $('#keys-chal').val()
|
||||
$('.current-key').each(function(){
|
||||
keys.push($(this).val());
|
||||
})
|
||||
$('#current-keys input[name*="key_type"]:checked').each(function(){
|
||||
vals.push($(this).val());
|
||||
})
|
||||
$.post(script_root + '/admin/keys/'+chal, {'keys':keys, 'vals':vals, 'nonce': $('#nonce').val()})
|
||||
loadchal(chal, true)
|
||||
$('#update-keys').modal('hide');
|
||||
}
|
||||
|
||||
|
||||
function deletekey(key_id){
|
||||
$.post(script_root + '/admin/keys/'+key_id+'/delete', {'nonce': $('#nonce').val()}, function(data){
|
||||
if (data == "1") {
|
||||
$('tr[name={0}]'.format(key_id)).remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatekey(){
|
||||
var key_id = $('#key-id').val();
|
||||
var chal = $('#keys-chal').val();
|
||||
var key_data = $('#key-data').val();
|
||||
var key_type = $('#key-type').val();
|
||||
var nonce = $('#nonce').val();
|
||||
$.post(script_root + '/admin/keys/'+key_id, {
|
||||
'chal':chal,
|
||||
'key':key_data,
|
||||
'key_type': key_type,
|
||||
'nonce': nonce
|
||||
}, function(data){
|
||||
if (data == "1") {
|
||||
loadkeys(chal);
|
||||
$('#edit-keys').modal('toggle');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadtags(chal){
|
||||
$('#tags-chal').val(chal)
|
||||
$('#current-tags').empty()
|
||||
$('#chal-tags').empty()
|
||||
$.get(script_root + '/admin/tags/'+chal, function(data){
|
||||
tags = $.parseJSON(JSON.stringify(data))
|
||||
tags = tags['tags']
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
tag = "<span class='label label-primary chal-tag'><span>"+tags[i].tag+"</span><a name='"+tags[i].id+"'' class='delete-tag'>×</a></span>"
|
||||
$('#current-tags').append(tag)
|
||||
};
|
||||
$('.delete-tag').click(function(e){
|
||||
deletetag(e.target.name)
|
||||
$(e.target).parent().remove()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deletetag(tagid){
|
||||
$.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()});
|
||||
}
|
||||
// function deletechal(chalid){
|
||||
// $.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid});
|
||||
// }
|
||||
|
||||
|
||||
function edithint(hintid){
|
||||
$.get(script_root + '/admin/hints/' + hintid, function(data){
|
||||
console.log(data);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function deletehint(hintid){
|
||||
$.delete(script_root + '/admin/hints/' + hintid, function(data, textStatus, jqXHR){
|
||||
if (jqXHR.status == 204){
|
||||
var chalid = $('.chal-id').val();
|
||||
loadhints(chalid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadhints(chal){
|
||||
$.get(script_root + '/admin/chal/{0}/hints'.format(chal), function(data){
|
||||
var table = $('#hintsboard > tbody');
|
||||
table.empty();
|
||||
for (var i = 0; i < data.hints.length; i++) {
|
||||
var hint = data.hints[i]
|
||||
var hint_row = "<tr>" +
|
||||
"<td class='hint-entry'>{0}</td>".format(hint.hint) +
|
||||
"<td class='hint-cost'>{0}</td>".format(hint.cost) +
|
||||
"<td class='hint-settings'><span>" +
|
||||
"<i role='button' class='fa fa-pencil-square-o' onclick=javascript:load_hint_modal('update',{0})></i>".format(hint.id)+
|
||||
"<i role='button' class='fa fa-times' onclick=javascript:deletehint({0})></i>".format(hint.id)+
|
||||
"</span></td>" +
|
||||
"</tr>";
|
||||
table.append(hint_row);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function deletechal(chalid){
|
||||
$.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid});
|
||||
}
|
||||
|
||||
function updatetags(){
|
||||
tags = [];
|
||||
chal = $('#tags-chal').val()
|
||||
$('#chal-tags > span > span').each(function(i, e){
|
||||
tags.push($(e).text())
|
||||
});
|
||||
$.post(script_root + '/admin/tags/'+chal, {'tags':tags, 'nonce': $('#nonce').val()});
|
||||
$('#update-tags').modal('toggle');
|
||||
}
|
||||
|
||||
function updatefiles(){
|
||||
chal = $('#files-chal').val();
|
||||
var form = $('#update-files form')[0];
|
||||
var formData = new FormData(form);
|
||||
$.ajax({
|
||||
url: script_root + '/admin/files/'+chal,
|
||||
data: formData,
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function(data){
|
||||
form.reset();
|
||||
loadfiles(chal);
|
||||
$('#update-files').modal('hide');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadfiles(chal){
|
||||
$('#update-files form').attr('action', script_root+'/admin/files/'+chal)
|
||||
$.get(script_root + '/admin/files/' + chal, function(data){
|
||||
$('#files-chal').val(chal)
|
||||
files = $.parseJSON(JSON.stringify(data));
|
||||
files = files['files']
|
||||
$('#current-files').empty()
|
||||
for(x=0; x<files.length; x++){
|
||||
filename = files[x].file.split('/')
|
||||
filename = filename[filename.length - 1]
|
||||
$('#current-files').append('<div class="row" style="margin:5px 0px;">'+'<a style="position:relative;top:10px;" href='+script_root+'/files/'+files[x].file+'>'+filename+'</a><a href="#" class="btn btn-danger" onclick="deletefile('+chal+','+files[x].id+', $(this))" value="'+files[x].id+'" style="float:right;">Delete</a></div>')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletefile(chal, file, elem){
|
||||
$.post(script_root + '/admin/files/' + chal,{
|
||||
'nonce': $('#nonce').val(),
|
||||
'method': 'delete',
|
||||
'file': file
|
||||
}, function (data){
|
||||
if (data == "1") {
|
||||
elem.parent().remove()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('#submit-key').click(function (e) {
|
||||
submitkey($('#chalid').val(), $('#answer').val())
|
||||
|
@ -237,42 +18,9 @@ $('#submit-keys').click(function (e) {
|
|||
$('#update-keys').modal('hide');
|
||||
});
|
||||
|
||||
$('#submit-tags').click(function (e) {
|
||||
e.preventDefault();
|
||||
updatetags()
|
||||
});
|
||||
|
||||
$('#submit-files').click(function (e) {
|
||||
e.preventDefault();
|
||||
updatefiles()
|
||||
});
|
||||
|
||||
$('#delete-chal form').submit(function(e){
|
||||
e.preventDefault();
|
||||
$.post(script_root + '/admin/chal/delete', $(this).serialize(), function(data){
|
||||
console.log(data)
|
||||
if (data){
|
||||
loadchals();
|
||||
}
|
||||
else {
|
||||
alert('There was an error');
|
||||
}
|
||||
})
|
||||
$("#delete-chal").modal("hide");
|
||||
$("#update-challenge").modal("hide");
|
||||
});
|
||||
|
||||
$(".tag-insert").keyup(function (e) {
|
||||
if (e.keyCode == 13) {
|
||||
tag = $('.tag-insert').val()
|
||||
tag = tag.replace(/'/g, '');
|
||||
if (tag.length > 0){
|
||||
tag = "<span class='label label-primary chal-tag'><span>"+tag+"</span><a class='delete-tag' onclick='$(this).parent().remove()'>×</a></span>"
|
||||
$('#chal-tags').append(tag)
|
||||
}
|
||||
$('.tag-insert').val("")
|
||||
}
|
||||
});
|
||||
|
||||
$('#limit_max_attempts').change(function() {
|
||||
if(this.checked) {
|
||||
|
@ -295,67 +43,11 @@ $('#new-desc-edit').on('shown.bs.tab', function (event) {
|
|||
}
|
||||
});
|
||||
|
||||
// Open New Challenge modal when New Challenge button is clicked
|
||||
// $('.create-challenge').click(function (e) {
|
||||
// $('#create-challenge').modal();
|
||||
// });
|
||||
|
||||
|
||||
$('#create-key').click(function(e){
|
||||
$.get(script_root + '/admin/key_types', function(data){
|
||||
$("#create-keys-select").empty();
|
||||
var option = "<option> -- </option>";
|
||||
$("#create-keys-select").append(option);
|
||||
for (var key in data){
|
||||
var option = "<option value='{0}'>{1}</option>".format(key, data[key]);
|
||||
$("#create-keys-select").append(option);
|
||||
}
|
||||
$("#create-keys").modal();
|
||||
});
|
||||
});
|
||||
|
||||
$('#create-keys-select').change(function(){
|
||||
var key_type_name = $(this).find("option:selected").text();
|
||||
|
||||
$.get(script_root + '/admin/key_types/' + key_type_name, function(key_data){
|
||||
$.get(script_root + key_data.templates.create, function(template_data){
|
||||
var template = Handlebars.compile(template_data);
|
||||
$("#create-keys-entry-div").html(template());
|
||||
$("#create-keys-button-div").show();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
$('#create-keys-submit').click(function (e) {
|
||||
e.preventDefault();
|
||||
var chalid = $('#create-keys').find('.chal-id').val();
|
||||
var key_data = $('#create-keys').find('input[name=key]').val();
|
||||
var key_type = $('#create-keys-select').val();
|
||||
create_key(chalid, key_data, key_type);
|
||||
});
|
||||
|
||||
|
||||
$('#create-hint').click(function(e){
|
||||
e.preventDefault();
|
||||
load_hint_modal('create');
|
||||
});
|
||||
|
||||
$('#hint-modal-submit').submit(function (e) {
|
||||
e.preventDefault();
|
||||
var params = {}
|
||||
$(this).serializeArray().map(function(x){
|
||||
params[x.name] = x.value;
|
||||
});
|
||||
$.post(script_root + $(this).attr('action'), params, function(data){
|
||||
loadhints(params['chal']);
|
||||
});
|
||||
$("#hint-modal").modal('hide');
|
||||
});
|
||||
|
||||
function loadchal(id, update) {
|
||||
$.get(script_root + '/admin/chals/' + id, function(obj){
|
||||
$('#desc-write-link').click() // Switch to Write tab
|
||||
$.get(script_root + '/admin/chal/' + id, function(obj){
|
||||
$('#desc-write-link').click(); // Switch to Write tab
|
||||
$('.chal-title').text(obj.name);
|
||||
$('.chal-name').val(obj.name);
|
||||
$('.chal-desc').val(obj.description);
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
<div id="update-challenge" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="chal-title text-center"></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ request.script_root }}/admin/chal/update">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Category
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home" role="tab" data-toggle="tab">
|
||||
Write
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
|
||||
Preview
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="desc-write">
|
||||
<div class="form-group">
|
||||
<label for="message-text" class="control-label">Message:
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
|
||||
</label>
|
||||
<textarea id="desc-editor" class="form-control chal-desc" name="description" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Value
|
||||
<i class="fa fa-question-circle text-muted" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" required>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox">
|
||||
Limit challenge attempts
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="chal-attempts-group" style="display:none;">
|
||||
<label for="value">Max Attempts</label>
|
||||
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value">
|
||||
</div>
|
||||
<input class="chal-id" type='hidden' name='id' placeholder='ID'>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="chal-hidden" name="hidden" type="checkbox">
|
||||
Hidden
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<div style="text-align:center">
|
||||
<button class="btn btn-success btn-outlined update-challenge-submit" type="submit">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -18,9 +18,9 @@ class BaseKey(object):
|
|||
class CTFdStaticKey(BaseKey):
|
||||
id = 0
|
||||
name = "static"
|
||||
templates = { # Handlebars templates used for key editing & viewing
|
||||
'create': '/plugins/keys/assets/static/create-static-modal.hbs',
|
||||
'update': '/plugins/keys/assets/static/edit-static-modal.hbs',
|
||||
templates = { # Nunjucks templates used for key editing & viewing
|
||||
'create': '/plugins/keys/assets/static/create-static-modal.njk',
|
||||
'update': '/plugins/keys/assets/static/edit-static-modal.njk',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -36,9 +36,9 @@ class CTFdStaticKey(BaseKey):
|
|||
class CTFdRegexKey(BaseKey):
|
||||
id = 1
|
||||
name = "regex"
|
||||
templates = { # Handlebars templates used for key editing & viewing
|
||||
'create': '/plugins/keys/assets/regex/create-regex-modal.hbs',
|
||||
'update': '/plugins/keys/assets/regex/edit-regex-modal.hbs',
|
||||
templates = { # Nunjucks templates used for key editing & viewing
|
||||
'create': '/plugins/keys/assets/regex/create-regex-modal.njk',
|
||||
'update': '/plugins/keys/assets/regex/edit-regex-modal.njk',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -1,17 +1,26 @@
|
|||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<h3>Regex Key</h3>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Regex Key</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}" style="text-align:center">
|
||||
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
|
||||
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
|
||||
<input type="hidden" id="key-type" name="key_type" value="regex">
|
||||
<input type="hidden" id="key-id">
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button id="submit-keys" class="btn btn-theme btn-outlined">Update</button>
|
||||
<button id="submit-keys" class="btn btn-success float-right">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -1,17 +1,26 @@
|
|||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<h3>Static Key</h3>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Static Key</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}" style="text-align:center">
|
||||
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
|
||||
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter static key data">
|
||||
<input type="hidden" id="key-type" name="key_type" value="static">
|
||||
<input type="hidden" id="key-id">
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button id="submit-keys" class="btn btn-theme btn-outlined">Update</button>
|
||||
<button id="submit-keys" class="btn btn-success float-right">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -0,0 +1,144 @@
|
|||
html, body, .container {
|
||||
font-family: 'Lato', 'LatoOffline', sans-serif;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
font-family: 'Raleway', 'RalewayOffline', sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #337ab7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table > thead > tr > td {
|
||||
/* Remove border line from thead of all tables */
|
||||
/* It can overlap with other element styles */
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.fa.spinner {
|
||||
margin-top: 225px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.spinner-error {
|
||||
padding-top: 20vh;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
/*background-color: #343a40;*/
|
||||
/*color: #FFF;*/
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
position: relative;
|
||||
display: block;
|
||||
/*padding: 0.8em;*/
|
||||
border-radius: 0;
|
||||
/*background: #f0f0f0;*/
|
||||
/*color: #aaa;*/
|
||||
font-weight: 400;
|
||||
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: transparent;
|
||||
border-color: #a3d39c;
|
||||
box-shadow: 0 0 0 0.2rem #a3d39c;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-filled-valid {
|
||||
background-color: transparent;
|
||||
border-color: #a3d39c;
|
||||
box-shadow: 0 0 0 0.2rem #a3d39c;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-outlined.btn-theme {
|
||||
background: none;
|
||||
color: #545454;
|
||||
border-color: #545454;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.btn-outlined {
|
||||
border-radius: 0;
|
||||
-webkit-transition: all 0.3s;
|
||||
-moz-transition: all 0.3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn {
|
||||
letter-spacing: 1px;
|
||||
text-decoration: none;
|
||||
-moz-user-select: none;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #5B7290 !important;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 0 !important;
|
||||
padding: 0.8em;
|
||||
}
|
||||
|
||||
#score-graph {
|
||||
height: 450px;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#solves-graph {
|
||||
display: block;
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
#keys-pie-graph {
|
||||
height: 400px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#categories-pie-graph {
|
||||
height: 400px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#solve-percentages-graph {
|
||||
height: 400px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.no-decoration {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.no-decoration:hover {
|
||||
color: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
.chal-desc {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0px;
|
||||
max-width: 1000px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #5B7290 !important;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: #5B7290 !important;
|
||||
}
|
||||
|
||||
.btn-file:nth-child(1):before {
|
||||
content: "\f0ed";
|
||||
display: inline-block;
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.solved-challenge {
|
||||
background-color: #37d63e !important;
|
||||
opacity: 0.4;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.solved-challenge p:after {
|
||||
content: "\f00c";
|
||||
display: inline-block;
|
||||
font: normal normal normal 18px/1 FontAwesome;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin-top: -10px;
|
||||
margin-right: 25px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.key-submit .btn {
|
||||
height: 51px;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/* Move down content because we have a fixed navbar that is 3.5rem tall */
|
||||
body {
|
||||
padding-top: 3.5rem;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/* Sticky footer styles
|
||||
-------------------------------------------------- */
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 60px; /* Margin bottom by footer height */
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px; /* Set the fixed height of the footer here */
|
||||
line-height: 60px; /* Vertically center the text there */
|
||||
/*background-color: #f5f5f5;*/
|
||||
}
|
|
@ -18,6 +18,10 @@ a {
|
|||
outline: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.body-container {
|
||||
min-height: 100%;
|
||||
position: relative;
|
||||
|
@ -106,6 +110,12 @@ table{
|
|||
line-height: 66px;
|
||||
}
|
||||
|
||||
#score-graph{
|
||||
height: 450px;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.fa.spinner {
|
||||
margin-top: 225px;
|
||||
text-align: center;
|
||||
|
@ -113,16 +123,11 @@ table{
|
|||
}
|
||||
|
||||
.spinner-error {
|
||||
padding-top: 20vh;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
#score-graph{
|
||||
height: 450px;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#keys-pie-graph{
|
||||
width: 50%;
|
||||
float: left;
|
||||
|
@ -385,118 +390,6 @@ table{
|
|||
margin: 0px;
|
||||
}
|
||||
|
||||
/* END OF INPUT CSS */
|
||||
|
||||
/* ADMIN CSS */
|
||||
|
||||
#submit-key{
|
||||
display: none;
|
||||
}
|
||||
|
||||
#chal > h1{
|
||||
text-align: center
|
||||
}
|
||||
|
||||
#chal > form{
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#chal > form > h3,h4{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#chal > form > input{
|
||||
display: none;
|
||||
}
|
||||
|
||||
table{
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*Not sure why foundation needs these two...*/
|
||||
.top-bar input{
|
||||
height: auto;
|
||||
padding-top: 0.35rem;
|
||||
padding-bottom: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.top-bar .button{
|
||||
padding-top: 0.45rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.dropdown{
|
||||
background-color: #333 !important;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown button{
|
||||
padding-top: 0.45rem;
|
||||
padding-bottom: 0.35rem;
|
||||
margin-bottom: 0;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
#challenges button{
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.row h1{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.textbox{
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.chal-tag{
|
||||
margin: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
#score-graph{
|
||||
clear:both;
|
||||
}
|
||||
|
||||
#keys-pie-graph{
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
#categories-pie-graph{
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.chal-button{
|
||||
width: 150px;
|
||||
color: white;
|
||||
border: None;
|
||||
}
|
||||
|
||||
.chal-button > .chal-name {
|
||||
margin-bottom: 0px;
|
||||
height: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.chal-button > .chal-points {
|
||||
margin: -20px 0 0 0;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.chal-button > .chal-percent{
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.remove-key{
|
||||
float:right;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
@ -506,6 +399,4 @@ table{
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.gray-text{
|
||||
color: lightgray;
|
||||
}
|
||||
/* END OF INPUT CSS */
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,98 +0,0 @@
|
|||
//http://stackoverflow.com/a/2648463 - wizardry!
|
||||
String.prototype.format = String.prototype.f = function() {
|
||||
var s = this,
|
||||
i = arguments.length;
|
||||
|
||||
while (i--) {
|
||||
s = s.replace(new RegExp('\\{' + i + '\\}', 'gm'), arguments[i]);
|
||||
}
|
||||
return s;
|
||||
};
|
||||
|
||||
//http://stackoverflow.com/a/7616484
|
||||
String.prototype.hashCode = function() {
|
||||
var hash = 0, i, chr, len;
|
||||
if (this.length == 0) return hash;
|
||||
for (i = 0, len = this.length; i < len; i++) {
|
||||
chr = this.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + chr;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
};
|
||||
|
||||
function load_edit_key_modal(key_id, key_type_name) {
|
||||
$.get(script_root + '/admin/keys/' + key_id, function(key_data){
|
||||
$.get(script_root + key_data.templates.update, function(template_data){
|
||||
$('#edit-keys').empty();
|
||||
var template = Handlebars.compile(template_data);
|
||||
key_data['script_root'] = script_root;
|
||||
key_data['nonce'] = $('#nonce').val();
|
||||
$('#edit-keys').append(template(key_data));
|
||||
$('#key-id').val(key_id);
|
||||
$('#submit-keys').click(function (e) {
|
||||
e.preventDefault();
|
||||
updatekey()
|
||||
});
|
||||
$('#edit-keys').modal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function load_chal_template(id, success_cb){
|
||||
var obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[0];
|
||||
$.get(script_root + obj.type_data.templates.update, function(template_data){
|
||||
var template = Handlebars.compile(template_data);
|
||||
$("#update-modals-entry-div").html(template({'nonce':$('#nonce').val(), 'script_root':script_root}));
|
||||
$.ajax({
|
||||
url: script_root + obj.type_data.scripts.update,
|
||||
dataType: "script",
|
||||
success: success_cb,
|
||||
cache: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadchals(){
|
||||
$('#challenges').empty();
|
||||
$.post(script_root + "/admin/chals", {
|
||||
'nonce': $('#nonce').val()
|
||||
}, function (data) {
|
||||
categories = [];
|
||||
challenges = $.parseJSON(JSON.stringify(data));
|
||||
|
||||
|
||||
for (var i = challenges['game'].length - 1; i >= 0; i--) {
|
||||
if ($.inArray(challenges['game'][i].category, categories) == -1) {
|
||||
categories.push(challenges['game'][i].category)
|
||||
$('#challenges').append($('<tr id="' + challenges['game'][i].category.replace(/ /g,"-").hashCode() + '"><td class="col-md-1"><h3>' + challenges['game'][i].category + '</h3></td></tr>'))
|
||||
}
|
||||
};
|
||||
|
||||
for (var i = 0; i <= challenges['game'].length - 1; i++) {
|
||||
var chal = challenges['game'][i];
|
||||
var chal_button = $('<button class="chal-button col-md-2 theme-background" value="{0}"><h5>{1}</h5><p class="chal-points">{2}</p><span class="chal-percent">{3}% solved</span></button>'.format(chal.id, chal.name, chal.value, Math.round(chal.percentage_solved * 100)));
|
||||
$('#' + challenges['game'][i].category.replace(/ /g,"-").hashCode()).append(chal_button);
|
||||
};
|
||||
|
||||
$('#challenges button').click(function (e) {
|
||||
id = this.value
|
||||
load_chal_template(id, function(){
|
||||
openchal(id);
|
||||
});
|
||||
});
|
||||
|
||||
// $('.create-challenge').click(function (e) {
|
||||
// $('#new-chal-category').val($($(this).siblings()[0]).text().trim());
|
||||
// $('#new-chal-title').text($($(this).siblings()[0]).text().trim());
|
||||
// $('#new-challenge').modal();
|
||||
// });
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
$(function(){
|
||||
loadchals();
|
||||
})
|
|
@ -1,8 +1,9 @@
|
|||
$.ajaxSetup({ cache: false });
|
||||
|
||||
function load_chal_template(challenge){
|
||||
$.get(script_root + challenge.templates.create, function(template_data){
|
||||
var template = Handlebars.compile(template_data);
|
||||
$("#create-chal-entry-div").html(template({'nonce':nonce, 'script_root':script_root}));
|
||||
var template = nunjucks.compile(template_data);
|
||||
$("#create-chal-entry-div").html(template.render({'nonce':nonce, 'script_root':script_root}));
|
||||
$.getScript(script_root + challenge.scripts.create, function(){
|
||||
console.log('loaded');
|
||||
});
|
||||
|
@ -23,9 +24,10 @@ $.get(script_root + '/admin/chal_types', function(data){
|
|||
option.data('meta', challenge);
|
||||
$("#create-chals-select").append(option);
|
||||
}
|
||||
$("#create-chals-select-div").show();
|
||||
} else if (chal_type_amt == 1) {
|
||||
var key = Object.keys(data)[0];
|
||||
$("#create-chals-select").parent().parent().parent().empty();
|
||||
$("#create-chals-select").empty();
|
||||
load_chal_template(data[key]);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,182 @@
|
|||
var challenges = {};
|
||||
|
||||
|
||||
function load_chal_template(id, success_cb){
|
||||
var obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[0];
|
||||
$.get(script_root + obj.type_data.templates.update, function(template_data){
|
||||
var template = nunjucks.compile(template_data);
|
||||
$("#update-modals-entry-div").html(template.render({'nonce':$('#nonce').val(), 'script_root':script_root}));
|
||||
$.ajax({
|
||||
url: script_root + obj.type_data.scripts.update,
|
||||
dataType: "script",
|
||||
success: success_cb,
|
||||
cache: false,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function get_challenge(id){
|
||||
var obj = $.grep(challenges['game'], function (e) {
|
||||
return e.id == id;
|
||||
})[0];
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
function load_challenge_preview(id){
|
||||
var chal = get_challenge(id);
|
||||
var modal_template = chal.type_data.templates.modal;
|
||||
var modal_script = chal.type_data.scripts.modal;
|
||||
|
||||
render_challenge_preview(chal, modal_template, modal_script)
|
||||
}
|
||||
|
||||
function render_challenge_preview(chal, modal_template, modal_script){
|
||||
var preview_window = $('#challenge-preview');
|
||||
preview_window.empty();
|
||||
$.get(script_root + modal_template, function (template_data) {
|
||||
|
||||
var template = nunjucks.compile(template_data);
|
||||
var data = {
|
||||
id: chal.id,
|
||||
name: chal.name,
|
||||
value: chal.value,
|
||||
tags: chal.tags,
|
||||
desc: chal.description,
|
||||
files: chal.files,
|
||||
hints: chal.hints
|
||||
};
|
||||
|
||||
var challenge = template.render(data);
|
||||
|
||||
preview_window.append(challenge);
|
||||
|
||||
$.getScript(script_root + modal_script, function () {
|
||||
preview_window.modal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadchals(cb){
|
||||
$.post(script_root + "/admin/chals", {
|
||||
'nonce': $('#nonce').val()
|
||||
}, function (data) {
|
||||
var categories = [];
|
||||
challenges = $.parseJSON(JSON.stringify(data));
|
||||
|
||||
|
||||
for (var i = challenges['game'].length - 1; i >= 0; i--) {
|
||||
if ($.inArray(challenges['game'][i].category, categories) == -1) {
|
||||
categories.push(challenges['game'][i].category)
|
||||
}
|
||||
}
|
||||
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadchals(function(){
|
||||
$('.edit-challenge').click(function (e) {
|
||||
var id = $(this).attr('chal-id');
|
||||
load_chal_template(id, function () {
|
||||
openchal(id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function submitkey(chal, key, nonce){
|
||||
$.post(script_root + "/admin/chal/" + chal, {
|
||||
key: key,
|
||||
nonce: nonce
|
||||
}, function (data) {
|
||||
console.log(data);
|
||||
var result = $.parseJSON(JSON.stringify(data));
|
||||
|
||||
var result_message = $('#result-message');
|
||||
var result_notification = $('#result-notification');
|
||||
var answer_input = $("#answer-input");
|
||||
result_notification.removeClass();
|
||||
result_message.text(result.message);
|
||||
|
||||
if (result.status == -1) {
|
||||
window.location = script_root + "/login?next=" + script_root + window.location.pathname + window.location.hash
|
||||
return
|
||||
}
|
||||
else if (result.status == 0) { // Incorrect key
|
||||
result_notification.addClass('alert alert-danger alert-dismissable text-center');
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.removeClass("correct");
|
||||
answer_input.addClass("wrong");
|
||||
setTimeout(function () {
|
||||
answer_input.removeClass("wrong");
|
||||
}, 3000);
|
||||
}
|
||||
else if (result.status == 1) { // Challenge Solved
|
||||
result_notification.addClass('alert alert-success alert-dismissable text-center');
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.val("");
|
||||
answer_input.removeClass("wrong");
|
||||
answer_input.addClass("correct");
|
||||
}
|
||||
else if (result.status == 2) { // Challenge already solved
|
||||
result_notification.addClass('alert alert-info alert-dismissable text-center');
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.addClass("correct");
|
||||
}
|
||||
else if (result.status == 3) { // Keys per minute too high
|
||||
result_notification.addClass('alert alert-warning alert-dismissable text-center');
|
||||
result_notification.slideDown();
|
||||
|
||||
answer_input.addClass("too-fast");
|
||||
setTimeout(function () {
|
||||
answer_input.removeClass("too-fast");
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
$('.alert').slideUp();
|
||||
$('#submit-key').removeClass("disabled-button");
|
||||
$('#submit-key').prop('disabled', false);
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$('.delete-challenge').click(function (e) {
|
||||
var chal_id = $(this).attr('chal-id');
|
||||
|
||||
ezq({
|
||||
title: "Delete Challenge",
|
||||
body: "Are you sure you want to delete this challenge?",
|
||||
success: function () {
|
||||
$.post(script_root + '/admin/chal/delete', {'id': chal_id, 'nonce': $('#nonce').val()}, function (data) {
|
||||
if (data == 1) {
|
||||
location.reload();
|
||||
}
|
||||
else {
|
||||
ezal({
|
||||
title: "Error",
|
||||
body: "There was an error"
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$('.preview-challenge').click(function (e) {
|
||||
var chal_id = $(this).attr('chal-id');
|
||||
|
||||
load_challenge_preview(chal_id);
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
var modal = '<div class="modal fade" tabindex="-1" role="dialog">' +
|
||||
' <div class="modal-dialog" role="document">' +
|
||||
' <div class="modal-content">' +
|
||||
' <div class="modal-header">' +
|
||||
' <h5 class="modal-title">\{0\}</h5>' +
|
||||
' <button type="button" class="close" data-dismiss="modal" aria-label="Close">' +
|
||||
' <span aria-hidden="true">×</span>' +
|
||||
' </button>' +
|
||||
' </div>' +
|
||||
' <div class="modal-body">' +
|
||||
' <p>\{1\}</p>' +
|
||||
' </div>' +
|
||||
' <div class="modal-footer">' +
|
||||
' </div>' +
|
||||
' </div>' +
|
||||
' </div>' +
|
||||
'</div>';
|
||||
|
||||
function ezal(args){
|
||||
var res = modal.format(args.title, args.body);
|
||||
var obj = $(res);
|
||||
var button = '<button type="button" class="btn btn-primary" data-dismiss="modal">{0}</button>'.format(args.button);
|
||||
|
||||
obj.find('.modal-footer').append(button);
|
||||
$('main').append(obj);
|
||||
|
||||
obj.modal('show');
|
||||
|
||||
$(obj).on('hidden.bs.modal', function (e) {
|
||||
$(this).modal('dispose');
|
||||
})
|
||||
}
|
||||
|
||||
function ezq(args){
|
||||
var res = modal.format(args.title, args.body);
|
||||
var obj = $(res);
|
||||
var deny = '<button type="button" class="btn btn-danger" data-dismiss="modal">No</button>';
|
||||
var confirm = $('<button type="button" class="btn btn-primary" data-dismiss="modal">Yes</button>');
|
||||
|
||||
obj.find('.modal-footer').append(deny);
|
||||
obj.find('.modal-footer').append(confirm);
|
||||
|
||||
$('main').append(obj);
|
||||
|
||||
$(obj).on('hidden.bs.modal', function (e) {
|
||||
$(this).modal('dispose');
|
||||
});
|
||||
|
||||
$(confirm).click(function(){
|
||||
args.success();
|
||||
});
|
||||
|
||||
obj.modal('show');
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
function updatefiles(){
|
||||
chal = $('#files-chal').val();
|
||||
var form = $('#update-files form')[0];
|
||||
var formData = new FormData(form);
|
||||
$.ajax({
|
||||
url: script_root + '/admin/files/'+chal,
|
||||
data: formData,
|
||||
type: 'POST',
|
||||
cache: false,
|
||||
contentType: false,
|
||||
processData: false,
|
||||
success: function(data){
|
||||
form.reset();
|
||||
loadfiles(chal);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadfiles(chal, cb){
|
||||
$('#update-files form').attr('action', script_root+'/admin/files/'+chal)
|
||||
$.get(script_root + '/admin/files/' + chal, function(data){
|
||||
$('#files-chal').val(chal);
|
||||
files = $.parseJSON(JSON.stringify(data));
|
||||
files = files['files'];
|
||||
$('#current-files').empty();
|
||||
for(x=0; x<files.length; x++){
|
||||
filename = files[x].file.split('/');
|
||||
filename = filename[filename.length - 1];
|
||||
|
||||
var curr_file = '<div class="col-md-12"><a href="{2}/files/{3}">{4}</a> <i class="fa fa-times float-right" onclick="deletefile({0}, {1}, $(this))" value="{2}" ></i></div>'.format(
|
||||
chal,
|
||||
files[x].id,
|
||||
script_root,
|
||||
files[x].file,
|
||||
filename
|
||||
);
|
||||
|
||||
$('#current-files').append(curr_file);
|
||||
}
|
||||
|
||||
if (cb){
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletefile(chal, file, elem){
|
||||
$.post(script_root + '/admin/files/' + chal,{
|
||||
'nonce': $('#nonce').val(),
|
||||
'method': 'delete',
|
||||
'file': file
|
||||
}, function (data){
|
||||
if (data == "1") {
|
||||
elem.parent().remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$('.edit-files').click(function (e) {
|
||||
var chal_id = $(this).attr('chal-id');
|
||||
loadfiles(chal_id, function () {
|
||||
$('#update-files').modal();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$('#submit-files').click(function (e) {
|
||||
e.preventDefault();
|
||||
updatefiles()
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
function load_hint_modal(method, hintid){
|
||||
$('#hint-modal-hint').val('');
|
||||
$('#hint-modal-cost').val('');
|
||||
if (method == 'create'){
|
||||
$('#hint-modal-submit').attr('action', '/admin/hints');
|
||||
$('#hint-modal-title').text('Create Hint');
|
||||
$("#hint-modal").modal();
|
||||
} else if (method == 'update'){
|
||||
$.get(script_root + '/admin/hints/' + hintid, function(data){
|
||||
$('#hint-modal-submit').attr('action', '/admin/hints/' + hintid);
|
||||
$('#hint-modal-hint').val(data.hint);
|
||||
$('#hint-modal-cost').val(data.cost);
|
||||
$('#hint-modal-title').text('Update Hint');
|
||||
$("#hint-modal-button").text('Update Hint');
|
||||
$("#hint-modal").modal();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function edithint(hintid){
|
||||
$.get(script_root + '/admin/hints/' + hintid, function(data){
|
||||
console.log(data);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function deletehint(hintid){
|
||||
$.delete(script_root + '/admin/hints/' + hintid, function(data, textStatus, jqXHR){
|
||||
if (jqXHR.status == 204){
|
||||
var chalid = $("#update-hints").attr('chal-id');
|
||||
loadhints(chalid);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function loadhints(chal, cb){
|
||||
$.get(script_root + '/admin/chal/{0}/hints'.format(chal), function(data){
|
||||
var table = $('#hintsboard > tbody');
|
||||
table.empty();
|
||||
for (var i = 0; i < data.hints.length; i++) {
|
||||
var hint = data.hints[i]
|
||||
var hint_row = "<tr>" +
|
||||
"<td class='hint-entry d-table-cell w-75'><pre>{0}</pre></td>".format(htmlentities(hint.hint)) +
|
||||
"<td class='hint-cost d-table-cell text-center'>{0}</td>".format(hint.cost) +
|
||||
"<td class='hint-settings d-table-cell text-center'><span>" +
|
||||
"<i role='button' class='fa fa-pencil-square-o' onclick=javascript:load_hint_modal('update',{0})></i>".format(hint.id)+
|
||||
"<i role='button' class='fa fa-times' onclick=javascript:deletehint({0})></i>".format(hint.id)+
|
||||
"</span></td>" +
|
||||
"</tr>";
|
||||
table.append(hint_row);
|
||||
}
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('.edit-hints').click(function (e) {
|
||||
var chal_id = $(this).attr('chal-id');
|
||||
loadhints(chal_id, function () {
|
||||
$("#chal-id-for-hint").val(chal_id); // Preload the challenge ID so the form submits properly. Remove in later iterations
|
||||
$("#update-hints").attr('chal-id', chal_id);
|
||||
$('#update-hints').modal();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$('#create-hint').click(function (e) {
|
||||
e.preventDefault();
|
||||
load_hint_modal('create');
|
||||
});
|
||||
|
||||
|
||||
$('#hint-modal-submit').submit(function (e) {
|
||||
e.preventDefault();
|
||||
var params = {};
|
||||
$(this).serializeArray().map(function (x) {
|
||||
params[x.name] = x.value;
|
||||
});
|
||||
$.post(script_root + $(this).attr('action'), params, function (data) {
|
||||
loadhints(params['chal']);
|
||||
});
|
||||
$("#hint-modal").modal('hide');
|
||||
});
|
||||
|
||||
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
var target = $(e.target).attr("href");
|
||||
if (target == '#hint-preview') {
|
||||
var obj = $('#hint-modal-hint');
|
||||
var data = marked(obj.val());
|
||||
$(event.target.hash).html(data, {'gfm': true, 'breaks': true});
|
||||
}
|
||||
});
|
|
@ -0,0 +1,137 @@
|
|||
function load_edit_key_modal(key_id, key_type_name) {
|
||||
$.get(script_root + '/admin/keys/' + key_id, function (key_data) {
|
||||
$.get(script_root + key_data.templates.update, function (template_data) {
|
||||
$('#edit-keys').empty();
|
||||
var template = nunjucks.compile(template_data);
|
||||
key_data['script_root'] = script_root;
|
||||
key_data['nonce'] = $('#nonce').val();
|
||||
$('#edit-keys').append(template.render(key_data));
|
||||
$('#key-id').val(key_id);
|
||||
$('#submit-keys').click(function (e) {
|
||||
e.preventDefault();
|
||||
updatekey()
|
||||
});
|
||||
$('#edit-keys').modal();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function create_key(chal, key, key_type) {
|
||||
$.post(script_root + "/admin/keys", {
|
||||
chal: chal,
|
||||
key: key,
|
||||
key_type: key_type,
|
||||
nonce: $('#nonce').val()
|
||||
}, function (data) {
|
||||
if (data == "1"){
|
||||
loadkeys(chal);
|
||||
$("#create-keys").modal('toggle');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadkeys(chal, cb){
|
||||
$.get(script_root + '/admin/chal/' + chal + '/keys', function(data){
|
||||
$('#keys-chal').val(chal);
|
||||
var keys = $.parseJSON(JSON.stringify(data));
|
||||
keys = keys['keys'];
|
||||
$('#current-keys').empty();
|
||||
$.get(script_root + "/themes/admin/static/js/templates/admin-keys-table.njk", function(data){
|
||||
var template = nunjucks.compile(data);
|
||||
var wrapper = {keys: keys, script_root: script_root};
|
||||
$('#current-keys').append(template.render(wrapper));
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function updatekeys(){
|
||||
keys = [];
|
||||
vals = [];
|
||||
chal = $('#keys-chal').val()
|
||||
$('.current-key').each(function(){
|
||||
keys.push($(this).val());
|
||||
})
|
||||
$('#current-keys input[name*="key_type"]:checked').each(function(){
|
||||
vals.push($(this).val());
|
||||
})
|
||||
$.post(script_root + '/admin/keys/'+chal, {'keys':keys, 'vals':vals, 'nonce': $('#nonce').val()})
|
||||
loadchal(chal, true)
|
||||
$('#update-keys').modal('hide');
|
||||
}
|
||||
|
||||
|
||||
function deletekey(key_id){
|
||||
$.post(script_root + '/admin/keys/'+key_id+'/delete', {'nonce': $('#nonce').val()}, function(data){
|
||||
if (data == "1") {
|
||||
$('tr[name={0}]'.format(key_id)).remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updatekey(){
|
||||
var key_id = $('#key-id').val();
|
||||
var chal = $("#update-keys").attr('chal-id');
|
||||
var key_data = $('#key-data').val();
|
||||
var key_type = $('#key-type').val();
|
||||
var nonce = $('#nonce').val();
|
||||
$.post(script_root + '/admin/keys/'+key_id, {
|
||||
'chal':chal,
|
||||
'key':key_data,
|
||||
'key_type': key_type,
|
||||
'nonce': nonce
|
||||
}, function(data){
|
||||
if (data == "1") {
|
||||
loadkeys(chal);
|
||||
$('#edit-keys').modal('toggle');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$('.edit-keys').click(function (e) {
|
||||
var chal_id = $(this).attr('chal-id');
|
||||
loadkeys(chal_id, function () {
|
||||
$("#update-keys").attr('chal-id', chal_id);
|
||||
$('#update-keys').modal();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
$('#create-key').click(function (e) {
|
||||
$.get(script_root + '/admin/key_types', function (data) {
|
||||
$("#create-keys-select").empty();
|
||||
var option = "<option> -- </option>";
|
||||
$("#create-keys-select").append(option);
|
||||
for (var key in data) {
|
||||
var option = "<option value='{0}'>{1}</option>".format(key, data[key]);
|
||||
$("#create-keys-select").append(option);
|
||||
}
|
||||
$("#create-keys").modal();
|
||||
});
|
||||
});
|
||||
|
||||
$('#create-keys-select').change(function () {
|
||||
var key_type_name = $(this).find("option:selected").text();
|
||||
|
||||
$.get(script_root + '/admin/key_types/' + key_type_name, function (key_data) {
|
||||
$.get(script_root + key_data.templates.create, function (template_data) {
|
||||
var template = nunjucks.compile(template_data);
|
||||
$("#create-keys-entry-div").html(template.render());
|
||||
$("#create-keys-button-div").show();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
$('#create-keys-submit').click(function (e) {
|
||||
e.preventDefault();
|
||||
var chalid = $("#update-keys").attr('chal-id');
|
||||
var key_data = $('#create-keys').find('input[name=key]').val();
|
||||
var key_type = $('#create-keys-select').val();
|
||||
create_key(chalid, key_data, key_type);
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
$(".form-control").bind({
|
||||
focus: function() {
|
||||
$(this).addClass('input-filled-valid' );
|
||||
},
|
||||
blur: function() {
|
||||
if ($(this).val() === '') {
|
||||
$(this).removeClass('input-filled-valid' );
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$('.modal').on('show.bs.modal', function (e) {
|
||||
$('.form-control').each(function () {
|
||||
if ($(this).val()) {
|
||||
$(this).addClass("input-filled-valid");
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
$(function () {
|
||||
$('.form-control').each(function () {
|
||||
if ($(this).val()) {
|
||||
$(this).addClass("input-filled-valid");
|
||||
}
|
||||
});
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
|
@ -0,0 +1,63 @@
|
|||
function loadtags(chal, cb){
|
||||
$('#tags-chal').val(chal)
|
||||
$('#current-tags').empty()
|
||||
$('#chal-tags').empty()
|
||||
$.get(script_root + '/admin/tags/'+chal, function(data){
|
||||
tags = $.parseJSON(JSON.stringify(data))
|
||||
tags = tags['tags']
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
tag = "<span class='badge badge-primary mx-1 chal-tag'><span>"+tags[i].tag+"</span><a name='"+tags[i].id+"'' class='delete-tag'> ×</a></span>"
|
||||
$('#current-tags').append(tag)
|
||||
};
|
||||
$('.delete-tag').click(function(e){
|
||||
deletetag(e.target.name)
|
||||
$(e.target).parent().remove()
|
||||
});
|
||||
|
||||
if (cb) {
|
||||
cb();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function deletetag(tagid){
|
||||
$.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()});
|
||||
}
|
||||
|
||||
|
||||
function updatetags(){
|
||||
tags = [];
|
||||
chal = $('#tags-chal').val();
|
||||
$('#chal-tags > span > span').each(function(i, e){
|
||||
tags.push($(e).text());
|
||||
});
|
||||
$.post(script_root + '/admin/tags/'+chal, {'tags':tags, 'nonce': $('#nonce').val()});
|
||||
$('#update-tags').modal('toggle');
|
||||
}
|
||||
|
||||
|
||||
$('.edit-tags').click(function (e) {
|
||||
var chal_id = $(this).attr('chal-id');
|
||||
loadtags(chal_id, function(){
|
||||
$('#update-tags').modal();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
$(".tag-insert").keyup(function (e) {
|
||||
if (e.keyCode == 13) {
|
||||
var tag = $('.tag-insert').val()
|
||||
tag = tag.replace(/'/g, '');
|
||||
if (tag.length > 0) {
|
||||
tag = "<span class='badge badge-primary mx-1 chal-tag'><span>" + tag + "</span><a class='delete-tag' onclick='$(this).parent().remove()'> ×</a></span>"
|
||||
$('#chal-tags').append(tag);
|
||||
}
|
||||
$('.tag-insert').val("");
|
||||
}
|
||||
});
|
||||
|
||||
$('#submit-tags').click(function (e) {
|
||||
e.preventDefault();
|
||||
updatetags()
|
||||
});
|
||||
|
|
@ -13,15 +13,16 @@ function cumulativesum (arr) {
|
|||
}
|
||||
|
||||
function scoregraph () {
|
||||
var times = []
|
||||
var scores = []
|
||||
var times = [];
|
||||
var scores = [];
|
||||
var teamname = $('#team-id').text()
|
||||
$.get(script_root + '/admin/solves/'+teamid(), function( data ) {
|
||||
var solves = $.parseJSON(JSON.stringify(data));
|
||||
solves = solves['solves'];
|
||||
|
||||
if (solves.length == 0)
|
||||
if (solves.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var i = 0; i < solves.length; i++) {
|
||||
var date = moment(solves[i].time * 1000);
|
||||
|
@ -34,13 +35,34 @@ function scoregraph () {
|
|||
{
|
||||
x: times,
|
||||
y: scores,
|
||||
type: 'scatter'
|
||||
type: 'scatter',
|
||||
marker: {
|
||||
color: colorhash(teamname + teamid())
|
||||
},
|
||||
line: {
|
||||
color: colorhash(teamname + teamid())
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
var layout = {
|
||||
title: 'Score over Time'
|
||||
title: 'Score over Time',
|
||||
paper_bgcolor: 'rgba(0,0,0,0)',
|
||||
plot_bgcolor: 'rgba(0,0,0,0)',
|
||||
hovermode: 'closest',
|
||||
xaxis: {
|
||||
showgrid: false,
|
||||
showspikes: true,
|
||||
},
|
||||
yaxis: {
|
||||
showgrid: false,
|
||||
showspikes: true,
|
||||
},
|
||||
legend: {
|
||||
"orientation": "h"
|
||||
}
|
||||
};
|
||||
|
||||
$('#score-graph').empty();
|
||||
Plotly.newPlot('score-graph', data, layout);
|
||||
});
|
||||
|
|
|
@ -7,16 +7,16 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{#each keys}}
|
||||
<tr name="{{this.id}}">
|
||||
<td class="key-type">{{this.type_name}}</td>
|
||||
<td class="key-value">{{this.key}}</td>
|
||||
{% for key in keys %}
|
||||
<tr name="{{key.id}}">
|
||||
<td class="key-type">{{key.type_name}}</td>
|
||||
<td class="key-value">{{key.key}}</td>
|
||||
<td class="text-center"><span>
|
||||
<i role="button" class="fa fa-pencil-square-o" onclick="javascript:load_edit_key_modal({{this.id}}, '{{this.type_name}}')"></i>
|
||||
<i role="button" class="fa fa-times" onclick="javascript:deletekey({{this.id}})"></i>
|
||||
<i role="button" class="fa fa-pencil-square-o" onclick="javascript:load_edit_key_modal({{key.id}}, '{{key.type_name}}')"></i>
|
||||
<i role="button" class="fa fa-times" onclick="javascript:deletekey({{key.id}})"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -40,28 +40,23 @@ String.prototype.hashCode = function() {
|
|||
return hash;
|
||||
};
|
||||
|
||||
function colorhash (x) {
|
||||
color = ""
|
||||
for (var i = 20; i <= 60; i+=20){
|
||||
x += i
|
||||
x *= i
|
||||
color += x.toString(16)
|
||||
};
|
||||
return "#" + color.substring(0, 6)
|
||||
function colorhash(str) {
|
||||
var hash = 0;
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
var colour = '#';
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var value = (hash >> (i * 8)) & 0xFF;
|
||||
colour += ('00' + value.toString(16)).substr(-2);
|
||||
}
|
||||
return colour;
|
||||
}
|
||||
|
||||
function htmlentities(string) {
|
||||
return $('<div/>').text(string).html();
|
||||
}
|
||||
|
||||
Handlebars.registerHelper('if_eq', function(a, b, opts) {
|
||||
if (a == b) {
|
||||
return opts.fn(this);
|
||||
} else {
|
||||
return opts.inverse(this);
|
||||
}
|
||||
});
|
||||
|
||||
// http://stepansuvorov.com/blog/2014/04/jquery-put-and-delete/
|
||||
jQuery.each(["put", "delete"], function(i, method) {
|
||||
jQuery[method] = function(url, data, callback, type) {
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5,16 +5,17 @@
|
|||
<title>Admin Panel</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{{ request.script_root }}/themes/original/static/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="{{ request.script_root }}/themes/original/static/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="{{ request.script_root }}/themes/core/static/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="{{ request.script_root }}/themes/core/static/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/font-awesome/css/font-awesome.min.css" />
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/style.css">
|
||||
<link href='{{ request.script_root }}/themes/admin/static/css/vendor/font.css' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/themes/admin/static/css/style.css">
|
||||
<link rel="stylesheet" href='{{ request.script_root }}/themes/admin/static/css/vendor/font.css'>
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/jumbotron.css">
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/sticky-footer.css">
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/base.css">
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment-timezone-with-data.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/handlebars.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/nunjucks.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
var script_root = "{{ request.script_root }}";
|
||||
</script>
|
||||
|
@ -22,41 +23,50 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<div class="body-container">
|
||||
<div class="navbar navbar-inverse home">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse" type="button">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a href="{{ request.script_root }}/" class="navbar-brand">CTFd</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#base-navbars"
|
||||
aria-controls="base-navbars" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="base-navbars">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="true">Pages</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{{ request.script_root }}/admin/pages">All Pages</a>
|
||||
<a class="dropdown-item" href="{{ request.script_root }}/admin/pages?mode=create">New Page</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse" aria-expanded="false" style="height: 0px">
|
||||
<ul class="nav navbar-nav navbar-nav-right">
|
||||
<li><a href="{{ request.script_root }}/admin/graphs">Graphs</a></li>
|
||||
<li>
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
||||
aria-expanded="false">Pages <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ request.script_root }}/admin/pages">All Pages</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/pages?mode=create">New Page</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="{{ request.script_root }}/admin/teams">Teams</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/chals">Challenges</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/config">Config</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/teams">Teams</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/chals">Challenges</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button"
|
||||
aria-haspopup="true" aria-expanded="true">Submissions</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{{ request.script_root }}/admin/correct_keys/1">Correct Submissions</a>
|
||||
<a class="dropdown-item" href="{{ request.script_root }}/admin/wrong_keys/1">Wrong Submissions</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/config">Config</a></li>
|
||||
|
||||
{% set plugin_menu = get_admin_plugin_menu_bar() %}
|
||||
{% set plugins = get_configurable_plugins() %}
|
||||
{% if plugin_menu or plugins %}
|
||||
<li><a style="padding-left:0px;padding-right:0px;">|</a></li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-none d-md-block d-lg-block">|</a>
|
||||
</li>
|
||||
|
||||
{% for menu in plugin_menu %}
|
||||
<li><a href="{{ request.script_root }}{{ menu.route }}">{{ menu.name }}</a></li>
|
||||
{% if request.script_root %}
|
||||
{% set route = '/' + request.script_root + '/' + menu.route %}
|
||||
{% else %}
|
||||
{% set route = '/' + menu.route %}
|
||||
{% endif %}
|
||||
<li class="nav-item"><a class="nav-link" href="{{ route }}">{{ menu.title }}</a></li>
|
||||
{% endfor %}
|
||||
|
||||
{% if plugins %}
|
||||
|
@ -64,7 +74,7 @@
|
|||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
{% for plugin in plugins %}
|
||||
<li><a href="{{ request.script_root }}{{ plugin.route }}">{{ plugin.name }}</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}{{ plugin.route }}">{{ plugin.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
|
@ -73,24 +83,41 @@
|
|||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container main-container">
|
||||
{% if get_config('version_latest') %}
|
||||
<div class="container-fluid bg-warning text-center py-3">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<a class="btn btn-warning" href="{{ get_config('version_latest') }}">
|
||||
A new CTFd version is available!
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<main role="main">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class="navbar navbar-bottom footer">
|
||||
<p class="text-muted text-center">
|
||||
<a style="text-decoration:none;" href="https://ctfd.io"><sub>Powered by CTFd</sub></a>
|
||||
</p>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container text-center">
|
||||
<a href="https://ctfd.io">
|
||||
<small class="text-muted">Powered by CTFd</small>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/ezq.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/style.js"></script>
|
||||
{% block scripts %} {% endblock %}
|
||||
</body>
|
||||
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% include "admin/modals/challenges/challenges.html" %}
|
||||
|
||||
{% include "admin/modals/tags/tags.html" %}
|
||||
{% include "admin/modals/files/files.html" %}
|
||||
{% include "admin/modals/hints/hints.html" %}
|
||||
{% include "admin/modals/keys/keys.html" %}
|
||||
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value="{{ nonce }}" id="nonce">
|
||||
|
||||
<div id="create-challenge" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3>New Challenge</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="update-modals-entry-div">
|
||||
</div>
|
||||
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Challenges
|
||||
<a class="no-decoration" href="{{ request.script_root }}/admin/chal/new">
|
||||
<i class="fa fa-plus-circle" role="button" data-toggle="tooltip" title="Create Challenge"></i>
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div>
|
||||
<table id="challenges" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>ID</b></td>
|
||||
<td><b>Name</b></td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Category</b></td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Value</b></td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell"><b>Type</b></td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Status</b></td>
|
||||
<td style="width: 200px;"><b>Settings</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for challenge in challenges %}
|
||||
<tr>
|
||||
<td>{{ challenge.id }}</td>
|
||||
<td>{{ challenge.name }}</td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.category }}</td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.value }}</td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell">{{ challenge.type }}</td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell text-center">
|
||||
{% if challenge.hidden %}
|
||||
<button class="btn-sm btn-danger" type="submit" disabled>Hidden</button>
|
||||
{% else %}
|
||||
<button class="btn-sm btn-success" type="submit" disabled>Visible</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa fa-pencil edit-challenge" aria-hidden="true" data-toggle="tooltip"
|
||||
data-placement="top" chal-id="{{ challenge.id }}"
|
||||
title="Edit {{ challenge.name }}"></i>
|
||||
|
||||
<i class="fa fa-file-text-o preview-challenge" aria-hidden="true" data-toggle="tooltip"
|
||||
data-placement="top" chal-id="{{ challenge.id }}"
|
||||
title="Preview {{ challenge.name }}"></i>
|
||||
|
||||
<span> </span>
|
||||
|
||||
<i class="fa fa-flag edit-keys" aria-hidden="true" data-toggle="tooltip" data-placement="top"
|
||||
title="Edit Flags" chal-id="{{ challenge.id }}"></i>
|
||||
<i class="fa fa-cloud-download edit-files" aria-hidden="true" data-toggle="tooltip"
|
||||
data-placement="top" title="Edit Files" chal-id="{{ challenge.id }}"></i>
|
||||
<i class="fa fa-tags edit-tags" aria-hidden="true" data-toggle="tooltip" data-placement="top"
|
||||
title="Edit Tags" chal-id="{{ challenge.id }}"></i>
|
||||
<i class="fa fa-question-circle edit-hints" aria-hidden="true" data-toggle="tooltip"
|
||||
data-placement="top" title="Edit Hints" chal-id="{{ challenge.id }}"></i>
|
||||
|
||||
<span> </span>
|
||||
|
||||
<i class="fa fa-times delete-challenge" aria-hidden="true" data-toggle="tooltip"
|
||||
data-placement="top" title="Delete {{ challenge.name }}" chal-id="{{ challenge.id }}"></i>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/challenges/chalboard.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/files/files-utils.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/hints/hints-utils.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/keys/keys-utils.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/tags/tags-utils.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/multi-modal.js"></script>
|
||||
{% endblock %}
|
|
@ -1,56 +0,0 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
.btn-primary { background-color: #337ab7; }
|
||||
.btn-danger { background-color: #d9534f; }
|
||||
.col-md-4 { margin-bottom: 15px; }
|
||||
.key-remove-button { margin-top: 10px; }
|
||||
.delete-tag { color: white; margin-left: 3px; cursor: pointer; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="email-user" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value="{{ nonce }}" id="nonce">
|
||||
|
||||
<div id="create-challenge" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3>New Challenge</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="update-modals-entry-div">
|
||||
</div>
|
||||
|
||||
<div style="text-align:center">
|
||||
<br>
|
||||
<h1 class="text-center">Challenges</h1>
|
||||
<a class="btn btn-theme btn-outlined create-challenge" href="{{ request.script_root }}/admin/chal/new" role="button">New Challenge</a>
|
||||
<div>
|
||||
<table id='challenges'>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/chalboard.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/multi-modal.js"></script>
|
||||
{% endblock %}
|
|
@ -1,33 +1,34 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
.btn-primary { background-color: #337ab7; }
|
||||
.btn-danger { background-color: #d9534f; }
|
||||
.col-md-4 { margin-bottom: 15px; }
|
||||
.key-remove-button { margin-top: 10px; }
|
||||
.delete-tag { color: white; margin-left: 3px; cursor: pointer; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="create-challenge" class="container main-container">
|
||||
<h1 class="text-center">Create Challenge</h1>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Create Challenge</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="create-chals-select-div">
|
||||
<div class="col-md-12">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
<div id="create-chals-select-div" style="display: none;">
|
||||
<label for="create-chals-select" class="control-label">Choose Challenge Type</label>
|
||||
<select class="form-control" id="create-chals-select">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row">
|
||||
<div id="create-chal-entry-div">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
@ -36,5 +37,6 @@
|
|||
<script>
|
||||
nonce = "{{ nonce }}";
|
||||
</script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/chal-new.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/challenges/chal-new.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/style.js"></script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,31 +1,34 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>Config</h1>
|
||||
<form method="POST" autocomplete="off">
|
||||
<div class="col-md-2 col-md-offset-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#appearance-section" aria-controls="appearance-section" role="tab" data-toggle="tab">Appearance</a>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Configuration</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link rounded-0 active" href="#appearance-section" aria-controls="appearance-section" role="tab" data-toggle="tab">Appearance</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#accounts-section" aria-controls="accounts-section" role="tab" data-toggle="tab">Accounts</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link rounded-0" href="#accounts-section" aria-controls="accounts-section" role="tab" data-toggle="tab">Accounts</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#email-section" aria-controls="email-section" role="tab" data-toggle="tab">Email</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link rounded-0" href="#email-section" aria-controls="email-section" role="tab" data-toggle="tab">Email</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#ctftime-section" aria-controls="ctftime-section" role="tab" data-toggle="tab">CTF Time</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link rounded-0" href="#ctftime-section" aria-controls="ctftime-section" role="tab" data-toggle="tab">Time</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#backup-section" aria-controls="backup-section" role="tab" data-toggle="tab">Backup</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link rounded-0" href="#backup-section" aria-controls="backup-section" role="tab" data-toggle="tab">Backup</a>
|
||||
</li>
|
||||
</ul>
|
||||
<br><br>
|
||||
<button type="submit" id="submit" tabindex="5" class="btn btn-md btn-primary btn-theme btn-outlined pull-left">Update</button>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="col-md-9">
|
||||
<form method="POST" autocomplete="off" class="w-100">
|
||||
{% for error in errors %}
|
||||
<div class="alert alert-danger alert-dismissable" role="alert">
|
||||
<span class="sr-only">Error:</span>
|
||||
|
@ -53,7 +56,7 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="workshop_mode" name="workshop_mode" type="checkbox"
|
||||
{% if workshop_mode %}checked{% endif %}>
|
||||
|
@ -63,29 +66,44 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="paused" name="paused" type="checkbox"
|
||||
{% if paused %}checked{% endif %}>
|
||||
Pause CTF <i class="fa fa-question-circle gray-text" data-toggle="tooltip"
|
||||
data-placement="right"
|
||||
title="Prevent users from submitting answers until unpaused. Challenges can still be viewed."></i>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="hide_scores" name="hide_scores" type="checkbox" {% if hide_scores %}checked{% endif %}>
|
||||
Hide Scores from public view
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>CSS editor</label>
|
||||
<textarea class="form-control" id="css-editor" name="css" rows="7">{{ css }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="accounts-section">
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="verify_emails" name="verify_emails" type="checkbox" {% if verify_emails %}checked{% endif %}>
|
||||
Only allow users with verified emails
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="view_challenges_unregistered" name="view_challenges_unregistered" type="checkbox" {% if view_challenges_unregistered %}checked{% endif %}>
|
||||
Unregistered users can view challenges
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="view_scoreboard_if_authed" name="view_scoreboard_if_authed" type="checkbox"
|
||||
{% if view_scoreboard_if_authed %}checked{% endif %}>
|
||||
|
@ -93,14 +111,14 @@
|
|||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="prevent_registration" name="prevent_registration" type="checkbox" {% if prevent_registration %}checked{% endif %}>
|
||||
Prevent public registration
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="prevent_name_change" name="prevent_name_change" type="checkbox" {% if prevent_name_change %}checked{% endif %}>
|
||||
Prevent team name changes
|
||||
|
@ -108,17 +126,17 @@
|
|||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="email-section">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#mailserver" aria-controls="mailserver" role="tab" data-toggle="tab">Mail Server</a>
|
||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||
<li class="nav-item" class="active">
|
||||
<a class="nav-link active" href="#mailserver" aria-controls="mailserver" role="tab" data-toggle="tab">Mail Server</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#mailgun" aria-controls="mailgun" role="tab" data-toggle="tab">Mailgun</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#mailgun" aria-controls="mailgun" role="tab" data-toggle="tab">Mailgun</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start">Mail From Address:</label>
|
||||
<label>Mail From Address:</label>
|
||||
<input class="form-control" id='mailfrom_addr' name='mailfrom_addr' type='text'
|
||||
placeholder="Email address used to send email"
|
||||
{% if mailfrom_addr is defined and mailfrom_addr != None %}value="{{ mailfrom_addr }}"{% endif %}>
|
||||
|
@ -128,19 +146,19 @@
|
|||
<div role="tabpanel" class="tab-pane active" id="mailserver">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start">Mail Server Address:</label>
|
||||
<label>Mail Server Address:</label>
|
||||
<input class="form-control" id='mail_server' name='mail_server' type='text'
|
||||
placeholder="Mail Server Address"
|
||||
{% if mail_server is defined and mail_server != None %}value="{{ mail_server }}"{% endif %}>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start">Mail Server Port:</label>
|
||||
<label>Mail Server Port:</label>
|
||||
<input class="form-control" id='mail_port' name='mail_port' type='text'
|
||||
placeholder="Mail Server Port"
|
||||
{% if mail_port is defined and mail_port != None %}value="{{ mail_port }}"{% endif %}>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="mail_useauth" name="mail_useauth" type="checkbox" {% if mail_useauth %}checked{% endif %}>
|
||||
Use Mail Server Username and Password
|
||||
|
@ -148,7 +166,7 @@
|
|||
</div>
|
||||
<div id="mail_username_password">
|
||||
<div class="form-group">
|
||||
<label for="start">Username:</label>
|
||||
<label>Username:</label>
|
||||
{% if mail_username is defined and mail_username != None %}
|
||||
<label for="mail_u"><sup>A mail server username is currently set</sup></label>
|
||||
{% endif %}
|
||||
|
@ -167,13 +185,13 @@
|
|||
<br>
|
||||
<br>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="mail_ssl" name="mail_ssl" type="checkbox" {% if mail_ssl %}checked{% endif %}>
|
||||
SSL
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="mail_tls" name="mail_tls" type="checkbox" {% if mail_tls %}checked{% endif %}>
|
||||
TLS
|
||||
|
@ -183,13 +201,13 @@
|
|||
<div role="tabpanel" class="tab-pane" id="mailgun">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start">Mailgun API Base URL:</label>
|
||||
<label>Mailgun API Base URL:</label>
|
||||
<input class="form-control" id='mg_base_url' name='mg_base_url' type='text'
|
||||
placeholder="Mailgun API Base URL"
|
||||
{% if mg_base_url is defined and mg_base_url != None %}value="{{ mg_base_url }}"{% endif %}>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="start">Mailgun API Key:</label>
|
||||
<label>Mailgun API Key:</label>
|
||||
<input class="form-control" id='mg_api_key' name='mg_api_key' type='text'
|
||||
placeholder="Mailgun API Key"
|
||||
{% if mg_api_key is defined and mg_api_key != None %}value="{{ mg_api_key }}"{% endif %}>
|
||||
|
@ -198,48 +216,54 @@
|
|||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="ctftime-section">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#start-date" aria-controls="start-date" role="tab" data-toggle="tab">Start Time</a>
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#start-date" aria-controls="start-date" role="tab" data-toggle="tab">Start Time</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#end-date" aria-controls="end-date" role="tab" data-toggle="tab">End Time</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#end-date" aria-controls="end-date" role="tab" data-toggle="tab">End Time</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#freeze-date" aria-controls="freeze-date" role="tab" data-toggle="tab">Freeze Time</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" 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>
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="start-date">
|
||||
<div class="row" id="start-date">
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="col-md-12">
|
||||
<p>This is the time when the competition will begin. Challenges will automatically
|
||||
unlock and users will be able to submit answers.</p>
|
||||
<sub class="text-right mb-3">* All time fields required</sub>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="start-month">Month:</label>
|
||||
<input class="form-control start-date" id='start-month' name='start-month' min="0" max="12" type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-2">
|
||||
<label for="start-day">Day:</label>
|
||||
<input class="form-control start-date" id='start-day' name='start-day' min="0" max="31" type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-3">
|
||||
<label for="start-year">Year:</label>
|
||||
<input class="form-control start-date" id='start-year' name='start-year' type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-2">
|
||||
<label for="start-hour">Hour:</label>
|
||||
<input class="form-control start-date" id='start-hour' name='start-hour' min="0" max="23" type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-3">
|
||||
<label for="start-minute">Minute:</label>
|
||||
<input class="form-control start-date" id='start-minute' name='start-minute' min="0" max="59" type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-4">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="start-timezone">Timezone:</label>
|
||||
<select class="form-control start-date" id="start-timezone">
|
||||
<script>
|
||||
|
@ -252,20 +276,20 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-12">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="start-local">Local Time:</label>
|
||||
<input class="form-control" id='start-local' type='text'
|
||||
placeholder="Start Date (Local Time)" readonly>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-12">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="start-zonetime">Timezone Time:</label>
|
||||
<input class="form-control" id='start-zonetime' type='text'
|
||||
placeholder="Start Date (Timezone Time)" readonly>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-12">
|
||||
<label for="start">UTC Timestamp:</label>
|
||||
<div class="form-group col-md-12">
|
||||
<label>UTC Timestamp:</label>
|
||||
<input class="form-control" id='start' name='start' type='text'
|
||||
placeholder="Start Date (UTC timestamp)"
|
||||
{% if start is defined and start != None %}value="{{ start }}"{% endif %} readonly>
|
||||
|
@ -274,36 +298,42 @@
|
|||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="end-date">
|
||||
<div class="row" id="end-date">
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="col-md-12">
|
||||
<p>This is the time when the competition will end. Challenges will automatically
|
||||
close and users won't be able to submit answers.</p>
|
||||
<sub class="text-right mb-3">* All time fields required</sub>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="end-month">Month:</label>
|
||||
<input class="form-control end-date" id='end-month' name='end-month' min="0" max="12"
|
||||
type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-2">
|
||||
<label for="end-day">Day:</label>
|
||||
<input class="form-control end-date" id='end-day' name='end-day' min="0" max="31"
|
||||
type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-4">
|
||||
<label for="end-year">Year:</label>
|
||||
<input class="form-control end-date" id='end-year' name='end-year' type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-2">
|
||||
<label for="end-hour">Hour:</label>
|
||||
<input class="form-control end-date" id='end-hour' name='end-hour' min="0" max="23"
|
||||
type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-2">
|
||||
<label for="end-minute">Minute:</label>
|
||||
<input class="form-control end-date" id='end-minute' name='end-minute' min="0"
|
||||
max="59" type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-4">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="end-timezone">Timezone:</label>
|
||||
<select class="form-control end-date" id="end-timezone">
|
||||
<script>
|
||||
|
@ -316,19 +346,19 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-12">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="end-local">Local Time:</label>
|
||||
<input class="form-control" id='end-local' type='text'
|
||||
placeholder="End Date (Local Time)" readonly>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-12">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="end-zonetime">Timezone Time:</label>
|
||||
<input class="form-control" id='end-zonetime' type='text'
|
||||
placeholder="End Date (Timezone Time)" readonly>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-12">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="end">UTC Timestamp:</label>
|
||||
<input class="form-control" id='end' name='end' type='text'
|
||||
placeholder="End Date (UTC timestamp)"
|
||||
|
@ -338,36 +368,42 @@
|
|||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="freeze-date">
|
||||
<div class="row" id="freeze-date">
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="col-md-12">
|
||||
<p>Freeze time specifies the timestamp that the competition will be frozen to.
|
||||
All solves before the freeze time will be shown, but new solves won't be shown to users. </p>
|
||||
<sub class="text-right mb-3">* All time fields required</sub>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="freeze-month">Month:</label>
|
||||
<input class="form-control freeze-date" id='freeze-month' name='freeze-month' min="0" max="12"
|
||||
type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-2">
|
||||
<label for="freeze-day">Day:</label>
|
||||
<input class="form-control freeze-date" id='freeze-day' name='freeze-day' min="0" max="31"
|
||||
type='number'>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-2">
|
||||
<div class="form-group col-md-4">
|
||||
<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">
|
||||
<div class="form-group col-md-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">
|
||||
<div class="form-group col-md-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">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="freeze-timezone">Timezone:</label>
|
||||
<select class="form-control freeze-date" id="freeze-timezone">
|
||||
<script>
|
||||
|
@ -380,19 +416,19 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-12">
|
||||
<div class="form-group col-md-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">
|
||||
<div class="form-group col-md-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">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="freeze">UTC Timestamp:</label>
|
||||
<input class="form-control" id='freeze' name='freeze' type='text'
|
||||
placeholder="Freeze Date (UTC timestamp)"
|
||||
|
@ -402,91 +438,87 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input id="view_after_ctf" name="view_after_ctf" type="checkbox" {% if view_after_ctf %}checked{% endif %}>
|
||||
Allow challenges to be viewed after CTF end
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div role="tabpanel" class="tab-pane" id="backup-section">
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li role="presentation" class="active">
|
||||
<a href="#export-ctf" aria-controls="export-ctf" role="tab" data-toggle="tab">Export</a>
|
||||
<ul class="nav nav-tabs mb-3" role="tablist">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link active" href="#export-ctf" aria-controls="export-ctf" role="tab" data-toggle="tab">Export</a>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<a href="#import-ctf" aria-controls="import-ctf" role="tab" data-toggle="tab">Import</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#import-ctf" aria-controls="import-ctf" role="tab" data-toggle="tab">Import</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="export-ctf">
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-8">
|
||||
<div class="checkbox">
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input class="export-config" value="challenges" type="checkbox" checked>Challenges
|
||||
<input class="export-config" value="challenges" type="checkbox" checked> Challenges
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input class="export-config" value="teams" type="checkbox" checked>Teams
|
||||
<input class="export-config" value="teams" type="checkbox" checked> Teams
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input class="export-config" value="both" type="checkbox" checked>Solves, Wrong Keys, Unlocks
|
||||
<input class="export-config" value="both" type="checkbox" checked> Solves, Wrong Keys, Unlocks
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input class="export-config" value="metadata" type="checkbox" checked>Configuration
|
||||
<input class="export-config" value="metadata" type="checkbox" checked> Configuration
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<a href="{{ request.script_root }}/admin/export" id="export-button" class="btn btn-default">Export</a>
|
||||
</div>
|
||||
<a href="{{ request.script_root }}/admin/export" id="export-button" class="btn btn-warning">Export</a>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="import-ctf">
|
||||
<div class="row" id="import-div">
|
||||
<br>
|
||||
<div class="form-group col-xs-8">
|
||||
<label for="container-files">Import File
|
||||
<input type="file" name="backup" id="import-file" accept=".zip">
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<label for="container-files">Import File</label>
|
||||
<input class="form-control-file" type="file" name="backup" id="import-file" accept=".zip">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-xs-8">
|
||||
<div class="checkbox">
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input class="import-config" value="challenges" type="checkbox" checked>Challenges
|
||||
<input class="import-config" value="challenges" type="checkbox" checked> Challenges
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input class="import-config" value="teams" type="checkbox" checked>Teams
|
||||
<input class="import-config" value="teams" type="checkbox" checked> Teams
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input class="import-config" value="both" type="checkbox" checked>Solves, Wrong Keys, Unlocks
|
||||
<input class="import-config" value="both" type="checkbox" checked> Solves, Wrong Keys, Unlocks
|
||||
</label>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<div class="form-check">
|
||||
<label>
|
||||
<input class="import-config" value="metadata" type="checkbox" checked>Configuration
|
||||
<input class="import-config" value="metadata" type="checkbox" checked> Configuration
|
||||
</label>
|
||||
</div>
|
||||
<input id="import-button" type="submit" class="btn btn-default" value="Import">
|
||||
</div>
|
||||
</div>
|
||||
<input id="import-button" type="submit" class="btn btn-warning" value="Import">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" id="submit" tabindex="5"
|
||||
class="btn btn-md btn-primary float-right">Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,57 +1,50 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
.btn-primary { background-color: #337ab7; }
|
||||
.btn-danger { background-color: #d9534f; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<h1>Correct Key Submissions</h1>
|
||||
<div id="confirm" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<h3>Delete Key</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="">
|
||||
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<div class="small-6 small-centered text-center columns">
|
||||
<p>Are you sure you want to delete successful key submission for team: <strong id="confirm-team-name"></strong> in challenge: <strong id="confirm-chal-name"></strong>?</p>
|
||||
<a onclick="$('#confirm').modal('hide')" class="btn btn-primary">No</a>
|
||||
<button class="btn btn-danger" id="delete-solve" type="button">Yes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Correct Submissions</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table id="teamsboard" class=" table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Team</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Challenge</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Date</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Key Submitted</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Delete</b>
|
||||
</td>
|
||||
<td class="text-center"><b>ID</b></td>
|
||||
<td><b>Team</b></td>
|
||||
<td><b>Challenge</b></td>
|
||||
<td><b>Submission</b></td>
|
||||
<td class="text-center"><b>Date</b></td>
|
||||
<td class="text-center"><b>Delete</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solve in solves %}
|
||||
<tr>
|
||||
<td class="text-center team" id="{{ solve.teamid }}"><a href="{{ request.script_root }}/admin/team/{{ solve.teamid }}">{{ solve.team_name }}</a>
|
||||
<td class="text-center chal" id="{{ solve.chalid }}">{{ solve.chal_name }}</td>
|
||||
<td class="text-center solve-time"><script>document.write( moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
|
||||
<td class="text-center flag" id="{{ solve.id }}">{{ solve.flag }}</td>
|
||||
<td class="text-center" id="{{ solve.id }}">
|
||||
{{ solve.id }}
|
||||
</td>
|
||||
<td class="team" id="{{ solve.teamid }}">
|
||||
<a href="{{ request.script_root }}/admin/team/{{ solve.teamid }}">{{ solve.team_name }}</a>
|
||||
</td>
|
||||
<td class="chal" id="{{ solve.chalid }}">
|
||||
{{ solve.chal_name }}
|
||||
</td>
|
||||
<td class="flag" id="{{ solve.id }}">
|
||||
<pre class="mb-0">{{ solve.flag }}</pre>
|
||||
</td>
|
||||
<td class="text-center solve-time">
|
||||
<small>
|
||||
<script>document.write( moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
|
||||
</small>
|
||||
</td>
|
||||
<td class="text-center"><i class="fa fa-times"></i></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -68,43 +61,44 @@
|
|||
<b>{{ page }}</b>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/correct_keys/{{ curr_page+1 }}">>>></a>{% endif %}
|
||||
<a href="{{ request.script_root }}">
|
||||
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/correct_keys/{{ curr_page + 1 }}">>>></a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||
<script>
|
||||
$('#delete-solve').click(function(e){
|
||||
e.preventDefault();
|
||||
var solve = $('#confirm input[name="solve"]').val()
|
||||
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
|
||||
var data = $.parseJSON(JSON.stringify(data))
|
||||
if (data == "1"){
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
});
|
||||
<script>
|
||||
var nonce = "{{ nonce }}";
|
||||
|
||||
function load_confirm_modal(key_id, team_name, chal_name){
|
||||
var modal = $('#confirm')
|
||||
modal.find('#confirm-team-name').text(team_name)
|
||||
modal.find('#confirm-chal-name').text(chal_name)
|
||||
$('#confirm form').attr('action', '{{ request.script_root }}/admin/solves/'+key_id+'/delete');
|
||||
$('#confirm').modal('show');
|
||||
}
|
||||
|
||||
$('.fa-times').click(function(){
|
||||
$('.fa-times').click(function(){
|
||||
var elem = $(this).parent().parent();
|
||||
var chal = elem.find('.chal').attr('id');
|
||||
var chal_name = elem.find('.chal').text().trim();
|
||||
var team = elem.find('.team').attr('id');
|
||||
var team_name = elem.find('.team').text().trim();
|
||||
var key_id = elem.find('.flag').attr('id');
|
||||
load_confirm_modal(key_id, team_name, chal_name)
|
||||
|
||||
ezq({
|
||||
title: 'Delete Submission',
|
||||
body: "Are you sure you want to delete correct submission from {0} for challenge {1}".format(
|
||||
"<strong>" + htmlentities(team_name) + "</strong>",
|
||||
"<strong>" + htmlentities(chal_name) + "</strong>"
|
||||
),
|
||||
success: function () {
|
||||
var route = script_root + '/admin/solves/' + key_id + '/delete';
|
||||
$.post(route, {
|
||||
nonce: nonce,
|
||||
}, function (data) {
|
||||
var data = $.parseJSON(JSON.stringify(data));
|
||||
if (data == "1") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -2,14 +2,6 @@
|
|||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/themes/admin/static/css/vendor/codemirror.min.css">
|
||||
<style>
|
||||
.row-fluid { margin: 25px; padding-bottom: 25px; }
|
||||
.media-item-wrapper { height: 120px; margin: 5px;
|
||||
float: left; border: 1px solid #eee; text-align: center; text-overflow: ellipsis; overflow: hidden;}
|
||||
.media-item-wrapper > a > i {line-height: 90px;}
|
||||
.media-item-title{ font-size: 10px; }
|
||||
#media-item{display: none;}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -18,43 +10,60 @@
|
|||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h3 class="text-center">Media Library</h3>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-md-offset-1" id="media-library-list">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Media Library</h3>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-header">
|
||||
<div class="container">
|
||||
<div class="row mh-100">
|
||||
<div class="col-md-8" id="media-library-list">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4 class="text-center">Media Details</h4>
|
||||
<div id="media-item">
|
||||
<div class="row text-center" id="media-icon">
|
||||
<div class="text-center" id="media-icon">
|
||||
</div>
|
||||
<br>
|
||||
<div class="row text-center" id="media-filename">
|
||||
<div class="text-center" id="media-filename">
|
||||
</div>
|
||||
<br>
|
||||
<div class="row form-group">
|
||||
Link: <input class="form-control" type="text" id="media-link">
|
||||
<div class="form-group">
|
||||
Link: <input class="form-control" type="text" id="media-link" readonly>
|
||||
</div>
|
||||
|
||||
<div class="row form-group text-center">
|
||||
<button class="btn btn-success" id="media-insert">Insert</button>
|
||||
<div class="form-group text-center">
|
||||
<button class="btn btn-success w-100" id="media-insert">Insert</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<form id="media-library-upload">
|
||||
<div class="form-group">
|
||||
<label for="media-files">Upload Files
|
||||
<label for="media-files">
|
||||
Upload Files
|
||||
</label>
|
||||
<input type="file" name="files[]" id="media-files" multiple>
|
||||
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
|
||||
<input type="file" name="files[]" id="media-files" class="form-control-file" multiple>
|
||||
<sub class="help-block">
|
||||
Attach multiple files using Control+Click or Cmd+Click.
|
||||
</sub>
|
||||
</div>
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
</form>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="pull-right">
|
||||
<button type="submit" class="btn btn-primary" onclick="uploadfiles();">Upload</button>
|
||||
</div>
|
||||
|
@ -64,11 +73,14 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="container">
|
||||
<div class="row pt-5">
|
||||
<div class="col-md-12">
|
||||
<div class="row">
|
||||
{% for error in errors %}
|
||||
<div class="large-8 large-centered columns">
|
||||
<div data-alert class="alert-box alert radius centered text-center">
|
||||
<div class="large-12 large-centered columns">
|
||||
<div data-alert class="alert-box alert centered text-center">
|
||||
<span>{{ error }}</span>
|
||||
<a href="#" class="close">×</a>
|
||||
</div>
|
||||
|
@ -77,59 +89,92 @@
|
|||
</div>
|
||||
|
||||
<form id="page-edit" method="POST">
|
||||
<div class="row-fluid">
|
||||
<div class="form-group">
|
||||
<div class="col-md-12">
|
||||
<h3>Route: </h3>
|
||||
<p class="help-block">This is the URL route that your page will be at (e.g. /page)</p>
|
||||
<h3>Title</h3>
|
||||
<p class="text-muted">This is the title shown on the navigation bar</p>
|
||||
<input class="form-control radius" id="route" type="text" name="title"
|
||||
value="{% if page is defined %}{{ page.title }}{% endif %}" placeholder="Title">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-12">
|
||||
<h3>Route</h3>
|
||||
<p class="text-muted">This is the URL route that your page will be at (e.g. /page). You can also enter links to link to that page.</p>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input class="form-control radius" id="route" type="text" name="route" value="{% if page is defined %}{{ page.route }}{% endif %}" placeholder="Route">
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label float-right">
|
||||
<input class="form-check-input" type="checkbox" name="auth_required" {% if page is defined and page.auth_required %}checked{% endif %}>
|
||||
Users must be logged in to see this page
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<div class="row-fluid">
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-12">
|
||||
|
||||
<h3>Content: </h3>
|
||||
<p class="help-block">This is the HTML content of your page</p>
|
||||
<h3>Content</h3>
|
||||
<p class="text-muted">This is the HTML content of your page</p>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="content-edit">
|
||||
<li role="presentation" class="active"><a href="#content-write" aria-controls="home" role="tab" data-toggle="tab">Write</a></li>
|
||||
<li role="presentation"><a href="#content-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a></li>
|
||||
<li class="nav-item" role="presentation" class="active">
|
||||
<a class="nav-link active" href="#content-write" aria-controls="home" role="tab" data-toggle="tab">Write</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link" onclick="preview_page(); return true;" href="#"">Preview</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="content-write" style="height:400px">
|
||||
<div class="form-group">
|
||||
<br>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="...">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default" id="media-button"><i class="fa fa-camera-retro" aria-hidden="true"></i> Media Library</button>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
|
||||
<div class="form-inline">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-primary" id="media-button">
|
||||
<i class="fa fa-camera-retro"></i>
|
||||
Media Library
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
|
||||
{% if page is defined %}
|
||||
<input name='id' type='hidden' value="{{ page.id }}">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="content-preview" style="height:400px">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-theme btn-outlined create-challenge pull-right">
|
||||
</div>
|
||||
|
||||
<div class="form-group float-right">
|
||||
<button class="btn btn-success" id="publish-page">
|
||||
Publish
|
||||
</button>
|
||||
<button class="btn btn-primary" id="save-page">
|
||||
{% if page is defined %}
|
||||
Update
|
||||
{% else %}
|
||||
Create
|
||||
Save
|
||||
{% endif %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<br> <!-- Don't compete with footer for bottom of page -->
|
||||
<br>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
@ -206,15 +251,17 @@
|
|||
var ext = f.location.split('.').pop();
|
||||
var fname = f.location.split('/')[1];
|
||||
|
||||
var wrapper = $('<div>').attr('class', 'media-item-wrapper col-md-2')
|
||||
var wrapper = $('<div>').attr('class', 'media-item-wrapper');
|
||||
|
||||
var link = $('<a>');
|
||||
link.attr('href', '##');
|
||||
|
||||
if (mapping[ext] == undefined)
|
||||
link.append('<i class="fa fa-file-o fa-4x" aria-hidden="true"></i>'.format(mapping[ext]));
|
||||
link.append('<i class="fa fa-file-o" aria-hidden="true"></i> '.format(mapping[ext]));
|
||||
else
|
||||
link.append('<i class="fa {0} fa-4x" aria-hidden="true"></i>'.format(mapping[ext]));
|
||||
link.append('<i class="fa {0}" aria-hidden="true"></i> '.format(mapping[ext]));
|
||||
|
||||
link.append($('<small>').attr('class', 'media-item-title').text(fname));
|
||||
|
||||
link.click(function(e){
|
||||
var media_div = $(this).parent();
|
||||
|
@ -222,14 +269,18 @@
|
|||
var f_loc = media_div.attr('data-location');
|
||||
var fname = media_div.attr('data-filename');
|
||||
$('#media-link').val(f_loc);
|
||||
$('#media-filename').text(fname);
|
||||
$('#media-filename').html(
|
||||
$('<a>').attr('href', f_loc).attr('target', '_blank').text(fname)
|
||||
);
|
||||
|
||||
$('#media-icon').empty()
|
||||
$('#media-icon').empty();
|
||||
if ($(icon).hasClass('fa-file-image-o')){
|
||||
$('#media-icon').append($('<img>').attr('src', f_loc).attr('width', '100%'));
|
||||
$('#media-icon').append($('<img>').attr('src', f_loc).css({'max-width':'100%', 'max-height': '100%', 'object-fit': 'contain'}));
|
||||
} else {
|
||||
// icon is empty so we need to pull outerHTML
|
||||
$('#media-icon').append(icon.outerHTML);
|
||||
var copy_icon = $(icon).clone();
|
||||
$(copy_icon).addClass('fa-4x');
|
||||
$('#media-icon').append(copy_icon);
|
||||
}
|
||||
$('#media-item').show();
|
||||
});
|
||||
|
@ -239,7 +290,7 @@
|
|||
wrapper.attr('data-filename', fname);
|
||||
list.append(wrapper);
|
||||
|
||||
wrapper.append($('<span>').attr('class', 'media-item-title').text(fname));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,8 +322,46 @@
|
|||
insert_at_cursor(editor, entry);
|
||||
});
|
||||
|
||||
$('#page-edit').submit(function (e){
|
||||
$(this).attr('action', '{{ request.script_root }}/admin/pages');
|
||||
|
||||
function submit_form(){
|
||||
editor.save(); // Save the CodeMirror data to the Textarea
|
||||
$.ajax({
|
||||
type: "POST",
|
||||
url: $("#page-edit").attr('action'),
|
||||
data: $("#page-edit").serialize(),
|
||||
success: function (data) {
|
||||
if (data.result == 'success'){
|
||||
if (data.operation == 'publish'){
|
||||
window.location = script_root + '/admin/pages'
|
||||
} else if (data.operation == 'save'){
|
||||
ezal({
|
||||
title: 'Saved',
|
||||
body: 'Your changes have been saved',
|
||||
button: 'Okay'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function preview_page(){
|
||||
editor.save(); // Save the CodeMirror data to the Textarea
|
||||
$('#page-edit').attr('action', '{{ request.script_root }}/admin/pages?operation=preview');
|
||||
$('#page-edit').attr('target', '_blank');
|
||||
$('#page-edit').submit();
|
||||
}
|
||||
|
||||
$('#publish-page').click(function (e) {
|
||||
e.preventDefault();
|
||||
$('#page-edit').attr('action', '{{ request.script_root }}/admin/pages?operation=publish');
|
||||
submit_form();
|
||||
});
|
||||
|
||||
$('#save-page').click(function (e) {
|
||||
e.preventDefault();
|
||||
$('#page-edit').attr('action', '{{ request.script_root }}/admin/pages?operation=save');
|
||||
submit_form();
|
||||
});
|
||||
|
||||
// Markdown Preview
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div id="solves-graph">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div>
|
||||
<div id="keys-pie-graph">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div id="categories-pie-graph">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<style>
|
||||
#solves-graph{
|
||||
display: block;
|
||||
height: 500px;
|
||||
}
|
||||
#keys-pie-graph{
|
||||
height: 400px;
|
||||
display: block;
|
||||
}
|
||||
#categories-pie-graph{
|
||||
height: 400px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/plotly.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
// $.distint(array)
|
||||
// Unique elements in array
|
||||
$.extend({
|
||||
distinct : function(anArray) {
|
||||
var result = [];
|
||||
$.each(anArray, function(i,v){
|
||||
if ($.inArray(v, result) == -1) result.push(v);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
// Praise Resig: http://ejohn.org/blog/fast-javascript-maxmin/
|
||||
Array.max = function( array ){
|
||||
return Math.max.apply( Math, array );
|
||||
};
|
||||
|
||||
function colorhash (x) {
|
||||
color = ""
|
||||
for (var i = 20; i <= 60; i+=20){
|
||||
x += i
|
||||
x *= i
|
||||
color += x.toString(16)
|
||||
};
|
||||
return "#" + color.substring(0, 6)
|
||||
}
|
||||
|
||||
function solves_graph() {
|
||||
$.get('{{ request.script_root }}/admin/graphs/solves', function (data) {
|
||||
var solves = $.parseJSON(JSON.stringify(data));
|
||||
var chals = [];
|
||||
var counts = [];
|
||||
var colors = [];
|
||||
var annotations = [];
|
||||
var i = 1;
|
||||
solves_order = Object.keys(solves).sort(function (a, b) {
|
||||
return solves[b] - solves[a]
|
||||
});
|
||||
$.each(solves_order, function (key, value) {
|
||||
chals.push(value);
|
||||
counts.push(solves[value]);
|
||||
var result = {
|
||||
x: value,
|
||||
y: solves[value],
|
||||
text: solves[value],
|
||||
xanchor: 'center',
|
||||
yanchor: 'bottom',
|
||||
showarrow: false
|
||||
};
|
||||
annotations.push(result);
|
||||
colors.push(colorhash(i++));
|
||||
});
|
||||
|
||||
var data = [{
|
||||
type: 'bar',
|
||||
x: chals,
|
||||
y: counts,
|
||||
text: counts,
|
||||
orientation: 'v'
|
||||
}];
|
||||
|
||||
var layout = {
|
||||
title: 'Score Counts',
|
||||
annotations: annotations
|
||||
};
|
||||
|
||||
$('#solves-graph').empty()
|
||||
Plotly.newPlot('solves-graph', data, layout);
|
||||
});
|
||||
}
|
||||
|
||||
function keys_percentage_graph(){
|
||||
// Solves and Fails pie chart
|
||||
$.get('{{ request.script_root }}/admin/fails/all', function(data){
|
||||
var res = $.parseJSON(JSON.stringify(data));
|
||||
var solves = res['solves'];
|
||||
var fails = res['fails'];
|
||||
|
||||
var data = [{
|
||||
values: [solves, fails],
|
||||
labels: ['Solves', 'Fails'],
|
||||
marker: {
|
||||
colors: [
|
||||
"rgb(0, 209, 64)",
|
||||
"rgb(207, 38, 0)"
|
||||
]
|
||||
},
|
||||
hole: .4,
|
||||
type: 'pie'
|
||||
}];
|
||||
|
||||
var layout = {
|
||||
title: 'Key Percentages'
|
||||
};
|
||||
|
||||
$('#keys-pie-graph').empty();
|
||||
Plotly.newPlot('keys-pie-graph', data, layout);
|
||||
});
|
||||
}
|
||||
|
||||
function category_breakdown_graph(){
|
||||
$.get('{{ request.script_root }}/admin/graphs/categories', function(data){
|
||||
res = $.parseJSON(JSON.stringify(data));
|
||||
res = res['categories']
|
||||
|
||||
categories = [];
|
||||
count = [];
|
||||
for (var i = 0; i < res.length; i++) {
|
||||
categories.push(res[i].category)
|
||||
count.push(res[i].count)
|
||||
};
|
||||
|
||||
var data = [{
|
||||
values: count,
|
||||
labels: categories,
|
||||
hole: .4,
|
||||
type: 'pie'
|
||||
}];
|
||||
|
||||
var layout = {
|
||||
title: 'Category Breakdown'
|
||||
};
|
||||
|
||||
$('#categories-pie-graph').empty();
|
||||
Plotly.newPlot('categories-pie-graph', data, layout);
|
||||
});
|
||||
}
|
||||
|
||||
function update(){
|
||||
solves_graph();
|
||||
keys_percentage_graph();
|
||||
category_breakdown_graph();
|
||||
}
|
||||
|
||||
$(function() {
|
||||
update();
|
||||
window.onresize = function () {
|
||||
console.log('resizing')
|
||||
Plotly.Plots.resize(document.getElementById('keys-pie-graph'));
|
||||
Plotly.Plots.resize(document.getElementById('categories-pie-graph'));
|
||||
Plotly.Plots.resize(document.getElementById('solves-graph'));
|
||||
};
|
||||
});
|
||||
|
||||
setInterval(update, 300000);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -0,0 +1,2 @@
|
|||
<div id="challenge-preview" class="modal fade" tabindex="-1">
|
||||
</div>
|
|
@ -0,0 +1,47 @@
|
|||
<div id="update-files" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Files</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form method="POST" action="{{ request.script_root }}/admin/files/" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="files-chal" name='chal' type='hidden'>
|
||||
<input name='method' type='hidden' value='upload'>
|
||||
<div id="current-files" class="pb-3">
|
||||
|
||||
</div>
|
||||
<input type="hidden" name="method" value="upload">
|
||||
<input class="form-control-file" type="file" name="files[]" multiple="multiple">
|
||||
<sub>Attach multiple files using Control+Click or Cmd+Click</sub>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button class="btn btn-primary float-right" id="submit-files">Upload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,111 @@
|
|||
<div id="hint-modal" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 id="hint-modal-title"></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<form id="hint-modal-submit" method='POST'>
|
||||
<div class="modal-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<ul class="nav nav-tabs" role="tablist" id="new-hint-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#hint-write" aria-controls="home" role="tab" data-toggle="tab">Write</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#hint-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="hint-write">
|
||||
<div class="form-group">
|
||||
<label class="text-muted">
|
||||
<small>Markdown & HTML are supported</small>
|
||||
</label>
|
||||
<textarea id="hint-modal-hint" type="text" class="form-control" name="hint"
|
||||
placeholder="Hint" rows="7"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="hint-preview"
|
||||
style="height:234px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input id="hint-modal-cost" type="number" class="form-control" name="cost" placeholder="Cost">
|
||||
</div>
|
||||
<input type="hidden" id="chal-id-for-hint" name="chal">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button class="btn btn-primary float-right" id="hint-modal-button">Add Hint</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="update-hints" class="modal fade" tabindex="-1" chal-id="">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Hints</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container text-center">
|
||||
<a href="#" id="create-hint" class="btn btn-primary text-center">New Hint</a>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class='current-hints'>
|
||||
<table id="hintsboard" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Hint</b></td>
|
||||
<td class="text-center"><b>Cost</b></td>
|
||||
<td class="text-center"><b>Settings</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,71 @@
|
|||
|
||||
|
||||
<div id="update-keys" class="modal fade" tabindex="-1" chal-id="">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Flags</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-md-12">
|
||||
<button id="create-key" class="btn btn-primary d-inline-block">New Flag</button>
|
||||
<form method="POST" action="{{ request.script_root }}/admin/keys">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="keys-chal" name='chal' type='hidden'>
|
||||
<div id="current-keys" class="row"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="create-keys" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<input type="hidden" class="chal-id" name="chal-id">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Create Flag</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="create-keys-select-div">
|
||||
<label for="create-keys-select" class="control-label">Choose Flag Type</label>
|
||||
<select class="form-control" id="create-keys-select">
|
||||
</select>
|
||||
</div>
|
||||
<br>
|
||||
<div id="create-keys-entry-div">
|
||||
</div>
|
||||
<br>
|
||||
<div style="display:none;" id="create-keys-button-div" class="text-center">
|
||||
<button id="create-keys-submit" class="btn btn-success">Create Flag</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="edit-keys" class="modal fade" tabindex="-1">
|
||||
</div>
|
|
@ -0,0 +1,49 @@
|
|||
<div id="update-tags" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Tags</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label for="tag-insert">Value</label>
|
||||
<input max-length="80" type="text" class="form-control tag-insert" name="tag-insert" placeholder="Type tag and press Enter">
|
||||
</div>
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
<input id="tags-chal" name='chal' type='hidden'>
|
||||
|
||||
<div id="current-tags">
|
||||
|
||||
</div>
|
||||
<br/>
|
||||
<div id="chal-tags">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button class="btn btn-primary float-right" id="submit-tags">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -5,14 +5,16 @@
|
|||
<title>Admin Panel</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{{ request.script_root }}/themes/original/static/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="icon" href="{{ request.script_root }}/themes/original/static/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="shortcut icon" href="{{ request.script_root }}/themes/core/static/img/favicon.ico"
|
||||
type="image/x-icon">
|
||||
<link rel="icon" href="{{ request.script_root }}/themes/core/static/img/favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/vendor/font-awesome/css/font-awesome.min.css" />
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/style.css">
|
||||
<link href='{{ request.script_root }}/themes/admin/static/css/vendor/lato.css' rel='stylesheet' type='text/css'>
|
||||
<link href='{{ request.script_root }}/themes/admin/static/css/vendor/raleway.css' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/themes/admin/static/css/style.css">
|
||||
<link rel="stylesheet"
|
||||
href="{{ request.script_root }}/themes/admin/static/css/vendor/font-awesome/css/font-awesome.min.css"/>
|
||||
<link rel="stylesheet" href='{{ request.script_root }}/themes/admin/static/css/vendor/font.css'>
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/jumbotron.css">
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/sticky-footer.css">
|
||||
<link rel="stylesheet" href="{{ request.script_root }}/themes/admin/static/css/base.css">
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment-timezone-with-data.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/handlebars.min.js"></script>
|
||||
|
@ -23,54 +25,84 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<div class="body-container">
|
||||
<div class="navbar navbar-inverse home">
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button class="navbar-toggle" data-target=".navbar-collapse" data-toggle="collapse" type="button">
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a href="{{ request.script_root }}/" class="navbar-brand">CTFd</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#base-navbars"
|
||||
aria-controls="base-navbars" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="base-navbars">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/graphs">Graphs</a></li>
|
||||
<li class="nav-item dropdown">
|
||||
<a href="#" class="nav-link dropdown-toggle" data-toggle="dropdown" role="button"
|
||||
aria-haspopup="true" aria-expanded="true">Pages</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" href="{{ request.script_root }}/admin/pages">All Pages</a>
|
||||
<a class="dropdown-item" href="{{ request.script_root }}/admin/pages?mode=create">New Page</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse" aria-expanded="false" style="height: 0px">
|
||||
<ul class="nav navbar-nav navbar-nav-right">
|
||||
<li><a href="{{ request.script_root }}/admin/graphs">Graphs</a></li>
|
||||
<li>
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true"
|
||||
aria-expanded="false">Pages <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="{{ request.script_root }}/admin/pages">All Pages</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/pages?mode=create">New Page</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="{{ request.script_root }}/admin/teams">Teams</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/chals">Challenges</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
|
||||
<li><a href="{{ request.script_root }}/admin/config">Config</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/teams">Teams</a></li>
|
||||
<li class="nav-item"><a class="nav-link"
|
||||
href="{{ request.script_root }}/admin/scoreboard">Scoreboard</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/chals">Challenges</a>
|
||||
</li>
|
||||
<li class="nav-item"><a class="nav-link"
|
||||
href="{{ request.script_root }}/admin/statistics">Statistics</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="{{ request.script_root }}/admin/config">Config</a></li>
|
||||
|
||||
{% set plugin_menu = get_admin_plugin_menu_bar() %}
|
||||
{% set plugins = get_configurable_plugins() %}
|
||||
{% if plugin_menu or plugins %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-none d-md-block d-lg-block">|</a>
|
||||
</li>
|
||||
|
||||
{% for menu in plugin_menu %}
|
||||
<li class="nav-item"><a class="nav-link"
|
||||
href="{{ request.script_root }}{{ menu.route }}">{{ menu.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if plugins %}
|
||||
<li>
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
||||
aria-haspopup="true" aria-expanded="false">Plugins <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
{% for plugin in get_configurable_plugins() %}
|
||||
<li><a href="{{ request.script_root }}/admin/plugins/{{ plugin }}">{{ plugin }}</a></li>
|
||||
{% for plugin in plugins %}
|
||||
<li class="nav-item"><a class="nav-link" href="
|
||||
{{ request.script_root }}{{ plugin.route }}">{{ plugin.name }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container">
|
||||
{{ content | safe }}
|
||||
<main role="main">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container text-center">
|
||||
<a href="https://ctfd.io">
|
||||
<small class="text-muted">Powered by CTFd</small>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.min.js"></script>
|
||||
{% block scripts %} {% endblock %}
|
||||
</footer>
|
||||
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/jquery.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/marked.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/bootstrap.bundle.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/main.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/style.js"></script>
|
||||
{% block scripts %} {% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -1,78 +1,65 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ request.script_root }}/themes/admin/static/css/vendor/codemirror.min.css">
|
||||
<style>
|
||||
.CodeMirror {
|
||||
padding-left: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>Pages</h1>
|
||||
<div id="confirm" class="modal fade" tabindex="-1" data-width="760">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="text-center">Delete Page</h2>
|
||||
</div>
|
||||
<div class="modal-body" style="height:110px">
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-12">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="route">
|
||||
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<div class="small-6 small-centered text-center columns">
|
||||
<p>Are you sure you want to delete <strong id="confirm-route-name"></strong>?</p>
|
||||
<button type="button" data-dismiss="modal" class="btn btn-theme btn-outlined">No</button>
|
||||
<button type="button" id="delete-route" class="btn btn-theme btn-outlined">Yes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Pages
|
||||
<a class="no-decoration" href="{{ request.script_root }}/admin/pages?operation=create">
|
||||
<i class="fa fa-plus-circle create-page" role="button" data-toggle="tooltip" title="Create Page"></i>
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3>Pages</h3>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table id="pages" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Title</b></td>
|
||||
<td><b>Route</b></td>
|
||||
<td class="text-center" style="width: 150px;"><b>Settings</b></td>
|
||||
<td><b>Authentication</b></td>
|
||||
<td class="text-center"><b>Published</b></td>
|
||||
<td class="text-center" width="10px"><b>Settings</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for route in routes %}
|
||||
<tr name="{{ route.route }}">
|
||||
<td class="route-name"><a
|
||||
href="{{ request.script_root }}/admin/pages?route={{ route.route }}">{{ route.route }}</a></td>
|
||||
<td class="text-center"><i class="fa fa-times"></i></td>
|
||||
{% for page in pages %}
|
||||
<tr>
|
||||
<td class="page-title">
|
||||
{{ page.title }}
|
||||
</td>
|
||||
<td class="page-route" page-id="{{ page.id }}" page-route="{{ page.route }}">
|
||||
<a href="{{ request.script_root }}/admin/pages?id={{ page.id }}">
|
||||
{{ page.route }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="page-authentication">
|
||||
{% if page.auth_required %}
|
||||
Required
|
||||
{% else %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if page.draft %}
|
||||
Draft
|
||||
{% else %}
|
||||
Published
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a class="no-decoration" href="{{ request.script_root }}/admin/pages?id={{ page.id }}"><i class="fa fa-edit"></i></a>
|
||||
<i class="fa fa-times" page-id="{{ page.id }}" page-route="{{ page.route }}" page-title="{{ page.title }}"></i>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a href="{{ request.script_root }}/admin/pages?mode=create">
|
||||
<button type="button" id="submit" class="btn btn-md btn-primary btn-theme btn-outlined">
|
||||
Add Page
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h3>CSS editor</h3>
|
||||
<form method="POST">
|
||||
<textarea id="pages-editor" name="css" style="padding-left:20px; visibility: hidden;">{{ css }}</textarea>
|
||||
<button onclick="save_css()" type="button" id="submit" class="btn btn-md btn-primary btn-theme btn-outlined">
|
||||
Update CSS
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -80,46 +67,31 @@
|
|||
{% block scripts %}
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/codemirror.min.js"></script>
|
||||
<script>
|
||||
var editor = CodeMirror.fromTextArea(document.getElementById("pages-editor"), {
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
mode: "css",
|
||||
htmlMode: false,
|
||||
theme: 'elegant'
|
||||
});
|
||||
|
||||
$('#delete-route').click(function(e){
|
||||
e.preventDefault();
|
||||
var route = $('#confirm input[name="route"]').val()
|
||||
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
|
||||
var data = $.parseJSON(JSON.stringify(data))
|
||||
if (data == "1"){
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function load_confirm_modal(route){
|
||||
var modal = $('#confirm')
|
||||
modal.find('input[name=route]').val(route)
|
||||
modal.find('#confirm-route-name').text(route)
|
||||
$('#confirm form').attr('action', '{{ request.script_root }}/admin/pages/delete');
|
||||
$('#confirm').modal();
|
||||
}
|
||||
|
||||
function save_css(){
|
||||
var css = editor.getValue();
|
||||
var nonce = $('#nonce').val();
|
||||
$.post(script_root + '/admin/css', {'css':css, 'nonce':nonce}, function(){
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
var nonce = "{{ nonce }}";
|
||||
|
||||
$('.fa-times').click(function(){
|
||||
var elem = $(this).parent().parent();
|
||||
var name = elem.find('.route-name').text().trim();
|
||||
load_confirm_modal(name)
|
||||
var elem = $(this);
|
||||
var name = elem.attr("page-route");
|
||||
var page_id = elem.attr("page-id");
|
||||
ezq({
|
||||
title: 'Delete '+ name,
|
||||
body: "Are you sure you want to delete {0}?".format(
|
||||
"<strong>" + htmlentities(name) + "</strong>"
|
||||
),
|
||||
success: function(){
|
||||
var page_delete_route = '{{ request.script_root }}/admin/pages/delete';
|
||||
$.post(page_delete_route, {
|
||||
route: name,
|
||||
id: page_id,
|
||||
nonce: nonce,
|
||||
}, function (data) {
|
||||
var data = $.parseJSON(JSON.stringify(data));
|
||||
if (data == "1") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Scoreboard</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table id="scoreboard" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td width="10px"><b>Place</b></td>
|
||||
<td><b>Team</b></td>
|
||||
<td><b>Score</b></td>
|
||||
<td><b>Status</b></td>
|
||||
<td><b>Visibility</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -20,13 +26,13 @@
|
|||
<td>{{ team.score }}</td>
|
||||
<td>
|
||||
{% if not team.banned %}
|
||||
<form method="POST" style="margin:0;" action="{{ request.script_root }}/admin/team/{{ team.teamid }}/ban">
|
||||
<a onclick="$(this).parent().submit()">Hide</a>
|
||||
<form method="POST" action="{{ request.script_root }}/admin/team/{{ team.teamid }}/ban">
|
||||
<button class="btn-sm btn-success" type="submit">Visible</button>
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce">
|
||||
</form>
|
||||
{%else %}
|
||||
<form method="POST" style="margin:0;" action="{{ request.script_root }}/admin/team/{{ team.teamid }}/unban">
|
||||
<a onclick="$(this).parent().submit()">Unhide</a>
|
||||
<form method="POST" action="{{ request.script_root }}/admin/team/{{ team.teamid }}/unban">
|
||||
<button class="btn-sm btn-danger" type="submit">Hidden</button>
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
@ -35,6 +41,8 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,24 +1,274 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row" style="text-align:center">
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Statistics</h1>
|
||||
|
||||
<h3><b>{{ team_count }}</b> teams registered</h3>
|
||||
<h3><b>{{ wrong_count }}</b> <a href="{{ request.script_root }}/admin/wrong_keys/1">wrong keys</a> submitted</h3>
|
||||
<h3><b>{{ solve_count }}</b> <a href="{{ request.script_root }}/admin/correct_keys/1">right keys</a> submitted</h3>
|
||||
<h3><b>{{ challenge_count }}</b> challenges</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-right">
|
||||
<h5><b>{{ team_count }}</b> teams registered</h5>
|
||||
<h5><b>{{ ip_count }}</b> IP addresses</h5>
|
||||
<hr>
|
||||
<h5><b>{{ challenge_count }}</b> challenges</h5>
|
||||
{% if most_solved %}
|
||||
<h3>Most solved: <b>{{ most_solved }}</b> with {{ solve_data[most_solved] }}</b> solves</h3>
|
||||
<h5><b>{{ most_solved }}</b> has the most solves with <br>{{ solve_data[most_solved] }} solves</h5>
|
||||
{% endif %}
|
||||
{% if least_solved %}
|
||||
<h3>Least solved: <b>{{ least_solved }}</b> with {{ solve_data[least_solved] }}</b> solves</h3>
|
||||
<h5><b>{{ least_solved }}</b> has the least solves with <br>{{ solve_data[least_solved] }} solves</h5>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div id="solves-graph">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div id="solve-percentages-graph">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div id="keys-pie-graph">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<h5><b>{{ solve_count }}</b> right submissions</h5>
|
||||
<h5><b>{{ wrong_count }}</b> wrong submissions</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div id="categories-pie-graph">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/plotly.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
// $.distint(array)
|
||||
// Unique elements in array
|
||||
$.extend({
|
||||
distinct: function (anArray) {
|
||||
var result = [];
|
||||
$.each(anArray, function (i, v) {
|
||||
if ($.inArray(v, result) == -1) result.push(v);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
// Praise Resig: http://ejohn.org/blog/fast-javascript-maxmin/
|
||||
Array.max = function (array) {
|
||||
return Math.max.apply(Math, array);
|
||||
};
|
||||
|
||||
function solves_graph() {
|
||||
$.get('{{ request.script_root }}/admin/graphs/solves', function (data) {
|
||||
var solves = $.parseJSON(JSON.stringify(data));
|
||||
var chals = [];
|
||||
var counts = [];
|
||||
var colors = [];
|
||||
var annotations = [];
|
||||
var i = 1;
|
||||
var solves_order = Object.keys(solves).sort(function (a, b) {
|
||||
return solves[b] - solves[a]
|
||||
});
|
||||
$.each(solves_order, function (key, value) {
|
||||
chals.push(value);
|
||||
counts.push(solves[value]);
|
||||
{# colors.push(colorhash(value));#}
|
||||
var result = {
|
||||
x: value,
|
||||
y: solves[value],
|
||||
text: solves[value],
|
||||
xanchor: 'center',
|
||||
yanchor: 'bottom',
|
||||
showarrow: false,
|
||||
};
|
||||
annotations.push(result);
|
||||
});
|
||||
|
||||
var data = [{
|
||||
type: 'bar',
|
||||
x: chals,
|
||||
y: counts,
|
||||
text: counts,
|
||||
orientation: 'v',
|
||||
{# marker: {#}
|
||||
{# color: colors#}
|
||||
{# },#}
|
||||
}];
|
||||
|
||||
var layout = {
|
||||
title: 'Solve Counts',
|
||||
annotations: annotations,
|
||||
xaxis: {
|
||||
title: 'Challenge Name'
|
||||
},
|
||||
yaxis: {
|
||||
title: 'Amount of Solves'
|
||||
}
|
||||
};
|
||||
|
||||
$('#solves-graph').empty();
|
||||
Plotly.newPlot('solves-graph', data, layout);
|
||||
});
|
||||
}
|
||||
|
||||
function keys_percentage_graph() {
|
||||
// Solves and Fails pie chart
|
||||
$.get('{{ request.script_root }}/admin/fails/all', function (data) {
|
||||
var res = $.parseJSON(JSON.stringify(data));
|
||||
var solves = res['solves'];
|
||||
var fails = res['fails'];
|
||||
|
||||
var data = [{
|
||||
values: [solves, fails],
|
||||
labels: ['Solves', 'Fails'],
|
||||
marker: {
|
||||
colors: [
|
||||
"rgb(0, 209, 64)",
|
||||
"rgb(207, 38, 0)"
|
||||
]
|
||||
},
|
||||
hole: .4,
|
||||
type: 'pie'
|
||||
}];
|
||||
|
||||
var layout = {
|
||||
title: 'Submission Percentages'
|
||||
};
|
||||
|
||||
$('#keys-pie-graph').empty();
|
||||
Plotly.newPlot('keys-pie-graph', data, layout);
|
||||
});
|
||||
}
|
||||
|
||||
function category_breakdown_graph() {
|
||||
$.get('{{ request.script_root }}/admin/graphs/categories', function (data) {
|
||||
var res = $.parseJSON(JSON.stringify(data));
|
||||
res = res['categories'];
|
||||
|
||||
var categories = [];
|
||||
var count = [];
|
||||
for (var i = 0; i < res.length; i++) {
|
||||
categories.push(res[i].category);
|
||||
count.push(res[i].count);
|
||||
}
|
||||
|
||||
var data = [{
|
||||
values: count,
|
||||
labels: categories,
|
||||
hole: .4,
|
||||
type: 'pie'
|
||||
}];
|
||||
|
||||
var layout = {
|
||||
title: 'Category Breakdown'
|
||||
};
|
||||
|
||||
$('#categories-pie-graph').empty();
|
||||
Plotly.newPlot('categories-pie-graph', data, layout);
|
||||
});
|
||||
}
|
||||
|
||||
function solve_percentages_graph() {
|
||||
$.get('{{ request.script_root }}/admin/graphs/solve-percentages', function (data) {
|
||||
var res = $.parseJSON(JSON.stringify(data));
|
||||
res = res['percentages'];
|
||||
|
||||
var names = [];
|
||||
var percents = [];
|
||||
var labels = [];
|
||||
|
||||
var annotations = [];
|
||||
|
||||
for (var key in res) {
|
||||
names.push(res[key].name);
|
||||
percents.push( (res[key].percentage * 100) );
|
||||
|
||||
var result = {
|
||||
x: res[key].name,
|
||||
y: (res[key].percentage * 100),
|
||||
text: Math.round(res[key].percentage * 100) + '%',
|
||||
xanchor: 'center',
|
||||
yanchor: 'bottom',
|
||||
showarrow: false,
|
||||
};
|
||||
annotations.push(result);
|
||||
}
|
||||
|
||||
var data = [{
|
||||
type: 'bar',
|
||||
x: names,
|
||||
y: percents,
|
||||
orientation: 'v'
|
||||
}];
|
||||
|
||||
var layout = {
|
||||
title: 'Solve Percentages per Challenge',
|
||||
xaxis: {
|
||||
title: 'Challenge Name'
|
||||
},
|
||||
yaxis: {
|
||||
title: 'Percentage of Teams (%)',
|
||||
range: [0, 100]
|
||||
},
|
||||
annotations: annotations
|
||||
};
|
||||
|
||||
$('#solve-percentages-graph').empty();
|
||||
Plotly.newPlot('solve-percentages-graph', data, layout);
|
||||
});
|
||||
}
|
||||
|
||||
function update() {
|
||||
solves_graph();
|
||||
keys_percentage_graph();
|
||||
category_breakdown_graph();
|
||||
solve_percentages_graph();
|
||||
}
|
||||
|
||||
$(function () {
|
||||
update();
|
||||
window.onresize = function () {
|
||||
console.log('resizing');
|
||||
Plotly.Plots.resize(document.getElementById('keys-pie-graph'));
|
||||
Plotly.Plots.resize(document.getElementById('categories-pie-graph'));
|
||||
Plotly.Plots.resize(document.getElementById('solves-graph'));
|
||||
Plotly.Plots.resize(document.getElementById('solve-percentages-graph'));
|
||||
};
|
||||
});
|
||||
|
||||
setInterval(update, 300000);
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,15 +1,6 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
.btn-primary {
|
||||
background-color: #337ab7;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -18,8 +9,8 @@
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title" id="create-award-label">Create Award</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="award-create-form" method="POST" action="{{ request.script_root }}/admin/awards/add">
|
||||
|
@ -54,53 +45,39 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row container-fluid">
|
||||
<div id="confirm" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<h3 id="confirm-title">Delete Key</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ request.script_root }}/admin/chal/delete">
|
||||
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<div class="small-6 small-centered text-center columns">
|
||||
<p id="confirm-description"></p>
|
||||
<a onclick="$('#confirm').modal('hide')" class="btn btn-primary">No</a>
|
||||
<button class="btn btn-danger" id="delete-solve" type="button">Yes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 id="team-id">{{ team.name }}</h1>
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1 id="team-id" class="text-center">{{ team.name }}</h1>
|
||||
<h2 id="team-email" class="text-center">{{ team.email }}</h2>
|
||||
<h2 id="team-place" class="text-center">
|
||||
{%if place %}
|
||||
{{ place }} <small>place</small>
|
||||
{% if place %}
|
||||
{{ place }}
|
||||
<small>place</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<h2 id="team-score" class="text-center">
|
||||
{%if score %}
|
||||
{{ score }} <small>points</small>
|
||||
{% if score %}
|
||||
{{ score }}
|
||||
<small>points</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
{% if solves %}
|
||||
<div id="keys-pie-graph">
|
||||
<div id="keys-pie-graph" class="col-md-6">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="categories-pie-graph">
|
||||
<div id="categories-pie-graph" class="col-md-6">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div id="score-graph">
|
||||
<div id="score-graph" class="col-md-12">
|
||||
<div class="text-center">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
|
@ -110,28 +87,92 @@
|
|||
<div class="text-center"><h3 class="spinner-error">No solves yet</h3></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<nav class="nav nav-tabs nav-fill" id="myTab" role="tablist">
|
||||
<a class="nav-item nav-link active" id="nav-solves-tab" data-toggle="tab" href="#nav-solves" role="tab"
|
||||
aria-controls="nav-solves" aria-selected="true">Solves</a>
|
||||
|
||||
<a class="nav-item nav-link" id="nav-wrong-tab" data-toggle="tab" href="#nav-wrong" role="tab"
|
||||
aria-controls="nav-wrong" aria-selected="false">Wrong</a>
|
||||
|
||||
<a class="nav-item nav-link" id="nav-awards-tab" data-toggle="tab" href="#nav-awards" role="tab"
|
||||
aria-controls="nav-awards" aria-selected="false">Awards</a>
|
||||
|
||||
<a class="nav-item nav-link" id="nav-missing-tab" data-toggle="tab" href="#nav-missing" role="tab"
|
||||
aria-controls="nav-missing" aria-selected="false">Missing</a>
|
||||
</nav>
|
||||
<div class="tab-content" id="nav-tabContent">
|
||||
<div class="tab-pane fade show active" id="nav-solves" role="tabpanel" aria-labelledby="nav-solves-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped">
|
||||
<h3>IP Addresses</h3>
|
||||
<h3 class="text-center py-3 d-block">Solves</h3>
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>IP Address</b></td>
|
||||
<td class="text-center"><b>Last Seen</b></td>
|
||||
<td class="text-center"><b>Challenge</b></td>
|
||||
<td class="text-center"><b>Submitted</b></td>
|
||||
<td class="text-center"><b>Category</b></td>
|
||||
<td class="text-center"><b>Value</b></td>
|
||||
<td class="text-center"><b>Time</b></td>
|
||||
<td class="text-center"><b>Delete</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for addr in addrs %}
|
||||
<tr>
|
||||
<td class="text-center">{{ addr[0] }}</td>
|
||||
<td class="text-center solve-time"><script>document.write( moment({{ addr[1]|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
|
||||
{% for solve in solves %}
|
||||
<tr class="chal-solve">
|
||||
<td class="text-center chal" id="{{ solve.chalid }}">{{ solve.chal.name }}</td>
|
||||
<td class="flag" id="{{ solve.id }}"><pre>{{ solve.flag }}</pre></td>
|
||||
<td class="text-center">{{ solve.chal.category }}</td>
|
||||
<td class="text-center">{{ solve.chal.value }}</td>
|
||||
<td class="text-center solve-time">
|
||||
<script>document.write(moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
|
||||
</td>
|
||||
<td class="text-center"><i class="fa fa-times"></i></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="tab-pane fade" id="nav-wrong" role="tabpanel" aria-labelledby="nav-wrong-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped">
|
||||
<h3>Awards</h3>
|
||||
<h3 class="text-center py-3 d-block">Wrong Keys</h3>
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Challenge</b></td>
|
||||
<td class="text-center"><b>Submitted</b></td>
|
||||
<td class="text-center"><b>Time</b></td>
|
||||
<td class="text-center"><b>Delete</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for wrong_key in wrong_keys %}
|
||||
<tr class="chal-wrong">
|
||||
<td class="text-center chal" id="{{ wrong_key.chalid }}">{{ wrong_key.chal.name }}</td>
|
||||
<td class="flag" id="{{ wrong_key.id }}"><pre>{{ wrong_key.flag }}</pre></td>
|
||||
<td class="text-center solve-time">
|
||||
<script>document.write(moment({{ wrong_key.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
|
||||
</td>
|
||||
<td class="text-center"><i class="fa fa-times"></i></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="nav-awards" role="tabpanel" aria-labelledby="nav-awards-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped">
|
||||
<h3 class="text-center py-3 d-block">Awards</h3>
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Name</b></td>
|
||||
|
@ -147,8 +188,10 @@
|
|||
{% for award in awards %}
|
||||
<tr class="award-row">
|
||||
<td class="text-center chal" id="{{ award.id }}">{{ award.name }}</td>
|
||||
<td class="text-center">{{ award.description }}</td>
|
||||
<td class="text-center solve-time"><script>document.write( moment({{ award.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
|
||||
<td class=""><pre>{{ award.description }}</pre></td>
|
||||
<td class="text-center solve-time">
|
||||
<script>document.write(moment({{ award.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
|
||||
</td>
|
||||
<td class="text-center">{{ award.value }}</td>
|
||||
<td class="text-center">{{ award.category }}</td>
|
||||
<td class="text-center">{{ award.icon }}</td>
|
||||
|
@ -159,38 +202,19 @@
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row container-fluid">
|
||||
<button type="button" data-toggle="modal" data-target="#create-award-modal" class="btn btn-primary pull-right">Create Award</button>
|
||||
<button type="button" data-toggle="modal" data-target="#create-award-modal"
|
||||
class="btn btn-primary float-right d-block">Create Award
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="nav-missing" role="tabpanel" aria-labelledby="nav-missing-tab">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped">
|
||||
<h3>Solves</h3>
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Challenge</b></td>
|
||||
<td class="text-center"><b>Submitted</b></td>
|
||||
<td class="text-center"><b>Category</b></td>
|
||||
<td class="text-center"><b>Value</b></td>
|
||||
<td class="text-center"><b>Time</b></td>
|
||||
<td class="text-center"><b>Delete</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solve in solves %}
|
||||
<tr class="chal-solve">
|
||||
<td class="text-center chal" id="{{ solve.chalid }}">{{ solve.chal.name }}</td>
|
||||
<td class="text-center flag" id="{{ solve.id }}">{{ solve.flag }}</td>
|
||||
<td class="text-center">{{ solve.chal.category }}</td>
|
||||
<td class="text-center">{{ solve.chal.value }}</td>
|
||||
<td class="text-center solve-time"><script>document.write( moment({{ solve.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
|
||||
<td class="text-center"><i class="fa fa-times"></i></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<table class="table table-striped">
|
||||
<h3>Missing</h3>
|
||||
<h3 class="text-center py-3 d-block">Missing</h3>
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Challenge</b></td>
|
||||
|
@ -205,67 +229,52 @@
|
|||
<td class="text-center chal" id="{{ chal.id }}">{{ chal.name }}</td>
|
||||
<td class="text-center">{{ chal.category }}</td>
|
||||
<td class="text-center">{{ chal.value }}</td>
|
||||
<td class="text-center"><i class="fa fa-check"></i></td>
|
||||
<td class="text-center"><i class="fa fa-check mark-correct"></i></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped">
|
||||
<h3>Wrong Keys</h3>
|
||||
<h3>IP Addresses</h3>
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Challenge</b></td>
|
||||
<td class="text-center"><b>Submitted</b></td>
|
||||
<td class="text-center"><b>Time</b></td>
|
||||
<td class="text-center"><b>Delete</b></td>
|
||||
<td class="text-center"><b>IP Address</b></td>
|
||||
<td class="text-center"><b>Last Seen</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for wrong_key in wrong_keys %}
|
||||
<tr class="chal-wrong">
|
||||
<td class="text-center chal" id="{{ wrong_key.chalid }}">{{ wrong_key.chal.name }}</td>
|
||||
<td class="text-center flag" id="{{ wrong_key.id }}">{{ wrong_key.flag }}</td>
|
||||
<td class="text-center solve-time"><script>document.write( moment({{ wrong_key.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
|
||||
<td class="text-center"><i class="fa fa-times"></i></td>
|
||||
{% for addr in addrs %}
|
||||
<tr>
|
||||
<td class="text-center">{{ addr[0] }}</td>
|
||||
<td class="text-center solve-time">
|
||||
<script>document.write(moment({{ addr[1]|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/plotly.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/team.js"></script>
|
||||
<script>
|
||||
$('#delete-solve').click(function (e) {
|
||||
e.preventDefault();
|
||||
var solve = $('#confirm input[name="solve"]').val()
|
||||
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function (data) {
|
||||
var data = $.parseJSON(JSON.stringify(data))
|
||||
if (data == "1") {
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
});
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/moment.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/vendor/plotly.min.js"></script>
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/team.js"></script>
|
||||
<script>
|
||||
var nonce = "{{ nonce }}";
|
||||
|
||||
function load_confirm_modal(msg) {
|
||||
var title = msg.title;
|
||||
var description = msg.description;
|
||||
var action = msg.action;
|
||||
$('#confirm-title').text(title);
|
||||
$('#confirm-description').html(description);
|
||||
$('#confirm form').attr('action', action);
|
||||
$('#confirm').modal('show');
|
||||
}
|
||||
|
||||
$('.fa-times').click(function () {
|
||||
$('.fa-times').click(function () {
|
||||
var elem = $(this).parent().parent();
|
||||
var type = elem.attr('class');
|
||||
var chal_name = elem.find('.chal').text().trim();
|
||||
|
@ -276,31 +285,30 @@
|
|||
var title = 'Delete Solve';
|
||||
var description = "<span>Are you sure you want to delete " +
|
||||
"<strong>correct</strong> " +
|
||||
"key submission for team: " +
|
||||
"submission from " +
|
||||
"<strong id='confirm-team-name'></strong> " +
|
||||
"in challenge: " +
|
||||
"for challenge: " +
|
||||
"<strong id='confirm-chal-name'></strong>?</span>"
|
||||
|
||||
|
||||
var description = $($.parseHTML(description));
|
||||
description.find('#confirm-team-name').text(team_name);
|
||||
description.find('#confirm-chal-name').text(chal_name);
|
||||
description = description.html()
|
||||
description = description.html();
|
||||
|
||||
var action = '{{ request.script_root }}/admin/solves/' + key_id + '/delete';
|
||||
} else if (type == 'chal-wrong') {
|
||||
var title = 'Delete Wrong Key';
|
||||
var description = "<span>Are you sure you want to delete " +
|
||||
"<strong>incorrect</strong> " +
|
||||
"key submission for team: " +
|
||||
"submission from " +
|
||||
"<strong id='confirm-team-name'></strong> " +
|
||||
"in challenge: " +
|
||||
"<strong id='confirm-chal-name'></strong>?</span>"
|
||||
"for <strong id='confirm-chal-name'></strong>?</span>";
|
||||
|
||||
var description = $($.parseHTML(description));
|
||||
description.find('#confirm-team-name').text(team_name);
|
||||
description.find('#confirm-chal-name').text(chal_name);
|
||||
description = description.html()
|
||||
description = description.html();
|
||||
|
||||
var action = '{{ request.script_root }}/admin/wrong_keys/' + key_id + '/delete';
|
||||
} else if (type == 'award-row') {
|
||||
|
@ -315,44 +323,66 @@
|
|||
title : title,
|
||||
description : description,
|
||||
action : action,
|
||||
};
|
||||
|
||||
ezq({
|
||||
title: title,
|
||||
body: description,
|
||||
success: function(){
|
||||
var route = action;
|
||||
$.post(route, {
|
||||
nonce: nonce,
|
||||
}, function (data) {
|
||||
var data = $.parseJSON(JSON.stringify(data));
|
||||
if (data == "1") {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
load_confirm_modal(msg)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('.fa-check').click(function () {
|
||||
$('.mark-correct').click(function () {
|
||||
var elem = $(this).parent().parent();
|
||||
var type = elem.attr('class');
|
||||
var chal = elem.find('.chal').attr('id');
|
||||
var chal_name = elem.find('.chal').text().trim();
|
||||
var team = window.location.pathname.split('/').pop();
|
||||
var team_name = $("#team-id").text();
|
||||
|
||||
var title = 'Mark '+chal_name+' Solved';
|
||||
var description = "<span>Are you sure you want to mark " +
|
||||
"<strong id='confirm-chal-name'></strong> " +
|
||||
"as solved for team " +
|
||||
"<strong id='confirm-team-name'></strong> " +
|
||||
"?</span>";
|
||||
var chal_name = htmlentities(elem.find('.chal').text().trim());
|
||||
var team_name = htmlentities($("#team-id").text());
|
||||
|
||||
|
||||
var description = $($.parseHTML(description));
|
||||
description.find('#confirm-team-name').text(team_name);
|
||||
description.find('#confirm-chal-name').text(chal_name);
|
||||
description = description.html()
|
||||
description = description.html();
|
||||
|
||||
var action = '{{request.script_root }}/admin/solves/' + team + '/' + chal + '/solve';
|
||||
|
||||
var msg = {
|
||||
title : title,
|
||||
description : description,
|
||||
action : action,
|
||||
};
|
||||
var title = 'Mark ' + chal_name + ' solved for ' + team_name;
|
||||
var description = "<span>Are you sure you want to mark " +
|
||||
"<strong>{0}</strong> ".format(team_name) +
|
||||
"as solved for team " +
|
||||
"<strong>{0}</strong>?</span>".format(chal_name);
|
||||
|
||||
load_confirm_modal(msg);
|
||||
ezq({
|
||||
title: title,
|
||||
body: description,
|
||||
success: function(){
|
||||
var route = script_root + '/admin/solves/' + team + '/' + chal + '/solve';
|
||||
$.post(route, {
|
||||
nonce: nonce,
|
||||
}, function (data) {
|
||||
var data = $.parseJSON(JSON.stringify(data));
|
||||
if (data == "1") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('#award-create-form').submit(function(e){
|
||||
$('#award-create-form').submit(function(e){
|
||||
$.post($(this).attr('action'), $(this).serialize(), function(res){
|
||||
if (res == '1'){
|
||||
var award = $('#award-create-form').serializeObject();
|
||||
|
@ -368,7 +398,7 @@
|
|||
}
|
||||
})
|
||||
e.preventDefault();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,136 +1,124 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
td { text-align:center; }
|
||||
.checkbox { margin: 0px !important; }
|
||||
input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<h1>
|
||||
Teams
|
||||
<i class="fa fa-plus-circle create-team" role="button" data-toggle="tooltip" title="Create Team"></i>
|
||||
</h1>
|
||||
<div id="confirm" class="modal fade" tabindex="-1">
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Teams <i class="fa fa-plus-circle create-team" role="button" data-toggle="tooltip" title="Create Team"></i></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="email-user" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h2 class="text-center">Delete User</h2>
|
||||
</div>
|
||||
<div class="modal-body" style="height:110px">
|
||||
<div class="row-fluid">
|
||||
<div class="col-md-12">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="route">
|
||||
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<div class="small-6 small-centered text-center columns">
|
||||
<p>Are you sure you want to delete <strong id="confirm-team-name"></strong>?</p>
|
||||
<button type="button" data-dismiss="modal" class="btn btn-theme btn-outlined">No</button>
|
||||
<button type="button" id="delete-user" class="btn btn-theme btn-outlined">Yes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="email-user" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h2 class="text-center">Email Team</h2>
|
||||
<h2>Email Team</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST">
|
||||
<input type="hidden" name="id">
|
||||
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<textarea class="form-control" name="msg" placeholder="Enter your message here" rows="15"></textarea>
|
||||
<textarea class="form-control" name="msg" placeholder="Enter your message here"
|
||||
rows="15"></textarea>
|
||||
<br>
|
||||
<div id="email-user-errors"></div>
|
||||
<button type="button" id="send-user-email" class="btn btn-theme btn-outlined">Send Message</button>
|
||||
<button type="button" id="send-user-email" class="btn btn-primary float-right">
|
||||
Send Message
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="user" class="modal fade">
|
||||
</div>
|
||||
|
||||
<div id="update-user-modal" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h2 class="text-center modal-action">Edit Team</h2>
|
||||
<h2 class="modal-action">Edit Team</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body clearfix" style="padding:20px;">
|
||||
<div class="modal-body clearfix">
|
||||
<form method="POST" action="{{ request.script_root }}/admin/teams/">
|
||||
<input type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<input type="hidden" name="id">
|
||||
<div class="form-group">
|
||||
<label for="name">Team Name</label>
|
||||
<input type="text" class="form-control" name="name" id="name" placeholder="Enter new team name" required>
|
||||
<input type="text" class="form-control" name="name" id="name"
|
||||
placeholder="Enter new team name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" class="form-control" name="email" id="email" placeholder="Enter new email" required>
|
||||
<input type="email" class="form-control" name="email" id="email"
|
||||
placeholder="Enter new email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" class="form-control" name="password" id="password" />
|
||||
<input type="password" class="form-control" name="password" id="password"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="website">Website</label>
|
||||
<input type="text" class="form-control" name="website" id="website" placeholder="Enter website">
|
||||
<input type="text" class="form-control" name="website" id="website"
|
||||
placeholder="Enter website">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="affiliation">Affiliation</label>
|
||||
<input type="text" class="form-control" name="affiliation" id="affiliation" placeholder="Enter affiliation">
|
||||
<input type="text" class="form-control" name="affiliation" id="affiliation"
|
||||
placeholder="Enter affiliation">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="affiliation">Country</label>
|
||||
<input type="text" class="form-control" name="country" id="country" placeholder="Enter country">
|
||||
<input type="text" class="form-control" name="country" id="country"
|
||||
placeholder="Enter country">
|
||||
</div>
|
||||
<div id="results">
|
||||
|
||||
</div>
|
||||
<button id="update-user" type="submit" class="btn btn-theme btn-outlined pull-right modal-action">Update</button>
|
||||
<button id="update-user" type="submit"
|
||||
class="btn btn-primary btn-outlined pull-right modal-action">Update
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if q and field%}
|
||||
<h4 class="text-center">Searching for teams with {{field}} matching {{q}}</h4>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<form method="GET">
|
||||
<div class="col-md-2 form-group">
|
||||
<label for="sel1">Search Field</label>
|
||||
<select name="field" class="form-control" id="sel1">
|
||||
<option value="id" {% if field == 'id' %}selected{% endif %}>ID</option>
|
||||
<form method="GET" class="form-inline">
|
||||
<div class="form-group col-md-2">
|
||||
<label for="sel1" class="sr-only" >Search Field</label>
|
||||
<select name="field" class="form-control w-100" id="sel1">
|
||||
<option value="name" {% if field == 'name' %}selected{% endif %}>Name</option>
|
||||
<option value="id" {% if field == 'id' %}selected{% endif %}>ID</option>
|
||||
<option value="email" {% if field == 'email' %}selected{% endif %}>Email</option>
|
||||
<option value="affiliation" {% if field == 'affiliation' %}selected{% endif %}>Affiliation</option>
|
||||
<option value="country" {% if field == 'country' %}selected{% endif %}>Country</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-8 form-group">
|
||||
<label for="team-name-search">Parameter</label>
|
||||
<input type="text" class="form-control" id="team-name-search" name="q" placeholder="Search for matching team names" {% if q %}value="{{q}}"{% endif %}>
|
||||
<div class="form-group col-md-8">
|
||||
<label for="team-name-search" class="sr-only">Parameter</label>
|
||||
<input type="text" class="form-control w-100" id="team-name-search" name="q" placeholder="Search for matching team names" {% if q %}value="{{q}}"{% endif %}>
|
||||
</div>
|
||||
<div class="col-md-2 form-group">
|
||||
<label for="team-name-search"> </label>
|
||||
<input type="submit" class="form-control btn btn-default" value="Search">
|
||||
<div class="form-group col-md-2">
|
||||
<label for="team-name-search" class="sr-only">Search</label>
|
||||
<button type="submit" class="btn btn-primary w-100"><i class="fa fa-search" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
|
||||
<table id="teamsboard" class="table table-striped">
|
||||
<thead>
|
||||
|
@ -139,13 +127,13 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
|||
</td>
|
||||
<td class="text-center"><b>Team</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Email</b>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Email</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Website</b>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Website</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Affiliation</b>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Affiliation</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Country</b>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell text-center"><b>Country</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Admin</b>
|
||||
</td>
|
||||
|
@ -161,12 +149,20 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
|||
<td class="team-id" value="{{ team.id }}">{{ team.id }}</td>
|
||||
<td class="team-name" value="{{ team.name }}"><a href="{{ request.script_root }}/admin/team/{{ team.id }}">{{ team.name | truncate(32) }}</a>
|
||||
</td>
|
||||
<td class="team-email" value="{{ team.email }}">{{ team.email | truncate(32) }}</td>
|
||||
<td class="team-website">{% if team.website %}<a href="{{ team.website }}">{{ team.website | truncate(32) }}</a>{% endif %}
|
||||
<td class="team-email d-none d-md-table-cell d-lg-table-cell" value="{{ team.email }}">{{ team.email | truncate(32) }}</td>
|
||||
<td class="team-website d-none d-md-table-cell d-lg-table-cell text-center">
|
||||
{% if team.website %}
|
||||
<a href="{{ team.website }}" target="_blank">
|
||||
<i class="fa fa-globe fa-2" data-toggle="tooltip" data-placement="top"
|
||||
title="{{ team.website }}" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="team-affiliation" value="{{ team.affiliation if team.affiliation is not none }}"><span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span>
|
||||
<td class="team-affiliation d-none d-md-table-cell d-lg-table-cell" value="{{ team.affiliation if team.affiliation is not none }}">
|
||||
<span>{% if team.affiliation %}{{ team.affiliation | truncate(20) }}{% endif %}</span>
|
||||
</td>
|
||||
<td class="team-country" value="{{ team.country if team.country is not none }}"><span>{% if team.country %}{{ team.country }}{% endif %}</span>
|
||||
<td class="team-country d-none d-md-table-cell d-lg-table-cell" value="{{ team.country if team.country is not none }}">
|
||||
<span>{% if team.country %}{{ team.country }}{% endif %}</span>
|
||||
</td>
|
||||
<td class="team-admin">
|
||||
<div class="center-block checkbox text-center">
|
||||
|
@ -180,7 +176,9 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
|||
</td>
|
||||
<td class="text-center"><span>
|
||||
<i class="fa fa-pencil-square-o" data-toggle="tooltip" data-placement="top" title="Modify {{ team.name }}"></i>
|
||||
{% if can_send_mail() %}<i class="fa fa-envelope"></i>{% endif %}
|
||||
{% if can_send_mail() %}
|
||||
<i class="fa fa-envelope" data-toggle="tooltip" data-placement="top" title="Email {{ team.name }}"></i>
|
||||
{% endif %}
|
||||
<i class="fa fa-times" data-toggle="tooltip" data-placement="top" title="Delete {{ team.name }}"></i>
|
||||
</span>
|
||||
</td>
|
||||
|
@ -199,16 +197,20 @@ input[type="checkbox"] { margin: 0px !important; position: relative; top: 5px; }
|
|||
<b>{{ page }}</b>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/teams/{{ curr_page+1 }}">>>></a>{% endif %}
|
||||
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/teams/{{ curr_page + 1 }}">>>></a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
var nonce = "{{ nonce }}";
|
||||
|
||||
function load_update_modal(id, name, email, website, affiliation, country){
|
||||
var modal_form = $('#user form');
|
||||
var modal_form = $('#update-user-modal form');
|
||||
|
||||
modal_form.find('input[name=name]').val(name);
|
||||
modal_form.find('input[name=id]').val(id);
|
||||
|
@ -219,26 +221,26 @@ function load_update_modal(id, name, email, website, affiliation, country){
|
|||
modal_form.find('input[name=password]').val('');
|
||||
|
||||
if (id == 'new'){
|
||||
$('#user .modal-action').text('Create Team');
|
||||
$('#update-user-modal .modal-action').text('Create Team');
|
||||
} else {
|
||||
$('#user .modal-action').text('Edit Team');
|
||||
$('#update-user-modal .modal-action').text('Edit Team');
|
||||
}
|
||||
|
||||
$('#results').empty();
|
||||
$('#user form').attr('action', '{{ request.script_root }}/admin/team/'+id);
|
||||
$('#user').modal("show");
|
||||
$('#update-user-modal form').attr('action', '{{ request.script_root }}/admin/team/'+id);
|
||||
$('#update-user-modal').modal("show");
|
||||
}
|
||||
|
||||
$('#update-user').click(function(e){
|
||||
e.preventDefault();
|
||||
var id = $('#user input[name="id"]').val()
|
||||
var user_data = $('#user form').serializeArray()
|
||||
var id = $('#update-user-modal input[name="id"]').val()
|
||||
var user_data = $('#update-user-modal form').serializeArray()
|
||||
$('#results').empty();
|
||||
$.post($('#user form').attr('action'), $('#user form').serialize(), function(data){
|
||||
$.post($('#update-user-modal form').attr('action'), $('#update-user-modal form').serialize(), function(data){
|
||||
var data = $.parseJSON(JSON.stringify(data))
|
||||
for (var i = 0; i < data['data'].length; i++) {
|
||||
if (data['data'][i] == 'success'){
|
||||
$('#user').modal('hide');
|
||||
$('#update-user-modal').modal('hide');
|
||||
location.reload();
|
||||
}
|
||||
else{
|
||||
|
@ -270,34 +272,30 @@ $('.team-verified input').on('change', function () {
|
|||
console.log(verified);
|
||||
|
||||
$.post('{{ request.script_root }}/admin/team/' + id, {'verified': verified, 'nonce': nonce});
|
||||
})
|
||||
});
|
||||
|
||||
$('#send-user-email').click(function(e){
|
||||
e.preventDefault();
|
||||
var id = $('#email-user input[name="id"]').val();
|
||||
var email_data = $('#email-user form').serializeArray();
|
||||
$.post($('#email-user form').attr('action'), $('#email-user form').serialize(), function(data){
|
||||
if (data == "1"){
|
||||
$('#email-user').modal('hide');
|
||||
if (data.result){
|
||||
var error = $('<div class="alert alert-success alert-dismissable">\n' +
|
||||
' <a href="#" class="close" data-dismiss="alert" aria-label="close">×</a>\n' +
|
||||
' {0}\n'.format(data.message) +
|
||||
'</div>');
|
||||
$('#email-user-errors').append(error);
|
||||
}
|
||||
else{
|
||||
$('#email-user-errors').append("<b>Failed to send email</b>");
|
||||
var error = $('<div class="alert alert-danger alert-dismissable">\n' +
|
||||
' <a href="#" class="close" data-dismiss="alert" aria-label="close">×</a>\n' +
|
||||
' {0}\n'.format(data.message) +
|
||||
'</div>');
|
||||
$('#email-user-errors').append(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#delete-user').click(function(e){
|
||||
e.preventDefault();
|
||||
var id = $('#confirm input[name="id"]').val()
|
||||
var user_data = $('#confirm form').serializeArray()
|
||||
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function(data){
|
||||
var data = $.parseJSON(JSON.stringify(data))
|
||||
if (data == "1"){
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$('.fa-pencil-square-o').click(function(){
|
||||
var elem = $(this).parent().parent().parent();
|
||||
var id = elem.find('.team-id').attr('value') || '';
|
||||
|
@ -314,26 +312,32 @@ $('.create-team').click(function(){
|
|||
load_update_modal('new', '', '', '', '', '');
|
||||
});
|
||||
|
||||
function load_confirm_modal(id, name){
|
||||
var modal = $('#confirm')
|
||||
modal.find('input[name=id]').val(id)
|
||||
modal.find('#confirm-team-name').text(name)
|
||||
$('#confirm form').attr('action', '{{ request.script_root }}/admin/team/'+id+'/delete');
|
||||
$('#confirm').modal();
|
||||
}
|
||||
|
||||
$('.fa-times').click(function(){
|
||||
var elem = $(this).parent().parent().parent();
|
||||
var id = elem.find('.team-id').text().trim();
|
||||
var team_id = elem.find('.team-id').text().trim();
|
||||
var name = elem.find('.team-name').text().trim();
|
||||
load_confirm_modal(id, name)
|
||||
ezq({
|
||||
title: "Delete User",
|
||||
body: "Are you sure you want to delete {0}".format("<strong>"+name+"</strong>"),
|
||||
success: function(){
|
||||
var route = script_root + '/admin/team/' + team_id + '/delete';
|
||||
$.post(route, {
|
||||
nonce: nonce,
|
||||
}, function (data) {
|
||||
var data = $.parseJSON(JSON.stringify(data));
|
||||
if (data == "1") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
function load_email_modal(id){
|
||||
var modal = $('#email-user')
|
||||
modal.find('textarea').val("")
|
||||
modal.find('input[name=id]').val(id)
|
||||
$('#email-user-errors').empty()
|
||||
var modal = $('#email-user');
|
||||
modal.find('textarea').val("");
|
||||
modal.find('input[name=id]').val(id);
|
||||
$('#email-user-errors').empty();
|
||||
$('#email-user form').attr('action', '{{ request.script_root }}/admin/team/'+id+'/mail');
|
||||
$('#email-user').modal();
|
||||
}
|
||||
|
|
|
@ -1,53 +1,31 @@
|
|||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
<style>
|
||||
.btn-primary {
|
||||
background-color: #337ab7;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #d9534f;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>Incorrect Submissions</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<h1>Incorrect Key Submissions</h1>
|
||||
<div id="confirm" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<h3>Delete Key</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="">
|
||||
<input id="nonce" type="hidden" name="nonce" value="{{ nonce }}">
|
||||
<div class="small-6 small-centered text-center columns">
|
||||
<p>Are you sure you want to delete incorrect key submission for team: <strong
|
||||
id="confirm-team-name"></strong> in challenge: <strong
|
||||
id="confirm-chal-name"></strong>?</p>
|
||||
<a onclick="$('#confirm').modal('hide')" class="btn btn-primary">No</a>
|
||||
<button class="btn btn-danger" id="delete-solve" type="button">Yes</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table id="teamsboard" class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>Team</b>
|
||||
<td class="text-center"><b>ID</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Challenge</b>
|
||||
<td><b>Team</b>
|
||||
</td>
|
||||
<td><b>Challenge</b>
|
||||
</td>
|
||||
<td><b>Submitted Key</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Date</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Submitted Key</b>
|
||||
</td>
|
||||
<td class="text-center"><b>Delete</b>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -55,11 +33,24 @@
|
|||
<tbody>
|
||||
{% for wrong_key in wrong_keys %}
|
||||
<tr>
|
||||
<td class="text-center team" id="{{ wrong_key.teamid }}"><a href="{{ request.script_root }}/admin/team/{{ wrong_key.teamid }}">{{ wrong_key.team_name }}</a>
|
||||
<td class="text-center chal" id="{{ wrong_key.chalid }}">{{ wrong_key.chal_name }}</td>
|
||||
<td class="text-center solve-time"><script>document.write( moment({{ wrong_key.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script></td>
|
||||
<td class="text-center flag" id="{{ wrong_key.id }}">{{ wrong_key.flag }}</td>
|
||||
<td class="text-center"><i class="fa fa-times"></i></td>
|
||||
<td class="text-center">{{ wrong_key.id }}</td>
|
||||
<td class="team" id="{{ wrong_key.teamid }}">
|
||||
<a href="{{ request.script_root }}/admin/team/{{ wrong_key.teamid }}">{{ wrong_key.team_name }}</a>
|
||||
</td>
|
||||
<td class="chal" id="{{ wrong_key.chalid }}">
|
||||
{{ wrong_key.chal_name }}
|
||||
</td>
|
||||
<td class="flag" id="{{ wrong_key.id }}">
|
||||
<pre class="mb-0">{{ wrong_key.flag }}</pre>
|
||||
</td>
|
||||
<td class="text-center solve-time">
|
||||
<small>
|
||||
<script>document.write( moment({{ wrong_key.date|unix_time_millis }}).local().format('MMMM Do, h:mm:ss A'))</script>
|
||||
</small>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<i class="fa fa-times"></i>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -75,43 +66,43 @@
|
|||
<b>{{ page }}</b>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/wrong_keys/{{ curr_page+1 }}">>>></a>{% endif %}
|
||||
<a href="{{ request.script_root }}">
|
||||
{% if curr_page != pages %}<a href="{{ request.script_root }}/admin/wrong_keys/{{ curr_page + 1 }}">>>></a>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ request.script_root }}/themes/admin/static/js/utils.js"></script>
|
||||
<script>
|
||||
$('#delete-solve').click(function (e) {
|
||||
e.preventDefault();
|
||||
var solve = $('#confirm input[name="solve"]').val();
|
||||
$.post($('#confirm form').attr('action'), $('#confirm form').serialize(), function (data) {
|
||||
var data = $.parseJSON(JSON.stringify(data));
|
||||
if (data == "1") {
|
||||
location.reload()
|
||||
}
|
||||
})
|
||||
});
|
||||
<script>
|
||||
var nonce = "{{ nonce }}";
|
||||
|
||||
function load_confirm_modal(key_id, team_name, chal_name) {
|
||||
var modal = $('#confirm')
|
||||
modal.find('#confirm-team-name').text(team_name);
|
||||
modal.find('#confirm-chal-name').text(chal_name);
|
||||
$('#confirm form').attr('action', '{{ request.script_root }}/admin/wrong_keys/' + key_id + '/delete');
|
||||
$('#confirm').modal('show');
|
||||
}
|
||||
|
||||
$('.fa-times').click(function () {
|
||||
$('.fa-times').click(function () {
|
||||
var elem = $(this).parent().parent();
|
||||
var chal = elem.find('.chal').attr('id');
|
||||
var chal_name = elem.find('.chal').text().trim();
|
||||
var team = elem.find('.team').attr('id');
|
||||
var team_name = elem.find('.team').text().trim();
|
||||
var key_id = elem.find('.flag').attr('id');
|
||||
load_confirm_modal(key_id, team_name, chal_name);
|
||||
ezq({
|
||||
title: 'Delete Submission',
|
||||
body: "Are you sure you want to delete incorrect submission from {0} for challenge {1}".format(
|
||||
"<strong>" + team_name + "</strong>",
|
||||
"<strong>" + chal_name + "</strong>"
|
||||
),
|
||||
success: function () {
|
||||
var route = script_root + '/admin/wrong_keys/' + key_id + '/delete';
|
||||
$.post(route, {
|
||||
nonce: nonce,
|
||||
}, function (data) {
|
||||
var data = $.parseJSON(JSON.stringify(data));
|
||||
if (data == "1") {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
html, body, .container {
|
||||
font-family: 'Lato', 'LatoOffline', sans-serif;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
font-family: 'Raleway', 'RalewayOffline', sans-serif;
|
||||
font-weight: 500;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #337ab7;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table > thead > tr > td {
|
||||
/* Remove border line from thead of all tables */
|
||||
/* It can overlap with other element styles */
|
||||
border-top: none !important;
|
||||
}
|
||||
|
||||
#score-graph {
|
||||
height: 450px;
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.fa.spinner {
|
||||
margin-top: 225px;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.spinner-error {
|
||||
padding-top: 20vh;
|
||||
text-align: center;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.jumbotron {
|
||||
background-color: #343a40;
|
||||
color: #FFF;
|
||||
border-radius: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0.8em;
|
||||
border-radius: 0;
|
||||
background: #f0f0f0;
|
||||
color: #aaa;
|
||||
font-weight: 400;
|
||||
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
background-color: transparent;
|
||||
border-color: #a3d39c;
|
||||
box-shadow: 0 0 0 0.2rem #a3d39c;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-filled-valid {
|
||||
background-color: transparent;
|
||||
border-color: #a3d39c;
|
||||
box-shadow: 0 0 0 0.2rem #a3d39c;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-outlined.btn-theme {
|
||||
background: none;
|
||||
color: #545454;
|
||||
border-color: #545454;
|
||||
border: 3px solid;
|
||||
}
|
||||
|
||||
.btn-outlined {
|
||||
border-radius: 0;
|
||||
-webkit-transition: all 0.3s;
|
||||
-moz-transition: all 0.3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn {
|
||||
letter-spacing: 1px;
|
||||
text-decoration: none;
|
||||
-moz-user-select: none;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #5B7290 !important;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 0 !important;
|
||||
padding: 0.8em;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
.chal-desc {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0px;
|
||||
max-width: 1000px;
|
||||
padding: 1em;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.btn-info {
|
||||
background-color: #5B7290 !important;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background-color: #5B7290 !important;
|
||||
}
|
||||
|
||||
.btn-file:nth-child(1):before {
|
||||
content: "\f0ed";
|
||||
display: inline-block;
|
||||
font: normal normal normal 14px/1 FontAwesome;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.solved-challenge {
|
||||
background-color: #37d63e !important;
|
||||
opacity: 0.4;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.solved-challenge p:after {
|
||||
content: "\f00c";
|
||||
display: inline-block;
|
||||
font: normal normal normal 18px/1 FontAwesome;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin-top: -10px;
|
||||
margin-right: 25px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.key-submit .btn {
|
||||
height: 51px;
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/* Move down content because we have a fixed navbar that is 3.5rem tall */
|
||||
body {
|
||||
padding-top: 3.5rem;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/* Sticky footer styles
|
||||
-------------------------------------------------- */
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 60px; /* Margin bottom by footer height */
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px; /* Set the fixed height of the footer here */
|
||||
line-height: 60px; /* Vertically center the text there */
|
||||
/*background-color: #f5f5f5;*/
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 434 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue