Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
09b0d73
Render the peer evaluation rubric from a single source
novo52 Aug 17, 2026
a938087
Add the Gazprea peer evaluation page and rubric chart
novo52 Aug 17, 2026
29fdaf3
Add the lab exam page
novo52 Aug 17, 2026
5e9f125
Ground the lab exam page in the confirmed room, schedule, and access …
novo52 Aug 17, 2026
b3d7c26
Describe the exam session as the monitor and Classroom 50 actually im…
novo52 Aug 17, 2026
e8b4592
State the closed-internet rule and the git exception it needs
novo52 Aug 17, 2026
487c49c
State the exam as open-computer and closed-internet, and trim what th…
novo52 Aug 17, 2026
f63a484
Describe grading by hidden test suite, mutant comparison, and written…
novo52 Aug 17, 2026
d25e057
Add an exam vehicles page explaining Sweep and the three littleCs
novo52 Aug 17, 2026
b943852
Removed mistakenly committed files
novo52 Aug 18, 2026
2d4e07d
Fixed wording
novo52 Aug 18, 2026
03651f5
Merge remote-tracking branch 'origin/master' into info/lab-exam
novo52 Aug 18, 2026
d9a2c70
Resolve the lab exam cross-references through Sphinx instead of raw H…
novo52 Aug 18, 2026
7e8430b
Three peer evaluations, with pairings and rooms published on Canvas
novo52 Aug 19, 2026
3f55c3f
Generate the peer evaluation quiz from the rubric
novo52 Aug 19, 2026
e6f21be
Emphasise live code navigation in the peer evaluation Q&A
novo52 Aug 19, 2026
22e1f1d
Revise the lab exam and peer evaluation pages from review
novo52 Aug 24, 2026
078240e
Share the text2qti machinery between the peer evaluation forms
novo52 Aug 26, 2026
ca020b8
Add the session feedback form the evaluated team fills out
novo52 Aug 26, 2026
3a5664a
Drop the rationale for grading how a session was run
novo52 Aug 26, 2026
1871f66
Add the evaluator cheat sheet and the session tracking sheet
novo52 Aug 26, 2026
d8f1093
State what each peer evaluation form is a condition of
novo52 Aug 26, 2026
2ade279
Match the generated quiz titles to Canvas and record the settings an …
novo52 Aug 26, 2026
5b30db2
Give the whole class the 2.5x universal time multiplier
novo52 Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
271 changes: 271 additions & 0 deletions info/_ext/rubric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
"""Directives that render the peer evaluation rubric from ``rubric_data.py``.

Each directive emits a reStructuredText ``list-table`` and hands it back to the
parser, so the output matches tables written by hand elsewhere in the docs.

- ``rubric-weights`` objective, scope and weight for all six objectives
- ``rubric-levels`` what each of the four performance levels means
- ``rubric-anchors`` the mark a given set of rubric placements is worth
- ``rubric-objective`` one objective: its lead-in and its four descriptors
- ``rubric-objectives`` every objective of a given scope, in order
- ``rubric-chart`` all objectives against all levels, as one wide grid
- ``rubric-coverage`` the areas an evaluator must ask about, as a bullet list
- ``rubric-required-questions`` the three questions every member is asked
- ``rubric-objective-titles`` every objective by title and scope, without descriptors
- ``rubric-session-expectations`` what the evaluated team judges its evaluators against
- ``rubric-session-anchors`` the mark a given quality of session is worth
- ``rubric-follow-ups`` follow-up question templates, by purpose
- ``rubric-etiquette`` how to ask, as a bullet list
- ``rubric-clock`` where the session should be at a given time
- ``rubric-tracking`` a blank grid, one row per evaluated member
- ``rubric-coverage-tracking`` the coverage areas with room to record who answered

The pages that are reference sheets rather than pages to read through get their
own stylesheets, attached here so that the rest of the site keeps the theme's
usual layout.
"""
Comment on lines +1 to +26

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there places in the doc where we rewrite these tables manually? Can we refactor the docs to have a single source of truth for these rubrics and point explicitly to the course info page?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had no luck with Canvas Rubriks :-( Consequently, I've been moving towards this document as the source of truth. If we want to post it (or link to it?) in other parts of the course description, what is the best format? PDF? HTML?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rcunrau The chart in the docs is intended to be printable to pdf or render nicely in a browser for student reference during evaluations. We can link to it.


from docutils import nodes
from docutils.parsers.rst import Directive, directives
from docutils.statemachine import StringList

import rubric_data


def _list_table(rows, widths):
"""Render ``rows`` as a header-row list-table, one line per cell."""
lines = [".. list-table::", " :header-rows: 1", " :widths: " + " ".join(str(w) for w in widths), ""]
for row in rows:
for index, cell in enumerate(row):
lines.append((" * - " if index == 0 else " - ") + cell)
lines.append("")
return lines


def _bullets(items):
return ["* " + item for item in items] + [""]


def _numbered(items):
return ["{}. {}".format(number, item) for number, item in enumerate(items, start=1)] + [""]


def _objective(objective_id):
for objective in rubric_data.OBJECTIVES:
if objective["id"] == objective_id:
return objective
raise KeyError(objective_id)


class _RubricDirective(Directive):
"""Parses the lines from :meth:`lines` in the context of the calling page."""

has_content = False

#: Classes to put on the table this directive produces. The ``rst-class``
#: directive resolves through a document-level transform, which a parse
#: into a detached node never reaches, so the class goes on directly.
table_classes = ()

def lines(self):
raise NotImplementedError

def run(self):
parent = nodes.Element()
self.state.nested_parse(StringList(self.lines(), source=""), self.content_offset, parent)
for table in parent.findall(nodes.table):
table["classes"].extend(self.table_classes)
return parent.children


class RubricWeights(_RubricDirective):
def lines(self):
rows = [["Objective", "Scope", "Weight"]]
rows += [[o["title"], o["scope"], o["weight"]] for o in rubric_data.OBJECTIVES]
return _list_table(rows, [60, 20, 20])


class RubricLevels(_RubricDirective):
def lines(self):
rows = [["Level", "Meaning"]]
rows += [[level, rubric_data.LEVEL_MEANINGS[level]] for level in rubric_data.LEVELS]
return _list_table(rows, [30, 70])


class RubricAnchors(_RubricDirective):
def lines(self):
rows = [["Rubric placement", "Mark"]] + [list(anchor) for anchor in rubric_data.ANCHORS]
return _list_table(rows, [70, 30])


class RubricObjective(_RubricDirective):
required_arguments = 1

def lines(self):
return _objective_lines(_objective(self.arguments[0]))


class RubricObjectives(_RubricDirective):
required_arguments = 1

def lines(self):
scope = self.arguments[0]
lines = []
for objective in rubric_data.OBJECTIVES:
if objective["scope"].lower() == scope.lower():
lines += _objective_lines(objective)
return lines


def _objective_lines(objective):
lines = ["**{}.** {}".format(objective["title"], objective["lead"]), ""]
rows = [["Level", "Descriptor"]]
rows += [[level, objective["descriptors"][level]] for level in rubric_data.LEVELS]
return lines + _list_table(rows, [22, 78])


class RubricChart(_RubricDirective):
def lines(self):
header = ["Objective"] + rubric_data.LEVELS
rows = [header]
for objective in rubric_data.OBJECTIVES:
label = "**{}** ({}, {})".format(objective["title"], objective["scope"], objective["weight"])
rows.append([label] + [objective["descriptors"][level] for level in rubric_data.LEVELS])
return _list_table(rows, [16, 21, 21, 21, 21])


class RubricCoverage(_RubricDirective):
def lines(self):
return _bullets(rubric_data.COVERAGE_AREAS)


class RubricRequiredQuestions(_RubricDirective):
#: ``:numbered:`` numbers the questions, so that a page can refer to one by
#: its number. The tracking sheet heads its tick boxes with them.
option_spec = {"numbered": directives.flag}

def lines(self):
if "numbered" in self.options:
return _numbered(rubric_data.REQUIRED_QUESTIONS)
return _bullets(rubric_data.REQUIRED_QUESTIONS)


class RubricObjectiveTitles(_RubricDirective):
"""Every objective by title and scope, without its descriptors.

What an evaluator has to have evidence for by the time the session ends,
at the size that fits on a sheet they hold during it.
"""

def lines(self):
rows = [["Judge each member on", "Scope"]]
rows += [[objective["title"], objective["scope"]] for objective in rubric_data.OBJECTIVES]
return _list_table(rows, [80, 20])


class RubricSessionExpectations(_RubricDirective):
def lines(self):
rows = [["The evaluating team", "What that looks like"]]
rows += [list(expectation) for expectation in rubric_data.SESSION_EXPECTATIONS]
return _list_table(rows, [30, 70])


class RubricSessionAnchors(_RubricDirective):
def lines(self):
rows = [["Mark", "The session"]]
rows += [[mark, description] for description, mark in rubric_data.SESSION_ANCHORS]
return _list_table(rows, [10, 90])


class RubricFollowUps(_RubricDirective):
def lines(self):
rows = [["To", "Ask"]] + [list(template) for template in rubric_data.FOLLOW_UP_TEMPLATES]
return _list_table(rows, [18, 82])


class RubricEtiquette(_RubricDirective):
def lines(self):
return _bullets(rubric_data.ETIQUETTE)


class RubricClock(_RubricDirective):
"""The two sessions of a lab period against where each should be by then."""

def lines(self):
rows = [["1st", "2nd", "Where you should be"]]
rows += [list(mark) for mark in rubric_data.SESSION_CLOCK]
return _list_table(rows, [9, 9, 82])


#: Written into a cell that the evaluator fills in by hand. A list-table cell
#: cannot be empty, so a blank one carries a space the page does not show.
BLANK = " "


class RubricTracking(_RubricDirective):
"""One row per evaluated member, left blank to be filled in during the session."""

#: A tick box per required question, headed by its number on the list the
#: page prints below the grid.
REQUIRED = [str(number) for number in range(1, len(rubric_data.REQUIRED_QUESTIONS) + 1)]

def lines(self):
headings = ["Member", "Areas they claim"] + self.REQUIRED
headings += ["Questions asked", "Points", "Notes"]
rows = [headings] + [[BLANK] * len(headings) for _ in range(4)]
return _list_table(rows, [12, 16, 4, 4, 4, 8, 6, 30])


class RubricCoverageTracking(_RubricDirective):
"""The coverage areas with a blank beside each.

One line of writing per area, against the several lines a member's row
gets, so the table is named for the stylesheet to size it on its own.
"""

table_classes = ("coverage-tracking",)

def lines(self):
rows = [["Area", "Who answered"]]
rows += [[area, BLANK] for area in rubric_data.COVERAGE_AREAS]
return _list_table(rows, [55, 45])


#: Pages laid out as reference sheets by ``_static/css/rubric_sheet.css``.
SHEET_PAGES = ("rubric_chart", "evaluator_sheet", "tracking_sheet")

#: Sheets that carry a stylesheet of their own past the shared layout, keyed by
#: page name. The tracking sheet is written on by hand and needs room to write
#: in; the cheat sheet is read from and has to fit its four tables on one side.
SHEET_CSS = {
"tracking_sheet": "css/tracking_sheet.css",
"evaluator_sheet": "css/evaluator_sheet.css",
}


def _attach_sheet_css(app, pagename, templatename, context, doctree):
if pagename in SHEET_PAGES:
app.add_css_file("css/rubric_sheet.css")
if pagename in SHEET_CSS:
app.add_css_file(SHEET_CSS[pagename])


def setup(app):
app.add_directive("rubric-weights", RubricWeights)
app.add_directive("rubric-levels", RubricLevels)
app.add_directive("rubric-anchors", RubricAnchors)
app.add_directive("rubric-objective", RubricObjective)
app.add_directive("rubric-objectives", RubricObjectives)
app.add_directive("rubric-chart", RubricChart)
app.add_directive("rubric-coverage", RubricCoverage)
app.add_directive("rubric-required-questions", RubricRequiredQuestions)
app.add_directive("rubric-objective-titles", RubricObjectiveTitles)
app.add_directive("rubric-session-expectations", RubricSessionExpectations)
app.add_directive("rubric-session-anchors", RubricSessionAnchors)
app.add_directive("rubric-follow-ups", RubricFollowUps)
app.add_directive("rubric-etiquette", RubricEtiquette)
app.add_directive("rubric-clock", RubricClock)
app.add_directive("rubric-tracking", RubricTracking)
app.add_directive("rubric-coverage-tracking", RubricCoverageTracking)
app.connect("html-page-context", _attach_sheet_css)
return {"parallel_read_safe": True, "parallel_write_safe": True}
Comment on lines +253 to +271

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have other places we can use these?

82 changes: 82 additions & 0 deletions info/_static/css/evaluator_sheet.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/* Fits the evaluator cheat sheet onto a single landscape sheet, on top of the
* reference-sheet layout in rubric_sheet.css. Loaded only on that page, by the
* html-page-context handler in _ext/rubric.py.
*
* The page is five short tables and a list. They run in two columns, which is
* what buys the single sheet: a landscape page is wide enough that one column
* leaves most of each line empty.
*
* The layout applies on screen as well as in print, so an evaluator working
* from a laptop sees the sheet their neighbour has on paper. */

.rst-content section section,
.rst-content .section .section {
break-inside: avoid;
}

/* The theme nests the page body a few levels below .rst-content, so the
* columns are set on the one section that holds the whole page. */
.rst-content [itemprop="articleBody"] > section,
.rst-content [itemprop="articleBody"] > .section {
column-count: 2;
column-gap: 1cm;
}

.rst-content h1 {
column-span: all;
font-size: 14pt;
margin: 0 0 6pt;
}

.rst-content h2 {
font-size: 11pt;
margin: 0 0 3pt;
}

.rst-content p,
.rst-content li {
font-size: 9.5pt;
line-height: 1.25;
margin: 0 0 4pt;
}

.rst-content ul {
margin: 0 0 4pt;
padding-left: 1.1em;
}

.rst-content table.docutils td,
.rst-content table.docutils th,
.rst-content table.docutils p {
font-size: 9pt !important;
line-height: 1.2 !important;
}

.rst-content table.docutils {
margin-bottom: 5pt;
}

.rst-content table.docutils td,
.rst-content table.docutils th {
padding: 3px 5px;
}

@media print {
@page {
size: landscape;
margin: 0.8cm;
}

/* On paper the column height is the page, so the first column is filled
* before the second is started. On screen the page has no such height and
* the columns are balanced against each other instead. */
.rst-content [itemprop="articleBody"] > section,
.rst-content [itemprop="articleBody"] > .section {
column-fill: auto;
}

/* Printed on a page whose footer the theme has already removed. */
.rst-content .admonition {
display: none !important;
}
}
Loading
Loading