def check_weight(weight): if weight == -1 or (weight != 0 and weight != 100): ut.sprint( "WARNING: Invalid answer weight for multiple choice question: " + str(weight)) return False return True
def parse_multiple_choice_question(q_name, index): """ Parse multiple choice question answers. Parameters ---------- q_name: str name of the question index: int index of the question in cell array Returns ------- [obj] parsed answers obj extra attributes to add to the question """ aindex = index + 1 length_check = len(cells) <= aindex cell_type_check = cells[aindex]["metadata"]["ctype"] != "answer" if length_check or cell_type_check: raise Exception( "WARNING: multiple choice question has no answer cell.") answers, extra = parse_multiple_answers_question(q_name, index) def check_weight(weight): if weight == -1 or (weight != 0 and weight != 100): ut.sprint( "WARNING: Invalid answer weight for multiple choice question: " + str(weight)) return False return True found_true = False final_answers = [] for answer in answers: weight = answer["answer_weight"] if not check_weight(weight): ut.sprint("WARNING: invalid weight in answer list for question " + q_name) continue if weight > 0: if found_true: ut.sprint("WARNING: multiple correct answers in question " + q_name + ", consider changing to multiple answers questions") continue else: found_true = True final_answers += [answer] return final_answers, extra
def parse_question(index): """ Parse Question. Parameters ---------- index: int index of the question in cell array Returns ------- obj parsed question """ question = {} for key, val, in cells[index]["metadata"].items(): if key == "ctype": continue elif key == "quesnum": question["question_name"] = str(val) else: question[key] = val if "question_type" not in question: ut.sprint( "WARNING: question does not have question-type, not including question" ) return None if "points_possible" not in question: question["points_possible"] = 1 q_type = question["question_type"] if q_type not in supp_q_types: ut.sprint("WARNING: unsupported question type of " + q_type) return None if "warning" in supp_q_types[q_type]: ut.sprint("WARNING: " + supp_q_types[q_type]["warning"]) try: question["answers"], extra = supp_q_types[q_type]["parser"]( question["question_name"], index) except Exception as e: ut.sprint(str(e)) return None question_text, image_paths = parse_md_text(cells[index]["source"]) question["question_text"] = question_text extra["image_paths"] = image_paths question = {**question, **extra} return question
def parse_md_text(text): """ Parse markdown into HTML, also check for latex. Parameters ---------- text: str text to parse Returns ------- str parsed text with Canvas latex [str] list of paths to images linked from this text """ text = md.convert(text) html = BeautifulSoup(text, 'html.parser') image_paths = [] for image in html.find_all("img"): image_name = image.get("src") image_path = os.path.join(file_dir, image.get("src")) image_path = os.path.abspath(image_path) if os.path.islink(image_path): continue if not os.path.exists(image_path): ut.sprint( "WARNING: image was not a link and does not exist on this computer: " + image_path) image_paths += [{"name": image_name, "path": image_path}] begin = 0 while text.find("$$", begin) != -1: start = text.find("$$", begin) if start - 1 >= 0 and text[start - 1] == "\\": begin += 2 continue end = text.find("$$", start + 2) if end == -1: break latex_str = to_canvas(text[start + 2:end]) text = text.replace(text[start:end + 2], latex_str) begin += len(latex_str) begin = 0 while text.find("$", begin) != -1: start = text.find("$", begin) if start - 1 >= 0 and text[start - 1] == "\\": begin += 1 continue end = text.find("$", start + 1) if end == -1: break latex_str = to_canvas(text[start + 1:end], True) text = text.replace(text[start:end + 1], latex_str) begin += len(latex_str) return text.replace("\\", ""), image_paths
def parse_numerical_question(q_name, index): """ Parse numerical question answers. Parameters ---------- q_name: str name of the question index: int index of the question in cell array Returns ------- [obj] parsed answers obj extra attributes to add to the question """ aindex = index + 1 length_check = len(cells) <= aindex cell_type_check = cells[aindex]["metadata"]["ctype"] != "answer" if length_check or cell_type_check: raise Exception("WARNING: numerical question has no answer cell.") answers = [] for line in cells[index + 1]["source"].split("\n"): if line.startswith("*"): v1 = 0 v2 = 0 answer = {"answer_text": "", "answer_weight": 100} text = line[1:].strip(" ") if "," not in text: if ":" in text: ut.sprint( "WARNING: numerical answer has one value but has answer type, " + "only one of these is allowed") v1 = float(text) answer["numerical_answer_type"] = "exact_answer" elif ":" not in text: ind = text.index(",") v1 = float(text[:ind].strip(" ")) v2 = float(text[ind + 1:].strip(" ")) answer["numerical_answer_type"] = "exact_answer" else: ind = text.index(":") vals = text[:ind] answer["numerical_answer_type"] = text[ind + 1:].strip(" ").replace( "-", "_") ind = vals.index(",") v1 = float(vals[:ind].strip(" ")) v2 = float(vals[ind + 1:].strip(" ")) if answer["numerical_answer_type"] == "exact_answer": answer["answer_exact"] = v1 answer["answer_error_margin"] = v2 elif answer["numerical_answer_type"] == "range_answer": answer["answer_range_start"] = v1 answer["answer_range_end"] = v2 elif answer["numerical_answer_type"] == "precision_answer": answer["answer_approximate"] = v1 answer["answer_precision"] = v2 else: ut.sprint("WARNING: numerical answer type not supported: " + answer["numerical_answer_type"]) continue answers += [answer] return answers, {}
def md2canvas(url, notebook_file, token, token_file, course_id, save_settings, quiz_id, dump, no_upload, hush, reset): """ Parse file into quiz and upload to Canvas. """ # Set printing settings ut.hush = hush # Check argument validity if not notebook_file or not path.exists(notebook_file): print("Invalid notebook file.") return if not notebook_file.endswith(".md") and \ not notebook_file.endswith(".ipynb"): print("Notebook file must be Markdown or Jupyter Notebook.") return if token and token_file: print("Only one of token or token file can be used.") return if token_file and not path.exists(token_file): print("Invalid token file.") return if no_upload and not dump: print( "WARNING: not dumping to file or uploading, nothing will happen.") # Get/Set configuration if token_file: with open(token_file, mode="r") as tf: token = tf.read() config_file = path.join(path.dirname(path.realpath(__file__)), "config.yaml") if not path.exists(config_file): with open(config_file, "w") as f: f.close() reset = True with open(config_file, "r") as f: config = yaml.load(f, Loader=yaml.FullLoader) if not config: config = {} if save_settings or reset: if url: config["url"] = url elif reset: config["url"] = "https://canvas.ubc.ca" if token: config["token"] = token elif reset: config["token"] = "11224~PLAy00HrlbYVp7a6DV0a6X7pGQ13uLukhxF4ouz3JUeDJ" + \ "R9dzY0hazkcDOlUuY0t" if course_id: config["course_id"] = course_id elif reset: config["course_id"] = "51824" with open(config_file, "w") as wf: yaml.dump(config, wf) if not no_upload: if not url: url = config["url"] if not token: token = config["token"] if not course_id: course_id = str(config["course_id"]) ut.sprint("Parsing the quiz at " + notebook_file) quiz = m2j.parse_quiz(notebook_file) if dump: with open(dump, "w") as f: json.dump(f, quiz) if not no_upload: ut.sprint("Uploading quiz to Canvas with following settings:") ut.sprint(" URL = " + url) ut.sprint(" Token = " + token[:4] + (len(token) - 8) * "*" + token[-4:]) ut.sprint(" Course ID = " + course_id) if quiz_id: ut.sprint(" Quiz ID = " + quiz_id) j2c.update_quiz(cv, quiz, url, token, course_id, quiz_id) else: j2c.upload_quiz(cv, quiz, url, token, course_id)