aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Gareth Coles <[email protected]>2018-05-18 16:00:31 +0100
committerGravatar Gareth Coles <[email protected]>2018-05-18 16:00:31 +0100
commite1846928439aa2a7e660d870a083872c415c274d (patch)
treee716f3466ca3914f80b2ca102d5d345658af7bc8
parentUpdate wiki footer in line with main site (diff)
[Jams] Huge amount of work on code jam admin area
-rw-r--r--pysite/base_route.py13
-rw-r--r--pysite/constants.py8
-rw-r--r--pysite/rst/__init__.py99
-rw-r--r--pysite/tables.py86
-rw-r--r--pysite/views/staff/index.py4
-rw-r--r--pysite/views/staff/jams/actions.py36
-rw-r--r--pysite/views/staff/jams/create.py49
-rw-r--r--pysite/views/staff/jams/edit_basics.py58
-rw-r--r--pysite/views/staff/jams/edit_ending.py54
-rw-r--r--pysite/views/staff/jams/edit_info.py54
-rw-r--r--pysite/views/staff/jams/index.py15
-rw-r--r--pysite/views/staff/render.py63
-rw-r--r--pysite/views/ws/bot.py59
-rw-r--r--static/js/fouc.js2
-rw-r--r--static/js/jams.js92
-rw-r--r--templates/main/base.html3
-rw-r--r--templates/staff/index.html (renamed from templates/staff/staff.html)5
-rw-r--r--templates/staff/jams/create.html76
-rw-r--r--templates/staff/jams/edit_basics.html76
-rw-r--r--templates/staff/jams/edit_ending.html150
-rw-r--r--templates/staff/jams/edit_info.html177
-rw-r--r--templates/staff/jams/index.html251
-rw-r--r--templates/staff/tables/index.html2
23 files changed, 1370 insertions, 62 deletions
diff --git a/pysite/base_route.py b/pysite/base_route.py
index 8449f228..fcd5f4b4 100644
--- a/pysite/base_route.py
+++ b/pysite/base_route.py
@@ -53,6 +53,7 @@ class BaseView(MethodView, OauthMixin):
context["logged_in"] = self.logged_in
context["static_file"] = self._static_file
context["debug"] = DEBUG_MODE
+ context["format_datetime"] = lambda dt: dt.strftime("%b %d %Y, %H:%M:%S")
return render_template(template_names, **context)
@@ -128,25 +129,25 @@ class APIView(RouteView):
data = {
"error_code": error_code.value,
- "error_message": "Unknown error"
+ "error_message": error_info or "Unknown error"
}
http_code = 200
if error_code is ErrorCodes.unknown_route:
- data["error_message"] = "Unknown API route"
+ data["error_message"] = error_info or "Unknown API route"
http_code = 404
elif error_code is ErrorCodes.unauthorized:
- data["error_message"] = "Unauthorized"
+ data["error_message"] = error_info or "Unauthorized"
http_code = 401
elif error_code is ErrorCodes.invalid_api_key:
- data["error_message"] = "Invalid API-key"
+ data["error_message"] = error_info or "Invalid API-key"
http_code = 401
elif error_code is ErrorCodes.bad_data_format:
- data["error_message"] = "Input data in incorrect format"
+ data["error_message"] = error_info or "Input data in incorrect format"
http_code = 400
elif error_code is ErrorCodes.incorrect_parameters:
- data["error_message"] = "Incorrect parameters provided"
+ data["error_message"] = error_info or "Incorrect parameters provided"
http_code = 400
response = jsonify(data)
diff --git a/pysite/constants.py b/pysite/constants.py
index cb8dad62..c93a09d1 100644
--- a/pysite/constants.py
+++ b/pysite/constants.py
@@ -67,6 +67,14 @@ ERROR_DESCRIPTIONS = {
429: "Please don't send us that many requests."
}
+JAM_STATES = [
+ "planning",
+ "announced",
+ "running",
+ "judging",
+ "finished"
+]
+
# PaperTrail logging
PAPERTRAIL_ADDRESS = environ.get("PAPERTRAIL_ADDRESS") or None
PAPERTRAIL_PORT = int(environ.get("PAPERTRAIL_PORT") or 0)
diff --git a/pysite/rst/__init__.py b/pysite/rst/__init__.py
index 97a77f40..744dd269 100644
--- a/pysite/rst/__init__.py
+++ b/pysite/rst/__init__.py
@@ -13,8 +13,10 @@ CONTENTS_REGEX = re.compile(r"""<div class=\"contents topic\" id=\"contents\">(.
HREF_REGEX = re.compile(r"""<a class=\"reference internal\" href=\"(.*?)\".*?>(.*?)</a>""")
-def render(rst: str):
- rst = RST_TEMPLATE.format(rst)
+def render(rst: str, link_headers=True):
+ if link_headers:
+ rst = RST_TEMPLATE.format(rst)
+
html = publish_parts(
source=rst, writer_name="html5", settings_overrides={
"halt_level": 2, "syntax_highlight": "short", "initial_header_level": 3
@@ -26,68 +28,69 @@ def render(rst: str):
"headers": []
}
- match = CONTENTS_REGEX.search(html) # Find the contents HTML
+ if link_headers:
+ match = CONTENTS_REGEX.search(html) # Find the contents HTML
- if match:
- data["html"] = html.replace(match.group(0), "") # Remove the contents from the document HTML
+ if match:
+ data["html"] = html.replace(match.group(0), "") # Remove the contents from the document HTML
- depth = 0
- headers = []
- current_header = {}
+ depth = 0
+ headers = []
+ current_header = {}
- group = match.group(1)
+ group = match.group(1)
- # Sanitize the output so we can more easily parse it
- group = group.replace("<li>", "<li>\n")
- group = group.replace("</li>", "\n</li>")
- group = group.replace("<p>", "<p>\n")
- group = group.replace("</p>", "\n</p>")
+ # Sanitize the output so we can more easily parse it
+ group = group.replace("<li>", "<li>\n")
+ group = group.replace("</li>", "\n</li>")
+ group = group.replace("<p>", "<p>\n")
+ group = group.replace("</p>", "\n</p>")
- for line in group.split("\n"):
- line = line.strip() # Remove excess whitespace
+ for line in group.split("\n"):
+ line = line.strip() # Remove excess whitespace
- if not line: # Nothing to process
- continue
+ if not line: # Nothing to process
+ continue
- if line.startswith("<li>") and depth <= 2:
- # We've found a header, or the start of a header group
- depth += 1
- elif line.startswith("</li>") and depth >= 0:
- # That's the end of a header or header group
+ if line.startswith("<li>") and depth <= 2:
+ # We've found a header, or the start of a header group
+ depth += 1
+ elif line.startswith("</li>") and depth >= 0:
+ # That's the end of a header or header group
- if depth == 1:
- # We just dealt with an entire header group, so store it
- headers.append(current_header.copy()) # Store a copy, since we're clearing the dict
- current_header.clear()
+ if depth == 1:
+ # We just dealt with an entire header group, so store it
+ headers.append(current_header.copy()) # Store a copy, since we're clearing the dict
+ current_header.clear()
- depth -= 1
- elif line.startswith("<a") and depth <= 2:
- # We've found an actual URL
- match = HREF_REGEX.match(line) # Parse the line for the ID and header title
+ depth -= 1
+ elif line.startswith("<a") and depth <= 2:
+ # We've found an actual URL
+ match = HREF_REGEX.match(line) # Parse the line for the ID and header title
- if depth == 1: # Top-level header, so just store it in the current header
- current_header["id"] = match.group(1)
+ if depth == 1: # Top-level header, so just store it in the current header
+ current_header["id"] = match.group(1)
- title = match.group(2)
+ title = match.group(2)
- if title.startswith("<i"): # We've found an icon, which needs to have a space after it
- title = title.replace("</i> ", "</i> &nbsp;")
+ if title.startswith("<i"): # We've found an icon, which needs to have a space after it
+ title = title.replace("</i> ", "</i> &nbsp;")
- current_header["title"] = title
- else: # Second-level (or deeper) header, should be stored in a list of sub-headers under the current
- sub_headers = current_header.get("sub_headers", [])
- title = match.group(2)
+ current_header["title"] = title
+ else: # Second-level (or deeper) header, should be stored in a list of sub-headers
+ sub_headers = current_header.get("sub_headers", [])
+ title = match.group(2)
- if title.startswith("<i"): # We've found an icon, which needs to have a space after it
- title = title.replace("</i> ", "</i> &nbsp;")
+ if title.startswith("<i"): # We've found an icon, which needs to have a space after it
+ title = title.replace("</i> ", "</i> &nbsp;")
- sub_headers.append({
- "id": match.group(1),
- "title": title
- })
- current_header["sub_headers"] = sub_headers
+ sub_headers.append({
+ "id": match.group(1),
+ "title": title
+ })
+ current_header["sub_headers"] = sub_headers
- data["headers"] = headers
+ data["headers"] = headers
return data
diff --git a/pysite/tables.py b/pysite/tables.py
index 4d9142fa..8ec550fb 100644
--- a/pysite/tables.py
+++ b/pysite/tables.py
@@ -8,6 +8,14 @@ class Table(NamedTuple):
TABLES = {
+ "bot_events": Table( # Events to be sent to the bot via websocket
+ primary_key="id",
+ keys=sorted([
+ "id",
+ "data"
+ ])
+ ),
+
"hiphopify": Table( # Users in hiphop prison
primary_key="user_id",
keys=sorted([
@@ -26,6 +34,81 @@ TABLES = {
locked=False
),
+ "code_jams": Table( # Information about each code jam
+ primary_key="number",
+ keys=sorted([
+ "date_end", # datetime
+ "date_start", # datetime
+ "end_html", # str
+ "end_rst", # str
+ "number", # int
+ "participants", # list[int]
+ "repo", # str
+ "state", # str
+ "task_html", # str
+ "task_rst", # str
+ "teams", # list[int]
+ "theme", # str
+ "title", # str
+ "winners" # list[int]
+ ])
+ ),
+
+ "code_jam_forms": Table( # Application forms for each jam
+ primary_key="number",
+ keys=sorted([
+ "number", # int
+ "questions" # list[dict[str, str]] {title, type, input_type, options?}
+ ])
+ ),
+
+ "code_jam_questions": Table( # Application form questions
+ primary_key="id",
+ keys=sorted([
+ "id", # uuid
+ "" # TODO
+ ])
+ ),
+
+ "code_jam_responses": Table( # Application form responses
+ primary_key="id",
+ keys=sorted([
+ "id", # uuid
+ "jam", # int
+ "answers", # dict {question, answer, metadata}
+ ])
+ ),
+
+ "code_jam_teams": Table( # Teams for each jam
+ primary_key="id",
+ keys=sorted([
+ "id", # uuid
+ "name", # str
+ "members" # list[int]
+ ])
+ ),
+
+ "code_jam_infractions": Table( # Individual infractions for each user
+ primary_key="id",
+ keys=sorted([
+ "snowflake", # int
+ "participant", # int
+ "reason", # str
+ "number" # int (optionally -1 for permanent)
+ ])
+ ),
+
+ "code_jam_participants": Table( # Info for each participant
+ primary_key="id",
+ keys=sorted([
+ "snowflake", # int
+ "skill_level", # str
+ "age", # str
+ "github_username", # str
+ "timezone" # str
+ ])
+ ),
+
"oauth_data": Table( # OAuth login information
primary_key="id",
keys=sorted([
@@ -70,7 +153,8 @@ TABLES = {
),
"wiki_revisions": Table( # Revisions of wiki articles
- primary_key="id", keys=sorted([
+ primary_key="id",
+ keys=sorted([
"id",
"date",
"post",
diff --git a/pysite/views/staff/index.py b/pysite/views/staff/index.py
index fc05f1a7..a090ebdd 100644
--- a/pysite/views/staff/index.py
+++ b/pysite/views/staff/index.py
@@ -9,12 +9,12 @@ from pysite.decorators import require_roles
class StaffView(RouteView):
path = "/"
- name = "staff_index"
+ name = "index"
@require_roles(*ALL_STAFF_ROLES)
def get(self):
return self.render(
- "staff/staff.html", manager=self.is_table_editor(),
+ "staff/index.html", manager=self.is_table_editor(),
app_config=pformat(current_app.config, indent=4, width=120)
)
diff --git a/pysite/views/staff/jams/actions.py b/pysite/views/staff/jams/actions.py
new file mode 100644
index 00000000..93a7f2c7
--- /dev/null
+++ b/pysite/views/staff/jams/actions.py
@@ -0,0 +1,36 @@
+from flask import jsonify, request
+
+from pysite.base_route import APIView
+from pysite.constants import ALL_STAFF_ROLES, ErrorCodes
+from pysite.decorators import csrf, require_roles
+from pysite.mixins import DBMixin
+
+ACTIONS = ["state"]
+KEYS = ["action"]
+
+
+class ActionView(APIView, DBMixin):
+ path = "/jams/action/"
+ name = "jams.action"
+ table_name = "code_jams"
+
+ @csrf
+ @require_roles(*ALL_STAFF_ROLES)
+ def post(self):
+ action = request.args.get("action")
+
+ if action not in ACTIONS:
+ return self.error(ErrorCodes.incorrect_parameters)
+
+ if action == "state":
+ jam = int(request.args.get("jam"))
+ state = request.args.get("state")
+
+ if not all((jam, state)):
+ return self.error(ErrorCodes.incorrect_parameters)
+
+ jam_obj = self.db.get(self.table_name, jam)
+ jam_obj["state"] = state
+ self.db.insert(self.table_name, jam_obj, conflict="update")
+
+ return jsonify({})
diff --git a/pysite/views/staff/jams/create.py b/pysite/views/staff/jams/create.py
new file mode 100644
index 00000000..e88056c5
--- /dev/null
+++ b/pysite/views/staff/jams/create.py
@@ -0,0 +1,49 @@
+from flask import redirect, request, url_for
+from werkzeug.exceptions import BadRequest
+
+from pysite.base_route import RouteView
+from pysite.constants import ALL_STAFF_ROLES
+from pysite.decorators import csrf, require_roles
+from pysite.mixins import DBMixin
+
+REQUIRED_KEYS = ["title", "date_start", "date_end"]
+
+
+class StaffView(RouteView, DBMixin):
+ path = "/jams/create"
+ name = "jams.create"
+ table_name = "code_jams"
+
+ @require_roles(*ALL_STAFF_ROLES)
+ def get(self):
+ number = self.get_next_number()
+ return self.render("staff/jams/create.html", number=number)
+
+ @require_roles(*ALL_STAFF_ROLES)
+ @csrf
+ def post(self):
+ data = {}
+
+ for key in REQUIRED_KEYS:
+ arg = request.form.get(key)
+
+ if not arg:
+ return BadRequest()
+
+ data[key] = arg
+
+ data["state"] = "planning"
+ data["number"] = self.get_next_number()
+
+ self.db.insert(self.table_name, data)
+
+ return redirect(url_for("staff.jams.index"))
+
+ def get_next_number(self) -> int:
+ count = self.db.run(self.table.count(), coerce=int)
+
+ if count:
+ max_num = self.db.run(self.table.max("number"))["number"]
+
+ return max_num + 1
+ return 1
diff --git a/pysite/views/staff/jams/edit_basics.py b/pysite/views/staff/jams/edit_basics.py
new file mode 100644
index 00000000..eb56e7d4
--- /dev/null
+++ b/pysite/views/staff/jams/edit_basics.py
@@ -0,0 +1,58 @@
+import datetime
+
+from flask import redirect, request, url_for
+from werkzeug.exceptions import BadRequest, NotFound
+
+from pysite.base_route import RouteView
+from pysite.constants import ALL_STAFF_ROLES
+from pysite.decorators import csrf, require_roles
+from pysite.mixins import DBMixin
+
+REQUIRED_KEYS = ["title", "date_start", "date_end"]
+
+
+class StaffView(RouteView, DBMixin):
+ path = "/jams/<int:jam>/edit/basics"
+ name = "jams.edit.basics"
+ table_name = "code_jams"
+
+ @require_roles(*ALL_STAFF_ROLES)
+ def get(self, jam):
+ jam_obj = self.db.get(self.table_name, jam)
+
+ if not jam_obj:
+ return NotFound()
+ return self.render("staff/jams/edit_basics.html", jam=jam_obj)
+
+ @require_roles(*ALL_STAFF_ROLES)
+ @csrf
+ def post(self, jam):
+ jam_obj = self.db.get(self.table_name, jam)
+
+ if not jam_obj:
+ return NotFound()
+
+ if not jam_obj["state"] == "planning":
+ return BadRequest()
+
+ for key in REQUIRED_KEYS:
+ arg = request.form.get(key)
+
+ if not arg:
+ return BadRequest()
+
+ jam_obj[key] = arg
+
+ # Convert given datetime strings into actual objects, adding timezones to keep rethinkdb happy
+ date_start = datetime.datetime.strptime(jam_obj["date_start"], "%Y-%m-%d %H:%M")
+ date_start = date_start.replace(tzinfo=datetime.timezone.utc)
+
+ date_end = datetime.datetime.strptime(jam_obj["date_end"], "%Y-%m-%d %H:%M")
+ date_end = date_end.replace(tzinfo=datetime.timezone.utc)
+
+ jam_obj["date_start"] = date_start
+ jam_obj["date_end"] = date_end
+
+ self.db.insert(self.table_name, jam_obj, conflict="replace")
+
+ return redirect(url_for("staff.jams.index"))
diff --git a/pysite/views/staff/jams/edit_ending.py b/pysite/views/staff/jams/edit_ending.py
new file mode 100644
index 00000000..69b91e29
--- /dev/null
+++ b/pysite/views/staff/jams/edit_ending.py
@@ -0,0 +1,54 @@
+from flask import redirect, request, url_for
+from werkzeug.exceptions import BadRequest, NotFound
+
+from pysite.base_route import RouteView
+from pysite.constants import ALL_STAFF_ROLES
+from pysite.decorators import csrf, require_roles
+from pysite.mixins import DBMixin
+from pysite.rst import render
+
+REQUIRED_KEYS = ["end_rst"]
+
+
+class StaffView(RouteView, DBMixin):
+ path = "/jams/<int:jam>/edit/ending"
+ name = "jams.edit.ending"
+ table_name = "code_jams"
+
+ @require_roles(*ALL_STAFF_ROLES)
+ def get(self, jam):
+ jam_obj = self.db.get(self.table_name, jam)
+
+ if not jam_obj:
+ return NotFound()
+
+ if not jam_obj["state"] == "judging":
+ return BadRequest()
+
+ return self.render("staff/jams/edit_ending.html", jam=jam_obj)
+
+ @require_roles(*ALL_STAFF_ROLES)
+ @csrf
+ def post(self, jam):
+ jam_obj = self.db.get(self.table_name, jam)
+
+ if not jam_obj:
+ return NotFound()
+
+ if not jam_obj["state"] == "judging":
+ return BadRequest()
+
+ print(request.form)
+ for key in REQUIRED_KEYS:
+ arg = request.form.get(key)
+
+ if not arg:
+ return BadRequest()
+
+ jam_obj[key] = arg
+
+ jam_obj["end_html"] = render(jam_obj["end_rst"], link_headers=False)["html"]
+
+ self.db.insert(self.table_name, jam_obj, conflict="replace")
+
+ return redirect(url_for("staff.jams.index"))
diff --git a/pysite/views/staff/jams/edit_info.py b/pysite/views/staff/jams/edit_info.py
new file mode 100644
index 00000000..2ec67ebb
--- /dev/null
+++ b/pysite/views/staff/jams/edit_info.py
@@ -0,0 +1,54 @@
+from flask import redirect, request, url_for
+from werkzeug.exceptions import BadRequest, NotFound
+
+from pysite.base_route import RouteView
+from pysite.constants import ALL_STAFF_ROLES
+from pysite.decorators import csrf, require_roles
+from pysite.mixins import DBMixin
+from pysite.rst import render
+
+REQUIRED_KEYS = ["repo", "task_rst", "theme"]
+ALLOWED_STATES = ["planning", "info"]
+
+
+class StaffView(RouteView, DBMixin):
+ path = "/jams/<int:jam>/edit/info"
+ name = "jams.edit.info"
+ table_name = "code_jams"
+
+ @require_roles(*ALL_STAFF_ROLES)
+ def get(self, jam):
+ jam_obj = self.db.get(self.table_name, jam)
+
+ if not jam_obj:
+ return NotFound()
+
+ if not jam_obj["state"] in ALLOWED_STATES:
+ return BadRequest()
+ return self.render("staff/jams/edit_info.html", jam=jam_obj)
+
+ @require_roles(*ALL_STAFF_ROLES)
+ @csrf
+ def post(self, jam):
+ jam_obj = self.db.get(self.table_name, jam)
+
+ if not jam_obj:
+ return NotFound()
+
+ if not jam_obj["state"] in ALLOWED_STATES:
+ return BadRequest()
+
+ print(request.form)
+ for key in REQUIRED_KEYS:
+ arg = request.form.get(key)
+
+ if not arg:
+ return BadRequest()
+
+ jam_obj[key] = arg
+
+ jam_obj["task_html"] = render(jam_obj["task_rst"], link_headers=False)["html"]
+
+ self.db.insert(self.table_name, jam_obj, conflict="replace")
+
+ return redirect(url_for("staff.jams.index"))
diff --git a/pysite/views/staff/jams/index.py b/pysite/views/staff/jams/index.py
new file mode 100644
index 00000000..40a8387c
--- /dev/null
+++ b/pysite/views/staff/jams/index.py
@@ -0,0 +1,15 @@
+from pysite.base_route import RouteView
+from pysite.constants import ALL_STAFF_ROLES, JAM_STATES
+from pysite.decorators import require_roles
+from pysite.mixins import DBMixin
+
+
+class StaffView(RouteView, DBMixin):
+ path = "/jams"
+ name = "jams.index"
+ table_name = "code_jams"
+
+ @require_roles(*ALL_STAFF_ROLES)
+ def get(self):
+ jams = self.db.get_all(self.table_name)
+ return self.render("staff/jams/index.html", jams=jams, states=JAM_STATES)
diff --git a/pysite/views/staff/render.py b/pysite/views/staff/render.py
new file mode 100644
index 00000000..00c9a9f3
--- /dev/null
+++ b/pysite/views/staff/render.py
@@ -0,0 +1,63 @@
+import re
+
+from docutils.utils import SystemMessage
+from flask import jsonify
+from schema import Schema
+
+from pysite.base_route import APIView
+from pysite.constants import EDITOR_ROLES, ValidationTypes
+from pysite.decorators import api_params, csrf, require_roles
+from pysite.rst import render
+
+SCHEMA = Schema([{
+ "data": str
+}])
+
+MESSAGE_REGEX = re.compile(r"<string>:(\d+): \([A-Z]+/\d\) (.*)")
+
+
+class RenderView(APIView):
+ path = "/render" # "path" means that it accepts slashes
+ name = "render"
+
+ @csrf
+ @require_roles(*EDITOR_ROLES)
+ @api_params(schema=SCHEMA, validation_type=ValidationTypes.json)
+ def post(self, data):
+ if not len(data):
+ return jsonify({"error": "No data!"})
+
+ data = data[0]["data"]
+ try:
+ html = render(data, link_headers=False)["html"]
+
+ return jsonify({"data": html})
+ except SystemMessage as e:
+ lines = str(e)
+ data = {
+ "error": lines,
+ "error_lines": []
+ }
+
+ if "\n" in lines:
+ lines = lines.split("\n")
+ else:
+ lines = [lines]
+
+ for message in lines:
+ match = MESSAGE_REGEX.match(message)
+
+ if match:
+ data["error_lines"].append(
+ {
+ "row": int(match.group(1)) - 3,
+ "column": 0,
+ "type": "error",
+ "text": match.group(2)
+ }
+ )
+
+ print(data)
+ return jsonify(data)
+ except Exception as e:
+ return jsonify({"error": str(e)})
diff --git a/pysite/views/ws/bot.py b/pysite/views/ws/bot.py
new file mode 100644
index 00000000..24af74df
--- /dev/null
+++ b/pysite/views/ws/bot.py
@@ -0,0 +1,59 @@
+import json
+import logging
+
+from geventwebsocket.websocket import WebSocket
+
+from pysite.constants import BOT_API_KEY
+from pysite.mixins import DBMixin
+from pysite.websockets import WS
+
+
+class BotWebsocket(WS, DBMixin):
+ path = "/bot"
+ name = "ws.bot"
+ table_name = "bot_events"
+
+ do_changefeed = True
+
+ def __init__(self, socket: WebSocket):
+ super().__init__(socket)
+ self.log = logging.getLogger()
+
+ def on_open(self):
+ self.log.debug("Bot | WS opened.")
+
+ def on_message(self, message):
+ self.log.debug(f"Bot | Message: {message}")
+
+ try:
+ message = json.loads(message)
+ except json.JSONDecodeError:
+ self.send_json({"error": "Message was not valid JSON"})
+ return self.socket.close()
+
+ action = message["action"]
+
+ if action == "login":
+ if message["key"] != BOT_API_KEY:
+ return self.socket.close()
+
+ self.do_changefeed = True
+
+ for document in self.db.changes(self.table_name, include_initial=True, include_types=True):
+ if not self.do_changefeed:
+ break
+
+ if document["type"] not in ["add", "initial"]:
+ continue
+
+ self.send_json({"action": "event", "event": document["new_val"]})
+ self.db.delete(self.table_name, document["id"])
+
+ self.send_json({"error": f"Unknown action: {action}"})
+
+ def on_close(self):
+ self.log.debug("Bot | WS closed.")
+ self.do_changefeed = False
+
+ def send_json(self, data):
+ return self.send(json.dumps(data))
diff --git a/static/js/fouc.js b/static/js/fouc.js
index c1414d9b..c3270bf8 100644
--- a/static/js/fouc.js
+++ b/static/js/fouc.js
@@ -31,6 +31,6 @@ document.onreadystatechange = function () {
'https://pro.fontawesome.com/releases/v5.0.13/js/all.js', // URL
'sha384-d84LGg2pm9KhR4mCAs3N29GQ4OYNy+K+FBHX8WhimHpPm86c839++MDABegrZ3gn', // Integrity
'anonymous' // Cross-origin
- )
+ );
}
} \ No newline at end of file
diff --git a/static/js/jams.js b/static/js/jams.js
new file mode 100644
index 00000000..bdce1060
--- /dev/null
+++ b/static/js/jams.js
@@ -0,0 +1,92 @@
+"use strict";
+
+function refreshLock() {
+ console.log("Refreshing lock");
+ let oReq = new XMLHttpRequest();
+ oReq.addEventListener("load", function() {
+ let response = JSON.parse(this.responseText);
+
+ if (response.error !== undefined) {
+ document.getElementById("submit").disabled = true;
+
+ if (response.error_lines !== undefined) {
+ editor.session.setAnnotations(response.error_lines);
+ document.getElementById("preview-div").innerHTML ="<h3>Error - see editor margin</h3>";
+ } else {
+ console.log("Error: " + response.error);
+ document.getElementById("preview-div").innerHTML ="<h3>Error</h3><p>" + response.error + "<p>";
+ }
+ } else {
+ document.getElementById("submit").disabled = false;
+ document.getElementById("preview-div").innerHTML = response.data;
+
+ editor.session.setAnnotations([]);
+ }
+ });
+
+ let data = editor.getValue();
+
+ if (data.replace("\s", "").length < 1 || document.getElementById("title").value.length < 1) {
+ document.getElementById("submit").disabled = true;
+ return false;
+ }
+
+ oReq.open("POST", "/render");
+
+ oReq.setRequestHeader("Content-type", "application/json");
+ oReq.setRequestHeader("X-CSRFToken", csrf_token);
+
+ oReq.send(JSON.stringify({"data": editor.getValue()}));
+}
+
+class Actions {
+ constructor(url, csrf_token) {
+ this.url = url;
+ this.csrf_token = csrf_token;
+ }
+
+ send(action, method, data, callback) {
+ let oReq = new XMLHttpRequest();
+
+ oReq.addEventListener("load", function() {
+ try {
+ data = JSON.parse(this.responseText);
+ } catch (e) {
+ return callback(false);
+ }
+
+ if ("error_code" in data) {
+ return callback(false, data);
+ }
+
+ return callback(true, data);
+ });
+
+ data["action"] = action;
+
+ let params = this.get_params(data);
+ let url = this.url + "?" + params;
+
+ oReq.open(method, url);
+ oReq.setRequestHeader("X-CSRFToken", this.csrf_token);
+ oReq.send();
+ }
+
+ get_params(data) { // https://stackoverflow.com/a/12040639
+ return Object.keys(data).map(function(key) {
+ return [key, data[key]].map(encodeURIComponent).join("=");
+ }).join("&");
+ }
+
+ set_state(jam, state, callback) {
+ this.send(
+ "state",
+ "POST",
+ {
+ "jam": jam,
+ "state": state
+ },
+ callback
+ );
+ }
+} \ No newline at end of file
diff --git a/templates/main/base.html b/templates/main/base.html
index d87376b1..8893be01 100644
--- a/templates/main/base.html
+++ b/templates/main/base.html
@@ -10,14 +10,15 @@
<script src="{{ static_file('js/fouc.js') }}"></script>
<!-- Other JS loads -->
-<!-- <script defer src="https://pro.fontawesome.com/releases/v5.0.9/js/all.js" integrity="sha384-DtPgXIYsUR6lLmJK14ZNUi11aAoezQtw4ut26Zwy9/6QXHH8W3+gjrRDT+lHiiW4" crossorigin="anonymous"></script>-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/uikit/3.0.0-beta.39/js/uikit.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<!-- Stylesheets -->
<link rel="shortcut icon" href="{{ static_file('favicon.ico') }}">
<link rel="stylesheet" href="{{ static_file('uikit_blurple.css') }}"/>
<link rel="stylesheet" href="{{ static_file('style.css') }}"/>
<link rel="stylesheet" href="{{ static_file('css/pygments-monokai.css') }}"/>
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<!-- OpenGraph metadata -->
<meta property="og:title" content="Python Discord | {% block og_title %}{% endblock %}">
diff --git a/templates/staff/staff.html b/templates/staff/index.html
index b22bfcec..31fddceb 100644
--- a/templates/staff/staff.html
+++ b/templates/staff/index.html
@@ -7,11 +7,12 @@
<h1 class="uk-text-center">
Management links
</h1>
+
+ <a class="uk-button uk-button-primary" href="{{ url_for("staff.jams.index") }}">Code Jams</a>
{% if manager %}
<a class="uk-button uk-button-primary" href="{{ url_for("staff.tables.index") }}">Table Management</a>
- {% else %}
- <p>Nothing for you yet, I'm afraid.</p>
{% endif %}
+
<h1 class="uk-title uk-text-center">
App config
</h1>
diff --git a/templates/staff/jams/create.html b/templates/staff/jams/create.html
new file mode 100644
index 00000000..49a8b615
--- /dev/null
+++ b/templates/staff/jams/create.html
@@ -0,0 +1,76 @@
+{% extends "main/base.html" %}
+{% block title %}Staff | Jams | Create{% endblock %}
+{% block og_title %}Staff | Jams | Create{% endblock %}
+{% block og_description %}Create a brand new code jam{% endblock %}
+{% block content %}
+ <div class="uk-container uk-container-small uk-section">
+ <h1 class="uk-text-center">Code Jam: Create</h1>
+
+ <form action="{{ url_for("staff.jams.create") }}" method="post" class="uk-form-horizontal">
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="title">Title</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="title" id="title" type="text" required />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="number">Number</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="number" id="number" type="text" value="{{ number }}" disabled />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="date_start">Starting date (UTC)</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="date_start" id="date_start" type="text" required />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="date_end">Ending date (UTC)</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="date_end" id="date_end" type="text" required />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="state">State</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <select class="uk-select" name="state" id="state" disabled>
+ <option value="planning" selected>Planning</option>
+ </select>
+ </div>
+ </div>
+
+ <input type="hidden" name="csrf_token" id="csrf_token" value="{{ csrf_token() }}"/>
+
+ <div class="uk-align-center uk-text-center">
+ <a id="back" class="uk-button uk-button-default" href="{{ url_for("staff.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back
+ </a>
+ <button id="done" class="uk-button uk-button-primary" type="submit">
+ <i class="uk-icon fa-fw far fa-check"></i> &nbsp;Done
+ </button>
+ </div>
+ </form>
+
+ </div>
+
+ <script type="application/javascript">
+ const date_start = flatpickr("#date_start", {enableTime: true, altInput: true});
+ const date_end = flatpickr("#date_end", {enableTime: true, altInput: true});
+ </script>
+{% endblock %}
diff --git a/templates/staff/jams/edit_basics.html b/templates/staff/jams/edit_basics.html
new file mode 100644
index 00000000..59d69b77
--- /dev/null
+++ b/templates/staff/jams/edit_basics.html
@@ -0,0 +1,76 @@
+{% extends "main/base.html" %}
+{% block title %}Staff | Jams | Edit (Basics){% endblock %}
+{% block og_title %}Staff | Jams | Edit (Basics){% endblock %}
+{% block og_description %}Edit the basic info for a code jam{% endblock %}
+{% block content %}
+ <div class="uk-container uk-container-small uk-section">
+ <h1 class="uk-text-center">Code Jam: Edit (Basics)</h1>
+
+ <form action="{{ url_for("staff.jams.edit.basics", jam=jam.number) }}" method="post" class="uk-form-horizontal">
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="title">Title</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="title" id="title" type="text" value="{{ jam.title }}" required />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="number">Number</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="number" id="number" type="text" value="{{ jam.number }}" disabled />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="date_start">Starting date (UTC)</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="date_start" id="date_start" type="text" value="{{ jam.date_start }}" required />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="date_end">Ending date (UTC)</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="date_end" id="date_end" type="text" value="{{ jam.date_end }}" required />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="state">State</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <select class="uk-select" name="state" id="state" disabled>
+ <option value="{{ jam.state }}" selected>{{ jam.state.title() }}</option>
+ </select>
+ </div>
+ </div>
+
+ <input type="hidden" name="csrf_token" id="csrf_token" value="{{ csrf_token() }}"/>
+
+ <div class="uk-align-center uk-text-center">
+ <a id="back" class="uk-button uk-button-default" href="{{ url_for("staff.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back
+ </a>
+ <button id="done" class="uk-button uk-button-primary" type="submit">
+ <i class="uk-icon fa-fw far fa-check"></i> &nbsp;Done
+ </button>
+ </div>
+ </form>
+
+ </div>
+
+ <script type="application/javascript">
+ const date_start = flatpickr("#date_start", {enableTime: true, altInput: true});
+ const date_end = flatpickr("#date_end", {enableTime: true, altInput: true});
+ </script>
+{% endblock %}
diff --git a/templates/staff/jams/edit_ending.html b/templates/staff/jams/edit_ending.html
new file mode 100644
index 00000000..cc27a208
--- /dev/null
+++ b/templates/staff/jams/edit_ending.html
@@ -0,0 +1,150 @@
+{% extends "main/base.html" %}
+{% block title %}Staff | Jams | Edit (Ending Comments){% endblock %}
+{% block og_title %}Staff | Jams | Edit (Ending Comments){% endblock %}
+{% block og_description %}Edit the ending comments for a code jam{% endblock %}
+{% block extra_head %}
+<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.3.3/ace.js" type="application/javascript"></script>
+{% endblock %}
+{% block content %}
+ <div class="uk-container uk-container-small uk-section">
+ <h1 class="uk-text-center">Code Jam: Edit (Ending Comments)</h1>
+
+ <form action="{{ url_for("staff.jams.edit.ending", jam=jam.number) }}" method="post" class="uk-form-horizontal">
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label">Comments (RST)</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <div id="editor" class="uk-textarea" style="resize: vertical; min-height: 15rem;">{{ jam.end_rst }}</div>
+ </div>
+
+ <input type="hidden" name="end_rst" id="end_rst" />
+ </div>
+
+ <input type="hidden" name="csrf_token" id="csrf_token" value="{{ csrf_token() }}"/>
+
+ <div class="uk-align-center uk-text-center">
+ <a id="back" class="uk-button uk-button-default" href="{{ url_for("staff.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back
+ </a>
+ <button class="uk-button uk-button-secondary" type="button" id="preview">
+ <i class="uk-icon fa-fw far fa-eye"></i> &nbsp;Preview
+ </button>
+ <button id="done" class="uk-button uk-button-primary" type="submit" disabled>
+ <i class="uk-icon fa-fw far fa-check"></i> &nbsp;Done
+ </button>
+ </div>
+ </form>
+ </div>
+
+ <div id="preview-modal" class="uk-flex-top" uk-modal>
+ <div class="uk-modal-dialog">
+ <button class="uk-modal-close-default" type="button" uk-close></button>
+
+ <div class="uk-modal-body">
+ <h2>Code Jam {{ jam.number }}: {{ jam.title }} <a href="{{ jam.repo }}" id="preview-url"><i class="uk-icon fa-fw fab fa-github"></i></a></h2>
+ <p class="uk-text-meta">Theme: <span id="preview-theme">{{ jam.theme }}</span></p>
+
+ <div id="preview-div">
+ {{ jam.end_rst | safe }}
+ </div>
+ </div>
+
+ <div class="uk-modal-footer">
+ <div class="uk-text-center">
+ <button class="uk-button uk-button-default uk-modal-close" type="button" id="state-cancel">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Close
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <script type="application/javascript">
+ "use strict";
+
+ let csrf_token = "{{ csrf_token() }}";
+ let modal = UIkit.modal(document.getElementById("preview-modal"));
+ let preview_url = "{{ url_for("staff.render") }}";
+
+ function do_preview(callback) {
+ let oReq = new XMLHttpRequest();
+
+ oReq.addEventListener("load", function() {
+ let response = JSON.parse(this.responseText);
+
+ if (response.error !== undefined) {
+ document.getElementById("done").disabled = true;
+
+ if (response.error_lines !== undefined) {
+ editor.session.setAnnotations(response.error_lines);
+ document.getElementById("preview-div").innerHTML ="<h3>Error - see editor margin</h3>";
+ } else {
+ console.log("Error: " + response.error);
+ document.getElementById("preview-div").innerHTML ="<h3>Error</h3><p>" + response.error + "<p>";
+ }
+ } else {
+ document.getElementById("done").disabled = false;
+ document.getElementById("preview-div").innerHTML = response.data;
+
+ editor.session.setAnnotations([]);
+ }
+
+ if (callback !== undefined) {
+ callback();
+ }
+ });
+
+ let data = editor.getValue();
+
+ if (data.replace("\s", "").length < 1) {
+ document.getElementById("done").disabled = true;
+
+ if (callback !== undefined) {
+ UIkit.notification({
+ "message": "Please enter some text to preview",
+ "status": "danger",
+ "pos": "bottom-center",
+ "timeout": 5000
+ });
+ }
+ return false;
+ }
+
+ oReq.open("POST", preview_url);
+
+ oReq.setRequestHeader("Content-type", "application/json");
+ oReq.setRequestHeader("X-CSRFToken", csrf_token);
+
+ oReq.send(JSON.stringify({"data": editor.getValue()}));
+
+ return false;
+ }
+
+ document.getElementById("preview").onclick = function() {
+ do_preview(function() {
+ modal.show();
+ })
+ };
+
+ let editor = ace.edit("editor");
+ let timer;
+
+ editor.session.setMode("ace/mode/rst");
+ editor.session.setUseWrapMode(true);
+
+ editor.setTheme("ace/theme/iplastic");
+ editor.setShowPrintMargin(false);
+
+ editor.on("input", function() {
+ document.getElementById("done").disabled = true;
+ document.getElementById("end_rst").value = editor.getValue();
+
+ if (timer !== undefined) {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(do_preview, 1000);
+ });
+ </script>
+{% endblock %}
diff --git a/templates/staff/jams/edit_info.html b/templates/staff/jams/edit_info.html
new file mode 100644
index 00000000..f1c50263
--- /dev/null
+++ b/templates/staff/jams/edit_info.html
@@ -0,0 +1,177 @@
+{% extends "main/base.html" %}
+{% block title %}Staff | Jams | Edit (Basics){% endblock %}
+{% block og_title %}Staff | Jams | Edit (Basics){% endblock %}
+{% block og_description %}Edit the basic info for a code jam{% endblock %}
+{% block extra_head %}
+<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.3.3/ace.js" type="application/javascript"></script>
+{% endblock %}
+{% block content %}
+ <div class="uk-container uk-container-small uk-section">
+ <h1 class="uk-text-center">Code Jam: Edit (Basics)</h1>
+
+ <form action="{{ url_for("staff.jams.edit.info", jam=jam.number) }}" method="post" class="uk-form-horizontal">
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="repo">Repo URL</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="repo" id="repo" type="text" value="{{ jam.repo }}" required />
+ </div>
+ </div>
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="theme">Theme</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <input class="uk-input" name="theme" id="theme" type="text" value="{{ jam.theme }}" required />
+ </div>
+ </div>
+
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label">Task (RST)</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <div id="editor" class="uk-textarea" style="resize: vertical; min-height: 15rem;">{{ jam.task_rst }}</div>
+ </div>
+
+ <input type="hidden" name="task_rst" id="task_rst" />
+ </div>
+
+ <input type="hidden" name="csrf_token" id="csrf_token" value="{{ csrf_token() }}"/>
+
+ <div class="uk-align-center uk-text-center">
+ <a id="back" class="uk-button uk-button-default" href="{{ url_for("staff.jams.index") }}">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back
+ </a>
+ <button class="uk-button uk-button-secondary" type="button" id="preview">
+ <i class="uk-icon fa-fw far fa-eye"></i> &nbsp;Preview
+ </button>
+ <button id="done" class="uk-button uk-button-primary" type="submit" disabled>
+ <i class="uk-icon fa-fw far fa-check"></i> &nbsp;Done
+ </button>
+ </div>
+ </form>
+ </div>
+
+ <div id="preview-modal" class="uk-flex-top" uk-modal>
+ <div class="uk-modal-dialog">
+ <button class="uk-modal-close-default" type="button" uk-close></button>
+
+ <div class="uk-modal-body">
+ <h2>Code Jam {{ jam.number }}: {{ jam.title }} <a href="{{ jam.repo }}" id="preview-url"><i class="uk-icon fa-fw fab fa-github"></i></a></h2>
+ <p class="uk-text-meta">Theme: <span id="preview-theme">{{ jam.theme }}</span></p>
+
+ <div id="preview-div">
+ {{ jam.task_html | safe }}
+ </div>
+ </div>
+
+ <div class="uk-modal-footer">
+ <div class="uk-text-center">
+ <button class="uk-button uk-button-default uk-modal-close" type="button" id="state-cancel">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Close
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <script type="application/javascript">
+ "use strict";
+
+ let csrf_token = "{{ csrf_token() }}";
+ let modal = UIkit.modal(document.getElementById("preview-modal"));
+ let preview_url = "{{ url_for("staff.render") }}";
+
+ function do_preview(callback) {
+ let oReq = new XMLHttpRequest();
+
+ oReq.addEventListener("load", function() {
+ let response = JSON.parse(this.responseText);
+
+ if (response.error !== undefined) {
+ document.getElementById("done").disabled = true;
+
+ if (response.error_lines !== undefined) {
+ editor.session.setAnnotations(response.error_lines);
+ document.getElementById("preview-div").innerHTML ="<h3>Error - see editor margin</h3>";
+ } else {
+ console.log("Error: " + response.error);
+ document.getElementById("preview-div").innerHTML ="<h3>Error</h3><p>" + response.error + "<p>";
+ }
+ } else {
+ document.getElementById("done").disabled = false;
+ document.getElementById("preview-div").innerHTML = response.data;
+
+ editor.session.setAnnotations([]);
+ }
+
+ if (callback !== undefined) {
+ callback();
+ }
+ });
+
+ let data = editor.getValue();
+
+ if (data.replace("\s", "").length < 1) {
+ document.getElementById("done").disabled = true;
+
+ if (callback !== undefined) {
+ UIkit.notification({
+ "message": "Please enter some text to preview",
+ "status": "danger",
+ "pos": "bottom-center",
+ "timeout": 5000
+ });
+ }
+ return false;
+ }
+
+ oReq.open("POST", preview_url);
+
+ oReq.setRequestHeader("Content-type", "application/json");
+ oReq.setRequestHeader("X-CSRFToken", csrf_token);
+
+ oReq.send(JSON.stringify({"data": editor.getValue()}));
+
+ return false;
+ }
+
+ document.getElementById("preview").onclick = function() {
+ do_preview(function() {
+ modal.show();
+ })
+ };
+
+ document.getElementById("theme").oninput = function() {
+ document.getElementById("preview-theme").textContent = this.value;
+ };
+
+ document.getElementById("repo").oninput = function() {
+ document.getElementById("preview-url").href = this.value;
+ };
+
+ let editor = ace.edit("editor");
+ let timer;
+
+ editor.session.setMode("ace/mode/rst");
+ editor.session.setUseWrapMode(true);
+
+ editor.setTheme("ace/theme/iplastic");
+ editor.setShowPrintMargin(false);
+
+ editor.on("input", function() {
+ document.getElementById("done").disabled = true;
+ document.getElementById("task_rst").value = editor.getValue();
+
+ if (timer !== undefined) {
+ clearTimeout(timer);
+ }
+ timer = setTimeout(do_preview, 1000);
+ });
+ </script>
+{% endblock %}
diff --git a/templates/staff/jams/index.html b/templates/staff/jams/index.html
new file mode 100644
index 00000000..0f0ce022
--- /dev/null
+++ b/templates/staff/jams/index.html
@@ -0,0 +1,251 @@
+{% extends "main/base.html" %}
+{% block title %}Staff | Home{% endblock %}
+{% block og_title %}Staff | Home{% endblock %}
+{% block og_description %}Landing page for the staff management area{% endblock %}
+{% block extra_head %}
+ <script src="{{ static_file('js/jams.js') }}"></script>
+{% endblock %}
+{% block content %}
+ <div class="uk-container uk-container-small uk-section">
+ <h1>Code Jams</h1>
+
+ <a class="uk-button uk-button-default" href="{{ url_for("staff.index") }}"><i class="uk-icon fa-fw fas fa-arrow-left"></i> &nbsp;Back</a>
+
+ <a class="uk-button uk-button-primary" href="{{ url_for("staff.jams.create") }}">Create</a>
+ {% if not jams %}
+ <p>
+ No code jams found. Create one above!
+ </p>
+ {% else %}
+ {% for jam in jams %}
+ <h2 class="uk-heading-divider">
+ Code Jam {{ jam.number }}: {{ jam.title }}
+
+ <span class="uk-align-right">
+ {% if jam.state == "planning" %}
+ <i class="uk-icon uk-text-muted fa-fw far fa-edit state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-planning"></i>
+ {% else %}
+ <i class="uk-icon uk-text-muted fa-fw far fa-edit state-{{ jam.number }}" style="display: none;" title="State: {{ jam.state }}" id="state-{{ jam.number }}-planning"></i>
+ {% endif %}
+
+ {% if jam.state == "announced" %}
+ <i class="uk-icon uk-text-primary fa-fw far fa-bullhorn state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-announced"></i>
+ {% else %}
+ <i class="uk-icon uk-text-primary fa-fw far fa-bullhorn state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-announced"></i>
+ {% endif %}
+
+ {% if jam.state == "running" %}
+ <i class="uk-icon uk-text-success fa-fw far fa-play state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-running"></i>
+ {% else %}
+ <i class="uk-icon uk-text-success fa-fw far fa-play state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-running"></i>
+ {% endif %}
+
+ {% if jam.state == "judging" %}
+ <i class="uk-icon uk-text-primary fa-fw far fa-balance-scale state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-judging"></i>
+ {% else %}
+ <i class="uk-icon uk-text-primary fa-fw far fa-balance-scale state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-judging"></i>
+ {% endif %}
+
+ {% if jam.state == "finished" %}
+ <i class="uk-icon uk-text-success fa-fw far fa-check-square state-{{ jam.number }}" title="State: {{ jam.state }}" id="state-{{ jam.number }}-finished"></i>
+ {% else %}
+ <i class="uk-icon uk-text-success fa-fw far fa-check-square state-{{ jam.number }}" hidden="hidden" title="State: {{ jam.state }}" id="state-{{ jam.number }}-finished"></i>
+ {% endif %}
+
+ {% if not jam.state in states %}
+ <i class="uk-icon uk-text-danger fa-fw far fa-question-square" title="Unknown state: {{ jam.state }}" id="state-{{ jam.number }}-unknown"></i>
+ {% else %}
+ <i class="uk-icon uk-text-danger fa-fw far fa-question-square" hidden="hidden" title="Unknown state: {{ jam.state }}" id="state-{{ jam.number }}-unknown"></i>
+ {% endif %}
+ </span>
+ </h2>
+
+ <span class="uk-label">Participants: {{ jam.participants | length }}</span>
+ <span class="uk-label uk-label-success">Start: {{ format_datetime(jam.date_start) }} (UTC)</span>
+ <span class="uk-label uk-label-danger">End: {{ format_datetime(jam.date_end) }} (UTC)</span>
+
+ <section class="uk-section">
+ <div class="uk-button-group uk-width-1-1">
+ <a class="uk-button uk-button-default uk-width-expand state-button" data-jam="{{ jam.number }}" id="jam-{{ jam.number }}-button-state">
+ <i class="uk-icon fa-fw far fa-pencil"></i> &nbsp;State
+ </a>
+
+ {% if jam.state == "planning" %}
+ <a class="uk-button uk-button-default uk-width-expand" data-jam="{{ jam.number }}" id="jam-{{ jam.number }}-button-basics" href="{{ url_for("staff.jams.edit.basics", jam=jam.number) }}">
+ <i class="uk-icon fa-fw far fa-pencil"></i> &nbsp;Basics
+ </a>
+ {% else %}
+ <a class="uk-button uk-button-default uk-width-expand" hidden="hidden" data-jam="{{ jam.number }}" id="jam-{{ jam.number }}-button-basics" href="{{ url_for("staff.jams.edit.basics", jam=jam.number) }}">
+ <i class="uk-icon fa-fw far fa-pencil"></i> &nbsp;Basics
+ </a>
+ {% endif %}
+
+ {% if jam.state in ["planning", "announced"] %}
+ <a class="uk-button uk-button-default uk-width-expand" data-jam="{{ jam.number }}" id="jam-{{ jam.number }}-button-info" href="{{ url_for("staff.jams.edit.info", jam=jam.number) }}">
+ <i class="uk-icon fa-fw far fa-pencil"></i> &nbsp;Info
+ </a>
+ {% else %}
+ <a class="uk-button uk-button-default uk-width-expand" hidden="hidden" data-jam="{{ jam.number }}" id="jam-{{ jam.number }}-button-info" href="{{ url_for("staff.jams.edit.info", jam=jam.number) }}">
+ <i class="uk-icon fa-fw far fa-pencil"></i> &nbsp;Info
+ </a>
+ {% endif %}
+
+ {% if jam.state == "judging" %}
+ <a class="uk-button uk-button-default uk-width-expand" data-jam="{{ jam.number }}" id="jam-{{ jam.number }}-button-ending" href="{{ url_for("staff.jams.edit.ending", jam=jam.number) }}">
+ <i class="uk-icon fa-fw far fa-pencil"></i> &nbsp;Ending
+ </a>
+ {% else %}
+ <a class="uk-button uk-button-default uk-width-expand" hidden="hidden" data-jam="{{ jam.number }}" id="jam-{{ jam.number }}-button-ending" href="{{ url_for("staff.jams.edit.ending", jam=jam.number) }}">
+ <i class="uk-icon fa-fw far fa-pencil"></i> &nbsp;Ending
+ </a>
+ {% endif %}
+ </div>
+ <br/>
+ <div class="uk-button-group uk-width-1-1">
+ <a class="uk-button uk-button-secondary uk-width-expand" href="# TODO">
+ <i class="uk-icon fa-fw far fa-user"></i> &nbsp;Participants
+ </a>
+ <a class="uk-button uk-button-primary uk-width-expand" href="# TODO">
+ <i class="uk-icon fa-fw far fa-users"></i> &nbsp;Teams
+ </a>
+ </div>
+ </section>
+ {% endfor %}
+ {% endif %}
+ </div>
+
+ <div id="state-modal" class="uk-flex-top" uk-modal>
+ <div class="uk-modal-dialog">
+ <button class="uk-modal-close-default" type="button" uk-close></button>
+
+ <div class="uk-modal-header">
+ <h2 class="uk-modal-title">Set State</h2>
+ </div>
+
+ <div class="uk-modal-body">
+ <form class="uk-form-horizontal">
+ <div>
+ <div class="uk-form-label">
+ <label class="uk-form-label" for="state">State</label>
+ </div>
+
+ <div class="uk-form-controls uk-form-controls-text">
+ <select class="uk-select" name="state" id="state">
+ {% for state in states %}
+ <option value="{{ state }}">{{ state.title() }}</option>
+ {% endfor %}
+ </select>
+ </div>
+ </div>
+ </form>
+ </div>
+
+ <div class="uk-modal-footer">
+ <div class="uk-text-center">
+ <button class="uk-button uk-button-default uk-modal-close" type="button" id="state-cancel">
+ <i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Cancel
+ </button>
+ <a class="uk-button uk-button-primary" type="button" id="state-submit">
+ <i class="uk-icon fa-fw far fa-check"></i> &nbsp;Save
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <script type="application/javascript">
+ const actions = new Actions("{{ url_for("staff.jams.action") }}", "{{ csrf_token() }}");
+
+ // State modal objects
+ const state_modal = UIkit.modal(document.getElementById("state-modal"));
+ const state_input = document.getElementById("state");
+ const state_cancel = document.getElementById("state-cancel");
+ const state_submit = document.getElementById("state-submit");
+
+ state_cancel.onclick = function() {
+ state_modal.hide();
+ };
+
+ for (let button of document.getElementsByClassName("state-button")) {
+ button.onclick = function() {
+ state_modal.show();
+
+ state_submit.onclick = function() {
+ let jam = parseInt(button.getAttribute("data-jam"));
+ let state = state_input.value;
+
+ actions.set_state(jam, state, function(success, data) {
+ if (success) {
+ UIkit.notification({
+ "message": "State set successfully",
+ "status": "success",
+ "pos": "bottom-center",
+ "timeout": 5000,
+ });
+
+ for (let icon of document.getElementsByClassName("state-" + jam)) {
+ console.log(icon);
+ icon.setAttribute("hidden", "hidden");
+ }
+
+ switch (state) { // Set the state on the page too so there's no reloading
+ case "planning":
+ document.getElementById("state-" + jam + "-planning").removeAttribute("hidden");
+
+ document.getElementById("jam-" + jam + "-button-basics").removeAttribute("hidden");
+ document.getElementById("jam-" + jam + "-button-info").removeAttribute("hidden");
+ document.getElementById("jam-" + jam + "-button-ending").setAttribute("hidden", "hidden");
+ break;
+ case "announced":
+ document.getElementById("state-" + jam + "-announced").removeAttribute("hidden");
+
+ document.getElementById("jam-" + jam + "-button-basics").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-info").removeAttribute("hidden");
+ document.getElementById("jam-" + jam + "-button-ending").setAttribute("hidden", "hidden");
+ break;
+ case "running":
+ document.getElementById("state-" + jam + "-running").removeAttribute("hidden");
+
+ document.getElementById("jam-" + jam + "-button-basics").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-info").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-ending").setAttribute("hidden", "hidden");
+ break;
+ case "judging":
+ document.getElementById("state-" + jam + "-judging").removeAttribute("hidden");
+
+ document.getElementById("jam-" + jam + "-button-basics").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-info").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-ending").removeAttribute("hidden");
+ break;
+ case "finished":
+ document.getElementById("state-" + jam + "-finished").removeAttribute("hidden");
+
+ document.getElementById("jam-" + jam + "-button-basics").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-info").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-ending").setAttribute("hidden", "hidden");
+ break;
+ default:
+ document.getElementById("state-" + jam + "-unknown").removeAttribute("hidden");
+
+ document.getElementById("jam-" + jam + "-button-basics").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-info").setAttribute("hidden", "hidden");
+ document.getElementById("jam-" + jam + "-button-ending").setAttribute("hidden", "hidden");
+ break;
+ }
+
+ state_modal.hide();
+ } else {
+ console.log(data);
+ UIkit.notification({
+ "message": "Failed to set state",
+ "status": "danger",
+ "pos": "bottom-center",
+ "timeout": 5000
+ })
+ }
+ });
+ };
+ }
+ }
+ </script>
+{% endblock %} \ No newline at end of file
diff --git a/templates/staff/tables/index.html b/templates/staff/tables/index.html
index 079ed306..206234bb 100644
--- a/templates/staff/tables/index.html
+++ b/templates/staff/tables/index.html
@@ -4,7 +4,7 @@
{% block og_description %}Table management and editor{% endblock %}
{% block content %}
<div class="uk-container uk-section uk-container-small">
- <a class="uk-button uk-button-default" href="{{ url_for("staff.staff_index") }}"><i class="uk-icon fa-fw fas fa-arrow-left"></i> &nbsp;Back</a>
+ <a class="uk-button uk-button-default" href="{{ url_for("staff.index") }}"><i class="uk-icon fa-fw fas fa-arrow-left"></i> &nbsp;Back</a>
<h1 class="uk-title uk-text-center">
Table manager
</h1>