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
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)
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"]
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
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)
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()
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)
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
def get_full_logs(*, exam, email): db = SafeFirestore() for ref in db.collection(exam).document(email).collection( "history").stream(): yield ref.to_dict()
def get_submissions(*, exam): db = SafeFirestore() for ref in db.collection(exam).stream(): yield ref.id, ref.to_dict()
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 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
def get_roster(*, exam): db = SafeFirestore() for student in (db.collection("roster").document(exam).collection( "deadline").stream()): yield student.id, student.to_dict()["deadline"]