blackboard-upc 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/package.json +1 -1
- package/src/api/courses.ts +2 -13
- package/src/commands/assignments.ts +13 -12
- package/src/commands/courses.ts +57 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `blackboard-upc` will be documented here.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.0.1] — 2026-03-30
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `courses members <courseId>` — lista compañeros e instructor de un curso (con `--role` y `--json`)
|
|
11
|
+
|
|
12
|
+
### Improved
|
|
13
|
+
- `courses list` — usa `expand=course` en una sola llamada en vez de 1+N (antes: 1 llamada por curso)
|
|
14
|
+
- `assignments list` — usa bulk grades (`/gradebook/users/{id}`) en paralelo con columns, eliminando N llamadas individuales
|
|
15
|
+
- `courses members` — usa `expand=user` para traer nombres en una sola llamada
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
7
19
|
## [1.0.0] — 2026-03-30
|
|
8
20
|
|
|
9
21
|
### Added
|
|
@@ -53,7 +65,7 @@ All notable changes to `blackboard-upc` will be documented here.
|
|
|
53
65
|
|
|
54
66
|
## Roadmap
|
|
55
67
|
|
|
56
|
-
- [
|
|
68
|
+
- [x] `npx` install sin clonar repo (publicación en npm)
|
|
57
69
|
- [ ] Refresh automático de sesión antes de expirar
|
|
58
70
|
- [ ] Notificaciones de entregas próximas (`assignments due`)
|
|
59
71
|
- [ ] Descarga de videos de grabaciones de clase
|
package/package.json
CHANGED
package/src/api/courses.ts
CHANGED
|
@@ -14,20 +14,9 @@ export async function getMyCourses(
|
|
|
14
14
|
const params: Record<string, any> = { limit: opts.limit ?? 50 };
|
|
15
15
|
if (opts.offset) params.offset = opts.offset;
|
|
16
16
|
|
|
17
|
+
params.expand = 'course';
|
|
17
18
|
const r = await client.get(`/learn/api/public/v1/users/${userId}/courses`, { params });
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// Resolve course names in parallel
|
|
21
|
-
const courseDetails = await Promise.allSettled(
|
|
22
|
-
memberships.map((m) => getCourse(client, m.courseId))
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
const results = memberships.map((m, i) => ({
|
|
26
|
-
...m,
|
|
27
|
-
course: courseDetails[i].status === 'fulfilled' ? courseDetails[i].value : undefined,
|
|
28
|
-
}));
|
|
29
|
-
|
|
30
|
-
return { results, paging: r.data.paging };
|
|
19
|
+
return { results: r.data.results, paging: r.data.paging };
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
export async function getCourse(client: AxiosInstance, courseId: string): Promise<Course> {
|
|
@@ -61,7 +61,16 @@ export function assignmentsCommand(program: Command) {
|
|
|
61
61
|
let userId = session.userId;
|
|
62
62
|
if (!userId) { const me = await getMe(client); userId = me.id; }
|
|
63
63
|
|
|
64
|
-
const columns = await
|
|
64
|
+
const [columns, gradesRes] = await Promise.all([
|
|
65
|
+
listAssignments(client, courseId),
|
|
66
|
+
client
|
|
67
|
+
.get(`/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}`, {
|
|
68
|
+
params: { limit: 200 },
|
|
69
|
+
})
|
|
70
|
+
.then((r) => r.data.results as any[])
|
|
71
|
+
.catch(() => [] as any[]),
|
|
72
|
+
]);
|
|
73
|
+
|
|
65
74
|
spinner.succeed(`${columns.length} assignments found`);
|
|
66
75
|
|
|
67
76
|
if (opts.json) { console.log(JSON.stringify(columns, null, 2)); return; }
|
|
@@ -71,19 +80,11 @@ export function assignmentsCommand(program: Command) {
|
|
|
71
80
|
return;
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
|
|
75
|
-
const gradeResults = await Promise.allSettled(
|
|
76
|
-
columns.map((c) =>
|
|
77
|
-
client
|
|
78
|
-
.get(`/learn/api/public/v1/courses/${courseId}/gradebook/users/${userId}/columns/${c.id}`)
|
|
79
|
-
.then((r) => r.data)
|
|
80
|
-
.catch(() => null)
|
|
81
|
-
)
|
|
82
|
-
);
|
|
83
|
+
const gradeMap = new Map(gradesRes.map((g: any) => [g.columnId, g]));
|
|
83
84
|
|
|
84
85
|
console.log('');
|
|
85
|
-
columns.forEach((col
|
|
86
|
-
const grade =
|
|
86
|
+
columns.forEach((col) => {
|
|
87
|
+
const grade = gradeMap.get(col.id) ?? null;
|
|
87
88
|
const possible = col.score?.possible ?? '?';
|
|
88
89
|
const due = col.grading?.due;
|
|
89
90
|
const attemptsAllowed = col.grading?.attemptsAllowed === 0
|
package/src/commands/courses.ts
CHANGED
|
@@ -231,6 +231,63 @@ export function coursesCommand(program: Command) {
|
|
|
231
231
|
}
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
// Members
|
|
235
|
+
courses
|
|
236
|
+
.command('members <courseId>')
|
|
237
|
+
.description('List students and instructors in a course')
|
|
238
|
+
.option('--json', 'Output raw JSON')
|
|
239
|
+
.option('--role <role>', 'Filter by role: Student, Instructor')
|
|
240
|
+
.action(async (courseId, opts) => {
|
|
241
|
+
const session = requireSession();
|
|
242
|
+
const client = createClient(session);
|
|
243
|
+
const spinner = ora({ text: 'Fetching course members...', stream: process.stderr }).start();
|
|
244
|
+
|
|
245
|
+
try {
|
|
246
|
+
const res = await client.get(
|
|
247
|
+
`/learn/api/public/v1/courses/${courseId}/users?expand=user&limit=200`
|
|
248
|
+
);
|
|
249
|
+
let members: any[] = res.data.results ?? [];
|
|
250
|
+
|
|
251
|
+
if (opts.role) {
|
|
252
|
+
const roleFilter = opts.role.toLowerCase();
|
|
253
|
+
members = members.filter((m: any) => m.courseRoleId?.toLowerCase() === roleFilter);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
spinner.succeed(`${members.length} members`);
|
|
257
|
+
|
|
258
|
+
if (opts.json) {
|
|
259
|
+
console.log(JSON.stringify({ results: members }, null, 2));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const students = members.filter((m: any) => m.courseRoleId === 'Student');
|
|
264
|
+
const instructors = members.filter((m: any) => m.courseRoleId !== 'Student');
|
|
265
|
+
|
|
266
|
+
console.log('');
|
|
267
|
+
if (instructors.length > 0) {
|
|
268
|
+
console.log(chalk.bold(' Instructores'));
|
|
269
|
+
for (const m of instructors) {
|
|
270
|
+
const name = m.user ? `${m.user.name.given} ${m.user.name.family}` : m.userId;
|
|
271
|
+
console.log(` ${chalk.yellow(name)}`);
|
|
272
|
+
}
|
|
273
|
+
console.log('');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (students.length > 0) {
|
|
277
|
+
console.log(chalk.bold(` Estudiantes (${students.length})`));
|
|
278
|
+
students.forEach((m: any, i: number) => {
|
|
279
|
+
const name = m.user ? `${m.user.name.given} ${m.user.name.family}` : m.userId;
|
|
280
|
+
const num = chalk.gray(`${String(i + 1).padStart(2, ' ')}.`);
|
|
281
|
+
console.log(` ${num} ${name}`);
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
console.log('');
|
|
285
|
+
} catch (err: any) {
|
|
286
|
+
spinner.fail(err.message);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
234
291
|
// Grades
|
|
235
292
|
courses
|
|
236
293
|
.command('grades <courseId>')
|