Example #1
0
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))
Example #3
0
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
Example #6
0
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})
Example #7
0
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")