def timeseries_grade_percentiles(c, assignment_name, num_points=40): """ Returns a timeseries of grades with percentiles. Here is an example: [["2015-07-17 19:00:36-0700", 0.0, 0.0, 0.0, ... 0.0, 0.0], ["2015-07-17 19:10:36-0700", 0.0, 0.0, 0.0, ... 1.0, 2.0], ["2015-07-17 19:20:36-0700", 0.0, 0.0, 0.0, ... 3.0, 4.0], ["2015-07-17 19:30:36-0700", 0.0, 0.0, 0.5, ... 5.0, 6.0], ["2015-07-17 19:40:36-0700", 0.0, 0.0, 1.0, ... 7.0, 8.0]] """ data_keys = range(0, 105, 5) assignment = get_assignment_by_name(assignment_name) if not assignment: return # There is a slight problem that because of DST, ordering by "started" may not always # produce the correct result. When the timezone changes, lexicographical order does not # match the actual order of the times. However, this only happens once a year in the middle # of the night, so f**k it. c.execute( """SELECT source, score, started FROM builds WHERE job = ? AND status = ? ORDER BY started""", [assignment_name, SUCCESS], ) # XXX: There is no easy way to exclude builds started by staff ("super") groups. # But because this graph is to show the general trend, it's usually fine if staff builds # are included. Plus, the graph only shows up in the admin interface anyway. builds = [(source, score, parse_time(started)) for source, score, started in c.fetchall()] if not builds: return [] source_set = map(lambda b: b[0], builds) started_time_set = map(lambda b: b[2], builds) min_started = min(started_time_set) max_started = max(started_time_set) assignment_min_started = parse_time(assignment.not_visible_before) assignment_max_started = parse_time(assignment.due_date) data_min = min(min_started, assignment_min_started) data_max = max(max_started, assignment_max_started) data_points = [] best_scores_so_far = {source: 0 for source in source_set} time_delta = (data_max - data_min) / (num_points - 1) current_time = data_min for source, score, started_time in builds: while current_time < started_time: percentiles = np.percentile(best_scores_so_far.values(), data_keys) data_points.append([format_js_compatible_time(current_time)] + list(percentiles)) current_time += time_delta if score is not None: best_scores_so_far[source] = max(score, best_scores_so_far[source]) percentiles = list(np.percentile(best_scores_so_far.values(), data_keys)) now_time = now() while current_time - (time_delta / 2) < data_max: data_points.append([format_js_compatible_time(current_time)] + percentiles) if current_time >= now_time: percentiles = [None] * len(percentiles) current_time += time_delta return data_points
def builds_one(name): with DbCursor() as c: c.execute('''SELECT build_name, status, score, source, `commit`, message, job, started, log FROM builds WHERE build_name = ? LIMIT 1''', [name]) build = c.fetchone() if not build: abort(404) build_info = build + (get_assignment_by_name(build[6]).full_score,) return render_template("ta/builds_one.html", build_info=build_info, **_template_common())
def builds_one(name): with DbCursor() as c: c.execute( '''SELECT build_name, status, score, source, `commit`, message, job, started, log FROM builds WHERE build_name = ? LIMIT 1''', [name]) build = c.fetchone() if not build: abort(404) build_info = build + (get_assignment_by_name(build[6]).full_score, ) return render_template("ta/builds_one.html", build_info=build_info, **_template_common())
def assignments_one(name, page): page_size = 50 assignment = get_assignment_by_name(name) if not assignment: abort(404) with DbCursor() as c: c.execute( '''SELECT id, name, sid, github, email, super, score, slipunits, updated FROM grades LEFT JOIN users ON grades.user = users.id WHERE assignment = ? ORDER BY super DESC, login''', [name]) grades = c.fetchall() c.execute( '''SELECT build_name, source, status, score, `commit`, message, started FROM builds WHERE job = ? ORDER BY started DESC LIMIT ? OFFSET ?''', [name, page_size + 1, (page - 1) * page_size]) builds = c.fetchall() if not builds and page > 1: abort(404) more_pages = len(builds) == page_size + 1 if more_pages: builds = builds[:-1] c.execute( '''SELECT COUNT(*), AVG(score) FROM grades WHERE assignment = ? AND score IS NOT NULL''', [name]) stats = c.fetchone() if stats[0] == 0: variance = None stddev = None else: c.execute( "SELECT AVG((score - ?) * (score - ?)) FROM grades WHERE assignment = ?", [stats[1], stats[1], name]) variance, = c.fetchone() stddev = sqrt(variance) assignment_info = ( (assignment.name, assignment.full_score, assignment.min_score, assignment.max_score, assignment.weight, assignment.due_date, assignment.category, assignment.is_group, assignment.manual_grading, assignment.not_visible_before, assignment.cannot_build_after, assignment.start_auto_building, assignment.end_auto_building) + stats + (stddev, )) return render_template("ta/assignments_one.html", grades=grades, builds=builds, assignment_info=assignment_info, page=page, more_pages=more_pages, **_template_common())
def gradeslog_one(name): with DbCursor() as c: c.execute('''SELECT gradeslog.transaction_name, gradeslog.source, users.id, users.name, users.github, users.super, gradeslog.assignment, gradeslog.score, gradeslog.slipunits, gradeslog.updated, gradeslog.description FROM gradeslog LEFT JOIN users ON gradeslog.user = users.id WHERE gradeslog.transaction_name = ? LIMIT 1''', [name]) entry = c.fetchone() assignment = get_assignment_by_name(entry[6]) full_score = assignment.full_score if assignment else 0.0 return render_template("ta/gradeslog_one.html", entry=entry, full_score=full_score, **_template_common())
def gradeslog_one(name): with DbCursor() as c: c.execute( '''SELECT gradeslog.transaction_name, gradeslog.source, users.id, users.name, users.github, users.super, gradeslog.assignment, gradeslog.score, gradeslog.slipunits, gradeslog.updated, gradeslog.description FROM gradeslog LEFT JOIN users ON gradeslog.user = users.id WHERE gradeslog.transaction_name = ? LIMIT 1''', [name]) entry = c.fetchone() assignment = get_assignment_by_name(entry[6]) full_score = assignment.full_score if assignment else 0.0 return render_template("ta/gradeslog_one.html", entry=entry, full_score=full_score, **_template_common())
def build_now(): job_name = request.form.get("f_job_name") repo = request.form.get("f_repo") assignment = get_assignment_by_name(job_name) if not assignment: abort(400) assignment = assignment.student_view(repo) if assignment.manual_grading: abort(400) with DbCursor() as c: student = _get_student(c) user_id, _, _, login, _, _ = student super_value = get_super(c, user_id) dropped = super_value is not None and super_value < 0 if assignment.is_group: repos = get_groups(c, user_id) else: repos = [login] if repo not in repos: abort(403) if now_compare(assignment.not_visible_before, add_grace_period( assignment.cannot_build_after)) != 0 or dropped: abort(400) branch_hash = get_branch_hash(repo, "master") message = None if branch_hash: message = get_commit_message(repo, branch_hash) # This section doesn't absolutely NEED to be consistent with the previous transaction, # since we have not specified any actual data constraints. The only possible logical error # would be to fulfill a request when the current user's permissions have been revoked # between these two transactions. with DbCursor() as c: build_name = create_build(c, job_name, repo, branch_hash, message) if should_limit_source(repo, job_name): rate_limit_fail_build(build_name) else: job = Job(build_name, repo, "Web interface") dockergrader_queue.enqueue(job) return redirect(url_for("dashboard.builds_one", name=build_name))
def assignments_one(name, page): page_size = 50 assignment = get_assignment_by_name(name) if not assignment: abort(404) with DbCursor() as c: c.execute('''SELECT id, name, sid, github, email, super, score, slipunits, updated FROM grades LEFT JOIN users ON grades.user = users.id WHERE assignment = ? ORDER BY super DESC, login''', [name]) grades = c.fetchall() c.execute('''SELECT build_name, source, status, score, `commit`, message, started FROM builds WHERE job = ? ORDER BY started DESC LIMIT ? OFFSET ?''', [name, page_size + 1, (page - 1) * page_size]) builds = c.fetchall() if not builds and page > 1: abort(404) more_pages = len(builds) == page_size + 1 if more_pages: builds = builds[:-1] c.execute('''SELECT COUNT(*), AVG(score) FROM grades WHERE assignment = ? AND score IS NOT NULL''', [name]) stats = c.fetchone() if stats[0] == 0: variance = None stddev = None else: c.execute("SELECT AVG((score - ?) * (score - ?)) FROM grades WHERE assignment = ?", [stats[1], stats[1], name]) variance, = c.fetchone() stddev = sqrt(variance) assignment_info = ((assignment.name, assignment.full_score, assignment.min_score, assignment.max_score, assignment.weight, assignment.due_date, assignment.category, assignment.is_group, assignment.manual_grading, assignment.not_visible_before, assignment.cannot_build_after, assignment.start_auto_building, assignment.end_auto_building) + stats + (stddev,)) return render_template("ta/assignments_one.html", grades=grades, builds=builds, assignment_info=assignment_info, page=page, more_pages=more_pages, **_template_common())
def builds_one(name): with DbCursor() as c: student = _get_student(c) user_id, _, _, login, _, _ = student group_repos = get_groups(c, user_id) repos = [login] + group_repos c.execute('''SELECT build_name, status, score, source, `commit`, message, job, started, log FROM builds WHERE build_name = ? AND source in (%s) LIMIT 1''' % (",".join(["?"] * len(repos))), [name] + repos) build = c.fetchone() if not build: abort(404) build_info = build + (get_assignment_by_name(build[6]).full_score,) template_common = _template_common(c) return render_template("dashboard/builds_one.html", build_info=build_info, **template_common)
def builds_one(name): with DbCursor() as c: student = _get_student(c) user_id, _, _, login, _, _ = student group_repos = get_groups(c, user_id) repos = [login] + group_repos c.execute( '''SELECT build_name, status, score, source, `commit`, message, job, started, log FROM builds WHERE build_name = ? AND source in (%s) LIMIT 1''' % (",".join(["?"] * len(repos))), [name] + repos) build = c.fetchone() if not build: abort(404) build_info = build + (get_assignment_by_name(build[6]).full_score, ) template_common = _template_common(c) return render_template("dashboard/builds_one.html", build_info=build_info, **template_common)
def build_now(): job_name = request.form.get("f_job_name") repo = request.form.get("f_repo") assignment = get_assignment_by_name(job_name) if not assignment: abort(400) if assignment.manual_grading: abort(400) if now_compare(assignment.not_visible_before, assignment.cannot_build_after) != 0: abort(400) with DbCursor() as c: student = _get_student(c) user_id, _, _, login, _, _ = student if assignment.is_group: repos = get_groups(c, user_id) else: repos = [login] if repo not in repos: abort(403) branch_hash = get_branch_hash(repo, "master") message = None if branch_hash: message = get_commit_message(repo, branch_hash) # This section doesn't absolutely NEED to be consistent with the previous transaction, # since we have not specified any actual data constraints. The only possible logical error # would be to fulfill a request when the current user's permissions have been revoked # between these two transactions. with DbCursor() as c: build_name = create_build(c, job_name, repo, branch_hash, message) job = Job(build_name, repo, "Web interface") dockergrader_queue.enqueue(job) return redirect(url_for("dashboard.builds_one", name=build_name))
def assignments_one(name): with DbCursor() as c: student = _get_student(c) user_id, _, _, login, _, _ = student assignment = get_assignment_by_name(name) if not assignment: abort(404) slipunits_now = slip_units_now(assignment.due_date) is_visible = now_compare(assignment.not_visible_before) >= 0 if assignment.manual_grading: can_build = False else: can_build = now_compare(assignment.cannot_build_after) <= 0 if not is_visible: abort(404) c.execute("SELECT score, slipunits, updated FROM grades WHERE user = ? AND assignment = ?", [user_id, name]) grade = c.fetchone() if not grade: grade = (None, None, None) if assignment.is_group: repos = get_groups(c, user_id) else: repos = [login] c.execute('''SELECT build_name, source, status, score, `commit`, message, started FROM builds WHERE job = ? AND source IN (%s) ORDER BY started DESC''' % (",".join(["?"] * len(repos))), [name] + repos) builds = c.fetchall() if builds: most_recent_repo = builds[0][1] else: most_recent_repo = None if grade[0] is not None: c.execute('''SELECT COUNT(*) + 1 FROM grades WHERE assignment = ? AND score > ?''', [name, grade[0]]) rank, = c.fetchone() else: rank = None c.execute('''SELECT COUNT(*), AVG(score) FROM grades WHERE assignment = ? AND score IS NOT NULL''', [name]) stats = c.fetchone() if stats[0] == 0: variance = None stddev = None else: c.execute("SELECT AVG((score - ?) * (score - ?)) FROM grades WHERE assignment = ?", [stats[1], stats[1], name]) variance, = c.fetchone() stddev = sqrt(variance) assignment_info = ((assignment.name, assignment.full_score, assignment.weight, assignment.due_date, assignment.category, assignment.is_group, assignment.manual_grading) + grade + (rank,) + stats + (stddev,)) template_common = _template_common(c) return render_template("dashboard/assignments_one.html", assignment_info=assignment_info, builds=builds, repos=repos, most_recent_repo=most_recent_repo, slipunits_now=slipunits_now, can_build=can_build, **template_common)
def grade_distribution(cls, c, assignment_name, max_bins=None): """ Returns a pre-binned histogram of grades for the assignment. Here is an example of what the data looks like: [{'x': 0, 'dx': 1, 'y': 3}, {'x': 1, 'dx': 2, 'y': 0}, {'x': 2, 'dx': 3, 'y': 0}, {'x': 3, 'dx': 4, 'y': 0}, {'x': 4, 'dx': 5, 'y': 1}, {'x': 5, 'dx': 6, 'y': 2}, {'x': 6, 'dx': 7, 'y': 5}, {'x': 7, 'dx': 8, 'y': 11}, {'x': 8, 'dx': 9, 'y': 7}, {'x': 9, 'dx': 10, 'y': 15}, {'x': 10, 'dx': 11, 'y': 35}, {'x': 11, 'dx': 12, 'y': 92}] """ assignment = get_assignment_by_name(assignment_name) if not assignment: return grade_set = cls._get_grade_set(c, assignment.name) if not grade_set: return [] grade_set_min, grade_set_max = min(grade_set), max(grade_set) assignment_min, assignment_max = assignment.min_score, assignment.full_score bin_min = int(floor(min(grade_set_min, assignment_min))) bin_max = int(ceil(max(grade_set_max, assignment_max))) bin_range = bin_max - bin_min if max_bins is None: if len(str(bin_max)) < 3: max_bins = 16 elif len(str(bin_max)) < 4: max_bins = 13 elif len(str(bin_max)) < 5: max_bins = 10 else: max_bins = 7 # The minimum number of bins for factorizable ranges. # Does not apply when range < max_bins. min_bins = max_bins / 2 # How flexible is the max_bins? We'll allow this many more bins. flexibility = 1.8 def factorize(n): for candidate in range(2, n): if n % candidate == 0: return [candidate] + factorize(n / candidate) return [n] if bin_range <= max_bins: per_bin = 1.0 num_bins = bin_range else: factorization = factorize(bin_range) per_bin = 1 for factor in factorization: if per_bin * factor * min_bins > bin_range: break else: per_bin *= factor if per_bin * max_bins > bin_range: break if per_bin * max_bins * flexibility > bin_range: num_bins = bin_range / per_bin per_bin = float(per_bin) else: # The best per_bin is too small to look good. # So just fallback to naive division method. num_bins = max_bins per_bin = float(bin_range) / max_bins boundaries = [int(round(per_bin * n)) for n in range(1, num_bins)] bin_sizes = [0] * num_bins for score in grade_set: bin_index = int(floor((score - bin_min) / per_bin)) if bin_index >= num_bins: bin_index = num_bins - 1 bin_sizes[bin_index] += 1 return [ {"x": x, "dx": dx, "y": y} for x, dx, y in zip([bin_min] + boundaries, boundaries + [bin_max], bin_sizes) ]
def _process_job(self, job): build_name = job.build_name with self.lock: self.status = build_name self.updated = now() # Mark the job as In Progress while True: try: with DbCursor() as c: c.execute('''SELECT source, `commit`, message, job, started FROM builds WHERE build_name = ? AND status = ? LIMIT 1''', [build_name, QUEUED]) row = c.fetchone() if row is None: self._log("Build %s was missing from the database. Skipping." % build_name) return source, commit, message, job_name, started = row owners = get_repo_owners(c, source) owner_emails = {owner: email for owner, (_, _, _, _, _, email) in get_users_by_ids(c, owners).items()} c.execute("UPDATE builds SET status = ?, updated = ? WHERE build_name = ?", [IN_PROGRESS, now_str(), build_name]) break except apsw.Error: self._log("Exception raised while setting status to IN_PROGRESS. Retrying...", exc=True) logging.exception("Failed to retrieve next dockergrader job") self._log("Started building %s" % build_name) try: # if the job doesn't exist for some reason, the resulting TypeError will be caught # and logged assignment = get_assignment_by_name(job_name) due_date = assignment.due_date job_handler = get_job(job_name) log, score = job_handler(source, commit) # Ignore any special encoding inside the log, and just treat it as a bytes log = buffer(log) min_score, max_score = assignment.min_score, assignment.max_score full_score = assignment.full_score if score < min_score or score > max_score: raise ValueError("A score of %s is not in the acceptable range of %f to %f" % (str(score), min_score, max_score)) except JobFailedError as e: self._log("Failed %s with JobFailedError" % build_name, exc=True) with DbCursor() as c: c.execute('''UPDATE builds SET status = ?, updated = ?, log = ? WHERE build_name = ?''', [FAILED, now_str(), str(e), build_name]) if config.mailer_enabled: try: for owner in owners: email = owner_emails.get(owner) if not email: continue subject = "%s failed to complete" % build_name send_template("build_failed", email, subject, build_name=build_name, job_name=job_name, source=source, commit=commit, message=message, error_message=str(e)) except Exception: self._log("Exception raised while reporting JobFailedError", exc=True) logging.exception("Exception raised while reporting JobFailedError") else: self._log("JobFailedError successfully reported via email") return except Exception as e: self._log("Exception raised while building %s" % build_name, exc=True) logging.exception("Internal error within build %s" % build_name) with DbCursor() as c: c.execute('''UPDATE builds SET status = ?, updated = ?, log = ? WHERE build_name = ?''', [FAILED, now_str(), "Build failed due to an internal error.", build_name]) return self._log("Autograder build %s complete (score: %s)" % (build_name, str(score))) while True: try: with DbCursor() as c: c.execute('''UPDATE builds SET status = ?, score = ?, updated = ?, log = ? WHERE build_name = ?''', [SUCCESS, score, now_str(), log, build_name]) slipunits = slip_units(due_date, started) affected_users = assign_grade_batch(c, owners, job_name, float(score), slipunits, build_name, "Automatic build.", "autograder", dont_lower=True) break except apsw.Error: self._log("Exception raised while assigning grades", exc=True) logging.exception("Failed to update build %s after build completed" % build_name) return if config.mailer_enabled: try: for owner in owners: email = owner_emails.get(owner) if not email: continue subject = "%s complete - score %s / %s" % (build_name, str(score), str(full_score)) if owner not in affected_users: subject += " (no effect on grade)" else: if slipunits == 1: subject += " (1 %s used)" % config.slip_unit_name_singular elif slipunits > 0: subject += " (%s slip %s used)" % (str(slipunits), config.slip_unit_name_plural) send_template("build_finished", email, subject, build_name=build_name, job_name=job_name, score=score, full_score=str(full_score), slipunits=slipunits, log=log, source=source, commit=commit, message=message, affected=(owner in affected_users)) except Exception: self._log("Exception raised while reporting grade", exc=True) logging.exception("Exception raised while reporting grade") else: self._log("Grade successfully reported via email")
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 timeseries_grade_percentiles(c, assignment_name, num_points=40): """ Returns a timeseries of grades with percentiles. Here is an example: [["2015-07-17 19:00:36-0700", 0.0, 0.0, 0.0, ... 0.0, 0.0], ["2015-07-17 19:10:36-0700", 0.0, 0.0, 0.0, ... 1.0, 2.0], ["2015-07-17 19:20:36-0700", 0.0, 0.0, 0.0, ... 3.0, 4.0], ["2015-07-17 19:30:36-0700", 0.0, 0.0, 0.5, ... 5.0, 6.0], ["2015-07-17 19:40:36-0700", 0.0, 0.0, 1.0, ... 7.0, 8.0]] """ data_keys = range(0, 105, 5) assignment = get_assignment_by_name(assignment_name) if not assignment: return # There is a slight problem that because of DST, ordering by "started" may not always # produce the correct result. When the timezone changes, lexicographical order does not # match the actual order of the times. However, this only happens once a year in the middle # of the night, so f**k it. c.execute( '''SELECT source, score, started FROM builds WHERE job = ? AND status = ? ORDER BY started''', [assignment_name, SUCCESS]) # XXX: There is no easy way to exclude builds started by staff ("super") groups. # But because this graph is to show the general trend, it's usually fine if staff builds # are included. Plus, the graph only shows up in the admin interface anyway. builds = [(source, score, parse_time(started)) for source, score, started in c.fetchall()] if not builds: return [] source_set = tuple(map(lambda b: b[0], builds)) started_time_set = tuple(map(lambda b: b[2], builds)) min_started = min(started_time_set) max_started = max(started_time_set) assignment_min_started = parse_time(assignment.not_visible_before) assignment_max_started = parse_time(assignment.due_date) data_min = min(min_started, assignment_min_started) data_max = max(max_started, assignment_max_started) data_points = [] best_scores_so_far = {source: 0 for source in source_set} time_delta = (data_max - data_min) / (num_points - 1) current_time = data_min for source, score, started_time in builds: while current_time < started_time: percentiles = np.percentile(tuple(best_scores_so_far.values()), data_keys) data_points.append([format_js_compatible_time(current_time)] + list(percentiles)) current_time += time_delta if score is not None: best_scores_so_far[source] = max(score, best_scores_so_far[source]) percentiles = list( np.percentile(tuple(best_scores_so_far.values()), data_keys)) now_time = now() while current_time - (time_delta / 2) < data_max: data_points.append([format_js_compatible_time(current_time)] + percentiles) if current_time >= now_time: percentiles = [None] * len(percentiles) current_time += time_delta return data_points
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 grade_distribution(cls, c, assignment_name, max_bins=None): """ Returns a pre-binned histogram of grades for the assignment. Here is an example of what the data looks like: [{'x': 0, 'dx': 1, 'y': 3}, {'x': 1, 'dx': 2, 'y': 0}, {'x': 2, 'dx': 3, 'y': 0}, {'x': 3, 'dx': 4, 'y': 0}, {'x': 4, 'dx': 5, 'y': 1}, {'x': 5, 'dx': 6, 'y': 2}, {'x': 6, 'dx': 7, 'y': 5}, {'x': 7, 'dx': 8, 'y': 11}, {'x': 8, 'dx': 9, 'y': 7}, {'x': 9, 'dx': 10, 'y': 15}, {'x': 10, 'dx': 11, 'y': 35}, {'x': 11, 'dx': 12, 'y': 92}] """ assignment = get_assignment_by_name(assignment_name) if not assignment: return grade_set = cls._get_grade_set(c, assignment.name) if not grade_set: return [] grade_set_min, grade_set_max = min(grade_set), max(grade_set) assignment_min, assignment_max = assignment.min_score, assignment.full_score bin_min = int(floor(min(grade_set_min, assignment_min))) bin_max = int(ceil(max(grade_set_max, assignment_max))) bin_range = bin_max - bin_min if max_bins is None: if len(str(bin_max)) < 3: max_bins = 16 elif len(str(bin_max)) < 4: max_bins = 13 elif len(str(bin_max)) < 5: max_bins = 10 else: max_bins = 7 # The minimum number of bins for factorizable ranges. # Does not apply when range < max_bins. min_bins = max_bins // 2 # How flexible is the max_bins? We'll allow this many more bins. flexibility = 1.8 def factorize(n): for candidate in range(2, n): if n % candidate == 0: return [candidate] + factorize(n // candidate) return [n] if bin_range <= max_bins: per_bin = 1.0 num_bins = bin_range else: factorization = factorize(bin_range) per_bin = 1 for factor in factorization: if per_bin * factor * min_bins > bin_range: break else: per_bin *= factor if per_bin * max_bins > bin_range: break if per_bin * max_bins * flexibility > bin_range: num_bins = bin_range // per_bin per_bin = float(per_bin) else: # The best per_bin is too small to look good. # So just fallback to naive division method. num_bins = max_bins per_bin = float(bin_range) / max_bins boundaries = [int(round(per_bin * n)) for n in range(1, num_bins)] bin_sizes = [0] * num_bins for score in grade_set: bin_index = int(floor((score - bin_min) / per_bin)) if bin_index >= num_bins: bin_index = num_bins - 1 bin_sizes[bin_index] += 1 return [{ "x": x, "dx": dx, "y": y } for x, dx, y in zip([bin_min] + boundaries, boundaries + [bin_max], bin_sizes)]
def assignments_one(name): with DbCursor() as c: student = _get_student(c) user_id, _, _, login, _, _ = student assignment = get_assignment_by_name(name) if not assignment: abort(404) slipunits_now = slip_units_now(assignment.due_date) is_visible = now_compare(assignment.not_visible_before) >= 0 if assignment.manual_grading: can_build = False else: can_build = now_compare(assignment.cannot_build_after) <= 0 if not is_visible: abort(404) c.execute( "SELECT score, slipunits, updated FROM grades WHERE user = ? AND assignment = ?", [user_id, name]) grade = c.fetchone() if not grade: grade = (None, None, None) if assignment.is_group: repos = get_groups(c, user_id) else: repos = [login] c.execute( '''SELECT build_name, source, status, score, `commit`, message, started FROM builds WHERE job = ? AND source IN (%s) ORDER BY started DESC''' % (",".join(["?"] * len(repos))), [name] + repos) builds = c.fetchall() if builds: most_recent_repo = builds[0][1] else: most_recent_repo = None if grade[0] is not None: c.execute( '''SELECT COUNT(*) + 1 FROM grades WHERE assignment = ? AND score > ?''', [name, grade[0]]) rank, = c.fetchone() else: rank = None c.execute( '''SELECT COUNT(*), AVG(score) FROM grades WHERE assignment = ? AND score IS NOT NULL''', [name]) stats = c.fetchone() if stats[0] == 0: variance = None stddev = None else: c.execute( "SELECT AVG((score - ?) * (score - ?)) FROM grades WHERE assignment = ?", [stats[1], stats[1], name]) variance, = c.fetchone() stddev = sqrt(variance) assignment_info = ( (assignment.name, assignment.full_score, assignment.weight, assignment.due_date, assignment.category, assignment.is_group, assignment.manual_grading) + grade + (rank, ) + stats + (stddev, )) template_common = _template_common(c) return render_template("dashboard/assignments_one.html", assignment_info=assignment_info, builds=builds, repos=repos, most_recent_repo=most_recent_repo, slipunits_now=slipunits_now, can_build=can_build, **template_common)