def delete_quiz_questions(qq_id): ''' Handles DELETE requests on a specific QuizQuestion ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to delete quiz questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) qq = models.QuizQuestion.query.get_or_404(qq_id) # validation - All of the quizzes using this qq_id must be HIDDEN to be able to delete for quiz in models.Quiz.query.all(): for qq in quiz.quiz_questions: if qq.id == qq_id and quiz.status != "HIDDEN": response = ({ "message": "Quiz not accessible at this time" }, 403, { "Content-Type": "application/json" }) return make_response(response) models.DB.session.delete(qq) models.DB.session.commit() response = ({ "message": "Quiz Question Deleted from database" }, 200, { "Content-Type": "application/json" }) #NOTE see previous note about using 204 vs 200 #NOTE the above is a bloody tuple, not 3 separate parameters to make_response return make_response(response)
def delete_quizzes(qid): ''' Handles DELETE requests on a specific quiz ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to delete quizzes" }, 403, { "Content-Type": "application/json" }) return make_response(response) quiz = models.Quiz.query.get_or_404(qid) # validation - quiz must be HIDDEN to be able to delete w/o affecting students who are taking it if quiz.status != "HIDDEN": response = ({ "message": "Quiz not accessible at this time" }, 403, { "Content-Type": "application/json" }) return make_response(response) models.DB.session.delete(quiz) models.DB.session.commit() response = ({ "message": "Quiz deleted from database" }, 200, { "Content-Type": "application/json" }) #NOTE see previous note about using 204 vs 200 return make_response(response)
def delete_question(question_id): ''' Delete given question. ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to delete questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) q = models.Question.query.get_or_404(question_id) # validation - All of the quizzes containing question_id must be HIDDEN to be able to update for quiz in models.Quiz.query.all(): for qq in quiz.quiz_questions: if qq.question_id == question_id and quiz.status != "HIDDEN": response = ({ "message": "Quiz not accessible at this time" }, 403, { "Content-Type": "application/json" }) return make_response(response) models.DB.session.delete(q) models.DB.session.commit() response = ({ "message": "Question Deleted from database" }, 200, { "Content-Type": "application/json" }) return make_response(response)
def put_question(question_id): ''' Update a given question. This method may be called by post_new_question if we detect a POST request coming from an HTML form that really wanted to send us a PUT. ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to modify questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) # validation - All of the quizzes containing question_id must be HIDDEN to be able to update for quiz in models.Quiz.query.all(): for qq in quiz.quiz_questions: if qq.question_id == question_id and quiz.status != "HIDDEN": response = ({ "message": "Quiz not accessible at this time" }, 403, { "Content-Type": "application/json" }) return make_response(response) #NOTE HTML5 forms can not submit a PUT (only POST), so we reject any non-json request #NOTE update to the above; we're going to sort out in the POST whether it's a PUT or not if not request.json: #abort(406, "JSON format required for request") # not acceptable title = request.form['title'] stem = request.form['stem'] answer = request.form['answer'] else: title = request.json['title'] stem = request.json['stem'] answer = request.json['answer'] # validate that all required information was sent if answer is None or stem is None or title is None: abort(400, "Unable to modify question due to missing data") # bad request q = models.Question.query.get_or_404(question_id) q.title = sanitize(title) q.stem = sanitize(stem) q.answer = sanitize(answer) models.DB.session.commit() if request.json: response = ({ "message": "Question updated in database" }, 200, { "Content-Type": "application/json" }) #NOTE should it be 204? probably but I prefer to return a message so that CURL displays something indicating that the operation succeeded return make_response(response) else: flash("Question successfully updated in database.", "shiny") return redirect(request.referrer)
def delete_distractor(distractor_id): ''' Delete given distractor. ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to delete distractors" }, 403, { "Content-Type": "application/json" }) return make_response(response) d = models.Distractor.query.get_or_404(distractor_id) # validation - All of the quizzes containing a question that distractor_id is related to must be HIDDEN to be able to update for quiz in models.Quiz.query.all(): for qq in quiz.quiz_questions: question = models.Question.query.get(qq.question_id) for d in question.distractors: if d.id == distractor_id and quiz.status != "HIDDEN": response = ({ "message": "Quiz not accessible at this time" }, 403, { "Content-Type": "application/json" }) return make_response(response) models.DB.session.delete(d) models.DB.session.commit() response = ({ "message": "Distractor deleted from database" }, 204, { "Content-Type": "application/json" }) return make_response(response)
def post_quizzes_status(qid): ''' Modifies the status of given quiz ''' if not current_user.is_instructor(): if request.json: response = ({ "message": "You are not allowed to get quiz status" }, 403, { "Content-Type": "application/json" }) return make_response(response) else: flash("You are not allowed to get quiz status", "postError") quiz = models.Quiz.query.get_or_404(qid) if not request.json: abort(406, "JSON format required for request") # not acceptable new_status = sanitize(request.json['status']) #FIXME how about check that the status is actually valid, eh? :) if (quiz.set_status(new_status)): response = ({ "message": "OK" }, 200, { "Content-Type": "application/json" }) models.DB.session.commit() else: response = ({ "message": "Unable to switch to new status" }, 400, { "Content-Type": "application/json" }) return make_response(response)
def post_new_quiz_question(): ''' Add a new QuizQuestion. ''' if not current_user.is_instructor(): if request.json: response = ({ "message": "You are not allowed to create quiz questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) else: flash("You are not allowed to create quiz questions", "postError") return redirect(request.referrer) if request.json: question_id = request.json['qid'] if question_id is None or request.json['distractors_ids'] is None: abort(400, "Unable to create new quiz question due to missing data" ) # bad request distractors_ids = [did for did in request.json['distractors_ids']] else: abort(406, "JSON format required for request") # not acceptable #TODO apply same modifications tha in post_new_question but also check # whether we currently have a form sending that data first # NOTE of particular interest is how the form will send those arrays. # that was easy in json but I vaguely remember reading some issues # with the variable number of fields and regular forms format q = models.Question.query.get_or_404(question_id) qq = models.QuizQuestion(question=q) distractors = [ models.Distractor.query.get_or_404(id) for id in distractors_ids ] #TODO verify that distractors belong to that question; that led to a funny # glitch in the deployement 2021 scripts whereby all distractors were # assigned to question #2 by the script but, since they were listed also # when creating the QuizQuestion, everything looked like it was working # just fine. Beware when your programs look like they are working... #TODO verify that distractors are also all different for d in distractors: qq.distractors.append(d) models.DB.session.add(qq) models.DB.session.commit() response = ({ "message": "Quiz Question added to database" }, 201, { "Content-Type": "application/json" }) return make_response(response)
def post_new_distractor_for_question(question_id): ''' Add a distractor to the specified question. ''' if not current_user.is_instructor(): if request.json: response = ({ "message": "You are not allowed to create distrators" }, 403, { "Content-Type": "application/json" }) return make_response(response) else: flash("You are not allowed to create distrators", "postError") return redirect(request.referrer) #TODO validation - All of the quizzes containing question_id must be HIDDEN to be able to add distractor if request.json: answer = request.json['answer'] else: #FIXME do we want to continue handling both formats? answer = request.form['answer'] #TODO detect if did was passed too; if so, then it's an update if 'did' in request.form: return put_distractor(request.form['did']) # validate that all required information was sent if answer is None: abort(400, "Unable to create new distractor due to missing data" ) # bad request q = models.Question.query.get_or_404(question_id) #NOTE same potential bug here than above, still a feature # escaped_answer = json.dumps(answer) # escapes "" used in code escaped_answer = Markup.escape(answer) # escapes HTML characters escaped_answer = sanitize(escaped_answer) q.distractors.append( models.Distractor(answer=escaped_answer, question_id=q.id)) models.DB.session.commit() if request.json: response = ({ "message": "Distractor added to Question in database" }, 201, { "Content-Type": "application/json" }) return make_response(response) else: flash("Distractor successfully added to database.", "shiny") return redirect(request.referrer)
def get_distractor(distractor_id): if not current_user.is_instructor(): response = ({ "message": "You are not allowed to view individual distractors" }, 403, { "Content-Type": "application/json" }) return make_response(response) d = models.Distractor.query.get_or_404(distractor_id) return jsonify({"answer": d.answer})
def put_distractor(distractor_id): if not current_user.is_instructor(): response = ({ "message": "You are not allowed to modify distractors" }, 403, { "Content-Type": "application/json" }) return make_response(response) # validation - All of the quizzes containing a question that distractor_id is related to must be HIDDEN to be able to update for quiz in models.Quiz.query.all(): for qq in quiz.quiz_questions: question = models.Question.query.get(qq.question_id) for d in question.distractors: if d.id == distractor_id and quiz.status != "HIDDEN": response = ({ "message": "Quiz not accessible at this time, unable to update distractors." }, 403, { "Content-Type": "application/json" }) return make_response(response) if not request.json: #TODO NOW accept form data if it's been sent from the POST handler with an ID answer = request.form['answer'] #abort(406, "JSON format required for request") # not acceptable else: answer = request.json['answer'] # validate that all required information was sent if answer is None: abort(400, "Unable to modify distractor due to missing data") # bad request d = models.Distractor.query.get_or_404(distractor_id) d.answer = sanitize(answer) models.DB.session.commit() if request.json: response = ({ "message": "Distractor updated in database" }, 200, { "Content-Type": "application/json" }) #NOTE see previous note about using 204 vs 200 return make_response(response) else: flash("Distractor successfully modified in database.", "shiny") return redirect(request.referrer)
def get_quizzes_responses(qid): ''' Returns all the data on all the attempts made so far on that quiz by students. ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to view quizzes responses" }, 403, { "Content-Type": "application/json" }) return make_response(response) attempts = models.QuizAttempt.query.filter_by(quiz_id=qid).all() return jsonify([a.dump_as_dict() for a in attempts])
def get_all_quizzes(): ''' Get us all quizzes, for debugging purposes ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to view all quizzes" }, 403, { "Content-Type": "application/json" }) return make_response(response) quizzes = models.Quiz.query.all() return jsonify([q.dump_as_dict() for q in quizzes])
def put_quiz_questions(qq_id): ''' Handles PUT requests on a specific QuizQuestion ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to modify quiz questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) qq = models.QuizQuestion.query.get_or_404(qq_id) # validation - All of the quizzes using this qq_id must be HIDDEN to be able to update for quiz in models.Quiz.query.all(): for qq in quiz.quiz_questions: if qq.id == qq_id and quiz.status != "HIDDEN": response = ({ "message": "Quiz not accessible at this time" }, 403, { "Content-Type": "application/json" }) return make_response(response) if not request.json: abort(406, "JSON format required for request") # not acceptable # validate that all required information was sent if request.json['qid'] is None or request.json['distractors_ids'] is None: abort(400, "Unable to modify quiz question due to missing data" ) # bad request question_id = request.json['qid'] distractors_ids = [did for did in request.json['distractors_ids']] qq.question = models.Question.query.get_or_404(question_id) distractors = [ models.Distractor.query.get_or_404(id) for id in distractors_ids ] qq.distractors = distractors models.DB.session.commit() response = ({ "message": "Quiz Question updated in database" }, 201, { "Content-Type": "application/json" }) return make_response(response)
def get_quizzes(qid): ''' Handles GET requests on a specific quiz ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to download individual quizzes" }, 403, { "Content-Type": "application/json" }) return make_response(response) quiz = models.Quiz.query.get_or_404(qid) return jsonify(quiz.dump_as_dict())
def get_quiz_questions(qq_id): ''' Handles GET requests on a specific QuizQuestion ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to access individual quiz questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) qq = models.QuizQuestion.query.get_or_404(qq_id) return jsonify(qq.dump_as_dict())
def get_all_quiz_questions(): ''' Get, in JSON format, all the QuizQuestions from the database. ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to view all quiz questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) all = models.QuizQuestion.query.all() result = [q.dump_as_dict() for q in all] return jsonify(result)
def get_all_questions(): ''' Get, in JSON format, all the questions from the database, including, for each, all its distractors. ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to view all questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) all_questions = [q.dump_as_dict() for q in models.Question.query.all()] return jsonify(all_questions)
def put_quizzes(qid): ''' Handles PUT requests on a specific quiz ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to modify quizzes" }, 403, { "Content-Type": "application/json" }) return make_response(response) quiz = models.Quiz.query.get_or_404(qid) # validation - quiz must be HIDDEN to be able to modify w/o affecting students who are taking it if quiz.status != "HIDDEN": response = ({ "message": "Quiz not accessible at this time" }, 403, { "Content-Type": "application/json" }) return make_response(response) if not request.json: abort(406, "JSON format required for request") # not acceptable # validate that all required information was sent if quiz.title is None or quiz.description is None or request.json[ 'questions_ids'] is None: abort(400, "Unable to modify quiz due to missing data") # bad request quiz.title = sanitize(request.json['title']) quiz.description = sanitize(request.json['description']) quiz.quiz_questions = [] for qid in request.json['questions_ids']: question = models.QuizQuestion.query.get_or_404(qid) quiz.quiz_questions.append(question) models.DB.session.commit() response = ({ "message": "Quiz updated in database" }, 200, { "Content-Type": "application/json" }) #NOTE see previous note about using 204 vs 200 return make_response(response)
def get_distractors_for_question(question_id): ''' Get all distractors for the specified question. ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to view individual distractors" }, 403, { "Content-Type": "application/json" }) return make_response(response) q = models.Question.query.get_or_404(question_id) result = [d.dump_as_dict() for d in q.distractors] return jsonify(result)
def post_new_quiz(): ''' Create a new quiz ''' if not current_user.is_instructor(): if request.json: response = ({ "message": "You are not allowed to create quizzes" }, 403, { "Content-Type": "application/json" }) return make_response(response) else: flash("You are not allowed to create quizzes", "postError") return redirect(request.referrer) title = request.json['title'] description = request.json['description'] # validate that all required information was sent if title is None or description is None: abort(400, "Unable to create new quiz due to missing data") # bad request if request.json['questions_ids'] is None: abort(400, "Unable to create new quiz due to missing data") # bad request bleached_title = sanitize(title) bleached_description = sanitize(description) q = models.Quiz(title=bleached_title, description=bleached_description) # Adding the questions, based on the questions_id that were submitted for qid in request.json['questions_ids']: question = models.QuizQuestion.query.get_or_404(qid) q.quiz_questions.append(question) models.DB.session.add(q) models.DB.session.commit() response = ({ "message": "Quiz added to database" }, 201, { "Content-Type": "application/json" }) return make_response(response)
def get_quizzes_status(qid): ''' Returns the status of a given quiz ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to get quiz status" }, 403, { "Content-Type": "application/json" }) return make_response(response) quiz = models.Quiz.query.get_or_404(qid) if not request.json: abort(406, "JSON format required for request") # not acceptable response = (f'{{ "status" : {quiz.status} }}', 403, { "Content-Type": "application/json" }) return make_response(response)
def get_question(question_id): ''' Get, in JSON format, a specified question from the database. We extend here the concept of "Question" by also including ALL its distractors. The fact that *all* of the distractors are included is the main difference between Question and QuizQuestion. While the answer and distractors are shuffled together as alternatives from which the student will have to pick, this is not intended to be presented to any student since it has ALL the distractors. It is more intended as a debugging feature. ''' if not current_user.is_instructor(): response = ({ "message": "You are not allowed to access individual questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) q = models.Question.query.get_or_404(question_id) return jsonify(q.dump_as_dict())
def post_new_question(): ''' Add a question and its answer to the database. ''' if not current_user.is_instructor(): if request.json: response = ({ "message": "You are not allowed to create questions" }, 403, { "Content-Type": "application/json" }) return make_response(response) else: flash("You are not allowed to create questions", "postError") return redirect(request.referrer) if request.json: title = request.json['title'] stem = request.json['stem'] answer = request.json['answer'] else: title = request.form['title'] stem = request.form['stem'] answer = request.form['answer'] #NOTE if the POST from the HTML form refers to an existing question, we forward it to the PUT route. # We are able to tell this is the case because the form embeds an hidden field named "qid". if 'qid' in request.form: return put_question(request.form['qid']) # validate that all required information was sent if answer is None or stem is None or title is None: abort( 400, "Unable to create new question due to missing data") # bad request #NOTE the code below is highly suspicious... why did we keep the jason.dumps lines since both use answer # instead of the 2nd line using escaped_answer? # Taking a shot at fixing this # might be why Paul reported seeing double quotes in the JSON still # UPDATE - Not a bug actually... # escaped_answer = json.dumps(answer) # escapes "" used in code escaped_answer = Markup.escape(answer) # escapes HTML characters escaped_answer = sanitize(escaped_answer) # escaped_stem = json.dumps(stem) escaped_stem = Markup.escape(stem) escaped_stem = sanitize(escaped_stem) # escaped_title = json.dumps(title) escaped_title = Markup.escape(title) escaped_title = sanitize(escaped_title) q = models.Question(title=escaped_title, stem=escaped_stem, answer=escaped_answer) models.DB.session.add(q) models.DB.session.commit() if request.json: response = ({ "message": "Question & answer added to database" }, 201, { "Content-Type": "application/json" }) return make_response(response) else: flash("Question successfully added to database.", "shiny") return redirect(request.referrer)