Example #1
0
def get_exam(*, exam):
    try:
        db = SafeFirestore()
        out = db.collection("exams").document(exam).get().to_dict()
        if "secret" in out and isinstance(out["secret"], bytes):
            out["secret"] = out["secret"].decode("utf-8")
        return out
    except NotFound:
        raise KeyError
Example #2
0
def set_exam(*, exam, json):
    db = SafeFirestore()
    db.collection("exams").document(exam).set(json)

    ref = db.collection("exams").document("all")
    data = ref.get().to_dict()
    if exam not in data["exam-list"]:
        data["exam-list"].append(exam)
    ref.set(data)
Example #3
0
def get_roster(*, exam, include_no_watermark=False):
    db = SafeFirestore()
    for student in (db.collection("roster").document(exam).collection(
            "deadline").stream()):
        if include_no_watermark:
            yield (
                student.id,
                student.to_dict()["deadline"],
                student.to_dict().get("no_watermark", False),
            )
        else:
            yield student.id, student.to_dict()["deadline"]
Example #4
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 == "/" or request.path == "/admin/":
            return main_html

        if request.path.endswith("is_valid"):
            return jsonify({
                "success":
                is_admin(get_email(request), request.json["course"])
            })

        if "api" in request.path:
            method = request.path.split("/")[-1]
            return handle_api_call(method, request.json)

        if not is_admin(get_email(request), request.json["course"]):
            abort(401)

        course = request.json["course"]

        if request.path.endswith("list_exams"):
            exams = db.collection("exams").document(
                "all").get().to_dict()["exam-list"]
            return jsonify(
                [exam for exam in exams if exam.startswith(course + "-")])

        if request.path.endswith("get_exam"):
            exam = request.json["exam"]
            if not exam.startswith(course):
                abort(401)
            exam_json = db.collection("exams").document(exam).get().to_dict()
            secret = exam_json.pop("secret")
            return jsonify({
                "exam": exam_json,
                "secret": secret[:-1],
            })

    except:
        return jsonify({"success": False})

    return request.path
Example #5
0
def process_ok_exam_upload(*,
                           exam,
                           data,
                           enable_clarifications=False,
                           clear=True):
    """
    data: {
        "students": [
            {
                "email": string,
                "questions": [
                    {
                        "student_question_name": string,
                        "canonical_question_name": string,
                        "start_time": int,
                        "end_time": int,
                    }
                ],
                "start_time": int,
                "end_time": int,
            }
        ]
        "questions": [
            {
                "canonical_question_name": string,
            }
        ],
    }
    """
    db = SafeFirestore()

    db.collection("exam-alerts").document(exam).set({
        "questions":
        data["questions"],
        "enable_clarifications":
        enable_clarifications
    })
    ref = db.collection("exam-alerts").document(exam).collection("students")
    if clear:
        clear_collection(db, ref)

    batch = db.batch()
    cnt = 0
    for student in data["students"]:
        doc_ref = ref.document(student["email"])
        batch.set(doc_ref, student)
        cnt += 1
        if cnt > BATCH_SIZE:
            batch.commit()
            batch = db.batch()
            cnt = 0
    batch.commit()

    ref = db.collection("exam-alerts").document("all")
    exam_list_data = ref.get().to_dict()
    if exam not in exam_list_data["exam-list"]:
        exam_list_data["exam-list"].append(exam)
    ref.set(exam_list_data)
Example #6
0
def set_roster(*, exam, roster):
    db = SafeFirestore()

    ref = db.collection("roster").document(exam).collection("deadline")

    batch = db.batch()
    cnt = 0
    for document in ref.stream():
        batch.delete(document.reference)
        cnt += 1
        if cnt > 400:
            batch.commit()
            batch = db.batch()
            cnt = 0
    batch.commit()

    batch = db.batch()
    cnt = 0
    for email, deadline in roster:
        doc_ref = ref.document(email)
        batch.set(doc_ref, {"deadline": int(deadline)})
        cnt += 1
        if cnt > 400:
            batch.commit()
            batch = db.batch()
            cnt = 0
    batch.commit()
