def _get_next_step(current_step): # I know this kind of logic requires O(2^N) different cases, but right now there's only 1 config # option that affects this list (it's the student photos enable/disable), so it's simplest to # express the 2 alternatives this way. # # When we add more features to the onboarding process, you can come up with a better, # generalized way of determining the onboarding steps. if config.student_photos_enabled: steps = ["onboarding.student_id", "onboarding.photo"] else: steps = ["onboarding.student_id"] if current_step is None: return steps[0] step_i = steps.index(current_step) if step_i == len(steps) - 1: # This is the final step of onboarding. # Authenticate the user and redirect them to the dashboard. with DbCursor() as c: user = get_user_by_github(c, github_username()) assert user user_id_, _, _, _, _, _ = user authenticate_as_user(user_id_) return "onboarding.welcome" else: return steps[step_i + 1]
def photo(): github = github_username() if request.method == "GET": return render_template("onboarding/photo.html", github=github) try: with DbCursor() as c: user = get_user_by_github(c, github) user_id, _, _, _, _, _ = user photo_base64 = request.form.get("f_photo_cropped") photo_prefix = "data:image/jpeg;base64," if not photo_base64: fail_validation( "No photo submitted. Please choose a photo.") if not photo_base64.startswith(photo_prefix): fail_validation( "Unrecognized photo format. (Potential autograder bug?)" ) photo_binary = buffer( binascii.a2b_base64(photo_base64[len(photo_prefix):])) if len(photo_binary) > 2**21: fail_validation( "Photo exceeds maximum allowed size (2MiB).") c.execute("UPDATE users SET photo = ? WHERE id = ?", [photo_binary, user_id]) return redirect(url_for(_get_next_step("onboarding.photo"))) except ValidationError as e: return redirect_with_error(url_for("onboarding.photo"), e)
def student_id(): github = github_username() if request.method == "GET": return render_template("onboarding/student_id.html", github=github) try: mailer_job = None github_job = None with DbCursor() as c: user = get_user_by_github(c, github) if user: return redirect(url_for("dashboard.index")) student_id = request.form.get("f_student_id") if not student_id: fail_validation("Student ID is required") user = get_user_by_student_id(c, student_id) if not user: fail_validation("Student not found with that student ID") user_id, name, _, login, old_github, email = user if old_github: fail_validation( "Another GitHub account has been associated with that student " "ID already.") if not name: fail_validation( "There is no name associated with this account. (Contact your TA?)" ) if not login: fail_validation( "There is no login associated with this account. (Contact your " "TA?)") if not email: fail_validation( "There is no email associated with this account. (Contact your " "TA?)") c.execute('''UPDATE users SET github = ? WHERE sid = ?''', [github, student_id]) if not config.github_read_only_mode: github_job = repomanager_queue.create(c, "assign_repo", (login, [github])) if config.mailer_enabled: if config.inst_account_enabled: attachments = [("pdf", get_inst_account_form_path(login))] else: attachments = [] email_payload = create_email( "onboarding_confirm", email, "%s Autograder Registration" % config.course_number, _attachments=attachments, name=name, login=login, inst_account_enabled=config.inst_account_enabled) mailer_job = mailer_queue.create(c, "send", email_payload) if config.mailer_enabled and mailer_job: mailer_queue.enqueue(mailer_job) if not config.github_read_only_mode and github_job: repomanager_queue.enqueue(github_job) return redirect(url_for(_get_next_step("onboarding.student_id"))) except ValidationError as e: return redirect_with_error(url_for("onboarding.student_id"), e)
def welcome(): github = github_username() with DbCursor() as c: user = get_user_by_id(c, user_id()) return render_template("onboarding/welcome.html", github=github, user=user, inst_account_enabled=config.inst_account_enabled)
def log_out(): if request.method == "POST": authenticate_as_user(None) authenticate_as_github_username(None) return redirect(url_for("onboarding.log_in")) elif github_username() or user_id(): return render_template("onboarding/log_out.html") else: return redirect(url_for("onboarding.log_in"))
def student_id(): github = github_username() if request.method == "GET": return render_template("onboarding/student_id.html", github=github) try: mailer_job = None github_job = None with DbCursor() as c: user = get_user_by_github(c, github) if user: return redirect(url_for("dashboard.index")) student_id = request.form.get("f_student_id") if not student_id: fail_validation("Student ID is required") user = get_user_by_student_id(c, student_id) if not user: fail_validation("Student not found with that student ID") user_id, name, _, login, old_github, email = user if old_github: fail_validation("Another GitHub account has been associated with that student " "ID already.") if not name: fail_validation("There is no name associated with this account. (Contact your TA?)") if not login: fail_validation("There is no login associated with this account. (Contact your " "TA?)") if not email: fail_validation("There is no email associated with this account. (Contact your " "TA?)") c.execute('''UPDATE users SET github = ? WHERE sid = ?''', [github, student_id]) if not config.github_read_only_mode: github_job = repomanager_queue.create(c, "assign_repo", (login, [github])) if config.mailer_enabled: if config.inst_account_enabled: attachments = [("pdf", get_inst_account_form_path(login))] else: attachments = [] email_payload = create_email("onboarding_confirm", email, "%s Autograder Registration" % config.course_number, _attachments=attachments, name=name, login=login, inst_account_enabled=config.inst_account_enabled) mailer_job = mailer_queue.create(c, "send", email_payload) if config.mailer_enabled and mailer_job: mailer_queue.enqueue(mailer_job) if not config.github_read_only_mode and github_job: repomanager_queue.enqueue(github_job) return redirect(url_for(_get_next_step("onboarding.student_id"))) except ValidationError as e: return redirect_with_error(url_for("onboarding.student_id"), e)
def _get_current_step(): if is_ta(): return "ta.index" if user_id(): return "dashboard.index" github = github_username() if not github: return "onboarding.log_in" with DbCursor() as c: user = get_user_by_github(c, github) if not user: return "onboarding.student_id" user_id_, _, _, _, _, _ = user if config.student_photos_enabled: photo = get_photo(c, user_id_) if not photo: return "onboarding.photo" authenticate_as_user(user_id_) return "dashboard.index"
def photo(): github = github_username() if request.method == "GET": return render_template("onboarding/photo.html", github=github) try: with DbCursor() as c: user = get_user_by_github(c, github) user_id, _, _, _, _, _ = user photo_base64 = request.form.get("f_photo_cropped") photo_prefix = "data:image/jpeg;base64," if not photo_base64: fail_validation("No photo submitted. Please choose a photo.") if not photo_base64.startswith(photo_prefix): fail_validation("Unrecognized photo format. (Potential autograder bug?)") photo_binary = buffer(binascii.a2b_base64(photo_base64[len(photo_prefix):])) if len(photo_binary) > 2**21: fail_validation("Photo exceeds maximum allowed size (2MiB).") c.execute("UPDATE users SET photo = ? WHERE id = ?", [photo_binary, user_id]) return redirect(url_for(_get_next_step("onboarding.photo"))) except ValidationError as e: return redirect_with_error(url_for("onboarding.photo"), e)
def _template_common(): return { "github_username": github_username(), "groups_enabled": config.groups_enabled }
def enter_grades_confirm(): try: f_step = request.form.get("f_step") if f_step not in ("1", "2"): fail_validation("Enum out of range (probably a programming error)") step = int(f_step) assignment_name = request.form.get("f_assignment") if not assignment_name: fail_validation("Assignment name is required") assignment = get_assignment_by_name(assignment_name) if assignment is None: fail_validation("Assignment not found: %s" % assignment_name) min_score, max_score = assignment.min_score, assignment.max_score description = request.form.get("f_description") if not description: fail_validation("Transaction description is required") transaction_source = github_username() entries = [] user_id_set = set() with DbCursor() as c: valid_identifiers, ambiguous_identifiers = get_valid_ambiguous_identifiers( c) def try_add(f_student, f_score, f_slipunits): if not any((f_student, f_score, f_slipunits)): return elif not f_student and (f_score or f_slipunits): fail_validation( "Expected student SID, login, or name, but none provided" ) elif f_student in ambiguous_identifiers: fail_validation( "The identifier '%s' is ambiguous. Please use another." % f_student) else: if step == 1: students = get_users_by_identifier(c, f_student) elif step == 2: student = get_user_by_id(c, f_student) # Let the usual error handling take care of this case students = [student] if student else [] if not students: fail_validation("Student or group not found: %s" % f_student) for student in students: user_id, student_name, _, _, _, _ = student if user_id in user_id_set: fail_validation( "Student was listed more than once: %s" % student_name) try: score = float_or_none(f_score) except ValueError: fail_validation("Not a valid score: %s" % f_score) try: slipunits = int_or_none(f_slipunits) except ValueError: fail_validation( "Slip %s amount not valid: %s" % (slip_unit_name_plural, f_slipunits)) if slipunits is not None and slipunits < 0: fail_validation("Slip %s cannot be negative" % slip_unit_name_plural) if score is not None and not min_score <= score <= max_score: fail_validation( "Score is out of allowed range: %s (Range: %s to %s)" % (f_score, str(min_score), str(max_score))) entries.append([user_id, score, slipunits]) user_id_set.add(user_id) if step == 1: f_students = request.form.getlist("f_student") f_scores = request.form.getlist("f_score") f_slipunitss = request.form.getlist("f_slipunits") if not same_length(f_students, f_scores, f_slipunitss): fail_validation( "Different numbers of students, scores, and slip %s " + "reported. Browser bug?" % slip_unit_name_plural) for f_student, f_score, f_slipunits in zip( f_students, f_scores, f_slipunitss): try_add(f_student, f_score, f_slipunits) f_csv = request.form.get("f_csv", "") for row in csv.reader(StringIO.StringIO(f_csv), delimiter=",", quotechar='"'): if len(row) != 3: fail_validation("CSV rows must contain 3 entries") try_add(*row) if not entries: fail_validation("No grade or slip %s changes entered" % slip_unit_name_plural) if step == 1: c.execute( '''SELECT id, name, sid, login, github FROM users WHERE id IN (%s)''' % (",".join(["?"] * len(entries))), [user_id for user_id, _, _ in entries]) students = c.fetchall() details_user = {} for user_id, name, sid, login, github in students: details_user[user_id] = [name, sid, login, github] c.execute( '''SELECT user, score, slipunits, updated FROM grades WHERE assignment = ? AND user IN (%s)''' % (",".join(["?"] * len(entries))), [assignment.name] + [user_id for user_id, _, _ in entries]) grades = c.fetchall() details_grade = {} for user_id, score, slipunits, updated in grades: details_grade[user_id] = [score, slipunits, updated] entries_details = [] for entry in entries: user_id = entry[0] entry_details = (entry + details_user.get(user_id, [None] * 4) + details_grade.get(user_id, [None] * 3)) entries_details.append(entry_details) elif step == 2: transaction_number = get_next_autoincrementing_value( c, "enter_grades_last_transaction_number") transaction_name = "enter-grades-%s" % transaction_number for user_id, score, slipunits in entries: assign_grade_batch(c, [user_id], assignment.name, score, slipunits, transaction_name, description, transaction_source, manual=True, dont_lower=False) if step == 1: entries_csv = StringIO.StringIO() entries_csv_writer = csv.writer(entries_csv, delimiter=",", quotechar='"') for entry in entries: entries_csv_writer.writerow(entry) return render_template("ta/enter_grades_confirm.html", entries_details=entries_details, entries_csv=entries_csv.getvalue(), assignment_name=assignment.name, description=description, full_score=assignment.full_score, **_template_common()) elif step == 2: if len(entries) == 1: flash("1 grade committed", "success") else: flash("%d grades committed" % len(entries), "success") return redirect(url_for("ta.enter_grades")) except ValidationError as e: return redirect_with_error(url_for("ta.enter_grades"), e)
def _template_common(): return {"github_username": github_username(), "groups_enabled": config.groups_enabled}
def enter_grades_confirm(): try: f_step = request.form.get("f_step") if f_step not in ("1", "2"): fail_validation("Enum out of range (probably a programming error)") step = int(f_step) assignment_name = request.form.get("f_assignment") if not assignment_name: fail_validation("Assignment name is required") assignment = get_assignment_by_name(assignment_name) if assignment is None: fail_validation("Assignment not found: %s" % assignment_name) min_score, max_score = assignment.min_score, assignment.max_score description = request.form.get("f_description") if not description: fail_validation("Transaction description is required") transaction_source = github_username() entries = [] user_id_set = set() with DbCursor() as c: valid_identifiers, ambiguous_identifiers = get_valid_ambiguous_identifiers(c) def try_add(f_student, f_score, f_slipunits): if not any((f_student, f_score, f_slipunits)): return elif not f_student and (f_score or f_slipunits): fail_validation("Expected student SID, login, or name, but none provided") elif f_student in ambiguous_identifiers: fail_validation("The identifier '%s' is ambiguous. Please use another." % f_student) else: if step == 1: students = get_users_by_identifier(c, f_student) elif step == 2: student = get_user_by_id(c, f_student) # Let the usual error handling take care of this case students = [student] if student else [] if not students: fail_validation("Student or group not found: %s" % f_student) for student in students: user_id, student_name, _, _, _, _ = student if user_id in user_id_set: fail_validation("Student was listed more than once: %s" % student_name) try: score = float_or_none(f_score) except ValueError: fail_validation("Not a valid score: %s" % f_score) try: slipunits = int_or_none(f_slipunits) except ValueError: fail_validation("Slip %s amount not valid: %s" % (slip_unit_name_plural, f_slipunits)) if slipunits is not None and slipunits < 0: fail_validation("Slip %s cannot be negative" % slip_unit_name_plural) if score is not None and not min_score <= score <= max_score: fail_validation("Score is out of allowed range: %s (Range: %s to %s)" % (f_score, str(min_score), str(max_score))) entries.append([user_id, score, slipunits]) user_id_set.add(user_id) if step == 1: f_students = request.form.getlist("f_student") f_scores = request.form.getlist("f_score") f_slipunitss = request.form.getlist("f_slipunits") if not same_length(f_students, f_scores, f_slipunitss): fail_validation("Different numbers of students, scores, and slip %s " + "reported. Browser bug?" % slip_unit_name_plural) for f_student, f_score, f_slipunits in zip(f_students, f_scores, f_slipunitss): try_add(f_student, f_score, f_slipunits) f_csv = request.form.get("f_csv", "") for row in csv.reader(StringIO.StringIO(f_csv), delimiter=",", quotechar='"'): if len(row) != 3: fail_validation("CSV rows must contain 3 entries") try_add(*row) if not entries: fail_validation("No grade or slip %s changes entered" % slip_unit_name_plural) if step == 1: c.execute('''SELECT id, name, sid, login, github FROM users WHERE id IN (%s)''' % (",".join(["?"] * len(entries))), [user_id for user_id, _, _ in entries]) students = c.fetchall() details_user = {} for user_id, name, sid, login, github in students: details_user[user_id] = [name, sid, login, github] c.execute('''SELECT user, score, slipunits, updated FROM grades WHERE assignment = ? AND user IN (%s)''' % (",".join(["?"] * len(entries))), [assignment.name] + [user_id for user_id, _, _ in entries]) grades = c.fetchall() details_grade = {} for user_id, score, slipunits, updated in grades: details_grade[user_id] = [score, slipunits, updated] entries_details = [] for entry in entries: user_id = entry[0] entry_details = (entry + details_user.get(user_id, [None] * 4) + details_grade.get(user_id, [None] * 3)) entries_details.append(entry_details) elif step == 2: transaction_number = get_next_autoincrementing_value( c, "enter_grades_last_transaction_number") transaction_name = "enter-grades-%s" % transaction_number for user_id, score, slipunits in entries: assign_grade_batch(c, [user_id], assignment.name, score, slipunits, transaction_name, description, transaction_source, manual=True, dont_lower=False) if step == 1: entries_csv = StringIO.StringIO() entries_csv_writer = csv.writer(entries_csv, delimiter=",", quotechar='"') for entry in entries: entries_csv_writer.writerow(entry) return render_template("ta/enter_grades_confirm.html", entries_details=entries_details, entries_csv=entries_csv.getvalue(), assignment_name=assignment.name, description=description, full_score=assignment.full_score, **_template_common()) elif step == 2: if len(entries) == 1: flash("1 grade committed", "success") else: flash("%d grades committed" % len(entries), "success") return redirect(url_for("ta.enter_grades")) except ValidationError as e: return redirect_with_error(url_for("ta.enter_grades"), e)