Skip to content

Commit 41bcee0

Browse files
committed
Fix course discovery missing TA/cross-listed/DE courses
- Remove enrollment_state=active filter from both the Electron IPC handler and downloader.py: this filter silently drops courses where the user's enrollment role is TA, auditor, observer, or where the course is cross-listed (e.g. EE4802/IE4213) or a Design Experience track (e.g. CS2040DE), since those enrollments often carry a state other than "active" in the Canvas API. - Add Link-header pagination to fetchCoursesFromCanvas() so users with more than 100 courses don't have later ones silently omitted. - Refactor httpGet into httpGetWithHeaders (returns body + headers) with httpGet as a thin wrapper; add parseLinkNext() helper.
1 parent e655c77 commit 41bcee0

3 files changed

Lines changed: 48 additions & 16 deletions

File tree

downloader.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,14 @@ def _secretly_wait_dir() -> None:
137137
# ── Canvas course listing ──────────────────────────────────────────────────────
138138

139139
def get_academic_courses(canvas: Canvas) -> list:
140-
"""Return all active academic courses."""
140+
"""Return all academic courses the user has any enrollment in.
141+
142+
Intentionally does NOT filter by enrollment_state="active" so that
143+
courses with TA, auditor, design-experience, or cross-listed enrollments
144+
are included — these often have an enrollment state other than "active".
145+
"""
141146
courses = []
142-
for c in canvas.get_courses(enrollment_state="active"):
147+
for c in canvas.get_courses():
143148
if _is_academic(c):
144149
courses.append(c)
145150
return courses

electron/main.js

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,10 @@ function saveCredentials(data) {
140140
}
141141

142142
// ── Canvas API ────────────────────────────────────────────────────────────────
143-
function httpGet(url, headers) {
143+
function httpGetWithHeaders(url, headers) {
144144
return new Promise((resolve, reject) => {
145-
const mod = url.startsWith('https') ? https : http;
146-
const req = mod.get(url, { headers }, (res) => {
145+
const mod = url.startsWith('https') ? https : http;
146+
const req = mod.get(url, { headers }, (res) => {
147147
let body = '';
148148
res.on('data', d => body += d);
149149
res.on('end', () => {
@@ -153,15 +153,32 @@ function httpGet(url, headers) {
153153
if (res.statusCode >= 400) {
154154
return reject(new Error(`HTTP ${res.statusCode}: ${url}`));
155155
}
156-
try { resolve(JSON.parse(body)); }
157-
catch { reject(new Error('Invalid JSON response from server')); }
156+
try {
157+
resolve({ data: JSON.parse(body), linkHeader: res.headers['link'] || '' });
158+
} catch {
159+
reject(new Error('Invalid JSON response from server'));
160+
}
158161
});
159162
});
160163
req.on('error', reject);
161164
req.setTimeout(12000, () => { req.destroy(); reject(new Error('Request timed out')); });
162165
});
163166
}
164167

168+
function httpGet(url, headers) {
169+
return httpGetWithHeaders(url, headers).then(r => r.data);
170+
}
171+
172+
// Parse the Canvas `Link` response header to find the next-page URL.
173+
function parseLinkNext(linkHeader) {
174+
if (!linkHeader) return null;
175+
for (const part of linkHeader.split(',')) {
176+
const m = part.match(/<([^>]+)>.*rel="next"/);
177+
if (m) return m[1];
178+
}
179+
return null;
180+
}
181+
165182
const SKIP_KW = [
166183
'training', 'pdp', 'rmcpdp', 'osa', 'soct', 'travel',
167184
'essentials', 'respect', 'consent', 'osh',
@@ -172,15 +189,25 @@ async function fetchCoursesFromCanvas() {
172189
const token = fs.existsSync(tokenFile) ? fs.readFileSync(tokenFile, 'utf8').trim() : '';
173190
if (!token) return { error: 'Canvas token not saved — enter it in Settings → API Keys.' };
174191
const cfg = loadConfig();
175-
let url = (cfg.CANVAS_URL || '').trim().replace(/\/$/, '');
176-
if (!url) return { error: 'Canvas URL not configured — enter it in Settings → Connection.' };
177-
if (!url.startsWith('http')) url = 'https://' + url;
192+
let baseUrl = (cfg.CANVAS_URL || '').trim().replace(/\/$/, '');
193+
if (!baseUrl) return { error: 'Canvas URL not configured — enter it in Settings → Connection.' };
194+
if (!baseUrl.startsWith('http')) baseUrl = 'https://' + baseUrl;
178195
try {
179-
const data = await httpGet(
180-
`${url}/api/v1/courses?enrollment_state=active&per_page=100`,
181-
{ 'Authorization': `Bearer ${token}` },
182-
);
183-
const courses = data
196+
// Fetch all courses the user has any enrollment in (student, TA, auditor, cross-listed…).
197+
// Do NOT filter by enrollment_state=active — that drops TAs, design-experience tracks,
198+
// and cross-listed courses whose enrollment state differs from "active".
199+
// Follow Link headers for pagination in case the user has >100 courses.
200+
const allData = [];
201+
const authHeaders = { 'Authorization': `Bearer ${token}` };
202+
let nextUrl = `${baseUrl}/api/v1/courses?per_page=100`;
203+
let pages = 0;
204+
while (nextUrl && pages < 20) { // safety cap: 20 pages = 2000 courses max
205+
const { data, linkHeader } = await httpGetWithHeaders(nextUrl, authHeaders);
206+
allData.push(...(Array.isArray(data) ? data : []));
207+
nextUrl = parseLinkNext(linkHeader);
208+
pages++;
209+
}
210+
const courses = allData
184211
.filter(c => c.name && !SKIP_KW.some(k => c.name.toLowerCase().includes(k)))
185212
.map(c => ({ id: c.id, name: c.name || c.course_code || String(c.id) }));
186213
return { courses };

electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "auto-note",
3-
"version": "0.9.10",
3+
"version": "0.9.11",
44
"description": "AutoNote — lecture notes generator from Canvas recordings",
55
"homepage": "https://github.com/nodeeeeee/Auto-Note",
66
"author": {

0 commit comments

Comments
 (0)