Example #7
0
def set_roster(*, exam, roster):
    db = SafeFirestore()

    ref = db.collection("roster").document(exam).collection("deadline")
    emails = []

    batch = db.batch()
    cnt = 0
    for document in ref.stream():
        batch.delete(document.reference)
        cnt += 1
        if cnt > 400:
            batch.commit()
            batch = db.batch()
            cnt = 0
    batch.commit()

    batch = db.batch()
    cnt = 0
    for email, deadline, *rest in roster:
        assert len(rest) <= 1
        doc_ref = ref.document(email)
        batch.set(
            doc_ref,
            {
                "deadline": int(deadline),
                "no_watermark": bool(int(rest[0]) if rest else False),
            },
        )
        emails.append(email)
        cnt += 1
        if cnt > 400:
            batch.commit()
            batch = db.batch()
            cnt = 0
    batch.commit()

    ref = db.collection("roster").document(exam)
    data = {"all_students": emails}
    ref.set(data)
Example #8
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"):
            email, is_admin = get_email(request)
            all_exams = list_exams(db)
            roster_exams = get_roster_exams(email, db)
            valid = [exam for exam in all_exams if exam in roster_exams]
            return jsonify(valid)

        if request.path.endswith("watermark.svg"):
            watermark = create_watermark(
                int(request.args["seed"]),
                brightness=int(request.args["brightness"]),
            )
            return Response(watermark, mimetype="image/svg+xml")

        if request.path == "/" or request.json is None:
            return main_html

        if request.path.endswith("get_exam"):
            exam = request.json["exam"]
            email, is_admin = get_email(request)
            ref = db.collection(exam).document(email)
            try:
                answers = ref.get().to_dict() or {}
            except NotFound:
                answers = {}

            student_data = get_student_data(exam, email, db)
            deadline = student_data["deadline"]
            no_watermark = student_data.get("no_watermark", False)

            exam_data = get_exam_dict(exam, db)
            exam_data = scramble(
                email,
                exam_data,
            )

            # 120 second grace period in case of network latency or something
            if deadline + 120 < time.time() and not is_admin:
                abort(401)
                return

            return jsonify({
                "success":
                True,
                "exam":
                exam,
                "publicGroup":
                exam_data["public"],
                "privateGroups": (Fernet(exam_data["secret"]).encrypt_at_time(
                    json.dumps(exam_data["groups"]).encode("ascii"),
                    0).decode("ascii")),
                # `or None` is to handle the case of watermark={}, which is truthy in JS
                "watermark": (None if no_watermark else
                              (exam_data.get("watermark") or None)),
                "answers":
                answers,
                "deadline":
                deadline,
                "timestamp":
                time.time(),
            })

        if request.path.endswith("submit_question"):
            exam = request.json["exam"]
            question_id = request.json["id"]
            value = request.json["value"]
            sent_time = request.json.get("sentTime", 0)
            email, is_admin = get_email(request)

            if exam not in list_exams(db):
                abort(401)

            db.collection(exam).document(email).collection(
                "log").document().set({
                    "timestamp": time.time(),
                    "sentTime": sent_time,
                    question_id: value
                })

            deadline = get_deadline(exam, email, db)

            if deadline + 120 < time.time() and not is_admin:
                abort(401)
                return

            recency_ref = (db.collection(exam).document(email).collection(
                "recency").document(question_id))
            try:
                recency = recency_ref.get().to_dict() or {}
            except NotFound:
                recency = {}

            recent_time = recency.get("sentTime", -1)
            if recent_time - 300 <= sent_time <= recent_time:
                # the current request was delayed and is now out of date
                abort(409)
                return

            recency_ref.set({"sentTime": sent_time})

            db.collection(exam).document(email).set({question_id: value},
                                                    merge=True)
            return jsonify({"success": True})

        if request.path.endswith("backup_all"):
            exam = request.json["exam"]
            if exam not in list_exams(db):
                abort(401)
            email, is_admin = get_email(request)
            history = request.json["history"]
            snapshot = request.json["snapshot"]
            db.collection(exam).document(email).collection(
                "history").document().set({
                    "timestamp": time.time(),
                    "history": history,
                    "snapshot": snapshot
                })
            return jsonify({"success": True})

        if request.path.endswith("log_event"):
            exam = request.json["exam"]
            email, is_admin = get_email(request)
            if exam not in list_exams(db):
                abort(401)
            event = request.json["event"]
            db.collection(exam).document(email).collection(
                "history").document().set({
                    "timestamp": time.time(),
                    "event": event,
                })
            return jsonify({"success": True})

        if getenv("ENV") == "dev" and "alerts" in request.path:
            from alerts import index as alerts_index

            return alerts_index(request)

    except Exception as e:
        if getenv("ENV") == "dev":
            raise
        print(e)
        print(dict(request.json))
        return jsonify({"success": False})

    return request.path
