mirror of
https://github.com/aljazceru/CTFd.git
synced 2026-02-18 04:34:22 +01:00
Squashed 'CTFd/themes/core-beta/' changes from 9126d77d..5ce3003b
5ce3003b Merge pull request #47 from aCursedComrade/patch-1 c9887cb1 Fix team template git-subtree-dir: CTFd/themes/core-beta git-subtree-split: 5ce3003b4d68352e629ee2d390bc999e7d6b071e
This commit is contained in:
65
templates/base.html
Normal file
65
templates/base.html
Normal file
@@ -0,0 +1,65 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ Configs.ctf_name }}</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="{{ Configs.ctf_small_icon }}" type="image/x-icon">
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ Assets.css("assets/scss/main.scss") }}
|
||||
{% endblock %}
|
||||
|
||||
{{ Plugins.styles }}
|
||||
|
||||
<script type="text/javascript">
|
||||
window.init = {
|
||||
'urlRoot': "{{ request.script_root }}",
|
||||
'csrfNonce': "{{ Session.nonce }}",
|
||||
'userMode': "{{ Configs.user_mode }}",
|
||||
'userId': {{ Session.id }},
|
||||
'userName': {{ User.name | tojson }},
|
||||
'userEmail': {{ User.email | tojson }},
|
||||
'teamId': {{ Team.id | tojson }},
|
||||
'teamName': {{ Team.name | tojson }},
|
||||
'start': {{ Configs.start | tojson }},
|
||||
'end': {{ Configs.end | tojson }},
|
||||
'themeSettings': {{ Configs.theme_settings | tojson }},
|
||||
'eventSounds': [
|
||||
"/themes/core/static/sounds/notification.webm",
|
||||
"/themes/core/static/sounds/notification.mp3",
|
||||
],
|
||||
}
|
||||
</script>
|
||||
|
||||
{{ Configs.theme_header }}
|
||||
</head>
|
||||
<body>
|
||||
{% include "components/navbar.html" %}
|
||||
|
||||
<main role="main">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<div class="container text-center">
|
||||
<a href="https://ctfd.io" class="text-secondary">
|
||||
<small class="text-muted">
|
||||
{% trans %}Powered by CTFd{% endtrans %}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% include "components/notifications.html" %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/index.js") }}
|
||||
{% endblock %}
|
||||
|
||||
{{ Plugins.scripts }}
|
||||
|
||||
{{ Configs.theme_footer }}
|
||||
</body>
|
||||
</html>
|
||||
208
templates/challenge.html
Normal file
208
templates/challenge.html
Normal file
@@ -0,0 +1,208 @@
|
||||
<div :class="getStyles()" role="document" x-data="Challenge" x-init="id = {{ challenge.id }}">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body py-4 px-4 px-sm-5">
|
||||
|
||||
<div>
|
||||
<button type="button" class="btn-close float-end" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-target="#challenge" @click="showChallenge()">
|
||||
{% trans %}Challenge{% endtrans %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% block solves %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link challenge-solves" data-bs-target="#solves" @click="showSolves()">
|
||||
{% if solves != None %}
|
||||
{{ ngettext("%(num)d Solve", "%(num)d Solves", solves) }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endblock %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
|
||||
<h2 class="challenge-name text-center pt-3">
|
||||
{{ challenge.name }}
|
||||
</h2>
|
||||
<h3 class="challenge-value text-center">
|
||||
{{ challenge.value }}
|
||||
</h3>
|
||||
|
||||
|
||||
{% if tags %}
|
||||
<div class="challenge-tags text-center pt-2 pb-3">
|
||||
{% block tags %}
|
||||
{% for tag in tags %}
|
||||
<span class="challenge-tag badge bg-info">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<span class="challenge-desc">{% block description %}{{ challenge.html }}{% endblock %}</span>
|
||||
|
||||
{% if challenge.connection_info %}
|
||||
<div class="mb-2">
|
||||
<span class="challenge-connection-info">
|
||||
{% block connection_info %}
|
||||
{% set conn = challenge.connection_info %}
|
||||
{% if not conn %}
|
||||
{% elif conn.startswith("http") %}
|
||||
{{ conn | urlize(target="_blank") }}
|
||||
{% else %}
|
||||
<code>{{ conn }}</code>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if hints %}
|
||||
<div class="challenge-hints hint-row row">
|
||||
<div class="col-12 mb-3">
|
||||
{% for hint in hints | sort(attribute="cost") %}
|
||||
<div x-data="Hint" x-init="id = {{ hint.id }}">
|
||||
{% if hint.content %}
|
||||
<details>
|
||||
<summary>{% trans %}View Hint{% endtrans %}</summary>
|
||||
<div>{{ hint.html | safe }}</div>
|
||||
</details>
|
||||
{% else %}
|
||||
<details @toggle="showHint(event)">
|
||||
<summary>Unlock Hint for {{ hint.cost }} point{{ hint.cost|pluralize }}</summary>
|
||||
<div x-html="html"></div>
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if files %}
|
||||
<div class="row challenge-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">
|
||||
{% set segments = file.split('/') %}
|
||||
{% set token = file.split('?') | last %}
|
||||
{% if token %}
|
||||
{% set filename = segments | last | replace("?" + token, "") %}
|
||||
{% else %}
|
||||
{% set filename = segments | last %}
|
||||
{% endif %}
|
||||
<a
|
||||
class="btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate"
|
||||
href="{{ file }}"
|
||||
title="{{ filename }}"
|
||||
>
|
||||
<i class="fas fa-download"></i>
|
||||
<small>
|
||||
{{ filename }}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if max_attempts > 0 %}
|
||||
<div class="row text-center">
|
||||
<div class="col-12">
|
||||
<p>
|
||||
{{ attempts }}/{{ max_attempts }} attempt{{ max_attempts|pluralize }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="row submit-row">
|
||||
<div class="col-12 col-sm-8">
|
||||
{% block input %}
|
||||
<input
|
||||
id="challenge-id" class="challenge-id" type="hidden"
|
||||
value="{{ challenge.id }}"
|
||||
>
|
||||
<input
|
||||
id="challenge-input" class="challenge-input form-control"
|
||||
type="text" name="submission"
|
||||
@keyup.enter="submitChallenge()"
|
||||
placeholder="{% trans %}Flag{% endtrans %}" x-model="submission"
|
||||
>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-sm-4 mt-3 mt-sm-0 key-submit">
|
||||
{% block submit %}
|
||||
<button
|
||||
id="challenge-submit"
|
||||
class="challenge-submit btn btn-outline-secondary w-100 h-100" type="submit"
|
||||
@click.debounce.500ms="submitChallenge()"
|
||||
>
|
||||
{% trans %}Submit{% endtrans %}
|
||||
</button>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row notification-row">
|
||||
<div class="col-12">
|
||||
<template x-if="response">
|
||||
{# This alert is re-used for all alerts, so it's important not to make it dismissble #}
|
||||
<div
|
||||
:class="{
|
||||
'alert text-center w-100 mt-3 alert-success': response.data.status == 'correct',
|
||||
'alert text-center w-100 mt-3 alert-info': response.data.status == 'already_solved',
|
||||
'alert text-center w-100 mt-3 alert-danger': response.data.status == 'incorrect',
|
||||
}" role="alert"
|
||||
>
|
||||
<strong x-text="response.data.message"></strong>
|
||||
<div x-show="(response.data.status == 'correct' || response.data.status == 'already_solved') && getNextId()">
|
||||
<button @click="nextChallenge()" class="btn btn-info mt-3">
|
||||
{% trans %}Next Challenge{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</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>{% trans %}Name{% endtrans %}</b>
|
||||
</td>
|
||||
<td>
|
||||
<b>{% trans %}Date{% endtrans %}</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="challenge-solves-names">
|
||||
<template x-for="solve in solves">
|
||||
<tr>
|
||||
<td>
|
||||
<a :href="solve.account_url" x-text="solve.name"></a>
|
||||
</td>
|
||||
<td x-text="solve.date"></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
72
templates/challenges.html
Normal file
72
templates/challenges.html
Normal file
@@ -0,0 +1,72 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{% trans %}Challenges{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include "components/errors.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
x-data="ChallengeBoard"
|
||||
@load-challenges.window="loadChallenges()"
|
||||
@load-challenge.window="loadChallenge($event.detail)"
|
||||
>
|
||||
<div
|
||||
x-ref="challengeWindow" id="challenge-window" class="modal fade" tabindex="-1" role="dialog" x-data=""
|
||||
x-html="$store.challenge.data.view"
|
||||
></div>
|
||||
|
||||
<div x-show="!loaded">
|
||||
<div class="min-vh-50 d-flex align-items-center">
|
||||
<div class="text-center w-100">
|
||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-show="loaded">
|
||||
<template x-for="(category, idx) in getCategories()" :key="idx">
|
||||
<div class="pt-5">
|
||||
<div class="category-header mb-3">
|
||||
<h3 x-text="category"></h3>
|
||||
</div>
|
||||
|
||||
<div class="category-challenges d-flex flex-column">
|
||||
<div class="challenges-row row">
|
||||
<template x-for="(c, idx) in getChallenges(category)" :key="c.id">
|
||||
<div class="col-sm-6 col-md-4 col-lg-3">
|
||||
<button
|
||||
class="challenge-button btn btn-dark w-100 text-truncate my-3"
|
||||
:class="c.solved_by_me ? 'challenge-solved' : ''"
|
||||
:value="c.id" @click="loadChallenge(c.id)"
|
||||
>
|
||||
|
||||
<div class="challenge-inner my-3">
|
||||
<p x-text="c.name"></p>
|
||||
<span x-text="c.value"></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/challenges.js") }}
|
||||
{% endblock %}
|
||||
19
templates/components/errors.html
Normal file
19
templates/components/errors.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div>
|
||||
{% for info in infos %}
|
||||
<div class="alert alert-info alert-dismissible text-center" role="alert">
|
||||
<span>{{ info }}</span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for error in errors %}
|
||||
<div class="alert alert-danger alert-dismissible text-center" role="alert">
|
||||
<span>{{ error }}</span>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true"></span>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
186
templates/components/navbar.html
Normal file
186
templates/components/navbar.html
Normal file
@@ -0,0 +1,186 @@
|
||||
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
|
||||
<div class="container">
|
||||
<a href="{{ url_for('views.static_html', route='/') }}" class="navbar-brand">
|
||||
{% if Configs.ctf_logo %}
|
||||
<img
|
||||
class="img-responsive ctf_logo"
|
||||
src="{{ url_for('views.files', path=Configs.ctf_logo) }}"
|
||||
alt="{{ Configs.ctf_name }}"
|
||||
height="25"
|
||||
>
|
||||
{% else %}
|
||||
{{ Configs.ctf_name }}
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<button
|
||||
class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-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 me-auto">
|
||||
{% for page in Plugins.user_menu_pages %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ page.route }}">{{ page.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% if Configs.account_visibility != 'admins' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('users.listing') }}">
|
||||
{% trans %}Users{% endtrans %}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if Configs.user_mode == 'teams' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('teams.listing') }}">
|
||||
{% trans %}Teams{% endtrans %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if Configs.account_visibility != 'admins' and Configs.score_visibility != 'admins' %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('scoreboard.listing') }}">
|
||||
{% trans %}Scoreboard{% endtrans %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('challenges.listing') }}">
|
||||
{% trans %}Challenges{% endtrans %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr class="d-sm-flex d-md-flex d-lg-none">
|
||||
|
||||
<ul class="navbar-nav ms-md-auto d-block d-sm-flex d-md-flex">
|
||||
{% if authed() %}
|
||||
|
||||
{% if is_admin() %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('admin.view') }}">
|
||||
<span
|
||||
class="d-block"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="bottom"
|
||||
title="Admin Panel"
|
||||
>
|
||||
<i class="fas fa-wrench d-none d-md-block d-lg-none"></i>
|
||||
</span>
|
||||
<span class="d-sm-block d-md-none d-lg-block">
|
||||
<i class="fas fa-wrench pe-1"></i>
|
||||
{% trans %}Admin Panel{% endtrans %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('views.notifications') }}">
|
||||
<span
|
||||
class="d-block" data-bs-toggle="tooltip" data-bs-placement="bottom"
|
||||
title="Notifications"
|
||||
>
|
||||
<i class="fas fa-bell d-none d-md-block d-lg-none"></i>
|
||||
</span>
|
||||
<span class="d-sm-block d-md-none d-lg-block">
|
||||
<i class="fas fa-bell pe-1"></i>
|
||||
<span x-data x-show="$store.unread_count > 0" x-text="$store.unread_count" class="badge rounded-pill bg-danger badge-notification"></span>
|
||||
{% trans %}Notifications{% endtrans %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if Configs.user_mode == "teams" %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('teams.private') }}">
|
||||
<span class="d-block" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Team">
|
||||
<i class="fas fa-users d-none d-md-block d-lg-none"></i>
|
||||
</span>
|
||||
<span class="d-sm-block d-md-none d-lg-block">
|
||||
<i class="fas fa-users pe-1"></i>
|
||||
{% trans %}Team{% endtrans %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('users.private') }}">
|
||||
<span class="d-block" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Profile">
|
||||
<i class="fas fa-user-circle d-none d-md-block d-lg-none"></i>
|
||||
</span>
|
||||
<span class="d-sm-block d-md-none d-lg-block">
|
||||
<i class="fas fa-user-circle pe-1"></i>
|
||||
{% trans %}Profile{% endtrans %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('views.settings') }}">
|
||||
<span class="d-block" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Settings">
|
||||
<i class="fas fa-cogs d-none d-md-block d-lg-none"></i>
|
||||
</span>
|
||||
<span class="d-sm-block d-md-none d-lg-block">
|
||||
<i class="fas fa-cogs pe-1"></i>
|
||||
{% trans %}Settings{% endtrans %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.logout') }}">
|
||||
<span class="d-block" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Logout">
|
||||
<i class="fas fa-sign-out-alt d-none d-md-block d-lg-none"></i>
|
||||
</span>
|
||||
<span class="d-sm-block d-md-none d-lg-block">
|
||||
<i class="fas fa-sign-out-alt pe-1"></i><span class="d-lg-none">
|
||||
{% trans %}Logout{% endtrans %}
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% else %}
|
||||
|
||||
{% if registration_visible() %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.register') }}">
|
||||
<span
|
||||
class="d-block" data-bs-toggle="tooltip" data-bs-placement="bottom"
|
||||
title="Register"
|
||||
>
|
||||
<i class="fas fa-user-plus d-none d-md-block d-lg-none"></i>
|
||||
</span>
|
||||
<span class="d-sm-block d-md-none d-lg-block">
|
||||
<i class="fas fa-user-plus pe-1"></i>
|
||||
{% trans %}Register{% endtrans %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.login') }}">
|
||||
<span class="d-block" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Login">
|
||||
<i class="fas fa-sign-in-alt d-none d-md-block d-lg-none"></i>
|
||||
</span>
|
||||
<span class="d-sm-block d-md-none d-lg-block">
|
||||
<i class="fas fa-sign-in-alt pe-1"></i>
|
||||
{% trans %}Login{% endtrans %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
33
templates/components/notifications.html
Normal file
33
templates/components/notifications.html
Normal file
@@ -0,0 +1,33 @@
|
||||
<div x-data>
|
||||
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 11">
|
||||
<div x-ref="toast" class="toast hide" role="alert" aria-live="assertive" aria-atomic="true">
|
||||
<div class="toast-header">
|
||||
<strong class="me-auto" x-text="$store.toast.title"></strong>
|
||||
<small>just now</small>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="toast-body" x-html="$store.toast.html">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div x-data>
|
||||
<div class="modal" x-ref="modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" x-text="$store.modal.title"></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p x-html="$store.modal.html"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Got it!</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
47
templates/config.html
Normal file
47
templates/config.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label>Challenge Window Size</label>
|
||||
<select class="form-control custom-select" name="challenge_window_size">
|
||||
<option value="sm">Small</option>
|
||||
<option value="norm">Normal</option>
|
||||
<option value="lg">Large</option>
|
||||
<option value="xl">Extra Large</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Challenge Category Order Custom Function<br>
|
||||
<small class="form-text text-muted">
|
||||
Define a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description" target="_blank">custom compareFn function for Array.sort()</a>
|
||||
used to sort the challenge categories
|
||||
</small>
|
||||
</label>
|
||||
<textarea class="form-control text-monospace" name="challenge_category_order" rows="5"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Challenge Order Custom Function<br>
|
||||
<small class="form-text text-muted">
|
||||
Define a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description" target="_blank">custom compareFn function for Array.sort()</a>
|
||||
used to sort the challenges within categories
|
||||
</small>
|
||||
</label>
|
||||
<textarea class="form-control text-monospace" name="challenge_order" rows="5"></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" id="use_builtin_code_highlighter" name="use_builtin_code_highlighter" checked>
|
||||
<label class="form-check-label" for="use_builtin_code_highlighter">
|
||||
Use Built-In Syntax Highlighter
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
The built-in syntax highlighter is lightweight and fast but may not always correctly highlight all languages. You can disable the built-in highlighter in order to use your own.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary float-right mt-4">Update</button>
|
||||
</form>
|
||||
53
templates/confirm.html
Normal file
53
templates/confirm.html
Normal file
@@ -0,0 +1,53 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{% trans %}Confirm{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-10 col-lg-8 col-xl-6 offset-md-1 offset-lg-2 offset-xl-3">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
<h5 class="text-center">
|
||||
{% trans %}We've sent a confirmation email to your email address.{% endtrans %}
|
||||
</h5>
|
||||
|
||||
<br>
|
||||
|
||||
<h5 class="text-center">
|
||||
{% trans %}Please click the link in that email to confirm your account.{% endtrans %}
|
||||
</h5>
|
||||
|
||||
<br>
|
||||
|
||||
<h5 class="text-center">
|
||||
{% trans %}If the email doesn’t arrive, check your spam folder or contact an administrator to manually verify your account.{% endtrans %}
|
||||
</h5>
|
||||
|
||||
<hr>
|
||||
|
||||
{% with form = Forms.auth.ConfirmForm() %}
|
||||
<form method="POST" action="{{ url_for('auth.confirm') }}">
|
||||
<div class="row">
|
||||
<div class="mb-3 col-md-6">
|
||||
{{ form.submit(class="btn btn-primary w-100") }}
|
||||
</div>
|
||||
<div class="mb-3 col-md-6">
|
||||
<a href="{{ url_for('views.settings') }}" class="btn btn-secondary w-100">
|
||||
{% trans %}Change Email Address{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
{{ form.nonce() }}
|
||||
</div>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
17
templates/errors/403.html
Normal file
17
templates/errors/403.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="pt-5 mt-5 text-center">
|
||||
<h1>{% trans %}Forbidden{% endtrans %}</h1>
|
||||
<hr class="w-50 m-auto">
|
||||
<h2>{{ error }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
19
templates/errors/404.html
Normal file
19
templates/errors/404.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="pt-5 mt-5 text-center">
|
||||
<h1>404</h1>
|
||||
<hr class="w-50 m-auto">
|
||||
<h2>{% trans %}File not found{% endtrans %}</h2>
|
||||
<h2>{% trans %}Sorry about that{% endtrans %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
19
templates/errors/429.html
Normal file
19
templates/errors/429.html
Normal file
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="pt-5 mt-5 text-center">
|
||||
<h1>429</h1>
|
||||
<hr class="w-50 m-auto">
|
||||
<h2>{% trans %}Too many requests{% endtrans %}</h2>
|
||||
<h2>{% trans %}Please slow down!{% endtrans %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
18
templates/errors/500.html
Normal file
18
templates/errors/500.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="pt-5 mt-5 text-center">
|
||||
<h1>500</h1>
|
||||
<hr class="w-50 m-auto">
|
||||
<h2>{{ error }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
18
templates/errors/502.html
Normal file
18
templates/errors/502.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="pt-5 mt-5 text-center">
|
||||
<h1>502</h1>
|
||||
<hr class="w-50 m-auto">
|
||||
<h2>{% trans %}Bad Gateway{% endtrans %}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock %}
|
||||
55
templates/login.html
Normal file
55
templates/login.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{% trans %}Login{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col offset-md-2 col-lg-6 offset-lg-3">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% if integrations.mlc() %}
|
||||
<a class="btn btn-secondary btn-lg btn-block w-100" href="{{ url_for('auth.oauth_login') }}">
|
||||
Login with Major League Cyber
|
||||
</a>
|
||||
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% with form = Forms.auth.LoginForm() %}
|
||||
<form method="post" accept-charset="utf-8" autocomplete="off">
|
||||
<div class="mb-3">
|
||||
<b>{{ form.name.label(class="form-label") }}</b>
|
||||
{{ form.name(class="form-control", value=name) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.password.label(class="form-label") }}</b>
|
||||
{{ form.password(class="form-control", value=password) }}
|
||||
</div>
|
||||
|
||||
<div class="row pt-3">
|
||||
<div class="col-6 col-md-8">
|
||||
<a class="align-middle" href="{{ url_for('auth.reset_password') }}">
|
||||
{% trans %}Forgot your password?{% endtrans %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="col-6 col-md-4">
|
||||
{{ form.submit(class="btn btn-block btn-primary w-100") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form.nonce() }}
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
60
templates/macros/forms.html
Normal file
60
templates/macros/forms.html
Normal file
@@ -0,0 +1,60 @@
|
||||
{% macro render_extra_fields(fields, show_labels=True, show_optionals=True, show_descriptions=True) -%}
|
||||
{% for field in fields %}
|
||||
{% if field.field_type == "text" %}
|
||||
|
||||
<div class="mb-3">
|
||||
{% if show_labels %}
|
||||
<b>{{ field.label(class="form-label") }}</b>
|
||||
{% endif %}
|
||||
|
||||
{% if show_optionals %}
|
||||
{% if field.flags.required is false %}
|
||||
<small class="ms-1 text-muted">
|
||||
{% trans %}(Optional){% endtrans %}
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{{ field(class="form-control") }}
|
||||
|
||||
{% if show_descriptions %}
|
||||
{% if field.description %}
|
||||
<small class="form-text text-muted">
|
||||
{{ field.description }}
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% elif field.field_type == "boolean" %}
|
||||
|
||||
<div>
|
||||
{{ field.label(class="form-label") }}
|
||||
{% if show_optionals %}
|
||||
{% if field.flags.required is false %}
|
||||
<small class="ms-1 text-muted">
|
||||
{% trans %}(Optional){% endtrans %}
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3 border border-1 p-3">
|
||||
<div class="form-check">
|
||||
{{ field(class="form-check-input") }}
|
||||
{{ field.label(class="form-check-label") }}
|
||||
</div>
|
||||
|
||||
{% if show_descriptions %}
|
||||
{% if field.description %}
|
||||
<small class="form-text text-muted">
|
||||
{{ field.description }}
|
||||
</small>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
34
templates/notifications.html
Normal file
34
templates/notifications.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{% trans %}Notifications{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{% if not notifications %}
|
||||
<h2 class="text-center">
|
||||
{% trans %}There are no notifications yet{% endtrans %}
|
||||
</h2>
|
||||
{% endif %}
|
||||
{% for notification in notifications %}
|
||||
<div class="card bg-light mb-4">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">{{ notification.title }}</h3>
|
||||
<blockquote class="blockquote mb-0">
|
||||
<p>{{ notification.html }}</p>
|
||||
<small class="text-muted"><span data-time="{{ notification.date | isoformat }}"></span></small>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/notifications.js") }}
|
||||
{% endblock %}
|
||||
|
||||
11
templates/page.html
Normal file
11
templates/page.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
{{ content | safe }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/page.js") }}
|
||||
{% endblock %}
|
||||
81
templates/register.html
Normal file
81
templates/register.html
Normal file
@@ -0,0 +1,81 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{% trans %}Register{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% if integrations.mlc() %}
|
||||
<a class="btn btn-secondary btn-lg btn-block w-100" href="{{ url_for('auth.oauth_login') }}">
|
||||
Login with Major League Cyber
|
||||
</a>
|
||||
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
||||
{% with form = Forms.auth.RegistrationForm() %}
|
||||
|
||||
{% from "macros/forms.html" import render_extra_fields %}
|
||||
|
||||
<form method="post" accept-charset="utf-8" autocomplete="off" role="form">
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.name.label(class="form-label") }}</b>
|
||||
{{ form.name(class="form-control", value=name) }}
|
||||
<small class="form-text text-muted">
|
||||
{% trans %}Your username on the site{% endtrans %}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.email.label(class="form-label") }}</b>
|
||||
{{ form.email(class="form-control", value=email) }}
|
||||
<small class="form-text text-muted">
|
||||
{% trans %}Never shown to the public{% endtrans %}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.password.label(class="form-label") }}</b>
|
||||
{{ form.password(class="form-control", value=password) }}
|
||||
<small class="form-text text-muted">
|
||||
{% trans %}Password used to log into your account{% endtrans %}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
{{ form.nonce() }}
|
||||
|
||||
{{ render_extra_fields(form.extra) }}
|
||||
|
||||
<div class="row pt-3">
|
||||
<div class="col-6 col-md-4 offset-6 offset-md-8">
|
||||
{{ form.submit(class="btn btn-block btn-primary w-100") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if Configs.tos_or_privacy %}
|
||||
<div class="row pt-3">
|
||||
<div class="col-md-12 text-center">
|
||||
<small class="text-muted text-center">
|
||||
By registering, you agree to the
|
||||
<a href="{{ Configs.privacy_link }}" rel="noopener" target="_blank">privacy policy</a>
|
||||
and <a href="{{ Configs.tos_link }}" rel="noopener" target="_blank">terms of service</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
66
templates/reset_password.html
Normal file
66
templates/reset_password.html
Normal file
@@ -0,0 +1,66 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{% trans %}Reset Password{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% if mode == "set" %}
|
||||
{% with form = Forms.auth.ResetPasswordForm() %}
|
||||
<form method="post" accept-charset="utf-8" autocomplete="off" role="form" class="form-horizontal">
|
||||
<p>
|
||||
{% trans %}You can now reset the password for your account and log in. Please enter in a new password below.{% endtrans %}
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.password.label(class="form-label") }}
|
||||
{{ form.password(class="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="row pt-3">
|
||||
<div class="col-6 col-md-4 offset-6 offset-md-8">
|
||||
{{ form.submit(class="btn btn-block btn-primary w-100") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form.nonce() }}
|
||||
</form>
|
||||
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with form = Forms.auth.ResetPasswordRequestForm() %}
|
||||
|
||||
<form method="post" accept-charset="utf-8" autocomplete="off" role="form" class="form-horizontal">
|
||||
<p>
|
||||
{% trans %}Please provide the email address associated with your account below.{% endtrans %}
|
||||
</p>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.email.label(class="form-label") }}
|
||||
{{ form.email(class="form-control ") }}
|
||||
</div>
|
||||
|
||||
<div class="row pt-3">
|
||||
<div class="col-6 col-md-4 offset-6 offset-md-8">
|
||||
{{ form.submit(class="btn btn-block btn-primary w-100") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form.nonce() }}
|
||||
</form>
|
||||
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
71
templates/scoreboard.html
Normal file
71
templates/scoreboard.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{% trans %}Scoreboard{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
<div id="score-graph" class="d-flex align-items-center" x-data="ScoreboardDetail" x-ref="scoregraph">
|
||||
<div class="col-md-12 text-center">
|
||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% cache 60, CacheKeys.PUBLIC_SCOREBOARD_TABLE %}
|
||||
{% if standings %}
|
||||
|
||||
<div id="scoreboard" class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style="width: 10px"><b>{% trans %}Place{% endtrans %}</b></td>
|
||||
<td><b>{{ get_mode_as_word(capitalize=True) }}</b></td>
|
||||
<td><b>{% trans %}Score{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{% for standing in standings %}
|
||||
<tr>
|
||||
<th scope="row" class="text-center">{{ loop.index }}</th>
|
||||
<td>
|
||||
<a href="{{ generate_account_url(standing.account_id) }}">
|
||||
{{ standing.name | truncate(50) }}
|
||||
|
||||
{% if standing.oauth_id %}
|
||||
{% if Configs.user_mode == 'teams' %}
|
||||
<a href="https://majorleaguecyber.org/t/{{ standing.name }}">
|
||||
<span class="badge bg-primary ms-2">Official</span>
|
||||
</a>
|
||||
{% elif Configs.user_mode == 'users' %}
|
||||
<a href="https://majorleaguecyber.org/u/{{ standing.name }}">
|
||||
<span class="badge bg-primary ms-2">Official</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ standing.score }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
{% endcache %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/scoreboard.js") }}
|
||||
{% endblock %}
|
||||
257
templates/settings.html
Normal file
257
templates/settings.html
Normal file
@@ -0,0 +1,257 @@
|
||||
{% extends "base.html" %}
|
||||
{% from "macros/forms.html" import render_extra_fields %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>
|
||||
{% trans %}Settings{% endtrans %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-3 offset-md-1 mb-3 mb-md-0">
|
||||
<div class="nav flex-column nav-pills" role="tablist">
|
||||
<button
|
||||
class="nav-link active" id="settings-profile-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#profile" role="tab"
|
||||
>{% trans %}Profile{% endtrans %}
|
||||
</button>
|
||||
<button
|
||||
class="nav-link" id="settings-tokens-tab" data-bs-toggle="pill" data-bs-target="#tokens"
|
||||
role="tab"
|
||||
>{% trans %}Access Tokens{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-8">
|
||||
<div class="tab-content" id="v-pills-tabContent">
|
||||
<div class="tab-pane fade show active" id="profile" role="tabpanel">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% with form = Forms.self.SettingsForm(language=language, country=country) %}
|
||||
<form
|
||||
method="post" accept-charset="utf-8" autocomplete="off" role="form"
|
||||
x-data="SettingsForm"
|
||||
@submit.prevent="updateProfile()"
|
||||
class="form-horizontal"
|
||||
>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.name.label(class="form-label") }}</b>
|
||||
{{ form.name(class="form-control", value=name) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.email.label(class="form-label") }}</b>
|
||||
{{ form.email(class="form-control", value=email) }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.language.label(class="form-label") }}</b>
|
||||
{{ form.language(class="form-control form-select", value=language) }}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.confirm.label(class="form-label") }}</b>
|
||||
{{ form.confirm(class="form-control") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.password.label(class="form-label") }}</b>
|
||||
{{ form.password(class="form-control") }}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.affiliation.label(class="form-label") }}</b>
|
||||
{{ form.affiliation(class="form-control", value=affiliation or "") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.website.label(class="form-label") }}</b>
|
||||
{{ form.website(class="form-control", value=website or "") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.country.label(class="form-label") }}</b>
|
||||
{{ form.country(class="form-control form-select", value=country) }}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{{ render_extra_fields(form.extra) }}
|
||||
|
||||
<div id="results" class="mb-3">
|
||||
<div
|
||||
class="alert alert-success alert-dismissible submit-row" role="alert"
|
||||
x-cloak="success" x-show="success"
|
||||
>
|
||||
<strong>Success!</strong>
|
||||
{% trans %}Your profile has been updated{% endtrans %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<template x-for="(msg, idx) in errors" :key="idx">
|
||||
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||
<span class="sr-only">{% trans %}Error:{% endtrans %}</span>
|
||||
<span x-text="msg"></span>
|
||||
<button
|
||||
type="button" class="btn-close" data-bs-dismiss="alert"
|
||||
aria-label="Close"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.submit(class="btn btn-primary float-end px-4") }}
|
||||
</div>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tokens" role="tabpanel">
|
||||
|
||||
{% with form = Forms.self.TokensForm() %}
|
||||
<form method="POST" x-data="TokensForm" @submit.prevent="generateToken()" class="mb-3">
|
||||
<div class="modal fade" tabindex="-1" x-ref="tokenModal">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans %}API Key Generated{% endtrans %}</h5>
|
||||
<button
|
||||
type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>{% trans %}Please copy your API Key, it won't be shown again!{% endtrans %}</p>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<input
|
||||
type="text" class="form-control bg-white" x-ref="token"
|
||||
x-model="token" readonly
|
||||
>
|
||||
<button
|
||||
class="btn btn-outline-secondary px-3" type="button"
|
||||
@click="copyToken()"
|
||||
>
|
||||
<i class="fas fa-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">
|
||||
Got it!
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.expiration.label(class="form-label") }}</b>
|
||||
{{ form.expiration(class="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{ form.submit(class="btn btn-block btn-primary float-end px-4") }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endwith %}
|
||||
|
||||
{% if tokens %}
|
||||
<hr>
|
||||
|
||||
<h4 class="text-center mt-3">{% trans %}Active Tokens{% endtrans %}</h4>
|
||||
|
||||
{# This has to be wrapping the table modal, because div with modal will get pushed out
|
||||
of the table and alpine ref will not work #}
|
||||
<div x-data="Tokens">
|
||||
<div
|
||||
class="modal fade" x-ref="confirmModal" tabindex="-1" role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans %}Delete Token{% endtrans %}</h5>
|
||||
<button
|
||||
type="button" class="btn-close" data-bs-dismiss="modal"
|
||||
aria-label="Close"
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<p>{% trans %}Are you sure you want to delete this token?{% endtrans %}</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal">
|
||||
No
|
||||
</button>
|
||||
<button
|
||||
type="button" class="btn btn-primary" data-bs-dismiss="modal"
|
||||
@click="deleteSelectedToken()"
|
||||
>Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>{% trans %}Created{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Expiration{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Delete{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for token in tokens %}
|
||||
<tr x-ref="token-{{ token.id }}">
|
||||
<td class="text-center">
|
||||
<span data-time="{{ token.created | isoformat }}"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span data-time="{{ token.expiration | isoformat }}"></span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span
|
||||
class="delete-token" role="button"
|
||||
@click="deleteTokenModal({{ token.id }})">
|
||||
<i class="cursor-pointer fas fa-times"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/settings.js") }}
|
||||
{% endblock %}
|
||||
322
templates/setup.html
Normal file
322
templates/setup.html
Normal file
@@ -0,0 +1,322 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
.card-radio:checked + .card {
|
||||
background-color: transparent !important;
|
||||
border-color: #a3d39c;
|
||||
box-shadow: 0 0 0 0.1rem #a3d39c;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
.card-radio:checked + .card .card-radio-clone{
|
||||
visibility: visible !important;
|
||||
}
|
||||
.card:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{% trans %}Setup{% endtrans %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="col-md-8 offset-md-2">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% with form = Forms.setup.SetupForm() %}
|
||||
<form
|
||||
x-data="SetupForm"
|
||||
@submit="submitSetup"
|
||||
method="post"
|
||||
accept-charset="utf-8"
|
||||
autocomplete="off"
|
||||
role="form" class="form-horizontal"
|
||||
id="setup-form"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<ul class="nav nav-pills nav-fill mb-4">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-bs-toggle="pill" data-bs-target="#general" role="button">{% trans %}General{% endtrans %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" data-bs-target="#mode" role="button">{% trans %}Mode{% endtrans %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" data-bs-target="#administration" role="button">{% trans %}Administration{% endtrans %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" data-bs-target="#style" role="button">{% trans %}Style{% endtrans %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" data-bs-target="#datetime" role="button">{% trans %}Date & Time{% endtrans %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-bs-toggle="pill" data-bs-target="#integrations" role="button">{% trans %}Integrations{% endtrans %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade show active" id="general" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<b>{{ form.ctf_name.label }}</b>
|
||||
{{ form.ctf_name(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.ctf_name.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.ctf_description.label }}</b>
|
||||
{{ form.ctf_description(class="form-control", rows="5") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.ctf_description.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<button type="button" class="btn btn-primary btn-outlined tab-next" data-href="#mode" @click="switchTab">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="mode" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<b>{{ form.user_mode.label }}</b>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.user_mode.description }}
|
||||
</small>
|
||||
|
||||
<div class="row pt-3">
|
||||
{% for radio in form.user_mode %}
|
||||
<label class="w-50 p-1">
|
||||
{{ radio(class="card-radio d-none") }}
|
||||
<div class="card rounded-0 h-100">
|
||||
<div class="card-body p-3">
|
||||
<span class="card-title">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input card-radio-clone" type="radio" style="visibility: hidden;" checked>
|
||||
<span class="form-check-label text-center">
|
||||
<h5>{{ radio.label }}</h5>
|
||||
</span>
|
||||
{% if radio.data == "teams" %}
|
||||
<ul class="p-0 small">
|
||||
<li>{% trans %}Participants register accounts and form teams{% endtrans %}</li>
|
||||
<li>{% trans %}If a team member solves a challenge, the entire team receives credit{% endtrans %}</li>
|
||||
<br>
|
||||
<li>{% trans %}Easier to see which team member solved a challenge{% endtrans %}</li>
|
||||
<li>{% trans %}May be slightly more difficult to administer{% endtrans %}</li>
|
||||
</ul>
|
||||
{% elif radio.data == "users" %}
|
||||
<ul class="p-0 small">
|
||||
<li>{% trans %}Participants only register an individual account{% endtrans %}</li>
|
||||
<li>{% trans %}Players can share accounts to form pseudo-teams{% endtrans %}</li>
|
||||
<br>
|
||||
<li>{% trans %}Easier to organize{% endtrans %}</li>
|
||||
<li>{% trans %}Difficult to attribute solutions to individual team members{% endtrans %}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<button type="button" class="btn btn-primary btn-outlined tab-next" data-href="#administration" @click="switchTab">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="administration" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<b>{{ form.name.label }}</b>
|
||||
{{ form.name(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.name.description }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.email.label }}</b>
|
||||
{{ form.email(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.email.description }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.password.label }}</b>
|
||||
{{ form.password(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.password.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" value="" id="newsletter-checkbox" checked>
|
||||
{% trans %}Subscribe email address to the CTFd LLC Newsletter for news and updates{% endtrans %}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<button type="button" class="btn btn-primary btn-outlined tab-next" data-href="#style" @click="switchTab">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="style" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<b>{{ form.ctf_logo.label }}</b>
|
||||
<div class="row">
|
||||
{{ form.ctf_logo(class="form-control-file", accept="image/*", **{'@change': 'validateFileSize($event, 128000)'}) }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.ctf_logo.description }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.ctf_banner.label }}</b>
|
||||
<div class="row">
|
||||
{{ form.ctf_banner(class="form-control-file", accept="image/*", **{'@change': 'validateFileSize($event, 512000)'}) }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.ctf_banner.description }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.ctf_small_icon.label }}</b>
|
||||
<div class="row">
|
||||
{{ form.ctf_small_icon(class="form-control-file", accept=".png", **{'@change': 'validateFileSize($event, 32000)'}) }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.ctf_small_icon.description }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.ctf_theme.label }}</b>
|
||||
{{ form.ctf_theme(class="form-control custom-select") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.ctf_theme.description }}
|
||||
</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.theme_color.label }}</b>
|
||||
<div class="row">
|
||||
<div class="d-inline-block">
|
||||
{{ form.theme_color(id="config-color-input") }}
|
||||
<div class="btn-group">
|
||||
<input type="color" id="config-color-picker" class="pr-1" style="width: 100px; height: 30px;" value="" @input="setThemeColor">
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="button" id="config-color-reset" @click="resetThemeColor">Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.theme_color.description }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button type="button" class="btn btn-primary btn-outlined tab-next" data-href="#datetime" @click="switchTab">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="datetime" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<b>{{ form.start.label }}</b>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label>{% trans %}Date{% endtrans %}</label>
|
||||
<input class="form-control" id="start-date" type="date" placeholder="yyyy-mm-dd" data-preview="#start" @click="processDateTime('start')"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label>{% trans %}Time{% endtrans %}</label>
|
||||
<input class="form-control" id="start-time" type="time" placeholder="hh:mm" data-preview="#start" @click="processDateTime('start')"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label>UTC Preview</label>
|
||||
{{ form.start(class="form-control", id="start-preview", readonly=True) }}
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.start.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<b>{{ form.end.label }}</b>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label>{% trans %}Date{% endtrans %}</label>
|
||||
<input class="form-control" id="end-date" type="date" placeholder="yyyy-mm-dd" data-preview="#end" @click="processDateTime('end')"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label>{% trans %}Time{% endtrans %}</label>
|
||||
<input class="form-control" id="end-time" type="time" placeholder="hh:mm" data-preview="#end" @click="processDateTime('end')"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label>{% trans %}UTC Preview{% endtrans %}</label>
|
||||
{{ form.end(class="form-control", id="end-preview", readonly=True) }}
|
||||
</div>
|
||||
</div>
|
||||
<small class="form-text text-muted">
|
||||
{{ form.end.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<button type="button" class="btn btn-primary btn-outlined tab-next" data-href="#integrations" @click="switchTab">
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="integrations" role="tabpanel">
|
||||
<div class="mb-3">
|
||||
<h4>MajorLeagueCyber Integration</h4>
|
||||
<p>
|
||||
MajorLeagueCyber (MLC) is a cyber security event tracker written and maintained by the developers of CTFd.
|
||||
Set up MLC integration to:
|
||||
<ul>
|
||||
<li>display your event on the MLC website and mailing list</li>
|
||||
<li>share and track participant statistics</li>
|
||||
<li>provide live updates in team Slack or Discord groups</li>
|
||||
<li>archive challenges, team participation & more</li>
|
||||
</ul>
|
||||
</p>
|
||||
<button type="button" id="integration-mlc" class="btn btn-primary btn-block" @click="mlcSetup()">
|
||||
Setup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<hr>
|
||||
<br>
|
||||
|
||||
<div class="submit-row text-right">
|
||||
{{ form.submit(class="btn btn-md btn-primary btn-outlined") }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form.nonce() }}
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
var STATE = {{ state | tojson }};
|
||||
</script>
|
||||
{{ Assets.js("assets/js/setup.js") }}
|
||||
{% endblock %}
|
||||
39
templates/teams/invite.html
Normal file
39
templates/teams/invite.html
Normal file
@@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{% trans %}Join Team{% endtrans %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-lg-6 offset-md-2 offset-lg-3">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
<p class="h3 text-center mt-md-4">
|
||||
{% trans %}Welcome to{% endtrans %} <strong>{{ team.name }}</strong>!
|
||||
</p>
|
||||
|
||||
<p class="text-center">
|
||||
{% trans %}Click the button below to join the team!{% endtrans %}
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
Or <a href="{{ url_for('teams.new') }}">click here</a> if you'd prefer to create your own team.
|
||||
</small>
|
||||
</p>
|
||||
|
||||
{% with form = Forms.teams.TeamInviteJoinForm() %}
|
||||
<form method="POST">
|
||||
<div class="row pt-3 text-center">
|
||||
<div class="col-md-12">
|
||||
{{ form.submit(class="btn btn-success w-100") }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form.nonce() }}
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
37
templates/teams/join_team.html
Normal file
37
templates/teams/join_team.html
Normal file
@@ -0,0 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{% trans %}Join Team{% endtrans %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-8 col-lg-6 offset-md-2 offset-lg-3">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% with form = Forms.teams.TeamJoinForm() %}
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
{{ form.name.label(class="form-label") }}
|
||||
{{ form.name(class="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
{{ form.password.label(class="form-label") }}
|
||||
{{ form.password(class="form-control") }}
|
||||
</div>
|
||||
|
||||
<div class="row pt-3">
|
||||
<div class="col-md-12">
|
||||
{{ form.submit(class="btn btn-success float-end px-4") }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form.nonce() }}
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
41
templates/teams/new_team.html
Normal file
41
templates/teams/new_team.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{% trans %}Create Team{% endtrans %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% with form = Forms.teams.TeamRegisterForm() %}
|
||||
{% from "macros/forms.html" import render_extra_fields %}
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<b>{{ form.name.label(class="form-label") }}</b>
|
||||
{{ form.name(class="form-control") }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b>{{ form.password.label(class="form-label") }}</b>
|
||||
{{ form.password(class="form-control") }}
|
||||
</div>
|
||||
|
||||
{{ render_extra_fields(form.extra) }}
|
||||
|
||||
<div class="row ">
|
||||
<div class="col-md-12">
|
||||
<p>After creating your team, share the team name and password with your teammates so they can join your
|
||||
team.</p>
|
||||
{{ form.submit(class="btn btn-success float-end px-4") }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form.nonce() }}
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
487
templates/teams/private.html
Normal file
487
templates/teams/private.html
Normal file
@@ -0,0 +1,487 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="team-edit-modal" class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-action text-center w-100">{% trans %}Edit Team{% endtrans %}</h2>
|
||||
<button type="button" class="cursor-pointer btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body clearfix" x-data="TeamEditModal">
|
||||
{% with form = Forms.teams.TeamSettingsForm(obj=team) %}
|
||||
{% from "macros/forms.html" import render_extra_fields %}
|
||||
<form id="team-info-form" method="POST" @submit.prevent="updateProfile()">
|
||||
|
||||
<div class="mb-2">
|
||||
<b>{{ form.name.label(clas="mb-2") }}</b>
|
||||
{{ form.name(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.name.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<b>{{ form.password.label(clas="mb-2") }}</b>
|
||||
{{ form.password(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.password.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<b>{{ form.confirm.label(clas="mb-2") }}</b>
|
||||
{{ form.confirm(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.confirm.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<b>{{ form.website.label(clas="mb-2") }}</b>
|
||||
{{ form.website(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.website.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<b>{{ form.affiliation.label(clas="mb-2") }}</b>
|
||||
{{ form.affiliation(class="form-control") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.affiliation.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<b>{{ form.country.label(clas="mb-2") }}</b>
|
||||
{{ form.country(class="form-control form-select") }}
|
||||
<small class="form-text text-muted">
|
||||
{{ form.country.description }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{{ render_extra_fields(form.extra) }}
|
||||
|
||||
<div id="results">
|
||||
<div
|
||||
class="alert alert-success alert-dismissible submit-row"
|
||||
role="alert" x-cloak="success" x-show="success"
|
||||
>
|
||||
<strong>Success!</strong>
|
||||
{% trans %}Your team's profile has been updated{% endtrans %}
|
||||
<button type="button" class="cursor-pointer btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template x-for="(msg, idx) in errors" :key="idx">
|
||||
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||
<span class="sr-only">{% trans %}Error:{% endtrans %}</span>
|
||||
<span x-text="msg"></span>
|
||||
|
||||
<button type="button" class="cursor-pointer btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
{{ form.submit(class="btn btn-primary float-end px-4 modal-action") }}
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="team-captain-modal" x-data="TeamCaptainModal" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-action text-center w-100">{% trans %}Choose Captain{% endtrans %}</h2>
|
||||
<button type="button" class="cursor-pointer btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body clearfix">
|
||||
{% with form = Forms.teams.TeamCaptainForm(captain_id=team.captain_id) %}
|
||||
<form id="team-captain-form" method="POST" @submit.prevent="updateCaptain()">
|
||||
<div class="mb-3">
|
||||
{{ form.captain_id.label(class="form-label") }}
|
||||
{% for member in team.members %}
|
||||
{# Append members to the select choices #}
|
||||
{% set _ = form.captain_id.choices.append((member.id, member.name)) %}
|
||||
{% endfor %}
|
||||
{{ form.captain_id(class="form-control form-select mb-2") }}
|
||||
</div>
|
||||
<div id="results">
|
||||
<div
|
||||
class="alert alert-success alert-dismissible submit-row"
|
||||
role="alert"
|
||||
x-cloak="success"
|
||||
x-show="success"
|
||||
>
|
||||
<strong>Success!</strong>
|
||||
{% trans %}Your captain rights have been transferred{% endtrans %}
|
||||
<button type="button" class="cursor-pointer btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<template x-for="(msg, idx) in errors" :key="idx">
|
||||
<div class="alert alert-danger alert-dismissible" role="alert">
|
||||
<span class="sr-only">Error:</span>
|
||||
<span x-text="msg"></span>
|
||||
<button type="button" class="cursor-pointer btn-close" data-bs-dismiss="alert" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
{{ form.submit(class="btn btn-primary float-end px-4 modal-action") }}
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="team-invite-modal" x-data="TeamInviteModal" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-action text-center w-100">{% trans %}Invite Users{% endtrans %}</h2>
|
||||
<button type="button" class="cursor-pointer btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body clearfix">
|
||||
{% with form = Forms.teams.TeamInviteForm() %}
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<b>{{ form.link.label(class="form-label") }}</b>
|
||||
|
||||
<div class="input-group mb-3">
|
||||
{{ form.link(id="team-invite-link", class="form-control", **{"x-ref": "link", "x-bind:value": "$store.inviteToken"}) }}
|
||||
|
||||
<button class="btn btn-outline-secondary px-3" type="button" @click="copy()">
|
||||
<i class="fas fa-clipboard"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<small class="form-text text-muted">
|
||||
{% trans %}Share this link with your team members for them to join your team{% endtrans %}
|
||||
</small>
|
||||
<small class="form-text text-muted">
|
||||
{% trans %}Invite links can be re-used and expire after 1 day{% endtrans %}
|
||||
</small>
|
||||
</div>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="team-disband-modal" x-data="TeamDisbandModal" class="modal fade">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-action text-center w-100">{% trans %}Disband Team{% endtrans %}</h2>
|
||||
<button type="button" class="cursor-pointer btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="mb-0">{% trans %}Are you sure you want to disband your team?{% endtrans %}</p>
|
||||
|
||||
<div class="mt-3">
|
||||
<template x-for="(msg, idx) in errors" :key="idx">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<span class="sr-only">{% trans %}Error:{% endtrans %}</span>
|
||||
<span x-text="msg"></span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal"{% trans %}>No{% endtrans %}</button>
|
||||
<button type="button" class="btn btn-primary" @click="disbandTeam()" :disabled="errors.length > 0">{% trans %}Yes{% endtrans %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1 id="team-id" team-id="{{ team.id }}">{{ team.name }}</h1>
|
||||
{% if team.oauth_id %}
|
||||
<a href="https://majorleaguecyber.org/t/{{ team.name }}">
|
||||
<h3><span class="badge bg-primary">{% trans %}Official{% endtrans %}</span></h3>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if team.affiliation %}
|
||||
<h3 class="d-inline-block">
|
||||
<span class="badge bg-primary">{{ team.affiliation }}</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if team.country %}
|
||||
<h3 class="d-inline-block">
|
||||
<span class="badge bg-primary">
|
||||
<i class="flag-{{ team.country.lower() }}"></i>
|
||||
{{ lookup_country_code(team.country) }}
|
||||
</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
{% for field in team.fields %}
|
||||
<h3 class="d-block">
|
||||
{{ field.name }}: {{ field.value }}
|
||||
</h3>
|
||||
{% endfor %}
|
||||
|
||||
<h2 id="team-place" class="text-center">
|
||||
{# This intentionally hides the team's place when scores are hidden because this can be their internal profile
|
||||
and we don't want to leak their place in the CTF. #}
|
||||
{# Public page hiding is done at the route level #}
|
||||
{% if scores_visible() %}
|
||||
{% if place %}
|
||||
{{ place }}
|
||||
<small>{% trans %}place{% endtrans %}</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<h2 id="team-score" class="text-center">
|
||||
{% if score %}
|
||||
{{ score }}
|
||||
<small>{% trans %}points{% endtrans %}</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<div x-data="CaptainMenu">
|
||||
<a class="edit-team text-white" @click="editTeam()">
|
||||
<i
|
||||
class="cursor-pointer fas fa-cogs fa-2x px-2 pt-3"
|
||||
data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Edit Team"
|
||||
></i>
|
||||
</a>
|
||||
<a class="edit-captain text-white" @click="chooseCaptain()">
|
||||
<i
|
||||
class="cursor-pointer fas fa-user-tag fa-2x px-2 pt-3"
|
||||
data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Choose Captain"
|
||||
></i>
|
||||
</a>
|
||||
|
||||
<a class="invite-members text-white" @click="inviteMembers()">
|
||||
<i
|
||||
class="cursor-pointer fas fa-ticket-alt fa-2x px-2 pt-3"
|
||||
data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Invite Users"
|
||||
></i>
|
||||
</a>
|
||||
<a class="disband-team text-white" @click="disbandTeam()">
|
||||
<i
|
||||
class="cursor-pointer fas fa-trash-alt fa-2x px-2 pt-3"
|
||||
data-bs-toggle="tooltip" data-bs-placement="top"
|
||||
title="Disband Team"
|
||||
></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="pt-3">{% if team.website and (team.website.startswith('http://') or team.website.startswith('https://')) %}
|
||||
<a href="{{ team.website }}" target="_blank" style="color: inherit;" rel="noopener">
|
||||
<i
|
||||
class="fas fa-external-link-alt fa-2x px-2 pt-2"
|
||||
data-toggle="tooltip" data-placement="top"
|
||||
title="{{ team.website }}"
|
||||
></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row min-vh-25">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Members{% endtrans %}</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>{% trans %}User Name{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Score{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for member in team.members %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('users.public', user_id=member.id) }}">
|
||||
{{ member.name }}
|
||||
</a>
|
||||
{% if team.captain_id == member.id %}
|
||||
<span class="badge bg-primary ms-2">{% trans %}Captain{% endtrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ member.score }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if solves or awards %}
|
||||
{% if awards %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Awards{% endtrans %}</h3>
|
||||
</div>
|
||||
|
||||
{% for award in awards %}
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<p class="text-center">
|
||||
<i class="award-icon award-{{ award.icon }} fa-2x"></i>
|
||||
<br>
|
||||
<strong>{{ award.name }}</strong>
|
||||
</p>
|
||||
{% if award.category %}<p class="text-center">{{ award.category }}</p>{% endif %}
|
||||
{% if award.description %}<p class="text-center">{{ award.description }}</p>{% endif %}
|
||||
<p class="text-center">{{ award.value }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Solves{% endtrans %}</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>{% trans %}Challenge{% endtrans %}</b></td>
|
||||
<td class="d-none d-md-block d-lg-block"><b>{% trans %}Category{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Value{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Time{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for solve in solves %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('challenges.listing') }}#{{ solve.challenge.name }}-{{ solve.challenge.id }}">
|
||||
{{ solve.challenge.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="d-none d-md-block d-lg-block">{{ solve.challenge.category }}</td>
|
||||
<td>{{ solve.challenge.value }}</td>
|
||||
<td class="solve-time">
|
||||
<span data-time="{{ solve.date | isoformat }}">{{ solve.date }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row" x-data="TeamGraphs">
|
||||
<div class="col-md-6 d-none d-md-block d-lg-block py-4">
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${getSolvePercentage()}%`, 'background-color': 'rgb(0, 209, 64)' }"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${getFailPercentage()}%`, 'background-color': 'rgb(207, 38, 0)' }"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" fill="rgb(0, 209, 64)" />
|
||||
</svg>
|
||||
<small x-text="`Solves (${getSolvePercentage()}%)`"></small>
|
||||
</div>
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" fill="rgb(207, 38, 0)" />
|
||||
</svg>
|
||||
<small x-text="`Fails (${getFailPercentage()}%)`"></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-none d-md-block d-lg-block py-4">
|
||||
<div class="progress">
|
||||
<template x-for="category in getCategoryBreakdown()" :key="category.name">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${category.percent}%`, 'background-color': category.color }"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-for="category in getCategoryBreakdown()" :key="category.name">
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" :fill="category.color" />
|
||||
</svg>
|
||||
<small x-text="`${category.name} (${category.percent}%)`"></small>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<br class="clearfix">
|
||||
|
||||
<div class="col-md-12 d-none d-md-block d-lg-block">
|
||||
<div id="score-graph" x-ref="scoregraph" class="w-100 d-flex align-items-center">
|
||||
<div class="text-center w-100">
|
||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row min-vh-25">
|
||||
<h3 class="opacity-50 text-center w-100 justify-content-center align-self-center">
|
||||
{% trans %}No solves yet{% endtrans %}
|
||||
</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
window.stats_data = {{ {
|
||||
'type': 'team',
|
||||
'id': team.id,
|
||||
'name': team.name,
|
||||
'account_id': 'me',
|
||||
} | tojson }};
|
||||
|
||||
window.team_captain = {{ (user.id == team.captain_id) | tojson }};
|
||||
</script>
|
||||
{{ Assets.js("assets/js/teams/private.js") }}
|
||||
{% endblock %}
|
||||
253
templates/teams/public.html
Normal file
253
templates/teams/public.html
Normal file
@@ -0,0 +1,253 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1 id="team-id" data-ctfd-team-id="{{ team.id }}">{{ team.name }}</h1>
|
||||
|
||||
{% if team.oauth_id %}
|
||||
<a href="https://majorleaguecyber.org/t/{{ team.name }}">
|
||||
<h3 class="d-inline-block mx-1"><span class="badge bg-primary rounded-pill">{% trans %}Official{% endtrans %}</span></h3>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if team.affiliation %}
|
||||
<h3 class="d-inline-block mx-1">
|
||||
<span class="badge bg-primary rounded-pill">{{ team.affiliation }}</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if team.country %}
|
||||
<h3 class="d-inline-block mx-1">
|
||||
<span class="badge bg-primary rounded-pill">
|
||||
<i class="flag-{{ team.country.lower() }}"></i>
|
||||
{{ lookup_country_code(team.country) }}
|
||||
</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
<div class="pt-2">
|
||||
{% for field in team.fields %}
|
||||
<h3 class="d-block">
|
||||
{{ field.name }}: {{ field.value }}
|
||||
</h3>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
{% if team.website and (team.website.startswith('http://') or team.website.startswith('https://')) %}
|
||||
<a href="{{ team.website }}" target="_blank" style="color: inherit;" rel="noopener">
|
||||
<i
|
||||
class="fas fa-external-link-alt fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
||||
title="{{ team.website }}"
|
||||
></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if team.fields or team.website %}
|
||||
<hr class="w-50 mx-auto">
|
||||
{% endif %}
|
||||
|
||||
<h2 id="team-place" class="text-center">
|
||||
{# This intentionally hides the team's place when scores are hidden because this can be
|
||||
their internal profile and we don't want to leak their place in the CTF. #}
|
||||
|
||||
{# Public page hiding is done at the route level #}
|
||||
{% if scores_visible() %}
|
||||
{% if place %}
|
||||
{{ place }}
|
||||
<small>place</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<h2 id="team-score" class="text-center">
|
||||
{% if score %}
|
||||
{{ score }}
|
||||
<small>points</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Members{% endtrans %}</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>{% trans %}User Name{% trans %}</b></td>
|
||||
<td><b>{% trans %}Score{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in team.members %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('users.public', user_id=member.id) }}">
|
||||
{{ member.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ member.score }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if solves or awards %}
|
||||
{% if awards %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Awards{% endtrans %}</h3>
|
||||
</div>
|
||||
|
||||
{% for award in awards %}
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<p class="text-center">
|
||||
<i class="award-icon award-{{ award.icon }} fa-2x"></i> <br>
|
||||
<strong>{{ award.name }}</strong>
|
||||
</p>
|
||||
|
||||
{% if award.category %}
|
||||
<p class="text-center">{{ award.category }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if award.description %}
|
||||
<p class="text-center">{{ award.description }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p class="text-center">{{ award.value }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Solves{% endtrans %}</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center"><b>{% trans %}Challenge{% endtrans %}</b></td>
|
||||
<td class="d-none d-md-block d-lg-block"><b>{% trans %}Category{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Value{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Time{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solve in solves %}
|
||||
<tr>
|
||||
<td class="text-center">
|
||||
<a href="{{ url_for('challenges.listing') }}#{{ solve.challenge.name }}-{{ solve.challenge.id }}">
|
||||
{{ solve.challenge.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="d-none d-md-block d-lg-block">{{ solve.challenge.category }}</td>
|
||||
<td>{{ solve.challenge.value }}</td>
|
||||
<td class="solve-time">
|
||||
<span data-time="{{ solve.date | isoformat }}"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row" x-data="TeamGraphs">
|
||||
<div class="col-md-6 d-none d-md-block d-lg-block py-4">
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${getSolvePercentage()}%`, 'background-color': 'rgb(0, 209, 64)' }"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${getFailPercentage()}%`, 'background-color': 'rgb(207, 38, 0)' }"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" fill="rgb(0, 209, 64)" />
|
||||
</svg>
|
||||
<small x-text="`Solves (${getSolvePercentage()}%)`"></small>
|
||||
</div>
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" fill="rgb(207, 38, 0)" />
|
||||
</svg>
|
||||
<small x-text="`Fails (${getFailPercentage()}%)`"></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-none d-md-block d-lg-block py-4">
|
||||
<div class="progress">
|
||||
<template x-for="category in getCategoryBreakdown()" :key="category.name">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${category.percent}%`, 'background-color': category.color }"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-for="category in getCategoryBreakdown()" :key="category.name">
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" :fill="category.color" />
|
||||
</svg>
|
||||
<small x-text="`${category.name} (${category.percent}%)`"></small>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<br class="clearfix">
|
||||
|
||||
<div class="col-md-12 d-none d-md-block d-lg-block">
|
||||
<div id="score-graph" x-ref="scoregraph" class="w-100 d-flex align-items-center">
|
||||
<div class="text-center w-100">
|
||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row min-vh-25">
|
||||
<h3 class="opacity-50 text-center w-100 justify-content-center align-self-center">
|
||||
{% trans %}No solves yet{% endtrans %}
|
||||
</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
window.TEAM =
|
||||
{{
|
||||
{
|
||||
'type': 'team',
|
||||
'id': team.id,
|
||||
'name': team.name,
|
||||
'account_id': team.id,
|
||||
} | tojson
|
||||
}}
|
||||
</script>
|
||||
|
||||
{{ Assets.js("assets/js/teams/public.js") }}
|
||||
{% endblock %}
|
||||
47
templates/teams/team_enrollment.html
Normal file
47
templates/teams/team_enrollment.html
Normal file
@@ -0,0 +1,47 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{% trans %}Team{% endtrans %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3 text-center">
|
||||
<p class="h2">{% trans %}Welcome to{% endtrans %} {{ Configs.ctf_name }}!</p>
|
||||
<p>
|
||||
{% trans %}In order to participate you must either join or create a team.{% endtrans %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if integrations.mlc() %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 offset-md-3 text-center">
|
||||
<a class="btn btn-primary w-100" href="{{ url_for('auth.oauth_login') }}">{% trans %}Play with Official Team{% endtrans %}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="mt-3 col-md-3 offset-md-3 text-center">
|
||||
<a class="btn btn-outline-info w-100" href="{{ url_for('teams.join') }}">{% trans %}Join Unofficial Team{% endtrans %}</a>
|
||||
</div>
|
||||
<div class="mt-3 col-md-3 text-center">
|
||||
<a class="btn btn-outline-info w-100" href="{{ url_for('teams.new') }}">{% trans %}Create Unofficial Team{% endtrans %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row">
|
||||
<div class="mt-3 col-sm-4 col-md-3 offset-sm-2 offset-md-3 text-center">
|
||||
<a class="btn btn-primary w-100" href="{{ url_for('teams.join') }}">{% trans %}Join Team{% endtrans %}</a>
|
||||
</div>
|
||||
<div class="mt-3 col-sm-4 col-md-3 text-center">
|
||||
<a class="btn btn-primary w-100" href="{{ url_for('teams.new') }}">{% trans %}Create Team{% endtrans %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
149
templates/teams/teams.html
Normal file
149
templates/teams/teams.html
Normal file
@@ -0,0 +1,149 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{% trans %}Teams{% endtrans %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if q and field %}
|
||||
<h5 class="text-muted text-center">
|
||||
Searching for teams with <strong>{{ field }}</strong> matching <strong>{{ q }}</strong>
|
||||
</h5>
|
||||
<h6 class="text-muted text-center pb-3">
|
||||
Page {{ teams.page }} of {{ teams.total }} results
|
||||
</h6>
|
||||
{% endif %}
|
||||
|
||||
{% with form = Forms.teams.PublicTeamSearchForm(field=field, q=q) %}
|
||||
<form method="GET" class="row justify-content-around align-items-center">
|
||||
<div class="mb-3 col-md-2">
|
||||
{{ form.field(class="form-control form-select w-100") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-md-8">
|
||||
{{ form.q(class="form-control w-100", placeholder="Search for matching teams") }}
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-md-start"><b>{% trans %}Team{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Website{% endtrans %}</b></td>
|
||||
<td class="d-none d-md-table-cell"><b>{% trans %}Affiliation{% endtrans %}</b></td>
|
||||
<td class="d-none d-md-table-cell"><b>{% trans %}Country{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for team in teams.items %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if scores_visible() %}
|
||||
<a href="{{ url_for('teams.public', team_id=team.id) }}">{{ team.name | truncate(50) }}</a>
|
||||
{% else %}
|
||||
<span>{{ team.name | truncate(50) }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if team.oauth_id %}
|
||||
<a href="https://majorleaguecyber.org/t/{{ team.name }}">
|
||||
<span class="badge bg-primary ms-2">Official</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td style="width: 10px;">
|
||||
{% if team.website and (team.website.startswith('http://') or team.website.startswith('https://')) %}
|
||||
<a href="{{ team.website }}" target="_blank" rel="noopener">
|
||||
<i
|
||||
class="fas fa-external-link-alt"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="{{ team.website }}"
|
||||
></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="d-none d-md-table-cell">
|
||||
{% if team.affiliation %}
|
||||
|
||||
{% if team.affiliation | length > 50 %}
|
||||
<span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ team.affiliation }}">
|
||||
{% if team.affiliation %}{{ team.affiliation | truncate(50) }}{% endif %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span>
|
||||
{% if team.affiliation %}
|
||||
{{ team.affiliation | truncate(50) }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="d-none d-md-table-cell">
|
||||
<span>
|
||||
{% if team.country %}
|
||||
<i class="flag-{{ team.country.lower() }}"></i>
|
||||
{{ lookup_country_code(team.country) }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if teams.pages > 1 %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="text-center">
|
||||
{% trans %}Page{% endtrans %} <br>
|
||||
|
||||
{% if teams.page != 1 %}
|
||||
<a href="{{ prev_page }}"><<<</a>
|
||||
{% endif %}
|
||||
|
||||
<select class="page-select">
|
||||
{% for page in range(1, teams.pages + 1) %}
|
||||
<option {% if teams.page == page %}selected{% endif %}>{{ page }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% if teams.next_num %}
|
||||
<a href="{{ next_page }}">>>></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/teams/list.js") }}
|
||||
{% endblock %}
|
||||
213
templates/users/private.html
Normal file
213
templates/users/private.html
Normal file
@@ -0,0 +1,213 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{{ user.name }}</h1>
|
||||
|
||||
{% if user.team_id %}
|
||||
<h2>
|
||||
<a href="{{ url_for('teams.public', team_id=user.team_id) }}">
|
||||
<span class="badge bg-secondary">
|
||||
{{ user.team.name }}
|
||||
</span>
|
||||
</a>
|
||||
</h2>
|
||||
{% endif %}
|
||||
|
||||
<div class="pt-2">
|
||||
{% if user.oauth_id %}
|
||||
<a href="https://majorleaguecyber.org/u/{{ user.name }}">
|
||||
<h3 class="d-inline-block mx-1"><span class="badge rounded-pill bg-primary">{% trans %}Official{% endtrans %}</span></h3>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.affiliation %}
|
||||
<h3 class="d-inline-block mx-1">
|
||||
<span class="badge rounded-pill bg-primary">{{ user.affiliation }}</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if user.country %}
|
||||
<h3 class="d-inline-block mx-1">
|
||||
<span class="badge rounded-pill bg-primary">
|
||||
<i class="flag-{{ user.country.lower() }}"></i>
|
||||
{{ lookup_country_code(user.country) }}
|
||||
</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
{% for field in user.fields %}
|
||||
<h3 class="d-block">
|
||||
{{ field.name }}: {{ field.value }}
|
||||
</h3>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if user.fields %}
|
||||
<hr class="w-50 mx-auto">
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h2 class="text-center">
|
||||
{% if account.place %}
|
||||
{{ account.place }} <small>place</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<h2 class="text-center">
|
||||
{% if account.place %}
|
||||
{{ account.score }} <small>points</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="pt-3">
|
||||
{% if user.website %}
|
||||
<a href="{{ user.website }}" target="_blank" style="color: inherit;" rel="noopener">
|
||||
<i
|
||||
class="fas fa-external-link-alt fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
||||
title="{{ user.website }}"
|
||||
></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% set solves = user.solves %}
|
||||
{% set awards = user.awards %}
|
||||
|
||||
{% if solves or awards %}
|
||||
{% if awards %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Awards{% endtrans %}</h3>
|
||||
|
||||
</div>
|
||||
{% for award in awards %}
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<p class="text-center">
|
||||
<i class="award-icon award-{{ award.icon }} fa-2x"></i>
|
||||
<br>
|
||||
<strong>{{ award.name }}</strong>
|
||||
</p>
|
||||
<p class="text-center">{{ award.category or "" }}</p>
|
||||
<p class="text-center">{{ award.description or "" }}</p>
|
||||
<p class="text-center">{{ award.value }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Solves{% endtrans %}</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>{% trans %}Challenge{% endtrans %}</b></td>
|
||||
<td class="d-none d-md-block d-lg-block"><b>{% trans %}Category{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Value{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Time{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solve in solves %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('challenges.listing') }}#{{ solve.challenge.name }}-{{ solve.challenge.id }}">
|
||||
{{ solve.challenge.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="d-none d-md-block d-lg-block">{{ solve.challenge.category }}</td>
|
||||
<td>{{ solve.challenge.value }}</td>
|
||||
<td class="solve-time">
|
||||
<span data-time="{{ solve.date | isoformat }}"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row" x-data="UserGraphs">
|
||||
<div class="col-md-6 d-none d-md-block d-lg-block py-4">
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${getSolvePercentage()}%`, 'background-color': 'rgb(0, 209, 64)' }"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${getFailPercentage()}%`, 'background-color': 'rgb(207, 38, 0)' }"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" fill="rgb(0, 209, 64)" />
|
||||
</svg>
|
||||
<small x-text="`Solves (${getSolvePercentage()}%)`"></small>
|
||||
</div>
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" fill="rgb(207, 38, 0)" />
|
||||
</svg>
|
||||
<small x-text="`Fails (${getFailPercentage()}%)`"></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-none d-md-block d-lg-block py-4">
|
||||
<div class="progress">
|
||||
<template x-for="category in getCategoryBreakdown()" :key="category.name">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${category.percent}%`, 'background-color': category.color }"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-for="category in getCategoryBreakdown()" :key="category.name">
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" :fill="category.color" />
|
||||
</svg>
|
||||
<small x-text="`${category.name} (${category.percent}%)`"></small>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<br class="clearfix">
|
||||
<div class="col-md-12 d-none d-md-block d-lg-block py-4">
|
||||
<div id="score-graph" x-ref="scoregraph" class="w-100 d-flex align-items-center">
|
||||
<div class="text-center w-100">
|
||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row min-vh-25">
|
||||
<h3 class="opacity-50 text-center w-100 justify-content-center align-self-center">
|
||||
{% trans %}No solves yet{% endtrans %}
|
||||
</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/users/private.js") }}
|
||||
{% endblock %}
|
||||
|
||||
222
templates/users/public.html
Normal file
222
templates/users/public.html
Normal file
@@ -0,0 +1,222 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{{ user.name }}</h1>
|
||||
|
||||
{% if user.team_id %}
|
||||
<h2>
|
||||
<a href="{{ url_for('teams.public', team_id=user.team_id) }}">
|
||||
<span class="badge bg-secondary">
|
||||
{{ user.team.name }}
|
||||
</span>
|
||||
</a>
|
||||
</h2>
|
||||
{% endif %}
|
||||
|
||||
<div class="pt-2">
|
||||
{% if user.oauth_id %}
|
||||
<a href="https://majorleaguecyber.org/u/{{ user.name }}">
|
||||
<h3 class="d-inline-block mx-1"><span class="badge rounded-pill bg-primary">{% trans %}Official{% endtrans %}</span></h3>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% if user.affiliation %}
|
||||
<h3 class="d-inline-block mx-1">
|
||||
<span class="badge rounded-pill bg-primary">{{ user.affiliation }}</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
|
||||
{% if user.country %}
|
||||
<h3 class="d-inline-block mx-1">
|
||||
<span class="badge rounded-pill bg-primary">
|
||||
<i class="flag-{{ user.country.lower() }}"></i>
|
||||
{{ lookup_country_code(user.country) }}
|
||||
</span>
|
||||
</h3>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="pt-2">
|
||||
{% for field in user.fields %}
|
||||
<h3 class="d-block">
|
||||
{{ field.name }}: {{ field.value }}
|
||||
</h3>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if user.fields %}
|
||||
<hr class="w-50 mx-auto">
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<h2 class="text-center">
|
||||
{% if account.place %}
|
||||
{{ account.place }} <small>place</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<h2 class="text-center">
|
||||
{% if account.place %}
|
||||
{{ account.score }} <small>points</small>
|
||||
{% endif %}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="pt-3">
|
||||
{% if user.website %}
|
||||
<a href="{{ user.website }}" target="_blank" style="color: inherit;" rel="noopener">
|
||||
<i
|
||||
class="fas fa-external-link-alt fa-2x px-2" data-toggle="tooltip" data-placement="top"
|
||||
title="{{ user.website }}"
|
||||
></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{% include "components/errors.html" %}
|
||||
|
||||
{% set solves = user.solves %}
|
||||
{% set awards = user.awards %}
|
||||
{% if solves or awards %}
|
||||
|
||||
{% if awards %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>{% trans %}Awards{% endtrans %}</h3>
|
||||
</div>
|
||||
{% for award in awards %}
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<p class="text-center">
|
||||
<i class="award-icon award-{{ award.icon }} fa-2x"></i>
|
||||
<br>
|
||||
<strong>{{ award.name }}</strong>
|
||||
</p>
|
||||
<p class="text-center">{{ award.category or "" }}</p>
|
||||
<p class="text-center">{{ award.description or "" }}</p>
|
||||
<p class="text-center">{{ award.value }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<br>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3>Solves</h3>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>{% trans %}Challenge{% endtrans %}</b></td>
|
||||
<td class="d-none d-md-block d-lg-block"><b>{% trans %}Category{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Value{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Time{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for solve in solves %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ url_for('challenges.listing') }}#{{ solve.challenge.name }}-{{ solve.challenge.id }}">
|
||||
{{ solve.challenge.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="d-none d-md-block d-lg-block">{{ solve.challenge.category }}</td>
|
||||
<td>{{ solve.challenge.value }}</td>
|
||||
<td class="solve-time">
|
||||
<span data-time="{{ solve.date | isoformat }}"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="row" x-data="UserGraphs">
|
||||
<div class="col-md-6 d-none d-md-block d-lg-block py-4">
|
||||
<div class="progress">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${getSolvePercentage()}%`, 'background-color': 'rgb(0, 209, 64)' }"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${getFailPercentage()}%`, 'background-color': 'rgb(207, 38, 0)' }"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" fill="rgb(0, 209, 64)" />
|
||||
</svg>
|
||||
<small x-text="`Solves (${getSolvePercentage()}%)`"></small>
|
||||
</div>
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" fill="rgb(207, 38, 0)" />
|
||||
</svg>
|
||||
<small x-text="`Fails (${getFailPercentage()}%)`"></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 d-none d-md-block d-lg-block py-4">
|
||||
<div class="progress">
|
||||
<template x-for="category in getCategoryBreakdown()" :key="category.name">
|
||||
<div
|
||||
class="progress-bar"
|
||||
role="progressbar"
|
||||
:style="{ width: `${category.percent}%`, 'background-color': category.color }"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<template x-for="category in getCategoryBreakdown()" :key="category.name">
|
||||
<div class="ps-2 float-start">
|
||||
<svg height="16" width="16">
|
||||
<circle cx="8" cy="8" r="5" :fill="category.color" />
|
||||
</svg>
|
||||
<small x-text="`${category.name} (${category.percent}%)`"></small>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<br class="clearfix">
|
||||
|
||||
<div class="col-md-12 d-none d-md-block d-lg-block py-4">
|
||||
<div id="score-graph" x-ref="scoregraph" class="w-100 d-flex align-items-center">
|
||||
<div class="text-center w-100">
|
||||
<i class="fas fa-circle-notch fa-spin fa-3x fa-fw spinner"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="row min-vh-25">
|
||||
<h3 class="opacity-50 text-center w-100 justify-content-center align-self-center">
|
||||
{% trans %}No solves yet{% endtrans %}
|
||||
</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
window.USER = {{ {
|
||||
'type': 'user',
|
||||
'id': user.id,
|
||||
'name': user.name,
|
||||
'account_id': user.id,
|
||||
} | tojson }};
|
||||
</script>
|
||||
|
||||
{{ Assets.js("assets/js/users/public.js") }}
|
||||
{% endblock %}
|
||||
138
templates/users/users.html
Normal file
138
templates/users/users.html
Normal file
@@ -0,0 +1,138 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="jumbotron">
|
||||
<div class="container">
|
||||
<h1>{% trans %}Users{% endtrans %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if q and field %}
|
||||
<h5 class="text-muted text-center">
|
||||
Searching for users with <strong>{{ field }}</strong> matching <strong>{{ q }}</strong>
|
||||
</h5>
|
||||
<h6 class="text-muted text-center pb-3">
|
||||
Page {{ users.page }} of {{ users.total }} results
|
||||
</h6>
|
||||
{% endif %}
|
||||
|
||||
{% with form = Forms.users.PublicUserSearchForm(field=field, q=q) %}
|
||||
<form method="GET" class="row justify-content-around align-items-center">
|
||||
<div class="mb-3 col-md-2">
|
||||
{{ form.field(class="form-control form-select w-100") }}
|
||||
</div>
|
||||
<div class="mb-3 col-md-8">
|
||||
{{ form.q(class="form-control w-100", placeholder=form.q.description) }}
|
||||
</div>
|
||||
<div class="mb-3 col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-search" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>{% trans %}User{% endtrans %}</b></td>
|
||||
<td><b>{% trans %}Website{% endtrans %}</b></td>
|
||||
<td class="d-none d-md-table-cell"><b>{% trans %}Affiliation{% endtrans %}</b></td>
|
||||
<td class="d-none d-md-table-cell"><b>{% trans %}Country{% endtrans %}</b></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users.items %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if scores_visible() %}
|
||||
<a href="{{ url_for('users.public', user_id=user.id) }}">
|
||||
{{ user.name | truncate(50) }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span>{{ user.name | truncate(50) }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if user.oauth_id %}
|
||||
<a href="https://majorleaguecyber.org/u/{{ user.name }}">
|
||||
<span class="badge bg-primary ms-2">{% trans %}Official{% endtrans %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="text-center" style="width: 10px;">
|
||||
{% if user.website and (user.website.startswith('http://') or user.website.startswith('https://')) %}
|
||||
<a href="{{ user.website }}" target="_blank" rel="noopener">
|
||||
<i
|
||||
class="fas fa-external-link-alt" data-toggle="tooltip" data-placement="top"
|
||||
title="{{ user.website }}"
|
||||
></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell">
|
||||
{% if user.affiliation %}
|
||||
{% if user.affiliation | length > 50 %}
|
||||
<span data-toggle="tooltip" data-placement="top" title="{{ user.affiliation }}">
|
||||
{% if user.affiliation %}{{ user.affiliation | truncate(50) }}{% endif %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span>
|
||||
{% if user.affiliation %}{{ user.affiliation | truncate(50) }}{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="d-none d-md-table-cell d-lg-table-cell">
|
||||
<span>
|
||||
{% if user.country %}
|
||||
<i class="flag-{{ user.country.lower() }}"></i>
|
||||
{{ lookup_country_code(user.country) }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if users.pages > 1 %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="text-center">
|
||||
{% trans %}Page{% endtrans %} <br>
|
||||
|
||||
{% if users.page != 1 %}
|
||||
<a href="{{ prev_page }}"><<<</a>
|
||||
{% endif %}
|
||||
|
||||
<select class="page-select">
|
||||
{% for page in range(1, users.pages + 1) %}
|
||||
<option {% if users.page == page %}selected{% endif %}>{{ page }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% if users.next_num %}
|
||||
<a href="{{ next_page }}">>>></a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ Assets.js("assets/js/users/list.js") }}
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user