Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions console/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,7 @@ student PII and must never be committed.
A private teacher repository can provide gradebook/workspaces.json when a submission account does not identify the student's actual workspace name. The optional file has schemaVersion 1, the section code, and a workspaces array of repo and studentNumber bindings. These bindings must be corroborated by enrollment and repository ownership before publication. They are private identity data and must never be included in the public Console site.

The Console treats each binding as a locator. It still reads the workspace's current student.json and grade receipt before reporting delivery. Conflicting CSV identities, invalid sections and malformed repository names stop loading explicitly. Duplicate workspace identities remain held. Missing locator files preserve the existing lookup path. A locator does not approve an AI score, change a grade or establish Canvas delivery.

### Renamed workspace repositories

An old GitHub repository URL can redirect to a renamed workspace. When several names share a student number, the Console accepts them as aliases only after current repository metadata confirms the same repository ID and canonical class workspace, and readable receipt text matches across every name. Separate repositories, unreadable metadata and missing or differing receipts remain held for review.
19 changes: 18 additions & 1 deletion console/site/lib/gradebook.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,24 @@ export async function loadSection(sc) {
const key = normNum(identity.studentNumber);
identityGroups.set(key, [...(identityGroups.get(key) || []), repo]);
}
const canonicalWorkspaces = new Map();
for (const repos of identityGroups.values()) if (repos.length > 1) {
// Renamed repository URLs redirect. Confirm physical identity before treating
// multiple names as separate workspaces; failed reads remain a hold.
let physical = [];
try { physical = await Promise.all(repos.map(repo => ghJSON(`/repos/${sc.org}/${repo}`))); } catch {}
const prefix = family ? `student-${family[1]}-${family[2]}-`.toLowerCase() : "";
const first = physical[0];
const canonical = first?.name?.toLowerCase();
const sameRepository = physical.length === repos.length && Number.isSafeInteger(first?.id) && first.id > 0 &&
canonical?.startsWith(prefix) && prefix && physical.every(info => info?.id === first.id &&
info?.full_name?.toLowerCase() === `${sc.org}/${canonical}`.toLowerCase()) &&
repos.every(repo => typeof receipts.get(repo) === "string" && receipts.get(repo) === receipts.get(repos[0]));
if (sameRepository) {
receipts.set(canonical, receipts.get(repos[0]));
for (const repo of repos) canonicalWorkspaces.set(repo, canonical);
continue;
}
reviewWarnings.push("Multiple workspaces share one student number; their finals rows remain held.");
for (const repo of repos) identities.set(repo, null);
}
Expand All @@ -138,7 +155,7 @@ export async function loadSection(sc) {
});
const csvRows = csv.slice(1).map(parse);
const rowKeys = new Set(csvRows.map(f => f[gi("assignment")] + "/" + f[gi("repo")]));
const workspaceByNumber = new Map([...identities].filter(([, identity]) => identity).map(([repo, identity]) => [normNum(identity.studentNumber), repo]));
const workspaceByNumber = new Map([...identities].filter(([, identity]) => identity).map(([repo, identity]) => [normNum(identity.studentNumber), canonicalWorkspaces.get(repo) || repo]));
for (const x of pending.values()) {
if (rowKeys.has(x.id + "/" + x.repo)) continue;
const identity = identities.get(x.repo.toLowerCase());
Expand Down
Loading