Example #9
0
def get_full_logs(*, exam, email):
    db = SafeFirestore()

    for ref in db.collection(exam).document(email).collection(
            "history").stream():
        yield ref.to_dict()
Example #10
0
def get_submissions(*, exam):
    db = SafeFirestore()

    for ref in db.collection(exam).stream():
        yield ref.id, ref.to_dict()
Example #11
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 #12
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("exams").document("all").get().to_dict()
                ["exam-list"])

        if request.path == "/" or request.json is None:
            return main_html

        if request.path.endswith("get_exam"):
            exam = request.json["exam"]
            email = get_email(request)
            ref = db.collection(exam).document(email)
            try:
                answers = ref.get().to_dict() or {}
            except NotFound:
                answers = {}

            deadline = get_deadline(exam, email, db)

            exam_data = get_exam_dict(exam, db)
            exam_data = scramble(
                email,
                exam_data,
            )

            # 120 second grace period in case of network latency or something
            if deadline + 120 < time.time():
                abort(401)
                return

            return jsonify({
                "success":
                True,
                "exam":
                exam,
                "publicGroup":
                exam_data["public"],
                "privateGroups": (Fernet(exam_data["secret"]).encrypt_at_time(
                    json.dumps(exam_data["groups"]).encode("ascii"),
                    0).decode("ascii")),
                "answers":
                answers,
                "deadline":
                deadline,
                "timestamp":
                time.time(),
            })

        if request.path.endswith("submit_question"):
            exam = request.json["exam"]
            question_id = request.json["id"]
            value = request.json["value"]
            sent_time = request.json.get("sentTime", 0)
            email = get_email(request)

            db.collection(exam).document(email).collection(
                "log").document().set({
                    "timestamp": time.time(),
                    "sentTime": sent_time,
                    question_id: value
                })

            deadline = get_deadline(exam, email, db)

            if deadline + 120 < time.time():
                abort(401)
                return

            recency_ref = (db.collection(exam).document(email).collection(
                "recency").document(question_id))
            try:
                recency = recency_ref.get().to_dict() or {}
            except NotFound:
                recency = {}

            recent_time = recency.get("sentTime", -1)
            if recent_time - 300 <= sent_time <= recent_time:
                # the current request was delayed and is now out of date
                abort(409)
                return

            recency_ref.set({"sentTime": sent_time})

            db.collection(exam).document(email).set({question_id: value},
                                                    merge=True)
            return jsonify({"success": True})

        if getenv("ENV") == "dev" and "alerts" in request.path:
            from alerts import index as alerts_index

            return alerts_index(request)

    except:
        print(dict(request.json))
        return jsonify({"success": False})

    return request.path
Example #13
0
def get_roster(*, exam):
    db = SafeFirestore()
    for student in (db.collection("roster").document(exam).collection(
            "deadline").stream()):
        yield student.id, student.to_dict()["deadline"]