diff options
-rw-r--r-- | pysite/constants.py | 10 | ||||
-rw-r--r-- | pysite/database.py | 2 | ||||
-rw-r--r-- | pysite/tables.py | 6 | ||||
-rw-r--r-- | pysite/views/staff/jams/actions.py | 138 | ||||
-rw-r--r-- | static/js/jams.js | 81 | ||||
-rw-r--r-- | templates/staff/jams/index.html | 8 |
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> 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> 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> 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> Form + </a> <a class="uk-button uk-button-secondary uk-width-expand" href="# TODO"> <i class="uk-icon fa-fw far fa-user"></i> 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 |