aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--pysite/constants.py10
-rw-r--r--pysite/database.py2
-rw-r--r--pysite/tables.py6
-rw-r--r--pysite/views/staff/jams/actions.py138
-rw-r--r--static/js/jams.js81
-rw-r--r--templates/staff/jams/index.html8
6 files changed, 234 insertions, 11 deletions
diff --git a/pysite/constants.py b/pysite/constants.py
index c93a09d1..e30ed10b 100644
--- a/pysite/constants.py
+++ b/pysite/constants.py
@@ -75,6 +75,16 @@ JAM_STATES = [
"finished"
]
+JAM_QUESTION_TYPES = [
+ "email",
+ "number",
+ "radio",
+ "range",
+ "text",
+ "textarea",
+ "slider"
+]
+
# PaperTrail logging
PAPERTRAIL_ADDRESS = environ.get("PAPERTRAIL_ADDRESS") or None
PAPERTRAIL_PORT = int(environ.get("PAPERTRAIL_PORT") or 0)
diff --git a/pysite/database.py b/pysite/database.py
index 2fb10598..a8c6c559 100644
--- a/pysite/database.py
+++ b/pysite/database.py
@@ -364,7 +364,7 @@ class RethinkDB:
coerce=list
)
- def get(self, table_name: str, key: str) -> Optional[Dict[str, Any]]:
+ def get(self, table_name: str, key: Any) -> Optional[Dict[str, Any]]:
"""
Get a single document from a table by primary key
diff --git a/pysite/tables.py b/pysite/tables.py
index 8ec550fb..499172d9 100644
--- a/pysite/tables.py
+++ b/pysite/tables.py
@@ -65,8 +65,11 @@ TABLES = {
"code_jam_questions": Table( # Application form questions
primary_key="id",
keys=sorted([
+ "data", # dict
"id", # uuid
- "" # TODO
+ "optional", # bool
+ "title", # str
+ "type", # str
])
),
@@ -76,6 +79,7 @@ TABLES = {
"id", # uuid
"jam", # int
"answers", # dict {question, answer, metadata}
+ "approved" # bool
])
),
diff --git a/pysite/views/staff/jams/actions.py b/pysite/views/staff/jams/actions.py
index 93a7f2c7..9aa7e79f 100644
--- a/pysite/views/staff/jams/actions.py
+++ b/pysite/views/staff/jams/actions.py
@@ -5,23 +5,93 @@ from pysite.constants import ALL_STAFF_ROLES, ErrorCodes
from pysite.decorators import csrf, require_roles
from pysite.mixins import DBMixin
-ACTIONS = ["state"]
+GET_ACTIONS = ["questions"]
+POST_ACTIONS = ["associate_question", "disassociate_question", "questions", "state"]
KEYS = ["action"]
+QUESTION_KEYS = ["optional", "title", "type"]
+
class ActionView(APIView, DBMixin):
- path = "/jams/action/"
+ path = "/jams/action"
name = "jams.action"
+
table_name = "code_jams"
+ forms_table = "code_jam_forms"
+ questions_table = "code_jam_questions"
+
+ @csrf
+ @require_roles(*ALL_STAFF_ROLES)
+ def get(self):
+ action = request.args.get("action")
+
+ if action not in GET_ACTIONS:
+ return self.error(ErrorCodes.incorrect_parameters)
+
+ if action == "questions":
+ questions = self.db.get_all(self.questions_table)
+
+ print(questions)
+ return jsonify({"questions": questions})
@csrf
@require_roles(*ALL_STAFF_ROLES)
def post(self):
action = request.args.get("action")
- if action not in ACTIONS:
+ if action not in POST_ACTIONS:
return self.error(ErrorCodes.incorrect_parameters)
+ if action == "associate_question":
+ form = int(request.args.get("form"))
+ question = request.args.get("question")
+
+ form_obj = self.db.get(self.forms_table, form)
+
+ if not form_obj:
+ return self.error(ErrorCodes.incorrect_parameters, f"Unknown form: {form}")
+
+ question_obj = self.db.get(self.questions_table, question)
+
+ if not question_obj:
+ return self.error(ErrorCodes.incorrect_parameters, f"Unknown question: {question}")
+
+ if question_obj["id"] not in form_obj["questions"]:
+ form_obj["questions"].append(question_obj["id"])
+ self.db.insert(self.forms_table, form_obj, conflict="replace")
+
+ return jsonify({"question": question_obj})
+ else:
+ return self.error(
+ ErrorCodes.incorrect_parameters,
+ f"Question {question} already associated with form {form}"
+ )
+
+ if action == "disassociate_question":
+ form = int(request.args.get("form"))
+ question = request.args.get("question")
+
+ form_obj = self.db.get(self.forms_table, form)
+
+ if not form_obj:
+ return self.error(ErrorCodes.incorrect_parameters, f"Unknown form: {form}")
+
+ question_obj = self.db.get(self.questions_table, question)
+
+ if not question_obj:
+ return self.error(ErrorCodes.incorrect_parameters, f"Unknown question: {question}")
+
+ if question_obj["id"] in form_obj["questions"]:
+ form_obj["questions"].remove(question_obj["id"])
+ self.db.insert(self.forms_table, form_obj, conflict="replace")
+
+ return jsonify({"question": question_obj})
+ else:
+ return self.error(
+ ErrorCodes.incorrect_parameters,
+ f"Question {question} not already associated with form {form}"
+ )
+
if action == "state":
jam = int(request.args.get("jam"))
state = request.args.get("state")
@@ -34,3 +104,65 @@ class ActionView(APIView, DBMixin):
self.db.insert(self.table_name, jam_obj, conflict="update")
return jsonify({})
+
+ if action == "questions":
+ data = request.get_json(force=True)
+
+ for key in QUESTION_KEYS:
+ if key not in data:
+ return self.error(ErrorCodes.incorrect_parameters, f"Missing key: {key}")
+
+ title = data["title"]
+ optional = data["optional"]
+ question_type = data["type"]
+ question_data = data.get("data", {})
+
+ if question_type in ["number", "range", "slider"]:
+ if "max" not in question_data or "min" not in question_data:
+ return self.error(
+ ErrorCodes.incorrect_parameters, f"{question_type} questions must have both max and min values"
+ )
+
+ result = self.db.insert(
+ self.questions_table,
+ {
+ "title": title,
+ "optional": optional,
+ "type": question_type,
+ "data": {
+ "max": question_data["max"],
+ "min": question_data["min"]
+ }
+ },
+ conflict="error"
+ )
+ elif question_type == "radio":
+ if "options" not in question_data:
+ return self.error(
+ ErrorCodes.incorrect_parameters, f"{question_type} questions must have both options"
+ )
+
+ result = self.db.insert(
+ self.questions_table,
+ {
+ "title": title,
+ "optional": optional,
+ "type": question_type,
+ "data": {
+ "options": question_data["options"]
+ }
+ },
+ conflict="error"
+ )
+ else:
+ result = self.db.insert(
+ self.questions_table,
+ { # No extra data for other types of question
+ "title": title,
+ "optional": optional,
+ "type": question_type
+ },
+ conflict="error"
+ )
+
+ return jsonify({"id": result["generated_keys"][0]})
diff --git a/static/js/jams.js b/static/js/jams.js
index bdce1060..b2d5b1bd 100644
--- a/static/js/jams.js
+++ b/static/js/jams.js
@@ -49,17 +49,19 @@ class Actions {
let oReq = new XMLHttpRequest();
oReq.addEventListener("load", function() {
+ let result;
+
try {
- data = JSON.parse(this.responseText);
+ result = JSON.parse(this.responseText);
} catch (e) {
return callback(false);
}
- if ("error_code" in data) {
- return callback(false, data);
+ if ("error_code" in result) {
+ return callback(false, result);
}
- return callback(true, data);
+ return callback(true, result);
});
data["action"] = action;
@@ -72,6 +74,35 @@ class Actions {
oReq.send();
}
+ send_json(action, method, data, callback) {
+ let oReq = new XMLHttpRequest();
+
+ oReq.addEventListener("load", function() {
+ let result;
+
+ try {
+ result = JSON.parse(this.responseText);
+ } catch (e) {
+ return callback(false);
+ }
+
+ if ("error_code" in result) {
+ return callback(false, result);
+ }
+
+ return callback(true, result);
+ });
+
+ data = JSON.stringify(data);
+
+ let params = this.get_params({"action": action});
+ let url = this.url + "?" + params;
+
+ oReq.open(method, url);
+ oReq.setRequestHeader("X-CSRFToken", this.csrf_token);
+ oReq.send(data);
+ }
+
get_params(data) { // https://stackoverflow.com/a/12040639
return Object.keys(data).map(function(key) {
return [key, data[key]].map(encodeURIComponent).join("=");
@@ -89,4 +120,46 @@ class Actions {
callback
);
}
+
+ get_questions(callback) {
+ this.send(
+ "questions",
+ "GET",
+ {},
+ callback
+ );
+ }
+
+ create_question(data, callback) {
+ this.send_json(
+ "questions",
+ "POST",
+ data,
+ callback
+ )
+ }
+
+ associate_question(form, question, callback) {
+ this.send(
+ "associate_question",
+ "POST",
+ {
+ "form": form,
+ "question": question,
+ },
+ callback
+ )
+ }
+
+ disassociate_question(form, question, callback) {
+ this.send(
+ "disassociate_question",
+ "POST",
+ {
+ "form": form,
+ "question": question,
+ },
+ callback
+ )
+ }
} \ No newline at end of file
diff --git a/templates/staff/jams/index.html b/templates/staff/jams/index.html
index 0f0ce022..336addee 100644
--- a/templates/staff/jams/index.html
+++ b/templates/staff/jams/index.html
@@ -9,9 +9,9 @@
<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-default" href="{{ url_for("staff.index") }}"><i class="uk-icon fa-fw far fa-arrow-left"></i> &nbsp;Back</a>
+ <a class="uk-button uk-button-primary" href="{{ url_for("staff.jams.create") }}"><i class="uk-icon fa-fw far fa-plus"></i> &nbsp;Create</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!
@@ -102,6 +102,9 @@
</div>
<br/>
<div class="uk-button-group uk-width-1-1">
+ <a class="uk-button uk-button-danger uk-width-expand" href="{{ url_for("staff.jams.forms.view", jam=jam.number) }}">
+ <i class="uk-icon fa-fw far fa-list"></i> &nbsp;Form
+ </a>
<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>
@@ -154,6 +157,7 @@
</div>
<script type="application/javascript">
+ "use strict";
const actions = new Actions("{{ url_for("staff.jams.action") }}", "{{ csrf_token() }}");
// State modal objects