diff options
author | 2018-05-18 16:00:31 +0100 | |
---|---|---|
committer | 2018-05-18 16:00:31 +0100 | |
commit | e1846928439aa2a7e660d870a083872c415c274d (patch) | |
tree | e716f3466ca3914f80b2ca102d5d345658af7bc8 | |
parent | Update wiki footer in line with main site (diff) |
[Jams] Huge amount of work on code jam admin area
-rw-r--r-- | pysite/base_route.py | 13 | ||||
-rw-r--r-- | pysite/constants.py | 8 | ||||
-rw-r--r-- | pysite/rst/__init__.py | 99 | ||||
-rw-r--r-- | pysite/tables.py | 86 | ||||
-rw-r--r-- | pysite/views/staff/index.py | 4 | ||||
-rw-r--r-- | pysite/views/staff/jams/actions.py | 36 | ||||
-rw-r--r-- | pysite/views/staff/jams/create.py | 49 | ||||
-rw-r--r-- | pysite/views/staff/jams/edit_basics.py | 58 | ||||
-rw-r--r-- | pysite/views/staff/jams/edit_ending.py | 54 | ||||
-rw-r--r-- | pysite/views/staff/jams/edit_info.py | 54 | ||||
-rw-r--r-- | pysite/views/staff/jams/index.py | 15 | ||||
-rw-r--r-- | pysite/views/staff/render.py | 63 | ||||
-rw-r--r-- | pysite/views/ws/bot.py | 59 | ||||
-rw-r--r-- | static/js/fouc.js | 2 | ||||
-rw-r--r-- | static/js/jams.js | 92 | ||||
-rw-r--r-- | templates/main/base.html | 3 | ||||
-rw-r--r-- | templates/staff/index.html (renamed from templates/staff/staff.html) | 5 | ||||
-rw-r--r-- | templates/staff/jams/create.html | 76 | ||||
-rw-r--r-- | templates/staff/jams/edit_basics.html | 76 | ||||
-rw-r--r-- | templates/staff/jams/edit_ending.html | 150 | ||||
-rw-r--r-- | templates/staff/jams/edit_info.html | 177 | ||||
-rw-r--r-- | templates/staff/jams/index.html | 251 | ||||
-rw-r--r-- | templates/staff/tables/index.html | 2 |
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> ") + if title.startswith("<i"): # We've found an icon, which needs to have a space after it + title = title.replace("</i> ", "</i> ") - 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> ") + if title.startswith("<i"): # We've found an icon, which needs to have a space after it + title = title.replace("</i> ", "</i> ") - 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> Back + </a> + <button id="done" class="uk-button uk-button-primary" type="submit"> + <i class="uk-icon fa-fw far fa-check"></i> 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> Back + </a> + <button id="done" class="uk-button uk-button-primary" type="submit"> + <i class="uk-icon fa-fw far fa-check"></i> 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> Back + </a> + <button class="uk-button uk-button-secondary" type="button" id="preview"> + <i class="uk-icon fa-fw far fa-eye"></i> Preview + </button> + <button id="done" class="uk-button uk-button-primary" type="submit" disabled> + <i class="uk-icon fa-fw far fa-check"></i> 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> 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> Back + </a> + <button class="uk-button uk-button-secondary" type="button" id="preview"> + <i class="uk-icon fa-fw far fa-eye"></i> Preview + </button> + <button id="done" class="uk-button uk-button-primary" type="submit" disabled> + <i class="uk-icon fa-fw far fa-check"></i> 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> 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> 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> 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> 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> 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> 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> 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> 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> 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> Participants + </a> + <a class="uk-button uk-button-primary uk-width-expand" href="# TODO"> + <i class="uk-icon fa-fw far fa-users"></i> 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> Cancel + </button> + <a class="uk-button uk-button-primary" type="button" id="state-submit"> + <i class="uk-icon fa-fw far fa-check"></i> 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> 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> Back</a> <h1 class="uk-title uk-text-center"> Table manager </h1> |