aboutsummaryrefslogtreecommitdiffstats
path: root/pysite/views
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 /pysite/views
parentUpdate wiki footer in line with main site (diff)
[Jams] Huge amount of work on code jam admin area
Diffstat (limited to 'pysite/views')
-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
9 files changed, 390 insertions, 2 deletions
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))