def get_student_question_mapping(student, exam): elements = list(extract_questions(exam, include_groups=True)) for element in elements: element["id"] = element.get("id", rand_id()) # add IDs to groups elements = { element["id"]: get_name(element) for element in elements if element["type"] != "group" or not is_compressible_group(element) } old_seed = int(random.random() * 1000000) out = [{ "student_question_name": get_name(element), "canonical_question_name": elements[element["id"]], } for element in list( extract_questions(scramble(student, exam), include_groups=True))] random.seed(old_seed) return out
def find_unexpected_words(exam, logs): data = get_exam(exam=exam) exam_json = json.dumps(data) original_questions = { q["id"]: q for q in extract_questions(json.loads(exam_json)) } for i, (email, log) in enumerate(logs): all_alternatives = get_substitutions(data) scrambled_questions = { q["id"]: q for q in extract_questions(scramble( email, json.loads(exam_json), keep_data=True), nest_all=True) } flagged_questions = set() for record in log: record.pop("timestamp") question = next(iter(record.keys())) answer = next(iter(record.values())) if question not in all_alternatives or question in flagged_questions: continue student_substitutions = scrambled_questions[question][ "substitutions"] for keyword in student_substitutions: for variant in all_alternatives[question][keyword]: if variant == student_substitutions[keyword]: continue if variant in answer: # check for false positives if variant in scrambled_questions[question]["text"]: continue flagged_questions.add(question) print( "In question {}, Student {} used keyword {} for {}, when they should have used {}" .format( get_name(original_questions[question]), email, variant, keyword, student_substitutions[keyword], )) print( "\tThey wrote `{}`. Their substitutions were: {}". format(" ".join(answer.split()), student_substitutions))
def substitutions(exam, email, show_all): """ Show the substitutions a particular student received """ original_exam = get_exam(exam=exam) exam = get_exam(exam=exam) exam = scramble(email, exam, keep_data=True) question_substitutions = get_all_substitutions(original_exam, exam) questions = extract_questions(exam) for question in questions: substitutions = question_substitutions[question["id"]] if substitutions or show_all: print(get_name(question), substitutions)
def find_unexpected_words(exam, logs): data = get_exam(exam=exam) exam_json = json.dumps(data) original_questions = {q["id"]: q for q in extract_questions(json.loads(exam_json))} suspected_cheating = [] for i, (email, log) in enumerate(tqdm(logs)): all_alternatives = get_substitutions(data) scrambled_questions = { q["id"]: q for q in extract_questions( scramble(email, json.loads(exam_json), keep_data=True), nest_all=True ) } flagged_question_variants = set() for record in log: record.pop("timestamp") for question, answer in record.items(): question = question.split("|")[0] if question not in all_alternatives: continue student_substitutions = scrambled_questions[question]["substitutions"] for keyword in student_substitutions: for variant in all_alternatives[question][keyword]: if variant == student_substitutions[keyword]: continue if (question, keyword, variant) in flagged_question_variants: continue if variant in answer: # check for false positives if variant in scrambled_questions[question]["text"]: continue flagged_question_variants.add((question, keyword, variant)) suspected_cheating.append( SuspectedCheating( get_name(original_questions[question]), email, keyword, student_substitutions[keyword], variant, answer, student_substitutions, ) ) return suspected_cheating
def assemble_exam( exam: str, email: Optional[str], response: Dict[str, Union[str, List[str]]], template_questions: List[Dict], student_questions: List[Dict], name_question: str, sid_question: str, dispatch, substitute_in_question_text: bool = False, ): questions = [] exam = AssembledExam( exam=exam, email=email, name=response.get(name_question, "NO NAME"), sid=response.get(sid_question, "NO SID"), questions=questions, ) student_question_lookup = {q["id"]: q for q in student_questions} for question in template_questions: question_name = get_name(question) if substitute_in_question_text: question_text = Text( student_question_lookup.get(question["id"], question)) else: question_text = Text(question) autograde_output = ( grade( email, student_question_lookup[question["id"]], response, dispatch, ) if question["id"] in response and question["id"] in student_question_lookup else "STUDENT LEFT QUESTION BLANK" if question["id"] in student_question_lookup else "STUDENT DID NOT RECEIVE QUESTION") if question.get("type") in ["multiple_choice", "select_all"]: selected_options = response.get(question["id"], []) if question.get("type") == "multiple_choice" and not isinstance( selected_options, list): selected_options = [selected_options] available_options = [ Text(option) for option in question["options"] ] if question["id"] not in student_question_lookup: student_options = available_options else: student_options = [ option["text"] for option in sorted( student_question_lookup[question["id"]]["options"], key=lambda option: option.get("index", ""), ) ] assert len(available_options) == len(student_options) assembled_question = OptionQuestion( name=question_name, prompt=question_text, options=available_options, selected=([ option for option in available_options if option.text in selected_options ]), autograde_output=autograde_output, ) else: assembled_question = TextQuestion( name=question_name, prompt=question_text, autograde_output=autograde_output, response=response.get(question["id"], "").replace("\t", " " * 4), height=question.get("options") or 1, ) questions.append(assembled_question) return exam
def index(request): try: if getenv("ENV") == "dev": update_cache() db = SafeFirestore() if request.path.endswith("main.js"): return main_js if request.path.endswith("list_exams"): return jsonify( db.collection("exam-alerts").document("all").get().to_dict() ["exam-list"]) if request.path == "/" or request.json is None: return main_html exam = request.json["exam"] course = exam.split("-")[0] prev_latest_timestamp = float(request.json["latestTimestamp"]) def get_message(id): message = (db.collection("exam-alerts").document(exam).collection( "messages").document(id).get()) return { **message.to_dict(), "id": message.id, } student_reply = False if request.path.endswith("ask_question"): email = get_email(request) student_question_name = request.json["question"] message = request.json["message"] student_data = get_student_data(db, email, exam) if student_question_name is not None: canonical_question_name = get_canonical_question_name( student_data, student_question_name) if canonical_question_name is None: return abort(400) else: canonical_question_name = None db.collection("exam-alerts").document(exam).collection( "messages").document().set( dict( question=canonical_question_name, message=message, email=email, timestamp=time.time(), )) student_reply = True if request.path.endswith("fetch_data") or student_reply: received_audio = request.json.get("receivedAudio") email = get_email(request) exam_data = db.collection("exam-alerts").document( exam).get().to_dict() student_data = get_student_data(db, email, exam) announcements = list( db.collection("exam-alerts").document(exam).collection( "announcements").stream()) messages = [{ **message.to_dict(), "id": message.id } for message in ( db.collection("exam-alerts").document(exam).collection( "messages").where("timestamp", ">", prev_latest_timestamp ).where("email", "==", email).stream()) if message.to_dict()["email"] == email] messages, latest_timestamp = group_messages(messages, get_message) messages = messages[email] latest_timestamp = max(latest_timestamp, prev_latest_timestamp) for message in messages: if message["question"] is not None: message["question"] = get_student_question_name( student_data, message["question"]) return jsonify({ "success": True, "exam_type": "ok-exam", "enableClarifications": exam_data.get("enable_clarifications", False), "startTime": student_data["start_time"], "endTime": student_data["end_time"], "timestamp": time.time(), "questions": [ question["student_question_name"] for question in student_data["questions"] ] if time.time() > student_data["start_time"] else [], "announcements": get_announcements( student_data, announcements, messages, received_audio, lambda x: (db.collection("exam-alerts").document( exam).collection("announcement_audio").document(x).get( ).to_dict() or {}).get("audio"), ), "messages": sorted( [{ "id": message["id"], "message": message["message"], "timestamp": message["timestamp"], "question": message["question"] or "Overall Exam", "responses": message["responses"], } for message in messages], key=lambda message: message["timestamp"], reverse=True, ), "latestTimestamp": latest_timestamp, }) # only staff endpoints from here onwards email = (get_email_from_secret(request.json["secret"]) if "secret" in request.json else get_email(request)) if not is_admin(email, course): abort(401) if request.path.endswith("fetch_staff_data"): pass elif request.path.endswith("add_announcement"): announcement = request.json["announcement"] announcement["timestamp"] = time.time() ref = (db.collection("exam-alerts").document(exam).collection( "announcements").document()) ref.set(announcement) spoken_message = announcement.get("spoken_message", announcement["message"]) if spoken_message: audio = generate_audio(spoken_message) db.collection("exam-alerts").document(exam).collection( "announcement_audio").document(ref.id).set( {"audio": audio}) elif request.path.endswith("clear_announcements"): clear_collection( db, db.collection("exam-alerts").document(exam).collection( "announcements"), ) clear_collection( db, db.collection("exam-alerts").document(exam).collection( "announcement_audio"), ) elif request.path.endswith("delete_announcement"): target = request.json["id"] db.collection("exam-alerts").document(exam).collection( "announcements").document(target).delete() elif request.path.endswith("send_response"): message_id = request.json["id"] reply = request.json["reply"] message = (db.collection("exam-alerts").document(exam).collection( "messages").document(message_id).get()) ref = (db.collection("exam-alerts").document(exam).collection( "messages").document()) ref.set({ "timestamp": time.time(), "email": message.to_dict()["email"], "reply_to": message.id, "message": reply, }) audio = generate_audio( reply, prefix="A staff member sent the following reply: ") db.collection("exam-alerts").document(exam).collection( "announcement_audio").document(ref.id).set({"audio": audio}) elif request.path.endswith("get_question"): question_title = request.json["id"] student = request.json["student"] student_data = get_student_data(db, student, exam) question_title = get_student_question_name(student_data, question_title) exam = db.collection("exams").document(exam).get().to_dict() questions = extract_questions(scramble(student, exam), include_groups=True) for question in questions: if get_name(question).strip() == question_title.strip(): return jsonify({"success": True, "question": question}) abort(400) else: abort(404) # (almost) all staff endpoints return an updated state exam_data = db.collection("exam-alerts").document(exam).get().to_dict() announcements = sorted( ({ "id": announcement.id, **announcement.to_dict() } for announcement in db.collection("exam-alerts").document( exam).collection("announcements").stream()), key=lambda announcement: announcement["timestamp"], reverse=True, ) grouped_messages, latest_timestamp = group_messages( [{ **message.to_dict(), "id": message.id } for message in db.collection( "exam-alerts").document(exam).collection("messages").where( "timestamp", ">", prev_latest_timestamp).stream()], get_message, ) latest_timestamp = max(prev_latest_timestamp, latest_timestamp) messages = sorted( [{ "email": email, **message } for email, messages in grouped_messages.items() for message in messages], key=lambda message: ( len(message["responses"]) > 0, -message["timestamp"], message["email"], ), ) return jsonify({ "success": True, "exam": exam_data, "announcements": announcements, "messages": messages, "latestTimestamp": latest_timestamp, }) except Exception as e: if getenv("ENV") == "dev": raise print(e) print(dict(request.json)) return jsonify({"success": False})
def deploy(exam, json, roster, start_time, enable_clarifications): """ Deploy an exam to the website. You must specify an exam JSON and associated roster CSV. You can deploy the JSON multiple times and the password will remain unchanged. """ json = json.read() roster = csv.reader(roster, delimiter=",") exam_content = loads(json) exam_content["default_deadline"] = 0 exam_content["secret"] = Fernet.generate_key().decode("utf-8") try: exam_content["secret"] = get_exam(exam=exam)["secret"] except: pass set_exam(exam=exam, json=exam_content) next(roster) # ditch headers roster = list(roster) set_roster(exam=exam, roster=roster) print("Exam uploaded with password:"******"secret"][:-1]) print("Exam deployed to https://exam.cs61a.org/{}".format(exam)) print("Initializing announcements...") elements = list(extract_questions(exam_content, include_groups=True)) for element in elements: element["id"] = element.get("id", rand_id()) # add IDs to groups elements = { element["id"]: get_name(element) for element in elements if element["type"] != "group" or not is_compressible_group(element) } students = [{ "email": email, "start_time": start_time, "end_time": int(deadline), } for email, deadline in roster] print("Updating announcements roster with {} students...".format( len(students))) process_ok_exam_upload( exam=exam, data={ "students": students, "questions": [{ "canonical_question_name": name } for name in elements.values()], }, clear=True, enable_clarifications=enable_clarifications, ) print("Announcements deployed to https://announcements.cs61a.org")