Esempio n. 1
0
    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
Esempio n. 2
0
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())
Esempio n. 3
0
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())
Esempio n. 4
0
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())
Esempio n. 5
0
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())
Esempio n. 6
0
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())
Esempio n. 7
0
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))
Esempio n. 8
0
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())
Esempio n. 9
0
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)
Esempio n. 10
0
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)
Esempio n. 11
0
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))
Esempio n. 12
0
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)
Esempio n. 13
0
    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)
        ]
Esempio n. 14
0
    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")
Esempio n. 15
0
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)
Esempio n. 16
0
    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
Esempio n. 17
0
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)
Esempio n. 18
0
    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)]
Esempio n. 19
0
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)