aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pysite/mixins.py3
-rw-r--r--pysite/tables.py11
-rw-r--r--pysite/views/api/bot/user.py13
-rw-r--r--pysite/views/main/jams/join.py188
-rw-r--r--pysite/views/main/jams/profile.py59
-rw-r--r--pysite/views/main/jams/signup.py9
-rw-r--r--pysite/views/staff/jams/actions.py3
-rw-r--r--static/style.css11
-rw-r--r--templates/main/about/privacy.html10
-rw-r--r--templates/main/jams/already.html26
-rw-r--r--templates/main/jams/banned.html44
-rw-r--r--templates/main/jams/index.html2
-rw-r--r--templates/main/jams/join.html357
-rw-r--r--templates/main/jams/profile.html106
-rw-r--r--templates/main/jams/signup.html15
-rw-r--r--templates/main/jams/thanks.html25
-rw-r--r--templates/main/navigation.html10
-rw-r--r--templates/staff/jams/edit_basics.html1
-rw-r--r--templates/staff/jams/infractions/view.html10
19 files changed, 861 insertions, 42 deletions
diff --git a/pysite/mixins.py b/pysite/mixins.py
index a3edc4f2..6e5032ab 100644
--- a/pysite/mixins.py
+++ b/pysite/mixins.py
@@ -4,6 +4,7 @@ from flask import Blueprint
from rethinkdb.ast import Table
from pysite.database import RethinkDB
+from pysite.oauth import OauthBackend
class DBMixin:
@@ -98,5 +99,5 @@ class OauthMixin:
return self.oauth.user_data()
@property
- def oauth(self):
+ def oauth(self) -> OauthBackend:
return self._oauth()
diff --git a/pysite/tables.py b/pysite/tables.py
index c592333d..e99ca989 100644
--- a/pysite/tables.py
+++ b/pysite/tables.py
@@ -79,8 +79,9 @@ TABLES = {
primary_key="id",
keys=sorted([
"id", # uuid
+ "snowflake", # str
"jam", # int
- "answers", # dict {question, answer, metadata}
+ "answers", # list [{question, answer, metadata}]
"approved" # bool
])
),
@@ -100,16 +101,16 @@ TABLES = {
"id", # uuid
"participant", # str
"reason", # str
- "number" # int (optionally -1 for permanent)
+ "number", # int (optionally -1 for permanent)
+ "decremented_for" # list[int]
])
),
"code_jam_participants": Table( # Info for each participant
primary_key="id",
keys=sorted([
- "snowflake", # int
- "skill_level", # str
- "age", # str
+ "id", # str
+ "dob", # str
"github_username", # str
"timezone" # str
])
diff --git a/pysite/views/api/bot/user.py b/pysite/views/api/bot/user.py
index 12f5a2c7..a353ccfe 100644
--- a/pysite/views/api/bot/user.py
+++ b/pysite/views/api/bot/user.py
@@ -32,6 +32,7 @@ class UserView(APIView, DBMixin):
name = "bot.users"
table_name = "users"
oauth_table_name = "oauth_data"
+ participants_table = "code_jam_participants"
@api_key
@api_params(schema=SCHEMA, validation_type=ValidationTypes.json)
@@ -40,6 +41,8 @@ class UserView(APIView, DBMixin):
deletions = 0
oauth_deletions = 0
+ profile_deletions = 0
+
user_ids = [user["user_id"] for user in data]
all_users = self.db.run(self.db.query(self.table_name), coerce=list)
@@ -54,7 +57,9 @@ class UserView(APIView, DBMixin):
for item in all_oauth_data:
if item["snowflake"] not in user_ids:
self.db.delete(self.oauth_table_name, item["id"], durability="soft")
+ self.db.delete(self.participants_table, item["id"], durability="soft")
oauth_deletions += 1
+ profile_deletions += 1
del user_ids
@@ -68,6 +73,7 @@ class UserView(APIView, DBMixin):
changes["deleted"] = deletions
changes["deleted_oauth"] = oauth_deletions
+ changes["deleted_jam_profiles"] = profile_deletions
return jsonify(changes) # pragma: no cover
@@ -98,6 +104,13 @@ class UserView(APIView, DBMixin):
.delete()
).get("deleted", 0)
+ profile_deletions = self.db.run(
+ self.db.query(self.participants_table)
+ .get_all(*user_ids)
+ .delete()
+ )
+
changes["deleted_oauth"] = oauth_deletions
+ changes["deleted_jam_profiles"] = profile_deletions
return jsonify(changes) # pragma: no cover
diff --git a/pysite/views/main/jams/join.py b/pysite/views/main/jams/join.py
new file mode 100644
index 00000000..83013a01
--- /dev/null
+++ b/pysite/views/main/jams/join.py
@@ -0,0 +1,188 @@
+from email.utils import parseaddr
+
+from flask import redirect, request, url_for
+from werkzeug.exceptions import BadRequest, NotFound
+
+from pysite.base_route import RouteView
+from pysite.decorators import csrf
+from pysite.mixins import DBMixin, OauthMixin
+
+
+class JamsJoinView(RouteView, DBMixin, OauthMixin):
+ path = "/jams/join/<int:jam>"
+ name = "jams.join"
+
+ table_name = "code_jams"
+ forms_table = "code_jam_forms"
+ questions_table = "code_jam_questions"
+ responses_table = "code_jam_responses"
+ participants_table = "code_jam_participants"
+ infractions_table = "code_jam_infractions"
+
+ def get(self, jam):
+ jam_obj = self.db.get(self.table_name, jam)
+
+ if not jam_obj:
+ return NotFound()
+
+ if not self.user_data:
+ return redirect(url_for("discord.login"))
+
+ infractions = self.get_infractions(self.user_data["user_id"])
+
+ for infraction in infractions:
+ if infraction["number"] == -1: # Indefinite ban
+ return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj)
+
+ if infraction["number"]: # Got some jams left
+ if jam not in infraction["decremented_for"]:
+ # Make sure they haven't already tried to apply for this jam
+ infraction["number"] -= 1
+ infraction["decremented_for"].append(jam)
+
+ self.db.insert(self.infractions_table, infraction, conflict="replace")
+
+ return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj)
+
+ if jam in infraction["decremented_for"]:
+ # They already tried to apply for this jam
+ return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj)
+
+ participant = self.db.get(self.participants_table, self.user_data["user_id"])
+
+ if not participant:
+ return redirect(url_for("info.jams.profile"))
+
+ if self.get_response(jam, self.user_data["user_id"]):
+ return self.render("main/jams/already.html", jam=jam_obj)
+
+ form_obj = self.db.get(self.forms_table, jam)
+ questions = []
+
+ if form_obj:
+ for question in form_obj["questions"]:
+ questions.append(self.db.get(self.questions_table, question))
+
+ return self.render(
+ "main/jams/join.html", jam=jam_obj, form=form_obj,
+ questions=questions, question_ids=[q["id"] for q in questions]
+ )
+
+ @csrf
+ def post(self, jam):
+ jam_obj = self.db.get(self.table_name, jam)
+
+ if not jam_obj:
+ return NotFound()
+
+ if not self.user_data:
+ return redirect(url_for("discord.login"))
+
+ infractions = self.get_infractions(self.user_data["user_id"])
+
+ for infraction in infractions:
+ if infraction["number"] == -1: # Indefinite ban
+ return self.render("main/jams/banned.html", infraction=infraction)
+
+ if infraction["number"]: # Got some jams left
+ if jam not in infraction["decremented_for"]:
+ # Make sure they haven't already tried to apply for this jam
+ infraction["number"] -= 1
+ infraction["decremented_for"].append(jam)
+
+ self.db.insert(self.infractions_table, infraction, conflict="replace")
+
+ return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj)
+
+ if jam in infraction["decremented_for"]:
+ # They already tried to apply for this jam
+ return self.render("main/jams/banned.html", infraction=infraction, jam=jam_obj)
+
+ participant = self.db.get(self.participants_table, self.user_data["user_id"])
+
+ if not participant:
+ return redirect(url_for("info.jams.profile"))
+
+ if self.get_response(jam, self.user_data["user_id"]):
+ return self.render("main/jams/already.html", jam=jam_obj)
+
+ form_obj = self.db.get(self.forms_table, jam)
+
+ if not form_obj:
+ return NotFound()
+
+ questions = []
+
+ for question in form_obj["questions"]:
+ questions.append(self.db.get(self.questions_table, question))
+
+ answers = []
+
+ for question in questions:
+ value = request.form.get(question["id"])
+ answer = {"question": question["id"]}
+
+ if not question["optional"] and value is None:
+ return BadRequest()
+
+ if question["type"] == "checkbox":
+ if value == "on":
+ answer["value"] = True
+ elif not question["optional"]:
+ return BadRequest()
+ else:
+ answer["value"] = False
+
+ elif question["type"] == "email":
+ if value:
+ address = parseaddr(value)
+
+ if address == ("", ""):
+ return BadRequest()
+
+ answer["value"] = value
+
+ elif question["type"] in ["number", "range", "slider"]:
+ if value is not None:
+ value = int(value)
+
+ if value > int(question["data"]["max"]) or value < int(question["data"]["min"]):
+ return BadRequest()
+
+ answer["value"] = value
+
+ elif question["type"] == "radio":
+ if value:
+ if value not in question["data"]["options"]:
+ return BadRequest()
+
+ answer["value"] = value
+
+ elif question["type"] in ["text", "textarea"]:
+ answer["value"] = value
+
+ answers.append(answer)
+
+ user_id = self.user_data["user_id"]
+
+ response = {
+ "snowflake": user_id,
+ "jam": jam,
+ "approved": False,
+ "answers": answers
+ }
+
+ self.db.insert(self.responses_table, response)
+ return self.render("main/jams/thanks.html", jam=jam_obj)
+
+ def get_response(self, jam, user_id):
+ query = self.db.query(self.responses_table).filter({"jam": jam, "snowflake": user_id})
+ result = self.db.run(query, coerce=list)
+
+ if result:
+ return result[0]
+ return None
+
+ def get_infractions(self, user_id):
+ query = self.db.query(self.infractions_table).filter({"participant": user_id})
+ return self.db.run(query, coerce=list)
diff --git a/pysite/views/main/jams/profile.py b/pysite/views/main/jams/profile.py
new file mode 100644
index 00000000..407f842e
--- /dev/null
+++ b/pysite/views/main/jams/profile.py
@@ -0,0 +1,59 @@
+import datetime
+
+from flask import redirect, request, url_for
+from werkzeug.exceptions import BadRequest
+
+from pysite.base_route import RouteView
+from pysite.decorators import csrf
+from pysite.mixins import DBMixin, OauthMixin
+
+
+class JamsProfileView(RouteView, DBMixin, OauthMixin):
+ path = "/jams/profile"
+ name = "jams.profile"
+
+ table_name = "code_jam_participants"
+
+ def get(self):
+ if not self.user_data:
+ return redirect(url_for("discord.login"))
+
+ participant = self.db.get(self.table_name, self.user_data["user_id"])
+
+ if not participant:
+ participant = {"id": self.user_data["user_id"]}
+
+ return self.render(
+ "main/jams/profile.html", participant=participant
+ )
+
+ @csrf
+ def post(self):
+ if not self.user_data:
+ return redirect(url_for("discord.login"))
+
+ participant = self.db.get(self.table_name, self.user_data["user_id"])
+
+ if not participant:
+ participant = {"id": self.user_data["user_id"]}
+
+ dob = request.form.get("dob")
+ github_username = request.form.get("github_username")
+ timezone = request.form.get("timezone")
+
+ if not dob or not github_username or not timezone:
+ return BadRequest()
+
+ # Convert given datetime strings into actual objects, adding timezones to keep rethinkdb happy
+ dob = datetime.datetime.strptime(dob, "%Y-%m-%d")
+ dob = dob.replace(tzinfo=datetime.timezone.utc)
+
+ participant["dob"] = dob
+ participant["github_username"] = github_username
+ participant["timezone"] = timezone
+
+ self.db.insert(self.table_name, participant, conflict="replace")
+
+ return self.render(
+ "main/jams/profile.html", participant=participant, done=True
+ )
diff --git a/pysite/views/main/jams/signup.py b/pysite/views/main/jams/signup.py
deleted file mode 100644
index 632da6d6..00000000
--- a/pysite/views/main/jams/signup.py
+++ /dev/null
@@ -1,9 +0,0 @@
-from pysite.base_route import RouteView
-
-
-class JamsSignupView(RouteView):
- path = "/jams/signup"
- name = "jams.signup"
-
- def get(self):
- return self.render("main/jams/signup.html")
diff --git a/pysite/views/staff/jams/actions.py b/pysite/views/staff/jams/actions.py
index f08b3635..1af215a5 100644
--- a/pysite/views/staff/jams/actions.py
+++ b/pysite/views/staff/jams/actions.py
@@ -183,7 +183,8 @@ class ActionView(APIView, DBMixin):
result = self.db.insert(self.infractions_table, {
"participant": participant,
"reason": reason,
- "number": number
+ "number": number,
+ "decremented_for": []
})
return jsonify({"id": result["generated_keys"][0]})
diff --git a/static/style.css b/static/style.css
index 2b9b329c..1f3f9f58 100644
--- a/static/style.css
+++ b/static/style.css
@@ -179,4 +179,15 @@ select {
left: auto !important;
-webkit-appearance: unset !important;
opacity: 1 !important;
+}
+
+div.danger-input * {
+ /*border-radius: 5px;*/
+ /*padding: 0.8rem;*/
+ /*min-height: 3.5rem;*/
+ color: red;
+ border-color: red !important;
+
+ transition: color 0.5s ease,
+ border-color 0.5s ease;
} \ No newline at end of file
diff --git a/templates/main/about/privacy.html b/templates/main/about/privacy.html
index 1a3260a1..870b75a8 100644
--- a/templates/main/about/privacy.html
+++ b/templates/main/about/privacy.html
@@ -53,6 +53,16 @@
<li>An access token and refresh token</li>
</ul>
+ <p>
+ Should you set up your code jam profile, we additionally collect...
+ </p>
+
+ <ul>
+ <li>Your date of birth</li>
+ <li>Your GitHub username</li>
+ <li>Your timezone</li>
+ </ul>
+
<h1 class="uk-article-title hover-title" id="usage">
How We Use Your Data
diff --git a/templates/main/jams/already.html b/templates/main/jams/already.html
new file mode 100644
index 00000000..0baaf4a9
--- /dev/null
+++ b/templates/main/jams/already.html
@@ -0,0 +1,26 @@
+{% extends "main/base.html" %}
+{% block title %}Code Jams | Banned{% endblock %}
+{% block og_title %}Code Jams | Banned{% endblock %}
+
+{% block content %}
+<div class="uk-section">
+ <div class="uk-container uk-container-small">
+ <h1 class="uk-header uk-article-title">
+ Code Jam {{ jam.number }}: {{ jam.title }}
+ </h1>
+ <p class="uk-article-meta">
+ Bring the thunder!
+ </p>
+
+ <p class="uk-alert uk-alert-danger">
+ Thanks for your interest in this code jam! It looks like we already have an application here for you,
+ so please just sit back, relax, and we'll let you know whether you've been selected for this code
+ jam when the time comes.
+ </p>
+
+ <a class="uk-button uk-button-secondary uk-align-center" href="{{ url_for("main.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back to all code jams
+ </a>
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/main/jams/banned.html b/templates/main/jams/banned.html
new file mode 100644
index 00000000..fa47c1ec
--- /dev/null
+++ b/templates/main/jams/banned.html
@@ -0,0 +1,44 @@
+{% extends "main/base.html" %}
+{% block title %}Code Jams | Banned{% endblock %}
+{% block og_title %}Code Jams | Banned{% endblock %}
+
+{% block content %}
+<div class="uk-section">
+ <div class="uk-container uk-container-small">
+ <h1 class="uk-header uk-article-title">
+ Code Jam {{ jam.number }}: {{ jam.title }}
+ </h1>
+ <p class="uk-article-meta">
+ Bring the thunder!
+ </p>
+
+ {% if infraction.number == -1 %}
+ <p class="uk-alert uk-alert-danger">
+ Thanks for your interest in this code jam! Unfortunately, due to your previous actions, you have been
+ permanently banned from participating in our code jams.
+ <br />
+ <br />
+ The reason given is: <strong>{{ infraction.reason }}</strong>
+ <br />
+ <br />
+ If you feel that this is a mistake, please feel free to contact one of the admins on Discord.
+ </p>
+ {% else %}
+ <p class="uk-alert uk-alert-danger">
+ Thanks for your interest in this code jam! Unfortunately, due to your previous actions, you have been
+ temporarily banned from participating in our code jams.
+ <br />
+ <br />
+ The reason given is: <strong>{{ infraction.reason }}</strong>
+ <br />
+ <br />
+ If you feel that this is a mistake, please feel free to contact one of the admins on Discord.
+ </p>
+ {% endif %}
+
+ <a class="uk-button uk-button-secondary uk-align-center" href="{{ url_for("main.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back to all code jams
+ </a>
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/main/jams/index.html b/templates/main/jams/index.html
index a5d2a5d0..71bce999 100644
--- a/templates/main/jams/index.html
+++ b/templates/main/jams/index.html
@@ -61,7 +61,7 @@
Code Jam {{ jam.number }}: {{ jam.title }}
<span class="uk-float-right">
{% if jam.state == "announced" %}
- <a class="uk-button uk-button-primary" target="_blank" href="{{ jam.repo }}">
+ <a class="uk-button uk-button-primary" href="{{ url_for("main.jams.join", jam=jam.number) }}">
<i class="uk-icon fa-fw far fa-plus"></i> &nbsp;Join
</a>
{% else %}
diff --git a/templates/main/jams/join.html b/templates/main/jams/join.html
new file mode 100644
index 00000000..ffa80cb5
--- /dev/null
+++ b/templates/main/jams/join.html
@@ -0,0 +1,357 @@
+{% extends "main/base.html" %}
+{% block title %}Code Jams | Join{% endblock %}
+{% block og_title %}Code Jams | Join{% endblock %}
+
+{% macro show_question(question) %}
+ <div id="div-{{ question.id }}">
+ <div class="uk-form-label">
+ {% if question.optional %}
+ <label class="uk-form-label" style="margin-top: 0" for="{{ question.id }}">
+ <strong>{{ question.title }}</strong>
+ <br />
+ <span class="uk-text-meta">You may skip this question</span>
+ </label>
+ {% else %}
+ <label class="uk-form-label" style="margin-top: 0" for="{{ question.id }}">
+ <strong>{{ question.title }}</strong>
+ <br />
+ <span class="uk-text-meta">This question is required</span>
+ </label>
+ {% endif %}
+ </div>
+ <div class="uk-form-controls uk-form-controls-text">
+ {% if question.type == "checkbox" %}
+ {% if question.optional %}
+ <input class="uk-checkbox" type="checkbox" name="{{ question.id }}" id="{{ question.id }}">
+ <label for="{{ question.id }}" style="padding-left: 0.7rem;">Confirm</label>
+ {% else %}
+ <input class="uk-checkbox" type="checkbox" name="{{ question.id }}" id="{{ question.id }}" required>
+ <label for="{{ question.id }}" style="padding-left: 0.7rem;">Confirm</label>
+ {% endif %}
+
+ {% elif question.type == "email" %}
+ {% if question.optional %}
+ <input class="uk-input" type="email" name="{{ question.id }}" id="{{ question.id }}" placeholder="[email protected]">
+ {% else %}
+ <input class="uk-input" type="email" name="{{ question.id }}" id="{{ question.id }}" placeholder="[email protected]" required>
+ {% endif %}
+
+ {% elif question.type == "number" %}
+ {% if question.optional %}
+ <input class="uk-input" type="number" max="{{ question.data.max }}" min="{{ question.data.min }}" name="{{ question.id }}" id="{{ question.id }}" value="{{ question.data.min }}">
+ {% else %}
+ <input class="uk-input" type="number" max="{{ question.data.max }}" min="{{ question.data.min }}" name="{{ question.id }}" id="{{ question.id }}" value="{{ question.data.min }}" required>
+ {% endif %}
+
+ {% elif question.type == "radio" %}
+ {% if question.optional %}
+ {% for option in question.data.options %}
+ <input class="uk-radio radio-{{ question.id }}" type="radio" name="{{ question.id }}" id="{{ question.id }}-{{ option }}" value="{{ option }}">
+ <label style="padding-left: 0.7rem; padding-right: 1rem;" for="{{ question.id }}-{{ option }}">{{ option }}</label>
+ {% endfor %}
+ {% else %}
+ {% for option in question.data.options %}
+ <input class="uk-radio radio-{{ question.id }}" type="radio" name="{{ question.id }}" id="{{ question.id }}-{{ option }}" value="{{ option }}" required>
+ <label style="padding-left: 0.7rem; padding-right: 1rem;" for="{{ question.id }}-{{ option }}">{{ option }}</label>
+ {% endfor %}
+ {% endif %}
+
+ {% elif question.type == "range" %}
+ <div class="uk-flex uk-flex-between">
+ {% if question.optional %}
+ {% for num in range(question.data.min, question.data.max + 1) %}
+ <span>
+ <input class="uk-radio radio-{{ question.id }}" type="radio" name="{{ question.id }}" id="{{ question.id }}-{{ num }}" value="{{ num }}">
+ <label style="padding-left: 0.3rem;" for="{{ question.id }}-{{ num }}">{{ num }}</label>
+ </span>
+ {% endfor %}
+ {% else %}
+ {% for num in range(question.data.min, question.data.max + 1) %}
+ <span>
+ <input class="uk-radio radio-{{ question.id }}" type="radio" name="{{ question.id }}" id="{{ question.id }}-{{ num }}" value="{{ num }}" required>
+ <label style="padding-left: 0.3rem;" for="{{ question.id }}-{{ num }}">{{ num }}</label>
+ </span>
+ {% endfor %}
+ {% endif %}
+ </div>
+
+ {% elif question.type == "text" %}
+ {% if question.optional %}
+ <input class="uk-input" type="text" name="{{ question.id }}" id="{{ question.id }}">
+ {% else %}
+ <input class="uk-input" type="text" name="{{ question.id }}" id="{{ question.id }}" required>
+ {% endif %}
+
+ {% elif question.type == "textarea" %}
+ {% if question.optional %}
+ <textarea class="uk-input uk-textarea" name="{{ question.id }}" id="{{ question.id }}" style="resize: vertical; min-height: 15rem; font-family: monospace;"></textarea>
+ {% else %}
+ <textarea class="uk-input uk-textarea" name="{{ question.id }}" id="{{ question.id }}" style="resize: vertical; min-height: 15rem; font-family: monospace;" required></textarea>
+ {% endif %}
+
+ {% elif question.type == "slider" %}
+ <div class="uk-flex uk-flex-between">
+ <label class="uk-label" style="margin-right: 1rem;" for="{{ question.id }}" id="{{ question.id }}-slider-value">{{ question.data.min }}</label>
+ <input class="uk-range range-slider" name="{{ question.id }}" id="{{ question.id }}" min="{{ question.data.min }}" max="{{ question.data.max }}" step="1" value="{{ question.data.min }}" type="range">
+ </div>
+
+ {% endif %}
+ </div>
+ </div>
+{% endmacro %}
+
+{% block content %}
+<div class="uk-section">
+ <div class="uk-container uk-container-small">
+ <h1 class="uk-header uk-article-title">
+ Code Jam {{ jam.number }}: {{ jam.title }}
+ </h1>
+ <p class="uk-article-meta">
+ Bring the thunder!
+ </p>
+ <p>
+ Please fill out the form below to apply for this code jam. Once you've submitted your application and the
+ application window has closed, we'll review it and let you know whether you've been entered!
+ </p>
+ <p>
+ Please note that you will not be able to edit your application after you've submitted it.
+ </p>
+ <hr class="uk-divider-icon" />
+
+ {% if jam.state != "announced" %}
+ <p class="uk-alert uk-alert-primary">
+ Unfortunately, we're not accepting applications for this code jam right now - but we appreciate your
+ interest. Keep an eye on <code>#announcements</code> on Discord for information on the next jam!
+ </p>
+ {% else %}
+ <form action="{{ url_for("main.jams.join", jam=jam.number) }}" method="post" class="uk-form-horizontal">
+ {% for question in questions %}
+ {{ show_question(question) }}
+ <br />
+ {% endfor %}
+ <br />
+
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+
+ <div class="uk-text-center">
+ <a class="uk-button uk-button-default" href="{{ url_for("main.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back
+ </a>
+ <button type="submit" class="uk-button uk-button-primary" name="submit" id="submit" disabled>
+ <i class="uk-icon fa-fw far fa-check"></i> &nbsp;Apply
+ </button>
+ </div>
+ </form>
+
+ {% endif %}
+ </div>
+</div>
+
+<script type="application/javascript">
+ "use strict";
+
+ // noinspection JSAnnotator (It thinks I'm not assigning this for some reason)
+ const questions = {{ questions | tojson }};
+ const button = document.getElementById("submit");
+
+ function validateEmail(email) {
+ let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+ return re.test(String(email).toLowerCase());
+ }
+
+ function isNum(value) {
+ return !isNaN(parseInt(value));
+ }
+
+ function checkInputs() {
+ let input, inputs, div;
+ let disabled = false;
+
+ for (let question of questions) {
+ div = document.getElementById("div-" + question.id);
+
+ switch (question.type) {
+ case "checkbox":
+ if (!question.optional) {
+ let input = document.getElementById(question.id);
+
+ if (!input.checked) {
+ disabled = true;
+ div.classList.add("danger-input");
+ } else {
+ div.classList.remove("danger-input");
+ }
+ }
+ break;
+ case "email":
+ input = document.getElementById(question.id);
+
+ if (!question.optional || input.value.length > 0) {
+ if (input.value.length < 5 || !validateEmail(input.value)) {
+ disabled = true;
+ div.classList.add("danger-input");
+ } else {
+ div.classList.remove("danger-input");
+ }
+ } else {
+ div.classList.remove("danger-input");
+ }
+ break;
+ case "number":
+ input = document.getElementById(question.id);
+
+ if (!question.optional || input.value.length > 0) {
+ if (input.value.length < 1 || !isNum(input.value)) {
+ disabled = true;
+ div.classList.add("danger-input");
+ } else {
+ let val = parseInt(input.value);
+
+ if (val < question.data.min || val > question.data.max) {
+ disabled = true;
+ div.classList.add("danger-input");
+ } else {
+ div.classList.remove("danger-input");
+ }
+ }
+ }
+ break;
+ case "radio":
+ if (! question.optional) {
+ inputs = document.getElementsByClassName("radio-" + question.id);
+ let selected = null;
+
+ for (let inner of inputs) {
+ if (inner.checked) {
+ selected = inner;
+ }
+ }
+
+ if (selected === null) {
+ disabled = true;
+ div.classList.add("danger-input");
+ } else {
+ div.classList.remove("danger-input");
+ }
+ }
+
+ break;
+ case "range":
+ if (! question.optional) {
+ inputs = document.getElementsByClassName("radio-" + question.id);
+ let selected = null;
+
+ for (let inner of inputs) {
+ if (inner.checked) {
+ selected = inner;
+ }
+ }
+
+ if (selected === null) {
+ disabled = true;
+
+ div.classList.add("danger-input");
+ } else {
+ div.classList.remove("danger-input");
+ }
+ }
+
+ break;
+ case "text":
+ if (!question.optional) {
+ input = document.getElementById(question.id);
+
+ if (input.value.length < 1) {
+ disabled = true;
+ div.classList.add("danger-input");
+ } else {
+ div.classList.remove("danger-input");
+ }
+ }
+
+ break;
+ case "textarea":
+ if (!question.optional) {
+ input = document.getElementById(question.id);
+
+ if (input.value.length < 1) {
+ disabled = true;
+ div.classList.add("danger-input");
+ } else {
+ div.classList.remove("danger-input");
+ }
+ }
+
+ break;
+ case "slider":
+ break;
+ }
+ }
+
+ button.disabled = disabled;
+ }
+
+ function setUpChecks() {
+ let input, inputs, label;
+
+ for (let question of questions) {
+ switch (question.type) {
+ case "checkbox":
+ input = document.getElementById(question.id);
+ input.onchange = checkInputs;
+
+ break;
+ case "email":
+ input = document.getElementById(question.id);
+ input.oninput = checkInputs;
+
+ break;
+ case "number":
+ input = document.getElementById(question.id);
+ input.oninput = checkInputs;
+ input.onchange = checkInputs;
+
+ break;
+ case "radio":
+ inputs = document.getElementsByClassName("radio-" + question.id);
+
+ for (let inner of inputs) {
+ inner.onchange = checkInputs;
+ }
+
+ break;
+ case "range":
+ inputs = document.getElementsByClassName("radio-" + question.id);
+
+ for (let inner of inputs) {
+ inner.onchange = checkInputs;
+ }
+
+ break;
+ case "text":
+ input = document.getElementById(question.id);
+ input.oninput = checkInputs;
+
+ break;
+ case "textarea":
+ input = document.getElementById(question.id);
+ input.oninput = checkInputs;
+
+ break;
+ case "slider":
+ input = document.getElementById(question.id);
+ label = document.getElementById(question.id + "-slider-value");
+
+ input.oninput = function() {
+ label.textContent = this.value;
+ checkInputs();
+ };
+ break;
+ }
+ }
+ }
+
+ setUpChecks();
+ checkInputs();
+</script>
+{% endblock %}
diff --git a/templates/main/jams/profile.html b/templates/main/jams/profile.html
new file mode 100644
index 00000000..6cf315d0
--- /dev/null
+++ b/templates/main/jams/profile.html
@@ -0,0 +1,106 @@
+{% extends "main/base.html" %}
+{% block title %}Code Jams | Banned{% endblock %}
+{% block og_title %}Code Jams | Banned{% endblock %}
+
+{% block extra_head %}
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.1/moment.min.js" type="application/javascript"></script>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/moment-timezone/0.5.17/moment-timezone.min.js" type="application/javascript"></script>
+{% endblock %}
+
+{% block content %}
+<div class="uk-section">
+ <div class="uk-container uk-container-small">
+ <h1 class="uk-header uk-article-title">
+ Code Jams: My Profile
+ </h1>
+
+ {% if done %}
+ <p class="uk-alert uk-alert-success">
+ Thanks - your data has been saved!
+ </p>
+ {% else %}
+ <p class="uk-alert uk-alert-primary">
+ Please make sure you've filled this out correctly, as we do use this data when evaluating your code jam
+ application.
+ <br />
+ <br />
+ You may come back here and edit your data at any time.
+ </p>
+ {% endif %}
+
+ <form class="uk-form-horizontal" action="{{ url_for("main.jams.profile") }}" method="post">
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="dob">Date of Birth</label>
+ </div>
+ <div class="uk-form-controls-text uk-form-controls">
+ <input class="uk-input" type="text" name="dob" id="dob" value="{{ participant.dob }}" required>
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="github_username">GitHub Username</label>
+ </div>
+ <div class="uk-form-controls-text uk-form-controls">
+ <input class="uk-input" type="text" name="github_username" id="github_username" value="{{ participant.github_username }}" required>
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="timezone">Timezone</label>
+ </div>
+ <div class="uk-form-controls-text uk-form-controls">
+ <input class="uk-input" type="text" name="timezone" id="timezone" value="{{ participant.timezone }}" required>
+ </div>
+ </div>
+ <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
+ <br />
+
+ <div class="uk-text-center">
+ <a class="uk-button uk-button-default" href="{{ url_for("main.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back
+ </a>
+ <button type="submit" class="uk-button uk-button-primary" id="submit">
+ <i class="uk-icon fa-fw far fa-check"></i> &nbsp;Save
+ </button>
+ </div>
+ </form>
+ </div>
+</div>
+
+
+
+<script type="application/javascript">
+ const date = flatpickr("#dob", {enableTime: false, altInput: true});
+ const tz = moment().format("Z");
+
+ const dob_input = document.getElementById("dob");
+ const github_input = document.getElementById("github_username");
+ const tz_input = document.getElementById("timezone");
+
+ const submit_button = document.getElementById("submit");
+
+ function checkInputs() {
+ if (dob_input.value.length < 1)
+ return submit_button.disabled = true;
+
+ if (github_input.value.length < 1)
+ return submit_button.disabled = true;
+
+ if (tz_input.value.length < 1)
+ return submit_button.disabled = true;
+
+ submit_button.disabled = false;
+ }
+
+ dob_input.oninput = checkInputs;
+ github_input.oninput = checkInputs;
+ tz_input.oninput = checkInputs;
+
+ if (tz_input.value.length < 1) {
+ document.getElementById("timezone").value = "UTC" + tz;
+ }
+
+ checkInputs();
+</script>
+{% endblock %}
diff --git a/templates/main/jams/signup.html b/templates/main/jams/signup.html
deleted file mode 100644
index ddb48733..00000000
--- a/templates/main/jams/signup.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% extends "main/base.html" %}
-{% block title %}Home{% endblock %}
-{% block og_title %}Home{% endblock %}
-{% block content %}
-<div class="uk-section">
- <div class="uk-container uk-container-small uk-text-center">
- <h1 class="uk-header">
- Sign Up
- </h1>
- <p class="uk-article-meta">
- # TODO
- </p>
- </div>
-</div>
-{% endblock %}
diff --git a/templates/main/jams/thanks.html b/templates/main/jams/thanks.html
new file mode 100644
index 00000000..e6709485
--- /dev/null
+++ b/templates/main/jams/thanks.html
@@ -0,0 +1,25 @@
+{% extends "main/base.html" %}
+{% block title %}Code Jams | Banned{% endblock %}
+{% block og_title %}Code Jams | Banned{% endblock %}
+
+{% block content %}
+<div class="uk-section">
+ <div class="uk-container uk-container-small">
+ <h1 class="uk-header uk-article-title">
+ Code Jam {{ jam.number }}: {{ jam.title }}
+ </h1>
+ <p class="uk-article-meta">
+ Bring the thunder!
+ </p>
+
+ <p class="uk-alert uk-alert-success">
+ Thanks for your application! Just sit back, relax, and we'll let you know whether you've been selected
+ for this code jam when the time comes.
+ </p>
+
+ <a class="uk-button uk-button-secondary uk-align-center" href="{{ url_for("main.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back to all code jams
+ </a>
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/main/navigation.html b/templates/main/navigation.html
index 391bbcf5..3130747b 100644
--- a/templates/main/navigation.html
+++ b/templates/main/navigation.html
@@ -102,15 +102,15 @@
{% endif %}
{% if current_page == "main.jams.index" %}
- <li class="uk-active"><a href="{{ url_for('main.jams.index') }}">Info</a></li>
+ <li class="uk-active"><a href="{{ url_for('main.jams.index') }}">All Jams</a></li>
{% else %}
- <li><a href="{{ url_for('main.jams.index') }}">Info</a></li>
+ <li><a href="{{ url_for('main.jams.index') }}">All Jams</a></li>
{% endif %}
- {% if current_page == "main.jams.signup" %}
- <li class="uk-active"><a href="{{ url_for('main.jams.signup') }}">Sign Up</a></li>
+ {% if current_page == "main.jams.profile" %}
+ <li class="uk-active"><a href="{{ url_for('main.jams.profile') }}">My Profile</a></li>
{% else %}
- <li><a href="{{ url_for('main.jams.signup') }}">Sign Up</a></li>
+ <li><a href="{{ url_for('main.jams.profile') }}">My Profile</a></li>
{% endif %}
<li class="uk-nav-divider"></li>
diff --git a/templates/staff/jams/edit_basics.html b/templates/staff/jams/edit_basics.html
index 59d69b77..e9bc69e1 100644
--- a/templates/staff/jams/edit_basics.html
+++ b/templates/staff/jams/edit_basics.html
@@ -66,7 +66,6 @@
</button>
</div>
</form>
-
</div>
<script type="application/javascript">
diff --git a/templates/staff/jams/infractions/view.html b/templates/staff/jams/infractions/view.html
index 0b515cdb..cace2369 100644
--- a/templates/staff/jams/infractions/view.html
+++ b/templates/staff/jams/infractions/view.html
@@ -51,9 +51,11 @@
<td class="uk-text-truncate" title="{{ infraction.id }}">{{ infraction.id }}</td>
<td class="uk-table-shrink">
{% if infraction.participant is not string %}
- {{ infraction.participant.username }}#{{ infraction.participant.discriminator }} ({{ infraction.participant.id }})
+ <code>{{ infraction.participant.user_id }}</code>
+ <br />
+ ({{ infraction.participant.username }}#{{ infraction.participant.discriminator }})
{% else %}
- {{ infraction.participant }}
+ <code>{{ infraction.participant.user_id }}</code>
{% endif %}
</td>
<td title="{{ infraction.reason }}">{{ infraction.reason }}</td>
@@ -314,10 +316,10 @@
}
const row = `
<td class="uk-table-shrink">
- <button class="uk-button-small uk-button uk-button-danger delete-infraction-button" style="padding-left: 5px; padding-right: 5px;" data-question-id="${infraction.id}"><i class="uk-icon fa-fw far fa-times"></i></button>
+ <button class="uk-button-small uk-button uk-button-danger delete-infraction-button" style="padding-left: 5px; padding-right: 5px;" data-infraction-id="${infraction.id}"><i class="uk-icon fa-fw far fa-trash"></i></button>
</td>
<td class="uk-text-truncate" title="${infraction.id}">${infraction.id}</td>
- <td class="uk-table-shrink">${participant}</td>
+ <td class="uk-table-shrink"><code>${participant}</code></td>
<td title="${infraction.reason}">${infraction.reason}</td>
<td class="uk-table-shrink">${infraction.number}</td>
`;